Commit f6aa874c authored by Mygod's avatar Mygod

Merge branch 'master' into autoconnect+

parents 16e0e3fd 0bd0207b
# Submitting Issues # Submitting Issues
* Questions and general discussions should go to [Discourse forum](https://discourse.shadowsocks.org/c/implementations/shadowsocks-android); * Issues can be submitted in two different places:
* Search for your issue here and at the forum and check [FAQ](https://github.com/shadowsocks/shadowsocks-android/blob/master/.github/faq.md) before submitting new issues; 1) [The GitHub issue log](https://github.com/shadowsocks/shadowsocks-android/issues)
* Always provide information asked in the template unless you know what you're doing; 2) [The Shadowsocks.org Discourse Forum](https://discourse.shadowsocks.org/c/implementations/shadowsocks-android)
* Logcat would also be helpful. * Please search for your issues in both places before submitting new issues.
* If you have any questions, please check the [FAQ](https://github.com/shadowsocks/shadowsocks-android/blob/master/.github/faq.md) and the questions submitted at the [Discourse Forum](https://discourse.shadowsocks.org/c/implementations/shadowsocks-android).
* General discussions should go to the [Discourse Forum](https://discourse.shadowsocks.org/c/implementations/shadowsocks-android).
* Always provide information asked in the template unless you know what you're doing.
* Utilizing Logcat would also be helpful.
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
[![Language: Kotlin](https://img.shields.io/github/languages/top/shadowsocks/shadowsocks-android.svg)](https://github.com/shadowsocks/shadowsocks-android/search?l=kotlin) [![Language: Kotlin](https://img.shields.io/github/languages/top/shadowsocks/shadowsocks-android.svg)](https://github.com/shadowsocks/shadowsocks-android/search?l=kotlin)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/1a21d48d466644cdbcb57a1889abea5b)](https://www.codacy.com/app/shadowsocks/shadowsocks-android?utm_source=github.com&utm_medium=referral&utm_content=shadowsocks/shadowsocks-android&utm_campaign=Badge_Grade) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/1a21d48d466644cdbcb57a1889abea5b)](https://www.codacy.com/app/shadowsocks/shadowsocks-android?utm_source=github.com&utm_medium=referral&utm_content=shadowsocks/shadowsocks-android&utm_campaign=Badge_Grade)
[![License: GPL-3.0](https://img.shields.io/badge/license-GPL--3.0-orange.svg)](https://www.gnu.org/licenses/gpl-3.0) [![License: GPL-3.0](https://img.shields.io/badge/license-GPL--3.0-orange.svg)](https://www.gnu.org/licenses/gpl-3.0)
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fshadowsocks%2Fshadowsocks-android.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fshadowsocks%2Fshadowsocks-android?ref=badge_shield)
<a href="https://play.google.com/store/apps/details?id=com.github.shadowsocks"><img src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png" height="48"></a> <a href="https://play.google.com/store/apps/details?id=com.github.shadowsocks"><img src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png" height="48"></a>
for Android & Chrome OS ([beta](https://play.google.com/apps/testing/com.github.shadowsocks)) for Android & Chrome OS ([beta](https://play.google.com/apps/testing/com.github.shadowsocks))
...@@ -34,6 +35,11 @@ sudo chown 3434:3434 build ...@@ -34,6 +35,11 @@ sudo chown 3434:3434 build
docker run --rm -v ${PWD}/build:/build circleci/android:api-28-ndk bash -c "cd /build; git clone https://github.com/shadowsocks/shadowsocks-android; cd shadowsocks-android; git submodule update --init --recursive; ./gradlew assembleDebug" docker run --rm -v ${PWD}/build:/build circleci/android:api-28-ndk bash -c "cd /build; git clone https://github.com/shadowsocks/shadowsocks-android; cd shadowsocks-android; git submodule update --init --recursive; ./gradlew assembleDebug"
``` ```
### CONTRIBUTING
If you are interested in contributing or getting involved with this project, please read the CONTRIBUTING page for more information. The page can be found [here](https://github.com/shadowsocks/shadowsocks-android/blob/master/CONTRIBUTING.md).
### [TRANSLATE](https://discourse.shadowsocks.org/t/poeditor-translation-main-thread/30) ### [TRANSLATE](https://discourse.shadowsocks.org/t/poeditor-translation-main-thread/30)
## OPEN SOURCE LICENSES ## OPEN SOURCE LICENSES
...@@ -50,6 +56,9 @@ docker run --rm -v ${PWD}/build:/build circleci/android:api-28-ndk bash -c "cd / ...@@ -50,6 +56,9 @@ docker run --rm -v ${PWD}/build:/build circleci/android:api-28-ndk bash -c "cd /
<li>libsodium: <a href="https://github.com/jedisct1/libsodium/blob/master/LICENSE">ISC</a></li> <li>libsodium: <a href="https://github.com/jedisct1/libsodium/blob/master/LICENSE">ISC</a></li>
</ul> </ul>
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fshadowsocks%2Fshadowsocks-android.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fshadowsocks%2Fshadowsocks-android?ref=badge_large)
### LICENSE ### LICENSE
Copyright (C) 2017 by Max Lv <<max.c.lv@gmail.com>> Copyright (C) 2017 by Max Lv <<max.c.lv@gmail.com>>
...@@ -66,4 +75,4 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ...@@ -66,4 +75,4 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details. GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
\ No newline at end of file
...@@ -4,12 +4,12 @@ apply plugin: 'com.github.ben-manes.versions' ...@@ -4,12 +4,12 @@ apply plugin: 'com.github.ben-manes.versions'
buildscript { buildscript {
ext { ext {
javaVersion = JavaVersion.VERSION_1_8
kotlinVersion = '1.3.31' kotlinVersion = '1.3.31'
minSdkVersion = 21 minSdkVersion = 21
sdkVersion = 28 sdkVersion = 28
compileSdkVersion = 28 compileSdkVersion = 28
buildToolsVersion = '28.0.3' buildToolsVersion = '28.0.3'
preferencexVersion = '1.0.0'
junitVersion = '4.12' junitVersion = '4.12'
androidTestVersion = '1.1.1' androidTestVersion = '1.1.1'
androidEspressoVersion = '3.1.1' androidEspressoVersion = '3.1.1'
...@@ -27,11 +27,11 @@ buildscript { ...@@ -27,11 +27,11 @@ buildscript {
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.4.0' classpath 'com.android.tools.build:gradle:3.4.1'
classpath 'com.github.ben-manes:gradle-versions-plugin:0.21.0' classpath 'com.github.ben-manes:gradle-versions-plugin:0.21.0'
classpath 'com.google.gms:google-services:4.2.0' classpath 'com.google.gms:google-services:4.2.0'
classpath 'com.vanniktech:gradle-maven-publish-plugin:0.8.0' classpath 'com.vanniktech:gradle-maven-publish-plugin:0.8.0'
classpath 'io.fabric.tools:gradle:1.28.1' classpath 'io.fabric.tools:gradle:1.29.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
} }
} }
......
...@@ -28,6 +28,11 @@ android { ...@@ -28,6 +28,11 @@ android {
} }
} }
compileOptions {
sourceCompatibility javaVersion
targetCompatibility javaVersion
}
externalNativeBuild { externalNativeBuild {
ndkBuild { ndkBuild {
path 'src/main/jni/Android.mk' path 'src/main/jni/Android.mk'
...@@ -47,19 +52,18 @@ def lifecycleVersion = '2.0.0' ...@@ -47,19 +52,18 @@ def lifecycleVersion = '2.0.0'
def roomVersion = '2.0.0' def roomVersion = '2.0.0'
dependencies { dependencies {
api project(':plugin') api project(':plugin')
api "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
api "androidx.lifecycle:lifecycle-extensions:$lifecycleVersion" api "androidx.lifecycle:lifecycle-extensions:$lifecycleVersion"
api "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion" api "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
api 'androidx.preference:preference:1.0.0' api 'androidx.preference:preference:1.1.0-alpha05'
api "androidx.room:room-runtime:$roomVersion" api "androidx.room:room-runtime:$roomVersion"
api 'androidx.work:work-runtime-ktx:2.0.1' api 'androidx.work:work-runtime-ktx:2.0.1'
api 'com.crashlytics.sdk.android:crashlytics:2.9.9' api 'com.crashlytics.sdk.android:crashlytics:2.10.0'
api 'com.google.firebase:firebase-config:16.5.0' api 'com.google.firebase:firebase-config:17.0.0'
api 'com.google.firebase:firebase-core:16.0.8' api 'com.google.firebase:firebase-core:16.0.9'
api "com.takisoft.preferencex:preferencex:$preferencexVersion"
api 'dnsjava:dnsjava:2.1.8' api 'dnsjava:dnsjava:2.1.8'
api 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.0' api 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.1'
api 'org.connectbot.jsocks:jsocks:1.0.0' api 'org.connectbot.jsocks:jsocks:1.0.0'
kapt "androidx.lifecycle:lifecycle-compiler:$lifecycleVersion"
kapt "androidx.room:room-compiler:$roomVersion" kapt "androidx.room:room-compiler:$roomVersion"
testImplementation "junit:junit:$junitVersion" testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.room:room-testing:$roomVersion" androidTestImplementation "androidx.room:room-testing:$roomVersion"
......
...@@ -55,12 +55,23 @@ ...@@ -55,12 +55,23 @@
android:exported="false"> android:exported="false">
</service> </service>
<activity
android:name="com.github.shadowsocks.UrlImportActivity"
android:theme="@style/Theme.AppCompat.Translucent"
android:excludeFromRecents="true">
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="ss"/>
</intent-filter>
</activity>
<activity <activity
android:name="com.github.shadowsocks.VpnRequestActivity" android:name="com.github.shadowsocks.VpnRequestActivity"
android:theme="@style/Theme.AppCompat.Translucent" android:theme="@style/Theme.AppCompat.Translucent"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:taskAffinity="" android:taskAffinity=""/>
android:launchMode="singleTask"/>
<receiver android:name="com.github.shadowsocks.BootReceiver" <receiver android:name="com.github.shadowsocks.BootReceiver"
android:process=":bg" android:process=":bg"
......
...@@ -1928,61 +1928,8 @@ ...@@ -1928,61 +1928,8 @@
(^|\.)goodtv\.com\.tw$ (^|\.)goodtv\.com\.tw$
(^|\.)goodtv\.tv$ (^|\.)goodtv\.tv$
(^|\.)goofind\.com$ (^|\.)goofind\.com$
(^|\.)google\.ae$ (^|\.)google(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?){1,2}$
(^|\.)google\.am$ (^|\.)googleapis(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?){1,2}$
(^|\.)google\.as$
(^|\.)google\.at$
(^|\.)google\.az$
(^|\.)google\.ba$
(^|\.)google\.be$
(^|\.)google\.bg$
(^|\.)google\.ca$
(^|\.)google\.calstate\.edu$
(^|\.)google\.cd$
(^|\.)google\.ci$
(^|\.)google\.co\.id$
(^|\.)google\.co\.jp$
(^|\.)google\.co\.kr$
(^|\.)google\.co\.ma$
(^|\.)google\.co\.uk$
(^|\.)google\.com$
(^|\.)google\.de$
(^|\.)google\.dj$
(^|\.)google\.dk$
(^|\.)google\.es$
(^|\.)google\.fi$
(^|\.)google\.fm$
(^|\.)google\.fr$
(^|\.)google\.gg$
(^|\.)google\.gl$
(^|\.)google\.gr$
(^|\.)google\.ie$
(^|\.)google\.is$
(^|\.)google\.it$
(^|\.)google\.jo$
(^|\.)google\.kz$
(^|\.)google\.lv$
(^|\.)google\.mn$
(^|\.)google\.ms$
(^|\.)google\.nl$
(^|\.)google\.no$
(^|\.)google\.nu$
(^|\.)google\.ro$
(^|\.)google\.ru$
(^|\.)google\.rw$
(^|\.)google\.sc$
(^|\.)google\.sh$
(^|\.)google\.sk$
(^|\.)google\.sm$
(^|\.)google\.sn$
(^|\.)google\.tk$
(^|\.)google\.tm$
(^|\.)google\.to$
(^|\.)google\.tt$
(^|\.)google\.vu$
(^|\.)google\.ws$
(^|\.)googleapis\.cn$
(^|\.)googleapis\.com$
(^|\.)googleapps\.com$ (^|\.)googleapps\.com$
(^|\.)googlearth\.com$ (^|\.)googlearth\.com$
(^|\.)googleartproject\.com$ (^|\.)googleartproject\.com$
......
/*******************************************************************************
* *
* Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks
import android.content.DialogInterface
import android.os.Bundle
import android.os.Parcelable
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.github.shadowsocks.core.R
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.plugin.AlertDialogFragment
import com.github.shadowsocks.plugin.Empty
import kotlinx.android.parcel.Parcelize
class UrlImportActivity : AppCompatActivity() {
@Parcelize
data class ProfilesArg(val profiles: List<Profile>) : Parcelable
class ImportProfilesDialogFragment : AlertDialogFragment<ProfilesArg, Empty>() {
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
setTitle(R.string.add_profile_dialog)
setPositiveButton(R.string.yes, listener)
setNegativeButton(R.string.no, listener)
setMessage(arg.profiles.joinToString("\n"))
}
override fun onClick(dialog: DialogInterface?, which: Int) {
if (which == DialogInterface.BUTTON_POSITIVE) arg.profiles.forEach { ProfileManager.createProfile(it) }
requireActivity().finish()
}
override fun onDismiss(dialog: DialogInterface) {
requireActivity().finish()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
when (val dialog = handleShareIntent()) {
null -> {
Toast.makeText(this, R.string.profile_invalid_input, Toast.LENGTH_SHORT).show()
finish()
}
else -> dialog.show(supportFragmentManager, null)
}
}
private fun handleShareIntent() = intent.data?.toString()?.let { sharedStr ->
val profiles = Profile.findAllUrls(sharedStr, Core.currentProfile?.first).toList()
if (profiles.isEmpty()) null else ImportProfilesDialogFragment().withArg(ProfilesArg(profiles))
}
}
...@@ -63,6 +63,7 @@ class VpnRequestActivity : AppCompatActivity() { ...@@ -63,6 +63,7 @@ class VpnRequestActivity : AppCompatActivity() {
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode != REQUEST_CONNECT) return super.onActivityResult(requestCode, resultCode, data)
if (resultCode == RESULT_OK) Core.startService() else { if (resultCode == RESULT_OK) Core.startService() else {
Toast.makeText(this, R.string.vpn_permission_denied, Toast.LENGTH_LONG).show() Toast.makeText(this, R.string.vpn_permission_denied, Toast.LENGTH_LONG).show()
Crashlytics.log(Log.ERROR, TAG, "Failed to start VpnService from onActivityResult: $data") Crashlytics.log(Log.ERROR, TAG, "Failed to start VpnService from onActivityResult: $data")
......
...@@ -23,10 +23,12 @@ package com.github.shadowsocks.bg ...@@ -23,10 +23,12 @@ package com.github.shadowsocks.bg
import com.github.shadowsocks.Core.app import com.github.shadowsocks.Core.app
import com.github.shadowsocks.acl.Acl import com.github.shadowsocks.acl.Acl
import com.github.shadowsocks.core.R import com.github.shadowsocks.core.R
import com.github.shadowsocks.net.HostsFile
import com.github.shadowsocks.net.LocalDnsServer import com.github.shadowsocks.net.LocalDnsServer
import com.github.shadowsocks.net.Socks5Endpoint import com.github.shadowsocks.net.Socks5Endpoint
import com.github.shadowsocks.net.Subnet import com.github.shadowsocks.net.Subnet
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.Key
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.net.URI import java.net.URI
...@@ -49,7 +51,8 @@ object LocalDnsService { ...@@ -49,7 +51,8 @@ object LocalDnsService {
val dns = URI("dns://${profile.remoteDns}") val dns = URI("dns://${profile.remoteDns}")
LocalDnsServer(this::resolver, LocalDnsServer(this::resolver,
Socks5Endpoint(dns.host, if (dns.port < 0) 53 else dns.port), Socks5Endpoint(dns.host, if (dns.port < 0) 53 else dns.port),
DataStore.proxyAddress).apply { DataStore.proxyAddress,
HostsFile(DataStore.publicStore.getString(Key.hosts) ?: "")).apply {
tcp = !profile.udpdns tcp = !profile.udpdns
when (profile.route) { when (profile.route) {
Acl.BYPASS_CHN, Acl.BYPASS_LAN_CHN, Acl.GFWLIST, Acl.CUSTOM_RULES -> { Acl.BYPASS_CHN, Acl.BYPASS_LAN_CHN, Acl.GFWLIST, Acl.CUSTOM_RULES -> {
......
...@@ -31,9 +31,9 @@ import com.github.shadowsocks.database.Profile ...@@ -31,9 +31,9 @@ import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.plugin.PluginConfiguration import com.github.shadowsocks.plugin.PluginConfiguration
import com.github.shadowsocks.plugin.PluginManager import com.github.shadowsocks.plugin.PluginManager
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.disconnectFromMain
import com.github.shadowsocks.utils.parseNumericAddress import com.github.shadowsocks.utils.parseNumericAddress
import com.github.shadowsocks.utils.signaturesCompat import com.github.shadowsocks.utils.signaturesCompat
import com.github.shadowsocks.utils.useCancellable
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.io.File import java.io.File
import java.net.HttpURLConnection import java.net.HttpURLConnection
...@@ -62,15 +62,11 @@ class ProxyInstance(val profile: Profile, private val route: String = profile.ro ...@@ -62,15 +62,11 @@ class ProxyInstance(val profile: Profile, private val route: String = profile.ro
conn.requestMethod = "POST" conn.requestMethod = "POST"
conn.doOutput = true conn.doOutput = true
val proxies = try { val proxies = conn.useCancellable {
withContext(Dispatchers.IO) { outputStream.bufferedWriter().use {
conn.outputStream.bufferedWriter().use { it.write("sig=" + Base64.encodeToString(mdg.digest(), Base64.DEFAULT))
it.write("sig=" + Base64.encodeToString(mdg.digest(), Base64.DEFAULT))
}
conn.inputStream.bufferedReader().readText()
} }
} finally { inputStream.bufferedReader().readText()
conn.disconnectFromMain()
}.split('|').toMutableList() }.split('|').toMutableList()
proxies.shuffle() proxies.shuffle()
val proxy = proxies.first().split(':') val proxy = proxies.first().split(':')
......
...@@ -87,7 +87,7 @@ data class Profile( ...@@ -87,7 +87,7 @@ data class Profile(
if (match != null) { if (match != null) {
val profile = Profile() val profile = Profile()
feature?.copyFeatureSettingsTo(profile) feature?.copyFeatureSettingsTo(profile)
profile.method = match.groupValues[1].toLowerCase() profile.method = match.groupValues[1].toLowerCase(Locale.ENGLISH)
profile.password = match.groupValues[2] profile.password = match.groupValues[2]
profile.host = match.groupValues[3] profile.host = match.groupValues[3]
profile.remotePort = match.groupValues[4].toInt() profile.remotePort = match.groupValues[4].toInt()
......
/*******************************************************************************
* *
* Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.net
import com.github.shadowsocks.utils.computeIfAbsentCompat
import com.github.shadowsocks.utils.parseNumericAddress
import java.net.InetAddress
class HostsFile(input: String = "") {
private val map = mutableMapOf<String, MutableSet<InetAddress>>()
init {
for (line in input.lineSequence()) {
val entries = line.substringBefore('#').splitToSequence(' ', '\t').filter { it.isNotEmpty() }
val address = entries.firstOrNull()?.parseNumericAddress() ?: continue
for (hostname in entries.drop(1)) map.computeIfAbsentCompat(hostname) { LinkedHashSet(1) }.add(address)
}
}
val configuredHostnames get() = map.size
fun resolve(hostname: String) = map[hostname]?.shuffled() ?: emptyList()
}
...@@ -30,8 +30,11 @@ import com.github.shadowsocks.acl.Acl ...@@ -30,8 +30,11 @@ import com.github.shadowsocks.acl.Acl
import com.github.shadowsocks.core.R import com.github.shadowsocks.core.R
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.Key import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.disconnectFromMain import com.github.shadowsocks.utils.useCancellable
import kotlinx.coroutines.* import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import java.io.IOException import java.io.IOException
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.Proxy import java.net.Proxy
...@@ -75,7 +78,7 @@ class HttpsTest : ViewModel() { ...@@ -75,7 +78,7 @@ class HttpsTest : ViewModel() {
} }
} }
private var running: Pair<HttpURLConnection, Job>? = null private var running: Job? = null
val status = MutableLiveData<Status>().apply { value = Status.Idle } val status = MutableLiveData<Status>().apply { value = Status.Idle }
fun testConnection() { fun testConnection() {
...@@ -91,26 +94,25 @@ class HttpsTest : ViewModel() { ...@@ -91,26 +94,25 @@ class HttpsTest : ViewModel() {
conn.setRequestProperty("Connection", "close") conn.setRequestProperty("Connection", "close")
conn.instanceFollowRedirects = false conn.instanceFollowRedirects = false
conn.useCaches = false conn.useCaches = false
running = conn to GlobalScope.launch(Dispatchers.Main.immediate) { running = GlobalScope.launch(Dispatchers.Main.immediate) {
status.value = withContext(Dispatchers.IO) { status.value = conn.useCancellable {
try { try {
val start = SystemClock.elapsedRealtime() val start = SystemClock.elapsedRealtime()
val code = conn.responseCode val code = responseCode
val elapsed = SystemClock.elapsedRealtime() - start val elapsed = SystemClock.elapsedRealtime() - start
if (code == 204 || code == 200 && conn.responseLength == 0L) Status.Success(elapsed) if (code == 204 || code == 200 && responseLength == 0L) Status.Success(elapsed)
else Status.Error.UnexpectedResponseCode(code) else Status.Error.UnexpectedResponseCode(code)
} catch (e: IOException) { } catch (e: IOException) {
Status.Error.IOFailure(e) Status.Error.IOFailure(e)
} finally { } finally {
conn.disconnect() disconnect()
} }
} }
} }
} }
private fun cancelTest() = running?.let { (conn, job) -> private fun cancelTest() {
job.cancel() // ensure job is cancelled before interrupting running?.cancel()
conn.disconnectFromMain()
running = null running = null
} }
......
...@@ -43,7 +43,9 @@ import java.nio.channels.SocketChannel ...@@ -43,7 +43,9 @@ import java.nio.channels.SocketChannel
* https://github.com/shadowsocks/overture/tree/874f22613c334a3b78e40155a55479b7b69fee04 * https://github.com/shadowsocks/overture/tree/874f22613c334a3b78e40155a55479b7b69fee04
*/ */
class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAddress>, class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAddress>,
private val remoteDns: Socks5Endpoint, private val proxy: SocketAddress) : CoroutineScope { private val remoteDns: Socks5Endpoint,
private val proxy: SocketAddress,
private val hosts: HostsFile) : CoroutineScope {
/** /**
* Forward all requests to remote and ignore localResolver. * Forward all requests to remote and ignore localResolver.
*/ */
...@@ -70,7 +72,18 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAd ...@@ -70,7 +72,18 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAd
if (request.header.getFlag(Flags.RD.toInt())) header.setFlag(Flags.RD.toInt()) if (request.header.getFlag(Flags.RD.toInt())) header.setFlag(Flags.RD.toInt())
request.question?.also { addRecord(it, Section.QUESTION) } request.question?.also { addRecord(it, Section.QUESTION) }
} }
private fun cookDnsResponse(request: Message, results: Iterable<InetAddress>) =
ByteBuffer.wrap(prepareDnsResponse(request).apply {
header.setFlag(Flags.RA.toInt()) // recursion available
for (address in results) addRecord(when (address) {
is Inet4Address -> ARecord(question.name, DClass.IN, TTL, address)
is Inet6Address -> AAAARecord(question.name, DClass.IN, TTL, address)
else -> throw IllegalStateException("Unsupported address $address")
}, Section.ANSWER)
}.toWire())
} }
private val monitor = ChannelMonitor() private val monitor = ChannelMonitor()
override val coroutineContext = SupervisorJob() + CoroutineExceptionHandler { _, t -> printLog(t) } override val coroutineContext = SupervisorJob() + CoroutineExceptionHandler { _, t -> printLog(t) }
...@@ -101,10 +114,16 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAd ...@@ -101,10 +114,16 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAd
return supervisorScope { return supervisorScope {
val remote = async { withTimeout(TIMEOUT) { forward(packet) } } val remote = async { withTimeout(TIMEOUT) { forward(packet) } }
try { try {
if (forwardOnly || request.header.opcode != Opcode.QUERY) return@supervisorScope remote.await() if (request.header.opcode != Opcode.QUERY) return@supervisorScope remote.await()
val question = request.question val question = request.question
if (question?.type != Type.A) return@supervisorScope remote.await() if (question?.type != Type.A) return@supervisorScope remote.await()
val host = question.name.toString(true) val host = question.name.toString(true)
val hostsResults = hosts.resolve(host)
if (hostsResults.isNotEmpty()) {
remote.cancel()
return@supervisorScope cookDnsResponse(request, hostsResults)
}
if (forwardOnly) return@supervisorScope remote.await()
if (remoteDomainMatcher?.containsMatchIn(host) == true) return@supervisorScope remote.await() if (remoteDomainMatcher?.containsMatchIn(host) == true) return@supervisorScope remote.await()
val localResults = try { val localResults = try {
withTimeout(TIMEOUT) { GlobalScope.async(Dispatchers.IO) { localResolver(host) }.await() } withTimeout(TIMEOUT) { GlobalScope.async(Dispatchers.IO) { localResolver(host) }.await() }
...@@ -117,14 +136,7 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAd ...@@ -117,14 +136,7 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAd
if (localResults.isEmpty()) return@supervisorScope remote.await() if (localResults.isEmpty()) return@supervisorScope remote.await()
if (localIpMatcher.isEmpty() || localIpMatcher.any { subnet -> localResults.any(subnet::matches) }) { if (localIpMatcher.isEmpty() || localIpMatcher.any { subnet -> localResults.any(subnet::matches) }) {
remote.cancel() remote.cancel()
ByteBuffer.wrap(prepareDnsResponse(request).apply { cookDnsResponse(request, localResults.asIterable())
header.setFlag(Flags.RA.toInt()) // recursion available
for (address in localResults) addRecord(when (address) {
is Inet4Address -> ARecord(question.name, DClass.IN, TTL, address)
is Inet6Address -> AAAARecord(question.name, DClass.IN, TTL, address)
else -> throw IllegalStateException("Unsupported address $address")
}, Section.ANSWER)
}.toWire())
} else remote.await() } else remote.await()
} catch (e: Exception) { } catch (e: Exception) {
remote.cancel() remote.cancel()
......
...@@ -43,7 +43,7 @@ object DataStore : OnPreferenceDataStoreChangeListener { ...@@ -43,7 +43,7 @@ object DataStore : OnPreferenceDataStoreChangeListener {
publicStore.registerChangeListener(this) publicStore.registerChangeListener(this)
} }
override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String?) { override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String) {
when (key) { when (key) {
Key.id -> if (directBootAware) DirectBoot.update() Key.id -> if (directBootAware) DirectBoot.update()
} }
......
/*******************************************************************************
* *
* Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.preference
import android.graphics.Typeface
import android.text.InputFilter
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import androidx.preference.EditTextPreference
object EditTextPreferenceModifiers {
object Monospace : EditTextPreference.OnBindEditTextListener {
override fun onBindEditText(editText: EditText) {
editText.typeface = Typeface.MONOSPACE
}
}
object Port : EditTextPreference.OnBindEditTextListener {
private val portLengthFilter = arrayOf(InputFilter.LengthFilter(5))
override fun onBindEditText(editText: EditText) {
editText.inputType = EditorInfo.TYPE_CLASS_NUMBER
editText.filters = portLengthFilter
editText.setSingleLine()
}
}
}
/*******************************************************************************
* *
* Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.preference
import androidx.preference.EditTextPreference
import androidx.preference.Preference
import com.github.shadowsocks.core.R
import com.github.shadowsocks.net.HostsFile
object HostsSummaryProvider : Preference.SummaryProvider<EditTextPreference> {
override fun provideSummary(preference: EditTextPreference?): CharSequence {
val count = HostsFile(preference!!.text ?: "").configuredHostnames
return preference.context.resources.getQuantityString(R.plurals.hosts_summary, count, count)
}
}
...@@ -23,5 +23,5 @@ package com.github.shadowsocks.preference ...@@ -23,5 +23,5 @@ package com.github.shadowsocks.preference
import androidx.preference.PreferenceDataStore import androidx.preference.PreferenceDataStore
interface OnPreferenceDataStoreChangeListener { interface OnPreferenceDataStoreChangeListener {
fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String?) fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String)
} }
...@@ -65,6 +65,7 @@ object Key { ...@@ -65,6 +65,7 @@ object Key {
const val dirty = "profileDirty" const val dirty = "profileDirty"
const val tfo = "tcp_fastopen" const val tfo = "tcp_fastopen"
const val hosts = "hosts"
const val assetUpdateTime = "assetUpdateTime" const val assetUpdateTime = "assetUpdateTime"
// TV specific values // TV specific values
......
/*******************************************************************************
* *
* Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.utils
import androidx.activity.ComponentActivity
import androidx.annotation.MainThread
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
/**
* See also: https://stackoverflow.com/a/30821062/2245107
*/
object SingleInstanceActivity : DefaultLifecycleObserver {
private val active = mutableSetOf<Class<LifecycleOwner>>()
@MainThread
fun register(activity: ComponentActivity) = if (active.add(activity.javaClass)) apply {
activity.lifecycle.addObserver(this)
} else {
activity.finish()
null
}
override fun onDestroy(owner: LifecycleOwner) {
check(active.remove(owner.javaClass)) { "Double destroy?" }
}
}
...@@ -36,11 +36,10 @@ import android.util.TypedValue ...@@ -36,11 +36,10 @@ import android.util.TypedValue
import androidx.annotation.AttrRes import androidx.annotation.AttrRes
import androidx.preference.Preference import androidx.preference.Preference
import com.crashlytics.android.Crashlytics import com.crashlytics.android.Crashlytics
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.*
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.InetAddress import java.net.InetAddress
import kotlin.coroutines.resume
val Throwable.readableMessage get() = localizedMessage ?: javaClass.name val Throwable.readableMessage get() = localizedMessage ?: javaClass.name
...@@ -54,11 +53,19 @@ private val parseNumericAddress by lazy { ...@@ -54,11 +53,19 @@ private val parseNumericAddress by lazy {
* *
* Bug: https://issuetracker.google.com/issues/123456213 * 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 } ?: Os.inet_pton(OsConstants.AF_INET6, this)?.let { parseNumericAddress.invoke(null, this) as InetAddress }
fun HttpURLConnection.disconnectFromMain() { fun <K, V> MutableMap<K, V>.computeIfAbsentCompat(key: K, value: () -> V) = if (Build.VERSION.SDK_INT >= 24)
if (Build.VERSION.SDK_INT >= 26) disconnect() else GlobalScope.launch(Dispatchers.IO) { disconnect() } computeIfAbsent(key) { value() } else this[key] ?: value().also { put(key, it) }
suspend fun <T> HttpURLConnection.useCancellable(block: HttpURLConnection.() -> T) = withContext(Dispatchers.IO) {
suspendCancellableCoroutine<T> { cont ->
cont.invokeOnCancellation {
if (Build.VERSION.SDK_INT >= 26) disconnect() else launch(Dispatchers.IO) { disconnect() }
}
cont.resume(block())
}
} }
fun parsePort(str: String?, default: Int, min: Int = 1025): Int { fun parsePort(str: String?, default: Int, min: Int = 1025): Int {
......
...@@ -59,6 +59,10 @@ ...@@ -59,6 +59,10 @@
<string name="tcp_fastopen_summary">Toggling might require ROOT permission</string> <string name="tcp_fastopen_summary">Toggling might require ROOT permission</string>
<string name="tcp_fastopen_summary_unsupported">Unsupported kernel version: %s &lt; 3.7.1</string> <string name="tcp_fastopen_summary_unsupported">Unsupported kernel version: %s &lt; 3.7.1</string>
<string name="tcp_fastopen_failure">Toggle failed</string> <string name="tcp_fastopen_failure">Toggle failed</string>
<plurals name="hosts_summary">
<item quantity="one">1 hostname configured</item>
<item quantity="other">%d hostnames configured</item>
</plurals>
<string name="udp_dns">Send DNS over UDP</string> <string name="udp_dns">Send DNS over UDP</string>
<string name="udp_dns_summary">Requires UDP forwarding on server side</string> <string name="udp_dns_summary">Requires UDP forwarding on server side</string>
<string name="udp_fallback">UDP Fallback</string> <string name="udp_fallback">UDP Fallback</string>
...@@ -80,7 +84,6 @@ ...@@ -80,7 +84,6 @@
<!-- alert category --> <!-- alert category -->
<string name="profile_empty">Please select a profile</string> <string name="profile_empty">Please select a profile</string>
<string name="proxy_empty">Proxy/Password should not be empty</string> <string name="proxy_empty">Proxy/Password should not be empty</string>
<string name="file_manager_missing">Please install a file manager like MiXplorer</string>
<string name="connect">Connect</string> <string name="connect">Connect</string>
<!-- menu category --> <!-- menu category -->
...@@ -109,7 +112,7 @@ ...@@ -109,7 +112,7 @@
<string name="profile_config">Profile config</string> <string name="profile_config">Profile config</string>
<string name="delete">Remove</string> <string name="delete">Remove</string>
<string name="delete_confirm_prompt">Are you sure you want to remove this profile?</string> <string name="delete_confirm_prompt">Are you sure you want to remove this profile?</string>
<string name="share_qr_nfc">QR code/NFC</string> <string name="share_qr_nfc">QR code</string>
<string name="add_profile_dialog">Add this Shadowsocks Profile?</string> <string name="add_profile_dialog">Add this Shadowsocks Profile?</string>
<string name="add_profile_methods_scan_qr_code">Scan QR code</string> <string name="add_profile_methods_scan_qr_code">Scan QR code</string>
<string name="add_profile_methods_manual_settings">Manual Settings</string> <string name="add_profile_methods_manual_settings">Manual Settings</string>
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# The setting is particularly useful for tweaking memory settings. # The setting is particularly useful for tweaking memory settings.
android.enableJetifier=true android.enableJetifier=true
android.enableR8=true android.enableR8=true
# android.enableR8.fullMode=true android.enableR8.fullMode=true
android.useAndroidX=true android.useAndroidX=true
# When configured, Gradle will run in incubating parallel mode. # When configured, Gradle will run in incubating parallel mode.
......
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
...@@ -39,6 +39,10 @@ android { ...@@ -39,6 +39,10 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
} }
} }
compileOptions {
sourceCompatibility javaVersion
targetCompatibility javaVersion
}
packagingOptions.exclude '**/*.kotlin_*' packagingOptions.exclude '**/*.kotlin_*'
splits { splits {
abi { abi {
...@@ -56,11 +60,11 @@ androidExtensions { ...@@ -56,11 +60,11 @@ androidExtensions {
dependencies { dependencies {
implementation project(':core') implementation project(':core')
implementation "androidx.browser:browser:1.0.0" implementation 'androidx.browser:browser:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'com.google.android.gms:play-services-vision:17.0.2' implementation 'com.google.android.gms:play-services-vision:17.0.2'
implementation 'com.google.firebase:firebase-ads:17.2.0' implementation 'com.google.firebase:firebase-ads:17.2.0'
implementation "com.takisoft.preferencex:preferencex-simplemenu:$preferencexVersion" implementation 'com.takisoft.preferencex:preferencex-simplemenu:1.0.0'
implementation 'com.twofortyfouram:android-plugin-api-for-locale:1.0.4' implementation 'com.twofortyfouram:android-plugin-api-for-locale:1.0.4'
implementation 'net.glxn.qrgen:android:2.0' implementation 'net.glxn.qrgen:android:2.0'
implementation 'xyz.belvi.mobilevision:barcodescanner:2.0.3' implementation 'xyz.belvi.mobilevision:barcodescanner:2.0.3'
......
...@@ -3,13 +3,10 @@ ...@@ -3,13 +3,10 @@
package="com.github.shadowsocks" package="com.github.shadowsocks"
tools:ignore="MissingLeanbackSupport"> tools:ignore="MissingLeanbackSupport">
<uses-permission android:name="android.permission.NFC" />
<uses-permission-sdk-23 android:name="android.permission.CAMERA" /> <uses-permission-sdk-23 android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.touchscreen" <uses-feature android:name="android.hardware.touchscreen"
android:required="false"/> android:required="false"/>
<uses-feature android:name="android.hardware.nfc"
android:required="false"/>
<uses-feature android:name="android.hardware.camera" <uses-feature android:name="android.hardware.camera"
android:required="false"/> android:required="false"/>
...@@ -21,7 +18,7 @@ ...@@ -21,7 +18,7 @@
android:name=".MainActivity" android:name=".MainActivity"
android:label="@string/app_name" android:label="@string/app_name"
android:theme="@style/Theme.Shadowsocks.Navigation" android:theme="@style/Theme.Shadowsocks.Navigation"
android:launchMode="singleTask"> android:launchMode="singleTop">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
...@@ -30,17 +27,6 @@ ...@@ -30,17 +27,6 @@
<intent-filter> <intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" /> <action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="ss"/>
</intent-filter>
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="ss" />
</intent-filter>
<meta-data android:name="android.app.shortcuts" <meta-data android:name="android.app.shortcuts"
android:resource="@xml/shortcuts"/> android:resource="@xml/shortcuts"/>
</activity> </activity>
...@@ -48,22 +34,19 @@ ...@@ -48,22 +34,19 @@
<activity <activity
android:name=".ProfileConfigActivity" android:name=".ProfileConfigActivity"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:label="@string/profile_config" android:label="@string/profile_config"/>
android:launchMode="singleTask"/>
<activity <activity
android:name=".AppManager" android:name=".AppManager"
android:label="@string/proxied_apps" android:label="@string/proxied_apps"
android:parentActivityName=".ProfileConfigActivity" android:parentActivityName=".ProfileConfigActivity"
android:excludeFromRecents="true" android:excludeFromRecents="true"/>
android:launchMode="singleTask"/>
<activity <activity
android:name=".UdpFallbackProfileActivity" android:name=".UdpFallbackProfileActivity"
android:label="@string/udp_fallback" android:label="@string/udp_fallback"
android:parentActivityName=".ProfileConfigActivity" android:parentActivityName=".ProfileConfigActivity"
android:excludeFromRecents="true" android:excludeFromRecents="true"/>
android:launchMode="singleTask"/>
<activity <activity
android:name=".ScannerActivity" android:name=".ScannerActivity"
...@@ -84,8 +67,7 @@ ...@@ -84,8 +67,7 @@
android:theme="@android:style/Theme.Translucent.NoTitleBar" android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:taskAffinity="" android:taskAffinity=""
android:process=":bg" android:process=":bg">
android:launchMode="singleTask">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.CREATE_SHORTCUT" /> <action android:name="android.intent.action.CREATE_SHORTCUT" />
</intent-filter> </intent-filter>
......
...@@ -23,17 +23,12 @@ package com.github.shadowsocks ...@@ -23,17 +23,12 @@ package com.github.shadowsocks
import android.app.Application import android.app.Application
import android.content.res.Configuration import android.content.res.Configuration
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import com.github.shadowsocks.preference.BottomSheetPreferenceDialogFragment
import com.github.shadowsocks.preference.IconListPreference
import com.takisoft.preferencex.PreferenceFragmentCompat
class App : Application() { class App : Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
Core.init(this, MainActivity::class) Core.init(this, MainActivity::class)
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true) AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
PreferenceFragmentCompat.registerPreferenceFragment(IconListPreference::class.java,
BottomSheetPreferenceDialogFragment::class.java)
} }
override fun onConfigurationChanged(newConfig: Configuration) { override fun onConfigurationChanged(newConfig: Configuration) {
......
...@@ -49,6 +49,7 @@ import com.github.shadowsocks.database.ProfileManager ...@@ -49,6 +49,7 @@ import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.DirectBoot import com.github.shadowsocks.utils.DirectBoot
import com.github.shadowsocks.utils.Key import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.SingleInstanceActivity
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.layout_apps.* import kotlinx.android.synthetic.main.layout_apps.*
import kotlinx.android.synthetic.main.layout_apps_item.view.* import kotlinx.android.synthetic.main.layout_apps_item.view.*
...@@ -208,6 +209,7 @@ class AppManager : AppCompatActivity() { ...@@ -208,6 +209,7 @@ class AppManager : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
SingleInstanceActivity.register(this) ?: return
setContentView(R.layout.layout_apps) setContentView(R.layout.layout_apps)
setSupportActionBar(toolbar) setSupportActionBar(toolbar)
supportActionBar!!.setDisplayHomeAsUpEnabled(true) supportActionBar!!.setDisplayHomeAsUpEnabled(true)
......
...@@ -20,35 +20,48 @@ ...@@ -20,35 +20,48 @@
package com.github.shadowsocks package com.github.shadowsocks
import android.app.Activity
import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.preference.EditTextPreference
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreference import androidx.preference.SwitchPreference
import com.github.shadowsocks.bg.BaseService import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.DirectBoot import com.github.shadowsocks.utils.DirectBoot
import com.github.shadowsocks.utils.Key import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.net.TcpFastOpen import com.github.shadowsocks.net.TcpFastOpen
import com.github.shadowsocks.preference.BrowsableEditTextPreferenceDialogFragment
import com.github.shadowsocks.preference.EditTextPreferenceModifiers
import com.github.shadowsocks.preference.HostsSummaryProvider
import com.github.shadowsocks.utils.readableMessage
import com.github.shadowsocks.utils.remove import com.github.shadowsocks.utils.remove
import com.takisoft.preferencex.PreferenceFragmentCompat
class GlobalSettingsPreferenceFragment : PreferenceFragmentCompat() { class GlobalSettingsPreferenceFragment : PreferenceFragmentCompat() {
override fun onCreatePreferencesFix(savedInstanceState: Bundle?, rootKey: String?) { companion object {
private const val REQUEST_BROWSE = 1
}
private val hosts by lazy { findPreference<EditTextPreference>(Key.hosts)!! }
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
preferenceManager.preferenceDataStore = DataStore.publicStore preferenceManager.preferenceDataStore = DataStore.publicStore
DataStore.initGlobal() DataStore.initGlobal()
addPreferencesFromResource(R.xml.pref_global) addPreferencesFromResource(R.xml.pref_global)
(findPreference(Key.persistAcrossReboot) as SwitchPreference).setOnPreferenceChangeListener { _, value -> findPreference<SwitchPreference>(Key.persistAcrossReboot)!!.setOnPreferenceChangeListener { _, value ->
BootReceiver.enabled = value as Boolean BootReceiver.enabled = value as Boolean
true true
} }
val canToggleLocked = findPreference(Key.directBootAware) val canToggleLocked = findPreference<Preference>(Key.directBootAware)!!
if (Build.VERSION.SDK_INT >= 24) canToggleLocked.setOnPreferenceChangeListener { _, newValue -> if (Build.VERSION.SDK_INT >= 24) canToggleLocked.setOnPreferenceChangeListener { _, newValue ->
if (Core.directBootSupported && newValue as Boolean) DirectBoot.update() else DirectBoot.clean() if (Core.directBootSupported && newValue as Boolean) DirectBoot.update() else DirectBoot.clean()
true true
} else canToggleLocked.remove() } else canToggleLocked.remove()
val tfo = findPreference(Key.tfo) as SwitchPreference val tfo = findPreference<SwitchPreference>(Key.tfo)!!
tfo.isChecked = DataStore.tcpFastOpen tfo.isChecked = DataStore.tcpFastOpen
tfo.setOnPreferenceChangeListener { _, value -> tfo.setOnPreferenceChangeListener { _, value ->
if (value as Boolean && !TcpFastOpen.sendEnabled) { if (value as Boolean && !TcpFastOpen.sendEnabled) {
...@@ -65,10 +78,15 @@ class GlobalSettingsPreferenceFragment : PreferenceFragmentCompat() { ...@@ -65,10 +78,15 @@ class GlobalSettingsPreferenceFragment : PreferenceFragmentCompat() {
tfo.summary = getString(R.string.tcp_fastopen_summary_unsupported, System.getProperty("os.version")) tfo.summary = getString(R.string.tcp_fastopen_summary_unsupported, System.getProperty("os.version"))
} }
val serviceMode = findPreference(Key.serviceMode) hosts.onBindEditTextListener = EditTextPreferenceModifiers.Monospace
val portProxy = findPreference(Key.portProxy) hosts.summaryProvider = HostsSummaryProvider
val portLocalDns = findPreference(Key.portLocalDns) val serviceMode = findPreference<Preference>(Key.serviceMode)!!
val portTransproxy = findPreference(Key.portTransproxy) val portProxy = findPreference<EditTextPreference>(Key.portProxy)!!
portProxy.onBindEditTextListener = EditTextPreferenceModifiers.Port
val portLocalDns = findPreference<EditTextPreference>(Key.portLocalDns)!!
portLocalDns.onBindEditTextListener = EditTextPreferenceModifiers.Port
val portTransproxy = findPreference<EditTextPreference>(Key.portTransproxy)!!
portTransproxy.onBindEditTextListener = EditTextPreferenceModifiers.Port
val onServiceModeChange = Preference.OnPreferenceChangeListener { _, newValue -> val onServiceModeChange = Preference.OnPreferenceChangeListener { _, newValue ->
val (enabledLocalDns, enabledTransproxy) = when (newValue as String?) { val (enabledLocalDns, enabledTransproxy) = when (newValue as String?) {
Key.modeProxy -> Pair(false, false) Key.modeProxy -> Pair(false, false)
...@@ -76,20 +94,18 @@ class GlobalSettingsPreferenceFragment : PreferenceFragmentCompat() { ...@@ -76,20 +94,18 @@ class GlobalSettingsPreferenceFragment : PreferenceFragmentCompat() {
Key.modeTransproxy -> Pair(true, true) Key.modeTransproxy -> Pair(true, true)
else -> throw IllegalArgumentException("newValue: $newValue") else -> throw IllegalArgumentException("newValue: $newValue")
} }
hosts.isEnabled = enabledLocalDns
portLocalDns.isEnabled = enabledLocalDns portLocalDns.isEnabled = enabledLocalDns
portTransproxy.isEnabled = enabledTransproxy portTransproxy.isEnabled = enabledTransproxy
true true
} }
val listener: (BaseService.State) -> Unit = { val listener: (BaseService.State) -> Unit = {
if (it == BaseService.State.Stopped) { val stopped = it == BaseService.State.Stopped
tfo.isEnabled = true tfo.isEnabled = stopped
serviceMode.isEnabled = true serviceMode.isEnabled = stopped
portProxy.isEnabled = true portProxy.isEnabled = stopped
onServiceModeChange.onPreferenceChange(null, DataStore.serviceMode) if (stopped) onServiceModeChange.onPreferenceChange(null, DataStore.serviceMode) else {
} else { hosts.isEnabled = false
tfo.isEnabled = false
serviceMode.isEnabled = false
portProxy.isEnabled = false
portLocalDns.isEnabled = false portLocalDns.isEnabled = false
portTransproxy.isEnabled = false portTransproxy.isEnabled = false
} }
...@@ -99,6 +115,29 @@ class GlobalSettingsPreferenceFragment : PreferenceFragmentCompat() { ...@@ -99,6 +115,29 @@ class GlobalSettingsPreferenceFragment : PreferenceFragmentCompat() {
serviceMode.onPreferenceChangeListener = onServiceModeChange serviceMode.onPreferenceChangeListener = onServiceModeChange
} }
override fun onDisplayPreferenceDialog(preference: Preference?) {
if (preference == hosts) BrowsableEditTextPreferenceDialogFragment().apply {
setKey(hosts.key)
setTargetFragment(this@GlobalSettingsPreferenceFragment, REQUEST_BROWSE)
}.show(fragmentManager ?: return, hosts.key) else super.onDisplayPreferenceDialog(preference)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) {
REQUEST_BROWSE -> {
if (resultCode != Activity.RESULT_OK) return
val activity = activity as MainActivity
try {
// we read and persist all its content here to avoid content URL permission issues
hosts.text = activity.contentResolver.openInputStream(data!!.data!!)!!.bufferedReader().readText()
} catch (e: RuntimeException) {
activity.snackbar(e.readableMessage).show()
}
}
else -> super.onActivityResult(requestCode, resultCode, data)
}
}
override fun onDestroy() { override fun onDestroy() {
MainActivity.stateListener = null MainActivity.stateListener = null
super.onDestroy() super.onDestroy()
......
...@@ -23,20 +23,15 @@ package com.github.shadowsocks ...@@ -23,20 +23,15 @@ package com.github.shadowsocks
import android.app.Activity import android.app.Activity
import android.app.backup.BackupManager import android.app.backup.BackupManager
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.net.VpnService import android.net.VpnService
import android.nfc.NdefMessage
import android.nfc.NfcAdapter
import android.os.Bundle import android.os.Bundle
import android.os.DeadObjectException import android.os.DeadObjectException
import android.os.Handler import android.os.Handler
import android.os.Parcelable
import android.util.Log import android.util.Log
import android.view.KeyCharacterMap import android.view.KeyCharacterMap
import android.view.KeyEvent import android.view.KeyEvent
import android.view.MenuItem import android.view.MenuItem
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
...@@ -51,18 +46,14 @@ import com.github.shadowsocks.aidl.IShadowsocksService ...@@ -51,18 +46,14 @@ import com.github.shadowsocks.aidl.IShadowsocksService
import com.github.shadowsocks.aidl.ShadowsocksConnection import com.github.shadowsocks.aidl.ShadowsocksConnection
import com.github.shadowsocks.aidl.TrafficStats import com.github.shadowsocks.aidl.TrafficStats
import com.github.shadowsocks.bg.BaseService import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.plugin.AlertDialogFragment
import com.github.shadowsocks.plugin.Empty
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.preference.OnPreferenceDataStoreChangeListener import com.github.shadowsocks.preference.OnPreferenceDataStoreChangeListener
import com.github.shadowsocks.utils.Key import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.SingleInstanceActivity
import com.github.shadowsocks.widget.ServiceButton import com.github.shadowsocks.widget.ServiceButton
import com.github.shadowsocks.widget.StatsBar import com.github.shadowsocks.widget.StatsBar
import com.google.android.material.navigation.NavigationView import com.google.android.material.navigation.NavigationView
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.parcel.Parcelize
class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPreferenceDataStoreChangeListener, class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPreferenceDataStoreChangeListener,
NavigationView.OnNavigationItemSelectedListener { NavigationView.OnNavigationItemSelectedListener {
...@@ -73,17 +64,6 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref ...@@ -73,17 +64,6 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
var stateListener: ((BaseService.State) -> Unit)? = null var stateListener: ((BaseService.State) -> Unit)? = null
} }
@Parcelize
data class ProfilesArg(val profiles: List<Profile>) : Parcelable
class ImportProfilesDialogFragment : AlertDialogFragment<ProfilesArg, Empty>() {
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
setTitle(R.string.add_profile_dialog)
setPositiveButton(R.string.yes) { _, _ -> arg.profiles.forEach { ProfileManager.createProfile(it) } }
setNegativeButton(R.string.no, null)
setMessage(arg.profiles.joinToString("\n"))
}
}
// UI // UI
private lateinit var fab: ServiceButton private lateinit var fab: ServiceButton
private lateinit var stats: StatsBar private lateinit var stats: StatsBar
...@@ -92,7 +72,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref ...@@ -92,7 +72,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
val snackbar by lazy { findViewById<CoordinatorLayout>(R.id.snackbar) } val snackbar by lazy { findViewById<CoordinatorLayout>(R.id.snackbar) }
fun snackbar(text: CharSequence = "") = Snackbar.make(snackbar, text, Snackbar.LENGTH_LONG).apply { fun snackbar(text: CharSequence = "") = Snackbar.make(snackbar, text, Snackbar.LENGTH_LONG).apply {
view.translationY += fab.top + fab.translationY - snackbar.measuredHeight anchorView = fab
} }
private val customTabsIntent by lazy { private val customTabsIntent by lazy {
...@@ -167,6 +147,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref ...@@ -167,6 +147,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
SingleInstanceActivity.register(this) ?: return
setContentView(R.layout.layout_main) setContentView(R.layout.layout_main)
stats = findViewById(R.id.stats) stats = findViewById(R.id.stats)
stats.setOnClickListener { if (state == BaseService.State.Connected) stats.testConnection() } stats.setOnClickListener { if (state == BaseService.State.Connected) stats.testConnection() }
...@@ -184,32 +165,9 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref ...@@ -184,32 +165,9 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
changeState(BaseService.State.Idle) // reset everything to init state changeState(BaseService.State.Idle) // reset everything to init state
connection.connect(this, this) connection.connect(this, this)
DataStore.publicStore.registerChangeListener(this) DataStore.publicStore.registerChangeListener(this)
val intent = this.intent
if (intent != null) handleShareIntent(intent)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
handleShareIntent(intent)
}
private fun handleShareIntent(intent: Intent) {
val sharedStr = when (intent.action) {
Intent.ACTION_VIEW -> intent.data?.toString()
NfcAdapter.ACTION_NDEF_DISCOVERED -> {
val rawMsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)
if (rawMsgs != null && rawMsgs.isNotEmpty()) String((rawMsgs[0] as NdefMessage).records[0].payload)
else null
}
else -> null
}
if (sharedStr.isNullOrEmpty()) return
val profiles = Profile.findAllUrls(sharedStr, Core.currentProfile?.first).toList()
if (profiles.isEmpty()) snackbar().setText(R.string.profile_invalid_input).show()
else ImportProfilesDialogFragment().withArg(ProfilesArg(profiles)).show(supportFragmentManager, null)
} }
override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String?) { override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String) {
when (key) { when (key) {
Key.serviceMode -> handler.post { Key.serviceMode -> handler.post {
connection.disconnect(this) connection.disconnect(this)
......
...@@ -32,6 +32,7 @@ import com.github.shadowsocks.plugin.AlertDialogFragment ...@@ -32,6 +32,7 @@ import com.github.shadowsocks.plugin.AlertDialogFragment
import com.github.shadowsocks.plugin.Empty import com.github.shadowsocks.plugin.Empty
import com.github.shadowsocks.plugin.PluginContract import com.github.shadowsocks.plugin.PluginContract
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.SingleInstanceActivity
class ProfileConfigActivity : AppCompatActivity() { class ProfileConfigActivity : AppCompatActivity() {
companion object { companion object {
...@@ -51,6 +52,7 @@ class ProfileConfigActivity : AppCompatActivity() { ...@@ -51,6 +52,7 @@ class ProfileConfigActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
SingleInstanceActivity.register(this) ?: return
setContentView(R.layout.layout_profile_config) setContentView(R.layout.layout_profile_config)
setSupportActionBar(findViewById(R.id.toolbar)) setSupportActionBar(findViewById(R.id.toolbar))
supportActionBar!!.apply { supportActionBar!!.apply {
......
...@@ -29,27 +29,21 @@ import android.os.Bundle ...@@ -29,27 +29,21 @@ import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.view.MenuItem import android.view.MenuItem
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.os.bundleOf import androidx.preference.*
import androidx.preference.Preference
import androidx.preference.PreferenceDataStore
import androidx.preference.SwitchPreference
import com.github.shadowsocks.Core.app import com.github.shadowsocks.Core.app
import com.github.shadowsocks.database.Profile import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.plugin.* import com.github.shadowsocks.plugin.*
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.*
import com.github.shadowsocks.preference.IconListPreference
import com.github.shadowsocks.preference.OnPreferenceDataStoreChangeListener
import com.github.shadowsocks.preference.PluginConfigurationDialogFragment
import com.github.shadowsocks.utils.* import com.github.shadowsocks.utils.*
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.takisoft.preferencex.EditTextPreference
import com.takisoft.preferencex.PreferenceFragmentCompat
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
class ProfileConfigFragment : PreferenceFragmentCompat(), class ProfileConfigFragment : PreferenceFragmentCompat(),
Preference.OnPreferenceChangeListener, OnPreferenceDataStoreChangeListener { Preference.OnPreferenceChangeListener, OnPreferenceDataStoreChangeListener {
companion object { companion object PasswordSummaryProvider : Preference.SummaryProvider<EditTextPreference> {
override fun provideSummary(preference: EditTextPreference?) = "\u2022".repeat(preference?.text?.length ?: 0)
private const val REQUEST_CODE_PLUGIN_CONFIGURE = 1 private const val REQUEST_CODE_PLUGIN_CONFIGURE = 1
const val REQUEST_UNSAVED_CHANGES = 2 const val REQUEST_UNSAVED_CHANGES = 2
} }
...@@ -75,27 +69,29 @@ class ProfileConfigFragment : PreferenceFragmentCompat(), ...@@ -75,27 +69,29 @@ class ProfileConfigFragment : PreferenceFragmentCompat(),
private lateinit var receiver: BroadcastReceiver private lateinit var receiver: BroadcastReceiver
private lateinit var udpFallback: Preference private lateinit var udpFallback: Preference
override fun onCreatePreferencesFix(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
preferenceManager.preferenceDataStore = DataStore.privateStore preferenceManager.preferenceDataStore = DataStore.privateStore
val activity = requireActivity() val activity = requireActivity()
profileId = activity.intent.getLongExtra(Action.EXTRA_PROFILE_ID, -1L) profileId = activity.intent.getLongExtra(Action.EXTRA_PROFILE_ID, -1L)
addPreferencesFromResource(R.xml.pref_profile) addPreferencesFromResource(R.xml.pref_profile)
findPreference<EditTextPreference>(Key.remotePort)!!.onBindEditTextListener = EditTextPreferenceModifiers.Port
findPreference<EditTextPreference>(Key.password)!!.summaryProvider = PasswordSummaryProvider
val serviceMode = DataStore.serviceMode val serviceMode = DataStore.serviceMode
findPreference(Key.remoteDns).isEnabled = serviceMode != Key.modeProxy findPreference<Preference>(Key.remoteDns)!!.isEnabled = serviceMode != Key.modeProxy
findPreference(Key.ipv6)!!.isEnabled = serviceMode == Key.modeVpn findPreference<Preference>(Key.ipv6)!!.isEnabled = serviceMode == Key.modeVpn
isProxyApps = findPreference(Key.proxyApps) as SwitchPreference isProxyApps = findPreference(Key.proxyApps)!!
isProxyApps.isEnabled = serviceMode == Key.modeVpn isProxyApps.isEnabled = serviceMode == Key.modeVpn
isProxyApps.setOnPreferenceClickListener { isProxyApps.setOnPreferenceClickListener {
startActivity(Intent(activity, AppManager::class.java)) startActivity(Intent(activity, AppManager::class.java))
isProxyApps.isChecked = true isProxyApps.isChecked = true
false false
} }
findPreference(Key.metered)!!.apply { findPreference<Preference>(Key.metered)!!.apply {
if (Build.VERSION.SDK_INT >= 28) isEnabled = serviceMode == Key.modeVpn else remove() if (Build.VERSION.SDK_INT >= 28) isEnabled = serviceMode == Key.modeVpn else remove()
} }
findPreference(Key.udpdns).isEnabled = serviceMode != Key.modeProxy findPreference<Preference>(Key.udpdns)!!.isEnabled = serviceMode != Key.modeProxy
plugin = findPreference(Key.plugin) as IconListPreference plugin = findPreference(Key.plugin)!!
pluginConfigure = findPreference(Key.pluginConfigure) as EditTextPreference pluginConfigure = findPreference(Key.pluginConfigure)!!
plugin.unknownValueSummary = getString(R.string.plugin_unknown) plugin.unknownValueSummary = getString(R.string.plugin_unknown)
plugin.setOnPreferenceChangeListener { _, newValue -> plugin.setOnPreferenceChangeListener { _, newValue ->
pluginConfiguration = PluginConfiguration(pluginConfiguration.pluginsOptions, newValue as String) pluginConfiguration = PluginConfiguration(pluginConfiguration.pluginsOptions, newValue as String)
...@@ -108,10 +104,11 @@ class ProfileConfigFragment : PreferenceFragmentCompat(), ...@@ -108,10 +104,11 @@ class ProfileConfigFragment : PreferenceFragmentCompat(),
} }
true true
} }
pluginConfigure.onBindEditTextListener = EditTextPreferenceModifiers.Monospace
pluginConfigure.onPreferenceChangeListener = this pluginConfigure.onPreferenceChangeListener = this
initPlugins() initPlugins()
receiver = Core.listenForPackageChanges(false) { initPlugins() } receiver = Core.listenForPackageChanges(false) { initPlugins() }
udpFallback = findPreference(Key.udpFallback) udpFallback = findPreference(Key.udpFallback)!!
DataStore.privateStore.registerChangeListener(this) DataStore.privateStore.registerChangeListener(this)
} }
...@@ -124,14 +121,16 @@ class ProfileConfigFragment : PreferenceFragmentCompat(), ...@@ -124,14 +121,16 @@ class ProfileConfigFragment : PreferenceFragmentCompat(),
pluginConfiguration = PluginConfiguration(DataStore.plugin) pluginConfiguration = PluginConfiguration(DataStore.plugin)
plugin.value = pluginConfiguration.selected plugin.value = pluginConfiguration.selected
plugin.init() plugin.init()
plugin.checkSummary()
pluginConfigure.isEnabled = pluginConfiguration.selected.isNotEmpty() pluginConfigure.isEnabled = pluginConfiguration.selected.isNotEmpty()
pluginConfigure.text = pluginConfiguration.selectedOptions.toString() pluginConfigure.text = pluginConfiguration.selectedOptions.toString()
} }
private fun showPluginEditor() = displayPreferenceDialog(PluginConfigurationDialogFragment(), Key.pluginConfigure, private fun showPluginEditor() {
bundleOf(Pair("key", Key.pluginConfigure), PluginConfigurationDialogFragment().apply {
Pair(PluginConfigurationDialogFragment.PLUGIN_ID_FRAGMENT_TAG, pluginConfiguration.selected))) setArg(Key.pluginConfigure, pluginConfiguration.selected)
setTargetFragment(this@ProfileConfigFragment, 0)
}.show(fragmentManager ?: return, Key.pluginConfigure)
}
private fun saveAndExit() { private fun saveAndExit() {
val profile = ProfileManager.getProfile(profileId) ?: Profile() val profile = ProfileManager.getProfile(profileId) ?: Profile()
...@@ -163,18 +162,26 @@ class ProfileConfigFragment : PreferenceFragmentCompat(), ...@@ -163,18 +162,26 @@ class ProfileConfigFragment : PreferenceFragmentCompat(),
false false
} }
override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String?) { override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String) {
if (key != Key.proxyApps && findPreference(key) != null) DataStore.dirty = true if (key != Key.proxyApps && findPreference<Preference>(key) != null) DataStore.dirty = true
} }
override fun onDisplayPreferenceDialog(preference: Preference) { override fun onDisplayPreferenceDialog(preference: Preference) {
if (preference.key == Key.pluginConfigure) { when (preference.key) {
val intent = PluginManager.buildIntent(pluginConfiguration.selected, PluginContract.ACTION_CONFIGURE) Key.plugin -> BottomSheetPreferenceDialogFragment().apply {
if (intent.resolveActivity(requireContext().packageManager) == null) showPluginEditor() else setArg(Key.plugin)
startActivityForResult(intent setTargetFragment(this@ProfileConfigFragment, 0)
.putExtra(PluginContract.EXTRA_OPTIONS, pluginConfiguration.selectedOptions.toString()), }.show(fragmentManager ?: return, Key.plugin)
REQUEST_CODE_PLUGIN_CONFIGURE) Key.pluginConfigure -> {
} else super.onDisplayPreferenceDialog(preference) val intent = PluginManager.buildIntent(pluginConfiguration.selected, PluginContract.ACTION_CONFIGURE)
if (intent.resolveActivity(requireContext().packageManager) == null) showPluginEditor() else {
startActivityForResult(intent
.putExtra(PluginContract.EXTRA_OPTIONS, pluginConfiguration.selectedOptions.toString()),
REQUEST_CODE_PLUGIN_CONFIGURE)
}
}
else -> super.onDisplayPreferenceDialog(preference)
}
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
......
...@@ -23,9 +23,6 @@ package com.github.shadowsocks ...@@ -23,9 +23,6 @@ package com.github.shadowsocks
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.* import android.content.*
import android.nfc.NdefMessage
import android.nfc.NdefRecord
import android.nfc.NfcAdapter
import android.os.Bundle import android.os.Bundle
import android.text.format.Formatter import android.text.format.Formatter
import android.util.LongSparseArray import android.util.LongSparseArray
...@@ -82,33 +79,13 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener { ...@@ -82,33 +79,13 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
arguments = bundleOf(Pair(KEY_URL, url)) arguments = bundleOf(Pair(KEY_URL, url))
} }
private val url get() = arguments?.getString(KEY_URL)!!
private val nfcShareItem by lazy { url.toByteArray() }
private var adapter: NfcAdapter? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val image = ImageView(context) val image = ImageView(context)
image.layoutParams = LinearLayout.LayoutParams(-1, -1) image.layoutParams = LinearLayout.LayoutParams(-1, -1)
val size = resources.getDimensionPixelSize(R.dimen.qr_code_size) val size = resources.getDimensionPixelSize(R.dimen.qr_code_size)
image.setImageBitmap((QRCode.from(url).withSize(size, size) as QRCode).bitmap()) image.setImageBitmap((QRCode.from(arguments?.getString(KEY_URL)!!).withSize(size, size) as QRCode).bitmap())
return image return image
} }
override fun onAttach(context: Context) {
super.onAttach(context)
val adapter = NfcAdapter.getDefaultAdapter(context)
adapter?.setNdefPushMessage(NdefMessage(arrayOf(
NdefRecord(NdefRecord.TNF_ABSOLUTE_URI, nfcShareItem, byteArrayOf(), nfcShareItem))), activity)
this.adapter = adapter
}
override fun onDetach() {
super.onDetach()
val activity = activity
if (activity != null && !activity.isFinishing && !activity.isDestroyed)
adapter?.setNdefPushMessage(null, activity)
adapter = null
}
} }
inner class ProfileViewHolder(view: View) : RecyclerView.ViewHolder(view), inner class ProfileViewHolder(view: View) : RecyclerView.ViewHolder(view),
...@@ -202,7 +179,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener { ...@@ -202,7 +179,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
} }
override fun onMenuItemClick(item: MenuItem): Boolean = when (item.itemId) { override fun onMenuItemClick(item: MenuItem): Boolean = when (item.itemId) {
R.id.action_qr_code_nfc -> { R.id.action_qr_code -> {
requireFragmentManager().beginTransaction().add(QRCodeDialog(this.item.toString()), "") requireFragmentManager().beginTransaction().add(QRCodeDialog(this.item.toString()), "")
.commitAllowingStateLoss() .commitAllowingStateLoss()
true true
......
...@@ -35,6 +35,7 @@ import com.github.shadowsocks.database.Profile ...@@ -35,6 +35,7 @@ import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.plugin.PluginConfiguration import com.github.shadowsocks.plugin.PluginConfiguration
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.SingleInstanceActivity
import com.github.shadowsocks.utils.resolveResourceId import com.github.shadowsocks.utils.resolveResourceId
class UdpFallbackProfileActivity : AppCompatActivity() { class UdpFallbackProfileActivity : AppCompatActivity() {
...@@ -77,11 +78,12 @@ class UdpFallbackProfileActivity : AppCompatActivity() { ...@@ -77,11 +78,12 @@ class UdpFallbackProfileActivity : AppCompatActivity() {
private val profilesAdapter = ProfilesAdapter() private val profilesAdapter = ProfilesAdapter()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (editingId == null) { if (editingId == null) {
finish() finish()
return return
} }
super.onCreate(savedInstanceState) SingleInstanceActivity.register(this) ?: return
setContentView(R.layout.layout_udp_fallback) setContentView(R.layout.layout_udp_fallback)
val toolbar = findViewById<Toolbar>(R.id.toolbar) val toolbar = findViewById<Toolbar>(R.id.toolbar)
......
...@@ -32,6 +32,7 @@ import android.view.View ...@@ -32,6 +32,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.core.os.bundleOf
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.preference.PreferenceDialogFragmentCompat import androidx.preference.PreferenceDialogFragmentCompat
import androidx.recyclerview.widget.DefaultItemAnimator import androidx.recyclerview.widget.DefaultItemAnimator
...@@ -97,6 +98,10 @@ class BottomSheetPreferenceDialogFragment : PreferenceDialogFragmentCompat() { ...@@ -97,6 +98,10 @@ class BottomSheetPreferenceDialogFragment : PreferenceDialogFragmentCompat() {
} }
} }
fun setArg(key: String) {
arguments = bundleOf(PreferenceDialogFragmentCompat.ARG_KEY to key)
}
private val preference by lazy { getPreference() as IconListPreference } private val preference by lazy { getPreference() as IconListPreference }
private var clickedIndex = -1 private var clickedIndex = -1
......
/*******************************************************************************
* *
* Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.preference
import android.content.ActivityNotFoundException
import android.content.Intent
import androidx.appcompat.app.AlertDialog
import androidx.core.os.bundleOf
import androidx.preference.EditTextPreferenceDialogFragmentCompat
import com.github.shadowsocks.MainActivity
import com.github.shadowsocks.R
class BrowsableEditTextPreferenceDialogFragment : EditTextPreferenceDialogFragmentCompat() {
fun setKey(key: String) {
arguments = bundleOf(Pair(ARG_KEY, key))
}
override fun onPrepareDialogBuilder(builder: AlertDialog.Builder) {
super.onPrepareDialogBuilder(builder)
builder.setNeutralButton(R.string.browse) { _, _ ->
val activity = activity as MainActivity
try {
targetFragment!!.startActivityForResult(Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
}, targetRequestCode)
return@setNeutralButton
} catch (_: ActivityNotFoundException) { } catch (_: SecurityException) { }
activity.snackbar(activity.getString(R.string.file_manager_missing)).show()
}
}
}
...@@ -26,6 +26,14 @@ import android.util.AttributeSet ...@@ -26,6 +26,14 @@ import android.util.AttributeSet
import androidx.preference.ListPreference import androidx.preference.ListPreference
class IconListPreference(context: Context, attrs: AttributeSet? = null) : ListPreference(context, attrs) { class IconListPreference(context: Context, attrs: AttributeSet? = null) : ListPreference(context, attrs) {
companion object FallbackProvider : SummaryProvider<IconListPreference> {
override fun provideSummary(preference: IconListPreference?): CharSequence? {
val i = preference?.selectedEntry
return if (i != null && i < 0) preference.unknownValueSummary?.format(preference.value) else
preference?.entry
}
}
var entryIcons: Array<Drawable?>? = null var entryIcons: Array<Drawable?>? = null
val selectedEntry: Int get() = entryValues.indexOf(value) val selectedEntry: Int get() = entryValues.indexOf(value)
val entryIcon: Drawable? get() = entryIcons?.getOrNull(selectedEntry) val entryIcon: Drawable? get() = entryIcons?.getOrNull(selectedEntry)
...@@ -49,7 +57,6 @@ class IconListPreference(context: Context, attrs: AttributeSet? = null) : ListPr ...@@ -49,7 +57,6 @@ class IconListPreference(context: Context, attrs: AttributeSet? = null) : ListPr
val listener = listener val listener = listener
if (listener == null || listener.onPreferenceChange(preference, newValue)) { if (listener == null || listener.onPreferenceChange(preference, newValue)) {
value = newValue.toString() value = newValue.toString()
checkSummary()
if (entryIcons != null) icon = entryIcon if (entryIcons != null) icon = entryIcon
true true
} else false } else false
...@@ -60,13 +67,9 @@ class IconListPreference(context: Context, attrs: AttributeSet? = null) : ListPr ...@@ -60,13 +67,9 @@ class IconListPreference(context: Context, attrs: AttributeSet? = null) : ListPr
// a.recycle() // a.recycle()
} }
fun checkSummary() {
val unknownValueSummary = unknownValueSummary
if (unknownValueSummary != null) summary = if (selectedEntry < 0) unknownValueSummary.format(value) else "%s"
}
fun init() { fun init() {
icon = entryIcon icon = entryIcon
summaryProvider = FallbackProvider
} }
override fun onSetInitialValue(defaultValue: Any?) { override fun onSetInitialValue(defaultValue: Any?) {
super.onSetInitialValue(defaultValue) super.onSetInitialValue(defaultValue)
......
...@@ -23,17 +23,23 @@ package com.github.shadowsocks.preference ...@@ -23,17 +23,23 @@ package com.github.shadowsocks.preference
import android.view.View import android.view.View
import android.widget.EditText import android.widget.EditText
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.os.bundleOf
import androidx.preference.EditTextPreferenceDialogFragmentCompat
import androidx.preference.PreferenceDialogFragmentCompat
import com.github.shadowsocks.ProfileConfigActivity import com.github.shadowsocks.ProfileConfigActivity
import com.github.shadowsocks.plugin.PluginContract import com.github.shadowsocks.plugin.PluginContract
import com.github.shadowsocks.plugin.PluginManager import com.github.shadowsocks.plugin.PluginManager
import com.takisoft.preferencex.EditTextPreferenceDialogFragmentCompat
class PluginConfigurationDialogFragment : EditTextPreferenceDialogFragmentCompat() { class PluginConfigurationDialogFragment : EditTextPreferenceDialogFragmentCompat() {
companion object { companion object {
const val PLUGIN_ID_FRAGMENT_TAG = private const val PLUGIN_ID_FRAGMENT_TAG =
"com.github.shadowsocks.preference.PluginConfigurationDialogFragment.PLUGIN_ID" "com.github.shadowsocks.preference.PluginConfigurationDialogFragment.PLUGIN_ID"
} }
fun setArg(key: String, plugin: String) {
arguments = bundleOf(PreferenceDialogFragmentCompat.ARG_KEY to key, PLUGIN_ID_FRAGMENT_TAG to plugin)
}
private lateinit var editText: EditText private lateinit var editText: EditText
override fun onPrepareDialogBuilder(builder: AlertDialog.Builder) { override fun onPrepareDialogBuilder(builder: AlertDialog.Builder) {
......
...@@ -77,14 +77,9 @@ class ServiceButton @JvmOverloads constructor(context: Context, attrs: Attribute ...@@ -77,14 +77,9 @@ class ServiceButton @JvmOverloads constructor(context: Context, attrs: Attribute
} }
else -> changeState(iconStopped, animate) else -> changeState(iconStopped, animate)
} }
if (state == BaseService.State.Connected) { checked = state == BaseService.State.Connected
checked = true
TooltipCompat.setTooltipText(this, context.getString(R.string.stop))
} else {
checked = false
TooltipCompat.setTooltipText(this, context.getString(R.string.connect))
}
refreshDrawableState() refreshDrawableState()
TooltipCompat.setTooltipText(this, context.getString(if (state.canStop) R.string.stop else R.string.connect))
isEnabled = state.canStop || state == BaseService.State.Stopped isEnabled = state.canStop || state == BaseService.State.Stopped
} }
......
...@@ -48,10 +48,11 @@ class StatsBar @JvmOverloads constructor(context: Context, attrs: AttributeSet? ...@@ -48,10 +48,11 @@ class StatsBar @JvmOverloads constructor(context: Context, attrs: AttributeSet?
private val behavior = object : Behavior() { private val behavior = object : Behavior() {
val threshold = context.resources.getDimensionPixelSize(R.dimen.stats_bar_scroll_threshold) val threshold = context.resources.getDimensionPixelSize(R.dimen.stats_bar_scroll_threshold)
override fun onNestedScroll(coordinatorLayout: CoordinatorLayout, child: BottomAppBar, target: View, override fun onNestedScroll(coordinatorLayout: CoordinatorLayout, child: BottomAppBar, target: View,
dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int, type: Int) { dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int, type: Int,
consumed: IntArray) {
val dy = dyConsumed + dyUnconsumed val dy = dyConsumed + dyUnconsumed
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, if (Math.abs(dy) >= threshold) dy else 0, super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, if (Math.abs(dy) >= threshold) dy else 0,
dxUnconsumed, 0, type) dxUnconsumed, 0, type, consumed)
} }
} }
override fun getBehavior() = behavior override fun getBehavior() = behavior
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical" android:orientation="vertical"
android:duplicateParentState="false"> android:duplicateParentState="false">
<androidx.appcompat.widget.Toolbar <com.google.android.material.appbar.MaterialToolbar
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:layout_width="match_parent" android:layout_width="match_parent"
android:background="?attr/colorPrimary" android:background="?attr/colorPrimary"
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
android:iconifiedByDefault="false" android:iconifiedByDefault="false"
android:queryHint="@android:string/search_go"/> android:queryHint="@android:string/search_go"/>
</androidx.appcompat.widget.Toolbar> </com.google.android.material.appbar.MaterialToolbar>
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="fill_parent" android:layout_width="fill_parent"
......
<?xml version="1.0" encoding="utf-8"?>
<!-- Based on: https://android.googlesource.com/platform/frameworks/support/+/9b8fb51/preference/res/layout/preference_dialog_edittext.xml -->
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="48dp"
android:layout_marginBottom="48dp"
android:overScrollMode="ifContentScrolls">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@android:id/message"
style="?android:attr/textAppearanceSmall"
android:layout_marginLeft="24dp"
android:layout_marginRight="24dp"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="48dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?android:attr/textColorSecondary"/>
<com.google.android.material.textfield.TextInputLayout
android:orientation="vertical"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:passwordToggleEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@android:id/edit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:inputType="textPassword"
android:typeface="monospace"/>
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</ScrollView>
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"> <menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/action_qr_code_nfc" <item android:id="@+id/action_qr_code"
android:title="@string/share_qr_nfc"/> android:title="@string/share_qr_nfc"/>
<item android:id="@+id/action_export_clipboard" <item android:id="@+id/action_export_clipboard"
android:title="@string/action_export"/> android:title="@string/action_export"/>
......
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen <PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android" app:initialExpandedChildrenCount="4">
xmlns:app="http://schemas.android.com/apk/res-auto"
android:key="settings_screen"
app:initialExpandedChildrenCount="3">
<SwitchPreference android:key="isAutoConnect"
android:summary="@string/auto_connect_summary"
android:title="@string/auto_connect"/>
<SwitchPreference <SwitchPreference
android:key="directBootAware" app:key="isAutoConnect"
android:icon="@drawable/ic_action_lock" app:summary="@string/auto_connect_summary"
android:summary="@string/direct_boot_aware_summary" app:title="@string/auto_connect"/>
android:title="@string/direct_boot_aware"/>
<SwitchPreference <SwitchPreference
android:key="tcp_fastopen" app:key="directBootAware"
android:icon="@drawable/ic_action_offline_bolt" app:icon="@drawable/ic_action_lock"
android:summary="@string/tcp_fastopen_summary" app:summary="@string/direct_boot_aware_summary"
android:title="TCP Fast Open"/> app:title="@string/direct_boot_aware"/>
<SwitchPreference
app:key="tcp_fastopen"
app:icon="@drawable/ic_action_offline_bolt"
app:summary="@string/tcp_fastopen_summary"
app:title="TCP Fast Open"/>
<EditTextPreference
app:key="hosts"
app:title="hosts"/>
<SimpleMenuPreference <com.takisoft.preferencex.SimpleMenuPreference
android:key="serviceMode" app:key="serviceMode"
android:entries="@array/service_modes" app:entries="@array/service_modes"
android:entryValues="@array/service_mode_values" app:entryValues="@array/service_mode_values"
android:defaultValue="vpn" app:defaultValue="vpn"
android:summary="%s" app:title="@string/service_mode"
android:title="@string/service_mode"/> app:useSimpleSummaryProvider="true"/>
<AutoSummaryEditTextPreference <EditTextPreference
android:key="portProxy" app:key="portProxy"
android:inputType="number" app:title="@string/port_proxy"
android:maxLength="5" app:useSimpleSummaryProvider="true"/>
app:pref_summaryHasText="%s" <EditTextPreference
android:singleLine="true" app:key="portLocalDns"
android:title="@string/port_proxy"/> app:icon="@drawable/ic_action_dns"
<AutoSummaryEditTextPreference app:title="@string/port_local_dns"
android:key="portLocalDns" app:useSimpleSummaryProvider="true"/>
android:icon="@drawable/ic_action_dns" <EditTextPreference
android:inputType="number" app:key="portTransproxy"
android:maxLength="5" app:title="@string/port_transproxy"
app:pref_summaryHasText="%s" app:useSimpleSummaryProvider="true"/>
android:singleLine="true"
android:title="@string/port_local_dns"/>
<AutoSummaryEditTextPreference
android:key="portTransproxy"
android:inputType="number"
android:maxLength="5"
app:pref_summaryHasText="%s"
android:singleLine="true"
android:title="@string/port_transproxy"/>
</PreferenceScreen> </PreferenceScreen>
...@@ -2,99 +2,93 @@ ...@@ -2,99 +2,93 @@
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<AutoSummaryEditTextPreference <EditTextPreference
android:key="profileName" app:key="profileName"
app:pref_summaryHasText="%s" app:title="@string/profile_name"
android:title="@string/profile_name"/> app:useSimpleSummaryProvider="true"/>
<PreferenceCategory <PreferenceCategory
android:title="@string/proxy_cat"> app:title="@string/proxy_cat">
<AutoSummaryEditTextPreference <EditTextPreference
android:key="proxy" app:key="proxy"
app:pref_summaryHasText="%s" app:singleLine="true"
android:singleLine="true" app:title="@string/proxy"
android:title="@string/proxy"/> app:useSimpleSummaryProvider="true"/>
<AutoSummaryEditTextPreference <EditTextPreference
android:key="remotePortNum" app:key="remotePortNum"
android:inputType="number" app:title="@string/remote_port"
android:maxLength="5" app:useSimpleSummaryProvider="true"/>
app:pref_summaryHasText="%s" <EditTextPreference
android:singleLine="true" app:dialogLayout="@layout/preference_dialog_password"
android:title="@string/remote_port"/> app:key="sitekey"
<AutoSummaryEditTextPreference app:title="@string/sitekey"/>
android:inputType="textPassword" <com.takisoft.preferencex.SimpleMenuPreference
android:typeface="monospace" app:key="encMethod"
app:pref_summaryPasswordSubstituteLength="0" app:icon="@drawable/ic_notification_enhanced_encryption"
android:key="sitekey" app:entries="@array/enc_method_entry"
app:pref_summaryHasText="%s" app:entryValues="@array/enc_method_value"
android:singleLine="true" app:title="@string/enc_method"
android:title="@string/sitekey"/> app:useSimpleSummaryProvider="true"/>
<SimpleMenuPreference
android:key="encMethod"
android:icon="@drawable/ic_notification_enhanced_encryption"
android:entries="@array/enc_method_entry"
android:entryValues="@array/enc_method_value"
android:summary="%s"
android:title="@string/enc_method"/>
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory <PreferenceCategory
android:title="@string/feature_cat"> app:title="@string/feature_cat">
<SimpleMenuPreference <com.takisoft.preferencex.SimpleMenuPreference
android:key="route" app:key="route"
android:entries="@array/route_entry" app:entries="@array/route_entry"
android:entryValues="@array/route_value" app:entryValues="@array/route_value"
android:summary="%s" app:title="@string/route_list"
android:title="@string/route_list"/> app:useSimpleSummaryProvider="true"/>
<SwitchPreference <SwitchPreference
android:key="isIpv6" app:key="isIpv6"
android:icon="@drawable/ic_image_looks_6" app:icon="@drawable/ic_image_looks_6"
android:summary="@string/ipv6_summary" app:summary="@string/ipv6_summary"
android:title="@string/ipv6"/> app:title="@string/ipv6"/>
<SwitchPreference <SwitchPreference
android:key="isProxyApps" app:key="isProxyApps"
android:icon="@drawable/ic_navigation_apps" app:icon="@drawable/ic_navigation_apps"
android:summary="@string/proxied_apps_summary" app:summary="@string/proxied_apps_summary"
android:title="@string/proxied_apps"/> app:title="@string/proxied_apps"/>
<SwitchPreference <SwitchPreference
android:key="metered" app:key="metered"
android:icon="@drawable/ic_device_data_usage" app:icon="@drawable/ic_device_data_usage"
android:summary="@string/metered_summary" app:summary="@string/metered_summary"
android:title="@string/metered"/> app:title="@string/metered"/>
<AutoSummaryEditTextPreference <EditTextPreference
android:key="remoteDns" app:key="remoteDns"
android:icon="@drawable/ic_action_dns" app:icon="@drawable/ic_action_dns"
android:title="@string/remote_dns" app:title="@string/remote_dns"
app:pref_summaryHasText="%s"/> app:useSimpleSummaryProvider="true"/>
<SwitchPreference <SwitchPreference
android:key="isUdpDns" app:key="isUdpDns"
android:summary="@string/udp_dns_summary" app:icon="@drawable/ic_action_dns"
android:title="@string/udp_dns"/> app:summary="@string/udp_dns_summary"
app:title="@string/udp_dns"/>
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory <PreferenceCategory
android:title="@string/plugin"> app:title="@string/plugin">
<com.github.shadowsocks.preference.IconListPreference <com.github.shadowsocks.preference.IconListPreference
android:key="plugin" app:key="plugin"
android:persistent="false" app:persistent="false"
android:summary="%s" app:title="@string/plugin"
android:title="@string/plugin"/> app:useSimpleSummaryProvider="true"/>
<AutoSummaryEditTextPreference <EditTextPreference
android:key="plugin.configure" app:key="plugin.configure"
android:icon="@drawable/ic_action_settings" app:icon="@drawable/ic_action_settings"
android:persistent="false" app:persistent="false"
android:singleLine="true" app:singleLine="true"
app:pref_summaryHasText="%s" app:title="@string/plugin_configure"
android:title="@string/plugin_configure"/> app:useSimpleSummaryProvider="true"/>
<Preference <Preference
android:key="udpFallback" app:key="udpFallback"
android:title="@string/udp_fallback" app:title="@string/udp_fallback"
android:summary="@string/plugin_disabled"> app:summary="@string/plugin_disabled">
<intent android:targetPackage="com.github.shadowsocks" <intent android:targetPackage="com.github.shadowsocks"
android:targetClass="com.github.shadowsocks.UdpFallbackProfileActivity"/> android:targetClass="com.github.shadowsocks.UdpFallbackProfileActivity"/>
</Preference> </Preference>
......
...@@ -26,6 +26,11 @@ android { ...@@ -26,6 +26,11 @@ android {
testInstrumentationRunner "androidx.testgetRepositoryPassword().runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.testgetRepositoryPassword().runner.AndroidJUnitRunner"
} }
compileOptions {
sourceCompatibility javaVersion
targetCompatibility javaVersion
}
buildTypes { buildTypes {
release { release {
minifyEnabled false minifyEnabled false
...@@ -49,9 +54,9 @@ mavenPublish { ...@@ -49,9 +54,9 @@ mavenPublish {
} }
dependencies { dependencies {
api 'androidx.core:core-ktx:1.0.1' api 'androidx.core:core-ktx:1.0.2'
api 'com.google.android.material:material:1.1.0-alpha05' api 'com.google.android.material:material:1.1.0-alpha06'
api "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" api "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion"
testImplementation "junit:junit:$junitVersion" testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test:runner:$androidTestVersion" androidTestImplementation "androidx.test:runner:$androidTestVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidEspressoVersion" androidTestImplementation "androidx.test.espresso:espresso-core:$androidEspressoVersion"
......
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.Toolbar <com.google.android.material.appbar.MaterialToolbar
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
......
...@@ -6,4 +6,6 @@ ...@@ -6,4 +6,6 @@
<string name="yes">Yes</string> <string name="yes">Yes</string>
<string name="no">No</string> <string name="no">No</string>
<string name="apply">Apply</string> <string name="apply">Apply</string>
<string name="browse">Browse…</string>
<string name="file_manager_missing">Please install a file manager like MiXplorer</string>
</resources> </resources>
...@@ -38,6 +38,10 @@ android { ...@@ -38,6 +38,10 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
} }
} }
compileOptions {
sourceCompatibility javaVersion
targetCompatibility javaVersion
}
packagingOptions.exclude '**/*.kotlin_*' packagingOptions.exclude '**/*.kotlin_*'
splits { splits {
abi { abi {
...@@ -52,7 +56,7 @@ android { ...@@ -52,7 +56,7 @@ android {
dependencies { dependencies {
implementation project(':core') implementation project(':core')
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "androidx.leanback:leanback-preference:1.1.0-alpha01" implementation "androidx.leanback:leanback-preference:1.1.0-alpha02"
} }
apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.gms.google-services'
......
...@@ -17,8 +17,7 @@ ...@@ -17,8 +17,7 @@
tools:replace="theme"> tools:replace="theme">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:label="@string/app_name" android:label="@string/app_name">
android:launchMode="singleTask">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
......
...@@ -22,10 +22,12 @@ package com.github.shadowsocks.tv ...@@ -22,10 +22,12 @@ package com.github.shadowsocks.tv
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import com.github.shadowsocks.utils.SingleInstanceActivity
class MainActivity : FragmentActivity() { class MainActivity : FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
SingleInstanceActivity.register(this) ?: return
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
} }
} }
...@@ -35,10 +35,7 @@ import androidx.leanback.preference.LeanbackPreferenceFragmentCompat ...@@ -35,10 +35,7 @@ import androidx.leanback.preference.LeanbackPreferenceFragmentCompat
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import androidx.lifecycle.get import androidx.lifecycle.get
import androidx.preference.ListPreference import androidx.preference.*
import androidx.preference.Preference
import androidx.preference.PreferenceDataStore
import androidx.preference.SwitchPreference
import com.crashlytics.android.Crashlytics import com.crashlytics.android.Crashlytics
import com.github.shadowsocks.BootReceiver import com.github.shadowsocks.BootReceiver
import com.github.shadowsocks.Core import com.github.shadowsocks.Core
...@@ -50,6 +47,8 @@ import com.github.shadowsocks.database.ProfileManager ...@@ -50,6 +47,8 @@ import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.net.HttpsTest import com.github.shadowsocks.net.HttpsTest
import com.github.shadowsocks.net.TcpFastOpen import com.github.shadowsocks.net.TcpFastOpen
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.preference.EditTextPreferenceModifiers
import com.github.shadowsocks.preference.HostsSummaryProvider
import com.github.shadowsocks.preference.OnPreferenceDataStoreChangeListener import com.github.shadowsocks.preference.OnPreferenceDataStoreChangeListener
import com.github.shadowsocks.utils.Key import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.datas import com.github.shadowsocks.utils.datas
...@@ -62,18 +61,20 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo ...@@ -62,18 +61,20 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
private const val REQUEST_CONNECT = 1 private const val REQUEST_CONNECT = 1
private const val REQUEST_REPLACE_PROFILES = 2 private const val REQUEST_REPLACE_PROFILES = 2
private const val REQUEST_EXPORT_PROFILES = 3 private const val REQUEST_EXPORT_PROFILES = 3
private const val REQUEST_HOSTS = 4
private const val TAG = "MainPreferenceFragment" private const val TAG = "MainPreferenceFragment"
} }
private lateinit var fab: ListPreference private lateinit var fab: ListPreference
private lateinit var stats: Preference private lateinit var stats: Preference
private lateinit var controlImport: Preference private lateinit var controlImport: Preference
private lateinit var hosts: EditTextPreference
private lateinit var serviceMode: Preference private lateinit var serviceMode: Preference
private lateinit var tfo: SwitchPreference private lateinit var tfo: SwitchPreference
private lateinit var shareOverLan: Preference private lateinit var shareOverLan: Preference
private lateinit var portProxy: Preference private lateinit var portProxy: EditTextPreference
private lateinit var portLocalDns: Preference private lateinit var portLocalDns: EditTextPreference
private lateinit var portTransproxy: Preference private lateinit var portTransproxy: EditTextPreference
private val onServiceModeChange = Preference.OnPreferenceChangeListener { _, newValue -> private val onServiceModeChange = Preference.OnPreferenceChangeListener { _, newValue ->
val (enabledLocalDns, enabledTransproxy) = when (newValue as String?) { val (enabledLocalDns, enabledTransproxy) = when (newValue as String?) {
Key.modeProxy -> Pair(false, false) Key.modeProxy -> Pair(false, false)
...@@ -81,6 +82,7 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo ...@@ -81,6 +82,7 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
Key.modeTransproxy -> Pair(true, true) Key.modeTransproxy -> Pair(true, true)
else -> throw IllegalArgumentException("newValue: $newValue") else -> throw IllegalArgumentException("newValue: $newValue")
} }
hosts.isEnabled = enabledLocalDns
portLocalDns.isEnabled = enabledLocalDns portLocalDns.isEnabled = enabledLocalDns
portTransproxy.isEnabled = enabledTransproxy portTransproxy.isEnabled = enabledTransproxy
true true
...@@ -121,19 +123,13 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo ...@@ -121,19 +123,13 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
}) })
if (msg != null) Toast.makeText(context, getString(R.string.vpn_error, msg), Toast.LENGTH_SHORT).show() if (msg != null) Toast.makeText(context, getString(R.string.vpn_error, msg), Toast.LENGTH_SHORT).show()
this.state = state this.state = state
if (state == BaseService.State.Stopped) { val stopped = state == BaseService.State.Stopped
controlImport.isEnabled = true controlImport.isEnabled = stopped
tfo.isEnabled = true tfo.isEnabled = stopped
serviceMode.isEnabled = true serviceMode.isEnabled = stopped
shareOverLan.isEnabled = true shareOverLan.isEnabled = stopped
portProxy.isEnabled = true portProxy.isEnabled = stopped
onServiceModeChange.onPreferenceChange(null, DataStore.serviceMode) if (stopped) onServiceModeChange.onPreferenceChange(null, DataStore.serviceMode) else {
} else {
controlImport.isEnabled = false
tfo.isEnabled = false
serviceMode.isEnabled = false
shareOverLan.isEnabled = false
portProxy.isEnabled = false
portLocalDns.isEnabled = false portLocalDns.isEnabled = false
portTransproxy.isEnabled = false portTransproxy.isEnabled = false
} }
...@@ -156,17 +152,17 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo ...@@ -156,17 +152,17 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
preferenceManager.preferenceDataStore = DataStore.publicStore preferenceManager.preferenceDataStore = DataStore.publicStore
DataStore.initGlobal() DataStore.initGlobal()
addPreferencesFromResource(R.xml.pref_main) addPreferencesFromResource(R.xml.pref_main)
fab = findPreference(Key.id) as ListPreference fab = findPreference(Key.id)!!
populateProfiles() populateProfiles()
stats = findPreference(Key.controlStats) stats = findPreference(Key.controlStats)!!
controlImport = findPreference(Key.controlImport) controlImport = findPreference(Key.controlImport)!!
(findPreference(Key.persistAcrossReboot) as SwitchPreference).setOnPreferenceChangeListener { _, value -> findPreference<SwitchPreference>(Key.persistAcrossReboot)!!.setOnPreferenceChangeListener { _, value ->
BootReceiver.enabled = value as Boolean BootReceiver.enabled = value as Boolean
true true
} }
tfo = findPreference(Key.tfo) as SwitchPreference tfo = findPreference(Key.tfo)!!
tfo.isChecked = DataStore.tcpFastOpen tfo.isChecked = DataStore.tcpFastOpen
tfo.setOnPreferenceChangeListener { _, value -> tfo.setOnPreferenceChangeListener { _, value ->
if (value as Boolean && !TcpFastOpen.sendEnabled) { if (value as Boolean && !TcpFastOpen.sendEnabled) {
...@@ -183,19 +179,18 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo ...@@ -183,19 +179,18 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
tfo.summary = getString(R.string.tcp_fastopen_summary_unsupported, System.getProperty("os.version")) tfo.summary = getString(R.string.tcp_fastopen_summary_unsupported, System.getProperty("os.version"))
} }
serviceMode = findPreference(Key.serviceMode) hosts = findPreference(Key.hosts)!!
shareOverLan = findPreference(Key.shareOverLan) hosts.summaryProvider = HostsSummaryProvider
portProxy = findPreference(Key.portProxy) serviceMode = findPreference(Key.serviceMode)!!
portLocalDns = findPreference(Key.portLocalDns) shareOverLan = findPreference(Key.shareOverLan)!!
portTransproxy = findPreference(Key.portTransproxy) portProxy = findPreference(Key.portProxy)!!
portProxy.onBindEditTextListener = EditTextPreferenceModifiers.Port
portLocalDns = findPreference(Key.portLocalDns)!!
portLocalDns.onBindEditTextListener = EditTextPreferenceModifiers.Port
portTransproxy = findPreference(Key.portTransproxy)!!
portTransproxy.onBindEditTextListener = EditTextPreferenceModifiers.Port
serviceMode.onPreferenceChangeListener = onServiceModeChange serviceMode.onPreferenceChangeListener = onServiceModeChange
findPreference(Key.about).apply { findPreference<Preference>(Key.about)!!.summary = getString(R.string.about_title, BuildConfig.VERSION_NAME)
summary = getString(R.string.about_title, BuildConfig.VERSION_NAME)
setOnPreferenceClickListener {
Toast.makeText(requireContext(), "https://shadowsocks.org/android", Toast.LENGTH_SHORT).show()
true
}
}
tester = ViewModelProviders.of(this).get() tester = ViewModelProviders.of(this).get()
changeState(BaseService.State.Idle) // reset everything to init state changeState(BaseService.State.Idle) // reset everything to init state
...@@ -233,7 +228,7 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo ...@@ -233,7 +228,7 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
} }
} }
override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String?) { override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String) {
when (key) { when (key) {
Key.serviceMode -> handler.post { Key.serviceMode -> handler.post {
connection.disconnect(requireContext()) connection.disconnect(requireContext())
...@@ -271,15 +266,25 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo ...@@ -271,15 +266,25 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
}, REQUEST_EXPORT_PROFILES) }, REQUEST_EXPORT_PROFILES)
true true
} }
Key.about -> {
Toast.makeText(requireContext(), "https://shadowsocks.org/android", Toast.LENGTH_SHORT).show()
true
}
else -> super.onPreferenceTreeClick(preference) else -> super.onPreferenceTreeClick(preference)
} }
private fun startFilesForResult(intent: Intent, requestCode: Int) { override fun onDisplayPreferenceDialog(preference: Preference?) {
if (preference != hosts || startFilesForResult(Intent(Intent.ACTION_GET_CONTENT).setType("*/*"), REQUEST_HOSTS))
super.onDisplayPreferenceDialog(preference)
}
private fun startFilesForResult(intent: Intent, requestCode: Int): Boolean {
try { try {
startActivityForResult(intent.addCategory(Intent.CATEGORY_OPENABLE), requestCode) startActivityForResult(intent.addCategory(Intent.CATEGORY_OPENABLE), requestCode)
return return false
} catch (_: ActivityNotFoundException) { } catch (_: SecurityException) { } } catch (_: ActivityNotFoundException) { } catch (_: SecurityException) { }
Toast.makeText(requireContext(), R.string.file_manager_missing, Toast.LENGTH_SHORT).show() Toast.makeText(requireContext(), R.string.file_manager_missing, Toast.LENGTH_SHORT).show()
return true
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
...@@ -314,6 +319,16 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo ...@@ -314,6 +319,16 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
Toast.makeText(context, e.readableMessage, Toast.LENGTH_SHORT).show() Toast.makeText(context, e.readableMessage, Toast.LENGTH_SHORT).show()
} }
} }
REQUEST_HOSTS -> {
if (resultCode != Activity.RESULT_OK) return
val context = requireContext()
try {
// we read and persist all its content here to avoid content URL permission issues
hosts.text = context.contentResolver.openInputStream(data!!.data!!)!!.bufferedReader().readText()
} catch (e: RuntimeException) {
Toast.makeText(context, e.readableMessage, Toast.LENGTH_SHORT).show()
}
}
else -> super.onActivityResult(requestCode, resultCode, data) else -> super.onActivityResult(requestCode, resultCode, data)
} }
} }
......
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen <PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android" app:title="@string/app_name">
xmlns:app="http://schemas.android.com/apk/res-auto"
android:title="@string/app_name">
<!-- profileId is stored in privateStore so its value needs to be populated manually --> <!-- profileId is stored in privateStore so its value needs to be populated manually -->
<ListPreference <ListPreference
android:key="profileId" app:key="profileId"
android:title="@string/connect" app:title="@string/connect"
android:summary="%s" app:persistent="false"
android:persistent="false"/> app:useSimpleSummaryProvider="true"/>
<Preference <Preference
android:key="control.stats" app:key="control.stats"
android:title="@string/connection_test_pending" app:title="@string/connection_test_pending"
android:summary="@string/stat_summary"/> app:summary="@string/stat_summary"/>
<Preference <Preference
android:key="control.import" app:key="control.import"
android:title="@string/action_replace_file"/> app:title="@string/action_replace_file"/>
<Preference <Preference
android:key="control.export" app:key="control.export"
android:title="@string/action_export_file"/> app:title="@string/action_export_file"/>
<PreferenceCategory <PreferenceCategory
android:key="settings" app:key="settings"
android:title="@string/settings" app:title="@string/settings"
app:initialExpandedChildrenCount="2"> app:initialExpandedChildrenCount="3">
<SwitchPreference <SwitchPreference
android:key="isAutoConnect" app:key="isAutoConnect"
android:summary="@string/auto_connect_summary" app:summary="@string/auto_connect_summary"
android:title="@string/auto_connect"/> app:title="@string/auto_connect"/>
<SwitchPreference <SwitchPreference
android:key="tcp_fastopen" app:key="tcp_fastopen"
android:summary="@string/tcp_fastopen_summary" app:summary="@string/tcp_fastopen_summary"
android:title="TCP Fast Open"/> app:title="TCP Fast Open"/>
<EditTextPreference
app:key="hosts"
app:title="hosts"/>
<ListPreference <ListPreference
android:key="serviceMode" app:key="serviceMode"
android:entries="@array/service_modes" app:entries="@array/service_modes"
android:entryValues="@array/service_mode_values" app:entryValues="@array/service_mode_values"
android:defaultValue="vpn" app:defaultValue="vpn"
android:summary="%s" app:title="@string/service_mode"
android:title="@string/service_mode"/> app:useSimpleSummaryProvider="true"/>
<SwitchPreference <SwitchPreference
android:key="shareOverLan" app:key="shareOverLan"
android:title="@string/share_over_lan"/> app:title="@string/share_over_lan"/>
<com.takisoft.preferencex.AutoSummaryEditTextPreference <EditTextPreference
android:key="portProxy" app:key="portProxy"
android:inputType="number" app:title="@string/port_proxy"
android:maxLength="5" app:useSimpleSummaryProvider="true"/>
app:pref_summaryHasText="%s" <EditTextPreference
android:singleLine="true" app:key="portLocalDns"
android:title="@string/port_proxy"/> app:title="@string/port_local_dns"
<com.takisoft.preferencex.AutoSummaryEditTextPreference app:useSimpleSummaryProvider="true"/>
android:key="portLocalDns" <EditTextPreference
android:inputType="number" app:key="portTransproxy"
android:maxLength="5" app:title="@string/port_transproxy"
app:pref_summaryHasText="%s" app:useSimpleSummaryProvider="true"/>
android:singleLine="true"
android:title="@string/port_local_dns"/>
<com.takisoft.preferencex.AutoSummaryEditTextPreference
android:key="portTransproxy"
android:inputType="number"
android:maxLength="5"
app:pref_summaryHasText="%s"
android:singleLine="true"
android:title="@string/port_transproxy"/>
</PreferenceCategory> </PreferenceCategory>
<Preference <Preference
android:key="about" app:key="about"
android:title="@string/about"/> app:title="@string/about"/>
</PreferenceScreen> </PreferenceScreen>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment