diff --git a/app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/3.json b/app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/3.json
new file mode 100644
index 0000000..491eb6f
--- /dev/null
+++ b/app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/3.json
@@ -0,0 +1,360 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 3,
+ "identityHash": "cff00d0142d9e53d2ca24a6a55cd213c",
+ "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, `isSelector` INTEGER NOT NULL, `frontProxy` INTEGER NOT NULL, `landingProxy` 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
+ },
+ {
+ "fieldPath": "isSelector",
+ "columnName": "isSelector",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "frontProxy",
+ "columnName": "frontProxy",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "landingProxy",
+ "columnName": "landingProxy",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "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, `vmessBean` BLOB, `trojanBean` BLOB, `trojanGoBean` BLOB, `mieruBean` BLOB, `naiveBean` BLOB, `hysteriaBean` BLOB, `tuicBean` BLOB, `sshBean` BLOB, `wgBean` BLOB, `shadowTLSBean` BLOB, `chainBean` BLOB, `nekoBean` BLOB, `configBean` 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": "vmessBean",
+ "columnName": "vmessBean",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "trojanBean",
+ "columnName": "trojanBean",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "trojanGoBean",
+ "columnName": "trojanGoBean",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "mieruBean",
+ "columnName": "mieruBean",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "naiveBean",
+ "columnName": "naiveBean",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "hysteriaBean",
+ "columnName": "hysteriaBean",
+ "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": "shadowTLSBean",
+ "columnName": "shadowTLSBean",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "chainBean",
+ "columnName": "chainBean",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "nekoBean",
+ "columnName": "nekoBean",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "configBean",
+ "columnName": "configBean",
+ "affinity": "BLOB",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "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, `outbound` INTEGER NOT NULL, `packages` TEXT NOT NULL)",
+ "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": "outbound",
+ "columnName": "outbound",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "packages",
+ "columnName": "packages",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'cff00d0142d9e53d2ca24a6a55cd213c')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index c132174..bd284c0 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -164,6 +164,9 @@
+
diff --git a/app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt b/app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt
index feaca9b..0f20337 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt
@@ -10,6 +10,8 @@ import io.nekohasekai.sagernet.fmt.ConfigBuildResult
import io.nekohasekai.sagernet.fmt.buildConfig
import io.nekohasekai.sagernet.fmt.hysteria.HysteriaBean
import io.nekohasekai.sagernet.fmt.hysteria.buildHysteria1Config
+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.trojan_go.TrojanGoBean
@@ -65,6 +67,11 @@ abstract class BoxInstance(
pluginConfigs[port] = profile.type to bean.buildTrojanGoConfig(port)
}
+ is MieruBean -> {
+ initPlugin("mieru-plugin")
+ pluginConfigs[port] = profile.type to bean.buildMieruConfig(port)
+ }
+
is NaiveBean -> {
initPlugin("naive-plugin")
pluginConfigs[port] = profile.type to bean.buildNaiveConfig(port)
@@ -129,6 +136,25 @@ abstract class BoxInstance(
processes.start(commands)
}
+ bean is MieruBean -> {
+ val configFile = File(
+ cacheDir, "mieru_" + SystemClock.elapsedRealtime() + ".json"
+ )
+
+ configFile.parentFile?.mkdirs()
+ configFile.writeText(config)
+ cacheFiles.add(configFile)
+
+ val envMap = mutableMapOf()
+ envMap["MIERU_CONFIG_JSON_FILE"] = configFile.absolutePath
+
+ val commands = mutableListOf(
+ initPlugin("mieru-plugin").path, "run",
+ )
+
+ processes.start(commands, envMap)
+ }
+
bean is NaiveBean -> {
val configFile = File(
cacheDir, "naive_" + SystemClock.elapsedRealtime() + ".json"
diff --git a/app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt b/app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt
index 401b468..45f0a31 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt
@@ -11,6 +11,8 @@ import io.nekohasekai.sagernet.fmt.http.HttpBean
import io.nekohasekai.sagernet.fmt.http.toUri
import io.nekohasekai.sagernet.fmt.hysteria.*
import io.nekohasekai.sagernet.fmt.internal.ChainBean
+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
@@ -56,6 +58,7 @@ data class ProxyEntity(
var vmessBean: VMessBean? = null,
var trojanBean: TrojanBean? = null,
var trojanGoBean: TrojanGoBean? = null,
+ var mieruBean: MieruBean? = null,
var naiveBean: NaiveBean? = null,
var hysteriaBean: HysteriaBean? = null,
var tuicBean: TuicBean? = null,
@@ -75,6 +78,7 @@ data class ProxyEntity(
const val TYPE_TROJAN = 6
const val TYPE_TROJAN_GO = 7
+ const val TYPE_MIERU = 21
const val TYPE_NAIVE = 9
const val TYPE_HYSTERIA = 15
const val TYPE_TUIC = 20
@@ -161,6 +165,7 @@ data class ProxyEntity(
TYPE_VMESS -> vmessBean = KryoConverters.vmessDeserialize(byteArray)
TYPE_TROJAN -> trojanBean = KryoConverters.trojanDeserialize(byteArray)
TYPE_TROJAN_GO -> trojanGoBean = KryoConverters.trojanGoDeserialize(byteArray)
+ TYPE_MIERU -> mieruBean = KryoConverters.mieruDeserialize(byteArray)
TYPE_NAIVE -> naiveBean = KryoConverters.naiveDeserialize(byteArray)
TYPE_HYSTERIA -> hysteriaBean = KryoConverters.hysteriaDeserialize(byteArray)
TYPE_SSH -> sshBean = KryoConverters.sshDeserialize(byteArray)
@@ -180,6 +185,7 @@ data class ProxyEntity(
TYPE_VMESS -> if (vmessBean!!.isVLESS) "VLESS" else "VMess"
TYPE_TROJAN -> "Trojan"
TYPE_TROJAN_GO -> "Trojan-Go"
+ TYPE_MIERU -> "Mieru"
TYPE_NAIVE -> "Naïve"
TYPE_HYSTERIA -> "Hysteria" + hysteriaBean!!.protocolVersion
TYPE_SSH -> "SSH"
@@ -203,6 +209,7 @@ data class ProxyEntity(
TYPE_VMESS -> vmessBean
TYPE_TROJAN -> trojanBean
TYPE_TROJAN_GO -> trojanGoBean
+ TYPE_MIERU -> mieruBean
TYPE_NAIVE -> naiveBean
TYPE_HYSTERIA -> hysteriaBean
TYPE_SSH -> sshBean
@@ -270,6 +277,11 @@ data class ProxyEntity(
append(bean.buildTrojanGoConfig(port))
}
+ is MieruBean -> {
+ append("\n\n")
+ append(bean.buildMieruConfig(port))
+ }
+
is NaiveBean -> {
append("\n\n")
append(bean.buildNaiveConfig(port))
@@ -289,6 +301,7 @@ data class ProxyEntity(
fun needExternal(): Boolean {
return when (type) {
TYPE_TROJAN_GO -> true
+ TYPE_MIERU -> true
TYPE_NAIVE -> true
TYPE_HYSTERIA -> !hysteriaBean!!.canUseSingBox()
TYPE_NEKO -> true
@@ -319,6 +332,7 @@ data class ProxyEntity(
vmessBean = null
trojanBean = null
trojanGoBean = null
+ mieruBean = null
naiveBean = null
hysteriaBean = null
sshBean = null
@@ -360,6 +374,11 @@ data class ProxyEntity(
trojanGoBean = bean
}
+ is MieruBean -> {
+ type = TYPE_MIERU
+ mieruBean = bean
+ }
+
is NaiveBean -> {
type = TYPE_NAIVE
naiveBean = bean
@@ -419,6 +438,7 @@ data class ProxyEntity(
TYPE_VMESS -> VMessSettingsActivity::class.java
TYPE_TROJAN -> TrojanSettingsActivity::class.java
TYPE_TROJAN_GO -> TrojanGoSettingsActivity::class.java
+ TYPE_MIERU -> MieruSettingsActivity::class.java
TYPE_NAIVE -> NaiveSettingsActivity::class.java
TYPE_HYSTERIA -> HysteriaSettingsActivity::class.java
TYPE_SSH -> SSHSettingsActivity::class.java
diff --git a/app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt b/app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt
index 4d57ed3..9f63fbb 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt
@@ -15,7 +15,7 @@ import kotlinx.coroutines.launch
@Database(
entities = [ProxyGroup::class, ProxyEntity::class, RuleEntity::class],
- version = 2
+ version = 3
)
@TypeConverters(value = [KryoConverters::class, GsonConverters::class])
@GenerateRoomMigrations
diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/KryoConverters.java b/app/src/main/java/io/nekohasekai/sagernet/fmt/KryoConverters.java
index b9a6338..627ea35 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/fmt/KryoConverters.java
+++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/KryoConverters.java
@@ -13,6 +13,7 @@ import io.nekohasekai.sagernet.database.SubscriptionBean;
import io.nekohasekai.sagernet.fmt.http.HttpBean;
import io.nekohasekai.sagernet.fmt.hysteria.HysteriaBean;
import io.nekohasekai.sagernet.fmt.internal.ChainBean;
+import io.nekohasekai.sagernet.fmt.mieru.MieruBean;
import io.nekohasekai.sagernet.fmt.naive.NaiveBean;
import io.nekohasekai.sagernet.fmt.shadowsocks.ShadowsocksBean;
import moe.matsuri.nb4a.proxy.shadowtls.ShadowTLSBean;
@@ -99,6 +100,12 @@ public class KryoConverters {
return deserialize(new TrojanGoBean(), bytes);
}
+ @TypeConverter
+ public static MieruBean mieruDeserialize(byte[] bytes) {
+ if (JavaUtil.isEmpty(bytes)) return null;
+ return deserialize(new MieruBean(), bytes);
+ }
+
@TypeConverter
public static NaiveBean naiveDeserialize(byte[] bytes) {
if (JavaUtil.isEmpty(bytes)) return null;
diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/PluginEntry.kt b/app/src/main/java/io/nekohasekai/sagernet/fmt/PluginEntry.kt
index ee26953..42769e2 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/fmt/PluginEntry.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/PluginEntry.kt
@@ -14,6 +14,16 @@ enum class PluginEntry(
SagerNet.application.getString(R.string.action_trojan_go),
"io.nekohasekai.sagernet.plugin.trojan_go"
),
+ MieruProxy(
+ "mieru-plugin",
+ SagerNet.application.getString(R.string.action_mieru),
+ "moe.matsuri.exe.mieru",
+ DownloadSource(
+ playStore = false,
+ fdroid = false,
+ downloadLink = "https://github.com/MatsuriDayo/plugins/releases?q=mieru"
+ )
+ ),
NaiveProxy(
"naive-plugin",
SagerNet.application.getString(R.string.action_naive),
diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/TypeMap.kt b/app/src/main/java/io/nekohasekai/sagernet/fmt/TypeMap.kt
index c55c58b..12554a1 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/fmt/TypeMap.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/TypeMap.kt
@@ -10,6 +10,7 @@ object TypeMap : HashMap() {
this["vmess"] = ProxyEntity.TYPE_VMESS
this["trojan"] = ProxyEntity.TYPE_TROJAN
this["trojan-go"] = ProxyEntity.TYPE_TROJAN_GO
+ this["mieru"] = ProxyEntity.TYPE_MIERU
this["naive"] = ProxyEntity.TYPE_NAIVE
this["hysteria"] = ProxyEntity.TYPE_HYSTERIA
this["ssh"] = ProxyEntity.TYPE_SSH
diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/mieru/MieruBean.java b/app/src/main/java/io/nekohasekai/sagernet/fmt/mieru/MieruBean.java
new file mode 100644
index 0000000..0a7c21f
--- /dev/null
+++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/mieru/MieruBean.java
@@ -0,0 +1,89 @@
+/******************************************************************************
+ * Copyright (C) 2022 by nekohasekai *
+ * *
+ * 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 . *
+ * *
+ ******************************************************************************/
+
+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 String protocol;
+ public String username;
+ public String password;
+ public Integer mtu;
+
+ @Override
+ public void initializeDefaultValues() {
+ super.initializeDefaultValues();
+ if (protocol == null) 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.writeString(protocol);
+ output.writeString(username);
+ output.writeString(password);
+ if (protocol.equals("UDP")) {
+ output.writeInt(mtu);
+ }
+ }
+
+ @Override
+ public void deserialize(ByteBufferInput input) {
+ int version = input.readInt();
+ super.deserialize(input);
+ protocol = input.readString();
+ username = input.readString();
+ password = input.readString();
+ if (protocol.equals("UDP")) {
+ mtu = input.readInt();
+ }
+ }
+
+ @NotNull
+ @Override
+ public MieruBean clone() {
+ return KryoConverters.deserialize(new MieruBean(), KryoConverters.serialize(this));
+ }
+
+ public static final Creator CREATOR = new CREATOR() {
+ @NonNull
+ @Override
+ public MieruBean newInstance() {
+ return new MieruBean();
+ }
+
+ @Override
+ public MieruBean[] newArray(int size) {
+ return new MieruBean[size];
+ }
+ };
+}
diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/mieru/MieruFmt.kt b/app/src/main/java/io/nekohasekai/sagernet/fmt/mieru/MieruFmt.kt
new file mode 100644
index 0000000..4085216
--- /dev/null
+++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/mieru/MieruFmt.kt
@@ -0,0 +1,53 @@
+/******************************************************************************
+ * Copyright (C) 2022 by nekohasekai *
+ * *
+ * 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 . *
+ * *
+ ******************************************************************************/
+
+package io.nekohasekai.sagernet.fmt.mieru
+
+import io.nekohasekai.sagernet.ktx.toStringPretty
+import org.json.JSONArray
+import org.json.JSONObject
+
+fun MieruBean.buildMieruConfig(port: Int): String {
+ val serverInfo = JSONArray().apply {
+ put(JSONObject().apply {
+ put("ipAddress", finalAddress)
+ put("portBindings", JSONArray().apply {
+ put(JSONObject().apply {
+ put("port", finalPort)
+ put("protocol", protocol)
+ })
+ })
+ })
+ }
+ return JSONObject().apply {
+ put("activeProfile", "default")
+ put("socks5Port", port)
+ // TODO: follow NekoBox logging level.
+ put("loggingLevel", "INFO")
+ put("profiles", JSONArray().apply {
+ put(JSONObject().apply {
+ put("profileName", "default")
+ put("user", JSONObject().apply {
+ put("name", username)
+ put("password", password)
+ })
+ put("servers", serverInfo)
+ })
+ })
+ }.toStringPretty()
+}
diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt
index 301f157..5b1c50f 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt
@@ -362,6 +362,10 @@ class ConfigurationFragment @JvmOverloads constructor(
startActivity(Intent(requireActivity(), TrojanGoSettingsActivity::class.java))
}
+ R.id.action_new_mieru -> {
+ startActivity(Intent(requireActivity(), MieruSettingsActivity::class.java))
+ }
+
R.id.action_new_naive -> {
startActivity(Intent(requireActivity(), NaiveSettingsActivity::class.java))
}
diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/profile/MieruSettingsActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/profile/MieruSettingsActivity.kt
new file mode 100644
index 0000000..f9195fb
--- /dev/null
+++ b/app/src/main/java/io/nekohasekai/sagernet/ui/profile/MieruSettingsActivity.kt
@@ -0,0 +1,77 @@
+/******************************************************************************
+ * *
+ * Copyright (C) 2021 by nekohasekai *
+ * *
+ * 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 . *
+ * *
+ ******************************************************************************/
+
+package io.nekohasekai.sagernet.ui.profile
+
+import android.os.Bundle
+import androidx.preference.EditTextPreference
+import androidx.preference.PreferenceFragmentCompat
+import io.nekohasekai.sagernet.Key
+import io.nekohasekai.sagernet.R
+import io.nekohasekai.sagernet.database.DataStore
+import io.nekohasekai.sagernet.database.preference.EditTextPreferenceModifiers
+import io.nekohasekai.sagernet.fmt.mieru.MieruBean
+import io.nekohasekai.sagernet.ktx.applyDefaultValues
+import moe.matsuri.nb4a.ui.SimpleMenuPreference
+
+class MieruSettingsActivity : ProfileSettingsActivity() {
+
+ override fun createEntity() = MieruBean().applyDefaultValues()
+
+ override fun MieruBean.init() {
+ DataStore.profileName = name
+ DataStore.serverAddress = serverAddress
+ DataStore.serverPort = serverPort
+ DataStore.serverProtocol = protocol
+ DataStore.serverUsername = username
+ DataStore.serverPassword = password
+ DataStore.mtu = mtu
+ }
+
+ override fun MieruBean.serialize() {
+ name = DataStore.profileName
+ serverAddress = DataStore.serverAddress
+ serverPort = DataStore.serverPort
+ protocol = DataStore.serverProtocol
+ username = DataStore.serverUsername
+ password = DataStore.serverPassword
+ mtu = DataStore.mtu
+ }
+
+ override fun PreferenceFragmentCompat.createPreferences(
+ savedInstanceState: Bundle?,
+ rootKey: String?,
+ ) {
+ addPreferencesFromResource(R.xml.mieru_preferences)
+ findPreference(Key.SERVER_PORT)!!.apply {
+ setOnBindEditTextListener(EditTextPreferenceModifiers.Port)
+ }
+ findPreference(Key.SERVER_PASSWORD)!!.apply {
+ summaryProvider = PasswordSummaryProvider
+ }
+ val protocol = findPreference(Key.SERVER_PROTOCOL)!!
+ val mtu = findPreference(Key.MTU)!!
+ mtu.isVisible = protocol.value.equals("UDP")
+ protocol.setOnPreferenceChangeListener { _, newValue ->
+ mtu.isVisible = newValue.equals("UDP")
+ true
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/res/menu/add_profile_menu.xml b/app/src/main/res/menu/add_profile_menu.xml
index 072cf8d..129d320 100644
--- a/app/src/main/res/menu/add_profile_menu.xml
+++ b/app/src/main/res/menu/add_profile_menu.xml
@@ -45,6 +45,9 @@
+
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
index 1e58c64..0be3670 100644
--- a/app/src/main/res/values/arrays.xml
+++ b/app/src/main/res/values/arrays.xml
@@ -332,6 +332,10 @@
- TPROXY
+
+ - TCP
+ - UDP
+
- HTTPS
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index ab6bc3d..c9e09ed 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -214,6 +214,7 @@
VMess
Trojan
Trojan Go
+ Mieru
Naïve
Ping Tunnel
Hysteria
diff --git a/app/src/main/res/xml/mieru_preferences.xml b/app/src/main/res/xml/mieru_preferences.xml
new file mode 100644
index 0000000..19a3dab
--- /dev/null
+++ b/app/src/main/res/xml/mieru_preferences.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file