世界 пре 3 година
родитељ
комит
6cff8ea8ca
55 измењених фајлова са 1352 додато и 163 уклоњено
  1. 3 0
      .gitmodules
  2. 2 0
      .idea/dictionaries/sekai.xml
  3. 1 0
      .idea/vcs.xml
  4. 458 0
      app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/17.json
  5. 146 143
      app/src/main/AndroidManifest.xml
  6. 5 0
      app/src/main/java/io/nekohasekai/sagernet/Constants.kt
  7. 32 0
      app/src/main/java/io/nekohasekai/sagernet/bg/proto/V2RayInstance.kt
  8. 5 0
      app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt
  9. 22 1
      app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt
  10. 7 1
      app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt
  11. 7 0
      app/src/main/java/io/nekohasekai/sagernet/fmt/KryoConverters.java
  12. 2 0
      app/src/main/java/io/nekohasekai/sagernet/fmt/PluginEntry.kt
  13. 1 0
      app/src/main/java/io/nekohasekai/sagernet/fmt/TypeMap.kt
  14. 105 0
      app/src/main/java/io/nekohasekai/sagernet/fmt/tuic/TuicBean.java
  15. 64 0
      app/src/main/java/io/nekohasekai/sagernet/fmt/tuic/TuicFmt.kt
  16. 3 0
      app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt
  17. 3 2
      app/src/main/java/io/nekohasekai/sagernet/ui/LogcatFragment.kt
  18. 89 0
      app/src/main/java/io/nekohasekai/sagernet/ui/profile/TuicSettingsActivity.kt
  19. 3 0
      app/src/main/res/menu/add_profile_menu.xml
  20. 23 0
      app/src/main/res/values/arrays.xml
  21. 6 0
      app/src/main/res/values/strings.xml
  22. 68 0
      app/src/main/res/xml/tuic_preferences.xml
  23. 17 14
      bin/init/env.sh
  24. 8 0
      bin/plugin/tuic.sh
  25. 15 0
      bin/plugin/tuic/arm64-v8a.sh
  26. 15 0
      bin/plugin/tuic/armeabi-v7a.sh
  27. 24 0
      bin/plugin/tuic/build.sh
  28. 5 0
      bin/plugin/tuic/end.sh
  29. 9 0
      bin/plugin/tuic/init.sh
  30. 15 0
      bin/plugin/tuic/x86.sh
  31. 16 0
      bin/plugin/tuic/x86_64.sh
  32. 1 0
      bin/rust-linker/linker-wrapper.bat
  33. 39 0
      bin/rust-linker/linker-wrapper.py
  34. 4 0
      bin/rust-linker/linker-wrapper.sh
  35. 5 0
      plugin/tuic/build.gradle.kts
  36. 39 0
      plugin/tuic/src/main/AndroidManifest.xml
  37. BIN
      plugin/tuic/src/main/ic_launcher-playstore.png
  38. 40 0
      plugin/tuic/src/main/java/io/nekohasekai/sagernet/plugin/tuic/BinaryProvider.kt
  39. 22 0
      plugin/tuic/src/main/res/drawable/ic_launcher_foreground.xml
  40. 6 0
      plugin/tuic/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
  41. 6 0
      plugin/tuic/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
  42. BIN
      plugin/tuic/src/main/res/mipmap-hdpi/ic_launcher.png
  43. BIN
      plugin/tuic/src/main/res/mipmap-hdpi/ic_launcher_round.png
  44. BIN
      plugin/tuic/src/main/res/mipmap-mdpi/ic_launcher.png
  45. BIN
      plugin/tuic/src/main/res/mipmap-mdpi/ic_launcher_round.png
  46. BIN
      plugin/tuic/src/main/res/mipmap-xhdpi/ic_launcher.png
  47. BIN
      plugin/tuic/src/main/res/mipmap-xhdpi/ic_launcher_round.png
  48. BIN
      plugin/tuic/src/main/res/mipmap-xxhdpi/ic_launcher.png
  49. BIN
      plugin/tuic/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
  50. BIN
      plugin/tuic/src/main/res/mipmap-xxxhdpi/ic_launcher.png
  51. BIN
      plugin/tuic/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
  52. 4 0
      plugin/tuic/src/main/res/values/ic_launcher_background.xml
  53. 1 0
      plugin/tuic/src/main/rust/tuic
  54. 5 2
      sager.properties
  55. 1 0
      settings.gradle.kts

+ 3 - 0
.gitmodules

@@ -34,3 +34,6 @@
 [submodule "plugin/mieru/src/main/go/mieru"]
 [submodule "plugin/mieru/src/main/go/mieru"]
 	path = plugin/mieru/src/main/go/mieru
 	path = plugin/mieru/src/main/go/mieru
 	url = https://github.com/SagerNet/mieru
 	url = https://github.com/SagerNet/mieru
+[submodule "plugin/tuic/src/main/rust/tuic"]
+	path = plugin/tuic/src/main/rust/tuic
+	url = https://github.com/EAimTY/tuic

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

@@ -20,6 +20,7 @@
       <w>libnaive</w>
       <w>libnaive</w>
       <w>libtrojan</w>
       <w>libtrojan</w>
       <w>loyalsoldier</w>
       <w>loyalsoldier</w>
+      <w>mieru</w>
       <w>naiveproxy</w>
       <w>naiveproxy</w>
       <w>nativeproxy</w>
       <w>nativeproxy</w>
       <w>naïve</w>
       <w>naïve</w>
@@ -38,6 +39,7 @@
       <w>thiz</w>
       <w>thiz</w>
       <w>tproxy</w>
       <w>tproxy</w>
       <w>transproxy</w>
       <w>transproxy</w>
+      <w>tuic</w>
       <w>uplink</w>
       <w>uplink</w>
       <w>utls</w>
       <w>utls</w>
       <w>vless</w>
       <w>vless</w>

+ 1 - 0
.idea/vcs.xml

@@ -19,5 +19,6 @@
     <mapping directory="$PROJECT_DIR$/plugin/pingtunnel/src/main/go/pingtunnel" vcs="Git" />
     <mapping directory="$PROJECT_DIR$/plugin/pingtunnel/src/main/go/pingtunnel" vcs="Git" />
     <mapping directory="$PROJECT_DIR$/plugin/relaybaton/src/main/go/relaybaton" vcs="Git" />
     <mapping directory="$PROJECT_DIR$/plugin/relaybaton/src/main/go/relaybaton" vcs="Git" />
     <mapping directory="$PROJECT_DIR$/plugin/trojan-go/src/main/go/trojan-go" vcs="Git" />
     <mapping directory="$PROJECT_DIR$/plugin/trojan-go/src/main/go/trojan-go" vcs="Git" />
+    <mapping directory="$PROJECT_DIR$/plugin/tuic/src/main/rust/tuic" vcs="Git" />
   </component>
   </component>
 </project>
 </project>

+ 458 - 0
app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/17.json

@@ -0,0 +1,458 @@
+{
+  "formatVersion": 1,
+  "database": {
+    "version": 17,
+    "identityHash": "7d32ef2ec98db74cfef253bb95891d73",
+    "entities": [
+      {
+        "tableName": "proxy_groups",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userOrder` INTEGER NOT NULL, `ungrouped` INTEGER NOT NULL, `name` TEXT, `type` INTEGER NOT NULL, `subscription` BLOB, `order` INTEGER NOT NULL)",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "userOrder",
+            "columnName": "userOrder",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "ungrouped",
+            "columnName": "ungrouped",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "name",
+            "columnName": "name",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "type",
+            "columnName": "type",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "subscription",
+            "columnName": "subscription",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "order",
+            "columnName": "order",
+            "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, `status` INTEGER NOT NULL, `ping` INTEGER NOT NULL, `uuid` TEXT NOT NULL, `error` TEXT, `socksBean` BLOB, `httpBean` BLOB, `ssBean` BLOB, `ssrBean` BLOB, `vmessBean` BLOB, `vlessBean` BLOB, `trojanBean` BLOB, `trojanGoBean` BLOB, `naiveBean` BLOB, `ptBean` BLOB, `rbBean` BLOB, `brookBean` BLOB, `hysteriaBean` BLOB, `mieruBean` BLOB, `tuicBean` BLOB, `sshBean` BLOB, `wgBean` BLOB, `configBean` BLOB, `chainBean` BLOB, `balancerBean` 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": "status",
+            "columnName": "status",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "ping",
+            "columnName": "ping",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "uuid",
+            "columnName": "uuid",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "error",
+            "columnName": "error",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "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
+          },
+          {
+            "fieldPath": "trojanGoBean",
+            "columnName": "trojanGoBean",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "naiveBean",
+            "columnName": "naiveBean",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "ptBean",
+            "columnName": "ptBean",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "rbBean",
+            "columnName": "rbBean",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "brookBean",
+            "columnName": "brookBean",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "hysteriaBean",
+            "columnName": "hysteriaBean",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "mieruBean",
+            "columnName": "mieruBean",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "tuicBean",
+            "columnName": "tuicBean",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "sshBean",
+            "columnName": "sshBean",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "wgBean",
+            "columnName": "wgBean",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "configBean",
+            "columnName": "configBean",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "chainBean",
+            "columnName": "chainBean",
+            "affinity": "BLOB",
+            "notNull": false
+          },
+          {
+            "fieldPath": "balancerBean",
+            "columnName": "balancerBean",
+            "affinity": "BLOB",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [
+          {
+            "name": "groupId",
+            "unique": false,
+            "columnNames": [
+              "groupId"
+            ],
+            "orders": [],
+            "createSql": "CREATE INDEX IF NOT EXISTS `groupId` ON `${TABLE_NAME}` (`groupId`)"
+          }
+        ],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "rules",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `userOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `domains` TEXT NOT NULL, `ip` TEXT NOT NULL, `port` TEXT NOT NULL, `sourcePort` TEXT NOT NULL, `network` TEXT NOT NULL, `source` TEXT NOT NULL, `protocol` TEXT NOT NULL, `attrs` TEXT NOT NULL, `outbound` INTEGER NOT NULL, `reverse` INTEGER NOT NULL, `redirect` TEXT NOT NULL, `packages` TEXT NOT NULL, `ssid` TEXT NOT NULL DEFAULT '', `networkType` TEXT NOT NULL DEFAULT '')",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "name",
+            "columnName": "name",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "userOrder",
+            "columnName": "userOrder",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "enabled",
+            "columnName": "enabled",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "domains",
+            "columnName": "domains",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "ip",
+            "columnName": "ip",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "port",
+            "columnName": "port",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "sourcePort",
+            "columnName": "sourcePort",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "network",
+            "columnName": "network",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "source",
+            "columnName": "source",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "protocol",
+            "columnName": "protocol",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "attrs",
+            "columnName": "attrs",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "outbound",
+            "columnName": "outbound",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "reverse",
+            "columnName": "reverse",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "redirect",
+            "columnName": "redirect",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "packages",
+            "columnName": "packages",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "ssid",
+            "columnName": "ssid",
+            "affinity": "TEXT",
+            "notNull": true,
+            "defaultValue": "''"
+          },
+          {
+            "fieldPath": "networkType",
+            "columnName": "networkType",
+            "affinity": "TEXT",
+            "notNull": true,
+            "defaultValue": "''"
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "stats",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `packageName` TEXT NOT NULL, `tcpConnections` INTEGER NOT NULL, `udpConnections` INTEGER NOT NULL, `uplink` INTEGER NOT NULL, `downlink` INTEGER NOT NULL)",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "packageName",
+            "columnName": "packageName",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "tcpConnections",
+            "columnName": "tcpConnections",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "udpConnections",
+            "columnName": "udpConnections",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "uplink",
+            "columnName": "uplink",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "downlink",
+            "columnName": "downlink",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [
+          {
+            "name": "index_stats_packageName",
+            "unique": true,
+            "columnNames": [
+              "packageName"
+            ],
+            "orders": [],
+            "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_stats_packageName` ON `${TABLE_NAME}` (`packageName`)"
+          }
+        ],
+        "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, '7d32ef2ec98db74cfef253bb95891d73')"
+    ]
+  }
+}

+ 146 - 143
app/src/main/AndroidManifest.xml

@@ -21,7 +21,7 @@
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
     <uses-permission android:name="android.permission.WAKE_LOCK" />
     <uses-permission android:name="android.permission.WAKE_LOCK" />
-<!--    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />-->
+    <!--    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />-->
 
 
     <uses-permission
     <uses-permission
         android:name="android.permission.QUERY_ALL_PACKAGES"
         android:name="android.permission.QUERY_ALL_PACKAGES"
@@ -200,6 +200,9 @@
         <activity
         <activity
             android:name="io.nekohasekai.sagernet.ui.profile.MieruSettingsActivity"
             android:name="io.nekohasekai.sagernet.ui.profile.MieruSettingsActivity"
             android:configChanges="uiMode" />
             android:configChanges="uiMode" />
+        <activity
+            android:name="io.nekohasekai.sagernet.ui.profile.TuicSettingsActivity"
+            android:configChanges="uiMode" />
         <activity
         <activity
             android:name="io.nekohasekai.sagernet.ui.profile.ConfigSettingsActivity"
             android:name="io.nekohasekai.sagernet.ui.profile.ConfigSettingsActivity"
             android:configChanges="uiMode" />
             android:configChanges="uiMode" />
@@ -363,148 +366,148 @@
             </intent-filter>
             </intent-filter>
         </activity>
         </activity>
 
 
-<!--
-        <activity
-            android:name="io.nekohasekai.sagernet.ui.sai.SplitAPKsInstallerActivity"
-            android:configChanges="uiMode"
-            android:excludeFromRecents="true"
-            android:exported="true"
-            android:label="@string/sai"
-            android:launchMode="singleInstance"
-            android:theme="@style/Theme.SagerNet.Translucent">
-
-            <intent-filter>
-                <action android:name="android.intent.action.VIEW" />
-
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
-
-                <data
-                    android:host="*"
-                    android:mimeType="*/*"
-                    android:pathPattern=".*\\.apks"
-                    android:scheme="file" />
-                <data
-                    android:host="*"
-                    android:mimeType="*/*"
-                    android:pathPattern=".*\\..*\\.apks"
-                    android:scheme="file" />
-                <data
-                    android:host="*"
-                    android:mimeType="*/*"
-                    android:pathPattern=".*\\..*\\..*\\.apks"
-                    android:scheme="file" />
-                <data
-                    android:host="*"
-                    android:mimeType="*/*"
-                    android:pathPattern=".*\\..*\\..*\\..*\\.apks"
-                    android:scheme="file" />
-                <data
-                    android:host="*"
-                    android:mimeType="*/*"
-                    android:pathPattern=".*\\..*\\..*\\..*\\..*\\.apks"
-                    android:scheme="file" />
-
-                <data
-                    android:host="*"
-                    android:mimeType="*/*"
-                    android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.apks"
-                    android:scheme="file" />
-
-                <data
-                    android:host="*"
-                    android:mimeType="*/*"
-                    android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.apks"
-                    android:scheme="file" />
-
-                <data
-                    android:host="*"
-                    android:mimeType="*/*"
-                    android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.apks"
-                    android:scheme="file" />
-
-                <data
-                    android:host="*"
-                    android:mimeType="*/*"
-                    android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.apks"
-                    android:scheme="file" />
-
-                <data
-                    android:host="*"
-                    android:mimeType="*/*"
-                    android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.apks"
-                    android:scheme="file" />
-
-                <data
-                    android:host="*"
-                    android:mimeType="*/*"
-                    android:pathPattern=".*\\.apks"
-                    android:scheme="content" />
-                <data
-                    android:host="*"
-                    android:mimeType="*/*"
-                    android:pathPattern=".*\\..*\\.apks"
-                    android:scheme="content" />
-                <data
-                    android:host="*"
-                    android:mimeType="*/*"
-                    android:pathPattern=".*\\..*\\..*\\.apks"
-                    android:scheme="content" />
-                <data
-                    android:host="*"
-                    android:mimeType="*/*"
-                    android:pathPattern=".*\\..*\\..*\\..*\\.apks"
-                    android:scheme="content" />
-                <data
-                    android:host="*"
-                    android:mimeType="*/*"
-                    android:pathPattern=".*\\..*\\..*\\..*\\..*\\.apks"
-                    android:scheme="content" />
-
-                <data
-                    android:host="*"
-                    android:mimeType="*/*"
-                    android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.apks"
-                    android:scheme="content" />
-
-                <data
-                    android:host="*"
-                    android:mimeType="*/*"
-                    android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.apks"
-                    android:scheme="content" />
-
-                <data
-                    android:host="*"
-                    android:mimeType="*/*"
-                    android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.apks"
-                    android:scheme="content" />
-
-                <data
-                    android:host="*"
-                    android:mimeType="*/*"
-                    android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.apks"
-                    android:scheme="content" />
-
-                <data
-                    android:host="*"
-                    android:mimeType="*/*"
-                    android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.apks"
-                    android:scheme="content" />
-            </intent-filter>
-        </activity>
-
-        <service
-            android:name=".ui.sai.SplitAPKsInstallerService"
-            android:exported="false" />
-
-        <provider
-            android:name="rikka.shizuku.ShizukuProvider"
-            android:authorities="${applicationId}.shizuku"
-            android:enabled="true"
-            android:exported="true"
-            android:multiprocess="false"
-            android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
--->
+        <!--
+                <activity
+                    android:name="io.nekohasekai.sagernet.ui.sai.SplitAPKsInstallerActivity"
+                    android:configChanges="uiMode"
+                    android:excludeFromRecents="true"
+                    android:exported="true"
+                    android:label="@string/sai"
+                    android:launchMode="singleInstance"
+                    android:theme="@style/Theme.SagerNet.Translucent">
+
+                    <intent-filter>
+                        <action android:name="android.intent.action.VIEW" />
+
+                        <category android:name="android.intent.category.DEFAULT" />
+                        <category android:name="android.intent.category.BROWSABLE" />
+
+                        <data
+                            android:host="*"
+                            android:mimeType="*/*"
+                            android:pathPattern=".*\\.apks"
+                            android:scheme="file" />
+                        <data
+                            android:host="*"
+                            android:mimeType="*/*"
+                            android:pathPattern=".*\\..*\\.apks"
+                            android:scheme="file" />
+                        <data
+                            android:host="*"
+                            android:mimeType="*/*"
+                            android:pathPattern=".*\\..*\\..*\\.apks"
+                            android:scheme="file" />
+                        <data
+                            android:host="*"
+                            android:mimeType="*/*"
+                            android:pathPattern=".*\\..*\\..*\\..*\\.apks"
+                            android:scheme="file" />
+                        <data
+                            android:host="*"
+                            android:mimeType="*/*"
+                            android:pathPattern=".*\\..*\\..*\\..*\\..*\\.apks"
+                            android:scheme="file" />
+
+                        <data
+                            android:host="*"
+                            android:mimeType="*/*"
+                            android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.apks"
+                            android:scheme="file" />
+
+                        <data
+                            android:host="*"
+                            android:mimeType="*/*"
+                            android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.apks"
+                            android:scheme="file" />
+
+                        <data
+                            android:host="*"
+                            android:mimeType="*/*"
+                            android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.apks"
+                            android:scheme="file" />
+
+                        <data
+                            android:host="*"
+                            android:mimeType="*/*"
+                            android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.apks"
+                            android:scheme="file" />
+
+                        <data
+                            android:host="*"
+                            android:mimeType="*/*"
+                            android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.apks"
+                            android:scheme="file" />
+
+                        <data
+                            android:host="*"
+                            android:mimeType="*/*"
+                            android:pathPattern=".*\\.apks"
+                            android:scheme="content" />
+                        <data
+                            android:host="*"
+                            android:mimeType="*/*"
+                            android:pathPattern=".*\\..*\\.apks"
+                            android:scheme="content" />
+                        <data
+                            android:host="*"
+                            android:mimeType="*/*"
+                            android:pathPattern=".*\\..*\\..*\\.apks"
+                            android:scheme="content" />
+                        <data
+                            android:host="*"
+                            android:mimeType="*/*"
+                            android:pathPattern=".*\\..*\\..*\\..*\\.apks"
+                            android:scheme="content" />
+                        <data
+                            android:host="*"
+                            android:mimeType="*/*"
+                            android:pathPattern=".*\\..*\\..*\\..*\\..*\\.apks"
+                            android:scheme="content" />
+
+                        <data
+                            android:host="*"
+                            android:mimeType="*/*"
+                            android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.apks"
+                            android:scheme="content" />
+
+                        <data
+                            android:host="*"
+                            android:mimeType="*/*"
+                            android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.apks"
+                            android:scheme="content" />
+
+                        <data
+                            android:host="*"
+                            android:mimeType="*/*"
+                            android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.apks"
+                            android:scheme="content" />
+
+                        <data
+                            android:host="*"
+                            android:mimeType="*/*"
+                            android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.apks"
+                            android:scheme="content" />
+
+                        <data
+                            android:host="*"
+                            android:mimeType="*/*"
+                            android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.apks"
+                            android:scheme="content" />
+                    </intent-filter>
+                </activity>
+
+                <service
+                    android:name=".ui.sai.SplitAPKsInstallerService"
+                    android:exported="false" />
+
+                <provider
+                    android:name="rikka.shizuku.ShizukuProvider"
+                    android:authorities="${applicationId}.shizuku"
+                    android:enabled="true"
+                    android:exported="true"
+                    android:multiprocess="false"
+                    android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
+        -->
 
 
     </application>
     </application>
 
 

+ 5 - 0
app/src/main/java/io/nekohasekai/sagernet/Constants.kt

@@ -167,6 +167,11 @@ object Key {
     const val SERVER_GRPC_MODE = "serverGrpcMode"
     const val SERVER_GRPC_MODE = "serverGrpcMode"
     const val SERVER_ENCRYPTED_PROTOCOL_EXTENSION = "serverEncryptedProtocolExtension"
     const val SERVER_ENCRYPTED_PROTOCOL_EXTENSION = "serverEncryptedProtocolExtension"
 
 
+    const val SERVER_UDP_RELAY_MODE = "serverUDPRelayMode"
+    const val SERVER_CONGESTION_CONTROLLER = "serverCongestionController"
+    const val SERVER_DISABLE_SNI = "serverDisableSNI"
+    const val SERVER_REDUCE_RTT= "serverReduceRTT"
+
     const val BALANCER_TYPE = "balancerType"
     const val BALANCER_TYPE = "balancerType"
     const val BALANCER_GROUP = "balancerGroup"
     const val BALANCER_GROUP = "balancerGroup"
     const val BALANCER_STRATEGY = "balancerStrategy"
     const val BALANCER_STRATEGY = "balancerStrategy"

+ 32 - 0
app/src/main/java/io/nekohasekai/sagernet/bg/proto/V2RayInstance.kt

@@ -56,6 +56,8 @@ import io.nekohasekai.sagernet.fmt.trojan.buildTrojanGoConfig
 import io.nekohasekai.sagernet.fmt.trojan_go.TrojanGoBean
 import io.nekohasekai.sagernet.fmt.trojan_go.TrojanGoBean
 import io.nekohasekai.sagernet.fmt.trojan_go.buildCustomTrojanConfig
 import io.nekohasekai.sagernet.fmt.trojan_go.buildCustomTrojanConfig
 import io.nekohasekai.sagernet.fmt.trojan_go.buildTrojanGoConfig
 import io.nekohasekai.sagernet.fmt.trojan_go.buildTrojanGoConfig
+import io.nekohasekai.sagernet.fmt.tuic.TuicBean
+import io.nekohasekai.sagernet.fmt.tuic.buildTuicConfig
 import io.nekohasekai.sagernet.ktx.*
 import io.nekohasekai.sagernet.ktx.*
 import io.nekohasekai.sagernet.plugin.PluginManager
 import io.nekohasekai.sagernet.plugin.PluginManager
 import kotlinx.coroutines.*
 import kotlinx.coroutines.*
@@ -148,6 +150,18 @@ abstract class V2RayInstance(
                         initPlugin("mieru-plugin")
                         initPlugin("mieru-plugin")
                         pluginConfigs[port] = profile.type to bean.buildMieruConfig(port)
                         pluginConfigs[port] = profile.type to bean.buildMieruConfig(port)
                     }
                     }
+                    is TuicBean -> {
+                        initPlugin("tuic-plugin")
+                        pluginConfigs[port] = profile.type to bean.buildTuicConfig(port) {
+                            File(
+                                app.noBackupFilesDir,
+                                "tuic_" + SystemClock.elapsedRealtime() + ".ca"
+                            ).apply {
+                                parentFile?.mkdirs()
+                                cacheFiles.add(this)
+                            }
+                        }
+                    }
                     is ConfigBean -> {
                     is ConfigBean -> {
                         when (bean.type) {
                         when (bean.type) {
                             "trojan-go" -> {
                             "trojan-go" -> {
@@ -370,6 +384,24 @@ abstract class V2RayInstance(
                             initPlugin("mieru-plugin").path, "run_plugin"
                             initPlugin("mieru-plugin").path, "run_plugin"
                         )
                         )
 
 
+                        processes.start(commands, env)
+                    }
+                    bean is TuicBean -> {
+                        val configFile = File(
+                            context.noBackupFilesDir,
+                            "tuic_" + SystemClock.elapsedRealtime() + ".json"
+                        )
+
+                        configFile.parentFile?.mkdirs()
+                        configFile.writeText(config)
+                        cacheFiles.add(configFile)
+
+                        val commands = mutableListOf(
+                            initPlugin("tuic-plugin").path,
+                            "-c",
+                            configFile.absolutePath,
+                        )
+
                         processes.start(commands, env)
                         processes.start(commands, env)
                     }
                     }
                 }
                 }

+ 5 - 0
app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt

@@ -277,6 +277,11 @@ object DataStore : OnPreferenceDataStoreChangeListener {
     var serverGrpcMode by profileCacheStore.string(Key.SERVER_GRPC_MODE)
     var serverGrpcMode by profileCacheStore.string(Key.SERVER_GRPC_MODE)
     var serverEncryptedProtocolExtension by profileCacheStore.boolean(Key.SERVER_ENCRYPTED_PROTOCOL_EXTENSION)
     var serverEncryptedProtocolExtension by profileCacheStore.boolean(Key.SERVER_ENCRYPTED_PROTOCOL_EXTENSION)
 
 
+    var serverUDPRelayMode by profileCacheStore.string(Key.SERVER_UDP_RELAY_MODE)
+    var serverCongestionController by profileCacheStore.string(Key.SERVER_CONGESTION_CONTROLLER)
+    var serverDisableSNI by profileCacheStore.boolean(Key.SERVER_DISABLE_SNI)
+    var serverReduceRTT by profileCacheStore.boolean(Key.SERVER_REDUCE_RTT)
+
     var balancerType by profileCacheStore.stringToInt(Key.BALANCER_TYPE)
     var balancerType by profileCacheStore.stringToInt(Key.BALANCER_TYPE)
     var balancerGroup by profileCacheStore.stringToLong(Key.BALANCER_GROUP)
     var balancerGroup by profileCacheStore.stringToLong(Key.BALANCER_GROUP)
     var balancerStrategy by profileCacheStore.string(Key.BALANCER_STRATEGY)
     var balancerStrategy by profileCacheStore.string(Key.BALANCER_STRATEGY)

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

@@ -64,6 +64,8 @@ import io.nekohasekai.sagernet.fmt.trojan.toUri
 import io.nekohasekai.sagernet.fmt.trojan_go.TrojanGoBean
 import io.nekohasekai.sagernet.fmt.trojan_go.TrojanGoBean
 import io.nekohasekai.sagernet.fmt.trojan_go.buildTrojanGoConfig
 import io.nekohasekai.sagernet.fmt.trojan_go.buildTrojanGoConfig
 import io.nekohasekai.sagernet.fmt.trojan_go.toUri
 import io.nekohasekai.sagernet.fmt.trojan_go.toUri
+import io.nekohasekai.sagernet.fmt.tuic.TuicBean
+import io.nekohasekai.sagernet.fmt.tuic.buildTuicConfig
 import io.nekohasekai.sagernet.fmt.v2ray.StandardV2RayBean
 import io.nekohasekai.sagernet.fmt.v2ray.StandardV2RayBean
 import io.nekohasekai.sagernet.fmt.v2ray.VLESSBean
 import io.nekohasekai.sagernet.fmt.v2ray.VLESSBean
 import io.nekohasekai.sagernet.fmt.v2ray.VMessBean
 import io.nekohasekai.sagernet.fmt.v2ray.VMessBean
@@ -102,6 +104,7 @@ data class ProxyEntity(
     var brookBean: BrookBean? = null,
     var brookBean: BrookBean? = null,
     var hysteriaBean: HysteriaBean? = null,
     var hysteriaBean: HysteriaBean? = null,
     var mieruBean: MieruBean? = null,
     var mieruBean: MieruBean? = null,
+    var tuicBean: TuicBean? = null,
     var sshBean: SSHBean? = null,
     var sshBean: SSHBean? = null,
     var wgBean: WireGuardBean? = null,
     var wgBean: WireGuardBean? = null,
     var configBean: ConfigBean? = null,
     var configBean: ConfigBean? = null,
@@ -127,6 +130,7 @@ data class ProxyEntity(
         const val TYPE_SSH = 17
         const val TYPE_SSH = 17
         const val TYPE_WG = 18
         const val TYPE_WG = 18
         const val TYPE_MIERU = 19
         const val TYPE_MIERU = 19
+        const val TYPE_TUIC = 20
 
 
         const val TYPE_CHAIN = 8
         const val TYPE_CHAIN = 8
         const val TYPE_BALANCER = 14
         const val TYPE_BALANCER = 14
@@ -220,6 +224,7 @@ data class ProxyEntity(
             TYPE_SSH -> sshBean = KryoConverters.sshDeserialize(byteArray)
             TYPE_SSH -> sshBean = KryoConverters.sshDeserialize(byteArray)
             TYPE_WG -> wgBean = KryoConverters.wireguardDeserialize(byteArray)
             TYPE_WG -> wgBean = KryoConverters.wireguardDeserialize(byteArray)
             TYPE_MIERU -> mieruBean = KryoConverters.mieruDeserialize(byteArray)
             TYPE_MIERU -> mieruBean = KryoConverters.mieruDeserialize(byteArray)
+            TYPE_TUIC -> tuicBean = KryoConverters.tuicDeserialize(byteArray)
 
 
             TYPE_CONFIG -> configBean = KryoConverters.configDeserialize(byteArray)
             TYPE_CONFIG -> configBean = KryoConverters.configDeserialize(byteArray)
             TYPE_CHAIN -> chainBean = KryoConverters.chainDeserialize(byteArray)
             TYPE_CHAIN -> chainBean = KryoConverters.chainDeserialize(byteArray)
@@ -245,6 +250,7 @@ data class ProxyEntity(
         TYPE_SSH -> "SSH"
         TYPE_SSH -> "SSH"
         TYPE_WG -> "WireGuard"
         TYPE_WG -> "WireGuard"
         TYPE_MIERU -> "Mieru"
         TYPE_MIERU -> "Mieru"
+        TYPE_TUIC -> "TUIC"
 
 
         TYPE_CHAIN -> chainName
         TYPE_CHAIN -> chainName
         TYPE_CONFIG -> configName
         TYPE_CONFIG -> configName
@@ -273,6 +279,7 @@ data class ProxyEntity(
             TYPE_SSH -> sshBean
             TYPE_SSH -> sshBean
             TYPE_WG -> wgBean
             TYPE_WG -> wgBean
             TYPE_MIERU -> mieruBean
             TYPE_MIERU -> mieruBean
+            TYPE_TUIC -> tuicBean
 
 
             TYPE_CONFIG -> configBean
             TYPE_CONFIG -> configBean
             TYPE_CHAIN -> chainBean
             TYPE_CHAIN -> chainBean
@@ -291,7 +298,7 @@ data class ProxyEntity(
 
 
     fun haveStandardLink(): Boolean {
     fun haveStandardLink(): Boolean {
         return haveLink() && when (type) {
         return haveLink() && when (type) {
-            TYPE_RELAY_BATON, TYPE_BROOK, TYPE_SSH, TYPE_WG, TYPE_HYSTERIA, TYPE_MIERU -> false
+            TYPE_RELAY_BATON, TYPE_BROOK, TYPE_SSH, TYPE_WG, TYPE_HYSTERIA, TYPE_MIERU, TYPE_TUIC -> false
             TYPE_CONFIG -> false
             TYPE_CONFIG -> false
             else -> true
             else -> true
         }
         }
@@ -317,6 +324,7 @@ data class ProxyEntity(
             is WireGuardBean -> toUniversalLink()
             is WireGuardBean -> toUniversalLink()
             is HysteriaBean -> toUniversalLink()
             is HysteriaBean -> toUniversalLink()
             is MieruBean -> toUniversalLink()
             is MieruBean -> toUniversalLink()
+            is TuicBean -> toUniversalLink()
             else -> null
             else -> null
         }
         }
     }
     }
@@ -369,6 +377,12 @@ data class ProxyEntity(
                                     Logs.d(it)
                                     Logs.d(it)
                                 })
                                 })
                             }
                             }
+                            is TuicBean -> {
+                                append("\n\n")
+                                append(bean.buildTuicConfig(port, null).also {
+                                    Logs.d(it)
+                                })
+                            }
                         }
                         }
                     }
                     }
                 }
                 }
@@ -386,6 +400,7 @@ data class ProxyEntity(
             TYPE_RELAY_BATON -> true
             TYPE_RELAY_BATON -> true
             TYPE_BROOK -> true
             TYPE_BROOK -> true
             TYPE_MIERU -> true
             TYPE_MIERU -> true
+            TYPE_TUIC -> true
 
 
             TYPE_CONFIG -> true
             TYPE_CONFIG -> true
             else -> false
             else -> false
@@ -433,6 +448,7 @@ data class ProxyEntity(
         sshBean = null
         sshBean = null
         wgBean = null
         wgBean = null
         mieruBean = null
         mieruBean = null
+        tuicBean = null
 
 
         configBean = null
         configBean = null
         chainBean = null
         chainBean = null
@@ -503,6 +519,10 @@ data class ProxyEntity(
                 type = TYPE_MIERU
                 type = TYPE_MIERU
                 mieruBean = bean
                 mieruBean = bean
             }
             }
+            is TuicBean -> {
+                type = TYPE_TUIC
+                tuicBean = bean
+            }
 
 
             is ConfigBean -> {
             is ConfigBean -> {
                 type = TYPE_CONFIG
                 type = TYPE_CONFIG
@@ -540,6 +560,7 @@ data class ProxyEntity(
                 TYPE_SSH -> SSHSettingsActivity::class.java
                 TYPE_SSH -> SSHSettingsActivity::class.java
                 TYPE_WG -> WireGuardSettingsActivity::class.java
                 TYPE_WG -> WireGuardSettingsActivity::class.java
                 TYPE_MIERU -> MieruSettingsActivity::class.java
                 TYPE_MIERU -> MieruSettingsActivity::class.java
+                TYPE_TUIC -> TuicSettingsActivity::class.java
 
 
                 TYPE_CONFIG -> ConfigSettingsActivity::class.java
                 TYPE_CONFIG -> ConfigSettingsActivity::class.java
                 TYPE_CHAIN -> ChainSettingsActivity::class.java
                 TYPE_CHAIN -> ChainSettingsActivity::class.java

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

@@ -31,7 +31,7 @@ import kotlinx.coroutines.launch
 
 
 @Database(
 @Database(
     entities = [ProxyGroup::class, ProxyEntity::class, RuleEntity::class, StatsEntity::class],
     entities = [ProxyGroup::class, ProxyEntity::class, RuleEntity::class, StatsEntity::class],
-    version = 16,
+    version = 17,
     autoMigrations = [AutoMigration(
     autoMigrations = [AutoMigration(
         from = 12,
         from = 12,
         to = 14,
         to = 14,
@@ -40,6 +40,12 @@ import kotlinx.coroutines.launch
     ), AutoMigration(
     ), AutoMigration(
         from = 15,
         from = 15,
         to = 16,
         to = 16,
+    ), AutoMigration(
+        from = 16,
+        to = 17,
+    ), AutoMigration(
+        from = 15,
+        to = 17,
     )]
     )]
 )
 )
 @TypeConverters(value = [KryoConverters::class, GsonConverters::class])
 @TypeConverters(value = [KryoConverters::class, GsonConverters::class])

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

@@ -47,6 +47,7 @@ import io.nekohasekai.sagernet.fmt.socks.SOCKSBean;
 import io.nekohasekai.sagernet.fmt.ssh.SSHBean;
 import io.nekohasekai.sagernet.fmt.ssh.SSHBean;
 import io.nekohasekai.sagernet.fmt.trojan.TrojanBean;
 import io.nekohasekai.sagernet.fmt.trojan.TrojanBean;
 import io.nekohasekai.sagernet.fmt.trojan_go.TrojanGoBean;
 import io.nekohasekai.sagernet.fmt.trojan_go.TrojanGoBean;
+import io.nekohasekai.sagernet.fmt.tuic.TuicBean;
 import io.nekohasekai.sagernet.fmt.v2ray.VLESSBean;
 import io.nekohasekai.sagernet.fmt.v2ray.VLESSBean;
 import io.nekohasekai.sagernet.fmt.v2ray.VMessBean;
 import io.nekohasekai.sagernet.fmt.v2ray.VMessBean;
 import io.nekohasekai.sagernet.fmt.wireguard.WireGuardBean;
 import io.nekohasekai.sagernet.fmt.wireguard.WireGuardBean;
@@ -177,6 +178,12 @@ public class KryoConverters {
         return deserialize(new MieruBean(), bytes);
         return deserialize(new MieruBean(), bytes);
     }
     }
 
 
+    @TypeConverter
+    public static TuicBean tuicDeserialize(byte[] bytes) {
+        if (ArrayUtil.isEmpty(bytes)) return null;
+        return deserialize(new TuicBean(), bytes);
+    }
+
     @TypeConverter
     @TypeConverter
     public static ConfigBean configDeserialize(byte[] bytes) {
     public static ConfigBean configDeserialize(byte[] bytes) {
         if (ArrayUtil.isEmpty(bytes)) return null;
         if (ArrayUtil.isEmpty(bytes)) return null;

+ 2 - 0
app/src/main/java/io/nekohasekai/sagernet/fmt/PluginEntry.kt

@@ -35,6 +35,8 @@ enum class PluginEntry(
     RelayBaton("relaybaton-plugin", R.string.action_relay_baton, "io.nekohasekai.sagernet.plugin.relaybaton"),
     RelayBaton("relaybaton-plugin", R.string.action_relay_baton, "io.nekohasekai.sagernet.plugin.relaybaton"),
     Brook("brook-plugin", R.string.action_brook, "io.nekohasekai.sagernet.plugin.brook"),
     Brook("brook-plugin", R.string.action_brook, "io.nekohasekai.sagernet.plugin.brook"),
     Hysteria("hysteria-plugin", R.string.action_hysteria, "io.nekohasekai.sagernet.plugin.hysteria", DownloadSource(fdroid = false)),
     Hysteria("hysteria-plugin", R.string.action_hysteria, "io.nekohasekai.sagernet.plugin.hysteria", DownloadSource(fdroid = false)),
+    Mieru("mieru-plugin", R.string.action_mieru, "io.nekohasekai.sagernet.plugin.mieru", DownloadSource(fdroid = false)),
+    TUIC("tuic-plugin", R.string.action_tuic, "io.nekohasekai.sagernet.plugin.tuic", DownloadSource(fdroid = false)),
 
 
     // shadowsocks plugins
     // shadowsocks plugins
 
 

+ 1 - 0
app/src/main/java/io/nekohasekai/sagernet/fmt/TypeMap.kt

@@ -40,6 +40,7 @@ object TypeMap : HashMap<String, Int>() {
         this["ssh"] = ProxyEntity.TYPE_SSH
         this["ssh"] = ProxyEntity.TYPE_SSH
         this["wg"] = ProxyEntity.TYPE_WG
         this["wg"] = ProxyEntity.TYPE_WG
         this["mieru"] = ProxyEntity.TYPE_MIERU
         this["mieru"] = ProxyEntity.TYPE_MIERU
+        this["tuic"] = ProxyEntity.TYPE_TUIC
     }
     }
 
 
     val reversed = HashMap<Int, String>()
     val reversed = HashMap<Int, String>()

+ 105 - 0
app/src/main/java/io/nekohasekai/sagernet/fmt/tuic/TuicBean.java

@@ -0,0 +1,105 @@
+/******************************************************************************
+ * Copyright (C) 2022 by nekohasekai <[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.tuic;
+
+import androidx.annotation.NonNull;
+
+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 TuicBean extends AbstractBean {
+
+    public String token;
+    public String caText;
+    public String udpRelayMode;
+    public String congestionController;
+    public String alpn;
+    public Boolean disableSNI;
+    public Boolean reduceRTT;
+    public Integer mtu;
+    public String sni;
+
+    @Override
+    public void initializeDefaultValues() {
+        super.initializeDefaultValues();
+        if (token == null) token = "";
+        if (caText == null) caText = "";
+        if (udpRelayMode == null) udpRelayMode = "native";
+        if (congestionController == null) congestionController = "cubic";
+        if (alpn == null) alpn = "";
+        if (disableSNI == null) disableSNI = false;
+        if (reduceRTT == null) reduceRTT = false;
+        if (mtu == null) mtu = 1400;
+        if (sni == null) sni = "";
+    }
+
+    @Override
+    public void serialize(ByteBufferOutput output) {
+        output.writeInt(0);
+        super.serialize(output);
+        output.writeString(token);
+        output.writeString(caText);
+        output.writeString(udpRelayMode);
+        output.writeString(congestionController);
+        output.writeString(alpn);
+        output.writeBoolean(disableSNI);
+        output.writeBoolean(reduceRTT);
+        output.writeInt(mtu);
+        output.writeString(sni);
+    }
+
+    @Override
+    public void deserialize(ByteBufferInput input) {
+        int version = input.readInt();
+        super.deserialize(input);
+        token = input.readString();
+        caText = input.readString();
+        udpRelayMode = input.readString();
+        congestionController = input.readString();
+        alpn = input.readString();
+        disableSNI = input.readBoolean();
+        reduceRTT = input.readBoolean();
+        mtu = input.readInt();
+        sni = input.readString();
+    }
+
+    @NotNull
+    @Override
+    public TuicBean clone() {
+        return KryoConverters.deserialize(new TuicBean(), KryoConverters.serialize(this));
+    }
+
+    public static final Creator<TuicBean> CREATOR = new CREATOR<TuicBean>() {
+        @NonNull
+        @Override
+        public TuicBean newInstance() {
+            return new TuicBean();
+        }
+
+        @Override
+        public TuicBean[] newArray(int size) {
+            return new TuicBean[size];
+        }
+    };
+}

+ 64 - 0
app/src/main/java/io/nekohasekai/sagernet/fmt/tuic/TuicFmt.kt

@@ -0,0 +1,64 @@
+/******************************************************************************
+ * Copyright (C) 2022 by nekohasekai <[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.tuic
+
+import cn.hutool.json.JSONArray
+import cn.hutool.json.JSONObject
+import io.nekohasekai.sagernet.database.DataStore
+import io.nekohasekai.sagernet.fmt.LOCALHOST
+import io.nekohasekai.sagernet.ktx.isIpAddress
+import java.io.File
+
+fun TuicBean.buildTuicConfig(port: Int, cacheFile: (() -> File)?): String {
+    return JSONObject().also {
+        it["relay"] = JSONObject().also {
+            if (serverAddress.isIpAddress()) {
+                it["server"] = finalAddress
+            } else if (sni.isNotBlank()) {
+                it["server"] = sni
+                it["ip"] = finalAddress
+            } else {
+                it["server"] = serverAddress
+                it["ip"] = finalAddress
+            }
+            it["port"] = finalPort
+            it["token"] = token
+
+            if (caText.isNotBlank() && cacheFile != null) {
+                val caFile = cacheFile()
+                caFile.writeText(caText)
+                it["certificate"] = caFile.absolutePath
+            }
+
+            it["udp_relay_mode"] = udpRelayMode
+            if (alpn.isNotBlank()) {
+                it["alpn"] = JSONArray(alpn.split("\n"))
+            }
+            it["congestion_controller"] = congestionController
+            it["disable_sni"] = disableSNI
+            it["reduce_rtt"] = reduceRTT
+            it["max_udp_relay_packet_size"] = mtu
+        }
+        it["local"] = JSONObject().also {
+            it["ip"] = LOCALHOST
+            it["port"] = port
+        }
+        it["log_level"] = if (DataStore.enableLog) "debug" else "info"
+    }.toStringPretty()
+}

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

@@ -337,6 +337,9 @@ class ConfigurationFragment @JvmOverloads constructor(
             R.id.action_new_mieru -> {
             R.id.action_new_mieru -> {
                 startActivity(Intent(requireActivity(), MieruSettingsActivity::class.java))
                 startActivity(Intent(requireActivity(), MieruSettingsActivity::class.java))
             }
             }
+            R.id.action_new_tuic -> {
+                startActivity(Intent(requireActivity(), TuicSettingsActivity::class.java))
+            }
             R.id.action_new_ssh -> {
             R.id.action_new_ssh -> {
                 startActivity(Intent(requireActivity(), SSHSettingsActivity::class.java))
                 startActivity(Intent(requireActivity(), SSHSettingsActivity::class.java))
             }
             }

+ 3 - 2
app/src/main/java/io/nekohasekai/sagernet/ui/LogcatFragment.kt

@@ -117,6 +117,8 @@ class LogcatFragment : ToolbarFragment(R.layout.layout_logcat),
                 "libbrook:D",
                 "libbrook:D",
                 "libhysteria:D",
                 "libhysteria:D",
                 "librelaybaton:D",
                 "librelaybaton:D",
+                "libmieru:D",
+                "libtuic:D",
                 "*:S",
                 "*:S",
             )
             )
         }
         }
@@ -155,8 +157,7 @@ class LogcatFragment : ToolbarFragment(R.layout.layout_logcat),
                 val context = requireContext()
                 val context = requireContext()
 
 
                 runOnDefaultDispatcher {
                 runOnDefaultDispatcher {
-                    val logFile = File.createTempFile(
-                        "SagerNet ",
+                    val logFile = File.createTempFile("SagerNet ",
                         ".log",
                         ".log",
                         File(app.cacheDir, "log").also { it.mkdirs() })
                         File(app.cacheDir, "log").also { it.mkdirs() })
 
 

+ 89 - 0
app/src/main/java/io/nekohasekai/sagernet/ui/profile/TuicSettingsActivity.kt

@@ -0,0 +1,89 @@
+/******************************************************************************
+ *                                                                            *
+ * Copyright (C) 2021 by nekohasekai <[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 android.widget.Switch
+import androidx.preference.EditTextPreference
+import androidx.preference.SwitchPreference
+import com.takisoft.preferencex.PreferenceFragmentCompat
+import com.takisoft.preferencex.SimpleMenuPreference
+import io.nekohasekai.sagernet.Key
+import io.nekohasekai.sagernet.R
+import io.nekohasekai.sagernet.database.DataStore
+import io.nekohasekai.sagernet.fmt.mieru.MieruBean
+import io.nekohasekai.sagernet.fmt.tuic.TuicBean
+import io.nekohasekai.sagernet.ktx.applyDefaultValues
+
+class TuicSettingsActivity : ProfileSettingsActivity<TuicBean>() {
+
+    override fun createEntity() = TuicBean().applyDefaultValues()
+
+    override fun TuicBean.init() {
+        DataStore.profileName = name
+        DataStore.serverAddress = serverAddress
+        DataStore.serverPort = serverPort
+        DataStore.serverPassword = token
+        DataStore.serverALPN = alpn
+        DataStore.serverCertificates = caText
+        DataStore.serverUDPRelayMode = udpRelayMode
+        DataStore.serverCongestionController = congestionController
+        DataStore.serverDisableSNI = disableSNI
+        DataStore.serverSNI = sni
+        DataStore.serverReduceRTT = reduceRTT
+        DataStore.serverMTU = mtu
+
+    }
+
+    override fun TuicBean.serialize() {
+        name = DataStore.profileName
+        serverAddress = DataStore.serverAddress
+        serverPort = DataStore.serverPort
+        token = DataStore.serverPassword
+        alpn = DataStore.serverALPN
+        caText = DataStore.serverCertificates
+        udpRelayMode = DataStore.serverUDPRelayMode
+        congestionController = DataStore.serverCongestionController
+        disableSNI = DataStore.serverDisableSNI
+        sni = DataStore.serverSNI
+        reduceRTT = DataStore.serverReduceRTT
+        mtu = DataStore.serverMTU
+    }
+
+    override fun PreferenceFragmentCompat.createPreferences(
+        savedInstanceState: Bundle?,
+        rootKey: String?,
+    ) {
+        addPreferencesFromResource(R.xml.tuic_preferences)
+
+        val disableSNI = findPreference<SwitchPreference>(Key.SERVER_DISABLE_SNI)!!
+        val sni = findPreference<EditTextPreference>(Key.SERVER_SNI)!!
+        sni.isEnabled = !disableSNI.isChecked
+        disableSNI.setOnPreferenceChangeListener { _, newValue ->
+            sni.isEnabled = !(newValue as Boolean)
+            true
+        }
+
+        findPreference<EditTextPreference>(Key.SERVER_PASSWORD)!!.apply {
+            summaryProvider = PasswordSummaryProvider
+        }
+    }
+
+}

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

@@ -60,6 +60,9 @@
                     <item
                     <item
                         android:id="@+id/action_new_mieru"
                         android:id="@+id/action_new_mieru"
                         android:title="@string/action_mieru" />
                         android:title="@string/action_mieru" />
+                    <item
+                        android:id="@+id/action_new_tuic"
+                        android:title="@string/action_tuic" />
                     <item
                     <item
                         android:id="@+id/action_new_ssh"
                         android:id="@+id/action_new_ssh"
                         android:title="@string/action_ssh" />
                         android:title="@string/action_ssh" />

+ 23 - 0
app/src/main/res/values/arrays.xml

@@ -693,4 +693,27 @@
         <item>UDP</item>
         <item>UDP</item>
     </string-array>
     </string-array>
 
 
+    <string-array name="tuic_udp_relay_mode_entry">
+        <item>NATIVE</item>
+        <item>QUIC</item>
+    </string-array>
+    
+    <string-array name="tuic_udp_relay_mode_value">
+        <item>native</item>
+        <item>quic</item>
+    </string-array>
+
+    <string-array name="tuic_congestion_controller_entry">
+        <item>CUBIC</item>
+        <item>NEW RENO</item>
+        <item>BBR</item>
+    </string-array>
+
+
+    <string-array name="tuic_congestion_controller_value">
+        <item>cubic</item>
+        <item>new_reno</item>
+        <item>bbr</item>
+    </string-array>
+
 </resources>
 </resources>

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

@@ -215,6 +215,7 @@
     <string name="action_ssh" translatable="false">SSH</string>
     <string name="action_ssh" translatable="false">SSH</string>
     <string name="action_wireguard" translatable="false">WireGuard</string>
     <string name="action_wireguard" translatable="false">WireGuard</string>
     <string name="action_mieru" translatable="false">Mieru</string>
     <string name="action_mieru" translatable="false">Mieru</string>
+    <string name="action_tuic" translatable="false">TUIC</string>
     <string name="proxy_chain">Proxy Chain</string>
     <string name="proxy_chain">Proxy Chain</string>
     <string name="custom_config">Custom Config</string>
     <string name="custom_config">Custom Config</string>
     <string name="balancer">Balancer</string>
     <string name="balancer">Balancer</string>
@@ -530,5 +531,10 @@
     <string name="udp_over_tcp_summary">Enable the SUoT protocol, requires server support.</string>
     <string name="udp_over_tcp_summary">Enable the SUoT protocol, requires server support.</string>
     <string name="without_brook_protocol">Without Brook Protocol</string>
     <string name="without_brook_protocol">Without Brook Protocol</string>
     <string name="grpc_mode">gRPC Mode</string>
     <string name="grpc_mode">gRPC Mode</string>
+    <string name="tuic_token">Token</string>
+    <string name="tuic_udp_relay_mode">UDP Relay Mode</string>
+    <string name="tuic_congestion_controller">Congestion Controller</string>
+    <string name="tuic_disable_sni">Disable SNI</string>
+    <string name="tuic_reduce_rtt">Reduce RTT</string>
 
 
 </resources>
 </resources>

+ 68 - 0
app/src/main/res/xml/tuic_preferences.xml

@@ -0,0 +1,68 @@
+<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/tuic_token" />
+        <EditTextPreference
+            app:icon="@drawable/ic_baseline_legend_toggle_24"
+            app:key="serverALPN"
+            app:title="@string/alpn"
+            app:useSimpleSummaryProvider="true" />
+        <EditTextPreference
+            app:icon="@drawable/ic_baseline_vpn_key_24"
+            app:key="serverCertificates"
+            app:title="@string/certificates"
+            app:useSimpleSummaryProvider="true" />
+        <com.takisoft.preferencex.SimpleMenuPreference
+            app:defaultValue="https"
+            app:entries="@array/tuic_udp_relay_mode_entry"
+            app:entryValues="@array/tuic_udp_relay_mode_value"
+            app:icon="@drawable/ic_baseline_add_road_24"
+            app:key="serverUDPRelayMode"
+            app:title="@string/tuic_udp_relay_mode"
+            app:useSimpleSummaryProvider="true" />
+        <com.takisoft.preferencex.SimpleMenuPreference
+            app:defaultValue="https"
+            app:entries="@array/tuic_congestion_controller_entry"
+            app:entryValues="@array/tuic_congestion_controller_value"
+            app:icon="@drawable/ic_baseline_compare_arrows_24"
+            app:key="serverCongestionController"
+            app:title="@string/tuic_congestion_controller"
+            app:useSimpleSummaryProvider="true" />
+        <SwitchPreference
+            app:icon="@drawable/ic_baseline_fingerprint_24"
+            app:key="serverDisableSNI"
+            app:title="@string/tuic_disable_sni" />
+        <EditTextPreference
+            app:icon="@drawable/ic_action_copyright"
+            app:key="serverSNI"
+            app:title="@string/sni"
+            app:useSimpleSummaryProvider="true" />
+        <EditTextPreference
+            app:icon="@drawable/baseline_public_24"
+            app:key="serverMTU"
+            app:title="@string/mtu"
+            app:useSimpleSummaryProvider="true" />
+    </PreferenceCategory>
+
+</PreferenceScreen>

+ 17 - 14
bin/init/env.sh

@@ -42,21 +42,24 @@ if [ $(command -v go) ]; then
   export PATH="$PATH:$(go env GOPATH)/bin"
   export PATH="$PATH:$(go env GOPATH)/bin"
 fi
 fi
 
 
-DEPS=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin
+export TOOLCHAIN=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin
 
 
-export ANDROID_ARM_CC=$DEPS/armv7a-linux-androideabi16-clang
-export ANDROID_ARM_CXX=$DEPS/armv7a-linux-androideabi16-clang++
-export ANDROID_ARM_CC_21=$DEPS/armv7a-linux-androideabi21-clang
-export ANDROID_ARM_CXX_21=$DEPS/armv7a-linux-androideabi21-clang++
+export ANDROID_ARM_CC=$TOOLCHAIN/armv7a-linux-androideabi16-clang
+export ANDROID_ARM_CXX=$TOOLCHAIN/armv7a-linux-androideabi16-clang++
+export ANDROID_ARM_CC_21=$TOOLCHAIN/armv7a-linux-androideabi21-clang
+export ANDROID_ARM_CXX_21=$TOOLCHAIN/armv7a-linux-androideabi21-clang++
 
 
-export ANDROID_ARM64_CC=$DEPS/aarch64-linux-android21-clang
-export ANDROID_ARM64_CXX=$DEPS/aarch64-linux-android21-clang++
+export ANDROID_ARM64_CC=$TOOLCHAIN/aarch64-linux-android21-clang
+export ANDROID_ARM64_CXX=$TOOLCHAIN/aarch64-linux-android21-clang++
+export ANDROID_ARM64_AR=$TOOLCHAIN/aarch64-linux-android21-ar
 
 
-export ANDROID_X86_CC=$DEPS/i686-linux-android16-clang
-export ANDROID_X86_CXX=$DEPS/i686-linux-android16-clang++
-export ANDROID_X86_CC_21=$DEPS/i686-linux-android21-clang
-export ANDROID_X86_CXX_21=$DEPS/i686-linux-android21-clang++
+export ANDROID_X86_CC=$TOOLCHAIN/i686-linux-android16-clang
+export ANDROID_X86_CXX=$TOOLCHAIN/i686-linux-android16-clang++
+export ANDROID_X86_CC_21=$TOOLCHAIN/i686-linux-android21-clang
+export ANDROID_X86_CXX_21=$TOOLCHAIN/i686-linux-android21-clang++
 
 
-export ANDROID_X86_64_CC=$DEPS/x86_64-linux-android21-clang
-export ANDROID_X86_64_CXX=$DEPS/x86_64-linux-android21-clang++
-export ANDROID_X86_64_STRIP=$DEPS/x86_64-linux-android-strip
+export ANDROID_X86_64_CC=$TOOLCHAIN/x86_64-linux-android21-clang
+export ANDROID_X86_64_CXX=$TOOLCHAIN/x86_64-linux-android21-clang++
+
+export ANDROID_LD=$TOOLCHAIN/ld
+export ANDROID_AR=$TOOLCHAIN/llvm-ar

+ 8 - 0
bin/plugin/tuic.sh

@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+bin/plugin/tuic/init.sh &&
+  bin/plugin/tuic/armeabi-v7a.sh &&
+  bin/plugin/tuic/arm64-v8a.sh &&
+  bin/plugin/tuic/x86.sh &&
+  bin/plugin/tuic/x86_64.sh &&
+  bin/plugin/tuic/end.sh

+ 15 - 0
bin/plugin/tuic/arm64-v8a.sh

@@ -0,0 +1,15 @@
+#!/bin/bash
+
+source "bin/init/env.sh"
+source "bin/plugin/tuic/build.sh"
+
+DIR="$ROOT/arm64-v8a"
+mkdir -p $DIR
+
+export CC=$ANDROID_ARM64_CC
+export CXX=$ANDROID_ARM64_CXX
+export RUST_ANDROID_GRADLE_CC=$ANDROID_ARM64_CC
+export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=$PROJECT/bin/rust-linker/linker-wrapper.sh
+
+cargo build --release -p tuic-client --target aarch64-linux-android
+cp target/aarch64-linux-android/release/tuic-client $DIR/$LIB_OUTPUT

+ 15 - 0
bin/plugin/tuic/armeabi-v7a.sh

@@ -0,0 +1,15 @@
+#!/bin/bash
+
+source "bin/init/env.sh"
+source "bin/plugin/tuic/build.sh"
+
+DIR="$ROOT/armeabi-v7a"
+mkdir -p $DIR
+
+export CC=$ANDROID_ARM_CC_21
+export CXX=$ANDROID_ARM_CXX_21
+export RUST_ANDROID_GRADLE_CC=$ANDROID_ARM_CC_21
+export CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER=$PROJECT/bin/rust-linker/linker-wrapper.sh
+
+cargo build --release -p tuic-client --target armv7-linux-androideabi
+cp target/armv7-linux-androideabi/release/tuic-client $DIR/$LIB_OUTPUT

+ 24 - 0
bin/plugin/tuic/build.sh

@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+
+source "bin/init/env.sh"
+
+CURR="plugin/tuic"
+CURR_PATH="$PROJECT/$CURR"
+
+ROOT="$CURR_PATH/src/main/jniLibs"
+OUTPUT="tuic"
+LIB_OUTPUT="lib$OUTPUT.so"
+
+cd $CURR_PATH/src/main/rust/tuic
+
+export AR=$ANDROID_AR
+export LD=$ANDROID_LD
+
+ndkVer=$(grep Pkg.Revision $ANDROID_NDK_HOME/source.properties)
+ndkVer=${ndkVer#*= }
+ndkVer=${ndkVer%%.*}
+
+export CARGO_NDK_MAJOR_VERSION=$ndkVer
+export RUST_ANDROID_GRADLE_CC_LINK_ARG="-Wl"
+export RUST_ANDROID_GRADLE_PYTHON_COMMAND=python
+export RUST_ANDROID_GRADLE_LINKER_WRAPPER_PY=$PROJECT/bin/rust-linker/linker-wrapper.py

+ 5 - 0
bin/plugin/tuic/end.sh

@@ -0,0 +1,5 @@
+source "bin/init/env.sh"
+source "bin/plugin/tuic/build.sh"
+
+#git reset HEAD --hard
+#git clean -fdx

+ 9 - 0
bin/plugin/tuic/init.sh

@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+
+source "bin/init/env.sh"
+
+CURR="plugin/tuic"
+CURR_PATH="$PROJECT/$CURR"
+
+git submodule update --init "$CURR/*"
+cd $CURR_PATH/src/main/rust/tuic

+ 15 - 0
bin/plugin/tuic/x86.sh

@@ -0,0 +1,15 @@
+#!/bin/bash
+
+source "bin/init/env.sh"
+source "bin/plugin/tuic/build.sh"
+
+DIR="$ROOT/x86"
+mkdir -p $DIR
+
+export CC=$ANDROID_X86_CC_21
+export CXX=$ANDROID_X86_CXX_21
+export RUST_ANDROID_GRADLE_CC=$ANDROID_X86_CC_21
+export CARGO_TARGET_I686_LINUX_ANDROID_LINKER=$PROJECT/bin/rust-linker/linker-wrapper.sh
+
+cargo build --release -p tuic-client --target i686-linux-android
+cp target/i686-linux-android/release/tuic-client $DIR/$LIB_OUTPUT

+ 16 - 0
bin/plugin/tuic/x86_64.sh

@@ -0,0 +1,16 @@
+#!/bin/bash
+
+source "bin/init/env.sh"
+source "bin/plugin/tuic/build.sh"
+
+DIR="$ROOT/x86_64"
+mkdir -p $DIR
+
+export CC=$ANDROID_X86_64_CC
+export CXX=$ANDROID_X86_64_CXX
+export RUST_ANDROID_GRADLE_CC=$ANDROID_X86_64_CC
+export CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER=$PROJECT/bin/rust-linker/linker-wrapper.sh
+
+cargo build --release -p tuic-client --target x86_64-linux-android
+cp target/x86_64-linux-android/release/tuic-client $DIR/$LIB_OUTPUT
+

+ 1 - 0
bin/rust-linker/linker-wrapper.bat

@@ -0,0 +1 @@
+"%RUST_ANDROID_GRADLE_PYTHON_COMMAND%" "%RUST_ANDROID_GRADLE_LINKER_WRAPPER_PY%" %*

+ 39 - 0
bin/rust-linker/linker-wrapper.py

@@ -0,0 +1,39 @@
+from __future__ import absolute_import, print_function, unicode_literals
+
+import os
+import pipes
+import subprocess
+import sys
+
+args = [
+    os.environ["RUST_ANDROID_GRADLE_CC"],
+    os.environ["RUST_ANDROID_GRADLE_CC_LINK_ARG"],
+] + sys.argv[1:]
+
+
+def update_in_place(arglist):
+    # The `gcc` library is not included starting from NDK version 23.
+    # Work around by using `unwind` replacement.
+    ndk_major_version = os.environ["CARGO_NDK_MAJOR_VERSION"]
+    if ndk_major_version.isdigit():
+        if 23 <= int(ndk_major_version):
+            for i, arg in enumerate(arglist):
+                if arg.startswith("-lgcc"):
+                    # This is one way to preserve line endings.
+                    arglist[i] = "-lunwind" + arg[len("-lgcc") :]
+
+
+update_in_place(args)
+
+for arg in args:
+    if arg.startswith("@"):
+        fileargs = open(arg[1:], "r").read().splitlines(keepends=True)
+        update_in_place(fileargs)
+        open(arg[1:], "w").write("".join(fileargs))
+
+
+# This only appears when the subprocess call fails, but it's helpful then.
+printable_cmd = " ".join(pipes.quote(arg) for arg in args)
+print(printable_cmd)
+
+sys.exit(subprocess.call(args))

+ 4 - 0
bin/rust-linker/linker-wrapper.sh

@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+
+# Invoke linker-wrapper.py with the correct Python command.
+"${RUST_ANDROID_GRADLE_PYTHON_COMMAND}" "${RUST_ANDROID_GRADLE_LINKER_WRAPPER_PY}" "$@"

+ 5 - 0
plugin/tuic/build.gradle.kts

@@ -0,0 +1,5 @@
+plugins {
+    id("com.android.application")
+}
+
+setupPlugin("tuic")

+ 39 - 0
plugin/tuic/src/main/AndroidManifest.xml

@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    package="io.nekohasekai.sagernet.plugin.tuic"
+    android:installLocation="internalOnly">
+
+    <application
+        android:allowBackup="false"
+        android:extractNativeLibs="true"
+        android:icon="@mipmap/ic_launcher"
+        android:label="TUIC Plugin"
+        android:roundIcon="@mipmap/ic_launcher_round">
+        <provider
+            android:name=".BinaryProvider"
+            android:authorities="io.nekohasekai.sagernet.plugin.tuic.BinaryProvider"
+            android:directBootAware="true"
+            android:exported="true"
+            tools:ignore="ExportedContentProvider">
+            <intent-filter>
+                <action android:name="io.nekohasekai.sagernet.plugin.ACTION_NATIVE_PLUGIN" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="io.nekohasekai.sagernet.plugin.ACTION_NATIVE_PLUGIN" />
+                <data
+                    android:host="io.nekohasekai.sagernet"
+                    android:path="/tuic-plugin"
+                    android:scheme="plugin" />
+            </intent-filter>
+
+            <meta-data
+                android:name="io.nekohasekai.sagernet.plugin.id"
+                android:value="tuic-plugin" />
+            <meta-data
+                android:name="io.nekohasekai.sagernet.plugin.executable_path"
+                android:value="libtuic.so" />
+        </provider>
+    </application>
+
+</manifest>

BIN
plugin/tuic/src/main/ic_launcher-playstore.png


+ 40 - 0
plugin/tuic/src/main/java/io/nekohasekai/sagernet/plugin/tuic/BinaryProvider.kt

@@ -0,0 +1,40 @@
+/******************************************************************************
+ *                                                                            *
+ * Copyright (C) 2021 by nekohasekai <[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.plugin.tuic
+
+import android.net.Uri
+import android.os.ParcelFileDescriptor
+import io.nekohasekai.sagernet.plugin.NativePluginProvider
+import io.nekohasekai.sagernet.plugin.PathProvider
+import java.io.File
+import java.io.FileNotFoundException
+
+class BinaryProvider : NativePluginProvider() {
+    override fun populateFiles(provider: PathProvider) {
+        provider.addPath("tuic-plugin", 0b111101101)
+    }
+
+    override fun getExecutable() = context!!.applicationInfo.nativeLibraryDir + "/libtuic.so"
+    override fun openFile(uri: Uri): ParcelFileDescriptor = when (uri.path) {
+        "/tuic-plugin" -> ParcelFileDescriptor.open(File(getExecutable()),
+            ParcelFileDescriptor.MODE_READ_ONLY)
+        else -> throw FileNotFoundException()
+    }
+}

+ 22 - 0
plugin/tuic/src/main/res/drawable/ic_launcher_foreground.xml

@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportWidth="376"
+    android:viewportHeight="172.80006">
+  <group android:scaleX="0.46867925"
+      android:scaleY="0.2153931"
+      android:translateX="101.52"
+      android:translateY="74.53905">
+    <group android:translateY="106.12804">
+      <path android:pathData="M91.140625,-104L4.890625,-104L2.734375,-63.953125L8.921875,-63.953125C9.921875,-71.15625,11.078125,-76.625,12.8125,-81.234375C16.546875,-91.890625,23.46875,-97.21875,33.25,-97.21875C38,-97.21875,40.015625,-95.640625,40.015625,-92.03125L40.015625,-17.703125C40.015625,-10.359375,40.015625,-10.078125,39.15625,-8.78125C38,-7.34375,35.265625,-6.765625,27.0625,-6.765625L23.3125,-6.765625L23.3125,-0L72.703125,-0L72.703125,-6.765625L68.96875,-6.765625C60.90625,-6.765625,58.171875,-7.34375,57.015625,-8.78125C56.15625,-10.078125,56.15625,-10.359375,56.15625,-17.703125L56.15625,-92.03125C56.15625,-95.640625,58.171875,-97.21875,62.921875,-97.21875C71.703125,-97.21875,78.765625,-92.328125,82.359375,-83.6875C84.515625,-78.34375,85.953125,-72.15625,87.109375,-63.953125L93.296875,-63.953125L91.140625,-104Z"
+          android:fillColor="#FFFFFF"/>
+      <path android:pathData="M170,-97.234375C183.82812,-96.515625,186.5625,-92.484375,186.5625,-72.78125L186.5625,-41C186.5625,-29.5,185.84375,-24.890625,183.10938,-19.859375C178.5,-10.953125,169.14062,-5.765625,157.48438,-5.765625C147.54688,-5.765625,139.1875,-9.5,134.4375,-15.984375C130.54688,-21.296875,129.82812,-24.75,129.82812,-38.125L129.82812,-86.296875C129.82812,-93.640625,129.82812,-93.921875,130.70312,-95.21875C131.84375,-96.65625,134.57812,-97.09375,142.79688,-97.234375L145.53125,-97.234375L145.53125,-104L98.4375,-104L98.4375,-97.234375L100.890625,-97.234375C108.953125,-97.09375,111.6875,-96.65625,112.84375,-95.21875C113.703125,-93.921875,113.703125,-93.640625,113.703125,-86.296875L113.703125,-38.984375L113.703125,-29.0625C114.140625,-21.4375,115.28125,-16.84375,118.03125,-12.8125C124.640625,-3.3125,137.3125,2,153.59375,2C169.85938,2,181.09375,-2.890625,188,-13.109375C192.60938,-19.71875,194.34375,-27.34375,194.34375,-40.140625L194.34375,-72.78125C194.20312,-92.34375,197.07812,-96.515625,211.04688,-97.234375L211.04688,-104L170,-104L170,-97.234375Z"
+          android:fillColor="#FFFFFF"/>
+      <path android:pathData="M218.46875,-97.21875L221.34375,-97.21875C229.54688,-97.078125,232.28125,-96.640625,233.4375,-95.203125C234.29688,-93.90625,234.29688,-93.46875,234.29688,-86.265625L234.29688,-17.703125C234.29688,-10.5,234.29688,-10.078125,233.4375,-8.78125C232.28125,-7.34375,229.54688,-6.765625,221.34375,-6.765625L218.46875,-6.765625L218.46875,0L266.125,0L266.125,-6.765625L263.25,-6.765625C255.1875,-6.765625,252.45312,-7.1875,251.29688,-8.78125C250.4375,-10.078125,250.4375,-10.359375,250.4375,-17.703125L250.4375,-86.265625C250.4375,-93.625,250.4375,-93.90625,251.29688,-95.203125C252.45312,-96.640625,255.04688,-97.078125,263.25,-97.21875L266.125,-97.21875L266.125,-104L218.46875,-104L218.46875,-97.21875Z"
+          android:fillColor="#FFFFFF"/>
+      <path android:pathData="M364,-104.84375L358.53125,-104.84375L352.78125,-96.359375C342.26562,-103.96875,336.9375,-106,327,-106C313.3125,-106,301.65625,-100.234375,292,-88.75C282.79688,-77.84375,278.46875,-65.78125,278.46875,-50.984375C278.46875,-19.984375,298.78125,2,327.14062,2C350.1875,2,364.875,-11.515625,368.1875,-35.625L361.42188,-36.78125C357.53125,-15.390625,346.57812,-4.765625,328.875,-4.765625C307.5625,-4.765625,297.48438,-19.40625,297.48438,-50.421875C297.48438,-66.78125,299.64062,-77.546875,304.53125,-86.171875C309,-94.203125,317.78125,-99.21875,327.14062,-99.21875C337.21875,-99.21875,346.15625,-93.921875,351.625,-84.734375C354.35938,-79.984375,356.375,-74.671875,359.82812,-63.34375L366.17188,-63.34375L364,-104.84375Z"
+          android:fillColor="#FFFFFF"/>
+    </group>
+  </group>
+</vector>

+ 6 - 0
plugin/tuic/src/main/res/mipmap-anydpi-v26/ic_launcher.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@color/ic_launcher_background"/>
+    <foreground android:drawable="@drawable/ic_launcher_foreground"/>
+    <monochrome android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>

+ 6 - 0
plugin/tuic/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@color/ic_launcher_background"/>
+    <foreground android:drawable="@drawable/ic_launcher_foreground"/>
+    <monochrome android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>

BIN
plugin/tuic/src/main/res/mipmap-hdpi/ic_launcher.png


BIN
plugin/tuic/src/main/res/mipmap-hdpi/ic_launcher_round.png


BIN
plugin/tuic/src/main/res/mipmap-mdpi/ic_launcher.png


BIN
plugin/tuic/src/main/res/mipmap-mdpi/ic_launcher_round.png


BIN
plugin/tuic/src/main/res/mipmap-xhdpi/ic_launcher.png


BIN
plugin/tuic/src/main/res/mipmap-xhdpi/ic_launcher_round.png


BIN
plugin/tuic/src/main/res/mipmap-xxhdpi/ic_launcher.png


BIN
plugin/tuic/src/main/res/mipmap-xxhdpi/ic_launcher_round.png


BIN
plugin/tuic/src/main/res/mipmap-xxxhdpi/ic_launcher.png


BIN
plugin/tuic/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png


+ 4 - 0
plugin/tuic/src/main/res/values/ic_launcher_background.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="ic_launcher_background">#E91E63</color>
+</resources>

+ 1 - 0
plugin/tuic/src/main/rust/tuic

@@ -0,0 +1 @@
+Subproject commit 04b4f0d923d1654faad9836c3770df1193fcec2c

+ 5 - 2
sager.properties

@@ -1,5 +1,5 @@
 PACKAGE_NAME=io.nekohasekai.sagernet
 PACKAGE_NAME=io.nekohasekai.sagernet
-VERSION_NAME=0.8-rc05
+VERSION_NAME=0.8.1-beta01
 VERSION_CODE=172
 VERSION_CODE=172
 
 
 NAIVE_VERSION_NAME=103.0.5060.53-1
 NAIVE_VERSION_NAME=103.0.5060.53-1
@@ -24,4 +24,7 @@ HYSTERIA_VERSION_NAME=1.0.4
 HYSTERIA_VERSION=16
 HYSTERIA_VERSION=16
 
 
 MIERU_VERSION_NAME=1.5.0
 MIERU_VERSION_NAME=1.5.0
-MIERU_VERSION=1
+MIERU_VERSION=1
+
+TUIC_VERSION_NAME=0.8.0-beta0
+TUIC_VERSION=1

+ 1 - 0
settings.gradle.kts

@@ -15,6 +15,7 @@ when {
         include(":plugin:trojan-go")
         include(":plugin:trojan-go")
         include(":plugin:hysteria")
         include(":plugin:hysteria")
         include(":plugin:mieru")
         include(":plugin:mieru")
+        include(":plugin:tuic")
     }
     }
     buildPlugin == "none" -> {
     buildPlugin == "none" -> {
     }
     }