diff --git a/build.gradle b/build.gradle index 04d6382acb..54c1798212 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,6 @@ buildscript { sdkVersion = 28 compileSdkVersion = 28 buildToolsVersion = '28.0.3' - preferencexVersion = '1.0.0' junitVersion = '4.12' androidTestVersion = '1.1.1' androidEspressoVersion = '3.1.1' diff --git a/core/build.gradle b/core/build.gradle index 683521a67e..658a9da77e 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -49,13 +49,12 @@ dependencies { api project(':plugin') api "androidx.lifecycle:lifecycle-extensions:$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.work:work-runtime-ktx:2.0.1' api 'com.crashlytics.sdk.android:crashlytics:2.10.0' api 'com.google.firebase:firebase-config:17.0.0' api 'com.google.firebase:firebase-core:16.0.9' - api "com.takisoft.preferencex:preferencex:$preferencexVersion" api 'dnsjava:dnsjava:2.1.8' api 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.1' api 'org.connectbot.jsocks:jsocks:1.0.0' diff --git a/core/src/main/java/com/github/shadowsocks/VpnRequestActivity.kt b/core/src/main/java/com/github/shadowsocks/VpnRequestActivity.kt index ac8b301c41..89b4843c31 100644 --- a/core/src/main/java/com/github/shadowsocks/VpnRequestActivity.kt +++ b/core/src/main/java/com/github/shadowsocks/VpnRequestActivity.kt @@ -63,6 +63,7 @@ class VpnRequestActivity : AppCompatActivity() { } 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 { Toast.makeText(this, R.string.vpn_permission_denied, Toast.LENGTH_LONG).show() Crashlytics.log(Log.ERROR, TAG, "Failed to start VpnService from onActivityResult: $data") diff --git a/core/src/main/java/com/github/shadowsocks/bg/LocalDnsService.kt b/core/src/main/java/com/github/shadowsocks/bg/LocalDnsService.kt index b32fa265e6..2bda73779e 100644 --- a/core/src/main/java/com/github/shadowsocks/bg/LocalDnsService.kt +++ b/core/src/main/java/com/github/shadowsocks/bg/LocalDnsService.kt @@ -23,10 +23,12 @@ package com.github.shadowsocks.bg import com.github.shadowsocks.Core.app import com.github.shadowsocks.acl.Acl import com.github.shadowsocks.core.R +import com.github.shadowsocks.net.HostsFile import com.github.shadowsocks.net.LocalDnsServer import com.github.shadowsocks.net.Socks5Endpoint import com.github.shadowsocks.net.Subnet import com.github.shadowsocks.preference.DataStore +import com.github.shadowsocks.utils.Key import kotlinx.coroutines.CoroutineScope import java.net.InetSocketAddress import java.net.URI @@ -49,7 +51,8 @@ object LocalDnsService { val dns = URI("dns://${profile.remoteDns}") LocalDnsServer(this::resolver, 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 when (profile.route) { Acl.BYPASS_CHN, Acl.BYPASS_LAN_CHN, Acl.GFWLIST, Acl.CUSTOM_RULES -> { diff --git a/core/src/main/java/com/github/shadowsocks/net/HostsFile.kt b/core/src/main/java/com/github/shadowsocks/net/HostsFile.kt new file mode 100644 index 0000000000..642ad96560 --- /dev/null +++ b/core/src/main/java/com/github/shadowsocks/net/HostsFile.kt @@ -0,0 +1,39 @@ +/******************************************************************************* + * * + * Copyright (C) 2019 by Max Lv * + * Copyright (C) 2019 by Mygod Studio * + * * + * 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 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>() + 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() +} diff --git a/core/src/main/java/com/github/shadowsocks/net/LocalDnsServer.kt b/core/src/main/java/com/github/shadowsocks/net/LocalDnsServer.kt index bf8cf2ba6f..3b8bc674cd 100644 --- a/core/src/main/java/com/github/shadowsocks/net/LocalDnsServer.kt +++ b/core/src/main/java/com/github/shadowsocks/net/LocalDnsServer.kt @@ -43,7 +43,9 @@ import java.nio.channels.SocketChannel * https://github.com/shadowsocks/overture/tree/874f22613c334a3b78e40155a55479b7b69fee04 */ class LocalDnsServer(private val localResolver: suspend (String) -> Array, - 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. */ @@ -70,7 +72,18 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array) = + 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() override val coroutineContext = SupervisorJob() + CoroutineExceptionHandler { _, t -> printLog(t) } @@ -101,10 +114,16 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array Array localResults.any(subnet::matches) }) { remote.cancel() - ByteBuffer.wrap(prepareDnsResponse(request).apply { - 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()) + cookDnsResponse(request, localResults.asIterable()) } else remote.await() } catch (e: Exception) { remote.cancel() diff --git a/core/src/main/java/com/github/shadowsocks/preference/DataStore.kt b/core/src/main/java/com/github/shadowsocks/preference/DataStore.kt index 808c405bc1..c70dfc9aaf 100644 --- a/core/src/main/java/com/github/shadowsocks/preference/DataStore.kt +++ b/core/src/main/java/com/github/shadowsocks/preference/DataStore.kt @@ -42,7 +42,7 @@ object DataStore : OnPreferenceDataStoreChangeListener { publicStore.registerChangeListener(this) } - override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String?) { + override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String) { when (key) { Key.id -> if (directBootAware) DirectBoot.update() } diff --git a/core/src/main/java/com/github/shadowsocks/preference/EditTextPreferenceModifiers.kt b/core/src/main/java/com/github/shadowsocks/preference/EditTextPreferenceModifiers.kt new file mode 100644 index 0000000000..72ba61c6ae --- /dev/null +++ b/core/src/main/java/com/github/shadowsocks/preference/EditTextPreferenceModifiers.kt @@ -0,0 +1,45 @@ +/******************************************************************************* + * * + * Copyright (C) 2019 by Max Lv * + * Copyright (C) 2019 by Mygod Studio * + * * + * 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 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() + } + } +} diff --git a/core/src/main/java/com/github/shadowsocks/preference/HostsSummaryProvider.kt b/core/src/main/java/com/github/shadowsocks/preference/HostsSummaryProvider.kt new file mode 100644 index 0000000000..a3afa4bef7 --- /dev/null +++ b/core/src/main/java/com/github/shadowsocks/preference/HostsSummaryProvider.kt @@ -0,0 +1,33 @@ +/******************************************************************************* + * * + * Copyright (C) 2019 by Max Lv * + * Copyright (C) 2019 by Mygod Studio * + * * + * 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 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 { + override fun provideSummary(preference: EditTextPreference?): CharSequence { + val count = HostsFile(preference!!.text ?: "").configuredHostnames + return preference.context.resources.getQuantityString(R.plurals.hosts_summary, count, count) + } +} diff --git a/core/src/main/java/com/github/shadowsocks/preference/OnPreferenceDataStoreChangeListener.kt b/core/src/main/java/com/github/shadowsocks/preference/OnPreferenceDataStoreChangeListener.kt index bd5c1599ab..e18484b335 100644 --- a/core/src/main/java/com/github/shadowsocks/preference/OnPreferenceDataStoreChangeListener.kt +++ b/core/src/main/java/com/github/shadowsocks/preference/OnPreferenceDataStoreChangeListener.kt @@ -23,5 +23,5 @@ package com.github.shadowsocks.preference import androidx.preference.PreferenceDataStore interface OnPreferenceDataStoreChangeListener { - fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String?) + fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String) } diff --git a/core/src/main/java/com/github/shadowsocks/utils/Constants.kt b/core/src/main/java/com/github/shadowsocks/utils/Constants.kt index fb7f9f549e..c2e13323db 100644 --- a/core/src/main/java/com/github/shadowsocks/utils/Constants.kt +++ b/core/src/main/java/com/github/shadowsocks/utils/Constants.kt @@ -65,6 +65,7 @@ object Key { const val dirty = "profileDirty" const val tfo = "tcp_fastopen" + const val hosts = "hosts" const val assetUpdateTime = "assetUpdateTime" // TV specific values diff --git a/core/src/main/java/com/github/shadowsocks/utils/Utils.kt b/core/src/main/java/com/github/shadowsocks/utils/Utils.kt index 3548d58847..5d105112c8 100644 --- a/core/src/main/java/com/github/shadowsocks/utils/Utils.kt +++ b/core/src/main/java/com/github/shadowsocks/utils/Utils.kt @@ -54,9 +54,12 @@ private val parseNumericAddress by lazy { * * Bug: https://issuetracker.google.com/issues/123456213 */ -fun String?.parseNumericAddress(): InetAddress? = Os.inet_pton(OsConstants.AF_INET, this) +fun String.parseNumericAddress(): InetAddress? = Os.inet_pton(OsConstants.AF_INET, this) ?: Os.inet_pton(OsConstants.AF_INET6, this)?.let { parseNumericAddress.invoke(null, this) as InetAddress } +fun MutableMap.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() { if (Build.VERSION.SDK_INT >= 26) disconnect() else GlobalScope.launch(Dispatchers.IO) { disconnect() } } diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 050cbd2e84..c8a890fff5 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -61,6 +61,10 @@ Toggling might require ROOT permission Unsupported kernel version: %s < 3.7.1 Toggle failed + + 1 hostname configured + %d hostnames configured + Send DNS over UDP Requires UDP forwarding on server side UDP Fallback @@ -82,7 +86,6 @@ Please select a profile Proxy/Password should not be empty - Please install a file manager like MiXplorer Connect diff --git a/gradle.properties b/gradle.properties index 00a8c31dfd..b96cc2fa3d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,7 +11,7 @@ # The setting is particularly useful for tweaking memory settings. android.enableJetifier=true android.enableR8=true -# android.enableR8.fullMode=true +android.enableR8.fullMode=true android.useAndroidX=true # When configured, Gradle will run in incubating parallel mode. diff --git a/mobile/build.gradle b/mobile/build.gradle index 890a9b9b35..965ed782b5 100644 --- a/mobile/build.gradle +++ b/mobile/build.gradle @@ -56,11 +56,11 @@ androidExtensions { dependencies { 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 'com.google.android.gms:play-services-vision:17.0.2' 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 'net.glxn.qrgen:android:2.0' implementation 'xyz.belvi.mobilevision:barcodescanner:2.0.3' diff --git a/mobile/src/main/java/com/github/shadowsocks/App.kt b/mobile/src/main/java/com/github/shadowsocks/App.kt index 399bcb07b2..13c58c9182 100644 --- a/mobile/src/main/java/com/github/shadowsocks/App.kt +++ b/mobile/src/main/java/com/github/shadowsocks/App.kt @@ -23,17 +23,12 @@ package com.github.shadowsocks import android.app.Application import android.content.res.Configuration 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() { override fun onCreate() { super.onCreate() Core.init(this, MainActivity::class) AppCompatDelegate.setCompatVectorFromResourcesEnabled(true) - PreferenceFragmentCompat.registerPreferenceFragment(IconListPreference::class.java, - BottomSheetPreferenceDialogFragment::class.java) } override fun onConfigurationChanged(newConfig: Configuration) { diff --git a/mobile/src/main/java/com/github/shadowsocks/GlobalSettingsPreferenceFragment.kt b/mobile/src/main/java/com/github/shadowsocks/GlobalSettingsPreferenceFragment.kt index 7d35072d8a..ae37045838 100644 --- a/mobile/src/main/java/com/github/shadowsocks/GlobalSettingsPreferenceFragment.kt +++ b/mobile/src/main/java/com/github/shadowsocks/GlobalSettingsPreferenceFragment.kt @@ -20,24 +20,37 @@ package com.github.shadowsocks +import android.app.Activity +import android.content.Intent import android.os.Build import android.os.Bundle +import androidx.preference.EditTextPreference import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat import androidx.preference.SwitchPreference import com.github.shadowsocks.bg.BaseService import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.utils.DirectBoot import com.github.shadowsocks.utils.Key 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.takisoft.preferencex.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(Key.hosts)!! } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { preferenceManager.preferenceDataStore = DataStore.publicStore DataStore.initGlobal() addPreferencesFromResource(R.xml.pref_global) - val boot = findPreference(Key.isAutoConnect) as SwitchPreference + val boot = findPreference(Key.isAutoConnect)!! boot.setOnPreferenceChangeListener { _, value -> BootReceiver.enabled = value as Boolean true @@ -45,13 +58,13 @@ class GlobalSettingsPreferenceFragment : PreferenceFragmentCompat() { boot.isChecked = BootReceiver.enabled if (Build.VERSION.SDK_INT >= 24) boot.setSummary(R.string.auto_connect_summary_v24) - val canToggleLocked = findPreference(Key.directBootAware) + val canToggleLocked = findPreference(Key.directBootAware)!! if (Build.VERSION.SDK_INT >= 24) canToggleLocked.setOnPreferenceChangeListener { _, newValue -> if (Core.directBootSupported && newValue as Boolean) DirectBoot.update() else DirectBoot.clean() true } else canToggleLocked.remove() - val tfo = findPreference(Key.tfo) as SwitchPreference + val tfo = findPreference(Key.tfo)!! tfo.isChecked = DataStore.tcpFastOpen tfo.setOnPreferenceChangeListener { _, value -> if (value as Boolean && !TcpFastOpen.sendEnabled) { @@ -68,10 +81,15 @@ class GlobalSettingsPreferenceFragment : PreferenceFragmentCompat() { tfo.summary = getString(R.string.tcp_fastopen_summary_unsupported, System.getProperty("os.version")) } - val serviceMode = findPreference(Key.serviceMode) - val portProxy = findPreference(Key.portProxy) - val portLocalDns = findPreference(Key.portLocalDns) - val portTransproxy = findPreference(Key.portTransproxy) + hosts.onBindEditTextListener = EditTextPreferenceModifiers.Monospace + hosts.summaryProvider = HostsSummaryProvider + val serviceMode = findPreference(Key.serviceMode)!! + val portProxy = findPreference(Key.portProxy)!! + portProxy.onBindEditTextListener = EditTextPreferenceModifiers.Port + val portLocalDns = findPreference(Key.portLocalDns)!! + portLocalDns.onBindEditTextListener = EditTextPreferenceModifiers.Port + val portTransproxy = findPreference(Key.portTransproxy)!! + portTransproxy.onBindEditTextListener = EditTextPreferenceModifiers.Port val onServiceModeChange = Preference.OnPreferenceChangeListener { _, newValue -> val (enabledLocalDns, enabledTransproxy) = when (newValue as String?) { Key.modeProxy -> Pair(false, false) @@ -79,20 +97,18 @@ class GlobalSettingsPreferenceFragment : PreferenceFragmentCompat() { Key.modeTransproxy -> Pair(true, true) else -> throw IllegalArgumentException("newValue: $newValue") } + hosts.isEnabled = enabledLocalDns portLocalDns.isEnabled = enabledLocalDns portTransproxy.isEnabled = enabledTransproxy true } val listener: (BaseService.State) -> Unit = { - if (it == BaseService.State.Stopped) { - tfo.isEnabled = true - serviceMode.isEnabled = true - portProxy.isEnabled = true - onServiceModeChange.onPreferenceChange(null, DataStore.serviceMode) - } else { - tfo.isEnabled = false - serviceMode.isEnabled = false - portProxy.isEnabled = false + val stopped = it == BaseService.State.Stopped + tfo.isEnabled = stopped + serviceMode.isEnabled = stopped + portProxy.isEnabled = stopped + if (stopped) onServiceModeChange.onPreferenceChange(null, DataStore.serviceMode) else { + hosts.isEnabled = false portLocalDns.isEnabled = false portTransproxy.isEnabled = false } @@ -102,6 +118,29 @@ class GlobalSettingsPreferenceFragment : PreferenceFragmentCompat() { 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() { MainActivity.stateListener = null super.onDestroy() diff --git a/mobile/src/main/java/com/github/shadowsocks/MainActivity.kt b/mobile/src/main/java/com/github/shadowsocks/MainActivity.kt index 1e511509cf..5159a556a5 100644 --- a/mobile/src/main/java/com/github/shadowsocks/MainActivity.kt +++ b/mobile/src/main/java/com/github/shadowsocks/MainActivity.kt @@ -209,7 +209,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref else ImportProfilesDialogFragment().withArg(ProfilesArg(profiles)).show(supportFragmentManager, null) } - override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String?) { + override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String) { when (key) { Key.serviceMode -> handler.post { connection.disconnect(this) diff --git a/mobile/src/main/java/com/github/shadowsocks/ProfileConfigFragment.kt b/mobile/src/main/java/com/github/shadowsocks/ProfileConfigFragment.kt index 68303dfe4f..eef6b89a0e 100644 --- a/mobile/src/main/java/com/github/shadowsocks/ProfileConfigFragment.kt +++ b/mobile/src/main/java/com/github/shadowsocks/ProfileConfigFragment.kt @@ -29,27 +29,21 @@ import android.os.Bundle import android.os.Parcelable import android.view.MenuItem import androidx.appcompat.app.AlertDialog -import androidx.core.os.bundleOf -import androidx.preference.Preference -import androidx.preference.PreferenceDataStore -import androidx.preference.SwitchPreference +import androidx.preference.* import com.github.shadowsocks.Core.app import com.github.shadowsocks.database.Profile import com.github.shadowsocks.database.ProfileManager import com.github.shadowsocks.plugin.* -import com.github.shadowsocks.preference.DataStore -import com.github.shadowsocks.preference.IconListPreference -import com.github.shadowsocks.preference.OnPreferenceDataStoreChangeListener -import com.github.shadowsocks.preference.PluginConfigurationDialogFragment +import com.github.shadowsocks.preference.* import com.github.shadowsocks.utils.* import com.google.android.material.snackbar.Snackbar -import com.takisoft.preferencex.EditTextPreference -import com.takisoft.preferencex.PreferenceFragmentCompat import kotlinx.android.parcel.Parcelize class ProfileConfigFragment : PreferenceFragmentCompat(), Preference.OnPreferenceChangeListener, OnPreferenceDataStoreChangeListener { - companion object { + companion object PasswordSummaryProvider : Preference.SummaryProvider { + override fun provideSummary(preference: EditTextPreference?) = "\u2022".repeat(preference?.text?.length ?: 0) + private const val REQUEST_CODE_PLUGIN_CONFIGURE = 1 const val REQUEST_UNSAVED_CHANGES = 2 } @@ -75,27 +69,29 @@ class ProfileConfigFragment : PreferenceFragmentCompat(), private lateinit var receiver: BroadcastReceiver private lateinit var udpFallback: Preference - override fun onCreatePreferencesFix(savedInstanceState: Bundle?, rootKey: String?) { + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { preferenceManager.preferenceDataStore = DataStore.privateStore val activity = requireActivity() profileId = activity.intent.getLongExtra(Action.EXTRA_PROFILE_ID, -1L) addPreferencesFromResource(R.xml.pref_profile) + findPreference(Key.remotePort)!!.onBindEditTextListener = EditTextPreferenceModifiers.Port + findPreference(Key.password)!!.summaryProvider = PasswordSummaryProvider val serviceMode = DataStore.serviceMode - findPreference(Key.remoteDns).isEnabled = serviceMode != Key.modeProxy - findPreference(Key.ipv6)!!.isEnabled = serviceMode == Key.modeVpn - isProxyApps = findPreference(Key.proxyApps) as SwitchPreference + findPreference(Key.remoteDns)!!.isEnabled = serviceMode != Key.modeProxy + findPreference(Key.ipv6)!!.isEnabled = serviceMode == Key.modeVpn + isProxyApps = findPreference(Key.proxyApps)!! isProxyApps.isEnabled = serviceMode == Key.modeVpn isProxyApps.setOnPreferenceClickListener { startActivity(Intent(activity, AppManager::class.java)) isProxyApps.isChecked = true false } - findPreference(Key.metered)!!.apply { + findPreference(Key.metered)!!.apply { if (Build.VERSION.SDK_INT >= 28) isEnabled = serviceMode == Key.modeVpn else remove() } - findPreference(Key.udpdns).isEnabled = serviceMode != Key.modeProxy - plugin = findPreference(Key.plugin) as IconListPreference - pluginConfigure = findPreference(Key.pluginConfigure) as EditTextPreference + findPreference(Key.udpdns)!!.isEnabled = serviceMode != Key.modeProxy + plugin = findPreference(Key.plugin)!! + pluginConfigure = findPreference(Key.pluginConfigure)!! plugin.unknownValueSummary = getString(R.string.plugin_unknown) plugin.setOnPreferenceChangeListener { _, newValue -> pluginConfiguration = PluginConfiguration(pluginConfiguration.pluginsOptions, newValue as String) @@ -108,10 +104,11 @@ class ProfileConfigFragment : PreferenceFragmentCompat(), } true } + pluginConfigure.onBindEditTextListener = EditTextPreferenceModifiers.Monospace pluginConfigure.onPreferenceChangeListener = this initPlugins() receiver = Core.listenForPackageChanges(false) { initPlugins() } - udpFallback = findPreference(Key.udpFallback) + udpFallback = findPreference(Key.udpFallback)!! DataStore.privateStore.registerChangeListener(this) } @@ -124,14 +121,16 @@ class ProfileConfigFragment : PreferenceFragmentCompat(), pluginConfiguration = PluginConfiguration(DataStore.plugin) plugin.value = pluginConfiguration.selected plugin.init() - plugin.checkSummary() pluginConfigure.isEnabled = pluginConfiguration.selected.isNotEmpty() pluginConfigure.text = pluginConfiguration.selectedOptions.toString() } - private fun showPluginEditor() = displayPreferenceDialog(PluginConfigurationDialogFragment(), Key.pluginConfigure, - bundleOf(Pair("key", Key.pluginConfigure), - Pair(PluginConfigurationDialogFragment.PLUGIN_ID_FRAGMENT_TAG, pluginConfiguration.selected))) + private fun showPluginEditor() { + PluginConfigurationDialogFragment().apply { + setArg(Key.pluginConfigure, pluginConfiguration.selected) + setTargetFragment(this@ProfileConfigFragment, 0) + }.show(fragmentManager ?: return, Key.pluginConfigure) + } private fun saveAndExit() { val profile = ProfileManager.getProfile(profileId) ?: Profile() @@ -163,18 +162,26 @@ class ProfileConfigFragment : PreferenceFragmentCompat(), false } - override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String?) { - if (key != Key.proxyApps && findPreference(key) != null) DataStore.dirty = true + override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String) { + if (key != Key.proxyApps && findPreference(key) != null) DataStore.dirty = true } override fun onDisplayPreferenceDialog(preference: Preference) { - if (preference.key == Key.pluginConfigure) { - 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) + when (preference.key) { + Key.plugin -> BottomSheetPreferenceDialogFragment().apply { + setArg(Key.plugin) + setTargetFragment(this@ProfileConfigFragment, 0) + }.show(fragmentManager ?: return, Key.plugin) + Key.pluginConfigure -> { + 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?) { diff --git a/mobile/src/main/java/com/github/shadowsocks/preference/BottomSheetPreferenceDialogFragment.kt b/mobile/src/main/java/com/github/shadowsocks/preference/BottomSheetPreferenceDialogFragment.kt index 9f0f226cf2..487bbf60e0 100644 --- a/mobile/src/main/java/com/github/shadowsocks/preference/BottomSheetPreferenceDialogFragment.kt +++ b/mobile/src/main/java/com/github/shadowsocks/preference/BottomSheetPreferenceDialogFragment.kt @@ -32,6 +32,7 @@ import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView +import androidx.core.os.bundleOf import androidx.core.view.isVisible import androidx.preference.PreferenceDialogFragmentCompat import androidx.recyclerview.widget.DefaultItemAnimator @@ -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 var clickedIndex = -1 diff --git a/mobile/src/main/java/com/github/shadowsocks/preference/BrowsableEditTextPreferenceDialogFragment.kt b/mobile/src/main/java/com/github/shadowsocks/preference/BrowsableEditTextPreferenceDialogFragment.kt new file mode 100644 index 0000000000..83f480803e --- /dev/null +++ b/mobile/src/main/java/com/github/shadowsocks/preference/BrowsableEditTextPreferenceDialogFragment.kt @@ -0,0 +1,50 @@ +/******************************************************************************* + * * + * Copyright (C) 2019 by Max Lv * + * Copyright (C) 2019 by Mygod Studio * + * * + * 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 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() + } + } +} diff --git a/mobile/src/main/java/com/github/shadowsocks/preference/IconListPreference.kt b/mobile/src/main/java/com/github/shadowsocks/preference/IconListPreference.kt index ac9b2c4d1d..76718a3ee2 100644 --- a/mobile/src/main/java/com/github/shadowsocks/preference/IconListPreference.kt +++ b/mobile/src/main/java/com/github/shadowsocks/preference/IconListPreference.kt @@ -26,6 +26,14 @@ import android.util.AttributeSet import androidx.preference.ListPreference class IconListPreference(context: Context, attrs: AttributeSet? = null) : ListPreference(context, attrs) { + companion object FallbackProvider : SummaryProvider { + 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? = null val selectedEntry: Int get() = entryValues.indexOf(value) val entryIcon: Drawable? get() = entryIcons?.getOrNull(selectedEntry) @@ -49,7 +57,6 @@ class IconListPreference(context: Context, attrs: AttributeSet? = null) : ListPr val listener = listener if (listener == null || listener.onPreferenceChange(preference, newValue)) { value = newValue.toString() - checkSummary() if (entryIcons != null) icon = entryIcon true } else false @@ -60,13 +67,9 @@ class IconListPreference(context: Context, attrs: AttributeSet? = null) : ListPr // a.recycle() } - fun checkSummary() { - val unknownValueSummary = unknownValueSummary - if (unknownValueSummary != null) summary = if (selectedEntry < 0) unknownValueSummary.format(value) else "%s" - } - fun init() { icon = entryIcon + summaryProvider = FallbackProvider } override fun onSetInitialValue(defaultValue: Any?) { super.onSetInitialValue(defaultValue) diff --git a/mobile/src/main/java/com/github/shadowsocks/preference/PluginConfigurationDialogFragment.kt b/mobile/src/main/java/com/github/shadowsocks/preference/PluginConfigurationDialogFragment.kt index 253397717f..902fd70202 100644 --- a/mobile/src/main/java/com/github/shadowsocks/preference/PluginConfigurationDialogFragment.kt +++ b/mobile/src/main/java/com/github/shadowsocks/preference/PluginConfigurationDialogFragment.kt @@ -23,17 +23,23 @@ package com.github.shadowsocks.preference import android.view.View import android.widget.EditText 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.plugin.PluginContract import com.github.shadowsocks.plugin.PluginManager -import com.takisoft.preferencex.EditTextPreferenceDialogFragmentCompat class PluginConfigurationDialogFragment : EditTextPreferenceDialogFragmentCompat() { companion object { - const val PLUGIN_ID_FRAGMENT_TAG = + private const val PLUGIN_ID_FRAGMENT_TAG = "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 override fun onPrepareDialogBuilder(builder: AlertDialog.Builder) { diff --git a/mobile/src/main/res/layout/preference_dialog_password.xml b/mobile/src/main/res/layout/preference_dialog_password.xml new file mode 100644 index 0000000000..1dfe01de86 --- /dev/null +++ b/mobile/src/main/res/layout/preference_dialog_password.xml @@ -0,0 +1,44 @@ + + + + + + + + + + diff --git a/mobile/src/main/res/xml/pref_global.xml b/mobile/src/main/res/xml/pref_global.xml index de0f09002a..9a7b15fc96 100644 --- a/mobile/src/main/res/xml/pref_global.xml +++ b/mobile/src/main/res/xml/pref_global.xml @@ -1,51 +1,43 @@ - - + + app:key="isAutoConnect" + app:persistent="false" + app:summary="@string/auto_connect_summary" + app:title="@string/auto_connect"/> + app:key="directBootAware" + app:icon="@drawable/ic_action_lock" + app:summary="@string/direct_boot_aware_summary" + app:title="@string/direct_boot_aware"/> + + - - - - + + + + diff --git a/mobile/src/main/res/xml/pref_profile.xml b/mobile/src/main/res/xml/pref_profile.xml index 8e8ea5e030..bb503938ca 100644 --- a/mobile/src/main/res/xml/pref_profile.xml +++ b/mobile/src/main/res/xml/pref_profile.xml @@ -2,99 +2,93 @@ - + + app:title="@string/proxy_cat"> - - - - + + + + + app:title="@string/feature_cat"> - + + app:key="isIpv6" + app:icon="@drawable/ic_image_looks_6" + app:summary="@string/ipv6_summary" + app:title="@string/ipv6"/> + app:key="isProxyApps" + app:icon="@drawable/ic_navigation_apps" + app:summary="@string/proxied_apps_summary" + app:title="@string/proxied_apps"/> - + app:key="metered" + app:icon="@drawable/ic_device_data_usage" + app:summary="@string/metered_summary" + app:title="@string/metered"/> + + app:key="isUdpDns" + app:icon="@drawable/ic_action_dns" + app:summary="@string/udp_dns_summary" + app:title="@string/udp_dns"/> + app:title="@string/plugin"> - + app:key="plugin" + app:persistent="false" + app:title="@string/plugin" + app:useSimpleSummaryProvider="true"/> + + app:key="udpFallback" + app:title="@string/udp_fallback" + app:summary="@string/plugin_disabled"> diff --git a/plugin/src/main/res/values/strings.xml b/plugin/src/main/res/values/strings.xml index 84ed381838..326355a659 100644 --- a/plugin/src/main/res/values/strings.xml +++ b/plugin/src/main/res/values/strings.xml @@ -6,4 +6,6 @@ Yes No Apply + Browse… + Please install a file manager like MiXplorer diff --git a/tv/build.gradle b/tv/build.gradle index 12f1026753..36b8b3382a 100644 --- a/tv/build.gradle +++ b/tv/build.gradle @@ -52,7 +52,7 @@ android { dependencies { implementation project(':core') 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' diff --git a/tv/src/main/java/com/github/shadowsocks/tv/MainPreferenceFragment.kt b/tv/src/main/java/com/github/shadowsocks/tv/MainPreferenceFragment.kt index c916e06213..27a595394f 100644 --- a/tv/src/main/java/com/github/shadowsocks/tv/MainPreferenceFragment.kt +++ b/tv/src/main/java/com/github/shadowsocks/tv/MainPreferenceFragment.kt @@ -35,10 +35,7 @@ import androidx.leanback.preference.LeanbackPreferenceFragmentCompat import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.get -import androidx.preference.ListPreference -import androidx.preference.Preference -import androidx.preference.PreferenceDataStore -import androidx.preference.SwitchPreference +import androidx.preference.* import com.crashlytics.android.Crashlytics import com.github.shadowsocks.BootReceiver import com.github.shadowsocks.Core @@ -50,6 +47,8 @@ import com.github.shadowsocks.database.ProfileManager import com.github.shadowsocks.net.HttpsTest import com.github.shadowsocks.net.TcpFastOpen 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.utils.Key import com.github.shadowsocks.utils.datas @@ -62,18 +61,20 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo private const val REQUEST_CONNECT = 1 private const val REQUEST_REPLACE_PROFILES = 2 private const val REQUEST_EXPORT_PROFILES = 3 + private const val REQUEST_HOSTS = 4 private const val TAG = "MainPreferenceFragment" } private lateinit var fab: ListPreference private lateinit var stats: Preference private lateinit var controlImport: Preference + private lateinit var hosts: EditTextPreference private lateinit var serviceMode: Preference private lateinit var tfo: SwitchPreference private lateinit var shareOverLan: Preference - private lateinit var portProxy: Preference - private lateinit var portLocalDns: Preference - private lateinit var portTransproxy: Preference + private lateinit var portProxy: EditTextPreference + private lateinit var portLocalDns: EditTextPreference + private lateinit var portTransproxy: EditTextPreference private val onServiceModeChange = Preference.OnPreferenceChangeListener { _, newValue -> val (enabledLocalDns, enabledTransproxy) = when (newValue as String?) { Key.modeProxy -> Pair(false, false) @@ -81,6 +82,7 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo Key.modeTransproxy -> Pair(true, true) else -> throw IllegalArgumentException("newValue: $newValue") } + hosts.isEnabled = enabledLocalDns portLocalDns.isEnabled = enabledLocalDns portTransproxy.isEnabled = enabledTransproxy true @@ -121,19 +123,13 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo }) if (msg != null) Toast.makeText(context, getString(R.string.vpn_error, msg), Toast.LENGTH_SHORT).show() this.state = state - if (state == BaseService.State.Stopped) { - controlImport.isEnabled = true - tfo.isEnabled = true - serviceMode.isEnabled = true - shareOverLan.isEnabled = true - portProxy.isEnabled = true - onServiceModeChange.onPreferenceChange(null, DataStore.serviceMode) - } else { - controlImport.isEnabled = false - tfo.isEnabled = false - serviceMode.isEnabled = false - shareOverLan.isEnabled = false - portProxy.isEnabled = false + val stopped = state == BaseService.State.Stopped + controlImport.isEnabled = stopped + tfo.isEnabled = stopped + serviceMode.isEnabled = stopped + shareOverLan.isEnabled = stopped + portProxy.isEnabled = stopped + if (stopped) onServiceModeChange.onPreferenceChange(null, DataStore.serviceMode) else { portLocalDns.isEnabled = false portTransproxy.isEnabled = false } @@ -156,12 +152,12 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo preferenceManager.preferenceDataStore = DataStore.publicStore DataStore.initGlobal() addPreferencesFromResource(R.xml.pref_main) - fab = findPreference(Key.id) as ListPreference + fab = findPreference(Key.id)!! populateProfiles() - stats = findPreference(Key.controlStats) - controlImport = findPreference(Key.controlImport) + stats = findPreference(Key.controlStats)!! + controlImport = findPreference(Key.controlImport)!! - (findPreference(Key.isAutoConnect) as SwitchPreference).apply { + findPreference(Key.isAutoConnect)!!.apply { setOnPreferenceChangeListener { _, value -> BootReceiver.enabled = value as Boolean true @@ -169,7 +165,7 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo isChecked = BootReceiver.enabled } - tfo = findPreference(Key.tfo) as SwitchPreference + tfo = findPreference(Key.tfo)!! tfo.isChecked = DataStore.tcpFastOpen tfo.setOnPreferenceChangeListener { _, value -> if (value as Boolean && !TcpFastOpen.sendEnabled) { @@ -186,19 +182,18 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo tfo.summary = getString(R.string.tcp_fastopen_summary_unsupported, System.getProperty("os.version")) } - serviceMode = findPreference(Key.serviceMode) - shareOverLan = findPreference(Key.shareOverLan) - portProxy = findPreference(Key.portProxy) - portLocalDns = findPreference(Key.portLocalDns) - portTransproxy = findPreference(Key.portTransproxy) + hosts = findPreference(Key.hosts)!! + hosts.summaryProvider = HostsSummaryProvider + serviceMode = findPreference(Key.serviceMode)!! + shareOverLan = findPreference(Key.shareOverLan)!! + 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 - findPreference(Key.about).apply { - summary = getString(R.string.about_title, BuildConfig.VERSION_NAME) - setOnPreferenceClickListener { - Toast.makeText(requireContext(), "https://shadowsocks.org/android", Toast.LENGTH_SHORT).show() - true - } - } + findPreference(Key.about)!!.summary = getString(R.string.about_title, BuildConfig.VERSION_NAME) tester = ViewModelProviders.of(this).get() changeState(BaseService.State.Idle) // reset everything to init state @@ -236,7 +231,7 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo } } - override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String?) { + override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String) { when (key) { Key.serviceMode -> handler.post { connection.disconnect(requireContext()) @@ -274,15 +269,25 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo }, REQUEST_EXPORT_PROFILES) true } + Key.about -> { + Toast.makeText(requireContext(), "https://shadowsocks.org/android", Toast.LENGTH_SHORT).show() + true + } 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 { startActivityForResult(intent.addCategory(Intent.CATEGORY_OPENABLE), requestCode) - return + return false } catch (_: ActivityNotFoundException) { } catch (_: SecurityException) { } Toast.makeText(requireContext(), R.string.file_manager_missing, Toast.LENGTH_SHORT).show() + return true } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { @@ -317,6 +322,16 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo 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) } } diff --git a/tv/src/main/res/xml/pref_main.xml b/tv/src/main/res/xml/pref_main.xml index 4f927be954..036e3f0eb8 100644 --- a/tv/src/main/res/xml/pref_main.xml +++ b/tv/src/main/res/xml/pref_main.xml @@ -1,73 +1,65 @@ - + + app:key="profileId" + app:title="@string/connect" + app:persistent="false" + app:useSimpleSummaryProvider="true"/> + app:key="control.stats" + app:title="@string/connection_test_pending" + app:summary="@string/stat_summary"/> + app:key="control.import" + app:title="@string/action_replace_file"/> + app:key="control.export" + app:title="@string/action_export_file"/> + app:key="settings" + app:title="@string/settings" + app:initialExpandedChildrenCount="3"> + app:key="isAutoConnect" + app:persistent="false" + app:summary="@string/auto_connect_summary" + app:title="@string/auto_connect"/> + app:key="tcp_fastopen" + app:summary="@string/tcp_fastopen_summary" + app:title="TCP Fast Open"/> + + app:key="serviceMode" + app:entries="@array/service_modes" + app:entryValues="@array/service_mode_values" + app:defaultValue="vpn" + app:title="@string/service_mode" + app:useSimpleSummaryProvider="true"/> - - - + app:key="shareOverLan" + app:title="@string/share_over_lan"/> + + + + app:key="about" + app:title="@string/about"/>