Răsfoiți Sursa

Add trojan & Fixes

世界 4 ani în urmă
părinte
comite
53bad876d1

+ 197 - 0
app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/2.json

@@ -0,0 +1,197 @@
+{
+  "formatVersion": 1,
+  "database": {
+    "version": 2,
+    "identityHash": "d47ba9e5ae4ffec84bea4a59664c779d",
+    "entities": [
+      {
+        "tableName": "proxy_groups",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userOrder` INTEGER NOT NULL, `isDefault` INTEGER NOT NULL, `name` TEXT, `isSubscription` INTEGER NOT NULL, `subscriptionLink` TEXT NOT NULL, `lastUpdate` INTEGER NOT NULL)",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "userOrder",
+            "columnName": "userOrder",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "isDefault",
+            "columnName": "isDefault",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "name",
+            "columnName": "name",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "isSubscription",
+            "columnName": "isSubscription",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "subscriptionLink",
+            "columnName": "subscriptionLink",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "lastUpdate",
+            "columnName": "lastUpdate",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "proxy_entities",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL, `type` INTEGER NOT NULL, `userOrder` INTEGER NOT NULL, `tx` INTEGER NOT NULL, `rx` INTEGER NOT NULL, `socksBean` BLOB, `ssBean` BLOB, `ssrBean` BLOB, `vmessBean` BLOB, `vlessBean` BLOB, `trojanBean` BLOB)",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "groupId",
+            "columnName": "groupId",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "type",
+            "columnName": "type",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "userOrder",
+            "columnName": "userOrder",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "tx",
+            "columnName": "tx",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "rx",
+            "columnName": "rx",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "socksBean",
+            "columnName": "socksBean",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "ssBean",
+            "columnName": "ssBean",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "ssrBean",
+            "columnName": "ssrBean",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "vmessBean",
+            "columnName": "vmessBean",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "vlessBean",
+            "columnName": "vlessBean",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "trojanBean",
+            "columnName": "trojanBean",
+            "affinity": "BLOB",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [
+          {
+            "name": "groupId",
+            "unique": false,
+            "columnNames": [
+              "groupId"
+            ],
+            "createSql": "CREATE INDEX IF NOT EXISTS `groupId` ON `${TABLE_NAME}` (`groupId`)"
+          }
+        ],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "KeyValuePair",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `valueType` INTEGER NOT NULL, `value` BLOB NOT NULL, PRIMARY KEY(`key`))",
+        "fields": [
+          {
+            "fieldPath": "key",
+            "columnName": "key",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "valueType",
+            "columnName": "valueType",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "value",
+            "columnName": "value",
+            "affinity": "BLOB",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "key"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [],
+        "foreignKeys": []
+      }
+    ],
+    "views": [],
+    "setupQueries": [
+      "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd47ba9e5ae4ffec84bea4a59664c779d')"
+    ]
+  }
+}

+ 1 - 0
app/src/main/AndroidManifest.xml

@@ -74,6 +74,7 @@
         <activity android:name="io.nekohasekai.sagernet.ui.profile.ShadowsocksRSettingsActivity" />
         <activity android:name="io.nekohasekai.sagernet.ui.profile.VMessSettingsActivity" />
         <activity android:name="io.nekohasekai.sagernet.ui.profile.VLESSSettingsActivity" />
+        <activity android:name="io.nekohasekai.sagernet.ui.profile.TrojanSettingsActivity" />
         <activity
             android:name="io.nekohasekai.sagernet.ui.AppManagerActivity"
             android:excludeFromRecents="true"

+ 1 - 1
app/src/main/java/io/nekohasekai/sagernet/bg/BaseService.kt

@@ -306,7 +306,7 @@ class BaseService {
                 data.closeReceiverRegistered = true
             }
 
-            data.notification = createNotification(profile.requireBean().name)
+            data.notification = createNotification(profile.displayName())
 
             data.changeState(State.Connecting)
             data.connectingJob = GlobalScope.launch(Dispatchers.Main) {

+ 2 - 2
app/src/main/java/io/nekohasekai/sagernet/bg/ProxyInstance.kt

@@ -36,7 +36,7 @@ import io.nekohasekai.sagernet.fmt.gson.gson
 import io.nekohasekai.sagernet.fmt.shadowsocks.ShadowsocksBean
 import io.nekohasekai.sagernet.fmt.shadowsocksr.ShadowsocksRBean
 import io.nekohasekai.sagernet.fmt.v2ray.AbstractV2RayBean
-import io.nekohasekai.sagernet.fmt.v2ray.V2rayConfig
+import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig
 import io.nekohasekai.sagernet.fmt.v2ray.buildV2rayConfig
 import io.nekohasekai.sagernet.ktx.Logs
 import io.nekohasekai.sagernet.utils.DirectBoot
@@ -51,7 +51,7 @@ import java.util.*
 class ProxyInstance(val profile: ProxyEntity) {
 
     lateinit var v2rayPoint: V2RayPoint
-    lateinit var config: V2rayConfig
+    lateinit var config: V2RayConfig
     lateinit var base: BaseService.Interface
     lateinit var wsForwarder: WebView
 

+ 14 - 0
app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt

@@ -36,6 +36,8 @@ import io.nekohasekai.sagernet.fmt.shadowsocksr.ShadowsocksRBean
 import io.nekohasekai.sagernet.fmt.shadowsocksr.toUri
 import io.nekohasekai.sagernet.fmt.socks.SOCKSBean
 import io.nekohasekai.sagernet.fmt.socks.toUri
+import io.nekohasekai.sagernet.fmt.trojan.TrojanBean
+import io.nekohasekai.sagernet.fmt.trojan.toUri
 import io.nekohasekai.sagernet.fmt.v2ray.VLESSBean
 import io.nekohasekai.sagernet.fmt.v2ray.VMessBean
 import io.nekohasekai.sagernet.fmt.v2ray.toV2rayN
@@ -58,6 +60,7 @@ data class ProxyEntity(
     var ssrBean: ShadowsocksRBean? = null,
     var vmessBean: VMessBean? = null,
     var vlessBean: VLESSBean? = null,
+    var trojanBean: TrojanBean? = null,
 ) : Parcelable {
 
     @Ignore
@@ -84,6 +87,8 @@ data class ProxyEntity(
             1 -> ssBean = KryoConverters.shadowsocksDeserialize(byteArray)
             2 -> ssrBean = KryoConverters.shadowsocksRDeserialize(byteArray)
             3 -> vmessBean = KryoConverters.vmessDeserialize(byteArray)
+            4 -> vlessBean = KryoConverters.vlessDeserialize(byteArray)
+            5 -> trojanBean = KryoConverters.trojanDeserialize(byteArray)
         }
     }
 
@@ -108,6 +113,7 @@ data class ProxyEntity(
             2 -> "ShadowsocksR"
             3 -> "VMess"
             4 -> "VLESS"
+            5 -> "Trojan"
             else -> "Undefined type $type"
         }
     }
@@ -125,6 +131,7 @@ data class ProxyEntity(
             2 -> ssrBean ?: error("Null ssr node")
             3 -> vmessBean ?: error("Null vmess node")
             4 -> vlessBean ?: error("Null vless node")
+            5 -> trojanBean ?: error("Null trojan node")
             else -> error("Undefined type $type")
         }
     }
@@ -136,6 +143,7 @@ data class ProxyEntity(
             2 -> requireSSR().toUri()
             3 -> requireVMess().toV2rayN()
             4 -> "目前 VLESS 不支持分享。(https://www.v2fly.org/config/protocols/vless.html)"
+            5 -> requireTrojan().toUri()
             else -> error("Undefined type $type")
         }
     }
@@ -174,6 +182,10 @@ data class ProxyEntity(
                 type = 4
                 vlessBean = bean
             }
+            is TrojanBean -> {
+                type = 5
+                trojanBean = bean
+            }
             else -> error("Undefined type $type")
         }
     }
@@ -183,6 +195,7 @@ data class ProxyEntity(
     fun requireSSR() = requireBean() as ShadowsocksRBean
     fun requireVMess() = requireBean() as VMessBean
     fun requireVLESS() = requireBean() as VMessBean
+    fun requireTrojan() = requireBean() as TrojanBean
 
     fun settingIntent(ctx: Context): Intent {
         return Intent(ctx, when (type) {
@@ -191,6 +204,7 @@ data class ProxyEntity(
             2 -> ShadowsocksRSettingsActivity::class.java
             3 -> VMessSettingsActivity::class.java
             4 -> VLESSSettingsActivity::class.java
+            5 -> TrojanSettingsActivity::class.java
             else -> throw IllegalArgumentException()
         }).apply {
             putExtra(ProfileSettingsActivity.EXTRA_PROFILE_ID, id)

+ 1 - 1
app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt

@@ -34,7 +34,7 @@ import io.nekohasekai.sagernet.fmt.gson.GsonConverters
 import kotlinx.coroutines.GlobalScope
 import kotlinx.coroutines.launch
 
-@Database(entities = [ProxyGroup::class, ProxyEntity::class, KeyValuePair::class], version = 1)
+@Database(entities = [ProxyGroup::class, ProxyEntity::class, KeyValuePair::class], version = 2)
 @TypeConverters(value = [KryoConverters::class, GsonConverters::class])
 @GenerateRoomMigrations
 abstract class SagerDatabase : RoomDatabase() {

+ 7 - 0
app/src/main/java/io/nekohasekai/sagernet/fmt/KryoConverters.java

@@ -34,6 +34,7 @@ import cn.hutool.core.util.ArrayUtil;
 import io.nekohasekai.sagernet.fmt.shadowsocks.ShadowsocksBean;
 import io.nekohasekai.sagernet.fmt.shadowsocksr.ShadowsocksRBean;
 import io.nekohasekai.sagernet.fmt.socks.SOCKSBean;
+import io.nekohasekai.sagernet.fmt.trojan.TrojanBean;
 import io.nekohasekai.sagernet.fmt.v2ray.VLESSBean;
 import io.nekohasekai.sagernet.fmt.v2ray.VMessBean;
 import io.nekohasekai.sagernet.ktx.KryosKt;
@@ -91,4 +92,10 @@ public class KryoConverters {
         return deserialize(new VLESSBean(), bytes);
     }
 
+    @TypeConverter
+    public static TrojanBean trojanDeserialize(byte[] bytes) {
+        if (ArrayUtil.isEmpty(bytes)) return null;
+        return deserialize(new TrojanBean(), bytes);
+    }
+
 }

+ 66 - 0
app/src/main/java/io/nekohasekai/sagernet/fmt/trojan/TrojanBean.java

@@ -0,0 +1,66 @@
+/******************************************************************************
+ *                                                                            *
+ * Copyright (C) 2021 by nekohasekai <[email protected]>                    *
+ * Copyright (C) 2021 by Max Lv <[email protected]>                          *
+ * Copyright (C) 2021 by Mygod Studio <[email protected]>  *
+ *                                                                            *
+ * This program is free software: you can redistribute it and/or modify       *
+ * it under the terms of the GNU General Public License as published by       *
+ * the Free Software Foundation, either version 3 of the License, or          *
+ *  (at your option) any later version.                                       *
+ *                                                                            *
+ * This program is distributed in the hope that it will be useful,            *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of             *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the              *
+ * GNU General Public License for more details.                               *
+ *                                                                            *
+ * You should have received a copy of the GNU General Public License          *
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.       *
+ *                                                                            *
+ ******************************************************************************/
+
+package io.nekohasekai.sagernet.fmt.trojan;
+
+import com.esotericsoftware.kryo.io.ByteBufferInput;
+import com.esotericsoftware.kryo.io.ByteBufferOutput;
+
+import org.jetbrains.annotations.NotNull;
+
+import io.nekohasekai.sagernet.fmt.AbstractBean;
+import io.nekohasekai.sagernet.fmt.KryoConverters;
+
+public class TrojanBean extends AbstractBean {
+
+    public static TrojanBean DEFAULT_BEAN = new TrojanBean() {{
+        name = "";
+        serverAddress = "127.0.0.1";
+        serverPort = 1080;
+        password = "";
+        sni = "";
+    }};
+
+    public String password;
+    public String sni;
+
+    @Override
+    public void serialize(ByteBufferOutput output) {
+        output.writeInt(0);
+        super.serialize(output);
+        output.writeString(password);
+        output.writeString(sni);
+    }
+
+    @Override
+    public void deserialize(ByteBufferInput input) {
+        int version = input.readInt();
+        super.deserialize(input);
+        password = input.readString();
+        sni = input.readString();
+    }
+
+    @NotNull
+    @Override
+    public AbstractBean clone() {
+        return KryoConverters.deserialize(new TrojanBean(), KryoConverters.serialize(this));
+    }
+}

+ 55 - 0
app/src/main/java/io/nekohasekai/sagernet/fmt/trojan/TrojanFmt.kt

@@ -0,0 +1,55 @@
+/******************************************************************************
+ *                                                                            *
+ * Copyright (C) 2021 by nekohasekai <[email protected]>                    *
+ * Copyright (C) 2021 by Max Lv <[email protected]>                          *
+ * Copyright (C) 2021 by Mygod Studio <[email protected]>  *
+ *                                                                            *
+ * This program is free software: you can redistribute it and/or modify       *
+ * it under the terms of the GNU General Public License as published by       *
+ * the Free Software Foundation, either version 3 of the License, or          *
+ *  (at your option) any later version.                                       *
+ *                                                                            *
+ * This program is distributed in the hope that it will be useful,            *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of             *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the              *
+ * GNU General Public License for more details.                               *
+ *                                                                            *
+ * You should have received a copy of the GNU General Public License          *
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.       *
+ *                                                                            *
+ ******************************************************************************/
+
+package io.nekohasekai.sagernet.fmt.trojan
+
+import io.nekohasekai.sagernet.ktx.urlSafe
+import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
+
+fun parseTrojan(server: String): TrojanBean {
+
+    val link = server.replace("trojan://", "https://").toHttpUrlOrNull()
+        ?: error("invalid trojan link $server")
+
+    return TrojanBean().apply {
+        serverAddress = link.host
+        serverPort = link.port
+        password = link.username
+
+        if (link.password.isNotBlank()) {
+            // https://github.com/trojan-gfw/igniter/issues/318
+            password += ":" + link.password
+        }
+
+        sni = link.queryParameter("sni") ?: ""
+        name = link.fragment ?: ""
+    }
+
+}
+
+fun TrojanBean.toUri(): String {
+
+    val params = if (sni.isNotBlank()) "?sni=" + sni.urlSafe() else ""
+    val remark = if (name.isNotBlank()) "#" + name.urlSafe() else ""
+
+    return "trojan://" + password.urlSafe() + "@" + serverAddress + ":" + serverPort + params + remark
+
+}

+ 4 - 1
app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/AbstractV2RayBean.java

@@ -46,7 +46,10 @@ public abstract class AbstractV2RayBean extends AbstractBean {
             name = "";
         }
         if (StrUtil.isBlank(serverAddress)) {
-            serverAddress = "";
+            serverAddress = "127.0.0.1";
+        }
+        if (serverPort == 0) {
+            serverPort = 1080;
         }
         if (StrUtil.isBlank(uuid)) {
             uuid = "";

+ 21 - 3
app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/V2rayConfig.java → app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/V2RayConfig.java

@@ -35,7 +35,7 @@ import io.nekohasekai.sagernet.fmt.gson.JsonLazyInterface;
 import io.nekohasekai.sagernet.fmt.gson.JsonOr;
 
 @SuppressWarnings({"SpellCheckingInspection", "unused", "RedundantSuppression"})
-public class V2rayConfig {
+public class V2RayConfig {
 
     public LogObject log;
 
@@ -403,10 +403,12 @@ public class V2rayConfig {
                         return SocksOutboundConfigurationObject.class;
                     case "vmess":
                         return VMessOutboundConfigurationObject.class;
-                    case "shadowsocks":
-                        return ShadowsocksOutboundConfigurationObject.class;
                     case "vless":
                         return VLESSOutboundConfigurationObject.class;
+                    case "shadowsocks":
+                        return ShadowsocksOutboundConfigurationObject.class;
+                    case "trojan":
+                        return TrojanOutboundConfigurationObject.class;
                     case "loopback":
                         return LoopbackOutboundConfigurationObject.class;
                 }
@@ -558,6 +560,22 @@ public class V2rayConfig {
 
     }
 
+    public static class TrojanOutboundConfigurationObject implements OutboundConfigurationObject {
+
+        public List<ServerObject> servers;
+
+        public static class ServerObject {
+
+            public String address;
+            public Integer port;
+            public String password;
+            public String email;
+            public Integer level;
+
+        }
+
+    }
+
     public static class LoopbackOutboundConfigurationObject implements OutboundConfigurationObject {
 
         public String inboundTag;

+ 27 - 3
app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/V2RayFmt.kt

@@ -28,7 +28,8 @@ import io.nekohasekai.sagernet.database.DataStore
 import io.nekohasekai.sagernet.database.ProxyEntity
 import io.nekohasekai.sagernet.fmt.shadowsocks.ShadowsocksBean
 import io.nekohasekai.sagernet.fmt.socks.SOCKSBean
-import io.nekohasekai.sagernet.fmt.v2ray.V2rayConfig.*
+import io.nekohasekai.sagernet.fmt.trojan.TrojanBean
+import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.*
 import io.nekohasekai.sagernet.ktx.urlSafe
 import okhttp3.HttpUrl
 import okhttp3.HttpUrl.Companion.toHttpUrl
@@ -40,7 +41,7 @@ const val TAG_DIRECT = "bypass"
 const val TAG_DNS_IN = "dns-in"
 const val TAG_DNS_OUT = "dns-out"
 
-fun buildV2rayConfig(proxy: ProxyEntity): V2rayConfig {
+fun buildV2rayConfig(proxy: ProxyEntity): V2RayConfig {
 
     val bind = if (DataStore.allowAccess) "0.0.0.0" else "127.0.0.1"
     val remoteDns = DataStore.remoteDNS.split(",")
@@ -50,7 +51,7 @@ fun buildV2rayConfig(proxy: ProxyEntity): V2rayConfig {
 
     val bean = proxy.requireBean()
 
-    return V2rayConfig().apply {
+    return V2RayConfig().apply {
 
         dns = DnsObject().apply {
             hosts = mapOf(
@@ -328,6 +329,29 @@ fun buildV2rayConfig(proxy: ProxyEntity): V2rayConfig {
                                 )
                             })
                     }
+                } else if (bean is TrojanBean) {
+                    protocol = "trojan"
+                    settings = LazyOutboundConfigurationObject(
+                        TrojanOutboundConfigurationObject().apply {
+                            servers = listOf(
+                                TrojanOutboundConfigurationObject.ServerObject().apply {
+                                    address = bean.serverAddress
+                                    port = bean.serverPort
+                                    password = bean.password
+                                    level = 8
+                                }
+                            )
+                        }
+                    )
+                    streamSettings = StreamSettingsObject().apply {
+                        network = "tcp"
+                        security = "tls"
+                        if (bean.sni.isNotBlank()) {
+                            tlsSettings = TLSObject().apply {
+                                serverName = bean.sni
+                            }
+                        }
+                    }
                 } else {
                     protocol = "socks"
                     settings = LazyOutboundConfigurationObject(

+ 3 - 0
app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt

@@ -196,6 +196,9 @@ class ConfigurationFragment : ToolbarFragment(R.layout.layout_group_list),
             R.id.action_new_vless -> {
                 startActivity(Intent(requireActivity(), VLESSSettingsActivity::class.java))
             }
+            R.id.action_new_trojan -> {
+                startActivity(Intent(requireActivity(), TrojanSettingsActivity::class.java))
+            }
         }
         return true
     }

+ 9 - 5
app/src/main/java/io/nekohasekai/sagernet/ui/profile/ProfileSettingsActivity.kt

@@ -223,11 +223,15 @@ abstract class ProfileSettingsActivity<T : AbstractBean> : AppCompatActivity(),
 
         override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
             R.id.action_delete -> {
-                DeleteConfirmationDialogFragment().apply {
-                    arg(ProfileIdArg(DataStore.editingId,
-                        DataStore.editingGroup))
-                    key()
-                }.show(parentFragmentManager, null)
+                if (DataStore.editingId == 0L) {
+                    requireActivity().finish()
+                } else {
+                    DeleteConfirmationDialogFragment().apply {
+                        arg(ProfileIdArg(DataStore.editingId,
+                            DataStore.editingGroup))
+                        key()
+                    }.show(parentFragmentManager, null)
+                }
                 true
             }
             R.id.action_apply -> {

+ 70 - 0
app/src/main/java/io/nekohasekai/sagernet/ui/profile/TrojanSettingsActivity.kt

@@ -0,0 +1,70 @@
+/******************************************************************************
+ *                                                                            *
+ * Copyright (C) 2021 by nekohasekai <[email protected]>                    *
+ * Copyright (C) 2021 by Max Lv <[email protected]>                          *
+ * Copyright (C) 2021 by Mygod Studio <[email protected]>  *
+ *                                                                            *
+ * This program is free software: you can redistribute it and/or modify       *
+ * it under the terms of the GNU General Public License as published by       *
+ * the Free Software Foundation, either version 3 of the License, or          *
+ *  (at your option) any later version.                                       *
+ *                                                                            *
+ * This program is distributed in the hope that it will be useful,            *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of             *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the              *
+ * GNU General Public License for more details.                               *
+ *                                                                            *
+ * You should have received a copy of the GNU General Public License          *
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.       *
+ *                                                                            *
+ ******************************************************************************/
+
+package io.nekohasekai.sagernet.ui.profile
+
+import android.os.Bundle
+import androidx.preference.EditTextPreference
+import androidx.preference.PreferenceFragmentCompat
+import io.nekohasekai.sagernet.Key
+import io.nekohasekai.sagernet.R
+import io.nekohasekai.sagernet.database.DataStore
+import io.nekohasekai.sagernet.database.preference.EditTextPreferenceModifiers
+import io.nekohasekai.sagernet.fmt.trojan.TrojanBean
+
+class TrojanSettingsActivity : ProfileSettingsActivity<TrojanBean>() {
+
+    override fun createEntity() = TrojanBean()
+
+    override fun init() {
+        TrojanBean.DEFAULT_BEAN.init()
+    }
+
+    override fun TrojanBean.init() {
+        DataStore.profileName = name
+        DataStore.serverAddress = serverAddress
+        DataStore.serverPort = serverPort
+        DataStore.serverPassword = password
+        DataStore.serverSNI = sni
+    }
+
+    override fun TrojanBean.serialize() {
+        name = DataStore.profileName
+        serverAddress = DataStore.serverAddress
+        serverPort = DataStore.serverPort
+        password = DataStore.serverPassword
+        sni = DataStore.serverSNI
+    }
+
+    override fun PreferenceFragmentCompat.createPreferences(
+        savedInstanceState: Bundle?,
+        rootKey: String?,
+    ) {
+        addPreferencesFromResource(R.xml.trojan_preferences)
+        findPreference<EditTextPreference>(Key.SERVER_PORT)!!.apply {
+            setOnBindEditTextListener(EditTextPreferenceModifiers.Port)
+        }
+        findPreference<EditTextPreference>(Key.SERVER_PASSWORD)!!.apply {
+            summaryProvider = PasswordSummaryProvider
+        }
+    }
+
+}

+ 3 - 0
app/src/main/res/menu/add_profile_menu.xml

@@ -34,6 +34,9 @@
                     <item
                         android:id="@+id/action_new_vless"
                         android:title="@string/action_vless" />
+                    <item
+                        android:id="@+id/action_new_trojan"
+                        android:title="@string/action_trojan" />
                 </menu>
             </item>
 

+ 1 - 0
app/src/main/res/values/strings.xml

@@ -171,6 +171,7 @@
     <string name="action_shadowsocksr">ShadowsocksR</string>
     <string name="action_vmess">VMess</string>
     <string name="action_vless">VLESS</string>
+    <string name="action_trojan">Trojan</string>
 
     <string name="action_group">Group</string>
     <string name="action_create_group">Empty group</string>

+ 34 - 0
app/src/main/res/xml/trojan_preferences.xml

@@ -0,0 +1,34 @@
+<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
+
+    <EditTextPreference
+        app:icon="@drawable/ic_social_emoji_symbols"
+        app:key="profileName"
+        app:title="@string/profile_name"
+        app:useSimpleSummaryProvider="true" />
+
+    <PreferenceCategory app:title="@string/proxy_cat">
+
+        <EditTextPreference
+            app:icon="@drawable/ic_hardware_router"
+            app:key="serverAddress"
+            app:title="@string/server_address"
+            app:useSimpleSummaryProvider="true" />
+        <EditTextPreference
+            app:icon="@drawable/ic_maps_directions_boat"
+            app:key="serverPort"
+            app:title="@string/server_port"
+            app:useSimpleSummaryProvider="true" />
+        <EditTextPreference
+            app:dialogLayout="@layout/layout_password_dialog"
+            app:icon="@drawable/ic_settings_password"
+            app:key="serverPassword"
+            app:title="@string/password" />
+        <EditTextPreference
+            app:icon="@drawable/ic_action_copyright"
+            app:key="serverSNI"
+            app:title="@string/sni"
+            app:useSimpleSummaryProvider="true" />
+    </PreferenceCategory>
+
+
+</PreferenceScreen>