Browse Source

Add mieru plugin

世界 3 years ago
parent
commit
5042448c1a
47 changed files with 1035 additions and 7 deletions
  1. 3 0
      .gitmodules
  2. 1 0
      .idea/codeStyles/Project.xml
  3. 1 0
      .idea/vcs.xml
  4. 452 0
      app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/16.json
  5. 3 0
      app/src/main/AndroidManifest.xml
  6. 24 0
      app/src/main/java/io/nekohasekai/sagernet/bg/proto/V2RayInstance.kt
  7. 38 5
      app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt
  8. 4 1
      app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt
  9. 7 0
      app/src/main/java/io/nekohasekai/sagernet/fmt/KryoConverters.java
  10. 1 0
      app/src/main/java/io/nekohasekai/sagernet/fmt/TypeMap.kt
  11. 92 0
      app/src/main/java/io/nekohasekai/sagernet/fmt/mieru/MieruBean.java
  12. 58 0
      app/src/main/java/io/nekohasekai/sagernet/fmt/mieru/MieruFmt.kt
  13. 3 0
      app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt
  14. 75 0
      app/src/main/java/io/nekohasekai/sagernet/ui/profile/MieruSettingsActivity.kt
  15. 3 0
      app/src/main/res/menu/add_profile_menu.xml
  16. 5 0
      app/src/main/res/values/arrays.xml
  17. 1 0
      app/src/main/res/values/strings.xml
  18. 45 0
      app/src/main/res/xml/mieru_preferences.xml
  19. 8 0
      bin/plugin/mieru.sh
  20. 8 0
      bin/plugin/mieru/arm64-v8a.sh
  21. 8 0
      bin/plugin/mieru/armeabi-v7a.sh
  22. 15 0
      bin/plugin/mieru/build.sh
  23. 5 0
      bin/plugin/mieru/end.sh
  24. 13 0
      bin/plugin/mieru/init.sh
  25. 8 0
      bin/plugin/mieru/x86.sh
  26. 9 0
      bin/plugin/mieru/x86_64.sh
  27. 5 0
      plugin/mieru/build.gradle.kts
  28. 47 0
      plugin/mieru/src/main/AndroidManifest.xml
  29. 1 0
      plugin/mieru/src/main/go/mieru
  30. BIN
      plugin/mieru/src/main/ic_launcher-playstore.png
  31. 40 0
      plugin/mieru/src/main/java/io/nekohasekai/sagernet/plugin/mieru/BinaryProvider.kt
  32. 30 0
      plugin/mieru/src/main/res/drawable/ic_launcher_foreground.xml
  33. 6 0
      plugin/mieru/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
  34. 7 0
      plugin/mieru/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
  35. BIN
      plugin/mieru/src/main/res/mipmap-hdpi/ic_launcher.png
  36. BIN
      plugin/mieru/src/main/res/mipmap-hdpi/ic_launcher_round.png
  37. BIN
      plugin/mieru/src/main/res/mipmap-mdpi/ic_launcher.png
  38. BIN
      plugin/mieru/src/main/res/mipmap-mdpi/ic_launcher_round.png
  39. BIN
      plugin/mieru/src/main/res/mipmap-xhdpi/ic_launcher.png
  40. BIN
      plugin/mieru/src/main/res/mipmap-xhdpi/ic_launcher_round.png
  41. BIN
      plugin/mieru/src/main/res/mipmap-xxhdpi/ic_launcher.png
  42. BIN
      plugin/mieru/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
  43. BIN
      plugin/mieru/src/main/res/mipmap-xxxhdpi/ic_launcher.png
  44. BIN
      plugin/mieru/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
  45. 4 0
      plugin/mieru/src/main/res/values/ic_launcher_background.xml
  46. 4 1
      sager.properties
  47. 1 0
      settings.gradle.kts

+ 3 - 0
.gitmodules

@@ -31,3 +31,6 @@
 [submodule "external/termux-view"]
 	path = external/termux-view
 	url = https://github.com/SagerNet/termux-view
+[submodule "plugin/mieru/src/main/go/mieru"]
+	path = plugin/mieru/src/main/go/mieru
+	url = https://github.com/SagerNet/mieru

+ 1 - 0
.idea/codeStyles/Project.xml

@@ -1,5 +1,6 @@
 <component name="ProjectCodeStyleConfiguration">
   <code_scheme name="Project" version="173">
+    <option name="ENABLE_SECOND_REFORMAT" value="true" />
     <JetCodeStyleSettings>
       <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
     </JetCodeStyleSettings>

+ 1 - 0
.idea/vcs.xml

@@ -14,6 +14,7 @@
     <mapping directory="$PROJECT_DIR$/library/core" vcs="Git" />
     <mapping directory="$PROJECT_DIR$/plugin/brook/src/main/go/brook" vcs="Git" />
     <mapping directory="$PROJECT_DIR$/plugin/hysteria/src/main/go/hysteria" vcs="Git" />
+    <mapping directory="$PROJECT_DIR$/plugin/mieru/src/main/go/mieru" vcs="Git" />
     <mapping directory="$PROJECT_DIR$/plugin/naive/src/main/jni/naiveproxy" 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" />

+ 452 - 0
app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/16.json

@@ -0,0 +1,452 @@
+{
+  "formatVersion": 1,
+  "database": {
+    "version": 16,
+    "identityHash": "4813d76aed60269761d6d10e7e7bc603",
+    "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, `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": "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, '4813d76aed60269761d6d10e7e7bc603')"
+    ]
+  }
+}

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

@@ -197,6 +197,9 @@
         <activity
             android:name="io.nekohasekai.sagernet.ui.profile.WireGuardSettingsActivity"
             android:configChanges="uiMode" />
+        <activity
+            android:name="io.nekohasekai.sagernet.ui.profile.MieruSettingsActivity"
+            android:configChanges="uiMode" />
         <activity
             android:name="io.nekohasekai.sagernet.ui.profile.ConfigSettingsActivity"
             android:configChanges="uiMode" />

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

@@ -43,6 +43,8 @@ import io.nekohasekai.sagernet.fmt.buildV2RayConfig
 import io.nekohasekai.sagernet.fmt.hysteria.HysteriaBean
 import io.nekohasekai.sagernet.fmt.hysteria.buildHysteriaConfig
 import io.nekohasekai.sagernet.fmt.internal.ConfigBean
+import io.nekohasekai.sagernet.fmt.mieru.MieruBean
+import io.nekohasekai.sagernet.fmt.mieru.buildMieruConfig
 import io.nekohasekai.sagernet.fmt.naive.NaiveBean
 import io.nekohasekai.sagernet.fmt.naive.buildNaiveConfig
 import io.nekohasekai.sagernet.fmt.pingtunnel.PingTunnelBean
@@ -142,6 +144,10 @@ abstract class V2RayInstance(
                             }
                         }
                     }
+                    is MieruBean -> {
+                        initPlugin("mieru-plugin")
+                        pluginConfigs[port] = profile.type to bean.buildMieruConfig(port)
+                    }
                     is ConfigBean -> {
                         when (bean.type) {
                             "trojan-go" -> {
@@ -346,6 +352,24 @@ abstract class V2RayInstance(
                             commands.addAll(0, listOf("su", "-c"))
                         }
 
+                        processes.start(commands, env)
+                    }
+                    bean is MieruBean -> {
+                        val configFile = File(
+                            context.noBackupFilesDir,
+                            "mieru_" + SystemClock.elapsedRealtime() + ".json"
+                        )
+
+                        configFile.parentFile?.mkdirs()
+                        configFile.writeText(config)
+                        cacheFiles.add(configFile)
+
+                        env["MIERU_CONFIG_FILE"] = configFile.absolutePath
+
+                        val commands = mutableListOf(
+                            initPlugin("mieru-plugin").path, "run_plugin"
+                        )
+
                         processes.start(commands, env)
                     }
                 }

+ 38 - 5
app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt

@@ -42,6 +42,8 @@ import io.nekohasekai.sagernet.fmt.hysteria.buildHysteriaConfig
 import io.nekohasekai.sagernet.fmt.internal.BalancerBean
 import io.nekohasekai.sagernet.fmt.internal.ChainBean
 import io.nekohasekai.sagernet.fmt.internal.ConfigBean
+import io.nekohasekai.sagernet.fmt.mieru.MieruBean
+import io.nekohasekai.sagernet.fmt.mieru.buildMieruConfig
 import io.nekohasekai.sagernet.fmt.naive.NaiveBean
 import io.nekohasekai.sagernet.fmt.naive.buildNaiveConfig
 import io.nekohasekai.sagernet.fmt.naive.toUri
@@ -67,6 +69,7 @@ import io.nekohasekai.sagernet.fmt.v2ray.VLESSBean
 import io.nekohasekai.sagernet.fmt.v2ray.VMessBean
 import io.nekohasekai.sagernet.fmt.v2ray.toUri
 import io.nekohasekai.sagernet.fmt.wireguard.WireGuardBean
+import io.nekohasekai.sagernet.ktx.Logs
 import io.nekohasekai.sagernet.ktx.app
 import io.nekohasekai.sagernet.ktx.applyDefaultValues
 import io.nekohasekai.sagernet.ui.profile.*
@@ -98,6 +101,7 @@ data class ProxyEntity(
     var rbBean: RelayBatonBean? = null,
     var brookBean: BrookBean? = null,
     var hysteriaBean: HysteriaBean? = null,
+    var mieruBean: MieruBean? = null,
     var sshBean: SSHBean? = null,
     var wgBean: WireGuardBean? = null,
     var configBean: ConfigBean? = null,
@@ -122,6 +126,7 @@ data class ProxyEntity(
         const val TYPE_SNELL = 16
         const val TYPE_SSH = 17
         const val TYPE_WG = 18
+        const val TYPE_MIERU = 19
 
         const val TYPE_CHAIN = 8
         const val TYPE_BALANCER = 14
@@ -214,6 +219,7 @@ data class ProxyEntity(
             TYPE_HYSTERIA -> hysteriaBean = KryoConverters.hysteriaDeserialize(byteArray)
             TYPE_SSH -> sshBean = KryoConverters.sshDeserialize(byteArray)
             TYPE_WG -> wgBean = KryoConverters.wireguardDeserialize(byteArray)
+            TYPE_MIERU -> mieruBean = KryoConverters.mieruDeserialize(byteArray)
 
             TYPE_CONFIG -> configBean = KryoConverters.configDeserialize(byteArray)
             TYPE_CHAIN -> chainBean = KryoConverters.chainDeserialize(byteArray)
@@ -238,6 +244,8 @@ data class ProxyEntity(
         TYPE_SNELL -> "Snell"
         TYPE_SSH -> "SSH"
         TYPE_WG -> "WireGuard"
+        TYPE_MIERU -> "Mieru"
+
         TYPE_CHAIN -> chainName
         TYPE_CONFIG -> configName
         TYPE_BALANCER -> balancerName
@@ -264,6 +272,7 @@ data class ProxyEntity(
             TYPE_HYSTERIA -> hysteriaBean
             TYPE_SSH -> sshBean
             TYPE_WG -> wgBean
+            TYPE_MIERU -> mieruBean
 
             TYPE_CONFIG -> configBean
             TYPE_CHAIN -> chainBean
@@ -282,7 +291,7 @@ data class ProxyEntity(
 
     fun haveStandardLink(): Boolean {
         return haveLink() && when (type) {
-            TYPE_RELAY_BATON, TYPE_BROOK, TYPE_SSH, TYPE_WG, TYPE_HYSTERIA -> false
+            TYPE_RELAY_BATON, TYPE_BROOK, TYPE_SSH, TYPE_WG, TYPE_HYSTERIA, TYPE_MIERU -> false
             TYPE_CONFIG -> false
             else -> true
         }
@@ -307,6 +316,7 @@ data class ProxyEntity(
             is SSHBean -> toUniversalLink()
             is WireGuardBean -> toUniversalLink()
             is HysteriaBean -> toUniversalLink()
+            is MieruBean -> toUniversalLink()
             else -> null
         }
     }
@@ -331,19 +341,33 @@ data class ProxyEntity(
                         when (val bean = profile.requireBean()) {
                             is TrojanGoBean -> {
                                 append("\n\n")
-                                append(bean.buildTrojanGoConfig(port, needMux))
+                                append(bean.buildTrojanGoConfig(port, needMux).also {
+                                    Logs.d(it)
+                                })
                             }
                             is NaiveBean -> {
                                 append("\n\n")
-                                append(bean.buildNaiveConfig(port))
+                                append(bean.buildNaiveConfig(port).also {
+                                    Logs.d(it)
+                                })
                             }
                             is RelayBatonBean -> {
                                 append("\n\n")
-                                append(bean.buildRelayBatonConfig(port))
+                                append(bean.buildRelayBatonConfig(port).also {
+                                    Logs.d(it)
+                                })
                             }
                             is HysteriaBean -> {
                                 append("\n\n")
-                                append(bean.buildHysteriaConfig(port, null))
+                                append(bean.buildHysteriaConfig(port, null).also {
+                                    Logs.d(it)
+                                })
+                            }
+                            is MieruBean -> {
+                                append("\n\n")
+                                append(bean.buildMieruConfig(port).also {
+                                    Logs.d(it)
+                                })
                             }
                         }
                     }
@@ -361,6 +385,8 @@ data class ProxyEntity(
             TYPE_HYSTERIA -> true
             TYPE_RELAY_BATON -> true
             TYPE_BROOK -> true
+            TYPE_MIERU -> true
+
             TYPE_CONFIG -> true
             else -> false
         }
@@ -406,6 +432,7 @@ data class ProxyEntity(
         hysteriaBean = null
         sshBean = null
         wgBean = null
+        mieruBean = null
 
         configBean = null
         chainBean = null
@@ -472,6 +499,11 @@ data class ProxyEntity(
                 type = TYPE_WG
                 wgBean = bean
             }
+            is MieruBean -> {
+                type = TYPE_MIERU
+                mieruBean = bean
+            }
+
             is ConfigBean -> {
                 type = TYPE_CONFIG
                 configBean = bean
@@ -507,6 +539,7 @@ data class ProxyEntity(
                 TYPE_HYSTERIA -> HysteriaSettingsActivity::class.java
                 TYPE_SSH -> SSHSettingsActivity::class.java
                 TYPE_WG -> WireGuardSettingsActivity::class.java
+                TYPE_MIERU -> MieruSettingsActivity::class.java
 
                 TYPE_CONFIG -> ConfigSettingsActivity::class.java
                 TYPE_CHAIN -> ChainSettingsActivity::class.java

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

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

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

@@ -37,6 +37,7 @@ import io.nekohasekai.sagernet.fmt.hysteria.HysteriaBean;
 import io.nekohasekai.sagernet.fmt.internal.BalancerBean;
 import io.nekohasekai.sagernet.fmt.internal.ChainBean;
 import io.nekohasekai.sagernet.fmt.internal.ConfigBean;
+import io.nekohasekai.sagernet.fmt.mieru.MieruBean;
 import io.nekohasekai.sagernet.fmt.naive.NaiveBean;
 import io.nekohasekai.sagernet.fmt.pingtunnel.PingTunnelBean;
 import io.nekohasekai.sagernet.fmt.relaybaton.RelayBatonBean;
@@ -170,6 +171,12 @@ public class KryoConverters {
         return deserialize(new WireGuardBean(), bytes);
     }
 
+    @TypeConverter
+    public static MieruBean mieruDeserialize(byte[] bytes) {
+        if (ArrayUtil.isEmpty(bytes)) return null;
+        return deserialize(new MieruBean(), bytes);
+    }
+
     @TypeConverter
     public static ConfigBean configDeserialize(byte[] bytes) {
         if (ArrayUtil.isEmpty(bytes)) return null;

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

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

+ 92 - 0
app/src/main/java/io/nekohasekai/sagernet/fmt/mieru/MieruBean.java

@@ -0,0 +1,92 @@
+/******************************************************************************
+ * 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.mieru;
+
+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 MieruBean extends AbstractBean {
+
+    public static final int PROTOCOL_TCP = 0;
+    public static final int PROTOCOL_UDP = 1;
+
+    public Integer protocol;
+    public String username;
+    public String password;
+    public Integer mtu;
+
+    @Override
+    public void initializeDefaultValues() {
+        super.initializeDefaultValues();
+        if (protocol == null) protocol = PROTOCOL_TCP;
+        if (username == null) username = "";
+        if (password == null) password = "";
+        if (mtu == null) mtu = 1400;
+    }
+
+    @Override
+    public void serialize(ByteBufferOutput output) {
+        output.writeInt(0);
+        super.serialize(output);
+        output.writeInt(protocol);
+        output.writeString(username);
+        output.writeString(password);
+        if (protocol == PROTOCOL_UDP) {
+            output.writeInt(mtu);
+        }
+    }
+
+    @Override
+    public void deserialize(ByteBufferInput input) {
+        int version = input.readInt();
+        super.deserialize(input);
+        protocol = input.readInt();
+        username = input.readString();
+        password = input.readString();
+        if (protocol == PROTOCOL_UDP) {
+            mtu = input.readInt();
+        }
+    }
+
+    @NotNull
+    @Override
+    public MieruBean clone() {
+        return KryoConverters.deserialize(new MieruBean(), KryoConverters.serialize(this));
+    }
+
+    public static final Creator<MieruBean> CREATOR = new CREATOR<MieruBean>() {
+        @NonNull
+        @Override
+        public MieruBean newInstance() {
+            return new MieruBean();
+        }
+
+        @Override
+        public MieruBean[] newArray(int size) {
+            return new MieruBean[size];
+        }
+    };
+}

+ 58 - 0
app/src/main/java/io/nekohasekai/sagernet/fmt/mieru/MieruFmt.kt

@@ -0,0 +1,58 @@
+/******************************************************************************
+ * 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.mieru
+
+import cn.hutool.json.JSONArray
+import cn.hutool.json.JSONObject
+import io.nekohasekai.sagernet.database.DataStore
+import io.nekohasekai.sagernet.ktx.isIpAddress
+import io.nekohasekai.sagernet.ktx.mkPort
+
+fun MieruBean.buildMieruConfig(port: Int): String {
+    return JSONObject().also {
+        it["activeProfile"] = "default"
+        it["rpcPort"] = 8964
+        it["socks5Port"] = port
+        it["loggingLevel"] = if (DataStore.enableLog) "DEBUG" else "WARN"
+        it["profiles"] = JSONArray().apply {
+            put(JSONObject().also {
+                it["profileName"] = "default"
+                it["user"] = JSONObject().also {
+                    it["name"] = username
+                    it["password"] = password
+                }
+                it["servers"] = JSONArray().apply {
+                    put(JSONObject().also {
+                        it["ipAddress"] = finalAddress
+                        it["portBindings"] = JSONArray().apply {
+                            put(JSONObject().also {
+                                it["port"] = finalPort
+                                it["protocol"] = when (protocol) {
+                                    MieruBean.PROTOCOL_TCP -> "TCP"
+                                    MieruBean.PROTOCOL_UDP -> "UDP"
+                                    else -> error("unexpected protocol $protocol")
+                                }
+                            })
+                        }
+                    })
+                }
+            })
+        }
+    }.toStringPretty()
+}

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

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

+ 75 - 0
app/src/main/java/io/nekohasekai/sagernet/ui/profile/MieruSettingsActivity.kt

@@ -0,0 +1,75 @@
+/******************************************************************************
+ *                                                                            *
+ * 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 androidx.preference.EditTextPreference
+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.ktx.applyDefaultValues
+
+class MieruSettingsActivity : ProfileSettingsActivity<MieruBean>() {
+
+    override fun createEntity() = MieruBean().applyDefaultValues()
+
+    override fun MieruBean.init() {
+        DataStore.profileName = name
+        DataStore.serverAddress = serverAddress
+        DataStore.serverPort = serverPort
+        DataStore.serverProtocolVersion = protocol
+        DataStore.serverUsername = username
+        DataStore.serverPassword = password
+        DataStore.serverMTU = mtu
+    }
+
+    override fun MieruBean.serialize() {
+        name = DataStore.profileName
+        serverAddress = DataStore.serverAddress
+        serverPort = DataStore.serverPort
+        protocol = DataStore.serverProtocolVersion
+        username = DataStore.serverUsername
+        password = DataStore.serverPassword
+        mtu = DataStore.serverMTU
+    }
+
+    override fun PreferenceFragmentCompat.createPreferences(
+        savedInstanceState: Bundle?,
+        rootKey: String?,
+    ) {
+        addPreferencesFromResource(R.xml.mieru_preferences)
+
+        val protocol = findPreference<SimpleMenuPreference>(Key.SERVER_PROTOCOL)!!
+        val mtu = findPreference<EditTextPreference>(Key.SERVER_MTU)!!
+        mtu.isVisible = protocol.value == "${MieruBean.PROTOCOL_UDP}"
+        protocol.setOnPreferenceChangeListener { _, newValue ->
+            mtu.isVisible = newValue == "${MieruBean.PROTOCOL_UDP}"
+            true
+        }
+
+        findPreference<EditTextPreference>(Key.SERVER_PASSWORD)!!.apply {
+            summaryProvider = PasswordSummaryProvider
+        }
+    }
+
+}

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

@@ -57,6 +57,9 @@
                     <item
                         android:id="@+id/action_new_hysteria"
                         android:title="@string/action_hysteria" />
+                    <item
+                        android:id="@+id/action_new_mieru"
+                        android:title="@string/action_mieru" />
                     <item
                         android:id="@+id/action_new_ssh"
                         android:title="@string/action_ssh" />

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

@@ -688,4 +688,9 @@
         <item>raw</item>
     </string-array>
 
+    <string-array name="mieru_protocol">
+        <item>TCP</item>
+        <item>UDP</item>
+    </string-array>
+
 </resources>

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

@@ -214,6 +214,7 @@
     <string name="action_hysteria" translatable="false">Hysteria</string>
     <string name="action_ssh" translatable="false">SSH</string>
     <string name="action_wireguard" translatable="false">WireGuard</string>
+    <string name="action_mieru" translatable="false">Mieru</string>
     <string name="proxy_chain">Proxy Chain</string>
     <string name="custom_config">Custom Config</string>
     <string name="balancer">Balancer</string>

+ 45 - 0
app/src/main/res/xml/mieru_preferences.xml

@@ -0,0 +1,45 @@
+<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" />
+        <com.takisoft.preferencex.SimpleMenuPreference
+            app:entries="@array/mieru_protocol"
+            app:entryValues="@array/int_array_2"
+            app:icon="@drawable/ic_baseline_compare_arrows_24"
+            app:key="serverProtocol"
+            app:title="@string/protocol"
+            app:useSimpleSummaryProvider="true" />
+        <EditTextPreference
+            app:icon="@drawable/ic_baseline_person_24"
+            app:key="serverUsername"
+            app:title="@string/username"
+            app:useSimpleSummaryProvider="true" />
+        <EditTextPreference
+            app:dialogLayout="@layout/layout_password_dialog"
+            app:icon="@drawable/ic_settings_password"
+            app:key="serverPassword"
+            app:title="@string/password" />
+        <EditTextPreference
+            app:icon="@drawable/baseline_public_24"
+            app:key="serverMTU"
+            app:title="@string/mtu"
+            app:useSimpleSummaryProvider="true" />
+    </PreferenceCategory>
+
+</PreferenceScreen>

+ 8 - 0
bin/plugin/mieru.sh

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

+ 8 - 0
bin/plugin/mieru/arm64-v8a.sh

@@ -0,0 +1,8 @@
+#!/bin/bash
+
+source "bin/init/env.sh"
+source "bin/plugin/mieru/build.sh"
+
+DIR="$ROOT/arm64-v8a"
+mkdir -p $DIR
+env CC=$ANDROID_ARM64_CC GOARCH=arm64 go build -v -o $DIR/$LIB_OUTPUT -trimpath -ldflags "-s -w -buildid=" ./cmd/mieru

+ 8 - 0
bin/plugin/mieru/armeabi-v7a.sh

@@ -0,0 +1,8 @@
+#!/bin/bash
+
+source "bin/init/env.sh"
+source "bin/plugin/mieru/build.sh"
+
+DIR="$ROOT/armeabi-v7a"
+mkdir -p $DIR
+env CC=$ANDROID_ARM_CC GOARCH=arm GOARM=7 go build -v -o $DIR/$LIB_OUTPUT -trimpath -ldflags "-s -w -buildid=" ./cmd/mieru

+ 15 - 0
bin/plugin/mieru/build.sh

@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+
+source "bin/init/env.sh"
+
+export CGO_ENABLED=1
+export GOOS=android
+
+CURR="plugin/mieru"
+CURR_PATH="$PROJECT/$CURR"
+
+ROOT="$CURR_PATH/src/main/jniLibs"
+OUTPUT="mieru"
+LIB_OUTPUT="lib$OUTPUT.so"
+
+cd $CURR_PATH/src/main/go/mieru

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

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

+ 13 - 0
bin/plugin/mieru/init.sh

@@ -0,0 +1,13 @@
+#!/usr/bin/env bash
+
+source "bin/init/env.sh"
+
+export CGO_ENABLED=1
+export GOOS=android
+
+CURR="plugin/mieru"
+CURR_PATH="$PROJECT/$CURR"
+
+git submodule update --init "$CURR/*"
+cd $CURR_PATH/src/main/go/mieru
+go mod download -x

+ 8 - 0
bin/plugin/mieru/x86.sh

@@ -0,0 +1,8 @@
+#!/bin/bash
+
+source "bin/init/env.sh"
+source "bin/plugin/mieru/build.sh"
+
+DIR="$ROOT/x86"
+mkdir -p $DIR
+env CC=$ANDROID_X86_CC GOARCH=386 go build -v -o $DIR/$LIB_OUTPUT -trimpath -ldflags "-s -w -buildid=" ./cmd/mieru

+ 9 - 0
bin/plugin/mieru/x86_64.sh

@@ -0,0 +1,9 @@
+#!/bin/bash
+
+source "bin/init/env.sh"
+source "bin/plugin/mieru/build.sh"
+
+DIR="$ROOT/x86_64"
+mkdir -p $DIR
+env CC=$ANDROID_X86_64_CC GOARCH=amd64 go build -v -o $DIR/$LIB_OUTPUT -trimpath -ldflags "-s -w -buildid=" ./cmd/mieru
+

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

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

+ 47 - 0
plugin/mieru/src/main/AndroidManifest.xml

@@ -0,0 +1,47 @@
+<?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.mieru"
+    android:installLocation="internalOnly"
+    tools:ignore="MissingLeanbackLauncher">
+
+    <uses-feature
+        android:name="android.software.leanback"
+        android:required="false" />
+    <uses-feature
+        android:name="android.hardware.touchscreen"
+        android:required="false" />
+
+    <application
+        android:allowBackup="false"
+        android:extractNativeLibs="true"
+        android:icon="@mipmap/ic_launcher"
+        android:label="Mieru Plugin"
+        android:roundIcon="@mipmap/ic_launcher_round">
+        <provider
+            android:name="io.nekohasekai.sagernet.plugin.mieru.BinaryProvider"
+            android:authorities="io.nekohasekai.sagernet.plugin.mieru.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="/mieru-plugin"
+                    android:scheme="plugin" />
+            </intent-filter>
+
+            <meta-data
+                android:name="io.nekohasekai.sagernet.plugin.id"
+                android:value="mieru-plugin" />
+            <meta-data
+                android:name="io.nekohasekai.sagernet.plugin.executable_path"
+                android:value="libmieru.so" />
+        </provider>
+    </application>
+
+</manifest>

+ 1 - 0
plugin/mieru/src/main/go/mieru

@@ -0,0 +1 @@
+Subproject commit 35885d28d5b5fb2d1730726d0b0a55d18ce00656

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


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

@@ -0,0 +1,40 @@
+/******************************************************************************
+ * 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.plugin.mieru
+
+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("mieru-plugin", 0b111101101)
+    }
+
+    override fun getExecutable() = context!!.applicationInfo.nativeLibraryDir + "/libmieru.so"
+    override fun openFile(uri: Uri): ParcelFileDescriptor = when (uri.path) {
+        "/mieru-plugin" -> ParcelFileDescriptor.open(
+            File(getExecutable()), ParcelFileDescriptor.MODE_READ_ONLY
+        )
+        else -> throw FileNotFoundException()
+    }
+}

+ 30 - 0
plugin/mieru/src/main/res/drawable/ic_launcher_foreground.xml

@@ -0,0 +1,30 @@
+<?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="405"
+    android:viewportHeight="172.80006">
+    <group
+        android:scaleX="0.46"
+        android:scaleY="0.19626674"
+        android:translateX="109.35"
+        android:translateY="75.69858">
+        <group android:translateY="106.12804">
+            <path
+                android:pathData="M41.3125,-104L4.171875,-104L4.171875,-97.21875L6.90625,-97.21875C15.109375,-97.078125,17.84375,-96.640625,18.859375,-95.203125C19.859375,-93.90625,19.859375,-93.765625,19.859375,-86.265625L19.859375,-31.109375C19.859375,-11.515625,17.125,-7.484375,3.15625,-6.765625L3.15625,-0L44.203125,-0L44.203125,-6.765625C30.375,-7.484375,27.640625,-11.515625,27.640625,-31.109375L27.640625,-94.765625L61.90625,-0L67.671875,-0L99.78125,-94.765625L99.78125,-17.703125C99.78125,-10.359375,99.78125,-10.078125,98.921875,-8.640625C97.765625,-7.1875,95.03125,-6.765625,86.828125,-6.765625L84.234375,-6.765625L84.234375,-0L131.46875,-0L131.46875,-6.765625L128.875,-6.765625C120.65625,-6.765625,117.921875,-7.1875,116.78125,-8.640625C115.90625,-10.078125,115.90625,-10.21875,115.90625,-17.703125L115.90625,-86.265625C115.90625,-93.625,115.90625,-93.90625,116.78125,-95.203125C117.921875,-96.640625,120.65625,-97.078125,128.875,-97.21875L131.46875,-97.21875L131.46875,-104L95.3125,-104L68.96875,-27.21875L41.3125,-104Z"
+                android:fillColor="#FFFFFF" />
+            <path
+                android:pathData="M165.65625,-67.125L138.57812,-65.546875L138.57812,-59.34375L145.92188,-59.34375C151.40625,-59.203125,152.125,-58.34375,152.125,-52.140625L152.125,-17.71875L152.125,-10.9375C151.96875,-8.78125,151.82812,-7.921875,151.25,-7.1875C150.25,-5.890625,147.51562,-5.328125,141.3125,-5.171875L140.3125,-5.171875L140.3125,0L177.60938,0L177.60938,-5.171875L176.59375,-5.171875C166.95312,-5.46875,165.65625,-6.1875,165.65625,-11.375L165.65625,-17.71875L165.65625,-67.125ZM158.89062,-103C154.28125,-103,150.53125,-99.25,150.53125,-94.640625C150.53125,-90.03125,154.28125,-86.28125,158.75,-86.28125C163.5,-86.28125,167.23438,-90.03125,167.23438,-94.640625C167.23438,-99.25,163.5,-103,158.89062,-103Z"
+                android:fillColor="#FFFFFF" />
+            <path
+                android:pathData="M247.95312,-32C248.09375,-53.671875,235.85938,-69,218.28125,-69C199.85938,-69,186.03125,-52.953125,186.03125,-31.28125C186.03125,-11.40625,199.14062,2,218.57812,2C232.40625,2,242.04688,-4.765625,247.95312,-18.171875L242.90625,-20.859375C237,-10,230.95312,-5.765625,221.3125,-5.765625C213.96875,-5.765625,208.64062,-8.734375,204.89062,-15.078125C202.29688,-19.296875,201.15625,-24.234375,201.29688,-32L247.95312,-32ZM201.4375,-38.1875C201.4375,-42.671875,202.01562,-45.859375,203.45312,-49.90625C206.48438,-58.609375,211.09375,-62.796875,218.14062,-62.796875C226.78125,-62.796875,232.54688,-55.125,232.54688,-43.390625C232.54688,-39.34375,231.25,-38.1875,226.64062,-38.1875L201.4375,-38.1875Z"
+                android:fillColor="#FFFFFF" />
+            <path
+                android:pathData="M281.5,-66.828125L256.01562,-65.25L256.01562,-59.078125L261.92188,-59.078125C267.39062,-58.9375,268.10938,-58.078125,268.10938,-51.921875L268.10938,-17.640625L268.10938,-10.90625C267.96875,-8.75,267.82812,-7.890625,267.25,-7.171875C266.23438,-5.890625,263.35938,-5.3125,257.17188,-5.171875L256.59375,-5.171875L256.59375,0L295.04688,0L295.04688,-5.171875L292.45312,-5.171875C282.79688,-5.453125,281.5,-6.171875,281.5,-11.328125L281.5,-17.640625L281.5,-19.078125C281.64062,-43.171875,290.28125,-61.53125,301.51562,-62.09375C299.5,-59.796875,298.92188,-58.375,298.92188,-55.640625C298.92188,-51.0625,302.23438,-47.75,306.84375,-47.75C312.17188,-47.75,315.625,-51.625,315.625,-57.359375C315.625,-64.390625,310.73438,-69,303.10938,-69C294.03125,-69,286.25,-61.8125,281.5,-49.046875L281.5,-66.828125Z"
+                android:fillColor="#FFFFFF" />
+            <path
+                android:pathData="M376.3125,1.015625L402.53125,0L402.53125,-6.171875L398.92188,-6.171875C390.57812,-6.46875,389.85938,-7.328125,389.85938,-16.671875L389.85938,-67L362.78125,-65.40625L362.78125,-59.21875L370.125,-59.21875C375.59375,-59.078125,376.3125,-58.21875,376.3125,-52.03125L376.3125,-34.359375C376.3125,-26.15625,375.45312,-21.265625,373.29688,-16.671875C370.26562,-10.359375,364.51562,-6.78125,357.45312,-6.78125C352.98438,-6.78125,349.25,-8.359375,347.375,-11.21875C345.5,-13.9375,345.21875,-15.375,345.21875,-23.265625L345.21875,-67L320.01562,-65.40625L320.01562,-59.234375L325.48438,-59.234375C330.95312,-59.09375,331.6875,-58.234375,331.6875,-52.078125L331.6875,-21.109375C331.6875,-13.796875,332.25,-10.5,334.42188,-7.203125C338.29688,-1.296875,345.21875,2,353.85938,2C363.35938,2,369.98438,-1.875,376.3125,-11.078125L376.3125,1.015625Z"
+                android:fillColor="#FFFFFF" />
+        </group>
+    </group>
+</vector>

+ 6 - 0
plugin/mieru/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>

+ 7 - 0
plugin/mieru/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml

@@ -0,0 +1,7 @@
+<?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/mieru/src/main/res/mipmap-hdpi/ic_launcher.png


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


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


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


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


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


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


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


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


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


+ 4 - 0
plugin/mieru/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>

+ 4 - 1
sager.properties

@@ -21,4 +21,7 @@ TROJAN_VERSION_NAME=1.16.0
 TROJAN_VERSION=1
 
 HYSTERIA_VERSION_NAME=1.0.4
-HYSTERIA_VERSION=16
+HYSTERIA_VERSION=16
+
+MIERU_VERSION_NAME=1.5.0
+MIERU_VERSION=1

+ 1 - 0
settings.gradle.kts

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