diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 9ffd2d425ce..b07ea31cd3a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -198,6 +198,8 @@
android:name="io.nekohasekai.sagernet.ui.profile.ShadowQUICSettingsActivity" />
+
anytlsBean = KryoConverters.anytlsDeserialize(byteArray)
TYPE_SHADOWQUIC -> shadowquicBean = KryoConverters.shadowquicDeserialize(byteArray)
TYPE_TRUSTTUNNEL -> trustTunnelBean = KryoConverters.trusttunnelDeserialize(byteArray)
+ TYPE_SNELL -> snellBean = KryoConverters.snellDeserialize(byteArray)
TYPE_CONFIG -> configBean = KryoConverters.configDeserialize(byteArray)
TYPE_CHAIN -> chainBean = KryoConverters.chainDeserialize(byteArray)
@@ -246,6 +252,7 @@ data class ProxyEntity(
TYPE_ANYTLS -> "AnyTLS"
TYPE_SHADOWQUIC -> "ShadowQUIC"
TYPE_TRUSTTUNNEL -> "TrustTunnel"
+ TYPE_SNELL -> "Snell"
TYPE_CHAIN -> chainName
TYPE_CONFIG -> configName
@@ -276,6 +283,7 @@ data class ProxyEntity(
TYPE_ANYTLS -> anytlsBean
TYPE_SHADOWQUIC -> shadowquicBean
TYPE_TRUSTTUNNEL -> trustTunnelBean
+ TYPE_SNELL -> snellBean
TYPE_CONFIG -> configBean
TYPE_CHAIN -> chainBean
@@ -317,6 +325,7 @@ data class ProxyEntity(
is Http3Bean -> toUri()
is AnyTLSBean -> toUri()
is TrustTunnelBean -> toUri()
+ is SnellBean -> toUri()
is ShadowQUICBean -> toUri()
else -> null
}
@@ -382,6 +391,7 @@ data class ProxyEntity(
anytlsBean = null
shadowquicBean = null
trustTunnelBean = null
+ snellBean = null
configBean = null
chainBean = null
@@ -460,6 +470,10 @@ data class ProxyEntity(
type = TYPE_TRUSTTUNNEL
trustTunnelBean = bean
}
+ is SnellBean -> {
+ type = TYPE_SNELL
+ snellBean = bean
+ }
is ConfigBean -> {
type = TYPE_CONFIG
@@ -498,6 +512,7 @@ data class ProxyEntity(
TYPE_ANYTLS -> AnyTLSSettingsActivity::class.java
TYPE_SHADOWQUIC -> ShadowQUICSettingsActivity::class.java
TYPE_TRUSTTUNNEL -> TrustTunnelSettingsActivity::class.java
+ TYPE_SNELL -> SnellSettingsActivity::class.java
TYPE_CONFIG -> ConfigSettingsActivity::class.java
TYPE_CHAIN -> ChainSettingsActivity::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 a941276e17f..bfb36bba699 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt
@@ -29,7 +29,7 @@ import kotlinx.coroutines.launch
@Database(
entities = [ProxyGroup::class, ProxyEntity::class, RuleEntity::class, StatsEntity::class, AssetEntity::class],
- version = 36,
+ version = 37,
autoMigrations = [AutoMigration(
from = 12,
to = 14,
@@ -126,7 +126,8 @@ abstract class SagerDatabase : RoomDatabase() {
SagerDatabase_Migration_8_9,
SagerDatabase_Migration_9_10,
SagerDatabase_Migration_10_11,
- SagerDatabase_Migration_11_12
+ SagerDatabase_Migration_11_12,
+ SagerDatabase_Migration_36_37
)
.fallbackToDestructiveMigrationOnDowngrade()
.allowMainThreadQueries()
diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt b/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt
index 4b1a06edf4c..4e57be74d08 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt
@@ -54,6 +54,7 @@ import io.nekohasekai.sagernet.fmt.socks.SOCKSBean
import io.nekohasekai.sagernet.fmt.ssh.SSHBean
import io.nekohasekai.sagernet.fmt.trojan.TrojanBean
import io.nekohasekai.sagernet.fmt.trusttunnel.TrustTunnelBean
+import io.nekohasekai.sagernet.fmt.snell.SnellBean
import io.nekohasekai.sagernet.fmt.tuic5.Tuic5Bean
import io.nekohasekai.sagernet.fmt.v2ray.StandardV2RayBean
import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig
@@ -1740,6 +1741,19 @@ fun buildV2RayConfig(
}
}
}
+ } else if (bean is SnellBean) {
+ protocol = "snell"
+ settings = LazyOutboundConfigurationObject(this, V2RayConfig.SnellOutboundConfigurationObject().apply {
+ address = bean.serverAddress
+ port = bean.serverPort
+ psk = bean.psk
+ userPSK = bean.userPSK
+ obfsMode = bean.obfsMode
+ obfsHost = bean.obfsHost
+ version = bean.version
+ reuse = bean.reuse
+ mode = bean.mode
+ })
} else if (bean is MieruBean) {
protocol = "mieru"
settings = LazyOutboundConfigurationObject(this,
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 5d99f76628b..60052ce3df4 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/fmt/KryoConverters.java
+++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/KryoConverters.java
@@ -46,6 +46,7 @@
import io.nekohasekai.sagernet.fmt.ssh.SSHBean;
import io.nekohasekai.sagernet.fmt.trojan.TrojanBean;
import io.nekohasekai.sagernet.fmt.trusttunnel.TrustTunnelBean;
+import io.nekohasekai.sagernet.fmt.snell.SnellBean;
import io.nekohasekai.sagernet.fmt.tuic5.Tuic5Bean;
import io.nekohasekai.sagernet.fmt.v2ray.VLESSBean;
import io.nekohasekai.sagernet.fmt.v2ray.VMessBean;
@@ -189,6 +190,12 @@ public static TrustTunnelBean trusttunnelDeserialize(byte[] bytes) {
return deserialize(new TrustTunnelBean(), bytes);
}
+ @TypeConverter
+ public static SnellBean snellDeserialize(byte[] bytes) {
+ if (bytes == null || bytes.length == 0) return null;
+ return deserialize(new SnellBean(), bytes);
+ }
+
@TypeConverter
public static ConfigBean configDeserialize(byte[] bytes) {
if (bytes == null || bytes.length == 0) return null;
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 9f7498c7dd8..ce2be09f484 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/fmt/TypeMap.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/TypeMap.kt
@@ -42,6 +42,7 @@ object TypeMap : HashMap() {
this["anytls"] = ProxyEntity.TYPE_ANYTLS
this["shadowquic"] = ProxyEntity.TYPE_SHADOWQUIC
this["trusttunnel"] = ProxyEntity.TYPE_TRUSTTUNNEL
+ this["snell"] = ProxyEntity.TYPE_SNELL
}
val reversed = HashMap()
diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/snell/SnellBean.java b/app/src/main/java/io/nekohasekai/sagernet/fmt/snell/SnellBean.java
new file mode 100644
index 00000000000..b1966da5f76
--- /dev/null
+++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/snell/SnellBean.java
@@ -0,0 +1,124 @@
+/******************************************************************************
+ * *
+ * Copyright (C) 2026 Snell support for Exclave *
+ * *
+ * 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. *
+ * *
+ ******************************************************************************/
+
+package io.nekohasekai.sagernet.fmt.snell;
+
+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 SnellBean extends AbstractBean {
+
+ public static final int VERSION_4 = 4;
+ public static final int VERSION_6 = 6;
+
+ public static final String OBFS_NONE = "none";
+ public static final String OBFS_HTTP = "http";
+ public static final String OBFS_TLS = "tls";
+
+ public static final String MODE_DEFAULT = "default";
+ public static final String MODE_UNSHAPED = "unshaped";
+ public static final String MODE_UNSAFE_RAW = "unsafe-raw";
+
+ public String psk;
+ /** sing-box private extension; only compatible with sing-box Snell server. */
+ public String userPSK;
+ public String obfsMode;
+ /** v4-only */
+ public String obfsHost;
+ public Integer version;
+ public Boolean reuse;
+ /** v6-only */
+ public String mode;
+
+ @Override
+ public void initializeDefaultValues() {
+ super.initializeDefaultValues();
+ if (psk == null) psk = "";
+ if (userPSK == null) userPSK = "";
+ if (obfsMode == null || obfsMode.isEmpty()) obfsMode = OBFS_NONE;
+ if (obfsHost == null) obfsHost = "";
+ if (version == null || version == 0) version = VERSION_4;
+ if (reuse == null) reuse = true;
+ if (mode == null || mode.isEmpty()) mode = MODE_DEFAULT;
+ }
+
+ @Override
+ public void serialize(ByteBufferOutput output) {
+ // v3: rename obfs->obfsMode, add userPSK; version 4/6 only
+ output.writeInt(3);
+ super.serialize(output);
+ output.writeString(psk);
+ output.writeString(obfsMode);
+ output.writeInt(version);
+ output.writeBoolean(reuse);
+ output.writeString(obfsHost);
+ output.writeString(mode);
+ output.writeString(userPSK);
+ }
+
+ @Override
+ public void deserialize(ByteBufferInput input) {
+ int ver = input.readInt();
+ super.deserialize(input);
+ psk = input.readString();
+ obfsMode = input.readString();
+ // migrate legacy "off" -> "none" for old profiles only
+ if ("off".equals(obfsMode)) {
+ obfsMode = OBFS_NONE;
+ }
+ version = input.readInt();
+ // migrate legacy 3/5 to 4
+ if (version != null && version != VERSION_4 && version != VERSION_6) {
+ version = VERSION_4;
+ }
+ if (ver >= 1) {
+ reuse = input.readBoolean();
+ }
+ if (ver >= 2) {
+ obfsHost = input.readString();
+ mode = input.readString();
+ }
+ if (ver >= 3) {
+ userPSK = input.readString();
+ }
+ }
+
+ @Override
+ public String network() {
+ return "tcp,udp";
+ }
+
+ @NotNull
+ @Override
+ public SnellBean clone() {
+ return KryoConverters.deserialize(new SnellBean(), KryoConverters.serialize(this));
+ }
+
+ public static final Creator CREATOR = new CREATOR<>() {
+ @NonNull
+ @Override
+ public SnellBean newInstance() {
+ return new SnellBean();
+ }
+
+ @Override
+ public SnellBean[] newArray(int size) {
+ return new SnellBean[size];
+ }
+ };
+}
diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/snell/SnellFmt.kt b/app/src/main/java/io/nekohasekai/sagernet/fmt/snell/SnellFmt.kt
new file mode 100644
index 00000000000..4ad6568a578
--- /dev/null
+++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/snell/SnellFmt.kt
@@ -0,0 +1,64 @@
+package io.nekohasekai.sagernet.fmt.snell
+
+import io.nekohasekai.sagernet.ktx.*
+import libexclavecore.Libexclavecore
+
+fun parseSnell(url: String): SnellBean {
+ val link = Libexclavecore.parseURL(url)
+ return SnellBean().apply {
+ name = link.fragment
+ serverAddress = link.host.ifEmpty { error("empty host") }
+ serverPort = link.port
+ psk = when {
+ link.username.isNotEmpty() -> link.username
+ link.password.isNotEmpty() -> link.password
+ else -> link.queryParameter("psk") ?: ""
+ }
+ link.queryParameter("version")?.toIntOrNull()?.also {
+ if (it != 4 && it != 6) error("snell version must be 4 or 6")
+ version = it
+ }
+ // accept only exact values; map legacy "off" only from share links that used it historically
+ (link.queryParameter("obfsMode") ?: link.queryParameter("obfs"))?.also {
+ obfsMode = if (it == "off") SnellBean.OBFS_NONE else it
+ }
+ link.queryParameter("obfs-host")?.also { obfsHost = it }
+ link.queryParameter("mode")?.also { mode = it }
+ link.queryParameter("user-psk")?.also { userPSK = it }
+ link.queryParameter("userPSK")?.also { userPSK = it }
+ link.queryParameter("reuse")?.also {
+ reuse = it == "1" || it.equals("true", ignoreCase = true)
+ }
+ initializeDefaultValues()
+ }
+}
+
+fun SnellBean.toUri(): String? {
+ val builder = Libexclavecore.newURL("snell").apply {
+ setHostPort(serverAddress.ifEmpty { error("empty server address") }, serverPort)
+ if (!psk.isNullOrEmpty()) {
+ username = psk
+ }
+ if (name.isNotEmpty()) {
+ fragment = name
+ }
+ }
+ val v = version ?: 4
+ builder.addQueryParameter("version", v.toString())
+ if (!obfsMode.isNullOrEmpty() && obfsMode != SnellBean.OBFS_NONE) {
+ builder.addQueryParameter("obfsMode", obfsMode)
+ }
+ if (!obfsHost.isNullOrEmpty()) {
+ builder.addQueryParameter("obfs-host", obfsHost)
+ }
+ if (v >= 6 && !mode.isNullOrEmpty() && mode != SnellBean.MODE_DEFAULT) {
+ builder.addQueryParameter("mode", mode)
+ }
+ if (!userPSK.isNullOrEmpty()) {
+ builder.addQueryParameter("user-psk", userPSK)
+ }
+ if (reuse == true) {
+ builder.addQueryParameter("reuse", "1")
+ }
+ return builder.string
+}
diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/V2RayConfig.java b/app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/V2RayConfig.java
index 0ecbbd9906a..f618744168f 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/V2RayConfig.java
+++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/V2RayConfig.java
@@ -696,6 +696,8 @@ protected Class extends OutboundConfigurationObject> getType() {
return MieruOutboundConfigurationObject.class;
case "trusttunnel":
return TrustTunnelOutboundConfigurationObject.class;
+ case "snell":
+ return SnellOutboundConfigurationObject.class;
}
return null;
}
@@ -985,7 +987,22 @@ public static class JuicityOutboundConfigurationObject implements OutboundConfig
}
- public static class MieruOutboundConfigurationObject implements OutboundConfigurationObject {
+
+ public static class SnellOutboundConfigurationObject implements OutboundConfigurationObject {
+
+ public String address;
+ public Integer port;
+ public String psk;
+ public String userPSK;
+ public String obfsMode;
+ public String obfsHost;
+ public Integer version;
+ public Boolean reuse;
+ public String mode;
+
+ }
+
+public static class MieruOutboundConfigurationObject implements OutboundConfigurationObject {
public String address;
public Integer port;
diff --git a/app/src/main/java/io/nekohasekai/sagernet/group/ClashYAMLParser.kt b/app/src/main/java/io/nekohasekai/sagernet/group/ClashYAMLParser.kt
index bcc79e8fe94..871c83fe755 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/group/ClashYAMLParser.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/group/ClashYAMLParser.kt
@@ -38,6 +38,7 @@ import io.nekohasekai.sagernet.fmt.socks.SOCKSBean
import io.nekohasekai.sagernet.fmt.ssh.SSHBean
import io.nekohasekai.sagernet.fmt.trojan.TrojanBean
import io.nekohasekai.sagernet.fmt.trusttunnel.TrustTunnelBean
+import io.nekohasekai.sagernet.fmt.snell.SnellBean
import io.nekohasekai.sagernet.fmt.tuic5.Tuic5Bean
import io.nekohasekai.sagernet.fmt.tuic5.supportedTuic5CongestionControl
import io.nekohasekai.sagernet.fmt.tuic5.supportedTuic5RelayMode
@@ -771,6 +772,29 @@ fun parseClashProxy(proxy: Map): List {
}*/
name = proxy.getString("name")
})
+ }
+ "snell" -> {
+ return listOf(SnellBean().apply {
+ serverAddress = proxy.getString("server") ?: return listOf()
+ serverPort = proxy.getInt("port")?.takeIf { it > 0 } ?: return listOf()
+ psk = proxy.getString("psk") ?: ""
+ proxy.getString("user-psk")?.also { userPSK = it }
+ proxy.getInt("version")?.also {
+ if (it != 4 && it != 6) return listOf()
+ version = it
+ }
+ proxy.getString("obfs")?.also {
+ obfsMode = if (it == "off") SnellBean.OBFS_NONE else it
+ }
+ proxy.getObject("obfs-opts")?.also { opts ->
+ opts.getString("mode")?.also {
+ obfsMode = if (it == "off") SnellBean.OBFS_NONE else it
+ }
+ opts.getString("host")?.also { obfsHost = it }
+ }
+ proxy.getString("mode")?.also { mode = it }
+ name = proxy.getString("name")
+ })
}
"anytls" -> {
return listOf(AnyTLSBean().apply {
diff --git a/app/src/main/java/io/nekohasekai/sagernet/ktx/Formats.kt b/app/src/main/java/io/nekohasekai/sagernet/ktx/Formats.kt
index 3b822747c73..9e45aa5a772 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/ktx/Formats.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/ktx/Formats.kt
@@ -36,6 +36,7 @@ import io.nekohasekai.sagernet.fmt.shadowsocksr.parseShadowsocksR
import io.nekohasekai.sagernet.fmt.socks.parseSOCKS
import io.nekohasekai.sagernet.fmt.ssh.parseSSH
import io.nekohasekai.sagernet.fmt.trusttunnel.parseTrustTunnel
+import io.nekohasekai.sagernet.fmt.snell.parseSnell
import io.nekohasekai.sagernet.fmt.tuic5.parseTuic
import io.nekohasekai.sagernet.fmt.v2ray.parseV2Ray
import io.nekohasekai.sagernet.fmt.wireguard.parseWireGuard
@@ -125,6 +126,10 @@ fun parseShareLinks(text: String): List {
runCatching {
entities.add(parseAnyTLS(this))
}
+ } else if (startsWith("snell://", ignoreCase = true)) {
+ runCatching {
+ entities.add(parseSnell(this))
+ }
} else if (startsWith("ssh://", ignoreCase = true)) {
runCatching {
entities.add(parseSSH(this))
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 a524a223a70..c812e240c81 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt
@@ -520,6 +520,9 @@ class ConfigurationFragment @JvmOverloads constructor(
R.id.action_new_trusttunnel -> {
startActivity(Intent(requireActivity(), TrustTunnelSettingsActivity::class.java))
}
+ R.id.action_new_snell -> {
+ startActivity(Intent(requireActivity(), SnellSettingsActivity::class.java))
+ }
R.id.action_new_config -> {
startActivity(Intent(requireActivity(), ConfigSettingsActivity::class.java))
}
diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/profile/SnellSettingsActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/profile/SnellSettingsActivity.kt
new file mode 100644
index 00000000000..ce8b997b75d
--- /dev/null
+++ b/app/src/main/java/io/nekohasekai/sagernet/ui/profile/SnellSettingsActivity.kt
@@ -0,0 +1,76 @@
+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.snell.SnellBean
+import io.nekohasekai.sagernet.ktx.unwrapIDN
+import io.nekohasekai.sagernet.widget.SimpleMenuPreference
+
+class SnellSettingsActivity : ProfileSettingsActivity() {
+
+ override fun createEntity() = SnellBean()
+
+ override fun SnellBean.init() {
+ DataStore.profileName = name
+ DataStore.serverAddress = serverAddress
+ DataStore.serverPort = serverPort
+ DataStore.serverPassword = psk
+ DataStore.serverPassword1 = userPSK
+ DataStore.serverObfs = obfsMode
+ DataStore.serverHost = obfsHost
+ DataStore.serverProtocolVersion = version ?: SnellBean.VERSION_4
+ DataStore.serverMux = reuse == true
+ DataStore.serverProtocolParam = mode
+ }
+
+ override fun SnellBean.serialize() {
+ name = DataStore.profileName
+ serverAddress = DataStore.serverAddress.unwrapIDN()
+ serverPort = DataStore.serverPort
+ psk = DataStore.serverPassword
+ userPSK = DataStore.serverPassword1
+ obfsMode = DataStore.serverObfs?.ifEmpty { SnellBean.OBFS_NONE } ?: SnellBean.OBFS_NONE
+ obfsHost = DataStore.serverHost
+ version = DataStore.serverProtocolVersion.takeIf { it == 4 || it == 6 } ?: SnellBean.VERSION_4
+ reuse = DataStore.serverMux
+ mode = DataStore.serverProtocolParam?.ifEmpty { SnellBean.MODE_DEFAULT } ?: SnellBean.MODE_DEFAULT
+ }
+
+ override fun PreferenceFragmentCompat.createPreferences(
+ savedInstanceState: Bundle?,
+ rootKey: String?,
+ ) {
+ addPreferencesFromResource(R.xml.snell_preferences)
+ findPreference(Key.SERVER_PORT)!!.apply {
+ setOnBindEditTextListener(EditTextPreferenceModifiers.Port)
+ }
+ findPreference(Key.SERVER_PASSWORD)!!.apply {
+ summaryProvider = PasswordSummaryProvider
+ }
+ findPreference(Key.SERVER_PASSWORD1)!!.apply {
+ summaryProvider = PasswordSummaryProvider
+ }
+ val versionPref = findPreference(Key.SERVER_PROTOCOL)!!
+ val modePref = findPreference(Key.SERVER_PROTOCOL_PARAM)!!
+ val obfsPref = findPreference(Key.SERVER_OBFS)!!
+ val obfsHostPref = findPreference(Key.SERVER_HOST)!!
+ fun updateVisibility(v: Int) {
+ val isV6 = v >= 6
+ modePref.isVisible = isV6
+ // v4-only: obfs mode/host
+ obfsPref.isVisible = !isV6
+ obfsHostPref.isVisible = !isV6
+ }
+ val cur = versionPref.value?.toIntOrNull() ?: DataStore.serverProtocolVersion
+ updateVisibility(cur)
+ versionPref.setOnPreferenceChangeListener { _, newValue ->
+ updateVisibility((newValue as? String)?.toIntOrNull() ?: 4)
+ true
+ }
+ }
+}
diff --git a/app/src/main/res/menu/add_profile_menu.xml b/app/src/main/res/menu/add_profile_menu.xml
index 195413bdde9..fe3a91319f4 100644
--- a/app/src/main/res/menu/add_profile_menu.xml
+++ b/app/src/main/res/menu/add_profile_menu.xml
@@ -55,6 +55,9 @@
+
diff --git a/app/src/main/res/values/snell_arrays.xml b/app/src/main/res/values/snell_arrays.xml
new file mode 100644
index 00000000000..6c3818733f4
--- /dev/null
+++ b/app/src/main/res/values/snell_arrays.xml
@@ -0,0 +1,31 @@
+
+
+
+ - 4
+ - 6 (beta)
+
+
+ - 4
+ - 6
+
+
+ - none
+ - http
+ - tls
+
+
+ - none
+ - http
+ - tls
+
+
+ - default
+ - unshaped
+ - unsafe-raw
+
+
+ - default
+ - unshaped
+ - unsafe-raw
+
+
diff --git a/app/src/main/res/values/snell_strings.xml b/app/src/main/res/values/snell_strings.xml
new file mode 100644
index 00000000000..f709422d2ca
--- /dev/null
+++ b/app/src/main/res/values/snell_strings.xml
@@ -0,0 +1,11 @@
+
+
+ Snell version
+ Connection reuse
+ Reuse the TCP connection
+ Snell
+ Obfs mode
+ Obfs host (v4)
+ Snell v6 mode
+ User PSK (sing-box extension)
+
diff --git a/app/src/main/res/xml/snell_preferences.xml b/app/src/main/res/xml/snell_preferences.xml
new file mode 100644
index 00000000000..648babdc047
--- /dev/null
+++ b/app/src/main/res/xml/snell_preferences.xml
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+