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 386c94d42b..73aa5abef6 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 2433f2a853..9f760eccd3 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() private val job = SupervisorJob() @@ -102,10 +115,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/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/PortPreferenceListener.kt b/core/src/main/java/com/github/shadowsocks/preference/HostsSummaryProvider.kt similarity index 78% rename from core/src/main/java/com/github/shadowsocks/preference/PortPreferenceListener.kt rename to core/src/main/java/com/github/shadowsocks/preference/HostsSummaryProvider.kt index 8e2a41749e..a3afa4bef7 100644 --- a/core/src/main/java/com/github/shadowsocks/preference/PortPreferenceListener.kt +++ b/core/src/main/java/com/github/shadowsocks/preference/HostsSummaryProvider.kt @@ -20,17 +20,14 @@ package com.github.shadowsocks.preference -import android.text.InputFilter -import android.view.inputmethod.EditorInfo -import android.widget.EditText import androidx.preference.EditTextPreference +import androidx.preference.Preference +import com.github.shadowsocks.core.R +import com.github.shadowsocks.net.HostsFile -object PortPreferenceListener : 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() +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/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/mobile/src/main/java/com/github/shadowsocks/GlobalSettingsPreferenceFragment.kt b/mobile/src/main/java/com/github/shadowsocks/GlobalSettingsPreferenceFragment.kt index f9340420e1..ae37045838 100644 --- a/mobile/src/main/java/com/github/shadowsocks/GlobalSettingsPreferenceFragment.kt +++ b/mobile/src/main/java/com/github/shadowsocks/GlobalSettingsPreferenceFragment.kt @@ -20,6 +20,8 @@ package com.github.shadowsocks +import android.app.Activity +import android.content.Intent import android.os.Build import android.os.Bundle import androidx.preference.EditTextPreference @@ -31,10 +33,19 @@ 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.PortPreferenceListener +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 class GlobalSettingsPreferenceFragment : PreferenceFragmentCompat() { + 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() @@ -70,13 +81,15 @@ class GlobalSettingsPreferenceFragment : PreferenceFragmentCompat() { tfo.summary = getString(R.string.tcp_fastopen_summary_unsupported, System.getProperty("os.version")) } + hosts.onBindEditTextListener = EditTextPreferenceModifiers.Monospace + hosts.summaryProvider = HostsSummaryProvider val serviceMode = findPreference(Key.serviceMode)!! val portProxy = findPreference(Key.portProxy)!! - portProxy.onBindEditTextListener = PortPreferenceListener + portProxy.onBindEditTextListener = EditTextPreferenceModifiers.Port val portLocalDns = findPreference(Key.portLocalDns)!! - portLocalDns.onBindEditTextListener = PortPreferenceListener + portLocalDns.onBindEditTextListener = EditTextPreferenceModifiers.Port val portTransproxy = findPreference(Key.portTransproxy)!! - portTransproxy.onBindEditTextListener = PortPreferenceListener + portTransproxy.onBindEditTextListener = EditTextPreferenceModifiers.Port val onServiceModeChange = Preference.OnPreferenceChangeListener { _, newValue -> val (enabledLocalDns, enabledTransproxy) = when (newValue as String?) { Key.modeProxy -> Pair(false, false) @@ -84,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 } @@ -107,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/ProfileConfigFragment.kt b/mobile/src/main/java/com/github/shadowsocks/ProfileConfigFragment.kt index 6933fc8c72..eef6b89a0e 100644 --- a/mobile/src/main/java/com/github/shadowsocks/ProfileConfigFragment.kt +++ b/mobile/src/main/java/com/github/shadowsocks/ProfileConfigFragment.kt @@ -74,7 +74,7 @@ class ProfileConfigFragment : PreferenceFragmentCompat(), val activity = requireActivity() profileId = activity.intent.getLongExtra(Action.EXTRA_PROFILE_ID, -1L) addPreferencesFromResource(R.xml.pref_profile) - findPreference(Key.remotePort)!!.onBindEditTextListener = PortPreferenceListener + findPreference(Key.remotePort)!!.onBindEditTextListener = EditTextPreferenceModifiers.Port findPreference(Key.password)!!.summaryProvider = PasswordSummaryProvider val serviceMode = DataStore.serviceMode findPreference(Key.remoteDns)!!.isEnabled = serviceMode != Key.modeProxy @@ -104,6 +104,7 @@ class ProfileConfigFragment : PreferenceFragmentCompat(), } true } + pluginConfigure.onBindEditTextListener = EditTextPreferenceModifiers.Monospace pluginConfigure.onPreferenceChangeListener = this initPlugins() receiver = Core.listenForPackageChanges(false) { initPlugins() } 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/res/xml/pref_global.xml b/mobile/src/main/res/xml/pref_global.xml index 953aed77b9..9a7b15fc96 100644 --- a/mobile/src/main/res/xml/pref_global.xml +++ b/mobile/src/main/res/xml/pref_global.xml @@ -1,6 +1,6 @@ + app:initialExpandedChildrenCount="4"> + Yes No Apply + Browse… + Please install a file manager like MiXplorer 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 83381d2e9f..f5a5949d88 100644 --- a/tv/src/main/java/com/github/shadowsocks/tv/MainPreferenceFragment.kt +++ b/tv/src/main/java/com/github/shadowsocks/tv/MainPreferenceFragment.kt @@ -47,8 +47,9 @@ 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.preference.PortPreferenceListener import com.github.shadowsocks.utils.Key import com.github.shadowsocks.utils.datas import com.github.shadowsocks.utils.printLog @@ -60,12 +61,14 @@ 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 @@ -79,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 @@ -118,19 +122,13 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo }) if (msg != null) Toast.makeText(requireContext(), 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 } @@ -183,22 +181,18 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo tfo.summary = getString(R.string.tcp_fastopen_summary_unsupported, System.getProperty("os.version")) } + hosts = findPreference(Key.hosts)!! + hosts.summaryProvider = HostsSummaryProvider serviceMode = findPreference(Key.serviceMode)!! shareOverLan = findPreference(Key.shareOverLan)!! portProxy = findPreference(Key.portProxy)!! - portProxy.onBindEditTextListener = PortPreferenceListener + portProxy.onBindEditTextListener = EditTextPreferenceModifiers.Port portLocalDns = findPreference(Key.portLocalDns)!! - portLocalDns.onBindEditTextListener = PortPreferenceListener + portLocalDns.onBindEditTextListener = EditTextPreferenceModifiers.Port portTransproxy = findPreference(Key.portTransproxy)!! - portTransproxy.onBindEditTextListener = PortPreferenceListener + 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 @@ -274,15 +268,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 +321,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 cc79b34386..e32ff76e5a 100644 --- a/tv/src/main/res/xml/pref_main.xml +++ b/tv/src/main/res/xml/pref_main.xml @@ -19,7 +19,7 @@ app:title="@string/action_export_file"/> + app:initialExpandedChildrenCount="3"> +