mirror of
https://github.com/MatsuriDayo/NekoBoxForAndroid.git
synced 2025-12-19 06:30:05 +08:00
tuic v5 support
This commit is contained in:
parent
34a3012772
commit
4b1800d782
@ -78,14 +78,11 @@ object Key {
|
||||
const val SERVER_USERNAME = "serverUsername"
|
||||
const val SERVER_PASSWORD = "serverPassword"
|
||||
const val SERVER_METHOD = "serverMethod"
|
||||
const val SERVER_PLUGIN = "serverPlugin"
|
||||
const val SERVER_PLUGIN_CONFIGURE = "serverPluginConfigure"
|
||||
const val SERVER_PASSWORD1 = "serverPassword1"
|
||||
|
||||
const val SERVER_PROTOCOL = "serverProtocol"
|
||||
const val SERVER_OBFS = "serverObfs"
|
||||
|
||||
const val SERVER_SECURITY = "serverSecurity"
|
||||
const val SERVER_NETWORK = "serverNetwork"
|
||||
const val SERVER_HOST = "serverHost"
|
||||
const val SERVER_PATH = "serverPath"
|
||||
|
||||
@ -17,6 +17,7 @@ import io.nekohasekai.sagernet.fmt.trojan_go.TrojanGoBean
|
||||
import io.nekohasekai.sagernet.fmt.trojan_go.buildTrojanGoConfig
|
||||
import io.nekohasekai.sagernet.fmt.tuic.TuicBean
|
||||
import io.nekohasekai.sagernet.fmt.tuic.buildTuicConfig
|
||||
import io.nekohasekai.sagernet.fmt.tuic.pluginId
|
||||
import io.nekohasekai.sagernet.ktx.*
|
||||
import io.nekohasekai.sagernet.plugin.PluginManager
|
||||
import kotlinx.coroutines.*
|
||||
@ -86,7 +87,7 @@ abstract class BoxInstance(
|
||||
}
|
||||
|
||||
is TuicBean -> {
|
||||
initPlugin("tuic-plugin")
|
||||
initPlugin(bean.pluginId())
|
||||
pluginConfigs[port] = profile.type to bean.buildTuicConfig(port) {
|
||||
File(
|
||||
app.noBackupFilesDir,
|
||||
@ -252,7 +253,7 @@ abstract class BoxInstance(
|
||||
cacheFiles.add(configFile)
|
||||
|
||||
val commands = mutableListOf(
|
||||
initPlugin("tuic-plugin").path,
|
||||
initPlugin(bean.pluginId()).path,
|
||||
"-c",
|
||||
configFile.absolutePath,
|
||||
)
|
||||
|
||||
@ -19,6 +19,7 @@ import io.nekohasekai.sagernet.fmt.socks.buildSingBoxOutboundSocksBean
|
||||
import io.nekohasekai.sagernet.fmt.ssh.SSHBean
|
||||
import io.nekohasekai.sagernet.fmt.ssh.buildSingBoxOutboundSSHBean
|
||||
import io.nekohasekai.sagernet.fmt.tuic.TuicBean
|
||||
import io.nekohasekai.sagernet.fmt.tuic.pluginId
|
||||
import io.nekohasekai.sagernet.fmt.v2ray.StandardV2RayBean
|
||||
import io.nekohasekai.sagernet.fmt.v2ray.buildSingBoxOutboundStandardV2RayBean
|
||||
import io.nekohasekai.sagernet.fmt.wireguard.WireGuardBean
|
||||
@ -35,6 +36,7 @@ import moe.matsuri.nb4a.proxy.config.ConfigBean
|
||||
import moe.matsuri.nb4a.proxy.shadowtls.ShadowTLSBean
|
||||
import moe.matsuri.nb4a.proxy.shadowtls.buildSingBoxOutboundShadowTLSBean
|
||||
import moe.matsuri.nb4a.utils.JavaUtil.gson
|
||||
import moe.matsuri.nb4a.utils.Util
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
|
||||
const val TAG_MIXED = "mixed-in"
|
||||
@ -63,20 +65,6 @@ class ConfigBuildResult(
|
||||
data class IndexEntity(var chain: LinkedHashMap<Int, ProxyEntity>)
|
||||
}
|
||||
|
||||
fun mergeJSON(j: String, to: MutableMap<String, Any>) {
|
||||
if (j.isBlank()) return
|
||||
val m = gson.fromJson(j, to.javaClass)
|
||||
m.forEach { (k, v) ->
|
||||
if (v is Map<*, *> && to[k] is Map<*, *>) {
|
||||
val currentMap = (to[k] as Map<*, *>).toMutableMap()
|
||||
currentMap += v
|
||||
to[k] = currentMap
|
||||
} else {
|
||||
to[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun buildConfig(
|
||||
proxy: ProxyEntity, forTest: Boolean = false, forExport: Boolean = false
|
||||
): ConfigBuildResult {
|
||||
@ -439,7 +427,7 @@ fun buildConfig(
|
||||
|
||||
// custom JSON merge
|
||||
if (bean.customOutboundJson.isNotBlank()) {
|
||||
mergeJSON(bean.customOutboundJson, currentOutbound)
|
||||
Util.mergeJSON(bean.customOutboundJson, currentOutbound)
|
||||
}
|
||||
}
|
||||
|
||||
@ -458,18 +446,19 @@ fun buildConfig(
|
||||
|
||||
// External proxy need a dokodemo-door inbound to forward the traffic
|
||||
// For external proxy software, their traffic must goes to v2ray-core to use protected fd.
|
||||
bean.finalPort = bean.serverPort
|
||||
if (bean.canMapping() && proxyEntity.needExternal()) {
|
||||
// With ss protect, don't use mapping
|
||||
var needExternal = true
|
||||
if (index == profileList.lastIndex) {
|
||||
val pluginId = when (bean) {
|
||||
is HysteriaBean -> "hysteria-plugin"
|
||||
is TuicBean -> "tuic-plugin"
|
||||
is TuicBean -> bean.pluginId()
|
||||
else -> ""
|
||||
}
|
||||
if (Plugins.isUsingMatsuriExe(pluginId)) {
|
||||
needExternal = false
|
||||
} else if (pluginId.isNotBlank()) {
|
||||
} else if (Plugins.getPlugin(pluginId) != null) {
|
||||
throw Exception("You are using an unsupported $pluginId, please download the correct plugin.")
|
||||
}
|
||||
}
|
||||
@ -593,19 +582,25 @@ fun buildConfig(
|
||||
if (uidList.isNotEmpty()) user_id = uidList
|
||||
domainList?.let { makeSingBoxRule(it) }
|
||||
}
|
||||
if (rule.outbound == -1L) {
|
||||
userDNSRuleList += dnsRuleObj.apply { server = "dns-direct" }
|
||||
} else if (rule.outbound == 0L) {
|
||||
if (useFakeDns) userDNSRuleList += dnsRuleObj.apply {
|
||||
server = "dns-fake"
|
||||
inbound = listOf("tun-in")
|
||||
when (rule.outbound) {
|
||||
-1L -> {
|
||||
userDNSRuleList += dnsRuleObj.apply { server = "dns-direct" }
|
||||
}
|
||||
userDNSRuleList += dnsRuleObj.apply {
|
||||
server = "dns-remote"
|
||||
inbound = null
|
||||
|
||||
0L -> {
|
||||
if (useFakeDns) userDNSRuleList += dnsRuleObj.apply {
|
||||
server = "dns-fake"
|
||||
inbound = listOf("tun-in")
|
||||
}
|
||||
userDNSRuleList += dnsRuleObj.apply {
|
||||
server = "dns-remote"
|
||||
inbound = null
|
||||
}
|
||||
}
|
||||
|
||||
-2L -> {
|
||||
userDNSRuleList += dnsRuleObj.apply { server = "dns-block" }
|
||||
}
|
||||
} else if (rule.outbound == -2L) {
|
||||
userDNSRuleList += dnsRuleObj.apply { server = "dns-block" }
|
||||
}
|
||||
|
||||
outbound = when (val outId = rule.outbound) {
|
||||
@ -786,7 +781,7 @@ fun buildConfig(
|
||||
}.let {
|
||||
ConfigBuildResult(
|
||||
gson.toJson(it.asMap().apply {
|
||||
mergeJSON(optionsToMerge, this)
|
||||
Util.mergeJSON(optionsToMerge, this)
|
||||
}),
|
||||
externalIndexMap,
|
||||
proxy.id,
|
||||
|
||||
@ -22,7 +22,8 @@ enum class PluginEntry(
|
||||
Hysteria(
|
||||
"hysteria-plugin",
|
||||
SagerNet.application.getString(R.string.action_hysteria),
|
||||
"moe.matsuri.exe.hysteria", DownloadSource(
|
||||
"moe.matsuri.exe.hysteria",
|
||||
DownloadSource(
|
||||
playStore = false,
|
||||
fdroid = false,
|
||||
downloadLink = "https://github.com/MatsuriDayo/plugins/releases?q=Hysteria"
|
||||
@ -30,8 +31,19 @@ enum class PluginEntry(
|
||||
),
|
||||
TUIC(
|
||||
"tuic-plugin",
|
||||
SagerNet.application.getString(R.string.action_tuic),
|
||||
"moe.matsuri.exe.tuic", DownloadSource(
|
||||
"TUIC(v4)",
|
||||
"moe.matsuri.exe.tuic",
|
||||
DownloadSource(
|
||||
playStore = false,
|
||||
fdroid = false,
|
||||
downloadLink = "https://github.com/MatsuriDayo/plugins/releases?q=tuic"
|
||||
)
|
||||
),
|
||||
TUIC5(
|
||||
"tuic-v5-plugin",
|
||||
"TUIC(v5)",
|
||||
"moe.matsuri.exe.tuic5",
|
||||
DownloadSource(
|
||||
playStore = false,
|
||||
fdroid = false,
|
||||
downloadLink = "https://github.com/MatsuriDayo/plugins/releases?q=tuic"
|
||||
|
||||
@ -24,6 +24,12 @@ public class TuicBean extends AbstractBean {
|
||||
public Boolean fastConnect;
|
||||
public Boolean allowInsecure;
|
||||
|
||||
// TUIC v5
|
||||
|
||||
public String customJSON;
|
||||
public Integer protocolVersion;
|
||||
public String uuid;
|
||||
|
||||
@Override
|
||||
public void initializeDefaultValues() {
|
||||
super.initializeDefaultValues();
|
||||
@ -38,11 +44,14 @@ public class TuicBean extends AbstractBean {
|
||||
if (sni == null) sni = "";
|
||||
if (fastConnect == null) fastConnect = false;
|
||||
if (allowInsecure == null) allowInsecure = false;
|
||||
if (customJSON == null) customJSON = "";
|
||||
if (protocolVersion == null) protocolVersion = 4;
|
||||
if (uuid == null) uuid = "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(ByteBufferOutput output) {
|
||||
output.writeInt(1);
|
||||
output.writeInt(2);
|
||||
super.serialize(output);
|
||||
output.writeString(token);
|
||||
output.writeString(caText);
|
||||
@ -55,6 +64,9 @@ public class TuicBean extends AbstractBean {
|
||||
output.writeString(sni);
|
||||
output.writeBoolean(fastConnect);
|
||||
output.writeBoolean(allowInsecure);
|
||||
output.writeString(customJSON);
|
||||
output.writeInt(protocolVersion);
|
||||
output.writeString(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -74,6 +86,11 @@ public class TuicBean extends AbstractBean {
|
||||
fastConnect = input.readBoolean();
|
||||
allowInsecure = input.readBoolean();
|
||||
}
|
||||
if (version >= 2) {
|
||||
customJSON = input.readString();
|
||||
protocolVersion = input.readInt();
|
||||
uuid = input.readString();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@ -1,39 +1,82 @@
|
||||
package io.nekohasekai.sagernet.fmt.tuic
|
||||
|
||||
import io.nekohasekai.sagernet.database.DataStore
|
||||
import io.nekohasekai.sagernet.fmt.LOCALHOST
|
||||
import io.nekohasekai.sagernet.ktx.isIpAddress
|
||||
import io.nekohasekai.sagernet.ktx.toStringPretty
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import moe.matsuri.nb4a.plugin.Plugins
|
||||
import io.nekohasekai.sagernet.ktx.wrapIPV6Host
|
||||
import moe.matsuri.nb4a.utils.JavaUtil
|
||||
import moe.matsuri.nb4a.utils.Util
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import java.net.InetAddress
|
||||
|
||||
fun TuicBean.pluginId(): String {
|
||||
return when (protocolVersion) {
|
||||
5 -> "tuic-v5-plugin"
|
||||
else -> "tuic-plugin"
|
||||
}
|
||||
}
|
||||
|
||||
fun TuicBean.buildTuicConfig(port: Int, cacheFile: (() -> File)?): String {
|
||||
if (Plugins.isUsingMatsuriExe("tuic-plugin")) {
|
||||
if (!serverAddress.isIpAddress()) {
|
||||
runBlocking {
|
||||
finalAddress = withContext(Dispatchers.IO) {
|
||||
InetAddress.getAllByName(serverAddress)
|
||||
}?.firstOrNull()?.hostAddress ?: "127.0.0.1"
|
||||
// TODO network on main thread, tuic don't support "sni"
|
||||
}
|
||||
}
|
||||
}
|
||||
val config = when (protocolVersion) {
|
||||
5 -> buildTuicConfigV5(port, cacheFile)
|
||||
else -> buildTuicConfigV4(port, cacheFile)
|
||||
}.toString()
|
||||
var gsonMap = mutableMapOf<String, Any>()
|
||||
gsonMap = JavaUtil.gson.fromJson(config, gsonMap.javaClass)
|
||||
Util.mergeJSON(customJSON, gsonMap)
|
||||
return JavaUtil.gson.toJson(gsonMap)
|
||||
}
|
||||
|
||||
fun TuicBean.buildTuicConfigV5(port: Int, cacheFile: (() -> File)?): JSONObject {
|
||||
return JSONObject().apply {
|
||||
put("relay", JSONObject().apply {
|
||||
if (sni.isNotBlank()) {
|
||||
put("server", sni)
|
||||
put("ip", finalAddress)
|
||||
} else if (serverAddress.isIpAddress()) {
|
||||
put("server", finalAddress)
|
||||
if (sni.isNotBlank() && !disableSNI) {
|
||||
put("server", "$sni:$finalPort")
|
||||
if (finalAddress.isIpAddress()) {
|
||||
put("ip", finalAddress)
|
||||
} else {
|
||||
throw Exception("TUIC must use IP address when you need spoof SNI.")
|
||||
}
|
||||
} else {
|
||||
put("server", serverAddress)
|
||||
put("ip", finalAddress)
|
||||
put("server", serverAddress.wrapIPV6Host() + ":" + finalPort)
|
||||
}
|
||||
|
||||
put("uuid", uuid)
|
||||
put("password", token)
|
||||
|
||||
if (caText.isNotBlank() && cacheFile != null) {
|
||||
val caFile = cacheFile()
|
||||
caFile.writeText(caText)
|
||||
put("certificates", JSONArray(listOf(caFile.absolutePath)))
|
||||
}
|
||||
|
||||
put("udp_relay_mode", udpRelayMode)
|
||||
if (alpn.isNotBlank()) {
|
||||
put("alpn", JSONArray(alpn.split("\n")))
|
||||
}
|
||||
put("congestion_control", congestionController)
|
||||
put("disable_sni", disableSNI)
|
||||
put("zero_rtt_handshake", disableSNI)
|
||||
})
|
||||
put("local", JSONObject().apply {
|
||||
put("server", "127.0.0.1:$port")
|
||||
})
|
||||
put("log_level", "debug")
|
||||
}
|
||||
}
|
||||
|
||||
fun TuicBean.buildTuicConfigV4(port: Int, cacheFile: (() -> File)?): JSONObject {
|
||||
return JSONObject().apply {
|
||||
put("relay", JSONObject().apply {
|
||||
if (sni.isNotBlank() && !disableSNI) {
|
||||
put("server", sni)
|
||||
if (finalAddress.isIpAddress()) {
|
||||
put("ip", finalAddress)
|
||||
} else {
|
||||
throw Exception("TUIC must use IP address when you need spoof SNI.")
|
||||
}
|
||||
} else {
|
||||
put("server", finalAddress)
|
||||
}
|
||||
put("port", finalPort)
|
||||
put("token", token)
|
||||
@ -59,6 +102,6 @@ fun TuicBean.buildTuicConfig(port: Int, cacheFile: (() -> File)?): String {
|
||||
put("ip", LOCALHOST)
|
||||
put("port", port)
|
||||
})
|
||||
put("log_level", if (DataStore.logLevel > 0) "debug" else "info")
|
||||
}.toStringPretty()
|
||||
put("log_level", "debug")
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,6 +9,8 @@ import io.nekohasekai.sagernet.R
|
||||
import io.nekohasekai.sagernet.database.DataStore
|
||||
import io.nekohasekai.sagernet.fmt.tuic.TuicBean
|
||||
import io.nekohasekai.sagernet.ktx.applyDefaultValues
|
||||
import moe.matsuri.nb4a.ui.EditConfigPreference
|
||||
import moe.matsuri.nb4a.ui.SimpleMenuPreference
|
||||
|
||||
class TuicSettingsActivity : ProfileSettingsActivity<TuicBean>() {
|
||||
|
||||
@ -27,8 +29,13 @@ class TuicSettingsActivity : ProfileSettingsActivity<TuicBean>() {
|
||||
DataStore.serverSNI = sni
|
||||
DataStore.serverReduceRTT = reduceRTT
|
||||
DataStore.serverMTU = mtu
|
||||
//
|
||||
DataStore.serverFastConnect = fastConnect
|
||||
DataStore.serverAllowInsecure = allowInsecure
|
||||
//
|
||||
DataStore.serverConfig = customJSON
|
||||
DataStore.serverProtocolVersion = protocolVersion
|
||||
DataStore.serverUsername = uuid
|
||||
}
|
||||
|
||||
override fun TuicBean.serialize() {
|
||||
@ -44,16 +51,48 @@ class TuicSettingsActivity : ProfileSettingsActivity<TuicBean>() {
|
||||
sni = DataStore.serverSNI
|
||||
reduceRTT = DataStore.serverReduceRTT
|
||||
mtu = DataStore.serverMTU
|
||||
//
|
||||
fastConnect = DataStore.serverFastConnect
|
||||
allowInsecure = DataStore.serverAllowInsecure
|
||||
//
|
||||
customJSON = DataStore.serverConfig
|
||||
protocolVersion = DataStore.serverProtocolVersion
|
||||
uuid = DataStore.serverUsername
|
||||
}
|
||||
|
||||
private lateinit var editConfigPreference: EditConfigPreference
|
||||
|
||||
override fun PreferenceFragmentCompat.createPreferences(
|
||||
savedInstanceState: Bundle?,
|
||||
rootKey: String?,
|
||||
) {
|
||||
addPreferencesFromResource(R.xml.tuic_preferences)
|
||||
|
||||
editConfigPreference = findPreference(Key.SERVER_CONFIG)!!
|
||||
|
||||
val uuid = findPreference<EditTextPreference>(Key.SERVER_USERNAME)!!
|
||||
val mtu = findPreference<EditTextPreference>(Key.SERVER_MTU)!!
|
||||
val fastConnect = findPreference<SwitchPreference>(Key.SERVER_FAST_CONNECT)!!
|
||||
val allowInsecure = findPreference<SwitchPreference>(Key.SERVER_ALLOW_INSECURE)!!
|
||||
fun updateVersion(v: Int) {
|
||||
if (v == 5) {
|
||||
uuid.isVisible = true
|
||||
mtu.isVisible = false
|
||||
fastConnect.isVisible = false
|
||||
allowInsecure.isVisible = false
|
||||
} else {
|
||||
uuid.isVisible = false
|
||||
mtu.isVisible = true
|
||||
fastConnect.isVisible = true
|
||||
allowInsecure.isVisible = true
|
||||
}
|
||||
}
|
||||
findPreference<SimpleMenuPreference>(Key.SERVER_PROTOCOL)!!.setOnPreferenceChangeListener { _, newValue ->
|
||||
updateVersion(newValue.toString().toIntOrNull() ?: 4)
|
||||
true
|
||||
}
|
||||
updateVersion(DataStore.serverProtocolVersion)
|
||||
|
||||
val disableSNI = findPreference<SwitchPreference>(Key.SERVER_DISABLE_SNI)!!
|
||||
val sni = findPreference<EditTextPreference>(Key.SERVER_SNI)!!
|
||||
sni.isEnabled = !disableSNI.isChecked
|
||||
@ -67,4 +106,12 @@ class TuicSettingsActivity : ProfileSettingsActivity<TuicBean>() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
if (::editConfigPreference.isInitialized) {
|
||||
editConfigPreference.notifyChanged()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
package moe.matsuri.nb4a.utils
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
@ -34,6 +35,7 @@ fun File.recreate(dir: Boolean) {
|
||||
|
||||
// Context utils
|
||||
|
||||
@SuppressLint("DiscouragedApi")
|
||||
fun Context.getDrawableByName(name: String?): Drawable? {
|
||||
val resourceId: Int = resources.getIdentifier(name, "drawable", packageName)
|
||||
return AppCompatResources.getDrawable(this, resourceId)
|
||||
|
||||
@ -113,6 +113,21 @@ object Util {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun mergeJSON(j: String, to: MutableMap<String, Any>) {
|
||||
if (j.isBlank()) return
|
||||
val m = JavaUtil.gson.fromJson(j, to.javaClass)
|
||||
m.forEach { (k, v) ->
|
||||
if (v is Map<*, *> && to[k] is Map<*, *>) {
|
||||
val currentMap = (to[k] as Map<*, *>).toMutableMap()
|
||||
currentMap += v
|
||||
to[k] = currentMap
|
||||
} else {
|
||||
to[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Format Time
|
||||
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
|
||||
@ -386,6 +386,16 @@
|
||||
<item>BASE64</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="tuic_version">
|
||||
<item>v4</item>
|
||||
<item>v5</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="tuic_version_value">
|
||||
<item>4</item>
|
||||
<item>5</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="socks_versions">
|
||||
<item>SOCKS4</item>
|
||||
<item>SOCKS4A</item>
|
||||
|
||||
@ -6,6 +6,15 @@
|
||||
app:title="@string/profile_name"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<moe.matsuri.nb4a.ui.SimpleMenuPreference
|
||||
app:defaultValue="4"
|
||||
app:entries="@array/tuic_version"
|
||||
app:entryValues="@array/tuic_version_value"
|
||||
app:icon="@drawable/ic_baseline_nfc_24"
|
||||
app:key="serverProtocol"
|
||||
app:title="@string/app_version"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<PreferenceCategory app:title="@string/proxy_cat">
|
||||
|
||||
<EditTextPreference
|
||||
@ -18,11 +27,16 @@
|
||||
app:key="serverPort"
|
||||
app:title="@string/server_port"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<EditTextPreference
|
||||
app:icon="@drawable/ic_baseline_person_24"
|
||||
app:key="serverUsername"
|
||||
app:title="@string/uuid"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<EditTextPreference
|
||||
app:dialogLayout="@layout/layout_password_dialog"
|
||||
app:icon="@drawable/ic_settings_password"
|
||||
app:key="serverPassword"
|
||||
app:title="@string/tuic_token" />
|
||||
app:title="@string/password" />
|
||||
<EditTextPreference
|
||||
app:icon="@drawable/ic_baseline_legend_toggle_24"
|
||||
app:key="serverALPN"
|
||||
@ -77,6 +91,11 @@
|
||||
app:key="serverMTU"
|
||||
app:title="@string/mtu"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<moe.matsuri.nb4a.ui.EditConfigPreference
|
||||
app:icon="@drawable/ic_baseline_layers_24"
|
||||
app:key="serverConfig"
|
||||
app:title="@string/custom_config"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
</PreferenceCategory>
|
||||
|
||||
</PreferenceScreen>
|
||||
Loading…
Reference in New Issue
Block a user