Commit e9524620 authored by Mygod's avatar Mygod

Merge branch 'master' into q-beta

parents fa6eee94 8889d8e0
...@@ -8,7 +8,6 @@ buildscript { ...@@ -8,7 +8,6 @@ buildscript {
minSdkVersion = 21 minSdkVersion = 21
sdkVersion = 28 sdkVersion = 28
compileSdkVersion = 'android-Q' compileSdkVersion = 'android-Q'
preferencexVersion = '1.0.0'
junitVersion = '4.12' junitVersion = '4.12'
androidTestVersion = '1.1.1' androidTestVersion = '1.1.1'
androidEspressoVersion = '3.1.1' androidEspressoVersion = '3.1.1'
......
...@@ -48,13 +48,12 @@ dependencies { ...@@ -48,13 +48,12 @@ dependencies {
api project(':plugin') api project(':plugin')
api "androidx.lifecycle:lifecycle-extensions:$lifecycleVersion" api "androidx.lifecycle:lifecycle-extensions:$lifecycleVersion"
api "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion" api "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
api 'androidx.preference:preference:1.0.0' api 'androidx.preference:preference:1.1.0-alpha05'
api "androidx.room:room-runtime:$roomVersion" api "androidx.room:room-runtime:$roomVersion"
api 'androidx.work:work-runtime-ktx:2.0.1' api 'androidx.work:work-runtime-ktx:2.0.1'
api 'com.crashlytics.sdk.android:crashlytics:2.10.0' api 'com.crashlytics.sdk.android:crashlytics:2.10.0'
api 'com.google.firebase:firebase-config:17.0.0' api 'com.google.firebase:firebase-config:17.0.0'
api 'com.google.firebase:firebase-core:16.0.9' api 'com.google.firebase:firebase-core:16.0.9'
api "com.takisoft.preferencex:preferencex:$preferencexVersion"
api 'dnsjava:dnsjava:2.1.8' api 'dnsjava:dnsjava:2.1.8'
api 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.1' api 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.1'
api 'org.connectbot.jsocks:jsocks:1.0.0' api 'org.connectbot.jsocks:jsocks:1.0.0'
......
...@@ -63,6 +63,7 @@ class VpnRequestActivity : AppCompatActivity() { ...@@ -63,6 +63,7 @@ class VpnRequestActivity : AppCompatActivity() {
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode != REQUEST_CONNECT) return super.onActivityResult(requestCode, resultCode, data)
if (resultCode == RESULT_OK) Core.startService() else { if (resultCode == RESULT_OK) Core.startService() else {
Toast.makeText(this, R.string.vpn_permission_denied, Toast.LENGTH_LONG).show() Toast.makeText(this, R.string.vpn_permission_denied, Toast.LENGTH_LONG).show()
Crashlytics.log(Log.ERROR, TAG, "Failed to start VpnService from onActivityResult: $data") Crashlytics.log(Log.ERROR, TAG, "Failed to start VpnService from onActivityResult: $data")
......
...@@ -23,10 +23,12 @@ package com.github.shadowsocks.bg ...@@ -23,10 +23,12 @@ package com.github.shadowsocks.bg
import com.github.shadowsocks.Core.app import com.github.shadowsocks.Core.app
import com.github.shadowsocks.acl.Acl import com.github.shadowsocks.acl.Acl
import com.github.shadowsocks.core.R import com.github.shadowsocks.core.R
import com.github.shadowsocks.net.HostsFile
import com.github.shadowsocks.net.LocalDnsServer import com.github.shadowsocks.net.LocalDnsServer
import com.github.shadowsocks.net.Socks5Endpoint import com.github.shadowsocks.net.Socks5Endpoint
import com.github.shadowsocks.net.Subnet import com.github.shadowsocks.net.Subnet
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.Key
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.net.URI import java.net.URI
...@@ -49,7 +51,8 @@ object LocalDnsService { ...@@ -49,7 +51,8 @@ object LocalDnsService {
val dns = URI("dns://${profile.remoteDns}") val dns = URI("dns://${profile.remoteDns}")
LocalDnsServer(this::resolver, LocalDnsServer(this::resolver,
Socks5Endpoint(dns.host, if (dns.port < 0) 53 else dns.port), Socks5Endpoint(dns.host, if (dns.port < 0) 53 else dns.port),
DataStore.proxyAddress).apply { DataStore.proxyAddress,
HostsFile(DataStore.publicStore.getString(Key.hosts) ?: "")).apply {
tcp = !profile.udpdns tcp = !profile.udpdns
when (profile.route) { when (profile.route) {
Acl.BYPASS_CHN, Acl.BYPASS_LAN_CHN, Acl.GFWLIST, Acl.CUSTOM_RULES -> { Acl.BYPASS_CHN, Acl.BYPASS_LAN_CHN, Acl.GFWLIST, Acl.CUSTOM_RULES -> {
......
...@@ -87,7 +87,7 @@ data class Profile( ...@@ -87,7 +87,7 @@ data class Profile(
if (match != null) { if (match != null) {
val profile = Profile() val profile = Profile()
feature?.copyFeatureSettingsTo(profile) feature?.copyFeatureSettingsTo(profile)
profile.method = match.groupValues[1].toLowerCase() profile.method = match.groupValues[1].toLowerCase(Locale.ENGLISH)
profile.password = match.groupValues[2] profile.password = match.groupValues[2]
profile.host = match.groupValues[3] profile.host = match.groupValues[3]
profile.remotePort = match.groupValues[4].toInt() profile.remotePort = match.groupValues[4].toInt()
......
/*******************************************************************************
* *
* Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.net
import com.github.shadowsocks.utils.computeIfAbsentCompat
import com.github.shadowsocks.utils.parseNumericAddress
import java.net.InetAddress
class HostsFile(input: String = "") {
private val map = mutableMapOf<String, MutableSet<InetAddress>>()
init {
for (line in input.lineSequence()) {
val entries = line.substringBefore('#').splitToSequence(' ', '\t').filter { it.isNotEmpty() }
val address = entries.firstOrNull()?.parseNumericAddress() ?: continue
for (hostname in entries.drop(1)) map.computeIfAbsentCompat(hostname) { LinkedHashSet(1) }.add(address)
}
}
val configuredHostnames get() = map.size
fun resolve(hostname: String) = map[hostname]?.shuffled() ?: emptyList()
}
...@@ -43,7 +43,9 @@ import java.nio.channels.SocketChannel ...@@ -43,7 +43,9 @@ import java.nio.channels.SocketChannel
* https://github.com/shadowsocks/overture/tree/874f22613c334a3b78e40155a55479b7b69fee04 * https://github.com/shadowsocks/overture/tree/874f22613c334a3b78e40155a55479b7b69fee04
*/ */
class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAddress>, class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAddress>,
private val remoteDns: Socks5Endpoint, private val proxy: SocketAddress) : CoroutineScope { private val remoteDns: Socks5Endpoint,
private val proxy: SocketAddress,
private val hosts: HostsFile) : CoroutineScope {
/** /**
* Forward all requests to remote and ignore localResolver. * Forward all requests to remote and ignore localResolver.
*/ */
...@@ -70,7 +72,18 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAd ...@@ -70,7 +72,18 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAd
if (request.header.getFlag(Flags.RD.toInt())) header.setFlag(Flags.RD.toInt()) if (request.header.getFlag(Flags.RD.toInt())) header.setFlag(Flags.RD.toInt())
request.question?.also { addRecord(it, Section.QUESTION) } request.question?.also { addRecord(it, Section.QUESTION) }
} }
private fun cookDnsResponse(request: Message, results: Iterable<InetAddress>) =
ByteBuffer.wrap(prepareDnsResponse(request).apply {
header.setFlag(Flags.RA.toInt()) // recursion available
for (address in results) addRecord(when (address) {
is Inet4Address -> ARecord(question.name, DClass.IN, TTL, address)
is Inet6Address -> AAAARecord(question.name, DClass.IN, TTL, address)
else -> throw IllegalStateException("Unsupported address $address")
}, Section.ANSWER)
}.toWire())
} }
private val monitor = ChannelMonitor() private val monitor = ChannelMonitor()
override val coroutineContext = SupervisorJob() + CoroutineExceptionHandler { _, t -> printLog(t) } override val coroutineContext = SupervisorJob() + CoroutineExceptionHandler { _, t -> printLog(t) }
...@@ -101,10 +114,16 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAd ...@@ -101,10 +114,16 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAd
return supervisorScope { return supervisorScope {
val remote = async { withTimeout(TIMEOUT) { forward(packet) } } val remote = async { withTimeout(TIMEOUT) { forward(packet) } }
try { try {
if (forwardOnly || request.header.opcode != Opcode.QUERY) return@supervisorScope remote.await() if (request.header.opcode != Opcode.QUERY) return@supervisorScope remote.await()
val question = request.question val question = request.question
if (question?.type != Type.A) return@supervisorScope remote.await() if (question?.type != Type.A) return@supervisorScope remote.await()
val host = question.name.toString(true) val host = question.name.toString(true)
val hostsResults = hosts.resolve(host)
if (hostsResults.isNotEmpty()) {
remote.cancel()
return@supervisorScope cookDnsResponse(request, hostsResults)
}
if (forwardOnly) return@supervisorScope remote.await()
if (remoteDomainMatcher?.containsMatchIn(host) == true) return@supervisorScope remote.await() if (remoteDomainMatcher?.containsMatchIn(host) == true) return@supervisorScope remote.await()
val localResults = try { val localResults = try {
withTimeout(TIMEOUT) { localResolver(host) } withTimeout(TIMEOUT) { localResolver(host) }
...@@ -117,14 +136,7 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAd ...@@ -117,14 +136,7 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAd
if (localResults.isEmpty()) return@supervisorScope remote.await() if (localResults.isEmpty()) return@supervisorScope remote.await()
if (localIpMatcher.isEmpty() || localIpMatcher.any { subnet -> localResults.any(subnet::matches) }) { if (localIpMatcher.isEmpty() || localIpMatcher.any { subnet -> localResults.any(subnet::matches) }) {
remote.cancel() remote.cancel()
ByteBuffer.wrap(prepareDnsResponse(request).apply { cookDnsResponse(request, localResults.asIterable())
header.setFlag(Flags.RA.toInt()) // recursion available
for (address in localResults) addRecord(when (address) {
is Inet4Address -> ARecord(question.name, DClass.IN, TTL, address)
is Inet6Address -> AAAARecord(question.name, DClass.IN, TTL, address)
else -> throw IllegalStateException("Unsupported address $address")
}, Section.ANSWER)
}.toWire())
} else remote.await() } else remote.await()
} catch (e: Exception) { } catch (e: Exception) {
remote.cancel() remote.cancel()
......
...@@ -42,7 +42,7 @@ object DataStore : OnPreferenceDataStoreChangeListener { ...@@ -42,7 +42,7 @@ object DataStore : OnPreferenceDataStoreChangeListener {
publicStore.registerChangeListener(this) publicStore.registerChangeListener(this)
} }
override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String?) { override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String) {
when (key) { when (key) {
Key.id -> if (directBootAware) DirectBoot.update() Key.id -> if (directBootAware) DirectBoot.update()
} }
......
/*******************************************************************************
* *
* Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.preference
import android.graphics.Typeface
import android.text.InputFilter
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import androidx.preference.EditTextPreference
object EditTextPreferenceModifiers {
object Monospace : EditTextPreference.OnBindEditTextListener {
override fun onBindEditText(editText: EditText) {
editText.typeface = Typeface.MONOSPACE
}
}
object Port : EditTextPreference.OnBindEditTextListener {
private val portLengthFilter = arrayOf(InputFilter.LengthFilter(5))
override fun onBindEditText(editText: EditText) {
editText.inputType = EditorInfo.TYPE_CLASS_NUMBER
editText.filters = portLengthFilter
editText.setSingleLine()
}
}
}
/*******************************************************************************
* *
* Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.preference
import androidx.preference.EditTextPreference
import androidx.preference.Preference
import com.github.shadowsocks.core.R
import com.github.shadowsocks.net.HostsFile
object HostsSummaryProvider : Preference.SummaryProvider<EditTextPreference> {
override fun provideSummary(preference: EditTextPreference?): CharSequence {
val count = HostsFile(preference!!.text ?: "").configuredHostnames
return preference.context.resources.getQuantityString(R.plurals.hosts_summary, count, count)
}
}
...@@ -23,5 +23,5 @@ package com.github.shadowsocks.preference ...@@ -23,5 +23,5 @@ package com.github.shadowsocks.preference
import androidx.preference.PreferenceDataStore import androidx.preference.PreferenceDataStore
interface OnPreferenceDataStoreChangeListener { interface OnPreferenceDataStoreChangeListener {
fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String?) fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String)
} }
...@@ -65,6 +65,7 @@ object Key { ...@@ -65,6 +65,7 @@ object Key {
const val dirty = "profileDirty" const val dirty = "profileDirty"
const val tfo = "tcp_fastopen" const val tfo = "tcp_fastopen"
const val hosts = "hosts"
const val assetUpdateTime = "assetUpdateTime" const val assetUpdateTime = "assetUpdateTime"
// TV specific values // TV specific values
......
...@@ -61,6 +61,9 @@ fun String?.parseNumericAddress(): InetAddress? = Os.inet_pton(OsConstants.AF_IN ...@@ -61,6 +61,9 @@ fun String?.parseNumericAddress(): InetAddress? = Os.inet_pton(OsConstants.AF_IN
if (BuildCompat.isAtLeastQ()) it else parseNumericAddress.invoke(null, this) as InetAddress if (BuildCompat.isAtLeastQ()) it else parseNumericAddress.invoke(null, this) as InetAddress
} }
fun <K, V> MutableMap<K, V>.computeIfAbsentCompat(key: K, value: () -> V) = if (Build.VERSION.SDK_INT >= 24)
computeIfAbsent(key) { value() } else this[key] ?: value().also { put(key, it) }
fun HttpURLConnection.disconnectFromMain() { fun HttpURLConnection.disconnectFromMain() {
if (Build.VERSION.SDK_INT >= 26) disconnect() else GlobalScope.launch(Dispatchers.IO) { disconnect() } if (Build.VERSION.SDK_INT >= 26) disconnect() else GlobalScope.launch(Dispatchers.IO) { disconnect() }
} }
......
...@@ -61,6 +61,10 @@ ...@@ -61,6 +61,10 @@
<string name="tcp_fastopen_summary">Toggling might require ROOT permission</string> <string name="tcp_fastopen_summary">Toggling might require ROOT permission</string>
<string name="tcp_fastopen_summary_unsupported">Unsupported kernel version: %s &lt; 3.7.1</string> <string name="tcp_fastopen_summary_unsupported">Unsupported kernel version: %s &lt; 3.7.1</string>
<string name="tcp_fastopen_failure">Toggle failed</string> <string name="tcp_fastopen_failure">Toggle failed</string>
<plurals name="hosts_summary">
<item quantity="one">1 hostname configured</item>
<item quantity="other">%d hostnames configured</item>
</plurals>
<string name="udp_dns">Send DNS over UDP</string> <string name="udp_dns">Send DNS over UDP</string>
<string name="udp_dns_summary">Requires UDP forwarding on server side</string> <string name="udp_dns_summary">Requires UDP forwarding on server side</string>
<string name="udp_fallback">UDP Fallback</string> <string name="udp_fallback">UDP Fallback</string>
...@@ -82,7 +86,6 @@ ...@@ -82,7 +86,6 @@
<!-- alert category --> <!-- alert category -->
<string name="profile_empty">Please select a profile</string> <string name="profile_empty">Please select a profile</string>
<string name="proxy_empty">Proxy/Password should not be empty</string> <string name="proxy_empty">Proxy/Password should not be empty</string>
<string name="file_manager_missing">Please install a file manager like MiXplorer</string>
<string name="connect">Connect</string> <string name="connect">Connect</string>
<!-- menu category --> <!-- menu category -->
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
# Specifies the JVM arguments used for the daemon process. # Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings. # The setting is particularly useful for tweaking memory settings.
android.enableJetifier=true android.enableJetifier=true
# android.enableR8.fullMode=true android.enableR8.fullMode=true
android.useAndroidX=true android.useAndroidX=true
# When configured, Gradle will run in incubating parallel mode. # When configured, Gradle will run in incubating parallel mode.
......
...@@ -55,11 +55,11 @@ androidExtensions { ...@@ -55,11 +55,11 @@ androidExtensions {
dependencies { dependencies {
implementation project(':core') implementation project(':core')
implementation "androidx.browser:browser:1.0.0" implementation 'androidx.browser:browser:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'com.google.android.gms:play-services-vision:17.0.2' implementation 'com.google.android.gms:play-services-vision:17.0.2'
implementation 'com.google.firebase:firebase-ads:17.2.0' implementation 'com.google.firebase:firebase-ads:17.2.0'
implementation "com.takisoft.preferencex:preferencex-simplemenu:$preferencexVersion" implementation 'com.takisoft.preferencex:preferencex-simplemenu:1.0.0'
implementation 'com.twofortyfouram:android-plugin-api-for-locale:1.0.4' implementation 'com.twofortyfouram:android-plugin-api-for-locale:1.0.4'
implementation 'net.glxn.qrgen:android:2.0' implementation 'net.glxn.qrgen:android:2.0'
implementation 'xyz.belvi.mobilevision:barcodescanner:2.0.3' implementation 'xyz.belvi.mobilevision:barcodescanner:2.0.3'
......
...@@ -23,17 +23,12 @@ package com.github.shadowsocks ...@@ -23,17 +23,12 @@ package com.github.shadowsocks
import android.app.Application import android.app.Application
import android.content.res.Configuration import android.content.res.Configuration
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import com.github.shadowsocks.preference.BottomSheetPreferenceDialogFragment
import com.github.shadowsocks.preference.IconListPreference
import com.takisoft.preferencex.PreferenceFragmentCompat
class App : Application() { class App : Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
Core.init(this, MainActivity::class) Core.init(this, MainActivity::class)
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true) AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
PreferenceFragmentCompat.registerPreferenceFragment(IconListPreference::class.java,
BottomSheetPreferenceDialogFragment::class.java)
} }
override fun onConfigurationChanged(newConfig: Configuration) { override fun onConfigurationChanged(newConfig: Configuration) {
......
...@@ -20,24 +20,37 @@ ...@@ -20,24 +20,37 @@
package com.github.shadowsocks package com.github.shadowsocks
import android.app.Activity
import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.preference.EditTextPreference
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreference import androidx.preference.SwitchPreference
import com.github.shadowsocks.bg.BaseService import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.DirectBoot import com.github.shadowsocks.utils.DirectBoot
import com.github.shadowsocks.utils.Key import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.net.TcpFastOpen import com.github.shadowsocks.net.TcpFastOpen
import com.github.shadowsocks.preference.BrowsableEditTextPreferenceDialogFragment
import com.github.shadowsocks.preference.EditTextPreferenceModifiers
import com.github.shadowsocks.preference.HostsSummaryProvider
import com.github.shadowsocks.utils.readableMessage
import com.github.shadowsocks.utils.remove import com.github.shadowsocks.utils.remove
import com.takisoft.preferencex.PreferenceFragmentCompat
class GlobalSettingsPreferenceFragment : PreferenceFragmentCompat() { class GlobalSettingsPreferenceFragment : PreferenceFragmentCompat() {
override fun onCreatePreferencesFix(savedInstanceState: Bundle?, rootKey: String?) { companion object {
private const val REQUEST_BROWSE = 1
}
private val hosts by lazy { findPreference<EditTextPreference>(Key.hosts)!! }
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
preferenceManager.preferenceDataStore = DataStore.publicStore preferenceManager.preferenceDataStore = DataStore.publicStore
DataStore.initGlobal() DataStore.initGlobal()
addPreferencesFromResource(R.xml.pref_global) addPreferencesFromResource(R.xml.pref_global)
val boot = findPreference(Key.isAutoConnect) as SwitchPreference val boot = findPreference<SwitchPreference>(Key.isAutoConnect)!!
boot.setOnPreferenceChangeListener { _, value -> boot.setOnPreferenceChangeListener { _, value ->
BootReceiver.enabled = value as Boolean BootReceiver.enabled = value as Boolean
true true
...@@ -45,13 +58,13 @@ class GlobalSettingsPreferenceFragment : PreferenceFragmentCompat() { ...@@ -45,13 +58,13 @@ class GlobalSettingsPreferenceFragment : PreferenceFragmentCompat() {
boot.isChecked = BootReceiver.enabled boot.isChecked = BootReceiver.enabled
if (Build.VERSION.SDK_INT >= 24) boot.setSummary(R.string.auto_connect_summary_v24) if (Build.VERSION.SDK_INT >= 24) boot.setSummary(R.string.auto_connect_summary_v24)
val canToggleLocked = findPreference(Key.directBootAware) val canToggleLocked = findPreference<Preference>(Key.directBootAware)!!
if (Build.VERSION.SDK_INT >= 24) canToggleLocked.setOnPreferenceChangeListener { _, newValue -> if (Build.VERSION.SDK_INT >= 24) canToggleLocked.setOnPreferenceChangeListener { _, newValue ->
if (Core.directBootSupported && newValue as Boolean) DirectBoot.update() else DirectBoot.clean() if (Core.directBootSupported && newValue as Boolean) DirectBoot.update() else DirectBoot.clean()
true true
} else canToggleLocked.remove() } else canToggleLocked.remove()
val tfo = findPreference(Key.tfo) as SwitchPreference val tfo = findPreference<SwitchPreference>(Key.tfo)!!
tfo.isChecked = DataStore.tcpFastOpen tfo.isChecked = DataStore.tcpFastOpen
tfo.setOnPreferenceChangeListener { _, value -> tfo.setOnPreferenceChangeListener { _, value ->
if (value as Boolean && !TcpFastOpen.sendEnabled) { if (value as Boolean && !TcpFastOpen.sendEnabled) {
...@@ -68,10 +81,15 @@ class GlobalSettingsPreferenceFragment : PreferenceFragmentCompat() { ...@@ -68,10 +81,15 @@ class GlobalSettingsPreferenceFragment : PreferenceFragmentCompat() {
tfo.summary = getString(R.string.tcp_fastopen_summary_unsupported, System.getProperty("os.version")) tfo.summary = getString(R.string.tcp_fastopen_summary_unsupported, System.getProperty("os.version"))
} }
val serviceMode = findPreference(Key.serviceMode) hosts.onBindEditTextListener = EditTextPreferenceModifiers.Monospace
val portProxy = findPreference(Key.portProxy) hosts.summaryProvider = HostsSummaryProvider
val portLocalDns = findPreference(Key.portLocalDns) val serviceMode = findPreference<Preference>(Key.serviceMode)!!
val portTransproxy = findPreference(Key.portTransproxy) val portProxy = findPreference<EditTextPreference>(Key.portProxy)!!
portProxy.onBindEditTextListener = EditTextPreferenceModifiers.Port
val portLocalDns = findPreference<EditTextPreference>(Key.portLocalDns)!!
portLocalDns.onBindEditTextListener = EditTextPreferenceModifiers.Port
val portTransproxy = findPreference<EditTextPreference>(Key.portTransproxy)!!
portTransproxy.onBindEditTextListener = EditTextPreferenceModifiers.Port
val onServiceModeChange = Preference.OnPreferenceChangeListener { _, newValue -> val onServiceModeChange = Preference.OnPreferenceChangeListener { _, newValue ->
val (enabledLocalDns, enabledTransproxy) = when (newValue as String?) { val (enabledLocalDns, enabledTransproxy) = when (newValue as String?) {
Key.modeProxy -> Pair(false, false) Key.modeProxy -> Pair(false, false)
...@@ -79,20 +97,18 @@ class GlobalSettingsPreferenceFragment : PreferenceFragmentCompat() { ...@@ -79,20 +97,18 @@ class GlobalSettingsPreferenceFragment : PreferenceFragmentCompat() {
Key.modeTransproxy -> Pair(true, true) Key.modeTransproxy -> Pair(true, true)
else -> throw IllegalArgumentException("newValue: $newValue") else -> throw IllegalArgumentException("newValue: $newValue")
} }
hosts.isEnabled = enabledLocalDns
portLocalDns.isEnabled = enabledLocalDns portLocalDns.isEnabled = enabledLocalDns
portTransproxy.isEnabled = enabledTransproxy portTransproxy.isEnabled = enabledTransproxy
true true
} }
val listener: (BaseService.State) -> Unit = { val listener: (BaseService.State) -> Unit = {
if (it == BaseService.State.Stopped) { val stopped = it == BaseService.State.Stopped
tfo.isEnabled = true tfo.isEnabled = stopped
serviceMode.isEnabled = true serviceMode.isEnabled = stopped
portProxy.isEnabled = true portProxy.isEnabled = stopped
onServiceModeChange.onPreferenceChange(null, DataStore.serviceMode) if (stopped) onServiceModeChange.onPreferenceChange(null, DataStore.serviceMode) else {
} else { hosts.isEnabled = false
tfo.isEnabled = false
serviceMode.isEnabled = false
portProxy.isEnabled = false
portLocalDns.isEnabled = false portLocalDns.isEnabled = false
portTransproxy.isEnabled = false portTransproxy.isEnabled = false
} }
...@@ -102,6 +118,29 @@ class GlobalSettingsPreferenceFragment : PreferenceFragmentCompat() { ...@@ -102,6 +118,29 @@ class GlobalSettingsPreferenceFragment : PreferenceFragmentCompat() {
serviceMode.onPreferenceChangeListener = onServiceModeChange serviceMode.onPreferenceChangeListener = onServiceModeChange
} }
override fun onDisplayPreferenceDialog(preference: Preference?) {
if (preference == hosts) BrowsableEditTextPreferenceDialogFragment().apply {
setKey(hosts.key)
setTargetFragment(this@GlobalSettingsPreferenceFragment, REQUEST_BROWSE)
}.show(fragmentManager ?: return, hosts.key) else super.onDisplayPreferenceDialog(preference)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) {
REQUEST_BROWSE -> {
if (resultCode != Activity.RESULT_OK) return
val activity = activity as MainActivity
try {
// we read and persist all its content here to avoid content URL permission issues
hosts.text = activity.contentResolver.openInputStream(data!!.data!!)!!.bufferedReader().readText()
} catch (e: RuntimeException) {
activity.snackbar(e.readableMessage).show()
}
}
else -> super.onActivityResult(requestCode, resultCode, data)
}
}
override fun onDestroy() { override fun onDestroy() {
MainActivity.stateListener = null MainActivity.stateListener = null
super.onDestroy() super.onDestroy()
......
...@@ -209,7 +209,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref ...@@ -209,7 +209,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
else ImportProfilesDialogFragment().withArg(ProfilesArg(profiles)).show(supportFragmentManager, null) else ImportProfilesDialogFragment().withArg(ProfilesArg(profiles)).show(supportFragmentManager, null)
} }
override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String?) { override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String) {
when (key) { when (key) {
Key.serviceMode -> handler.post { Key.serviceMode -> handler.post {
connection.disconnect(this) connection.disconnect(this)
......
...@@ -29,27 +29,21 @@ import android.os.Bundle ...@@ -29,27 +29,21 @@ import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.view.MenuItem import android.view.MenuItem
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.os.bundleOf import androidx.preference.*
import androidx.preference.Preference
import androidx.preference.PreferenceDataStore
import androidx.preference.SwitchPreference
import com.github.shadowsocks.Core.app import com.github.shadowsocks.Core.app
import com.github.shadowsocks.database.Profile import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.plugin.* import com.github.shadowsocks.plugin.*
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.*
import com.github.shadowsocks.preference.IconListPreference
import com.github.shadowsocks.preference.OnPreferenceDataStoreChangeListener
import com.github.shadowsocks.preference.PluginConfigurationDialogFragment
import com.github.shadowsocks.utils.* import com.github.shadowsocks.utils.*
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.takisoft.preferencex.EditTextPreference
import com.takisoft.preferencex.PreferenceFragmentCompat
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
class ProfileConfigFragment : PreferenceFragmentCompat(), class ProfileConfigFragment : PreferenceFragmentCompat(),
Preference.OnPreferenceChangeListener, OnPreferenceDataStoreChangeListener { Preference.OnPreferenceChangeListener, OnPreferenceDataStoreChangeListener {
companion object { companion object PasswordSummaryProvider : Preference.SummaryProvider<EditTextPreference> {
override fun provideSummary(preference: EditTextPreference?) = "\u2022".repeat(preference?.text?.length ?: 0)
private const val REQUEST_CODE_PLUGIN_CONFIGURE = 1 private const val REQUEST_CODE_PLUGIN_CONFIGURE = 1
const val REQUEST_UNSAVED_CHANGES = 2 const val REQUEST_UNSAVED_CHANGES = 2
} }
...@@ -75,27 +69,29 @@ class ProfileConfigFragment : PreferenceFragmentCompat(), ...@@ -75,27 +69,29 @@ class ProfileConfigFragment : PreferenceFragmentCompat(),
private lateinit var receiver: BroadcastReceiver private lateinit var receiver: BroadcastReceiver
private lateinit var udpFallback: Preference private lateinit var udpFallback: Preference
override fun onCreatePreferencesFix(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
preferenceManager.preferenceDataStore = DataStore.privateStore preferenceManager.preferenceDataStore = DataStore.privateStore
val activity = requireActivity() val activity = requireActivity()
profileId = activity.intent.getLongExtra(Action.EXTRA_PROFILE_ID, -1L) profileId = activity.intent.getLongExtra(Action.EXTRA_PROFILE_ID, -1L)
addPreferencesFromResource(R.xml.pref_profile) addPreferencesFromResource(R.xml.pref_profile)
findPreference<EditTextPreference>(Key.remotePort)!!.onBindEditTextListener = EditTextPreferenceModifiers.Port
findPreference<EditTextPreference>(Key.password)!!.summaryProvider = PasswordSummaryProvider
val serviceMode = DataStore.serviceMode val serviceMode = DataStore.serviceMode
findPreference(Key.remoteDns).isEnabled = serviceMode != Key.modeProxy findPreference<Preference>(Key.remoteDns)!!.isEnabled = serviceMode != Key.modeProxy
findPreference(Key.ipv6)!!.isEnabled = serviceMode == Key.modeVpn findPreference<Preference>(Key.ipv6)!!.isEnabled = serviceMode == Key.modeVpn
isProxyApps = findPreference(Key.proxyApps) as SwitchPreference isProxyApps = findPreference(Key.proxyApps)!!
isProxyApps.isEnabled = serviceMode == Key.modeVpn isProxyApps.isEnabled = serviceMode == Key.modeVpn
isProxyApps.setOnPreferenceClickListener { isProxyApps.setOnPreferenceClickListener {
startActivity(Intent(activity, AppManager::class.java)) startActivity(Intent(activity, AppManager::class.java))
isProxyApps.isChecked = true isProxyApps.isChecked = true
false false
} }
findPreference(Key.metered)!!.apply { findPreference<Preference>(Key.metered)!!.apply {
if (Build.VERSION.SDK_INT >= 28) isEnabled = serviceMode == Key.modeVpn else remove() if (Build.VERSION.SDK_INT >= 28) isEnabled = serviceMode == Key.modeVpn else remove()
} }
findPreference(Key.udpdns).isEnabled = serviceMode != Key.modeProxy findPreference<Preference>(Key.udpdns)!!.isEnabled = serviceMode != Key.modeProxy
plugin = findPreference(Key.plugin) as IconListPreference plugin = findPreference(Key.plugin)!!
pluginConfigure = findPreference(Key.pluginConfigure) as EditTextPreference pluginConfigure = findPreference(Key.pluginConfigure)!!
plugin.unknownValueSummary = getString(R.string.plugin_unknown) plugin.unknownValueSummary = getString(R.string.plugin_unknown)
plugin.setOnPreferenceChangeListener { _, newValue -> plugin.setOnPreferenceChangeListener { _, newValue ->
pluginConfiguration = PluginConfiguration(pluginConfiguration.pluginsOptions, newValue as String) pluginConfiguration = PluginConfiguration(pluginConfiguration.pluginsOptions, newValue as String)
...@@ -108,10 +104,11 @@ class ProfileConfigFragment : PreferenceFragmentCompat(), ...@@ -108,10 +104,11 @@ class ProfileConfigFragment : PreferenceFragmentCompat(),
} }
true true
} }
pluginConfigure.onBindEditTextListener = EditTextPreferenceModifiers.Monospace
pluginConfigure.onPreferenceChangeListener = this pluginConfigure.onPreferenceChangeListener = this
initPlugins() initPlugins()
receiver = Core.listenForPackageChanges(false) { initPlugins() } receiver = Core.listenForPackageChanges(false) { initPlugins() }
udpFallback = findPreference(Key.udpFallback) udpFallback = findPreference(Key.udpFallback)!!
DataStore.privateStore.registerChangeListener(this) DataStore.privateStore.registerChangeListener(this)
} }
...@@ -124,14 +121,16 @@ class ProfileConfigFragment : PreferenceFragmentCompat(), ...@@ -124,14 +121,16 @@ class ProfileConfigFragment : PreferenceFragmentCompat(),
pluginConfiguration = PluginConfiguration(DataStore.plugin) pluginConfiguration = PluginConfiguration(DataStore.plugin)
plugin.value = pluginConfiguration.selected plugin.value = pluginConfiguration.selected
plugin.init() plugin.init()
plugin.checkSummary()
pluginConfigure.isEnabled = pluginConfiguration.selected.isNotEmpty() pluginConfigure.isEnabled = pluginConfiguration.selected.isNotEmpty()
pluginConfigure.text = pluginConfiguration.selectedOptions.toString() pluginConfigure.text = pluginConfiguration.selectedOptions.toString()
} }
private fun showPluginEditor() = displayPreferenceDialog(PluginConfigurationDialogFragment(), Key.pluginConfigure, private fun showPluginEditor() {
bundleOf(Pair("key", Key.pluginConfigure), PluginConfigurationDialogFragment().apply {
Pair(PluginConfigurationDialogFragment.PLUGIN_ID_FRAGMENT_TAG, pluginConfiguration.selected))) setArg(Key.pluginConfigure, pluginConfiguration.selected)
setTargetFragment(this@ProfileConfigFragment, 0)
}.show(fragmentManager ?: return, Key.pluginConfigure)
}
private fun saveAndExit() { private fun saveAndExit() {
val profile = ProfileManager.getProfile(profileId) ?: Profile() val profile = ProfileManager.getProfile(profileId) ?: Profile()
...@@ -163,18 +162,26 @@ class ProfileConfigFragment : PreferenceFragmentCompat(), ...@@ -163,18 +162,26 @@ class ProfileConfigFragment : PreferenceFragmentCompat(),
false false
} }
override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String?) { override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String) {
if (key != Key.proxyApps && findPreference(key) != null) DataStore.dirty = true if (key != Key.proxyApps && findPreference<Preference>(key) != null) DataStore.dirty = true
} }
override fun onDisplayPreferenceDialog(preference: Preference) { override fun onDisplayPreferenceDialog(preference: Preference) {
if (preference.key == Key.pluginConfigure) { when (preference.key) {
val intent = PluginManager.buildIntent(pluginConfiguration.selected, PluginContract.ACTION_CONFIGURE) Key.plugin -> BottomSheetPreferenceDialogFragment().apply {
if (intent.resolveActivity(requireContext().packageManager) == null) showPluginEditor() else setArg(Key.plugin)
startActivityForResult(intent setTargetFragment(this@ProfileConfigFragment, 0)
.putExtra(PluginContract.EXTRA_OPTIONS, pluginConfiguration.selectedOptions.toString()), }.show(fragmentManager ?: return, Key.plugin)
REQUEST_CODE_PLUGIN_CONFIGURE) Key.pluginConfigure -> {
} else super.onDisplayPreferenceDialog(preference) val intent = PluginManager.buildIntent(pluginConfiguration.selected, PluginContract.ACTION_CONFIGURE)
if (intent.resolveActivity(requireContext().packageManager) == null) showPluginEditor() else {
startActivityForResult(intent
.putExtra(PluginContract.EXTRA_OPTIONS, pluginConfiguration.selectedOptions.toString()),
REQUEST_CODE_PLUGIN_CONFIGURE)
}
}
else -> super.onDisplayPreferenceDialog(preference)
}
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
......
...@@ -32,6 +32,7 @@ import android.view.View ...@@ -32,6 +32,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.core.os.bundleOf
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.preference.PreferenceDialogFragmentCompat import androidx.preference.PreferenceDialogFragmentCompat
import androidx.recyclerview.widget.DefaultItemAnimator import androidx.recyclerview.widget.DefaultItemAnimator
...@@ -97,6 +98,10 @@ class BottomSheetPreferenceDialogFragment : PreferenceDialogFragmentCompat() { ...@@ -97,6 +98,10 @@ class BottomSheetPreferenceDialogFragment : PreferenceDialogFragmentCompat() {
} }
} }
fun setArg(key: String) {
arguments = bundleOf(PreferenceDialogFragmentCompat.ARG_KEY to key)
}
private val preference by lazy { getPreference() as IconListPreference } private val preference by lazy { getPreference() as IconListPreference }
private var clickedIndex = -1 private var clickedIndex = -1
......
/*******************************************************************************
* *
* Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.preference
import android.content.ActivityNotFoundException
import android.content.Intent
import androidx.appcompat.app.AlertDialog
import androidx.core.os.bundleOf
import androidx.preference.EditTextPreferenceDialogFragmentCompat
import com.github.shadowsocks.MainActivity
import com.github.shadowsocks.R
class BrowsableEditTextPreferenceDialogFragment : EditTextPreferenceDialogFragmentCompat() {
fun setKey(key: String) {
arguments = bundleOf(Pair(ARG_KEY, key))
}
override fun onPrepareDialogBuilder(builder: AlertDialog.Builder) {
super.onPrepareDialogBuilder(builder)
builder.setNeutralButton(R.string.browse) { _, _ ->
val activity = activity as MainActivity
try {
targetFragment!!.startActivityForResult(Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
}, targetRequestCode)
return@setNeutralButton
} catch (_: ActivityNotFoundException) { } catch (_: SecurityException) { }
activity.snackbar(activity.getString(R.string.file_manager_missing)).show()
}
}
}
...@@ -26,6 +26,14 @@ import android.util.AttributeSet ...@@ -26,6 +26,14 @@ import android.util.AttributeSet
import androidx.preference.ListPreference import androidx.preference.ListPreference
class IconListPreference(context: Context, attrs: AttributeSet? = null) : ListPreference(context, attrs) { class IconListPreference(context: Context, attrs: AttributeSet? = null) : ListPreference(context, attrs) {
companion object FallbackProvider : SummaryProvider<IconListPreference> {
override fun provideSummary(preference: IconListPreference?): CharSequence? {
val i = preference?.selectedEntry
return if (i != null && i < 0) preference.unknownValueSummary?.format(preference.value) else
preference?.entry
}
}
var entryIcons: Array<Drawable?>? = null var entryIcons: Array<Drawable?>? = null
val selectedEntry: Int get() = entryValues.indexOf(value) val selectedEntry: Int get() = entryValues.indexOf(value)
val entryIcon: Drawable? get() = entryIcons?.getOrNull(selectedEntry) val entryIcon: Drawable? get() = entryIcons?.getOrNull(selectedEntry)
...@@ -49,7 +57,6 @@ class IconListPreference(context: Context, attrs: AttributeSet? = null) : ListPr ...@@ -49,7 +57,6 @@ class IconListPreference(context: Context, attrs: AttributeSet? = null) : ListPr
val listener = listener val listener = listener
if (listener == null || listener.onPreferenceChange(preference, newValue)) { if (listener == null || listener.onPreferenceChange(preference, newValue)) {
value = newValue.toString() value = newValue.toString()
checkSummary()
if (entryIcons != null) icon = entryIcon if (entryIcons != null) icon = entryIcon
true true
} else false } else false
...@@ -60,13 +67,9 @@ class IconListPreference(context: Context, attrs: AttributeSet? = null) : ListPr ...@@ -60,13 +67,9 @@ class IconListPreference(context: Context, attrs: AttributeSet? = null) : ListPr
// a.recycle() // a.recycle()
} }
fun checkSummary() {
val unknownValueSummary = unknownValueSummary
if (unknownValueSummary != null) summary = if (selectedEntry < 0) unknownValueSummary.format(value) else "%s"
}
fun init() { fun init() {
icon = entryIcon icon = entryIcon
summaryProvider = FallbackProvider
} }
override fun onSetInitialValue(defaultValue: Any?) { override fun onSetInitialValue(defaultValue: Any?) {
super.onSetInitialValue(defaultValue) super.onSetInitialValue(defaultValue)
......
...@@ -23,17 +23,23 @@ package com.github.shadowsocks.preference ...@@ -23,17 +23,23 @@ package com.github.shadowsocks.preference
import android.view.View import android.view.View
import android.widget.EditText import android.widget.EditText
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.os.bundleOf
import androidx.preference.EditTextPreferenceDialogFragmentCompat
import androidx.preference.PreferenceDialogFragmentCompat
import com.github.shadowsocks.ProfileConfigActivity import com.github.shadowsocks.ProfileConfigActivity
import com.github.shadowsocks.plugin.PluginContract import com.github.shadowsocks.plugin.PluginContract
import com.github.shadowsocks.plugin.PluginManager import com.github.shadowsocks.plugin.PluginManager
import com.takisoft.preferencex.EditTextPreferenceDialogFragmentCompat
class PluginConfigurationDialogFragment : EditTextPreferenceDialogFragmentCompat() { class PluginConfigurationDialogFragment : EditTextPreferenceDialogFragmentCompat() {
companion object { companion object {
const val PLUGIN_ID_FRAGMENT_TAG = private const val PLUGIN_ID_FRAGMENT_TAG =
"com.github.shadowsocks.preference.PluginConfigurationDialogFragment.PLUGIN_ID" "com.github.shadowsocks.preference.PluginConfigurationDialogFragment.PLUGIN_ID"
} }
fun setArg(key: String, plugin: String) {
arguments = bundleOf(PreferenceDialogFragmentCompat.ARG_KEY to key, PLUGIN_ID_FRAGMENT_TAG to plugin)
}
private lateinit var editText: EditText private lateinit var editText: EditText
override fun onPrepareDialogBuilder(builder: AlertDialog.Builder) { override fun onPrepareDialogBuilder(builder: AlertDialog.Builder) {
......
<?xml version="1.0" encoding="utf-8"?>
<!-- Based on: https://android.googlesource.com/platform/frameworks/support/+/9b8fb51/preference/res/layout/preference_dialog_edittext.xml -->
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="48dp"
android:layout_marginBottom="48dp"
android:overScrollMode="ifContentScrolls">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@android:id/message"
style="?android:attr/textAppearanceSmall"
android:layout_marginLeft="24dp"
android:layout_marginRight="24dp"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="48dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?android:attr/textColorSecondary"/>
<com.google.android.material.textfield.TextInputLayout
android:orientation="vertical"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:passwordToggleEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@android:id/edit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:inputType="textPassword"
android:typeface="monospace"/>
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</ScrollView>
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen <PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android" app:initialExpandedChildrenCount="4">
xmlns:app="http://schemas.android.com/apk/res-auto"
android:key="settings_screen"
app:initialExpandedChildrenCount="3">
<SwitchPreference android:key="isAutoConnect"
android:persistent="false"
android:summary="@string/auto_connect_summary"
android:title="@string/auto_connect"/>
<SwitchPreference <SwitchPreference
android:key="directBootAware" app:key="isAutoConnect"
android:icon="@drawable/ic_action_lock" app:persistent="false"
android:summary="@string/direct_boot_aware_summary" app:summary="@string/auto_connect_summary"
android:title="@string/direct_boot_aware"/> app:title="@string/auto_connect"/>
<SwitchPreference <SwitchPreference
android:key="tcp_fastopen" app:key="directBootAware"
android:icon="@drawable/ic_action_offline_bolt" app:icon="@drawable/ic_action_lock"
android:summary="@string/tcp_fastopen_summary" app:summary="@string/direct_boot_aware_summary"
android:title="TCP Fast Open"/> app:title="@string/direct_boot_aware"/>
<SwitchPreference
app:key="tcp_fastopen"
app:icon="@drawable/ic_action_offline_bolt"
app:summary="@string/tcp_fastopen_summary"
app:title="TCP Fast Open"/>
<EditTextPreference
app:key="hosts"
app:title="hosts"/>
<SimpleMenuPreference <com.takisoft.preferencex.SimpleMenuPreference
android:key="serviceMode" app:key="serviceMode"
android:entries="@array/service_modes" app:entries="@array/service_modes"
android:entryValues="@array/service_mode_values" app:entryValues="@array/service_mode_values"
android:defaultValue="vpn" app:defaultValue="vpn"
android:summary="%s" app:title="@string/service_mode"
android:title="@string/service_mode"/> app:useSimpleSummaryProvider="true"/>
<AutoSummaryEditTextPreference <EditTextPreference
android:key="portProxy" app:key="portProxy"
android:inputType="number" app:title="@string/port_proxy"
android:maxLength="5" app:useSimpleSummaryProvider="true"/>
app:pref_summaryHasText="%s" <EditTextPreference
android:singleLine="true" app:key="portLocalDns"
android:title="@string/port_proxy"/> app:icon="@drawable/ic_action_dns"
<AutoSummaryEditTextPreference app:title="@string/port_local_dns"
android:key="portLocalDns" app:useSimpleSummaryProvider="true"/>
android:icon="@drawable/ic_action_dns" <EditTextPreference
android:inputType="number" app:key="portTransproxy"
android:maxLength="5" app:title="@string/port_transproxy"
app:pref_summaryHasText="%s" app:useSimpleSummaryProvider="true"/>
android:singleLine="true"
android:title="@string/port_local_dns"/>
<AutoSummaryEditTextPreference
android:key="portTransproxy"
android:inputType="number"
android:maxLength="5"
app:pref_summaryHasText="%s"
android:singleLine="true"
android:title="@string/port_transproxy"/>
</PreferenceScreen> </PreferenceScreen>
...@@ -2,99 +2,93 @@ ...@@ -2,99 +2,93 @@
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<AutoSummaryEditTextPreference <EditTextPreference
android:key="profileName" app:key="profileName"
app:pref_summaryHasText="%s" app:title="@string/profile_name"
android:title="@string/profile_name"/> app:useSimpleSummaryProvider="true"/>
<PreferenceCategory <PreferenceCategory
android:title="@string/proxy_cat"> app:title="@string/proxy_cat">
<AutoSummaryEditTextPreference <EditTextPreference
android:key="proxy" app:key="proxy"
app:pref_summaryHasText="%s" app:singleLine="true"
android:singleLine="true" app:title="@string/proxy"
android:title="@string/proxy"/> app:useSimpleSummaryProvider="true"/>
<AutoSummaryEditTextPreference <EditTextPreference
android:key="remotePortNum" app:key="remotePortNum"
android:inputType="number" app:title="@string/remote_port"
android:maxLength="5" app:useSimpleSummaryProvider="true"/>
app:pref_summaryHasText="%s" <EditTextPreference
android:singleLine="true" app:dialogLayout="@layout/preference_dialog_password"
android:title="@string/remote_port"/> app:key="sitekey"
<AutoSummaryEditTextPreference app:title="@string/sitekey"/>
android:inputType="textPassword" <com.takisoft.preferencex.SimpleMenuPreference
android:typeface="monospace" app:key="encMethod"
app:pref_summaryPasswordSubstituteLength="0" app:icon="@drawable/ic_notification_enhanced_encryption"
android:key="sitekey" app:entries="@array/enc_method_entry"
app:pref_summaryHasText="%s" app:entryValues="@array/enc_method_value"
android:singleLine="true" app:title="@string/enc_method"
android:title="@string/sitekey"/> app:useSimpleSummaryProvider="true"/>
<SimpleMenuPreference
android:key="encMethod"
android:icon="@drawable/ic_notification_enhanced_encryption"
android:entries="@array/enc_method_entry"
android:entryValues="@array/enc_method_value"
android:summary="%s"
android:title="@string/enc_method"/>
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory <PreferenceCategory
android:title="@string/feature_cat"> app:title="@string/feature_cat">
<SimpleMenuPreference <com.takisoft.preferencex.SimpleMenuPreference
android:key="route" app:key="route"
android:entries="@array/route_entry" app:entries="@array/route_entry"
android:entryValues="@array/route_value" app:entryValues="@array/route_value"
android:summary="%s" app:title="@string/route_list"
android:title="@string/route_list"/> app:useSimpleSummaryProvider="true"/>
<SwitchPreference <SwitchPreference
android:key="isIpv6" app:key="isIpv6"
android:icon="@drawable/ic_image_looks_6" app:icon="@drawable/ic_image_looks_6"
android:summary="@string/ipv6_summary" app:summary="@string/ipv6_summary"
android:title="@string/ipv6"/> app:title="@string/ipv6"/>
<SwitchPreference <SwitchPreference
android:key="isProxyApps" app:key="isProxyApps"
android:icon="@drawable/ic_navigation_apps" app:icon="@drawable/ic_navigation_apps"
android:summary="@string/proxied_apps_summary" app:summary="@string/proxied_apps_summary"
android:title="@string/proxied_apps"/> app:title="@string/proxied_apps"/>
<SwitchPreference <SwitchPreference
android:key="metered" app:key="metered"
android:icon="@drawable/ic_device_data_usage" app:icon="@drawable/ic_device_data_usage"
android:summary="@string/metered_summary" app:summary="@string/metered_summary"
android:title="@string/metered"/> app:title="@string/metered"/>
<AutoSummaryEditTextPreference <EditTextPreference
android:key="remoteDns" app:key="remoteDns"
android:icon="@drawable/ic_action_dns" app:icon="@drawable/ic_action_dns"
android:title="@string/remote_dns" app:title="@string/remote_dns"
app:pref_summaryHasText="%s"/> app:useSimpleSummaryProvider="true"/>
<SwitchPreference <SwitchPreference
android:key="isUdpDns" app:key="isUdpDns"
android:summary="@string/udp_dns_summary" app:icon="@drawable/ic_action_dns"
android:title="@string/udp_dns"/> app:summary="@string/udp_dns_summary"
app:title="@string/udp_dns"/>
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory <PreferenceCategory
android:title="@string/plugin"> app:title="@string/plugin">
<com.github.shadowsocks.preference.IconListPreference <com.github.shadowsocks.preference.IconListPreference
android:key="plugin" app:key="plugin"
android:persistent="false" app:persistent="false"
android:summary="%s" app:title="@string/plugin"
android:title="@string/plugin"/> app:useSimpleSummaryProvider="true"/>
<AutoSummaryEditTextPreference <EditTextPreference
android:key="plugin.configure" app:key="plugin.configure"
android:icon="@drawable/ic_action_settings" app:icon="@drawable/ic_action_settings"
android:persistent="false" app:persistent="false"
android:singleLine="true" app:singleLine="true"
app:pref_summaryHasText="%s" app:title="@string/plugin_configure"
android:title="@string/plugin_configure"/> app:useSimpleSummaryProvider="true"/>
<Preference <Preference
android:key="udpFallback" app:key="udpFallback"
android:title="@string/udp_fallback" app:title="@string/udp_fallback"
android:summary="@string/plugin_disabled"> app:summary="@string/plugin_disabled">
<intent android:targetPackage="com.github.shadowsocks" <intent android:targetPackage="com.github.shadowsocks"
android:targetClass="com.github.shadowsocks.UdpFallbackProfileActivity"/> android:targetClass="com.github.shadowsocks.UdpFallbackProfileActivity"/>
</Preference> </Preference>
......
...@@ -6,4 +6,6 @@ ...@@ -6,4 +6,6 @@
<string name="yes">Yes</string> <string name="yes">Yes</string>
<string name="no">No</string> <string name="no">No</string>
<string name="apply">Apply</string> <string name="apply">Apply</string>
<string name="browse">Browse…</string>
<string name="file_manager_missing">Please install a file manager like MiXplorer</string>
</resources> </resources>
...@@ -51,7 +51,7 @@ android { ...@@ -51,7 +51,7 @@ android {
dependencies { dependencies {
implementation project(':core') implementation project(':core')
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "androidx.leanback:leanback-preference:1.1.0-alpha01" implementation "androidx.leanback:leanback-preference:1.1.0-alpha02"
} }
apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.gms.google-services'
......
...@@ -35,10 +35,7 @@ import androidx.leanback.preference.LeanbackPreferenceFragmentCompat ...@@ -35,10 +35,7 @@ import androidx.leanback.preference.LeanbackPreferenceFragmentCompat
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import androidx.lifecycle.get import androidx.lifecycle.get
import androidx.preference.ListPreference import androidx.preference.*
import androidx.preference.Preference
import androidx.preference.PreferenceDataStore
import androidx.preference.SwitchPreference
import com.crashlytics.android.Crashlytics import com.crashlytics.android.Crashlytics
import com.github.shadowsocks.BootReceiver import com.github.shadowsocks.BootReceiver
import com.github.shadowsocks.Core import com.github.shadowsocks.Core
...@@ -50,6 +47,8 @@ import com.github.shadowsocks.database.ProfileManager ...@@ -50,6 +47,8 @@ import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.net.HttpsTest import com.github.shadowsocks.net.HttpsTest
import com.github.shadowsocks.net.TcpFastOpen import com.github.shadowsocks.net.TcpFastOpen
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.preference.EditTextPreferenceModifiers
import com.github.shadowsocks.preference.HostsSummaryProvider
import com.github.shadowsocks.preference.OnPreferenceDataStoreChangeListener import com.github.shadowsocks.preference.OnPreferenceDataStoreChangeListener
import com.github.shadowsocks.utils.Key import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.datas import com.github.shadowsocks.utils.datas
...@@ -62,18 +61,20 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo ...@@ -62,18 +61,20 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
private const val REQUEST_CONNECT = 1 private const val REQUEST_CONNECT = 1
private const val REQUEST_REPLACE_PROFILES = 2 private const val REQUEST_REPLACE_PROFILES = 2
private const val REQUEST_EXPORT_PROFILES = 3 private const val REQUEST_EXPORT_PROFILES = 3
private const val REQUEST_HOSTS = 4
private const val TAG = "MainPreferenceFragment" private const val TAG = "MainPreferenceFragment"
} }
private lateinit var fab: ListPreference private lateinit var fab: ListPreference
private lateinit var stats: Preference private lateinit var stats: Preference
private lateinit var controlImport: Preference private lateinit var controlImport: Preference
private lateinit var hosts: EditTextPreference
private lateinit var serviceMode: Preference private lateinit var serviceMode: Preference
private lateinit var tfo: SwitchPreference private lateinit var tfo: SwitchPreference
private lateinit var shareOverLan: Preference private lateinit var shareOverLan: Preference
private lateinit var portProxy: Preference private lateinit var portProxy: EditTextPreference
private lateinit var portLocalDns: Preference private lateinit var portLocalDns: EditTextPreference
private lateinit var portTransproxy: Preference private lateinit var portTransproxy: EditTextPreference
private val onServiceModeChange = Preference.OnPreferenceChangeListener { _, newValue -> private val onServiceModeChange = Preference.OnPreferenceChangeListener { _, newValue ->
val (enabledLocalDns, enabledTransproxy) = when (newValue as String?) { val (enabledLocalDns, enabledTransproxy) = when (newValue as String?) {
Key.modeProxy -> Pair(false, false) Key.modeProxy -> Pair(false, false)
...@@ -81,6 +82,7 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo ...@@ -81,6 +82,7 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
Key.modeTransproxy -> Pair(true, true) Key.modeTransproxy -> Pair(true, true)
else -> throw IllegalArgumentException("newValue: $newValue") else -> throw IllegalArgumentException("newValue: $newValue")
} }
hosts.isEnabled = enabledLocalDns
portLocalDns.isEnabled = enabledLocalDns portLocalDns.isEnabled = enabledLocalDns
portTransproxy.isEnabled = enabledTransproxy portTransproxy.isEnabled = enabledTransproxy
true true
...@@ -121,19 +123,13 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo ...@@ -121,19 +123,13 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
}) })
if (msg != null) Toast.makeText(context, getString(R.string.vpn_error, msg), Toast.LENGTH_SHORT).show() if (msg != null) Toast.makeText(context, getString(R.string.vpn_error, msg), Toast.LENGTH_SHORT).show()
this.state = state this.state = state
if (state == BaseService.State.Stopped) { val stopped = state == BaseService.State.Stopped
controlImport.isEnabled = true controlImport.isEnabled = stopped
tfo.isEnabled = true tfo.isEnabled = stopped
serviceMode.isEnabled = true serviceMode.isEnabled = stopped
shareOverLan.isEnabled = true shareOverLan.isEnabled = stopped
portProxy.isEnabled = true portProxy.isEnabled = stopped
onServiceModeChange.onPreferenceChange(null, DataStore.serviceMode) if (stopped) onServiceModeChange.onPreferenceChange(null, DataStore.serviceMode) else {
} else {
controlImport.isEnabled = false
tfo.isEnabled = false
serviceMode.isEnabled = false
shareOverLan.isEnabled = false
portProxy.isEnabled = false
portLocalDns.isEnabled = false portLocalDns.isEnabled = false
portTransproxy.isEnabled = false portTransproxy.isEnabled = false
} }
...@@ -156,12 +152,12 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo ...@@ -156,12 +152,12 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
preferenceManager.preferenceDataStore = DataStore.publicStore preferenceManager.preferenceDataStore = DataStore.publicStore
DataStore.initGlobal() DataStore.initGlobal()
addPreferencesFromResource(R.xml.pref_main) addPreferencesFromResource(R.xml.pref_main)
fab = findPreference(Key.id) as ListPreference fab = findPreference(Key.id)!!
populateProfiles() populateProfiles()
stats = findPreference(Key.controlStats) stats = findPreference(Key.controlStats)!!
controlImport = findPreference(Key.controlImport) controlImport = findPreference(Key.controlImport)!!
(findPreference(Key.isAutoConnect) as SwitchPreference).apply { findPreference<SwitchPreference>(Key.isAutoConnect)!!.apply {
setOnPreferenceChangeListener { _, value -> setOnPreferenceChangeListener { _, value ->
BootReceiver.enabled = value as Boolean BootReceiver.enabled = value as Boolean
true true
...@@ -169,7 +165,7 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo ...@@ -169,7 +165,7 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
isChecked = BootReceiver.enabled isChecked = BootReceiver.enabled
} }
tfo = findPreference(Key.tfo) as SwitchPreference tfo = findPreference(Key.tfo)!!
tfo.isChecked = DataStore.tcpFastOpen tfo.isChecked = DataStore.tcpFastOpen
tfo.setOnPreferenceChangeListener { _, value -> tfo.setOnPreferenceChangeListener { _, value ->
if (value as Boolean && !TcpFastOpen.sendEnabled) { if (value as Boolean && !TcpFastOpen.sendEnabled) {
...@@ -186,19 +182,18 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo ...@@ -186,19 +182,18 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
tfo.summary = getString(R.string.tcp_fastopen_summary_unsupported, System.getProperty("os.version")) tfo.summary = getString(R.string.tcp_fastopen_summary_unsupported, System.getProperty("os.version"))
} }
serviceMode = findPreference(Key.serviceMode) hosts = findPreference(Key.hosts)!!
shareOverLan = findPreference(Key.shareOverLan) hosts.summaryProvider = HostsSummaryProvider
portProxy = findPreference(Key.portProxy) serviceMode = findPreference(Key.serviceMode)!!
portLocalDns = findPreference(Key.portLocalDns) shareOverLan = findPreference(Key.shareOverLan)!!
portTransproxy = findPreference(Key.portTransproxy) portProxy = findPreference(Key.portProxy)!!
portProxy.onBindEditTextListener = EditTextPreferenceModifiers.Port
portLocalDns = findPreference(Key.portLocalDns)!!
portLocalDns.onBindEditTextListener = EditTextPreferenceModifiers.Port
portTransproxy = findPreference(Key.portTransproxy)!!
portTransproxy.onBindEditTextListener = EditTextPreferenceModifiers.Port
serviceMode.onPreferenceChangeListener = onServiceModeChange serviceMode.onPreferenceChangeListener = onServiceModeChange
findPreference(Key.about).apply { findPreference<Preference>(Key.about)!!.summary = getString(R.string.about_title, BuildConfig.VERSION_NAME)
summary = getString(R.string.about_title, BuildConfig.VERSION_NAME)
setOnPreferenceClickListener {
Toast.makeText(requireContext(), "https://shadowsocks.org/android", Toast.LENGTH_SHORT).show()
true
}
}
tester = ViewModelProviders.of(this).get() tester = ViewModelProviders.of(this).get()
changeState(BaseService.State.Idle) // reset everything to init state changeState(BaseService.State.Idle) // reset everything to init state
...@@ -236,7 +231,7 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo ...@@ -236,7 +231,7 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
} }
} }
override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String?) { override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String) {
when (key) { when (key) {
Key.serviceMode -> handler.post { Key.serviceMode -> handler.post {
connection.disconnect(requireContext()) connection.disconnect(requireContext())
...@@ -274,15 +269,25 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo ...@@ -274,15 +269,25 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
}, REQUEST_EXPORT_PROFILES) }, REQUEST_EXPORT_PROFILES)
true true
} }
Key.about -> {
Toast.makeText(requireContext(), "https://shadowsocks.org/android", Toast.LENGTH_SHORT).show()
true
}
else -> super.onPreferenceTreeClick(preference) else -> super.onPreferenceTreeClick(preference)
} }
private fun startFilesForResult(intent: Intent, requestCode: Int) { override fun onDisplayPreferenceDialog(preference: Preference?) {
if (preference != hosts || startFilesForResult(Intent(Intent.ACTION_GET_CONTENT).setType("*/*"), REQUEST_HOSTS))
super.onDisplayPreferenceDialog(preference)
}
private fun startFilesForResult(intent: Intent, requestCode: Int): Boolean {
try { try {
startActivityForResult(intent.addCategory(Intent.CATEGORY_OPENABLE), requestCode) startActivityForResult(intent.addCategory(Intent.CATEGORY_OPENABLE), requestCode)
return return false
} catch (_: ActivityNotFoundException) { } catch (_: SecurityException) { } } catch (_: ActivityNotFoundException) { } catch (_: SecurityException) { }
Toast.makeText(requireContext(), R.string.file_manager_missing, Toast.LENGTH_SHORT).show() Toast.makeText(requireContext(), R.string.file_manager_missing, Toast.LENGTH_SHORT).show()
return true
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
...@@ -317,6 +322,16 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo ...@@ -317,6 +322,16 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
Toast.makeText(context, e.readableMessage, Toast.LENGTH_SHORT).show() Toast.makeText(context, e.readableMessage, Toast.LENGTH_SHORT).show()
} }
} }
REQUEST_HOSTS -> {
if (resultCode != Activity.RESULT_OK) return
val context = requireContext()
try {
// we read and persist all its content here to avoid content URL permission issues
hosts.text = context.contentResolver.openInputStream(data!!.data!!)!!.bufferedReader().readText()
} catch (e: RuntimeException) {
Toast.makeText(context, e.readableMessage, Toast.LENGTH_SHORT).show()
}
}
else -> super.onActivityResult(requestCode, resultCode, data) else -> super.onActivityResult(requestCode, resultCode, data)
} }
} }
......
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen <PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android" app:title="@string/app_name">
xmlns:app="http://schemas.android.com/apk/res-auto"
android:title="@string/app_name">
<!-- profileId is stored in privateStore so its value needs to be populated manually --> <!-- profileId is stored in privateStore so its value needs to be populated manually -->
<ListPreference <ListPreference
android:key="profileId" app:key="profileId"
android:title="@string/connect" app:title="@string/connect"
android:summary="%s" app:persistent="false"
android:persistent="false"/> app:useSimpleSummaryProvider="true"/>
<Preference <Preference
android:key="control.stats" app:key="control.stats"
android:title="@string/connection_test_pending" app:title="@string/connection_test_pending"
android:summary="@string/stat_summary"/> app:summary="@string/stat_summary"/>
<Preference <Preference
android:key="control.import" app:key="control.import"
android:title="@string/action_replace_file"/> app:title="@string/action_replace_file"/>
<Preference <Preference
android:key="control.export" app:key="control.export"
android:title="@string/action_export_file"/> app:title="@string/action_export_file"/>
<PreferenceCategory <PreferenceCategory
android:key="settings" app:key="settings"
android:title="@string/settings" app:title="@string/settings"
app:initialExpandedChildrenCount="2"> app:initialExpandedChildrenCount="3">
<SwitchPreference <SwitchPreference
android:key="isAutoConnect" app:key="isAutoConnect"
android:persistent="false" app:persistent="false"
android:summary="@string/auto_connect_summary" app:summary="@string/auto_connect_summary"
android:title="@string/auto_connect"/> app:title="@string/auto_connect"/>
<SwitchPreference <SwitchPreference
android:key="tcp_fastopen" app:key="tcp_fastopen"
android:summary="@string/tcp_fastopen_summary" app:summary="@string/tcp_fastopen_summary"
android:title="TCP Fast Open"/> app:title="TCP Fast Open"/>
<EditTextPreference
app:key="hosts"
app:title="hosts"/>
<ListPreference <ListPreference
android:key="serviceMode" app:key="serviceMode"
android:entries="@array/service_modes" app:entries="@array/service_modes"
android:entryValues="@array/service_mode_values" app:entryValues="@array/service_mode_values"
android:defaultValue="vpn" app:defaultValue="vpn"
android:summary="%s" app:title="@string/service_mode"
android:title="@string/service_mode"/> app:useSimpleSummaryProvider="true"/>
<SwitchPreference <SwitchPreference
android:key="shareOverLan" app:key="shareOverLan"
android:title="@string/share_over_lan"/> app:title="@string/share_over_lan"/>
<com.takisoft.preferencex.AutoSummaryEditTextPreference <EditTextPreference
android:key="portProxy" app:key="portProxy"
android:inputType="number" app:title="@string/port_proxy"
android:maxLength="5" app:useSimpleSummaryProvider="true"/>
app:pref_summaryHasText="%s" <EditTextPreference
android:singleLine="true" app:key="portLocalDns"
android:title="@string/port_proxy"/> app:title="@string/port_local_dns"
<com.takisoft.preferencex.AutoSummaryEditTextPreference app:useSimpleSummaryProvider="true"/>
android:key="portLocalDns" <EditTextPreference
android:inputType="number" app:key="portTransproxy"
android:maxLength="5" app:title="@string/port_transproxy"
app:pref_summaryHasText="%s" app:useSimpleSummaryProvider="true"/>
android:singleLine="true"
android:title="@string/port_local_dns"/>
<com.takisoft.preferencex.AutoSummaryEditTextPreference
android:key="portTransproxy"
android:inputType="number"
android:maxLength="5"
app:pref_summaryHasText="%s"
android:singleLine="true"
android:title="@string/port_transproxy"/>
</PreferenceCategory> </PreferenceCategory>
<Preference <Preference
android:key="about" app:key="about"
android:title="@string/about"/> app:title="@string/about"/>
</PreferenceScreen> </PreferenceScreen>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment