浏览代码

Add TLS SOCKS & Add HTTP & Fixes

世界 4 年之前
父节点
当前提交
933be0b2c7
共有 35 个文件被更改,包括 859 次插入196 次删除
  1. 1 0
      .idea/dictionaries/sekai.xml
  2. 0 5
      .idea/vcs.xml
  3. 2 1
      README.md
  4. 2 2
      app/build.gradle
  5. 209 0
      app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/4.json
  6. 2 0
      app/src/main/AndroidManifest.xml
  7. 37 7
      app/src/main/java/io/nekohasekai/sagernet/database/ProfileManager.kt
  8. 14 1
      app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt
  9. 1 1
      app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt
  10. 11 1
      app/src/main/java/io/nekohasekai/sagernet/fmt/AbstractBean.java
  11. 7 0
      app/src/main/java/io/nekohasekai/sagernet/fmt/KryoConverters.java
  12. 49 0
      app/src/main/java/io/nekohasekai/sagernet/fmt/http/HttpBean.java
  13. 63 0
      app/src/main/java/io/nekohasekai/sagernet/fmt/http/HttpFmt.kt
  14. 9 0
      app/src/main/java/io/nekohasekai/sagernet/fmt/shadowsocks/ShadowsocksBean.java
  15. 13 0
      app/src/main/java/io/nekohasekai/sagernet/fmt/shadowsocksr/ShadowsocksRBean.java
  16. 20 12
      app/src/main/java/io/nekohasekai/sagernet/fmt/socks/SOCKSBean.java
  17. 9 1
      app/src/main/java/io/nekohasekai/sagernet/fmt/socks/SOCKSFmt.kt
  18. 5 9
      app/src/main/java/io/nekohasekai/sagernet/fmt/trojan/TrojanBean.java
  19. 5 12
      app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/AbstractV2RayBean.java
  20. 92 3
      app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/V2RayFmt.kt
  21. 9 0
      app/src/main/java/io/nekohasekai/sagernet/ktx/Formats.kt
  22. 51 0
      app/src/main/java/io/nekohasekai/sagernet/ktx/Layouts.kt
  23. 2 2
      app/src/main/java/io/nekohasekai/sagernet/ktx/Utils.kt
  24. 36 31
      app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt
  25. 1 2
      app/src/main/java/io/nekohasekai/sagernet/ui/GroupFragment.kt
  26. 6 2
      app/src/main/java/io/nekohasekai/sagernet/ui/ScannerActivity.kt
  27. 74 0
      app/src/main/java/io/nekohasekai/sagernet/ui/profile/HttpSettingsActivity.kt
  28. 5 1
      app/src/main/java/io/nekohasekai/sagernet/ui/profile/SocksSettingsActivity.kt
  29. 3 0
      app/src/main/res/menu/add_profile_menu.xml
  30. 1 1
      app/src/main/res/menu/scanner_menu.xml
  31. 46 57
      app/src/main/res/values-zh-rCN/strings.xml
  32. 19 31
      app/src/main/res/values-zh-rTW/strings.xml
  33. 2 13
      app/src/main/res/values/strings.xml
  34. 43 0
      app/src/main/res/xml/http_preferences.xml
  35. 10 1
      app/src/main/res/xml/socks_preferences.xml

+ 1 - 0
.idea/dictionaries/sekai.xml

@@ -7,6 +7,7 @@
       <w>grpc</w>
       <w>gson</w>
       <w>libev</w>
+      <w>nativeproxy</w>
       <w>nekohasekai</w>
       <w>obfs</w>
       <w>quic</w>

+ 0 - 5
.idea/vcs.xml

@@ -4,11 +4,6 @@
     <mapping directory="$PROJECT_DIR$" vcs="Git" />
     <mapping directory="$PROJECT_DIR$/app/src/main/jni/badvpn" vcs="Git" />
     <mapping directory="$PROJECT_DIR$/app/src/main/jni/libancillary" vcs="Git" />
-    <mapping directory="$PROJECT_DIR$/openvpn/src/main/cpp/asio" vcs="Git" />
-    <mapping directory="$PROJECT_DIR$/openvpn/src/main/cpp/lz4" vcs="Git" />
-    <mapping directory="$PROJECT_DIR$/openvpn/src/main/cpp/openssl" vcs="Git" />
-    <mapping directory="$PROJECT_DIR$/openvpn/src/main/cpp/openvpn" vcs="Git" />
-    <mapping directory="$PROJECT_DIR$/openvpn/src/main/cpp/openvpn3" vcs="Git" />
     <mapping directory="$PROJECT_DIR$/shadowsocks/src/main/rust/shadowsocks-rust" vcs="Git" />
     <mapping directory="$PROJECT_DIR$/shadowsocksr/src/main/jni/libancillary" vcs="Git" />
     <mapping directory="$PROJECT_DIR$/shadowsocksr/src/main/jni/libsodium" vcs="Git" />

+ 2 - 1
README.md

@@ -14,6 +14,7 @@ The application is designed to be used whenever possible.
 ### Protocols
 
 * SOCKS
+* HTTP(s) / nativeproxy
 * Shadowsocks
 * ShadowsocksR
 * VMess
@@ -23,7 +24,7 @@ The application is designed to be used whenever possible.
 ### Subscription protocols
 
 * Universal base64 format
-* SIP008
+* Shadowsocks SIP008
 * Clash
 
 ## FEATURES

+ 2 - 2
app/build.gradle

@@ -38,8 +38,8 @@ android {
         applicationId "io.nekohasekai.sagernet"
         minSdkVersion 21
         targetSdkVersion 30
-        versionCode 2
-        versionName "0.1-beta3"
+        versionCode 4
+        versionName "0.1-beta4"
 
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
 

+ 209 - 0
app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/4.json

@@ -0,0 +1,209 @@
+{
+  "formatVersion": 1,
+  "database": {
+    "version": 4,
+    "identityHash": "43a741fb8cb74a9fef7b790cd036a5a9",
+    "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, `type` 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
+          },
+          {
+            "fieldPath": "type",
+            "columnName": "type",
+            "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, `httpBean` 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": "httpBean",
+            "columnName": "httpBean",
+            "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, '43a741fb8cb74a9fef7b790cd036a5a9')"
+    ]
+  }
+}

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

@@ -71,11 +71,13 @@
             android:taskAffinity=""
             android:theme="@style/Theme.AppCompat.Translucent" />
         <activity android:name="io.nekohasekai.sagernet.ui.profile.SocksSettingsActivity" />
+        <activity android:name="io.nekohasekai.sagernet.ui.profile.HttpSettingsActivity" />
         <activity android:name="io.nekohasekai.sagernet.ui.profile.ShadowsocksSettingsActivity" />
         <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"

+ 37 - 7
app/src/main/java/io/nekohasekai/sagernet/database/ProfileManager.kt

@@ -27,6 +27,7 @@ import com.github.shadowsocks.plugin.PluginOptions
 import io.nekohasekai.sagernet.R
 import io.nekohasekai.sagernet.aidl.TrafficStats
 import io.nekohasekai.sagernet.fmt.AbstractBean
+import io.nekohasekai.sagernet.fmt.http.HttpBean
 import io.nekohasekai.sagernet.fmt.shadowsocks.ShadowsocksBean
 import io.nekohasekai.sagernet.fmt.shadowsocks.fixInvalidParams
 import io.nekohasekai.sagernet.fmt.shadowsocks.parseShadowsocks
@@ -150,8 +151,9 @@ object ProfileManager {
         if (SagerDatabase.proxyDao.countByGroup(groupId) == 0L) {
             val group = SagerDatabase.groupDao.getById(groupId) ?: return
             if (group.isDefault) {
-                val created = createProfile(groupId, SOCKSBean.DEFAULT_BEAN.clone().apply {
+                val created = createProfile(groupId, SOCKSBean().apply {
                     name = "Local tunnel"
+                    initDefaultValues()
                 })
                 if (DataStore.selectedProxy == 0L) {
                     DataStore.selectedProxy = created.id
@@ -291,6 +293,31 @@ object ProfileManager {
                 Map::class.java)["proxies"] as List<Map<String, Any?>>)) {
                 val type = proxy["type"] as String
                 when (type) {
+                    "socks5" -> {
+                        proxies.add(SOCKSBean().apply {
+                            serverAddress = proxy["server"] as String
+                            serverPort = proxy["port"].toString().toInt()
+                            username = proxy["username"] as String?
+                            password = proxy["password"] as String?
+                            tls = proxy["tls"]?.toString() == "true"
+                            sni = proxy["sni"] as String?
+                            udp = proxy["udp"]?.toString() == "true"
+                            name = proxy["name"] as String?
+                        })
+                    }
+                    "http" -> {
+                        proxies.add(
+                            HttpBean().apply {
+                                serverAddress = proxy["server"] as String
+                                serverPort = proxy["port"].toString().toInt()
+                                username = proxy["username"] as String?
+                                password = proxy["password"] as String?
+                                tls = proxy["tls"]?.toString() == "true"
+                                sni = proxy["sni"] as String?
+                                name = proxy["name"] as String?
+                            }
+                        )
+                    }
                     "ss" -> {
                         var pluginStr = ""
                         if (proxy.contains("plugin")) {
@@ -305,7 +332,7 @@ object ProfileManager {
                             password = proxy["password"] as String
                             method = proxy["cipher"] as String
                             plugin = pluginStr
-                            name = proxy["name"] as String? ?: ""
+                            name = proxy["name"] as String?
 
                             fixInvalidParams()
                         })
@@ -337,24 +364,26 @@ object ProfileManager {
                                             (httpOpt.value as List<String>).first()
                                     }
                                 }
+                                "grpc-opts" -> for (grpcOpt in (opt.value as Map<String, Any>)) {
+                                    when (grpcOpt.key) {
+                                        "grpc-service-name" -> bean.path = grpcOpt.value as String
+                                    }
+                                }
                             }
                         }
                         proxies.add(bean)
                     }
-
-
                     "trojan" -> {
                         val bean = TrojanBean()
                         for (opt in proxy) {
                             when (opt.key) {
-                                "name" -> bean.name = opt.value as String
+                                "name" -> bean.name = opt.value as String?
                                 "server" -> bean.serverAddress = opt.value as String
                                 "port" -> bean.serverPort = opt.value.toString().toInt()
                                 "password" -> bean.password = opt.value as String
-                                "sni" -> bean.sni = opt.value as String
+                                "sni" -> bean.sni = opt.value as String?
                             }
                         }
-                        bean.initDefaultValues()
                         proxies.add(bean)
                     }
 
@@ -378,6 +407,7 @@ object ProfileManager {
                     }
                 }
             }
+            proxies.forEach { it.initDefaultValues() }
             return 1 to proxies
         }
 

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

@@ -29,6 +29,8 @@ import androidx.room.*
 import io.nekohasekai.sagernet.aidl.TrafficStats
 import io.nekohasekai.sagernet.fmt.AbstractBean
 import io.nekohasekai.sagernet.fmt.KryoConverters
+import io.nekohasekai.sagernet.fmt.http.HttpBean
+import io.nekohasekai.sagernet.fmt.http.toUri
 import io.nekohasekai.sagernet.fmt.shadowsocks.ShadowsocksBean
 import io.nekohasekai.sagernet.fmt.shadowsocks.methodsV2fly
 import io.nekohasekai.sagernet.fmt.shadowsocks.toUri
@@ -56,6 +58,7 @@ data class ProxyEntity(
     var tx: Long = 0L,
     var rx: Long = 0L,
     var socksBean: SOCKSBean? = null,
+    var httpBean: HttpBean? = null,
     var ssBean: ShadowsocksBean? = null,
     var ssrBean: ShadowsocksRBean? = null,
     var vmessBean: VMessBean? = null,
@@ -89,6 +92,7 @@ data class ProxyEntity(
             3 -> vmessBean = KryoConverters.vmessDeserialize(byteArray)
             4 -> vlessBean = KryoConverters.vlessDeserialize(byteArray)
             5 -> trojanBean = KryoConverters.trojanDeserialize(byteArray)
+            6 -> httpBean = KryoConverters.httpDeserialize(byteArray)
         }
     }
 
@@ -114,6 +118,7 @@ data class ProxyEntity(
             3 -> "VMess"
             4 -> "VLESS"
             5 -> "Trojan"
+            6 -> if (requireHttp().tls) "HTTPS" else "HTTP"
             else -> "Undefined type $type"
         }
     }
@@ -132,6 +137,7 @@ data class ProxyEntity(
             3 -> vmessBean ?: error("Null vmess node")
             4 -> vlessBean ?: error("Null vless node")
             5 -> trojanBean ?: error("Null trojan node")
+            6 -> httpBean ?: error("Null http node")
             else -> error("Undefined type $type")
         }
     }
@@ -144,19 +150,20 @@ data class ProxyEntity(
             3 -> requireVMess().toV2rayN()
             4 -> "目前 VLESS 不支持分享。(https://www.v2fly.org/config/protocols/vless.html)"
             5 -> requireTrojan().toUri()
+            6 -> requireHttp().toUri()
             else -> error("Undefined type $type")
         }
     }
 
     fun useExternalShadowsocks(): Boolean {
         if (type != 1) return false
+        if (DataStore.forceShadowsocksRust) return true
         val bean = requireSS()
         if (bean.plugin.isNotBlank()) {
             Logs.d("Requiring plugin ${bean.plugin}")
             return true
         }
         if (bean.method !in methodsV2fly) return true
-        if (DataStore.forceShadowsocksRust) return true
         return false
     }
 
@@ -166,6 +173,10 @@ data class ProxyEntity(
                 type = 0
                 socksBean = bean
             }
+            is HttpBean -> {
+                type = 6
+                httpBean = bean
+            }
             is ShadowsocksBean -> {
                 type = 1
                 ssBean = bean
@@ -196,6 +207,7 @@ data class ProxyEntity(
     fun requireVMess() = requireBean() as VMessBean
     fun requireVLESS() = requireBean() as VMessBean
     fun requireTrojan() = requireBean() as TrojanBean
+    fun requireHttp() = requireBean() as HttpBean
 
     fun settingIntent(ctx: Context): Intent {
         return Intent(ctx, when (type) {
@@ -205,6 +217,7 @@ data class ProxyEntity(
             3 -> VMessSettingsActivity::class.java
             4 -> VLESSSettingsActivity::class.java
             5 -> TrojanSettingsActivity::class.java
+            6 -> HttpSettingsActivity::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 = 3)
+@Database(entities = [ProxyGroup::class, ProxyEntity::class, KeyValuePair::class], version = 4)
 @TypeConverters(value = [KryoConverters::class, GsonConverters::class])
 @GenerateRoomMigrations
 abstract class SagerDatabase : RoomDatabase() {

+ 11 - 1
app/src/main/java/io/nekohasekai/sagernet/fmt/AbstractBean.java

@@ -29,14 +29,24 @@ import org.jetbrains.annotations.NotNull;
 import java.util.Arrays;
 
 import cn.hutool.core.clone.Cloneable;
+import cn.hutool.core.util.StrUtil;
 
 public abstract class AbstractBean implements Cloneable<AbstractBean> {
 
     public String serverAddress;
     public int serverPort;
-
     public String name;
 
+    public void initDefaultValues() {
+        if (StrUtil.isBlank(serverAddress)) {
+            serverAddress = "127.0.0.1";
+        }
+        if (serverPort == 0) {
+            serverPort = 1080;
+        }
+        if (name == null) name = "";
+    }
+
     public void serialize(ByteBufferOutput output) {
         output.writeString(name);
         output.writeString(serverAddress);

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

@@ -31,6 +31,7 @@ import java.io.ByteArrayOutputStream;
 
 import cn.hutool.core.io.IoUtil;
 import cn.hutool.core.util.ArrayUtil;
+import io.nekohasekai.sagernet.fmt.http.HttpBean;
 import io.nekohasekai.sagernet.fmt.shadowsocks.ShadowsocksBean;
 import io.nekohasekai.sagernet.fmt.shadowsocksr.ShadowsocksRBean;
 import io.nekohasekai.sagernet.fmt.socks.SOCKSBean;
@@ -68,6 +69,12 @@ public class KryoConverters {
         return deserialize(new SOCKSBean(), bytes);
     }
 
+    @TypeConverter
+    public static HttpBean httpDeserialize(byte[] bytes) {
+        if (ArrayUtil.isEmpty(bytes)) return null;
+        return deserialize(new HttpBean(), bytes);
+    }
+
     @TypeConverter
     public static ShadowsocksBean shadowsocksDeserialize(byte[] bytes) {
         if (ArrayUtil.isEmpty(bytes)) return null;

+ 49 - 0
app/src/main/java/io/nekohasekai/sagernet/fmt/http/HttpBean.java

@@ -0,0 +1,49 @@
+/******************************************************************************
+ *                                                                            *
+ * 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.http;
+
+import org.jetbrains.annotations.NotNull;
+
+import io.nekohasekai.sagernet.fmt.AbstractBean;
+import io.nekohasekai.sagernet.fmt.KryoConverters;
+
+public class HttpBean extends AbstractBean {
+
+    public String username;
+    public String password;
+    public boolean tls;
+    public String sni;
+
+    @Override
+    public void initDefaultValues() {
+        super.initDefaultValues();
+        if (username == null) username = "";
+        if (password == null) password = "";
+        if (sni == null) sni = "";
+    }
+
+    @NotNull
+    @Override
+    public AbstractBean clone() {
+        return KryoConverters.deserialize(new HttpBean(), KryoConverters.serialize(this));
+    }
+}

+ 63 - 0
app/src/main/java/io/nekohasekai/sagernet/fmt/http/HttpFmt.kt

@@ -0,0 +1,63 @@
+/******************************************************************************
+ *                                                                            *
+ * 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.http
+
+import io.nekohasekai.sagernet.ktx.urlSafe
+import okhttp3.HttpUrl
+import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
+
+fun parseHttp(link: String): HttpBean {
+    val httpUrl = link.replace("native+https://", "https://").toHttpUrlOrNull()
+        ?: error("Invalid http(s) link: $link")
+
+    return HttpBean().apply {
+        serverAddress = httpUrl.host
+        serverPort = httpUrl.port
+        username = httpUrl.username
+        password = httpUrl.password
+        sni = httpUrl.queryParameter("sni")
+        name = httpUrl.fragment
+        tls = httpUrl.scheme == "https"
+    }
+}
+
+fun HttpBean.toUri(): String {
+    val builder = HttpUrl.Builder()
+        .scheme(if (tls) "https" else "http")
+        .host(serverAddress)
+        .port(serverPort)
+
+    if (username.isNotBlank()) {
+        builder.username(username)
+    }
+    if (password.isNotBlank()) {
+        builder.password(password)
+    }
+    if (sni.isNotBlank()) {
+        builder.addQueryParameter("sni", sni)
+    }
+    if (name.isNotBlank()) {
+        builder.encodedFragment(name.urlSafe())
+    }
+
+    return builder.toString()
+}

+ 9 - 0
app/src/main/java/io/nekohasekai/sagernet/fmt/shadowsocks/ShadowsocksBean.java

@@ -44,6 +44,15 @@ public class ShadowsocksBean extends AbstractBean {
     public String password;
     public String plugin;
 
+    @Override
+    public void initDefaultValues() {
+        super.initDefaultValues();
+
+        if (method == null) method = "";
+        if (password == null) password = "";
+        if (plugin == null) plugin = "";
+    }
+
     @Override
     public void serialize(ByteBufferOutput output) {
         output.writeInt(0);

+ 13 - 0
app/src/main/java/io/nekohasekai/sagernet/fmt/shadowsocksr/ShadowsocksRBean.java

@@ -50,6 +50,19 @@ public class ShadowsocksRBean extends AbstractBean {
     public String obfs;
     public String obfsParam;
 
+    @Override
+    public void initDefaultValues() {
+        super.initDefaultValues();
+
+        if (password == null) password = "";
+        if (method == null) method = "";
+        if (protocol == null) protocol = "";
+        if (protocolParam == null) protocolParam = "";
+        if (obfs == null) obfs = "";
+        if (obfsParam == null) obfsParam = "";
+
+    }
+
     @Override
     public void serialize(ByteBufferOutput output) {
         output.writeInt(0);

+ 20 - 12
app/src/main/java/io/nekohasekai/sagernet/fmt/socks/SOCKSBean.java

@@ -26,33 +26,35 @@ import com.esotericsoftware.kryo.io.ByteBufferOutput;
 
 import org.jetbrains.annotations.NotNull;
 
-import java.util.Objects;
-
 import io.nekohasekai.sagernet.fmt.AbstractBean;
 import io.nekohasekai.sagernet.fmt.KryoConverters;
 
 public class SOCKSBean extends AbstractBean {
 
-    public static SOCKSBean DEFAULT_BEAN = new SOCKSBean() {{
-        name = "";
-        serverAddress = "127.0.0.1";
-        serverPort = 1080;
-        username = "";
-        password = "";
-        udp = false;
-    }};
-
     public String username;
     public String password;
     public boolean udp;
+    public boolean tls;
+    public String sni;
+
+    @Override
+    public void initDefaultValues() {
+        super.initDefaultValues();
+
+        if (username == null) username = "";
+        if (password == null) password = "";
+        if (sni == null) sni = "";
+    }
 
     @Override
     public void serialize(ByteBufferOutput output) {
-        output.writeInt(0);
+        output.writeInt(1);
         super.serialize(output);
         output.writeString(username);
         output.writeString(password);
         output.writeBoolean(udp);
+        output.writeBoolean(tls);
+        output.writeString(sni);
     }
 
     @Override
@@ -62,6 +64,12 @@ public class SOCKSBean extends AbstractBean {
         username = input.readString();
         password = input.readString();
         udp = input.readBoolean();
+        if (version > 0) {
+            tls = input.readBoolean();
+            sni = input.readString();
+        } else {
+            initDefaultValues();
+        }
     }
 
     @NotNull

+ 9 - 1
app/src/main/java/io/nekohasekai/sagernet/fmt/socks/SOCKSFmt.kt

@@ -36,7 +36,7 @@ fun parseSOCKS(link: String): SOCKSBean {
         }
         url = Base64.decodeStr(url)
         val httpUrl = "http://$url".toHttpUrlOrNull() ?: error("Invalid v2rayN link content: $url")
-        return SOCKSBean.DEFAULT_BEAN.clone().apply {
+        return SOCKSBean().apply {
             serverAddress = httpUrl.host
             serverPort = httpUrl.port
             username = httpUrl.username.takeIf { it != "null" } ?: ""
@@ -58,6 +58,8 @@ fun parseSOCKS(link: String): SOCKSBean {
             password = url.password
             name = url.fragment
             udp = url.queryParameter("udp") == "true"
+            tls = url.queryParameter("tls") == "true"
+            sni = url.queryParameter("sni")
         }
     }
 }
@@ -70,6 +72,12 @@ fun SOCKSBean.toUri(): String {
         .port(serverPort)
     if (!username.isNullOrBlank()) builder.username(username)
     if (!password.isNullOrBlank()) builder.password(password)
+    if (tls) {
+        builder.addQueryParameter("tls", "true")
+        if (sni.isNotBlank()) {
+            builder.addQueryParameter("sni", sni)
+        }
+    }
     if (!name.isNullOrBlank()) builder.encodedFragment(name.urlSafe())
     if (udp) builder.addQueryParameter("udp", "true")
     return builder.build().toString().replaceRange(0..3, "socks")

+ 5 - 9
app/src/main/java/io/nekohasekai/sagernet/fmt/trojan/TrojanBean.java

@@ -43,16 +43,12 @@ public class TrojanBean extends AbstractBean {
     public String password;
     public String sni;
 
+    @Override
     public void initDefaultValues() {
-        if (StrUtil.isNotBlank(serverAddress)) {
-            serverAddress = "";
-        }
-        if (StrUtil.isNotBlank(password)) {
-            password = "";
-        }
-        if (StrUtil.isNotBlank(sni)) {
-            sni = "";
-        }
+        super.initDefaultValues();
+
+        if (password == null) password = "";
+        if (sni == null) sni = "";
     }
 
     @Override

+ 5 - 12
app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/AbstractV2RayBean.java

@@ -41,19 +41,12 @@ public abstract class AbstractV2RayBean extends AbstractBean {
     public String sni;
     public boolean tls;
 
+    @Override
     public void initDefaultValues() {
-        if (StrUtil.isBlank(name)) {
-            name = "";
-        }
-        if (StrUtil.isBlank(serverAddress)) {
-            serverAddress = "127.0.0.1";
-        }
-        if (serverPort == 0) {
-            serverPort = 1080;
-        }
-        if (StrUtil.isBlank(uuid)) {
-            uuid = "";
-        }
+        super.initDefaultValues();
+
+        if (uuid == null) uuid = "";
+
         if (StrUtil.isBlank(network)) {
             network = "tcp";
         }

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

@@ -27,6 +27,7 @@ import io.nekohasekai.sagernet.BuildConfig
 import io.nekohasekai.sagernet.RouteMode
 import io.nekohasekai.sagernet.database.DataStore
 import io.nekohasekai.sagernet.database.ProxyEntity
+import io.nekohasekai.sagernet.fmt.http.HttpBean
 import io.nekohasekai.sagernet.fmt.shadowsocks.ShadowsocksBean
 import io.nekohasekai.sagernet.fmt.socks.SOCKSBean
 import io.nekohasekai.sagernet.fmt.trojan.TrojanBean
@@ -154,6 +155,45 @@ fun buildV2RayConfig(proxy: ProxyEntity): V2RayConfig {
                                 }
                             )
                         })
+                    if (bean.tls) {
+                        streamSettings = StreamSettingsObject().apply {
+                            network = "tls"
+                            if (bean.sni.isNotBlank()) {
+                                tlsSettings = TLSObject().apply {
+                                    serverName = bean.sni
+                                }
+                            }
+                        }
+                    }
+                } else if (bean is HttpBean) {
+                    protocol = "http"
+                    settings = LazyOutboundConfigurationObject(
+                        HTTPOutboundConfigurationObject().apply {
+                            servers = listOf(
+                                HTTPOutboundConfigurationObject.ServerObject().apply {
+                                    address = bean.serverAddress
+                                    port = bean.serverPort
+                                    if (!bean.username.isNullOrBlank()) {
+                                        users =
+                                            listOf(HTTPInboundConfigurationObject.AccountObject()
+                                                .apply {
+                                                    user = bean.username
+                                                    pass = bean.password
+                                                })
+                                    }
+                                }
+                            )
+                        })
+                    if (bean.tls) {
+                        streamSettings = StreamSettingsObject().apply {
+                            network = "tls"
+                            if (bean.sni.isNotBlank()) {
+                                tlsSettings = TLSObject().apply {
+                                    serverName = bean.sni
+                                }
+                            }
+                        }
+                    }
                 } else if (bean is AbstractV2RayBean) {
                     if (bean is VMessBean) {
                         protocol = "vmess"
@@ -492,9 +532,12 @@ fun buildV2RayConfig(proxy: ProxyEntity): V2RayConfig {
 
 fun parseVmess(link: String): VMessBean {
     if (link.contains("?") || link.startsWith("vmess1://")) return parseVmess1(link)
-
+    val result = Base64.decodeStr(link.substringAfter("vmess://"))
+    if (result.contains("= vmess")) {
+        return parseCsvVMess(result)
+    }
     val bean = VMessBean()
-    val json = JSONObject(Base64.decodeStr(link.substringAfter("vmess://")))
+    val json = JSONObject(result)
 
     bean.serverAddress = json.getStr("add")
     bean.serverPort = json.getInt("port")
@@ -542,11 +585,57 @@ fun parseVmess(link: String): VMessBean {
         }
     }
 
-    bean.initDefaultValues()
     return bean
 
 }
 
+private fun parseCsvVMess(csv: String): VMessBean {
+
+    val args = csv.split(",")
+
+    val bean = VMessBean()
+
+    bean.serverAddress = args[1]
+    bean.serverPort = args[2].toInt()
+    bean.security = args[3]
+    bean.uuid = args[4].replace("\"", "")
+
+    args.subList(5, args.size).forEach {
+
+        when {
+            it == "over-tls=true" -> bean.tls = true
+            it.startsWith("tls-host=") -> bean.requestHost = it.substringAfter("=")
+            it.startsWith("obfs=") -> bean.network = it.substringAfter("=")
+
+            it.startsWith("obfs-path=") || it.contains("Host:") -> {
+
+                runCatching {
+
+                    bean.path = it
+                        .substringAfter("obfs-path=\"")
+                        .substringBefore("\"obfs")
+
+                }
+
+                runCatching {
+
+                    bean.requestHost = it
+                        .substringAfter("Host:")
+                        .substringBefore("[")
+
+                }
+
+            }
+
+        }
+
+    }
+
+    return bean
+
+}
+
+
 fun parseVmess1(link: String): VMessBean {
     val bean = VMessBean()
     val lnk = link

+ 9 - 0
app/src/main/java/io/nekohasekai/sagernet/ktx/Formats.kt

@@ -22,6 +22,7 @@
 package io.nekohasekai.sagernet.ktx
 
 import io.nekohasekai.sagernet.fmt.AbstractBean
+import io.nekohasekai.sagernet.fmt.http.parseHttp
 import io.nekohasekai.sagernet.fmt.shadowsocks.parseShadowsocks
 import io.nekohasekai.sagernet.fmt.shadowsocksr.parseShadowsocksR
 import io.nekohasekai.sagernet.fmt.socks.parseSOCKS
@@ -40,6 +41,13 @@ fun parseProxies(text: String): List<AbstractBean> {
             }.onFailure {
                 Logs.w(it)
             }
+        } else if (link.matches("(http|https|native\\+https)://.*".toRegex())) {
+            Logs.d("Try parse http link: $link")
+            runCatching {
+                entities.add(parseHttp(link))
+            }.onFailure {
+                Logs.w(it)
+            }
         } else if (link.startsWith("vmess://") || link.startsWith("vmess1://")) {
             Logs.d("Try parse vmess link: $link")
             runCatching {
@@ -70,5 +78,6 @@ fun parseProxies(text: String): List<AbstractBean> {
             }
         }
     }
+    entities.forEach { it.initDefaultValues() }
     return entities
 }

+ 51 - 0
app/src/main/java/io/nekohasekai/sagernet/ktx/Layouts.kt

@@ -0,0 +1,51 @@
+/******************************************************************************
+ *                                                                            *
+ * 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.ktx
+
+import android.content.Context
+import androidx.recyclerview.widget.GridLayoutManager
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+
+class FixedLinearLayoutManager(context: Context) :
+    LinearLayoutManager(context, RecyclerView.VERTICAL, false) {
+
+    override fun onLayoutChildren(recycler: RecyclerView.Recycler?, state: RecyclerView.State?) {
+        try {
+            super.onLayoutChildren(recycler, state)
+        } catch (ignored: IndexOutOfBoundsException) {
+        }
+    }
+
+}
+
+class FixedGridLayoutManager(context: Context, spanCount: Int) :
+    GridLayoutManager(context, spanCount) {
+
+    override fun onLayoutChildren(recycler: RecyclerView.Recycler?, state: RecyclerView.State?) {
+        try {
+            super.onLayoutChildren(recycler, state)
+        } catch (ignored: IndexOutOfBoundsException) {
+        }
+    }
+
+}

+ 2 - 2
app/src/main/java/io/nekohasekai/sagernet/ktx/Utils.kt

@@ -173,8 +173,8 @@ fun String.unUrlSafe(): String {
     return URLDecoder.decode(this, CharsetUtil.CHARSET_UTF_8)
 }
 
-fun RecyclerView.scrollTo(index: Int) {
-    post {
+fun RecyclerView.scrollTo(index: Int, force: Boolean = false) {
+    if (force) post {
         scrollToPosition(index)
     }
     post {

+ 36 - 31
app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt

@@ -188,6 +188,9 @@ class ConfigurationFragment : ToolbarFragment(R.layout.layout_group_list),
             R.id.action_new_socks -> {
                 startActivity(Intent(requireActivity(), SocksSettingsActivity::class.java))
             }
+            R.id.action_new_http -> {
+                startActivity(Intent(requireActivity(), HttpSettingsActivity::class.java))
+            }
             R.id.action_new_ss -> {
                 startActivity(Intent(requireActivity(), ShadowsocksSettingsActivity::class.java))
             }
@@ -262,17 +265,15 @@ class ConfigurationFragment : ToolbarFragment(R.layout.layout_group_list),
         }
 
         override suspend fun onAdd(group: ProxyGroup) {
-            groupList.add(group)
-
-            if (groupList.all { it.isDefault }) tabLayout.post {
-                tabLayout.visibility = View.VISIBLE
-            }
+            tabLayout.post {
+                groupList.add(group)
 
-            onMainDispatcher {
-                tabLayout.post {
-                    notifyItemInserted(groupList.size - 1)
-                    tabLayout.getTabAt(groupList.size - 1)?.select()
+                if (groupList.all { it.isDefault }) tabLayout.post {
+                    tabLayout.visibility = View.VISIBLE
                 }
+
+                notifyItemInserted(groupList.size - 1)
+                tabLayout.getTabAt(groupList.size - 1)?.select()
             }
         }
 
@@ -280,7 +281,7 @@ class ConfigurationFragment : ToolbarFragment(R.layout.layout_group_list),
             val index = groupList.indexOfFirst { it.id == groupId }
             if (index == -1) return
 
-            onMainDispatcher {
+            tabLayout.post {
                 groupList.removeAt(index)
                 notifyItemRemoved(index)
             }
@@ -290,7 +291,7 @@ class ConfigurationFragment : ToolbarFragment(R.layout.layout_group_list),
             val index = groupList.indexOfFirst { it.id == group.id }
             if (index == -1) return
 
-            onMainDispatcher {
+            tabLayout.post {
                 tabLayout.getTabAt(index)?.text = group.displayName()
             }
         }
@@ -324,9 +325,9 @@ class ConfigurationFragment : ToolbarFragment(R.layout.layout_group_list),
             if (!::proxyGroup.isInitialized) return
 
             layoutManager = if (proxyGroup.type != 1) {
-                LinearLayoutManager(context, RecyclerView.VERTICAL, false)
+                FixedLinearLayoutManager(view.context)
             } else {
-                GridLayoutManager(context, 2)
+                FixedGridLayoutManager(view.context, 2)
             }
 
             configurationListView = view.findViewById(R.id.configuration_list)
@@ -476,9 +477,11 @@ class ConfigurationFragment : ToolbarFragment(R.layout.layout_group_list),
 
             override fun undo(actions: List<Pair<Int, ProxyEntity>>) {
                 for ((index, item) in actions) {
-                    configurationList[item.id] = item
-                    configurationIdList.add(index, item.id)
-                    notifyItemInserted(index)
+                    configurationListView.post {
+                        configurationList[item.id] = item
+                        configurationIdList.add(index, item.id)
+                        notifyItemInserted(index)
+                    }
                 }
             }
 
@@ -494,7 +497,7 @@ class ConfigurationFragment : ToolbarFragment(R.layout.layout_group_list),
             override suspend fun onAdd(profile: ProxyEntity) {
                 if (profile.groupId != proxyGroup.id) return
 
-                onMainDispatcher {
+                configurationListView.post {
                     undoManager.flush()
                     val pos = itemCount
                     configurationList[profile.id] = profile
@@ -507,9 +510,9 @@ class ConfigurationFragment : ToolbarFragment(R.layout.layout_group_list),
                 if (profile.groupId != proxyGroup.id) return
                 val index = configurationIdList.indexOf(profile.id)
                 if (index < 0) return
-                undoManager.flush()
-                configurationList[profile.id] = profile
-                onMainDispatcher {
+                configurationListView.post {
+                    undoManager.flush()
+                    configurationList[profile.id] = profile
                     notifyItemChanged(index)
                 }
             }
@@ -532,16 +535,16 @@ class ConfigurationFragment : ToolbarFragment(R.layout.layout_group_list),
                 if (groupId != proxyGroup.id) return
                 val index = configurationIdList.indexOf(profileId)
                 if (index < 0) return
-                configurationIdList.removeAt(index)
-                configurationList.remove(profileId)
-                onMainDispatcher {
+                configurationListView.post {
+                    configurationIdList.removeAt(index)
+                    configurationList.remove(profileId)
                     notifyItemRemoved(index)
                 }
             }
 
             override suspend fun onCleared(groupId: Long) {
                 if (groupId != proxyGroup.id) return
-                onMainDispatcher {
+                configurationListView.post {
                     configurationList.clear()
                     configurationList.clear()
                     notifyDataSetChanged()
@@ -551,27 +554,29 @@ class ConfigurationFragment : ToolbarFragment(R.layout.layout_group_list),
             override suspend fun reloadProfiles(groupId: Long) {
                 if (groupId != proxyGroup.id) return
 
-                configurationIdList.clear()
-                configurationIdList.addAll(SagerDatabase.proxyDao.getIdsByGroup(proxyGroup.id))
+                val newProfiles = SagerDatabase.proxyDao.getIdsByGroup(proxyGroup.id)
 
                 if (selected && !scrolled) {
                     scrolled = true
                     val selectedProxy = DataStore.selectedProxy
-                    val selectedProfileIndex = configurationIdList.indexOf(selectedProxy)
+                    val selectedProfileIndex = newProfiles.indexOf(selectedProxy)
 
                     configurationListView.post {
-                        configurationListView.scrollTo(selectedProfileIndex)
+                        configurationListView.scrollTo(selectedProfileIndex, true)
                     }
                 }
 
-                onMainDispatcher {
+                configurationListView.post {
+                    configurationIdList.clear()
+                    configurationIdList.addAll(newProfiles)
                     notifyDataSetChanged()
                 }
 
-                if (configurationIdList.isEmpty() && proxyGroup.isDefault) {
+                if (newProfiles.isEmpty() && proxyGroup.isDefault) {
                     ProfileManager.createProfile(groupId,
-                        SOCKSBean.DEFAULT_BEAN.clone().apply {
+                        SOCKSBean().apply {
                             name = "Local tunnel"
+                            initDefaultValues()
                         })
                 }
             }

+ 1 - 2
app/src/main/java/io/nekohasekai/sagernet/ui/GroupFragment.kt

@@ -40,7 +40,6 @@ import androidx.core.view.isInvisible
 import androidx.core.view.isVisible
 import androidx.core.widget.addTextChangedListener
 import androidx.recyclerview.widget.ItemTouchHelper
-import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
 import cn.hutool.core.date.DateUtil
 import com.github.shadowsocks.plugin.fragment.AlertDialogFragment
@@ -80,7 +79,7 @@ class GroupFragment : ToolbarFragment(R.layout.layout_group), Toolbar.OnMenuItem
         toolbar.setOnMenuItemClickListener(this)
 
         groupListView = view.findViewById(R.id.group_list)
-        groupListView.layoutManager = LinearLayoutManager(requireContext())
+        groupListView.layoutManager = FixedLinearLayoutManager(view.context)
         groupAdapter = GroupAdapter()
         ProfileManager.addListener(groupAdapter)
         groupListView.adapter = groupAdapter

+ 6 - 2
app/src/main/java/io/nekohasekai/sagernet/ui/ScannerActivity.kt

@@ -151,8 +151,12 @@ class ScannerActivity : AppCompatActivity(), BarcodeCallback {
     }
 
     override fun onOptionsItemSelected(item: MenuItem): Boolean {
-        importCodeFile.launch("image/*")
-        return true
+        if (item.itemId == R.id.action_import_from_file) {
+            importCodeFile.launch("image/*")
+            return true
+        } else {
+            return super.onOptionsItemSelected(item)
+        }
     }
 
     /**

+ 74 - 0
app/src/main/java/io/nekohasekai/sagernet/ui/profile/HttpSettingsActivity.kt

@@ -0,0 +1,74 @@
+/******************************************************************************
+ *                                                                            *
+ * 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.http.HttpBean
+
+class HttpSettingsActivity : ProfileSettingsActivity<HttpBean>() {
+
+    override fun createEntity() = HttpBean()
+
+    override fun init() {
+        HttpBean().apply { initDefaultValues() }.init()
+    }
+
+    override fun HttpBean.init() {
+        DataStore.profileName = name
+        DataStore.serverAddress = serverAddress
+        DataStore.serverPort = serverPort
+        DataStore.serverUsername = username
+        DataStore.serverPassword = password
+        DataStore.serverTLS = tls
+        DataStore.serverSNI = sni
+    }
+
+    override fun HttpBean.serialize() {
+        name = DataStore.profileName
+        serverAddress = DataStore.serverAddress
+        serverPort = DataStore.serverPort
+        username = DataStore.serverUsername
+        password = DataStore.serverPassword
+        tls = DataStore.serverTLS
+        sni = DataStore.serverSNI
+    }
+
+    override fun PreferenceFragmentCompat.createPreferences(
+        savedInstanceState: Bundle?,
+        rootKey: String?,
+    ) {
+        addPreferencesFromResource(R.xml.http_preferences)
+        findPreference<EditTextPreference>(Key.SERVER_PORT)!!.apply {
+            setOnBindEditTextListener(EditTextPreferenceModifiers.Port)
+        }
+        findPreference<EditTextPreference>(Key.SERVER_PASSWORD)!!.apply {
+            summaryProvider = PasswordSummaryProvider
+        }
+    }
+
+}

+ 5 - 1
app/src/main/java/io/nekohasekai/sagernet/ui/profile/SocksSettingsActivity.kt

@@ -35,7 +35,7 @@ class SocksSettingsActivity : ProfileSettingsActivity<SOCKSBean>() {
     override fun createEntity() = SOCKSBean()
 
     override fun init() {
-        SOCKSBean.DEFAULT_BEAN.init()
+        SOCKSBean().apply { initDefaultValues() }.init()
     }
 
     override fun SOCKSBean.init() {
@@ -45,6 +45,8 @@ class SocksSettingsActivity : ProfileSettingsActivity<SOCKSBean>() {
         DataStore.serverUsername = username
         DataStore.serverPassword = password
         DataStore.serverUdp = udp
+        DataStore.serverTLS = tls
+        DataStore.serverSNI = sni
     }
 
     override fun SOCKSBean.serialize() {
@@ -54,6 +56,8 @@ class SocksSettingsActivity : ProfileSettingsActivity<SOCKSBean>() {
         username = DataStore.serverUsername
         password = DataStore.serverPassword
         udp = DataStore.serverUdp
+        tls = DataStore.serverTLS
+        sni = DataStore.serverSNI
     }
 
     override fun PreferenceFragmentCompat.createPreferences(

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

@@ -22,6 +22,9 @@
                     <item
                         android:id="@+id/action_new_socks"
                         android:title="@string/action_socks" />
+                    <item
+                        android:id="@+id/action_new_http"
+                        android:title="@string/action_http" />
                     <item
                         android:id="@+id/action_new_ss"
                         android:title="@string/action_shadowsocks" />

+ 1 - 1
app/src/main/res/menu/scanner_menu.xml

@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <menu xmlns:android="http://schemas.android.com/apk/res/android"
       xmlns:app="http://schemas.android.com/apk/res-auto">
-    <item android:id="@+id/action_import_clipboard"
+    <item android:id="@+id/action_import_from_file"
           android:title="@string/action_import_file"
           android:icon="@drawable/ic_image_photo"
           android:alphabeticShortcut="o"

+ 46 - 57
app/src/main/res/values-zh-rCN/strings.xml

@@ -1,51 +1,50 @@
 <resources>
     <string name="app_name">SagerNet</string>
-    <string name="app_desc">使用 Kotlin 开发的 Android 端通用代理工具.</string>
+    <string name="app_desc">适用于 Android 之通用代理工具, 由 Kotlin 编写.</string>
 
     <string name="project">项目</string>
     <string name="github">源代码</string>
     <string name="telegram">Telegram 更新频道</string>
-    <string name="oss_licenses">源代码授权协议</string>
+    <string name="oss_licenses">开放源代码协议</string>
 
     <string name="misc">其他</string>
     <string name="app_version">版本</string>
-    <string name="v2ray_version">v2ray-core 版本</string>
-    <string name="logcat">导出日志</string>
-    <string name="logcat_summary">这对修复 BUG 非常有用</string>
+    <string name="v2ray_version">版本 (v2ray-core)</string>
+    <string name="logcat">导出调试信息</string>
+    <string name="logcat_summary">这种非常有用哇</string>
 
     <string name="action_settings">设置</string>
     <string name="menu_configuration">配置</string>
-    <string name="menu_group">组</string>
+    <string name="menu_group">组</string>
     <string name="menu_about">关于</string>
 
-    <string name="group_default">初始配置</string>
+    <string name="group_default">默认</string>
     <string name="profile_address">127.0.0.1:1080</string>
     <string name="profile_type">socks5</string>
 
     <string name="quick_toggle">切换</string>
-    <string name="send_email">发送电子邮件</string>
 
     <!-- group -->
     <string name="group_name">分组名</string>
     <string name="group_update">更新</string>
     <string name="group_subscription_link">订阅链接</string>
     <string name="group_name_required">需要分组名称</string>
-    <string name="group_create">创建分组</string>
-    <string name="group_create_subscription">添加订阅</string>
+    <string name="group_create">创建分组</string>
+    <string name="group_create_subscription">添加订阅</string>
     <string name="group_edit">编辑</string>
     <string name="group_edit_subscription">编辑订阅</string>
 
-    <string name="group_status_empty">空分组</string>
-    <string name="group_status_empty_subscription">未更新</string>
+    <string name="group_status_empty">空</string>
+    <string name="group_status_empty_subscription">未更新</string>
     <string name="group_status_proxies">%d 个配置</string>
     <string name="group_status_proxies_subscription">%d 个配置, 最后更新于 %s.</string>
     <string name="group_no_difference">没有变化</string>
     <string name="group_updated">更新了 %d 个节点</string>
     <string name="group_show_diff">变化</string>
-    <string name="group_delete_confirm_prompt">您确定要删除这个分组/订阅吗?</string>
-    <string name="group_added">添加时间: \n%s</string>
-    <string name="group_changed">最后更新时间: \n%s</string>
-    <string name="group_deleted">删除时间: \n%s</string>
+    <string name="group_delete_confirm_prompt">您确定要删除这个分组吗?</string>
+    <string name="group_added">新增: \n%s</string>
+    <string name="group_changed">更新: \n%s</string>
+    <string name="group_deleted">删除: \n%s</string>
 
     <!-- misc -->
     <string name="service_mode">运行模式</string>
@@ -53,18 +52,18 @@
     <string name="service_mode_vpn">VPN</string>
     <string name="service_mode_transproxy">透明代理</string>
     <string name="share_over_lan">LAN 共用</string>
-    <string name="port_proxy">SOCKS5代理端口</string>
-    <string name="port_http">HTTP代理端口</string>
-    <string name="port_local_dns">本地DNS端口</string>
+    <string name="port_proxy">SOCKS5 代理端口</string>
+    <string name="port_http">HTTP 代理端口</string>
+    <string name="port_local_dns">本地 DNS 端口</string>
     <string name="port_transproxy">透明代理端口</string>
     <string name="allow_access">允许来自局域网的连接</string>
-    <string name="allow_access_sum">将 传入服务器绑定至 0.0.0.0</string>
-    <string name="cag_dns">DNS设置</string>
-    <string name="local_dns">启用本地DNS</string>
-    <string name="domestic_dns">国内DNS</string>
+    <string name="allow_access_sum">将传入服务器绑定至 0.0.0.0</string>
+    <string name="cag_dns">DNS 设置</string>
+    <string name="local_dns">启用本地 DNS</string>
+    <string name="domestic_dns">国内 DNS</string>
     <string name="cag_route">路由设置</string>
     <string name="remote_dns">远程DNS</string>
-    <string name="force_ss_rust">不使用 v2ray-core 附带的 shadowsocks</string>
+    <string name="force_ss_rust">不要使用 v2ray 路由 shadowsocks</string>
     <string name="force_ss_sum">强制所有 shadowsocks 节点使用和 Shadowsocks Android 相同的 shadowsocks-rust 后端</string>
     <string name="inbound_settings">入站设置</string>
     <string name="general_settings">软件设置</string>
@@ -73,14 +72,14 @@
     <string name="ws_max_early_data">Max early data</string>
     <string name="ws_browser_forwarding">浏览器转发</string>
     <string name="ws_browser_forwarding_sum">转发相应的 WebSockets 到浏览器.</string>
-    <string name="speed_interval">通知栏网速更新时间间隔</string>
+    <string name="speed_interval">速度通知更新间隔</string>
 
     <string name="traffic">%1$s↑ %2$s↓</string>
     <string name="stat_summary">上传: \t\t\t\t\t%3$s\t↑\t%1$s\n下载: \t%4$s\t↓\t%2$s</string>
     <string name="speed">%s/s</string>
     <string name="connection_test_pending">测试连接</string>
     <string name="connection_test_testing">测试中…</string>
-    <string name="connection_test_available">连接成功: HTTP 握手耗时 %dms</string>
+    <string name="connection_test_available">连接成功: HTTPS 握手耗时 %dms</string>
     <string name="connection_test_error">连接失败: %s</string>
     <string name="connection_test_fail">网络不可用</string>
     <string name="connection_test_error_status_code">错误代码: #%d</string>
@@ -93,28 +92,28 @@
     <string name="password_opt">密码 (可选)</string>
     <string name="password">密码</string>
     <string name="server_udp">UDP</string>
-    <string name="udp_summary">转发 UDP 到服务器</string>
+    <string name="udp_summary">路由 UDP 流量到服务器</string>
     <string name="enc_method">加密方式</string>
     <string name="protocol">协议</string>
     <string name="protocol_param">协议参数</string>
     <string name="obfs">混淆</string>
     <string name="obfs_param">混淆参数</string>
     <string name="uuid">用户ID</string>
-    <string name="alter_id">Alter ID</string>
+    <string name="alter_id">替代 ID</string>
     <string name="security">加密方式</string>
     <string name="network">传输协议</string>
     <string name="header_type">伪装类型</string>
     <string name="request_host">请求域名 / QUIC 加密方式</string>
     <string name="path">Path / Key / Seed / ServiceName</string>
     <string name="tls">使用 TLS</string>
-    <string name="sni">SNI</string>
-    <string name="encryption">加密方式</string>
+    <string name="sni">服务器名称指示</string>
+    <string name="encryption">加密</string>
 
     <!-- feature category -->
-    <string name="ipv6">转发 IPv6 流量</string>
-    <string name="ipv6_summary">向服务器转发 IPV6 流量</string>
-    <string name="ipv6_prefer">优先 IPv6</string>
-    <string name="ipv6_prefer_summary">优先使用 IPV6 地址和路由</string>
+    <string name="ipv6">IPv6 路由</string>
+    <string name="ipv6_summary">向服务器重定向 IPv6 流量</string>
+    <string name="ipv6_prefer">IPv6 优先</string>
+    <string name="ipv6_prefer_summary">优先使用 IPv6 地址和路由</string>
 
     <string name="metered">按流量计费</string>
     <string name="metered_summary">让系统认为此 VPN 按流量计费</string>
@@ -123,34 +122,26 @@
     <string name="route_entry_bypass_lan">绕过局域网</string>
     <string name="route_entry_bypass_chn">绕过中国大陆地址</string>
     <string name="route_entry_bypass_lan_chn">绕过局域网和中国大陆地址</string>
-    <string name="route_entry_gfwlist">GFW List</string>
-    <string name="route_entry_chinalist">China List</string>
     <string name="proxied_apps">分应用代理</string>
     <string name="proxied_apps_summary">代理被勾选应用的流量</string>
     <string name="on">打开</string>
     <string name="off">关闭</string>
-    <string name="search_apps">搜索…</string>
+    <string name="search_apps">搜索…</string>
     <string name="scanning">扫描中…</string>
     <string name="invert_selections">反选</string>
-    <string name="clear_selections">取消选择</string>
+    <string name="clear_selections">清空</string>
 
-    <string name="proxied_apps_mode">模式</string>
-    <string name="bypass_apps">黑名单模式</string>
-    <string name="bypass_apps_summary">打开此选项后,被选中的应用将不会被代理</string>
+    <string name="bypass_apps">绕过</string>
     <string name="show_system_apps">显示系统应用</string>
-    <string name="show_search">显示搜索</string>
 
     <string name="auto_connect">自动连接</string>
-    <string name="auto_connect_summary">SagerNet 将在手机启动或 SagerNet 更新完成后自动重新连接</string>
-    <string name="direct_boot_aware">允许在锁屏界面显示</string>
+    <string name="auto_connect_summary">SagerNet 将在手机启动或更新完成后自动重新连接</string>
+    <string name="direct_boot_aware">允许在解锁前启动</string>
     <string name="direct_boot_aware_summary">您正在使用的服务器可能会受到更少的保护</string>
     <plurals name="hosts_summary">
         <item quantity="one">1 个主机已设置</item>
         <item quantity="other"> %d 个主机已设置 </item>
     </plurals>
-    <string name="udp_dns">通过 UDP 发送 DNS 请求</string>
-    <string name="udp_dns_summary">需要服务器支持转发 UDP 流量</string>
-    <string name="udp_fallback">UDP Fallback</string>
 
     <!-- notification category -->
     <string name="service_vpn">VPN 服务</string>
@@ -168,7 +159,6 @@
 
     <!-- alert category -->
     <string name="profile_empty">请选择一个服务器配置</string>
-    <string name="proxy_empty">代理/密码不能为空</string>
     <string name="connect">连接</string>
     <string name="clipboard_empty">剪切板没有内容</string>
 
@@ -179,18 +169,18 @@
     <string name="share">分享</string>
     <string name="add_profile">添加服务器配置</string>
     <string name="action_profile">服务器配置</string>
-    <string name="action_socks">Socks</string>
+    <string name="action_socks">SOCKS</string>
+    <string name="action_http">HTTP</string>
     <string name="action_shadowsocks">Shadowsocks</string>
     <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">分组</string>
-    <string name="action_create_group">空分组</string>
+    <string name="action_create_group">空组</string>
     <string name="action_from_link">来自订阅</string>
 
-    <string name="action_scan_china_apps">扫描中国软件</string>
+    <string name="action_scan_china_apps">扫描中国应用</string>
     <string name="action_export_more">导出…</string>
     <string name="action_export_file">导出到文件…</string>
     <string name="action_export">导出到剪切板</string>
@@ -240,7 +230,6 @@
     <!-- subscriptions -->
     <string name="subscriptions">订阅</string>
     <string name="cleartext_http_warning">使用纯文字 HTTP 流量传输的订阅并不安全</string>
-    <string name="fetching_subscription">正在获取订阅...</string>
     <string name="error_title">错误</string>
     <string name="no_proxies_found">未在此订阅内找到服务器配置</string>
 
@@ -249,12 +238,12 @@
     <string name="plugin_configure">设置…</string>
     <string name="plugin_disabled">已停用</string>
     <string name="plugin_unknown">未知插件: %s</string>
-    <string name="plugin_untrusted">请注意: 此插件的来源不可靠</string>
+    <string name="plugin_untrusted">请注意: 此插件的不来自可靠的来源.</string>
     <string name="plugin_auto_connect_unlock_only">此插件可能不支持自动连接</string>
 
-    <string name="proxy_cat">服务器设置</string>
-    <string name="feature_cat">软件设置</string>
-    <string name="unsaved_changes_prompt">设置未保存 , 是否需要保存</string>
+    <string name="proxy_cat">"服务器设置"</string>
+    <string name="feature_cat">"功能设置"</string>
+    <string name="unsaved_changes_prompt">"是否要保存修改?"</string>
     <string name="yes">是</string>
     <string name="no">否</string>
     <string name="apply">应用</string>

+ 19 - 31
app/src/main/res/values-zh-rTW/strings.xml

@@ -1,6 +1,6 @@
 <resources>
     <string name="app_name">SagerNet</string>
-    <string name="app_desc">適用於 Android 的通用代理工具鏈,由 Kotlin 開發。</string>
+    <string name="app_desc">適用於 Android 的通用代理工具鏈,由 Kotlin 編寫。</string>
 
     <string name="project">專案</string>
     <string name="github">原始碼</string>
@@ -9,9 +9,9 @@
 
     <string name="misc">雜項</string>
     <string name="app_version">版本</string>
-    <string name="v2ray_version">版本(v2ray-core)</string>
-    <string name="logcat">匯出除錯資訊</string>
-    <string name="logcat_summary">Such useful very wow</string>
+    <string name="v2ray_version">版本 (v2ray-core)</string>
+    <string name="logcat">匯出 debug 所需資訊</string>
+    <string name="logcat_summary">這種非常有用啊(^O^)/</string>
 
     <string name="action_settings">設定</string>
     <string name="menu_configuration">設定檔</string>
@@ -23,7 +23,6 @@
     <string name="profile_type">socks5</string>
 
     <string name="quick_toggle">切換</string>
-    <string name="send_email">傳送電子郵件</string>
 
     <!-- group -->
     <string name="group_name">群組名稱</string>
@@ -43,9 +42,9 @@
     <string name="group_updated">更新了 %d 個代理</string>
     <string name="group_show_diff">變化</string>
     <string name="group_delete_confirm_prompt">您真的希望刪除這個群組嗎?</string>
-    <string name="group_added">新增:\n%s</string>
-    <string name="group_changed">更新:\n%s</string>
-    <string name="group_deleted">刪除:\n%s</string>
+    <string name="group_added">新增:\n%s</string>
+    <string name="group_changed">更新:\n%s</string>
+    <string name="group_deleted">刪除:\n%s</string>
 
     <!-- misc -->
     <string name="service_mode">執行模式</string>
@@ -64,7 +63,7 @@
     <string name="domestic_dns">國內 DNS</string>
     <string name="cag_route">路由設定</string>
     <string name="remote_dns">遠端 DNS</string>
-    <string name="force_ss_rust">不使用 v2ray-core 的 shadowsocks</string>
+    <string name="force_ss_rust">不使用 v2ray 路由 shadowsocks</string>
     <string name="force_ss_sum">強制所有 Shadowsocks 設定檔使用和 Shadowsocks Android 相同的 shadowsocks-rust 後端。</string>
     <string name="inbound_settings">傳入設定</string>
     <string name="general_settings">應用程式設定</string>
@@ -80,7 +79,7 @@
     <string name="speed">%s/s</string>
     <string name="connection_test_pending">檢查連線能力</string>
     <string name="connection_test_testing">測試中……</string>
-    <string name="connection_test_available">成功:HTTP 握手花費 %dms</string>
+    <string name="connection_test_available">成功:HTTPS 握手花費 %dms</string>
     <string name="connection_test_error">檢查網路連線失敗:%s</string>
     <string name="connection_test_fail">網路不可用</string>
     <string name="connection_test_error_status_code">錯誤碼:#%d</string>
@@ -123,34 +122,26 @@
     <string name="route_entry_bypass_lan">略過區域網路</string>
     <string name="route_entry_bypass_chn">略過中國大陸</string>
     <string name="route_entry_bypass_lan_chn">略過區域網路和中國大陸</string>
-    <string name="route_entry_gfwlist">GFW List</string>
-    <string name="route_entry_chinalist">China List</string>
     <string name="proxied_apps">應用程式 VPN 模式</string>
     <string name="proxied_apps_summary">為被選取的應用程式設定 VPN 模式。</string>
     <string name="on">啟用</string>
     <string name="off">停用</string>
-    <string name="search_apps">搜尋中……</string>
+    <string name="search_apps">搜尋…</string>
     <string name="scanning">掃描中……</string>
     <string name="invert_selections">反向選取</string>
     <string name="clear_selections">清除選取</string>
 
-    <string name="proxied_apps_mode">模式</string>
     <string name="bypass_apps">略過</string>
-    <string name="bypass_apps_summary">啟用此選項以略過被選取的應用。</string>
     <string name="show_system_apps">顯示系統應用</string>
-    <string name="show_search">顯示搜尋</string>
 
     <string name="auto_connect">自動連線</string>
     <string name="auto_connect_summary">如果 SagerNet 在開機或應用更新前正在運行,啟用 SagerNet。</string>
-    <string name="direct_boot_aware">允許鎖定螢幕切換</string>
+    <string name="direct_boot_aware">允許與解鎖前啓動</string>
     <string name="direct_boot_aware_summary">您選取的設定檔將受到較少的保護。</string>
     <plurals name="hosts_summary">
         <item quantity="one">1 個主機名稱已被設定</item>
         <item quantity="other">%d 個主機名稱已被設定</item>
     </plurals>
-    <string name="udp_dns">透過 UDP 傳送 DNS 請求</string>
-    <string name="udp_dns_summary">需要伺服器支援轉傳 UDP 流量。</string>
-    <string name="udp_fallback">遞補 UDP</string>
 
     <!-- notification category -->
     <string name="service_vpn">VPN 服務</string>
@@ -168,7 +159,6 @@
 
     <!-- alert category -->
     <string name="profile_empty">請選擇一個設定檔</string>
-    <string name="proxy_empty">代理或密碼必須不為空</string>
     <string name="connect">連線</string>
     <string name="clipboard_empty">剪貼簿為空</string>
 
@@ -179,14 +169,14 @@
     <string name="share">分享</string>
     <string name="add_profile">新增設定檔</string>
     <string name="action_profile">設定檔</string>
-    <string name="action_socks">Socks</string>
+    <string name="action_socks">SOCKS</string>
+    <string name="action_http">HTTP</string>
     <string name="action_shadowsocks">Shadowsocks</string>
     <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">群組</string>
     <string name="action_create_group">空白群組</string>
     <string name="action_from_link">來自訂閱</string>
 
@@ -240,7 +230,6 @@
     <!-- subscriptions -->
     <string name="subscriptions">訂閱</string>
     <string name="cleartext_http_warning">純文字 HTTP 流量並不安全</string>
-    <string name="fetching_subscription">擷取訂閱中……</string>
     <string name="error_title">錯誤</string>
     <string name="no_proxies_found">此連接不包含任何代理</string>
 
@@ -252,13 +241,12 @@
     <string name="plugin_untrusted">警告:此外掛程式並非來自可靠的可信來源。</string>
     <string name="plugin_auto_connect_unlock_only">此外掛程式可能不支援自動連線</string>
 
-    <string name="proxy_cat">Server Settings</string>
-    <string name="feature_cat">Feature Settings</string>
-    <string name="unsaved_changes_prompt">Changes not saved. Do you want to save?</string>
-    <string name="yes">是</string>
-    <string name="no">否</string>
-    <string name="apply">套用</string>
-
+    <string name="proxy_cat">"伺服器設定"</string>
+    <string name="feature_cat">"功能設定"</string>
+    <string name="unsaved_changes_prompt">"要儲存變更嗎?"</string>
+    <string name="yes">"是"</string>
+    <string name="no">"否"</string>
+    <string name="apply">"套用"</string>
     <string name="license">授權協定</string>
 
 </resources>

+ 2 - 13
app/src/main/res/values/strings.xml

@@ -23,7 +23,6 @@
     <string name="profile_type">socks5</string>
 
     <string name="quick_toggle">Toggle</string>
-    <string name="send_email">Send email</string>
 
     <!-- group -->
     <string name="group_name">Group name</string>
@@ -123,8 +122,6 @@
     <string name="route_entry_bypass_lan">Bypass LAN</string>
     <string name="route_entry_bypass_chn">Bypass mainland China</string>
     <string name="route_entry_bypass_lan_chn">Bypass LAN &amp; mainland China</string>
-    <string name="route_entry_gfwlist">GFW List</string>
-    <string name="route_entry_chinalist">China List</string>
     <string name="proxied_apps">Apps VPN mode</string>
     <string name="proxied_apps_summary">Configure VPN mode for selected apps</string>
     <string name="on">On</string>
@@ -134,11 +131,8 @@
     <string name="invert_selections">Invert selections</string>
     <string name="clear_selections">Clear selections</string>
 
-    <string name="proxied_apps_mode">Mode</string>
     <string name="bypass_apps">Bypass</string>
-    <string name="bypass_apps_summary">Enable this option to bypass selected apps</string>
     <string name="show_system_apps">Show system apps</string>
-    <string name="show_search">Show search</string>
 
     <string name="auto_connect">Auto Connect</string>
     <string name="auto_connect_summary">Enable SagerNet on startup/app update if it was running before</string>
@@ -148,9 +142,6 @@
         <item quantity="one">1 hostname configured</item>
         <item quantity="other">%d hostnames configured</item>
     </plurals>
-    <string name="udp_dns">Send DNS over UDP</string>
-    <string name="udp_dns_summary">Requires UDP forwarding on server side</string>
-    <string name="udp_fallback">UDP Fallback</string>
 
     <!-- notification category -->
     <string name="service_vpn">VPN Service</string>
@@ -168,7 +159,6 @@
 
     <!-- alert category -->
     <string name="profile_empty">Please select a profile</string>
-    <string name="proxy_empty">Proxy/Password should not be empty</string>
     <string name="connect">Connect</string>
     <string name="clipboard_empty">Clipboard is empty</string>
 
@@ -179,14 +169,14 @@
     <string name="share">Share</string>
     <string name="add_profile">Add Profile</string>
     <string name="action_profile">Profile</string>
-    <string name="action_socks">Socks</string>
+    <string name="action_socks">SOCKS</string>
+    <string name="action_http">HTTP</string>
     <string name="action_shadowsocks">Shadowsocks</string>
     <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>
     <string name="action_from_link">From subscription</string>
 
@@ -240,7 +230,6 @@
     <!-- subscriptions -->
     <string name="subscriptions">Subscriptions</string>
     <string name="cleartext_http_warning">Cleartext HTTP traffic is insecure</string>
-    <string name="fetching_subscription">Fetching subscription…</string>
     <string name="error_title">Error</string>
     <string name="no_proxies_found">No proxies found in the link</string>
 

+ 43 - 0
app/src/main/res/xml/http_preferences.xml

@@ -0,0 +1,43 @@
+<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:icon="@drawable/ic_baseline_person_24"
+            app:key="serverUsername"
+            app:title="@string/username_opt"
+            app:useSimpleSummaryProvider="true" />
+        <EditTextPreference
+            app:dialogLayout="@layout/layout_password_dialog"
+            app:icon="@drawable/ic_settings_password"
+            app:key="serverPassword"
+            app:title="@string/password_opt" />
+        <SwitchPreference
+            app:icon="@drawable/ic_baseline_https_24"
+            app:key="serverTLS"
+            app:title="@string/tls" />
+        <EditTextPreference
+            app:icon="@drawable/ic_action_copyright"
+            app:key="serverSNI"
+            app:title="@string/sni"
+            app:useSimpleSummaryProvider="true" />
+    </PreferenceCategory>
+
+
+</PreferenceScreen>

+ 10 - 1
app/src/main/res/xml/socks_preferences.xml

@@ -28,11 +28,20 @@
             app:icon="@drawable/ic_settings_password"
             app:key="serverPassword"
             app:title="@string/password_opt" />
-        <CheckBoxPreference
+        <SwitchPreference
             app:icon="@drawable/ic_baseline_radio_button_checked_24"
             app:key="serverUdp"
             app:summary="@string/udp_summary"
             app:title="@string/server_udp" />
+        <SwitchPreference
+            app:icon="@drawable/ic_baseline_https_24"
+            app:key="serverTLS"
+            app:title="@string/tls" />
+        <EditTextPreference
+            app:icon="@drawable/ic_action_copyright"
+            app:key="serverSNI"
+            app:title="@string/sni"
+            app:useSimpleSummaryProvider="true" />
     </PreferenceCategory>