/******************************************************************************
* *
* 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.content.BroadcastReceiver
import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.activity.result.component1
import androidx.activity.result.component2
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.setFragmentResultListener
import androidx.lifecycle.lifecycleScope
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreference
import com.github.shadowsocks.plugin.*
import com.github.shadowsocks.preference.PluginConfigurationDialogFragment
import com.github.shadowsocks.preference.PluginPreference
import com.github.shadowsocks.preference.PluginPreferenceDialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
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.http.HttpBean
import io.nekohasekai.sagernet.fmt.shadowsocks.ShadowsocksBean
import io.nekohasekai.sagernet.fmt.socks.SOCKSBean
import io.nekohasekai.sagernet.fmt.trojan.TrojanBean
import io.nekohasekai.sagernet.fmt.v2ray.StandardV2RayBean
import io.nekohasekai.sagernet.fmt.v2ray.VLESSBean
import io.nekohasekai.sagernet.fmt.v2ray.VMessBean
import io.nekohasekai.sagernet.ktx.app
import io.nekohasekai.sagernet.ktx.getBooleanProperty
import io.nekohasekai.sagernet.ktx.listenForPackageChanges
import io.nekohasekai.sagernet.ktx.readableMessage
import io.nekohasekai.sagernet.ktx.runOnMainDispatcher
import io.nekohasekai.sagernet.ktx.showAllowingStateLoss
import io.nekohasekai.sagernet.ktx.unwrapIDN
import io.nekohasekai.sagernet.widget.NonBlackEditTextPreference
import kotlinx.coroutines.launch
abstract class StandardV2RaySettingsActivity : ProfileSettingsActivity(),
Preference.OnPreferenceChangeListener {
override fun StandardV2RayBean.init() {
DataStore.profileName = name
DataStore.serverAddress = serverAddress
DataStore.serverPort = serverPort
when (this) {
is TrojanBean -> DataStore.serverUserId = password
is ShadowsocksBean -> {
DataStore.serverEncryption = method
DataStore.serverUserId = password
}
is VMessBean -> {
DataStore.serverUserId = uuid
DataStore.serverEncryption = encryption
}
is VLESSBean -> {
DataStore.serverUserId = uuid
DataStore.serverEncryptionForVless = encryption
}
is HttpBean -> {
DataStore.serverUserId = password
DataStore.serverUsername = username
}
is SOCKSBean -> {
DataStore.serverUserId = password
DataStore.serverUsername = username
}
}
DataStore.serverNetwork = type
DataStore.serverHeader = headerType
DataStore.serverHost = host
when (type) {
"kcp" -> DataStore.serverPath = mKcpSeed
"quic" -> DataStore.serverPath = quicKey
"grpc" -> DataStore.serverPath = grpcServiceName
"meek" -> DataStore.serverPath = meekUrl
else -> DataStore.serverPath = path
}
DataStore.serverSecurity = security
DataStore.serverSNI = sni
DataStore.serverALPN = alpn
DataStore.serverCertificates = certificates
when (this) {
is VLESSBean -> DataStore.serverFlow = flow
is VMessBean -> {
DataStore.serverAlterId = alterId
DataStore.serverVMessExperimentalAuthenticatedLength = experimentalAuthenticatedLength
DataStore.serverVMessExperimentalNoTerminationSignal = experimentalNoTerminationSignal
}
is ShadowsocksBean -> {
DataStore.serverPlugin = plugin
DataStore.serverReducedIvHeadEntropy = experimentReducedIvHeadEntropy
}
is SOCKSBean -> {
DataStore.serverProtocolVersion = protocol
}
}
DataStore.serverPinnedCertificateChain = pinnedPeerCertificateChainSha256
DataStore.serverPinnedCertificatePublicKey = pinnedPeerCertificatePublicKeySha256
DataStore.serverPinnedCertificate = pinnedPeerCertificateSha256
DataStore.serverQuicSecurity = quicSecurity
DataStore.serverWsMaxEarlyData = maxEarlyData
DataStore.serverEarlyDataHeaderName = earlyDataHeaderName
DataStore.serverSplithttpMode = splithttpMode
DataStore.serverSplithttpExtra = splithttpExtra
DataStore.serverUTLSFingerprint = utlsFingerprint
DataStore.serverEchEnabled = echEnabled
DataStore.serverEchConfig = echConfig
DataStore.serverMtlsCertificate = mtlsCertificate
DataStore.serverMtlsCertificatePrivateKey = mtlsCertificatePrivateKey
DataStore.serverServerNameToVerify = serverNameToVerify
DataStore.serverRealityPublicKey = realityPublicKey
DataStore.serverRealityShortId = realityShortId
DataStore.serverRealityMldsa65Verify = realityMldsa65Verify
DataStore.serverRealityFingerprint = realityFingerprint
DataStore.serverRealityDisableX25519Mlkem768 = realityDisableX25519Mlkem768
DataStore.serverGrpcServiceNameCompat = grpcServiceNameCompat
DataStore.serverGrpcMultiMode = grpcMultiMode
DataStore.serverUploadSpeed = hy2UpMbps
DataStore.serverDownloadSpeed = hy2DownMbps
DataStore.serverPassword = hy2Password
DataStore.serverMekyaKcpSeed = mekyaKcpSeed
DataStore.serverMekyaKcpHeaderType = mekyaKcpHeaderType
DataStore.serverMekyaUrl = mekyaUrl
DataStore.serverWsBrowserForwarding = wsUseBrowserForwarder
DataStore.serverShBrowserForwarding = shUseBrowserForwarder
DataStore.serverAllowInsecure = allowInsecure
DataStore.serverPacketEncoding = packetEncoding
DataStore.serverMux = mux
DataStore.serverMuxConcurrency = muxConcurrency
DataStore.serverMuxPacketEncoding = muxPacketEncoding
when (this) {
is ShadowsocksBean -> DataStore.serverSingUot = singUoT
is SOCKSBean -> DataStore.serverSingUot = singUoT
}
DataStore.serverSingMux = singMux
DataStore.serverSingMuxProtocol = singMuxProtocol
DataStore.serverSingMuxMaxConnections = singMuxMaxConnections
DataStore.serverSingMuxMinStreams = singMuxMinStreams
DataStore.serverSingMuxMaxStreams = singMuxMaxStreams
DataStore.serverSingMuxPadding = singMuxPadding
}
override fun StandardV2RayBean.serialize() {
name = DataStore.profileName
serverAddress = DataStore.serverAddress.unwrapIDN()
serverPort = DataStore.serverPort
when (this) {
is TrojanBean -> {
password = DataStore.serverUserId
}
is ShadowsocksBean -> {
method = DataStore.serverEncryption
password = DataStore.serverUserId
}
is VMessBean -> {
uuid = DataStore.serverUserId
encryption = DataStore.serverEncryption
}
is VLESSBean -> {
uuid = DataStore.serverUserId
encryption = DataStore.serverEncryptionForVless
}
is HttpBean -> {
password = DataStore.serverUserId
username = DataStore.serverUsername
}
is SOCKSBean -> {
password = DataStore.serverUserId
username = DataStore.serverUsername
}
}
type = DataStore.serverNetwork
headerType = DataStore.serverHeader
host = DataStore.serverHost
when (type) {
"kcp" -> mKcpSeed = DataStore.serverPath
"quic" -> quicKey = DataStore.serverPath
"grpc" -> grpcServiceName = DataStore.serverPath
"meek" -> meekUrl = DataStore.serverPath
else -> path = DataStore.serverPath
}
security = DataStore.serverSecurity
sni = DataStore.serverSNI
alpn = DataStore.serverALPN
certificates = DataStore.serverCertificates
pinnedPeerCertificateChainSha256 = DataStore.serverPinnedCertificateChain
pinnedPeerCertificatePublicKeySha256 = DataStore.serverPinnedCertificatePublicKey
pinnedPeerCertificateSha256 = DataStore.serverPinnedCertificate
when (this) {
is VLESSBean -> flow = DataStore.serverFlow
is VMessBean -> {
alterId = DataStore.serverAlterId
experimentalAuthenticatedLength = DataStore.serverVMessExperimentalAuthenticatedLength
experimentalNoTerminationSignal = DataStore.serverVMessExperimentalNoTerminationSignal
}
is ShadowsocksBean -> {
plugin = DataStore.serverPlugin
experimentReducedIvHeadEntropy = DataStore.serverReducedIvHeadEntropy
}
is SOCKSBean -> protocol = DataStore.serverProtocolVersion
}
quicSecurity = DataStore.serverQuicSecurity
maxEarlyData = DataStore.serverWsMaxEarlyData
earlyDataHeaderName = DataStore.serverEarlyDataHeaderName
splithttpMode = DataStore.serverSplithttpMode
splithttpExtra = DataStore.serverSplithttpExtra
utlsFingerprint = DataStore.serverUTLSFingerprint
mtlsCertificate = DataStore.serverMtlsCertificate
mtlsCertificatePrivateKey = DataStore.serverMtlsCertificatePrivateKey
echEnabled = DataStore.serverEchEnabled
echConfig = DataStore.serverEchConfig
serverNameToVerify = DataStore.serverServerNameToVerify
realityPublicKey = DataStore.serverRealityPublicKey
realityShortId = DataStore.serverRealityShortId
realityMldsa65Verify = DataStore.serverRealityMldsa65Verify
realityFingerprint = DataStore.serverRealityFingerprint
realityDisableX25519Mlkem768 = DataStore.serverRealityDisableX25519Mlkem768
grpcServiceNameCompat = DataStore.serverGrpcServiceNameCompat
grpcMultiMode = DataStore.serverGrpcMultiMode
hy2UpMbps = DataStore.serverUploadSpeed
hy2DownMbps = DataStore.serverDownloadSpeed
hy2Password = DataStore.serverPassword
mekyaKcpSeed = DataStore.serverMekyaKcpSeed
mekyaKcpHeaderType = DataStore.serverMekyaKcpHeaderType
mekyaUrl = DataStore.serverMekyaUrl
wsUseBrowserForwarder = DataStore.serverWsBrowserForwarding
shUseBrowserForwarder = DataStore.serverShBrowserForwarding
allowInsecure = DataStore.serverAllowInsecure
packetEncoding = DataStore.serverPacketEncoding
mux = DataStore.serverMux
muxConcurrency = DataStore.serverMuxConcurrency
muxPacketEncoding = DataStore.serverMuxPacketEncoding
when (this) {
is ShadowsocksBean -> singUoT = DataStore.serverSingUot
is SOCKSBean -> singUoT = DataStore.serverSingUot
}
singMux = DataStore.serverSingMux
singMuxProtocol = DataStore.serverSingMuxProtocol
singMuxMaxConnections = DataStore.serverSingMuxMaxConnections
singMuxMinStreams = DataStore.serverSingMuxMinStreams
singMuxMaxStreams = DataStore.serverSingMuxMaxStreams
singMuxPadding = DataStore.serverSingMuxPadding
}
lateinit var encryption: ListPreference
lateinit var vlessEncryption: NonBlackEditTextPreference
lateinit var network: ListPreference
lateinit var header: ListPreference
lateinit var requestHost: EditTextPreference
lateinit var path: EditTextPreference
lateinit var quicSecurity: ListPreference
lateinit var security: ListPreference
lateinit var xtlsFlow: ListPreference
lateinit var alterId: EditTextPreference
lateinit var sni: EditTextPreference
lateinit var alpn: EditTextPreference
lateinit var securityCategory: PreferenceCategory
lateinit var certificates: EditTextPreference
lateinit var pinnedCertificateChain: EditTextPreference
lateinit var pinnedCertificatePublickey: EditTextPreference
lateinit var pinnedCertificate: EditTextPreference
lateinit var allowInsecure: SwitchPreference
lateinit var utlsFingerprint: ListPreference
lateinit var mtlsCertificate: EditTextPreference
lateinit var mtlsCertificatePrivateKey: EditTextPreference
lateinit var echEnabled: SwitchPreference
lateinit var echConfig: EditTextPreference
lateinit var serverNameToVerify: EditTextPreference
lateinit var realityPublicKey: EditTextPreference
lateinit var realityShortId: EditTextPreference
lateinit var realityMldsa65Verify: EditTextPreference
lateinit var realityFingerprint: ListPreference
lateinit var realityDisableX25519Mlkem768: SwitchPreference
lateinit var packetEncoding: ListPreference
lateinit var hy2UpMbps: EditTextPreference
lateinit var hy2DownMbps: EditTextPreference
lateinit var hy2Password: EditTextPreference
lateinit var mekyaKcpSeed: EditTextPreference
lateinit var mekyaKcpHeaderType: ListPreference
lateinit var mekyaUrl: EditTextPreference
lateinit var socksProtocol: ListPreference
lateinit var passwordUUID: EditTextPreference
lateinit var wsCategory: PreferenceCategory
lateinit var wsUseBrowserForwarder: SwitchPreference
lateinit var splithttpCategory: PreferenceCategory
lateinit var splithttpMode: ListPreference
lateinit var splithttpExtra: EditTextPreference
lateinit var grpcCategory: PreferenceCategory
lateinit var ssExperimentsCategory: PreferenceCategory
lateinit var plugin: PluginPreference
lateinit var pluginConfigure: EditTextPreference
lateinit var pluginConfiguration: PluginConfiguration
lateinit var receiver: BroadcastReceiver
lateinit var mux: SwitchPreference
lateinit var muxConcurrency: EditTextPreference
lateinit var muxPacketEncoding: ListPreference
lateinit var singMux: SwitchPreference
lateinit var singMuxProtocol: ListPreference
lateinit var singMuxMaxConnections: EditTextPreference
lateinit var singMuxMinStreams: EditTextPreference
lateinit var singMuxMaxStreams: EditTextPreference
lateinit var singMuxPadding: SwitchPreference
override fun PreferenceFragmentCompat.createPreferences(
savedInstanceState: Bundle?,
rootKey: String?,
) {
addPreferencesFromResource(R.xml.standard_v2ray_preferences)
findPreference(Key.SERVER_PORT)!!.apply {
setOnBindEditTextListener(EditTextPreferenceModifiers.Port)
}
encryption = findPreference(Key.SERVER_ENCRYPTION)!!
vlessEncryption = findPreference(Key.SERVER_ENCRYPTION_FOR_VLESS)!!
network = findPreference(Key.SERVER_NETWORK)!!
header = findPreference(Key.SERVER_HEADER)!!
requestHost = findPreference(Key.SERVER_HOST)!!
path = findPreference(Key.SERVER_PATH)!!
quicSecurity = findPreference(Key.SERVER_QUIC_SECURITY)!!
security = findPreference(Key.SERVER_SECURITY)!!
sni = findPreference(Key.SERVER_SNI)!!
alpn = findPreference(Key.SERVER_ALPN)!!
securityCategory = findPreference(Key.SERVER_SECURITY_CATEGORY)!!
certificates = findPreference(Key.SERVER_CERTIFICATES)!!
pinnedCertificateChain = findPreference(Key.SERVER_PINNED_CERTIFICATE_CHAIN)!!
pinnedCertificatePublickey = findPreference(Key.SERVER_PINNED_CERTIFICATE_PUBLIC_KEY)!!
pinnedCertificate = findPreference(Key.SERVER_PINNED_CERTIFICATE)!!
allowInsecure = findPreference(Key.SERVER_ALLOW_INSECURE)!!
xtlsFlow = findPreference(Key.SERVER_FLOW)!!
alterId = findPreference(Key.SERVER_ALTER_ID)!!
utlsFingerprint = findPreference(Key.SERVER_UTLS_FINGERPRINT)!!
mtlsCertificate = findPreference(Key.SERVER_MTLS_CERTIFICATE)!!
mtlsCertificatePrivateKey = findPreference(Key.SERVER_MTLS_CERTIFICATE_PRIVATE_KEY)!!
echEnabled = findPreference(Key.SERVER_ECH_ENABLED)!!
echConfig = findPreference(Key.SERVER_ECH_CONFIG)!!
echConfig.isEnabled = echEnabled.isChecked
echEnabled.setOnPreferenceChangeListener { _, newValue ->
echConfig.isEnabled = newValue as Boolean
true
}
serverNameToVerify = findPreference(Key.SERVER_SERVER_NAME_TO_VERIFY)!!
realityPublicKey = findPreference(Key.SERVER_REALITY_PUBLIC_KEY)!!
realityShortId = findPreference(Key.SERVER_REALITY_SHORT_ID)!!
realityMldsa65Verify = findPreference(Key.SERVER_REALITY_MLDSA65_VERIFY)!!
realityFingerprint = findPreference(Key.SERVER_REALITY_FINGERPRINT)!!
realityDisableX25519Mlkem768 = findPreference(Key.SERVER_REALITY_DISABLE_X25519MLKEM768)!!
realityPublicKey.apply {
summaryProvider = PasswordSummaryProvider
}
hy2UpMbps = findPreference(Key.SERVER_UPLOAD_SPEED)!!
hy2UpMbps.apply {
setOnBindEditTextListener(EditTextPreferenceModifiers.Number)
}
hy2DownMbps = findPreference(Key.SERVER_DOWNLOAD_SPEED)!!
hy2DownMbps.apply {
setOnBindEditTextListener(EditTextPreferenceModifiers.Number)
}
hy2Password = findPreference(Key.SERVER_PASSWORD)!!
hy2Password.apply {
summaryProvider = PasswordSummaryProvider
title = resources.getString(R.string.hysteria2_password)
dialogTitle = resources.getString(R.string.hysteria2_password)
}
mekyaKcpSeed = findPreference(Key.SERVER_MEKYA_KCP_SEED)!!
mekyaKcpHeaderType = findPreference(Key.SERVER_MEKYA_KCP_HEADER_TYPE)!!
mekyaUrl = findPreference(Key.SERVER_MEKYA_URL)!!
wsCategory = findPreference(Key.SERVER_WS_CATEGORY)!!
wsUseBrowserForwarder = findPreference(Key.SERVER_WS_BROWSER_FORWARDING)!!
splithttpCategory = findPreference(Key.SERVER_SH_CATEGORY)!!
splithttpMode = findPreference(Key.SERVER_SPLITHTTP_MODE)!!
splithttpExtra = findPreference(Key.SERVER_SPLITHTTP_EXTRA)!!
grpcCategory = findPreference(Key.SERVER_GRPC_CATAGORY)!!
findPreference(Key.SERVER_WS_BROWSER_FORWARDING)!!.setOnPreferenceChangeListener { _, newValue ->
if (newValue as Boolean) {
runOnMainDispatcher {
MaterialAlertDialogBuilder(this@StandardV2RaySettingsActivity)
.setMessage(getString(R.string.browser_forwarder_hint, packageName))
.setPositiveButton(android.R.string.ok, null)
.show()
}
}
true
}
when (bean) {
is VLESSBean -> {
encryption.isVisible = false
val xfv = resources.getStringArray(R.array.xtls_flow_value)
if (xtlsFlow.value !in xfv) {
xtlsFlow.value = xfv[0]
}
}
is VMessBean -> {
vlessEncryption.isVisible = false
encryption.setEntries(R.array.vmess_encryption_value)
encryption.setEntryValues(R.array.vmess_encryption_value)
val vev = resources.getStringArray(R.array.vmess_encryption_value)
if (encryption.value !in vev) {
encryption.value = "auto"
}
}
is ShadowsocksBean -> {
vlessEncryption.isVisible = false
encryption.setEntries(R.array.enc_method_value)
encryption.setEntryValues(R.array.enc_method_value)
val sev = resources.getStringArray(R.array.enc_method_value)
if (encryption.value !in sev) {
encryption.value = "none"
}
}
else -> {
encryption.isVisible = false
vlessEncryption.isVisible = false
}
}
passwordUUID = findPreference(Key.SERVER_USER_ID)!!
passwordUUID.apply {
summaryProvider = PasswordSummaryProvider
if (bean is TrojanBean || bean is ShadowsocksBean || bean is SOCKSBean || bean is HttpBean) {
title = resources.getString(R.string.password)
dialogTitle = resources.getString(R.string.password)
}
}
alterId.isVisible = bean is VMessBean
alterId.setOnBindEditTextListener(EditTextPreferenceModifiers.Port)
updateView(network.value)
network.setOnPreferenceChangeListener { _, newValue ->
updateView(newValue as String)
true
}
security.setOnPreferenceChangeListener { _, newValue ->
updateTle(newValue as String)
true
}
findPreference(Key.SERVER_VMESS_EXPERIMENTS_CATEGORY)!!.isVisible = bean is VMessBean
findPreference(Key.SERVER_PLUGIN_CATEGORY)!!.isVisible = bean is ShadowsocksBean
xtlsFlow.isVisible = bean is VLESSBean
val fv = resources.getStringArray(R.array.xtls_flow_value)
packetEncoding = findPreference(Key.SERVER_PACKET_ENCODING)!!
packetEncoding.isVisible = bean is VMessBean || bean is VLESSBean
packetEncoding.isEnabled = bean is VMessBean || (bean is VLESSBean && xtlsFlow.value == fv[0])
val pev = resources.getStringArray(R.array.packet_encoding_value)
if (packetEncoding.value !in pev) {
packetEncoding.value = pev[0]
}
if (bean is VLESSBean && xtlsFlow.value != fv[0]) {
packetEncoding.value = pev[2]
}
xtlsFlow.setOnPreferenceChangeListener { _, newValue ->
newValue as String
packetEncoding.isEnabled = newValue == fv[0]
if (newValue != fv[0]) {
packetEncoding.value = pev[2]
}
true
}
ssExperimentsCategory = findPreference(Key.SERVER_SS_EXPERIMENTS_CATEGORY)!!
fun updateMethod(method: String) {
ssExperimentsCategory.isVisible = bean is ShadowsocksBean && !method.startsWith("2022-blake3-")
}
updateMethod(DataStore.serverEncryption)
encryption.setOnPreferenceChangeListener { _, newValue ->
updateMethod(newValue as String)
true
}
socksProtocol = findPreference(Key.SERVER_PROTOCOL)!!
socksProtocol.isVisible = bean is SOCKSBean
fun updateProtocol(version: Int) {
passwordUUID.isVisible = version == SOCKSBean.PROTOCOL_SOCKS5
}
if (bean is SOCKSBean) {
updateProtocol(DataStore.serverProtocolVersion)
}
socksProtocol.setOnPreferenceChangeListener { _, newValue ->
updateProtocol((newValue as String).toInt())
true
}
findPreference(Key.SERVER_USERNAME)!!.isVisible = bean is SOCKSBean || bean is HttpBean
mux = findPreference(Key.SERVER_MUX)!!
muxConcurrency = findPreference(Key.SERVER_MUX_CONCURRENCY)!!
muxConcurrency.isVisible = mux.isChecked
muxConcurrency.setOnBindEditTextListener(EditTextPreferenceModifiers.Mux)
muxPacketEncoding = findPreference(Key.SERVER_MUX_PACKET_ENCODING)!!
muxPacketEncoding.isVisible = mux.isChecked
if (muxPacketEncoding.value !in pev) {
muxPacketEncoding.value = pev[0]
}
mux.setOnPreferenceChangeListener { _, newValue ->
newValue as Boolean
muxConcurrency.isVisible = newValue
muxPacketEncoding.isVisible = newValue
true
}
plugin = findPreference(Key.SERVER_PLUGIN)!!
pluginConfigure = findPreference(Key.SERVER_PLUGIN_CONFIGURE)!!
pluginConfigure.setOnBindEditTextListener(EditTextPreferenceModifiers.Monospace)
pluginConfigure.onPreferenceChangeListener = this@StandardV2RaySettingsActivity
pluginConfiguration = PluginConfiguration(DataStore.serverPlugin)
initPlugins()
findPreference(Key.SERVER_SING_UOT_CATEGORY)!!.isVisible =
(bean is ShadowsocksBean || bean is SOCKSBean) && DataStore.experimentalFlagsProperties.getBooleanProperty("singuot")
findPreference(Key.SERVER_SING_MUX_CATEGORY)!!.isVisible =
(bean is ShadowsocksBean || bean is TrojanBean || bean is VMessBean || bean is VLESSBean) &&
DataStore.experimentalFlagsProperties.getBooleanProperty("singmux")
singMux = findPreference(Key.SERVER_SING_MUX)!!
singMuxProtocol = findPreference(Key.SERVER_SING_MUX_PROTOCOL)!!
singMuxMaxConnections = findPreference(Key.SERVER_SING_MUX_MAX_CONNECTIONS)!!
singMuxMinStreams = findPreference(Key.SERVER_SING_MUX_MIN_STREAMS)!!
singMuxMaxStreams = findPreference(Key.SERVER_SING_MUX_MAX_STREAMS)!!
singMuxPadding = findPreference(Key.SERVER_SING_MUX_PADDING)!!
singMuxProtocol.isVisible = singMux.isChecked
singMuxMaxConnections.isVisible = singMux.isChecked
singMuxMinStreams.isVisible = singMux.isChecked
singMuxMaxStreams.isVisible = singMux.isChecked
singMuxPadding.isVisible = singMux.isChecked
singMuxMaxConnections.setOnBindEditTextListener(EditTextPreferenceModifiers.Number)
singMuxMinStreams.setOnBindEditTextListener(EditTextPreferenceModifiers.Number)
singMuxMaxStreams.setOnBindEditTextListener(EditTextPreferenceModifiers.Number)
val singMuxProtocolValues = resources.getStringArray(R.array.sing_mux_protocol_value)
if (singMuxProtocol.value !in singMuxProtocolValues) {
singMuxProtocol.value = singMuxProtocolValues[0]
}
singMux.setOnPreferenceChangeListener { _, newValue ->
newValue as Boolean
singMuxProtocol.isVisible = newValue
singMuxMaxConnections.isVisible = newValue
singMuxMinStreams.isVisible = newValue
singMuxMaxStreams.isVisible = newValue
singMuxPadding.isVisible = newValue
true
}
}
val tcpHeadersValue = app.resources.getStringArray(R.array.tcp_headers_value)
val kcpQuicHeadersValue = app.resources.getStringArray(R.array.kcp_quic_headers_value)
val quicSecurityValue = app.resources.getStringArray(R.array.quic_security_value)
fun updateView(network: String) {
security.setEntries(R.array.transport_layer_encryption_entry)
security.setEntryValues(R.array.transport_layer_encryption_value)
security.value = DataStore.serverSecurity
val tlev = resources.getStringArray(R.array.transport_layer_encryption_value)
if (security.value !in tlev) {
security.value = tlev[0]
}
updateTle(security.value)
hy2UpMbps.isVisible = network == "hysteria2"
hy2DownMbps.isVisible = network == "hysteria2"
hy2Password.isVisible = network == "hysteria2"
quicSecurity.isVisible = network == "quic"
mekyaKcpSeed.isVisible = network == "mekya"
mekyaKcpHeaderType.isVisible = network == "mekya"
mekyaUrl.isVisible = network == "mekya"
if (network == "quic") {
if (DataStore.serverQuicSecurity !in quicSecurityValue) {
quicSecurity.value = quicSecurityValue[0]
} else {
quicSecurity.value = DataStore.serverQuicSecurity
}
}
wsCategory.isVisible = network == "ws" || network == "httpupgrade"
if (network == "ws") wsCategory.setTitle(R.string.cag_ws)
if (network == "httpupgrade") wsCategory.setTitle(R.string.cag_httpupgrade)
wsUseBrowserForwarder.isVisible = network == "ws"
splithttpCategory.isVisible = network == "splithttp"
if (splithttpMode.value !in resources.getStringArray(R.array.splithttp_mode_value)) {
splithttpMode.value = resources.getStringArray(R.array.splithttp_mode_value)[0]
}
// Do not translate this as it is not a stable string.
splithttpExtra.dialogMessage = "`downloadSettings` is not supported. Use custom config for `downloadSettings`."
grpcCategory.isVisible = network == "grpc"
when (network) {
"tcp" -> {
header.setEntries(R.array.tcp_headers_entry)
header.setEntryValues(R.array.tcp_headers_value)
if (DataStore.serverHeader !in tcpHeadersValue) {
header.value = tcpHeadersValue[0]
} else {
header.value = DataStore.serverHeader
}
var isHttp = header.value == "http"
requestHost.isVisible = isHttp
path.isVisible = isHttp
header.setOnPreferenceChangeListener { _, newValue ->
isHttp = newValue == "http"
requestHost.isVisible = isHttp
path.isVisible = isHttp
true
}
requestHost.setTitle(R.string.http_host)
requestHost.setDialogTitle(R.string.http_host)
path.setTitle(R.string.http_path)
path.setDialogTitle(R.string.http_path)
header.isVisible = true
}
"http", "httpupgrade", "splithttp" -> {
requestHost.setTitle(R.string.http_host)
requestHost.setDialogTitle(R.string.http_host)
path.setTitle(R.string.http_path)
path.setDialogTitle(R.string.http_path)
header.isVisible = false
requestHost.isVisible = true
path.isVisible = true
}
"ws" -> {
requestHost.setTitle(R.string.ws_host)
requestHost.setDialogTitle(R.string.ws_host)
path.setTitle(R.string.ws_path)
path.setDialogTitle(R.string.ws_path)
header.isVisible = false
requestHost.isVisible = true
path.isVisible = true
}
"kcp" -> {
header.setEntries(R.array.kcp_quic_headers_value)
header.setEntryValues(R.array.kcp_quic_headers_value)
path.setTitle(R.string.kcp_seed)
path.setDialogTitle(R.string.kcp_seed)
if (DataStore.serverHeader !in kcpQuicHeadersValue) {
header.value = kcpQuicHeadersValue[0]
} else {
header.value = DataStore.serverHeader
}
header.onPreferenceChangeListener = null
header.isVisible = true
requestHost.isVisible = false
path.isVisible = true
}
"quic" -> {
header.setEntries(R.array.kcp_quic_headers_entry)
header.setEntryValues(R.array.kcp_quic_headers_value)
path.setTitle(R.string.quic_key)
path.setDialogTitle(R.string.quic_key)
if (DataStore.serverHeader !in kcpQuicHeadersValue) {
header.value = kcpQuicHeadersValue[0]
} else {
header.value = DataStore.serverHeader
}
header.onPreferenceChangeListener = null
header.isVisible = true
requestHost.isVisible = false
path.isVisible = true
}
"grpc" -> {
path.setTitle(R.string.grpc_service_name)
path.setDialogTitle(R.string.grpc_service_name)
header.isVisible = false
requestHost.isVisible = false
path.isVisible = true
}
"meek" -> {
path.setTitle(R.string.url)
path.setDialogTitle(R.string.url)
header.isVisible = false
requestHost.isVisible = false
path.isVisible = true
}
"hysteria2" -> {
path.isVisible = false
header.isVisible = false
requestHost.isVisible = false
}
"mekya" -> {
path.isVisible = false
header.isVisible = false
requestHost.isVisible = false
mekyaKcpHeaderType.setEntries(R.array.kcp_quic_headers_entry)
mekyaKcpHeaderType.setEntryValues(R.array.kcp_quic_headers_value)
if (DataStore.serverMekyaKcpHeaderType !in kcpQuicHeadersValue) {
mekyaKcpHeaderType.value = kcpQuicHeadersValue[0]
} else {
mekyaKcpHeaderType.value = DataStore.serverMekyaKcpHeaderType
}
mekyaKcpHeaderType.onPreferenceChangeListener = null
}
}
}
fun updateTle(security: String) {
securityCategory.isVisible = security == "tls" || security == "reality"
certificates.isVisible = security == "tls"
pinnedCertificateChain.isVisible = security == "tls"
pinnedCertificatePublickey.isVisible = security == "tls"
pinnedCertificate.isVisible = security == "tls"
allowInsecure.isVisible = security == "tls"
sni.isVisible = security == "tls" || security == "reality"
alpn.isVisible = security == "tls"
realityPublicKey.isVisible = security == "reality"
realityShortId.isVisible = security == "reality"
realityMldsa65Verify.isVisible = security == "reality"
utlsFingerprint.isVisible = security == "tls" && (network.value == "tcp" || network.value == "ws"
|| network.value == "http" || network.value == "meek" || network.value == "httpupgrade"
|| network.value == "grpc" || network.value == "splithttp" || network.value == "mekya")
mtlsCertificate.isVisible = security == "tls"
mtlsCertificatePrivateKey.isVisible = security == "tls"
echEnabled.isVisible = security == "tls"
echConfig.isVisible = security == "tls"
serverNameToVerify.isVisible = security == "tls"
realityFingerprint.isVisible = security == "reality"
realityDisableX25519Mlkem768.isVisible = security == "reality"
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
receiver = listenForPackageChanges(false) {
// wait until changes were flushed
lifecycleScope.launch { initPlugins() }
}
}
override fun PreferenceFragmentCompat.viewCreated(view: View, savedInstanceState: Bundle?) {
setFragmentResultListener(PluginPreferenceDialogFragment::class.java.name) { _, bundle ->
val selected = plugin.plugins.lookup.getValue(
bundle.getString(PluginPreferenceDialogFragment.KEY_SELECTED_ID)!!
)
val override = pluginConfiguration.pluginsOptions.keys.firstOrNull {
plugin.plugins.lookup[it] == selected
}
pluginConfiguration = PluginConfiguration(
pluginConfiguration.pluginsOptions, override ?: selected.id
)
DataStore.serverPlugin = pluginConfiguration.toString()
dirty = true
onBackPressedCallback.isEnabled = true
plugin.value = pluginConfiguration.selected
pluginConfigure.isEnabled = selected !is NoPlugin
pluginConfigure.text = pluginConfiguration.getOptions().toString()
}
}
private fun initPlugins() {
plugin.value = pluginConfiguration.selected
plugin.init()
pluginConfigure.isEnabled = plugin.selectedEntry?.let { it is NoPlugin } == false
pluginConfigure.text = pluginConfiguration.getOptions().toString()
}
private fun showPluginEditor() {
PluginConfigurationDialogFragment().apply {
setArg(Key.SERVER_PLUGIN_CONFIGURE, pluginConfiguration.selected)
setTargetFragment(child, 0)
}.showAllowingStateLoss(supportFragmentManager, Key.SERVER_PLUGIN_CONFIGURE)
}
override fun onPreferenceChange(preference: Preference, newValue: Any?): Boolean = try {
val selected = pluginConfiguration.selected
pluginConfiguration = PluginConfiguration(
(pluginConfiguration.pluginsOptions + (pluginConfiguration.selected to PluginOptions(
selected, newValue as? String?
))).toMutableMap(), selected
)
DataStore.serverPlugin = pluginConfiguration.toString()
dirty = true
onBackPressedCallback.isEnabled = true
true
} catch (exc: RuntimeException) {
Snackbar.make(child.requireView(), exc.readableMessage, Snackbar.LENGTH_LONG).show()
false
}
private val configurePlugin = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { (resultCode, data) ->
when (resultCode) {
RESULT_OK -> {
val options = data?.getStringExtra(PluginContract.EXTRA_OPTIONS)
pluginConfigure.text = options
onPreferenceChange(pluginConfigure, options)
}
PluginContract.RESULT_FALLBACK -> showPluginEditor()
}
}
override fun PreferenceFragmentCompat.displayPreferenceDialog(preference: Preference): Boolean {
when (preference.key) {
Key.SERVER_PLUGIN -> PluginPreferenceDialogFragment().apply {
setArg(Key.SERVER_PLUGIN)
setTargetFragment(child, 0)
}.showAllowingStateLoss(supportFragmentManager, Key.SERVER_PLUGIN)
Key.SERVER_PLUGIN_CONFIGURE -> {
val intent = PluginManager.buildIntent(
plugin.selectedEntry!!.id, PluginContract.ACTION_CONFIGURE
)
val component = intent.resolveActivity(packageManager)
when {
DataStore.experimentalFlagsProperties.getBooleanProperty("disableShadowsocksPluginConfigActivity") -> {
showPluginEditor()
}
plugin.selectedEntry!!.id == "v2ray-plugin" && (component == null || DataStore.experimentalFlagsProperties.getBooleanProperty("useInternalV2RayPlugin")) -> {
configurePlugin.launch(
Intent().apply {
setClassName(app.packageName, "com.github.shadowsocks.plugin.v2ray.ConfigActivity")
putExtra(
PluginContract.EXTRA_OPTIONS,
pluginConfiguration.getOptions().toString()
)
}
)
}
plugin.selectedEntry!!.id == "obfs-local" && (component == null || DataStore.experimentalFlagsProperties.getBooleanProperty("useInternalObfsLocal")) -> {
configurePlugin.launch(
Intent().apply {
setClassName(app.packageName, "com.github.shadowsocks.plugin.obfs_local.ConfigActivity")
putExtra(
PluginContract.EXTRA_OPTIONS,
pluginConfiguration.getOptions().toString()
)
}
)
}
component == null -> {
showPluginEditor()
}
else -> {
configurePlugin.launch(
intent.putExtra(
PluginContract.EXTRA_OPTIONS,
pluginConfiguration.getOptions().toString()
)
)
}
}
}
else -> return false
}
return true
}
val pluginHelp = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { (resultCode, data) ->
if (resultCode == RESULT_OK) MaterialAlertDialogBuilder(this).setTitle("?")
.setMessage(data?.getCharSequenceExtra(PluginContract.EXTRA_HELP_MESSAGE))
.show()
}
}