From b89dccd765dcbcffa02ed60c97f89b49e4769d15 Mon Sep 17 00:00:00 2001 From: ayanamist Date: Fri, 15 Mar 2019 18:10:13 +0800 Subject: [PATCH 1/5] support hosts --- .../github/shadowsocks/net/LocalDnsServer.kt | 70 ++++++++++++++++--- 1 file changed, 61 insertions(+), 9 deletions(-) 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..b65fbf8fa2 100644 --- a/core/src/main/java/com/github/shadowsocks/net/LocalDnsServer.kt +++ b/core/src/main/java/com/github/shadowsocks/net/LocalDnsServer.kt @@ -22,10 +22,13 @@ package com.github.shadowsocks.net import android.util.Log import com.crashlytics.android.Crashlytics +import com.github.shadowsocks.Core +import com.github.shadowsocks.utils.parseNumericAddress import com.github.shadowsocks.utils.printLog import kotlinx.coroutines.* import org.xbill.DNS.* import java.io.EOFException +import java.io.File import java.io.IOException import java.net.* import java.nio.ByteBuffer @@ -55,6 +58,8 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array = emptyList() + private val hostsMap: Map> = readHosts() + companion object { private const val TAG = "LocalDnsServer" private const val TIMEOUT = 10_000L @@ -65,12 +70,25 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array): ByteBuffer = + 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 +120,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()) + prepareDnsResponseWithResults(request, localResults) } else remote.await() } catch (e: Exception) { remote.cancel() @@ -142,6 +159,41 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array = + this.hostsMap[h]?.toTypedArray() ?: emptyArray() + + private fun readHosts(): Map> { + val hostsMap: MutableMap> = HashMap() + try { + val hostsFile = File(Core.deviceStorage.getExternalFilesDir(null), "hosts") + hostsFile.createNewFile() + hostsFile.forEachLine { + val line = it.substringBefore('#') + if (line.isEmpty()) { + return@forEachLine + } + + val splitted = line.trim().split(hostsDelimiter) + if (splitted.size < 2) { + return@forEachLine + } + val addr = splitted[0].parseNumericAddress() ?: return@forEachLine + for (d in splitted.asSequence().drop(1)) { + var el = hostsMap[d] + if (el == null) { + el = HashSet(1) + hostsMap[d] = el + } + el.add(addr) + } + } + return hostsMap.mapValues { it.value.toList() } + } catch (e: IOException) { + printLog(e) + } + return emptyMap() + } + private suspend fun forward(packet: ByteBuffer): ByteBuffer { packet.position(0) // the packet might have been parsed, reset to beginning return if (tcp) SocketChannel.open().use { channel -> From a85c5c200b4b5a203a2d94f276706e3f1a82f525 Mon Sep 17 00:00:00 2001 From: Mygod Date: Sat, 16 Mar 2019 00:47:27 +0800 Subject: [PATCH 2/5] Add hosts to global settings --- .../github/shadowsocks/bg/LocalDnsService.kt | 5 +- .../com/github/shadowsocks/net/HostsFile.kt | 39 ++++++++++++++ .../github/shadowsocks/net/LocalDnsServer.kt | 54 +++---------------- .../preference/HostsSummaryProvider.kt | 33 ++++++++++++ .../com/github/shadowsocks/utils/Constants.kt | 1 + .../com/github/shadowsocks/utils/Utils.kt | 5 +- core/src/main/res/values/strings.xml | 5 +- .../GlobalSettingsPreferenceFragment.kt | 50 +++++++++++++---- ...owsableEditTextPreferenceDialogFragment.kt | 52 ++++++++++++++++++ mobile/src/main/res/xml/pref_global.xml | 5 +- plugin/src/main/res/values/strings.xml | 2 + 11 files changed, 191 insertions(+), 60 deletions(-) create mode 100644 core/src/main/java/com/github/shadowsocks/net/HostsFile.kt create mode 100644 core/src/main/java/com/github/shadowsocks/preference/HostsSummaryProvider.kt create mode 100644 mobile/src/main/java/com/github/shadowsocks/preference/BrowsableEditTextPreferenceDialogFragment.kt 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 b65fbf8fa2..9f760eccd3 100644 --- a/core/src/main/java/com/github/shadowsocks/net/LocalDnsServer.kt +++ b/core/src/main/java/com/github/shadowsocks/net/LocalDnsServer.kt @@ -22,13 +22,10 @@ package com.github.shadowsocks.net import android.util.Log import com.crashlytics.android.Crashlytics -import com.github.shadowsocks.Core -import com.github.shadowsocks.utils.parseNumericAddress import com.github.shadowsocks.utils.printLog import kotlinx.coroutines.* import org.xbill.DNS.* import java.io.EOFException -import java.io.File import java.io.IOException import java.net.* import java.nio.ByteBuffer @@ -46,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. */ @@ -58,8 +57,6 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array = emptyList() - private val hostsMap: Map> = readHosts() - companion object { private const val TAG = "LocalDnsServer" private const val TIMEOUT = 10_000L @@ -70,15 +67,13 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array): ByteBuffer = + private fun cookDnsResponse(request: Message, results: Iterable) = ByteBuffer.wrap(prepareDnsResponse(request).apply { header.setFlag(Flags.RA.toInt()) // recursion available for (address in results) addRecord(when (address) { @@ -124,10 +119,10 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array Array localResults.any(subnet::matches) }) { remote.cancel() - prepareDnsResponseWithResults(request, localResults) + cookDnsResponse(request, localResults.asIterable()) } else remote.await() } catch (e: Exception) { remote.cancel() @@ -159,41 +154,6 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array = - this.hostsMap[h]?.toTypedArray() ?: emptyArray() - - private fun readHosts(): Map> { - val hostsMap: MutableMap> = HashMap() - try { - val hostsFile = File(Core.deviceStorage.getExternalFilesDir(null), "hosts") - hostsFile.createNewFile() - hostsFile.forEachLine { - val line = it.substringBefore('#') - if (line.isEmpty()) { - return@forEachLine - } - - val splitted = line.trim().split(hostsDelimiter) - if (splitted.size < 2) { - return@forEachLine - } - val addr = splitted[0].parseNumericAddress() ?: return@forEachLine - for (d in splitted.asSequence().drop(1)) { - var el = hostsMap[d] - if (el == null) { - el = HashSet(1) - hostsMap[d] = el - } - el.add(addr) - } - } - return hostsMap.mapValues { it.value.toList() } - } catch (e: IOException) { - printLog(e) - } - return emptyMap() - } - private suspend fun forward(packet: ByteBuffer): ByteBuffer { packet.position(0) // the packet might have been parsed, reset to beginning return if (tcp) SocketChannel.open().use { channel -> 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/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..eafefb618e 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,18 @@ 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.HostsSummaryProvider import com.github.shadowsocks.preference.PortPreferenceListener 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,6 +80,7 @@ class GlobalSettingsPreferenceFragment : PreferenceFragmentCompat() { tfo.summary = getString(R.string.tcp_fastopen_summary_unsupported, System.getProperty("os.version")) } + hosts.summaryProvider = HostsSummaryProvider val serviceMode = findPreference(Key.serviceMode)!! val portProxy = findPreference(Key.portProxy)!! portProxy.onBindEditTextListener = PortPreferenceListener @@ -84,20 +95,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 +116,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.localizedMessage).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/preference/BrowsableEditTextPreferenceDialogFragment.kt b/mobile/src/main/java/com/github/shadowsocks/preference/BrowsableEditTextPreferenceDialogFragment.kt new file mode 100644 index 0000000000..6b2c286606 --- /dev/null +++ b/mobile/src/main/java/com/github/shadowsocks/preference/BrowsableEditTextPreferenceDialogFragment.kt @@ -0,0 +1,52 @@ +/******************************************************************************* + * * + * 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 android.view.View +import androidx.appcompat.app.AlertDialog +import androidx.core.os.bundleOf +import androidx.preference.EditTextPreferenceDialogFragmentCompat +import com.github.shadowsocks.R +import com.google.android.material.snackbar.Snackbar + +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 = requireActivity() + try { + targetFragment!!.startActivityForResult(Intent(Intent.ACTION_GET_CONTENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "*/*" + }, targetRequestCode) + return@setNeutralButton + } catch (_: ActivityNotFoundException) { } catch (_: SecurityException) { } + Snackbar.make(activity.findViewById(R.id.content), + R.string.file_manager_missing, Snackbar.LENGTH_SHORT).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 From 420397fd0e6c839bd615c399c4dc3fc44ecf3584 Mon Sep 17 00:00:00 2001 From: Mygod Date: Sat, 16 Mar 2019 01:05:50 +0800 Subject: [PATCH 3/5] Set hosts to be monospace --- ...ener.kt => EditTextPreferenceModifiers.kt} | 21 +++++++++++++------ .../GlobalSettingsPreferenceFragment.kt | 9 ++++---- .../shadowsocks/ProfileConfigFragment.kt | 3 ++- .../shadowsocks/tv/MainPreferenceFragment.kt | 8 +++---- 4 files changed, 26 insertions(+), 15 deletions(-) rename core/src/main/java/com/github/shadowsocks/preference/{PortPreferenceListener.kt => EditTextPreferenceModifiers.kt} (73%) diff --git a/core/src/main/java/com/github/shadowsocks/preference/PortPreferenceListener.kt b/core/src/main/java/com/github/shadowsocks/preference/EditTextPreferenceModifiers.kt similarity index 73% rename from core/src/main/java/com/github/shadowsocks/preference/PortPreferenceListener.kt rename to core/src/main/java/com/github/shadowsocks/preference/EditTextPreferenceModifiers.kt index 8e2a41749e..72ba61c6ae 100644 --- a/core/src/main/java/com/github/shadowsocks/preference/PortPreferenceListener.kt +++ b/core/src/main/java/com/github/shadowsocks/preference/EditTextPreferenceModifiers.kt @@ -20,17 +20,26 @@ 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 PortPreferenceListener : EditTextPreference.OnBindEditTextListener { - private val portLengthFilter = arrayOf(InputFilter.LengthFilter(5)) +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() + override fun onBindEditText(editText: EditText) { + editText.inputType = EditorInfo.TYPE_CLASS_NUMBER + editText.filters = portLengthFilter + editText.setSingleLine() + } } } diff --git a/mobile/src/main/java/com/github/shadowsocks/GlobalSettingsPreferenceFragment.kt b/mobile/src/main/java/com/github/shadowsocks/GlobalSettingsPreferenceFragment.kt index eafefb618e..340123f61a 100644 --- a/mobile/src/main/java/com/github/shadowsocks/GlobalSettingsPreferenceFragment.kt +++ b/mobile/src/main/java/com/github/shadowsocks/GlobalSettingsPreferenceFragment.kt @@ -34,8 +34,8 @@ 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.preference.PortPreferenceListener import com.github.shadowsocks.utils.remove class GlobalSettingsPreferenceFragment : PreferenceFragmentCompat() { @@ -80,14 +80,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) 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/tv/src/main/java/com/github/shadowsocks/tv/MainPreferenceFragment.kt b/tv/src/main/java/com/github/shadowsocks/tv/MainPreferenceFragment.kt index 83381d2e9f..b9310869f2 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,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.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 @@ -186,11 +186,11 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo 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) From 83704994ca0efe311cff782ef4a6b4e5405311f5 Mon Sep 17 00:00:00 2001 From: Mygod Date: Sat, 16 Mar 2019 13:15:16 +0800 Subject: [PATCH 4/5] Add hosts to TV --- .../GlobalSettingsPreferenceFragment.kt | 3 +- ...owsableEditTextPreferenceDialogFragment.kt | 6 +- .../shadowsocks/tv/MainPreferenceFragment.kt | 58 ++++++++++++------- tv/src/main/res/xml/pref_main.xml | 5 +- 4 files changed, 45 insertions(+), 27 deletions(-) diff --git a/mobile/src/main/java/com/github/shadowsocks/GlobalSettingsPreferenceFragment.kt b/mobile/src/main/java/com/github/shadowsocks/GlobalSettingsPreferenceFragment.kt index 340123f61a..ae37045838 100644 --- a/mobile/src/main/java/com/github/shadowsocks/GlobalSettingsPreferenceFragment.kt +++ b/mobile/src/main/java/com/github/shadowsocks/GlobalSettingsPreferenceFragment.kt @@ -36,6 +36,7 @@ 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 class GlobalSettingsPreferenceFragment : PreferenceFragmentCompat() { @@ -133,7 +134,7 @@ class GlobalSettingsPreferenceFragment : PreferenceFragmentCompat() { // 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.localizedMessage).show() + activity.snackbar(e.readableMessage).show() } } else -> super.onActivityResult(requestCode, resultCode, data) diff --git a/mobile/src/main/java/com/github/shadowsocks/preference/BrowsableEditTextPreferenceDialogFragment.kt b/mobile/src/main/java/com/github/shadowsocks/preference/BrowsableEditTextPreferenceDialogFragment.kt index 6b2c286606..12af378f6b 100644 --- a/mobile/src/main/java/com/github/shadowsocks/preference/BrowsableEditTextPreferenceDialogFragment.kt +++ b/mobile/src/main/java/com/github/shadowsocks/preference/BrowsableEditTextPreferenceDialogFragment.kt @@ -26,6 +26,7 @@ import android.view.View import androidx.appcompat.app.AlertDialog import androidx.core.os.bundleOf import androidx.preference.EditTextPreferenceDialogFragmentCompat +import com.github.shadowsocks.MainActivity import com.github.shadowsocks.R import com.google.android.material.snackbar.Snackbar @@ -37,7 +38,7 @@ class BrowsableEditTextPreferenceDialogFragment : EditTextPreferenceDialogFragme override fun onPrepareDialogBuilder(builder: AlertDialog.Builder) { super.onPrepareDialogBuilder(builder) builder.setNeutralButton(R.string.browse) { _, _ -> - val activity = requireActivity() + val activity = activity as MainActivity try { targetFragment!!.startActivityForResult(Intent(Intent.ACTION_GET_CONTENT).apply { addCategory(Intent.CATEGORY_OPENABLE) @@ -45,8 +46,7 @@ class BrowsableEditTextPreferenceDialogFragment : EditTextPreferenceDialogFragme }, targetRequestCode) return@setNeutralButton } catch (_: ActivityNotFoundException) { } catch (_: SecurityException) { } - Snackbar.make(activity.findViewById(R.id.content), - R.string.file_manager_missing, Snackbar.LENGTH_SHORT).show() + activity.snackbar(activity.getString(R.string.file_manager_missing)).show() } } } 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 b9310869f2..f5a5949d88 100644 --- a/tv/src/main/java/com/github/shadowsocks/tv/MainPreferenceFragment.kt +++ b/tv/src/main/java/com/github/shadowsocks/tv/MainPreferenceFragment.kt @@ -48,6 +48,7 @@ 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 @@ -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,6 +181,8 @@ 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)!! @@ -192,13 +192,7 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo 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 @@ -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"> + Date: Mon, 18 Mar 2019 17:21:19 +0800 Subject: [PATCH 5/5] Fix unused imports --- .../preference/BrowsableEditTextPreferenceDialogFragment.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/mobile/src/main/java/com/github/shadowsocks/preference/BrowsableEditTextPreferenceDialogFragment.kt b/mobile/src/main/java/com/github/shadowsocks/preference/BrowsableEditTextPreferenceDialogFragment.kt index 12af378f6b..83f480803e 100644 --- a/mobile/src/main/java/com/github/shadowsocks/preference/BrowsableEditTextPreferenceDialogFragment.kt +++ b/mobile/src/main/java/com/github/shadowsocks/preference/BrowsableEditTextPreferenceDialogFragment.kt @@ -22,13 +22,11 @@ package com.github.shadowsocks.preference import android.content.ActivityNotFoundException import android.content.Intent -import android.view.View import androidx.appcompat.app.AlertDialog import androidx.core.os.bundleOf import androidx.preference.EditTextPreferenceDialogFragmentCompat import com.github.shadowsocks.MainActivity import com.github.shadowsocks.R -import com.google.android.material.snackbar.Snackbar class BrowsableEditTextPreferenceDialogFragment : EditTextPreferenceDialogFragmentCompat() { fun setKey(key: String) {