Commit f6aa874c authored by Mygod's avatar Mygod

Merge branch 'master' into autoconnect+

parents 16e0e3fd 0bd0207b
# Submitting Issues
* Questions and general discussions should go to [Discourse forum](https://discourse.shadowsocks.org/c/implementations/shadowsocks-android);
* 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;
* Always provide information asked in the template unless you know what you're doing;
* Logcat would also be helpful.
* Issues can be submitted in two different places:
1) [The GitHub issue log](https://github.com/shadowsocks/shadowsocks-android/issues)
2) [The Shadowsocks.org Discourse Forum](https://discourse.shadowsocks.org/c/implementations/shadowsocks-android)
* 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 @@
[![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)
[![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>
for Android & Chrome OS ([beta](https://play.google.com/apps/testing/com.github.shadowsocks))
......@@ -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"
```
### 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)
## OPEN SOURCE LICENSES
......@@ -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>
</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
Copyright (C) 2017 by Max Lv <<max.c.lv@gmail.com>>
......@@ -66,4 +75,4 @@ 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/>.
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'
buildscript {
ext {
javaVersion = JavaVersion.VERSION_1_8
kotlinVersion = '1.3.31'
minSdkVersion = 21
sdkVersion = 28
compileSdkVersion = 28
buildToolsVersion = '28.0.3'
preferencexVersion = '1.0.0'
junitVersion = '4.12'
androidTestVersion = '1.1.1'
androidEspressoVersion = '3.1.1'
......@@ -27,11 +27,11 @@ buildscript {
}
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.google.gms:google-services:4.2.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"
}
}
......
......@@ -28,6 +28,11 @@ android {
}
}
compileOptions {
sourceCompatibility javaVersion
targetCompatibility javaVersion
}
externalNativeBuild {
ndkBuild {
path 'src/main/jni/Android.mk'
......@@ -47,19 +52,18 @@ def lifecycleVersion = '2.0.0'
def roomVersion = '2.0.0'
dependencies {
api project(':plugin')
api "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
api "androidx.lifecycle:lifecycle-extensions:$lifecycleVersion"
api "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
api 'androidx.preference:preference:1.0.0'
api 'androidx.preference:preference:1.1.0-alpha05'
api "androidx.room:room-runtime:$roomVersion"
api 'androidx.work:work-runtime-ktx:2.0.1'
api 'com.crashlytics.sdk.android:crashlytics:2.9.9'
api 'com.google.firebase:firebase-config:16.5.0'
api 'com.google.firebase:firebase-core:16.0.8'
api "com.takisoft.preferencex:preferencex:$preferencexVersion"
api 'com.crashlytics.sdk.android:crashlytics:2.10.0'
api 'com.google.firebase:firebase-config:17.0.0'
api 'com.google.firebase:firebase-core:16.0.9'
api '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'
kapt "androidx.lifecycle:lifecycle-compiler:$lifecycleVersion"
kapt "androidx.room:room-compiler:$roomVersion"
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.room:room-testing:$roomVersion"
......
......@@ -55,12 +55,23 @@
android:exported="false">
</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
android:name="com.github.shadowsocks.VpnRequestActivity"
android:theme="@style/Theme.AppCompat.Translucent"
android:excludeFromRecents="true"
android:taskAffinity=""
android:launchMode="singleTask"/>
android:taskAffinity=""/>
<receiver android:name="com.github.shadowsocks.BootReceiver"
android:process=":bg"
......
......@@ -1928,61 +1928,8 @@
(^|\.)goodtv\.com\.tw$
(^|\.)goodtv\.tv$
(^|\.)goofind\.com$
(^|\.)google\.ae$
(^|\.)google\.am$
(^|\.)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$
(^|\.)google(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?){1,2}$
(^|\.)googleapis(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?){1,2}$
(^|\.)googleapps\.com$
(^|\.)googlearth\.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() {
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode != REQUEST_CONNECT) return super.onActivityResult(requestCode, resultCode, data)
if (resultCode == RESULT_OK) Core.startService() else {
Toast.makeText(this, R.string.vpn_permission_denied, Toast.LENGTH_LONG).show()
Crashlytics.log(Log.ERROR, TAG, "Failed to start VpnService from onActivityResult: $data")
......
......@@ -23,10 +23,12 @@ package com.github.shadowsocks.bg
import com.github.shadowsocks.Core.app
import com.github.shadowsocks.acl.Acl
import com.github.shadowsocks.core.R
import com.github.shadowsocks.net.HostsFile
import com.github.shadowsocks.net.LocalDnsServer
import com.github.shadowsocks.net.Socks5Endpoint
import com.github.shadowsocks.net.Subnet
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.Key
import kotlinx.coroutines.CoroutineScope
import java.net.InetSocketAddress
import java.net.URI
......@@ -49,7 +51,8 @@ object LocalDnsService {
val dns = URI("dns://${profile.remoteDns}")
LocalDnsServer(this::resolver,
Socks5Endpoint(dns.host, if (dns.port < 0) 53 else dns.port),
DataStore.proxyAddress).apply {
DataStore.proxyAddress,
HostsFile(DataStore.publicStore.getString(Key.hosts) ?: "")).apply {
tcp = !profile.udpdns
when (profile.route) {
Acl.BYPASS_CHN, Acl.BYPASS_LAN_CHN, Acl.GFWLIST, Acl.CUSTOM_RULES -> {
......
......@@ -31,9 +31,9 @@ import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.plugin.PluginConfiguration
import com.github.shadowsocks.plugin.PluginManager
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.disconnectFromMain
import com.github.shadowsocks.utils.parseNumericAddress
import com.github.shadowsocks.utils.signaturesCompat
import com.github.shadowsocks.utils.useCancellable
import kotlinx.coroutines.*
import java.io.File
import java.net.HttpURLConnection
......@@ -62,15 +62,11 @@ class ProxyInstance(val profile: Profile, private val route: String = profile.ro
conn.requestMethod = "POST"
conn.doOutput = true
val proxies = try {
withContext(Dispatchers.IO) {
conn.outputStream.bufferedWriter().use {
it.write("sig=" + Base64.encodeToString(mdg.digest(), Base64.DEFAULT))
}
conn.inputStream.bufferedReader().readText()
val proxies = conn.useCancellable {
outputStream.bufferedWriter().use {
it.write("sig=" + Base64.encodeToString(mdg.digest(), Base64.DEFAULT))
}
} finally {
conn.disconnectFromMain()
inputStream.bufferedReader().readText()
}.split('|').toMutableList()
proxies.shuffle()
val proxy = proxies.first().split(':')
......
......@@ -87,7 +87,7 @@ data class Profile(
if (match != null) {
val profile = Profile()
feature?.copyFeatureSettingsTo(profile)
profile.method = match.groupValues[1].toLowerCase()
profile.method = match.groupValues[1].toLowerCase(Locale.ENGLISH)
profile.password = match.groupValues[2]
profile.host = match.groupValues[3]
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
import com.github.shadowsocks.core.R
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.disconnectFromMain
import kotlinx.coroutines.*
import com.github.shadowsocks.utils.useCancellable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import java.io.IOException
import java.net.HttpURLConnection
import java.net.Proxy
......@@ -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 }
fun testConnection() {
......@@ -91,26 +94,25 @@ class HttpsTest : ViewModel() {
conn.setRequestProperty("Connection", "close")
conn.instanceFollowRedirects = false
conn.useCaches = false
running = conn to GlobalScope.launch(Dispatchers.Main.immediate) {
status.value = withContext(Dispatchers.IO) {
running = GlobalScope.launch(Dispatchers.Main.immediate) {
status.value = conn.useCancellable {
try {
val start = SystemClock.elapsedRealtime()
val code = conn.responseCode
val code = responseCode
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)
} catch (e: IOException) {
Status.Error.IOFailure(e)
} finally {
conn.disconnect()
disconnect()
}
}
}
}
private fun cancelTest() = running?.let { (conn, job) ->
job.cancel() // ensure job is cancelled before interrupting
conn.disconnectFromMain()
private fun cancelTest() {
running?.cancel()
running = null
}
......
......@@ -43,7 +43,9 @@ import java.nio.channels.SocketChannel
* https://github.com/shadowsocks/overture/tree/874f22613c334a3b78e40155a55479b7b69fee04
*/
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.
*/
......@@ -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())
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()
override val coroutineContext = SupervisorJob() + CoroutineExceptionHandler { _, t -> printLog(t) }
......@@ -101,10 +114,16 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAd
return supervisorScope {
val remote = async { withTimeout(TIMEOUT) { forward(packet) } }
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
if (question?.type != Type.A) return@supervisorScope remote.await()
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()
val localResults = try {
withTimeout(TIMEOUT) { GlobalScope.async(Dispatchers.IO) { localResolver(host) }.await() }
......@@ -117,14 +136,7 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAd
if (localResults.isEmpty()) return@supervisorScope remote.await()
if (localIpMatcher.isEmpty() || localIpMatcher.any { subnet -> localResults.any(subnet::matches) }) {
remote.cancel()
ByteBuffer.wrap(prepareDnsResponse(request).apply {
header.setFlag(Flags.RA.toInt()) // recursion available
for (address in localResults) addRecord(when (address) {
is Inet4Address -> ARecord(question.name, DClass.IN, TTL, address)
is Inet6Address -> AAAARecord(question.name, DClass.IN, TTL, address)
else -> throw IllegalStateException("Unsupported address $address")
}, Section.ANSWER)
}.toWire())
cookDnsResponse(request, localResults.asIterable())
} else remote.await()
} catch (e: Exception) {
remote.cancel()
......
......@@ -43,7 +43,7 @@ object DataStore : OnPreferenceDataStoreChangeListener {
publicStore.registerChangeListener(this)
}
override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String?) {
override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String) {
when (key) {
Key.id -> if (directBootAware) DirectBoot.update()
}
......
/*******************************************************************************
* *
* 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
import androidx.preference.PreferenceDataStore
interface OnPreferenceDataStoreChangeListener {
fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String?)
fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String)
}
......@@ -65,6 +65,7 @@ object Key {
const val dirty = "profileDirty"
const val tfo = "tcp_fastopen"
const val hosts = "hosts"
const val assetUpdateTime = "assetUpdateTime"
// TV specific values
......
/*******************************************************************************
* *
* 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
import androidx.annotation.AttrRes
import androidx.preference.Preference
import com.crashlytics.android.Crashlytics
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.*
import java.net.HttpURLConnection
import java.net.InetAddress
import kotlin.coroutines.resume
val Throwable.readableMessage get() = localizedMessage ?: javaClass.name
......@@ -54,11 +53,19 @@ private val parseNumericAddress by lazy {
*
* Bug: https://issuetracker.google.com/issues/123456213
*/
fun String?.parseNumericAddress(): InetAddress? = Os.inet_pton(OsConstants.AF_INET, this)
fun String.parseNumericAddress(): InetAddress? = Os.inet_pton(OsConstants.AF_INET, this)
?: Os.inet_pton(OsConstants.AF_INET6, this)?.let { parseNumericAddress.invoke(null, this) as InetAddress }
fun HttpURLConnection.disconnectFromMain() {
if (Build.VERSION.SDK_INT >= 26) disconnect() else GlobalScope.launch(Dispatchers.IO) { disconnect() }
fun <K, V> MutableMap<K, V>.computeIfAbsentCompat(key: K, value: () -> V) = if (Build.VERSION.SDK_INT >= 24)
computeIfAbsent(key) { value() } else this[key] ?: value().also { put(key, it) }
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 {
......
......@@ -59,6 +59,10 @@
<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_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_summary">Requires UDP forwarding on server side</string>
<string name="udp_fallback">UDP Fallback</string>
......@@ -80,7 +84,6 @@
<!-- alert category -->
<string name="profile_empty">Please select a profile</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>
<!-- menu category -->
......@@ -109,7 +112,7 @@
<string name="profile_config">Profile config</string>
<string name="delete">Remove</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_methods_scan_qr_code">Scan QR code</string>
<string name="add_profile_methods_manual_settings">Manual Settings</string>
......
......@@ -11,7 +11,7 @@
# The setting is particularly useful for tweaking memory settings.
android.enableJetifier=true
android.enableR8=true
# android.enableR8.fullMode=true
android.enableR8.fullMode=true
android.useAndroidX=true
# When configured, Gradle will run in incubating parallel mode.
......
distributionBase=GRADLE_USER_HOME
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
zipStorePath=wrapper/dists
......@@ -39,6 +39,10 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility javaVersion
targetCompatibility javaVersion
}
packagingOptions.exclude '**/*.kotlin_*'
splits {
abi {
......@@ -56,11 +60,11 @@ androidExtensions {
dependencies {
implementation project(':core')
implementation "androidx.browser:browser:1.0.0"
implementation 'androidx.browser:browser:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'com.google.android.gms:play-services-vision:17.0.2'
implementation 'com.google.firebase:firebase-ads:17.2.0'
implementation "com.takisoft.preferencex:preferencex-simplemenu:$preferencexVersion"
implementation 'com.takisoft.preferencex:preferencex-simplemenu:1.0.0'
implementation 'com.twofortyfouram:android-plugin-api-for-locale:1.0.4'
implementation 'net.glxn.qrgen:android:2.0'
implementation 'xyz.belvi.mobilevision:barcodescanner:2.0.3'
......
......@@ -3,13 +3,10 @@
package="com.github.shadowsocks"
tools:ignore="MissingLeanbackSupport">
<uses-permission android:name="android.permission.NFC" />
<uses-permission-sdk-23 android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.touchscreen"
android:required="false"/>
<uses-feature android:name="android.hardware.nfc"
android:required="false"/>
<uses-feature android:name="android.hardware.camera"
android:required="false"/>
......@@ -21,7 +18,7 @@
android:name=".MainActivity"
android:label="@string/app_name"
android:theme="@style/Theme.Shadowsocks.Navigation"
android:launchMode="singleTask">
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
......@@ -30,17 +27,6 @@
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
</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"
android:resource="@xml/shortcuts"/>
</activity>
......@@ -48,22 +34,19 @@
<activity
android:name=".ProfileConfigActivity"
android:excludeFromRecents="true"
android:label="@string/profile_config"
android:launchMode="singleTask"/>
android:label="@string/profile_config"/>
<activity
android:name=".AppManager"
android:label="@string/proxied_apps"
android:parentActivityName=".ProfileConfigActivity"
android:excludeFromRecents="true"
android:launchMode="singleTask"/>
android:excludeFromRecents="true"/>
<activity
android:name=".UdpFallbackProfileActivity"
android:label="@string/udp_fallback"
android:parentActivityName=".ProfileConfigActivity"
android:excludeFromRecents="true"
android:launchMode="singleTask"/>
android:excludeFromRecents="true"/>
<activity
android:name=".ScannerActivity"
......@@ -84,8 +67,7 @@
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:excludeFromRecents="true"
android:taskAffinity=""
android:process=":bg"
android:launchMode="singleTask">
android:process=":bg">
<intent-filter>
<action android:name="android.intent.action.CREATE_SHORTCUT" />
</intent-filter>
......
......@@ -23,17 +23,12 @@ package com.github.shadowsocks
import android.app.Application
import android.content.res.Configuration
import androidx.appcompat.app.AppCompatDelegate
import com.github.shadowsocks.preference.BottomSheetPreferenceDialogFragment
import com.github.shadowsocks.preference.IconListPreference
import com.takisoft.preferencex.PreferenceFragmentCompat
class App : Application() {
override fun onCreate() {
super.onCreate()
Core.init(this, MainActivity::class)
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
PreferenceFragmentCompat.registerPreferenceFragment(IconListPreference::class.java,
BottomSheetPreferenceDialogFragment::class.java)
}
override fun onConfigurationChanged(newConfig: Configuration) {
......
......@@ -49,6 +49,7 @@ import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.DirectBoot
import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.SingleInstanceActivity
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.layout_apps.*
import kotlinx.android.synthetic.main.layout_apps_item.view.*
......@@ -208,6 +209,7 @@ class AppManager : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
SingleInstanceActivity.register(this) ?: return
setContentView(R.layout.layout_apps)
setSupportActionBar(toolbar)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
......
......@@ -20,35 +20,48 @@
package com.github.shadowsocks
import android.app.Activity
import android.content.Intent
import android.os.Build
import android.os.Bundle
import androidx.preference.EditTextPreference
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreference
import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.DirectBoot
import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.net.TcpFastOpen
import com.github.shadowsocks.preference.BrowsableEditTextPreferenceDialogFragment
import com.github.shadowsocks.preference.EditTextPreferenceModifiers
import com.github.shadowsocks.preference.HostsSummaryProvider
import com.github.shadowsocks.utils.readableMessage
import com.github.shadowsocks.utils.remove
import com.takisoft.preferencex.PreferenceFragmentCompat
class GlobalSettingsPreferenceFragment : PreferenceFragmentCompat() {
override fun onCreatePreferencesFix(savedInstanceState: Bundle?, rootKey: String?) {
companion object {
private const val REQUEST_BROWSE = 1
}
private val hosts by lazy { findPreference<EditTextPreference>(Key.hosts)!! }
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
preferenceManager.preferenceDataStore = DataStore.publicStore
DataStore.initGlobal()
addPreferencesFromResource(R.xml.pref_global)
(findPreference(Key.persistAcrossReboot) as SwitchPreference).setOnPreferenceChangeListener { _, value ->
findPreference<SwitchPreference>(Key.persistAcrossReboot)!!.setOnPreferenceChangeListener { _, value ->
BootReceiver.enabled = value as Boolean
true
}
val canToggleLocked = findPreference(Key.directBootAware)
val canToggleLocked = findPreference<Preference>(Key.directBootAware)!!
if (Build.VERSION.SDK_INT >= 24) canToggleLocked.setOnPreferenceChangeListener { _, newValue ->
if (Core.directBootSupported && newValue as Boolean) DirectBoot.update() else DirectBoot.clean()
true
} else canToggleLocked.remove()
val tfo = findPreference(Key.tfo) as SwitchPreference
val tfo = findPreference<SwitchPreference>(Key.tfo)!!
tfo.isChecked = DataStore.tcpFastOpen
tfo.setOnPreferenceChangeListener { _, value ->
if (value as Boolean && !TcpFastOpen.sendEnabled) {
......@@ -65,10 +78,15 @@ class GlobalSettingsPreferenceFragment : PreferenceFragmentCompat() {
tfo.summary = getString(R.string.tcp_fastopen_summary_unsupported, System.getProperty("os.version"))
}
val serviceMode = findPreference(Key.serviceMode)
val portProxy = findPreference(Key.portProxy)
val portLocalDns = findPreference(Key.portLocalDns)
val portTransproxy = findPreference(Key.portTransproxy)
hosts.onBindEditTextListener = EditTextPreferenceModifiers.Monospace
hosts.summaryProvider = HostsSummaryProvider
val serviceMode = findPreference<Preference>(Key.serviceMode)!!
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 (enabledLocalDns, enabledTransproxy) = when (newValue as String?) {
Key.modeProxy -> Pair(false, false)
......@@ -76,20 +94,18 @@ class GlobalSettingsPreferenceFragment : PreferenceFragmentCompat() {
Key.modeTransproxy -> Pair(true, true)
else -> throw IllegalArgumentException("newValue: $newValue")
}
hosts.isEnabled = enabledLocalDns
portLocalDns.isEnabled = enabledLocalDns
portTransproxy.isEnabled = enabledTransproxy
true
}
val listener: (BaseService.State) -> Unit = {
if (it == BaseService.State.Stopped) {
tfo.isEnabled = true
serviceMode.isEnabled = true
portProxy.isEnabled = true
onServiceModeChange.onPreferenceChange(null, DataStore.serviceMode)
} else {
tfo.isEnabled = false
serviceMode.isEnabled = false
portProxy.isEnabled = false
val stopped = it == BaseService.State.Stopped
tfo.isEnabled = stopped
serviceMode.isEnabled = stopped
portProxy.isEnabled = stopped
if (stopped) onServiceModeChange.onPreferenceChange(null, DataStore.serviceMode) else {
hosts.isEnabled = false
portLocalDns.isEnabled = false
portTransproxy.isEnabled = false
}
......@@ -99,6 +115,29 @@ class GlobalSettingsPreferenceFragment : PreferenceFragmentCompat() {
serviceMode.onPreferenceChangeListener = onServiceModeChange
}
override fun onDisplayPreferenceDialog(preference: Preference?) {
if (preference == hosts) BrowsableEditTextPreferenceDialogFragment().apply {
setKey(hosts.key)
setTargetFragment(this@GlobalSettingsPreferenceFragment, REQUEST_BROWSE)
}.show(fragmentManager ?: return, hosts.key) else super.onDisplayPreferenceDialog(preference)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) {
REQUEST_BROWSE -> {
if (resultCode != Activity.RESULT_OK) return
val activity = activity as MainActivity
try {
// we read and persist all its content here to avoid content URL permission issues
hosts.text = activity.contentResolver.openInputStream(data!!.data!!)!!.bufferedReader().readText()
} catch (e: RuntimeException) {
activity.snackbar(e.readableMessage).show()
}
}
else -> super.onActivityResult(requestCode, resultCode, data)
}
}
override fun onDestroy() {
MainActivity.stateListener = null
super.onDestroy()
......
......@@ -23,20 +23,15 @@ package com.github.shadowsocks
import android.app.Activity
import android.app.backup.BackupManager
import android.content.ActivityNotFoundException
import android.content.DialogInterface
import android.content.Intent
import android.net.VpnService
import android.nfc.NdefMessage
import android.nfc.NfcAdapter
import android.os.Bundle
import android.os.DeadObjectException
import android.os.Handler
import android.os.Parcelable
import android.util.Log
import android.view.KeyCharacterMap
import android.view.KeyEvent
import android.view.MenuItem
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.browser.customtabs.CustomTabsIntent
import androidx.coordinatorlayout.widget.CoordinatorLayout
......@@ -51,18 +46,14 @@ import com.github.shadowsocks.aidl.IShadowsocksService
import com.github.shadowsocks.aidl.ShadowsocksConnection
import com.github.shadowsocks.aidl.TrafficStats
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.OnPreferenceDataStoreChangeListener
import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.SingleInstanceActivity
import com.github.shadowsocks.widget.ServiceButton
import com.github.shadowsocks.widget.StatsBar
import com.google.android.material.navigation.NavigationView
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.parcel.Parcelize
class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPreferenceDataStoreChangeListener,
NavigationView.OnNavigationItemSelectedListener {
......@@ -73,17 +64,6 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
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
private lateinit var fab: ServiceButton
private lateinit var stats: StatsBar
......@@ -92,7 +72,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
val snackbar by lazy { findViewById<CoordinatorLayout>(R.id.snackbar) }
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 {
......@@ -167,6 +147,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
SingleInstanceActivity.register(this) ?: return
setContentView(R.layout.layout_main)
stats = findViewById(R.id.stats)
stats.setOnClickListener { if (state == BaseService.State.Connected) stats.testConnection() }
......@@ -184,32 +165,9 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
changeState(BaseService.State.Idle) // reset everything to init state
connection.connect(this, 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) {
Key.serviceMode -> handler.post {
connection.disconnect(this)
......
......@@ -32,6 +32,7 @@ import com.github.shadowsocks.plugin.AlertDialogFragment
import com.github.shadowsocks.plugin.Empty
import com.github.shadowsocks.plugin.PluginContract
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.SingleInstanceActivity
class ProfileConfigActivity : AppCompatActivity() {
companion object {
......@@ -51,6 +52,7 @@ class ProfileConfigActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
SingleInstanceActivity.register(this) ?: return
setContentView(R.layout.layout_profile_config)
setSupportActionBar(findViewById(R.id.toolbar))
supportActionBar!!.apply {
......
......@@ -29,27 +29,21 @@ import android.os.Bundle
import android.os.Parcelable
import android.view.MenuItem
import androidx.appcompat.app.AlertDialog
import androidx.core.os.bundleOf
import androidx.preference.Preference
import androidx.preference.PreferenceDataStore
import androidx.preference.SwitchPreference
import androidx.preference.*
import com.github.shadowsocks.Core.app
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.plugin.*
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.preference.IconListPreference
import com.github.shadowsocks.preference.OnPreferenceDataStoreChangeListener
import com.github.shadowsocks.preference.PluginConfigurationDialogFragment
import com.github.shadowsocks.preference.*
import com.github.shadowsocks.utils.*
import com.google.android.material.snackbar.Snackbar
import com.takisoft.preferencex.EditTextPreference
import com.takisoft.preferencex.PreferenceFragmentCompat
import kotlinx.android.parcel.Parcelize
class ProfileConfigFragment : PreferenceFragmentCompat(),
Preference.OnPreferenceChangeListener, OnPreferenceDataStoreChangeListener {
companion object {
companion object PasswordSummaryProvider : Preference.SummaryProvider<EditTextPreference> {
override fun provideSummary(preference: EditTextPreference?) = "\u2022".repeat(preference?.text?.length ?: 0)
private const val REQUEST_CODE_PLUGIN_CONFIGURE = 1
const val REQUEST_UNSAVED_CHANGES = 2
}
......@@ -75,27 +69,29 @@ class ProfileConfigFragment : PreferenceFragmentCompat(),
private lateinit var receiver: BroadcastReceiver
private lateinit var udpFallback: Preference
override fun onCreatePreferencesFix(savedInstanceState: Bundle?, rootKey: String?) {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
preferenceManager.preferenceDataStore = DataStore.privateStore
val activity = requireActivity()
profileId = activity.intent.getLongExtra(Action.EXTRA_PROFILE_ID, -1L)
addPreferencesFromResource(R.xml.pref_profile)
findPreference<EditTextPreference>(Key.remotePort)!!.onBindEditTextListener = EditTextPreferenceModifiers.Port
findPreference<EditTextPreference>(Key.password)!!.summaryProvider = PasswordSummaryProvider
val serviceMode = DataStore.serviceMode
findPreference(Key.remoteDns).isEnabled = serviceMode != Key.modeProxy
findPreference(Key.ipv6)!!.isEnabled = serviceMode == Key.modeVpn
isProxyApps = findPreference(Key.proxyApps) as SwitchPreference
findPreference<Preference>(Key.remoteDns)!!.isEnabled = serviceMode != Key.modeProxy
findPreference<Preference>(Key.ipv6)!!.isEnabled = serviceMode == Key.modeVpn
isProxyApps = findPreference(Key.proxyApps)!!
isProxyApps.isEnabled = serviceMode == Key.modeVpn
isProxyApps.setOnPreferenceClickListener {
startActivity(Intent(activity, AppManager::class.java))
isProxyApps.isChecked = true
false
}
findPreference(Key.metered)!!.apply {
findPreference<Preference>(Key.metered)!!.apply {
if (Build.VERSION.SDK_INT >= 28) isEnabled = serviceMode == Key.modeVpn else remove()
}
findPreference(Key.udpdns).isEnabled = serviceMode != Key.modeProxy
plugin = findPreference(Key.plugin) as IconListPreference
pluginConfigure = findPreference(Key.pluginConfigure) as EditTextPreference
findPreference<Preference>(Key.udpdns)!!.isEnabled = serviceMode != Key.modeProxy
plugin = findPreference(Key.plugin)!!
pluginConfigure = findPreference(Key.pluginConfigure)!!
plugin.unknownValueSummary = getString(R.string.plugin_unknown)
plugin.setOnPreferenceChangeListener { _, newValue ->
pluginConfiguration = PluginConfiguration(pluginConfiguration.pluginsOptions, newValue as String)
......@@ -108,10 +104,11 @@ class ProfileConfigFragment : PreferenceFragmentCompat(),
}
true
}
pluginConfigure.onBindEditTextListener = EditTextPreferenceModifiers.Monospace
pluginConfigure.onPreferenceChangeListener = this
initPlugins()
receiver = Core.listenForPackageChanges(false) { initPlugins() }
udpFallback = findPreference(Key.udpFallback)
udpFallback = findPreference(Key.udpFallback)!!
DataStore.privateStore.registerChangeListener(this)
}
......@@ -124,14 +121,16 @@ class ProfileConfigFragment : PreferenceFragmentCompat(),
pluginConfiguration = PluginConfiguration(DataStore.plugin)
plugin.value = pluginConfiguration.selected
plugin.init()
plugin.checkSummary()
pluginConfigure.isEnabled = pluginConfiguration.selected.isNotEmpty()
pluginConfigure.text = pluginConfiguration.selectedOptions.toString()
}
private fun showPluginEditor() = displayPreferenceDialog(PluginConfigurationDialogFragment(), Key.pluginConfigure,
bundleOf(Pair("key", Key.pluginConfigure),
Pair(PluginConfigurationDialogFragment.PLUGIN_ID_FRAGMENT_TAG, pluginConfiguration.selected)))
private fun showPluginEditor() {
PluginConfigurationDialogFragment().apply {
setArg(Key.pluginConfigure, pluginConfiguration.selected)
setTargetFragment(this@ProfileConfigFragment, 0)
}.show(fragmentManager ?: return, Key.pluginConfigure)
}
private fun saveAndExit() {
val profile = ProfileManager.getProfile(profileId) ?: Profile()
......@@ -163,18 +162,26 @@ class ProfileConfigFragment : PreferenceFragmentCompat(),
false
}
override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String?) {
if (key != Key.proxyApps && findPreference(key) != null) DataStore.dirty = true
override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String) {
if (key != Key.proxyApps && findPreference<Preference>(key) != null) DataStore.dirty = true
}
override fun onDisplayPreferenceDialog(preference: Preference) {
if (preference.key == Key.pluginConfigure) {
val intent = PluginManager.buildIntent(pluginConfiguration.selected, PluginContract.ACTION_CONFIGURE)
if (intent.resolveActivity(requireContext().packageManager) == null) showPluginEditor() else
startActivityForResult(intent
.putExtra(PluginContract.EXTRA_OPTIONS, pluginConfiguration.selectedOptions.toString()),
REQUEST_CODE_PLUGIN_CONFIGURE)
} else super.onDisplayPreferenceDialog(preference)
when (preference.key) {
Key.plugin -> BottomSheetPreferenceDialogFragment().apply {
setArg(Key.plugin)
setTargetFragment(this@ProfileConfigFragment, 0)
}.show(fragmentManager ?: return, Key.plugin)
Key.pluginConfigure -> {
val intent = PluginManager.buildIntent(pluginConfiguration.selected, PluginContract.ACTION_CONFIGURE)
if (intent.resolveActivity(requireContext().packageManager) == null) showPluginEditor() else {
startActivityForResult(intent
.putExtra(PluginContract.EXTRA_OPTIONS, pluginConfiguration.selectedOptions.toString()),
REQUEST_CODE_PLUGIN_CONFIGURE)
}
}
else -> super.onDisplayPreferenceDialog(preference)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
......
......@@ -23,9 +23,6 @@ package com.github.shadowsocks
import android.annotation.SuppressLint
import android.app.Activity
import android.content.*
import android.nfc.NdefMessage
import android.nfc.NdefRecord
import android.nfc.NfcAdapter
import android.os.Bundle
import android.text.format.Formatter
import android.util.LongSparseArray
......@@ -82,33 +79,13 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
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? {
val image = ImageView(context)
image.layoutParams = LinearLayout.LayoutParams(-1, -1)
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
}
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),
......@@ -202,7 +179,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
}
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()), "")
.commitAllowingStateLoss()
true
......
......@@ -35,6 +35,7 @@ import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.plugin.PluginConfiguration
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.SingleInstanceActivity
import com.github.shadowsocks.utils.resolveResourceId
class UdpFallbackProfileActivity : AppCompatActivity() {
......@@ -77,11 +78,12 @@ class UdpFallbackProfileActivity : AppCompatActivity() {
private val profilesAdapter = ProfilesAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (editingId == null) {
finish()
return
}
super.onCreate(savedInstanceState)
SingleInstanceActivity.register(this) ?: return
setContentView(R.layout.layout_udp_fallback)
val toolbar = findViewById<Toolbar>(R.id.toolbar)
......
......@@ -32,6 +32,7 @@ import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.preference.PreferenceDialogFragmentCompat
import androidx.recyclerview.widget.DefaultItemAnimator
......@@ -97,6 +98,10 @@ class BottomSheetPreferenceDialogFragment : PreferenceDialogFragmentCompat() {
}
}
fun setArg(key: String) {
arguments = bundleOf(PreferenceDialogFragmentCompat.ARG_KEY to key)
}
private val preference by lazy { getPreference() as IconListPreference }
private var clickedIndex = -1
......
/*******************************************************************************
* *
* 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
import androidx.preference.ListPreference
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
val selectedEntry: Int get() = entryValues.indexOf(value)
val entryIcon: Drawable? get() = entryIcons?.getOrNull(selectedEntry)
......@@ -49,7 +57,6 @@ class IconListPreference(context: Context, attrs: AttributeSet? = null) : ListPr
val listener = listener
if (listener == null || listener.onPreferenceChange(preference, newValue)) {
value = newValue.toString()
checkSummary()
if (entryIcons != null) icon = entryIcon
true
} else false
......@@ -60,13 +67,9 @@ class IconListPreference(context: Context, attrs: AttributeSet? = null) : ListPr
// a.recycle()
}
fun checkSummary() {
val unknownValueSummary = unknownValueSummary
if (unknownValueSummary != null) summary = if (selectedEntry < 0) unknownValueSummary.format(value) else "%s"
}
fun init() {
icon = entryIcon
summaryProvider = FallbackProvider
}
override fun onSetInitialValue(defaultValue: Any?) {
super.onSetInitialValue(defaultValue)
......
......@@ -23,17 +23,23 @@ package com.github.shadowsocks.preference
import android.view.View
import android.widget.EditText
import androidx.appcompat.app.AlertDialog
import androidx.core.os.bundleOf
import androidx.preference.EditTextPreferenceDialogFragmentCompat
import androidx.preference.PreferenceDialogFragmentCompat
import com.github.shadowsocks.ProfileConfigActivity
import com.github.shadowsocks.plugin.PluginContract
import com.github.shadowsocks.plugin.PluginManager
import com.takisoft.preferencex.EditTextPreferenceDialogFragmentCompat
class PluginConfigurationDialogFragment : EditTextPreferenceDialogFragmentCompat() {
companion object {
const val PLUGIN_ID_FRAGMENT_TAG =
private const val PLUGIN_ID_FRAGMENT_TAG =
"com.github.shadowsocks.preference.PluginConfigurationDialogFragment.PLUGIN_ID"
}
fun setArg(key: String, plugin: String) {
arguments = bundleOf(PreferenceDialogFragmentCompat.ARG_KEY to key, PLUGIN_ID_FRAGMENT_TAG to plugin)
}
private lateinit var editText: EditText
override fun onPrepareDialogBuilder(builder: AlertDialog.Builder) {
......
......@@ -77,14 +77,9 @@ class ServiceButton @JvmOverloads constructor(context: Context, attrs: Attribute
}
else -> changeState(iconStopped, animate)
}
if (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))
}
checked = state == BaseService.State.Connected
refreshDrawableState()
TooltipCompat.setTooltipText(this, context.getString(if (state.canStop) R.string.stop else R.string.connect))
isEnabled = state.canStop || state == BaseService.State.Stopped
}
......
......@@ -48,10 +48,11 @@ class StatsBar @JvmOverloads constructor(context: Context, attrs: AttributeSet?
private val behavior = object : Behavior() {
val threshold = context.resources.getDimensionPixelSize(R.dimen.stats_bar_scroll_threshold)
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
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
......
......@@ -7,7 +7,7 @@
android:layout_height="match_parent"
android:orientation="vertical"
android:duplicateParentState="false">
<androidx.appcompat.widget.Toolbar
<com.google.android.material.appbar.MaterialToolbar
android:layout_height="?attr/actionBarSize"
android:layout_width="match_parent"
android:background="?attr/colorPrimary"
......@@ -25,7 +25,7 @@
android:iconifiedByDefault="false"
android:queryHint="@android:string/search_go"/>
</androidx.appcompat.widget.Toolbar>
</com.google.android.material.appbar.MaterialToolbar>
<androidx.constraintlayout.widget.ConstraintLayout
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"?>
<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"/>
<item android:id="@+id/action_export_clipboard"
android:title="@string/action_export"/>
......
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
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"/>
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto"
app:initialExpandedChildrenCount="4">
<SwitchPreference
android:key="directBootAware"
android:icon="@drawable/ic_action_lock"
android:summary="@string/direct_boot_aware_summary"
android:title="@string/direct_boot_aware"/>
app:key="isAutoConnect"
app:summary="@string/auto_connect_summary"
app:title="@string/auto_connect"/>
<SwitchPreference
android:key="tcp_fastopen"
android:icon="@drawable/ic_action_offline_bolt"
android:summary="@string/tcp_fastopen_summary"
android:title="TCP Fast Open"/>
app:key="directBootAware"
app:icon="@drawable/ic_action_lock"
app:summary="@string/direct_boot_aware_summary"
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
android:key="serviceMode"
android:entries="@array/service_modes"
android:entryValues="@array/service_mode_values"
android:defaultValue="vpn"
android:summary="%s"
android:title="@string/service_mode"/>
<AutoSummaryEditTextPreference
android:key="portProxy"
android:inputType="number"
android:maxLength="5"
app:pref_summaryHasText="%s"
android:singleLine="true"
android:title="@string/port_proxy"/>
<AutoSummaryEditTextPreference
android:key="portLocalDns"
android:icon="@drawable/ic_action_dns"
android:inputType="number"
android:maxLength="5"
app:pref_summaryHasText="%s"
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"/>
<com.takisoft.preferencex.SimpleMenuPreference
app:key="serviceMode"
app:entries="@array/service_modes"
app:entryValues="@array/service_mode_values"
app:defaultValue="vpn"
app:title="@string/service_mode"
app:useSimpleSummaryProvider="true"/>
<EditTextPreference
app:key="portProxy"
app:title="@string/port_proxy"
app:useSimpleSummaryProvider="true"/>
<EditTextPreference
app:key="portLocalDns"
app:icon="@drawable/ic_action_dns"
app:title="@string/port_local_dns"
app:useSimpleSummaryProvider="true"/>
<EditTextPreference
app:key="portTransproxy"
app:title="@string/port_transproxy"
app:useSimpleSummaryProvider="true"/>
</PreferenceScreen>
......@@ -2,99 +2,93 @@
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<AutoSummaryEditTextPreference
android:key="profileName"
app:pref_summaryHasText="%s"
android:title="@string/profile_name"/>
<EditTextPreference
app:key="profileName"
app:title="@string/profile_name"
app:useSimpleSummaryProvider="true"/>
<PreferenceCategory
android:title="@string/proxy_cat">
app:title="@string/proxy_cat">
<AutoSummaryEditTextPreference
android:key="proxy"
app:pref_summaryHasText="%s"
android:singleLine="true"
android:title="@string/proxy"/>
<AutoSummaryEditTextPreference
android:key="remotePortNum"
android:inputType="number"
android:maxLength="5"
app:pref_summaryHasText="%s"
android:singleLine="true"
android:title="@string/remote_port"/>
<AutoSummaryEditTextPreference
android:inputType="textPassword"
android:typeface="monospace"
app:pref_summaryPasswordSubstituteLength="0"
android:key="sitekey"
app:pref_summaryHasText="%s"
android:singleLine="true"
android:title="@string/sitekey"/>
<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"/>
<EditTextPreference
app:key="proxy"
app:singleLine="true"
app:title="@string/proxy"
app:useSimpleSummaryProvider="true"/>
<EditTextPreference
app:key="remotePortNum"
app:title="@string/remote_port"
app:useSimpleSummaryProvider="true"/>
<EditTextPreference
app:dialogLayout="@layout/preference_dialog_password"
app:key="sitekey"
app:title="@string/sitekey"/>
<com.takisoft.preferencex.SimpleMenuPreference
app:key="encMethod"
app:icon="@drawable/ic_notification_enhanced_encryption"
app:entries="@array/enc_method_entry"
app:entryValues="@array/enc_method_value"
app:title="@string/enc_method"
app:useSimpleSummaryProvider="true"/>
</PreferenceCategory>
<PreferenceCategory
android:title="@string/feature_cat">
app:title="@string/feature_cat">
<SimpleMenuPreference
android:key="route"
android:entries="@array/route_entry"
android:entryValues="@array/route_value"
android:summary="%s"
android:title="@string/route_list"/>
<com.takisoft.preferencex.SimpleMenuPreference
app:key="route"
app:entries="@array/route_entry"
app:entryValues="@array/route_value"
app:title="@string/route_list"
app:useSimpleSummaryProvider="true"/>
<SwitchPreference
android:key="isIpv6"
android:icon="@drawable/ic_image_looks_6"
android:summary="@string/ipv6_summary"
android:title="@string/ipv6"/>
app:key="isIpv6"
app:icon="@drawable/ic_image_looks_6"
app:summary="@string/ipv6_summary"
app:title="@string/ipv6"/>
<SwitchPreference
android:key="isProxyApps"
android:icon="@drawable/ic_navigation_apps"
android:summary="@string/proxied_apps_summary"
android:title="@string/proxied_apps"/>
app:key="isProxyApps"
app:icon="@drawable/ic_navigation_apps"
app:summary="@string/proxied_apps_summary"
app:title="@string/proxied_apps"/>
<SwitchPreference
android:key="metered"
android:icon="@drawable/ic_device_data_usage"
android:summary="@string/metered_summary"
android:title="@string/metered"/>
<AutoSummaryEditTextPreference
android:key="remoteDns"
android:icon="@drawable/ic_action_dns"
android:title="@string/remote_dns"
app:pref_summaryHasText="%s"/>
app:key="metered"
app:icon="@drawable/ic_device_data_usage"
app:summary="@string/metered_summary"
app:title="@string/metered"/>
<EditTextPreference
app:key="remoteDns"
app:icon="@drawable/ic_action_dns"
app:title="@string/remote_dns"
app:useSimpleSummaryProvider="true"/>
<SwitchPreference
android:key="isUdpDns"
android:summary="@string/udp_dns_summary"
android:title="@string/udp_dns"/>
app:key="isUdpDns"
app:icon="@drawable/ic_action_dns"
app:summary="@string/udp_dns_summary"
app:title="@string/udp_dns"/>
</PreferenceCategory>
<PreferenceCategory
android:title="@string/plugin">
app:title="@string/plugin">
<com.github.shadowsocks.preference.IconListPreference
android:key="plugin"
android:persistent="false"
android:summary="%s"
android:title="@string/plugin"/>
<AutoSummaryEditTextPreference
android:key="plugin.configure"
android:icon="@drawable/ic_action_settings"
android:persistent="false"
android:singleLine="true"
app:pref_summaryHasText="%s"
android:title="@string/plugin_configure"/>
app:key="plugin"
app:persistent="false"
app:title="@string/plugin"
app:useSimpleSummaryProvider="true"/>
<EditTextPreference
app:key="plugin.configure"
app:icon="@drawable/ic_action_settings"
app:persistent="false"
app:singleLine="true"
app:title="@string/plugin_configure"
app:useSimpleSummaryProvider="true"/>
<Preference
android:key="udpFallback"
android:title="@string/udp_fallback"
android:summary="@string/plugin_disabled">
app:key="udpFallback"
app:title="@string/udp_fallback"
app:summary="@string/plugin_disabled">
<intent android:targetPackage="com.github.shadowsocks"
android:targetClass="com.github.shadowsocks.UdpFallbackProfileActivity"/>
</Preference>
......
......@@ -26,6 +26,11 @@ android {
testInstrumentationRunner "androidx.testgetRepositoryPassword().runner.AndroidJUnitRunner"
}
compileOptions {
sourceCompatibility javaVersion
targetCompatibility javaVersion
}
buildTypes {
release {
minifyEnabled false
......@@ -49,9 +54,9 @@ mavenPublish {
}
dependencies {
api 'androidx.core:core-ktx:1.0.1'
api 'com.google.android.material:material:1.1.0-alpha05'
api "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
api 'androidx.core:core-ktx:1.0.2'
api 'com.google.android.material:material:1.1.0-alpha06'
api "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion"
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test:runner:$androidTestVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidEspressoVersion"
......
<?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:app="http://schemas.android.com/apk/res-auto"
android:layout_height="?attr/actionBarSize"
......
......@@ -6,4 +6,6 @@
<string name="yes">Yes</string>
<string name="no">No</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>
......@@ -38,6 +38,10 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility javaVersion
targetCompatibility javaVersion
}
packagingOptions.exclude '**/*.kotlin_*'
splits {
abi {
......@@ -52,7 +56,7 @@ android {
dependencies {
implementation project(':core')
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "androidx.leanback:leanback-preference:1.1.0-alpha01"
implementation "androidx.leanback:leanback-preference:1.1.0-alpha02"
}
apply plugin: 'com.google.gms.google-services'
......
......@@ -17,8 +17,7 @@
tools:replace="theme">
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:launchMode="singleTask">
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
......
......@@ -22,10 +22,12 @@ package com.github.shadowsocks.tv
import android.os.Bundle
import androidx.fragment.app.FragmentActivity
import com.github.shadowsocks.utils.SingleInstanceActivity
class MainActivity : FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
SingleInstanceActivity.register(this) ?: return
setContentView(R.layout.activity_main)
}
}
......@@ -35,10 +35,7 @@ import androidx.leanback.preference.LeanbackPreferenceFragmentCompat
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.lifecycle.get
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceDataStore
import androidx.preference.SwitchPreference
import androidx.preference.*
import com.crashlytics.android.Crashlytics
import com.github.shadowsocks.BootReceiver
import com.github.shadowsocks.Core
......@@ -50,6 +47,8 @@ import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.net.HttpsTest
import com.github.shadowsocks.net.TcpFastOpen
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.preference.EditTextPreferenceModifiers
import com.github.shadowsocks.preference.HostsSummaryProvider
import com.github.shadowsocks.preference.OnPreferenceDataStoreChangeListener
import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.datas
......@@ -62,18 +61,20 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
private const val REQUEST_CONNECT = 1
private const val REQUEST_REPLACE_PROFILES = 2
private const val REQUEST_EXPORT_PROFILES = 3
private const val REQUEST_HOSTS = 4
private const val TAG = "MainPreferenceFragment"
}
private lateinit var fab: ListPreference
private lateinit var stats: Preference
private lateinit var controlImport: Preference
private lateinit var hosts: EditTextPreference
private lateinit var serviceMode: Preference
private lateinit var tfo: SwitchPreference
private lateinit var shareOverLan: Preference
private lateinit var portProxy: Preference
private lateinit var portLocalDns: Preference
private lateinit var portTransproxy: Preference
private lateinit var portProxy: EditTextPreference
private lateinit var portLocalDns: EditTextPreference
private lateinit var portTransproxy: EditTextPreference
private val onServiceModeChange = Preference.OnPreferenceChangeListener { _, newValue ->
val (enabledLocalDns, enabledTransproxy) = when (newValue as String?) {
Key.modeProxy -> Pair(false, false)
......@@ -81,6 +82,7 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
Key.modeTransproxy -> Pair(true, true)
else -> throw IllegalArgumentException("newValue: $newValue")
}
hosts.isEnabled = enabledLocalDns
portLocalDns.isEnabled = enabledLocalDns
portTransproxy.isEnabled = enabledTransproxy
true
......@@ -121,19 +123,13 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
})
if (msg != null) Toast.makeText(context, getString(R.string.vpn_error, msg), Toast.LENGTH_SHORT).show()
this.state = state
if (state == BaseService.State.Stopped) {
controlImport.isEnabled = true
tfo.isEnabled = true
serviceMode.isEnabled = true
shareOverLan.isEnabled = true
portProxy.isEnabled = true
onServiceModeChange.onPreferenceChange(null, DataStore.serviceMode)
} else {
controlImport.isEnabled = false
tfo.isEnabled = false
serviceMode.isEnabled = false
shareOverLan.isEnabled = false
portProxy.isEnabled = false
val stopped = state == BaseService.State.Stopped
controlImport.isEnabled = stopped
tfo.isEnabled = stopped
serviceMode.isEnabled = stopped
shareOverLan.isEnabled = stopped
portProxy.isEnabled = stopped
if (stopped) onServiceModeChange.onPreferenceChange(null, DataStore.serviceMode) else {
portLocalDns.isEnabled = false
portTransproxy.isEnabled = false
}
......@@ -156,17 +152,17 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
preferenceManager.preferenceDataStore = DataStore.publicStore
DataStore.initGlobal()
addPreferencesFromResource(R.xml.pref_main)
fab = findPreference(Key.id) as ListPreference
fab = findPreference(Key.id)!!
populateProfiles()
stats = findPreference(Key.controlStats)
controlImport = findPreference(Key.controlImport)
stats = findPreference(Key.controlStats)!!
controlImport = findPreference(Key.controlImport)!!
(findPreference(Key.persistAcrossReboot) as SwitchPreference).setOnPreferenceChangeListener { _, value ->
findPreference<SwitchPreference>(Key.persistAcrossReboot)!!.setOnPreferenceChangeListener { _, value ->
BootReceiver.enabled = value as Boolean
true
}
tfo = findPreference(Key.tfo) as SwitchPreference
tfo = findPreference(Key.tfo)!!
tfo.isChecked = DataStore.tcpFastOpen
tfo.setOnPreferenceChangeListener { _, value ->
if (value as Boolean && !TcpFastOpen.sendEnabled) {
......@@ -183,19 +179,18 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
tfo.summary = getString(R.string.tcp_fastopen_summary_unsupported, System.getProperty("os.version"))
}
serviceMode = findPreference(Key.serviceMode)
shareOverLan = findPreference(Key.shareOverLan)
portProxy = findPreference(Key.portProxy)
portLocalDns = findPreference(Key.portLocalDns)
portTransproxy = findPreference(Key.portTransproxy)
hosts = findPreference(Key.hosts)!!
hosts.summaryProvider = HostsSummaryProvider
serviceMode = findPreference(Key.serviceMode)!!
shareOverLan = findPreference(Key.shareOverLan)!!
portProxy = findPreference(Key.portProxy)!!
portProxy.onBindEditTextListener = EditTextPreferenceModifiers.Port
portLocalDns = findPreference(Key.portLocalDns)!!
portLocalDns.onBindEditTextListener = EditTextPreferenceModifiers.Port
portTransproxy = findPreference(Key.portTransproxy)!!
portTransproxy.onBindEditTextListener = EditTextPreferenceModifiers.Port
serviceMode.onPreferenceChangeListener = onServiceModeChange
findPreference(Key.about).apply {
summary = getString(R.string.about_title, BuildConfig.VERSION_NAME)
setOnPreferenceClickListener {
Toast.makeText(requireContext(), "https://shadowsocks.org/android", Toast.LENGTH_SHORT).show()
true
}
}
findPreference<Preference>(Key.about)!!.summary = getString(R.string.about_title, BuildConfig.VERSION_NAME)
tester = ViewModelProviders.of(this).get()
changeState(BaseService.State.Idle) // reset everything to init state
......@@ -233,7 +228,7 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
}
}
override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String?) {
override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String) {
when (key) {
Key.serviceMode -> handler.post {
connection.disconnect(requireContext())
......@@ -271,15 +266,25 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
}, REQUEST_EXPORT_PROFILES)
true
}
Key.about -> {
Toast.makeText(requireContext(), "https://shadowsocks.org/android", Toast.LENGTH_SHORT).show()
true
}
else -> super.onPreferenceTreeClick(preference)
}
private fun startFilesForResult(intent: Intent, requestCode: Int) {
override fun onDisplayPreferenceDialog(preference: Preference?) {
if (preference != hosts || startFilesForResult(Intent(Intent.ACTION_GET_CONTENT).setType("*/*"), REQUEST_HOSTS))
super.onDisplayPreferenceDialog(preference)
}
private fun startFilesForResult(intent: Intent, requestCode: Int): Boolean {
try {
startActivityForResult(intent.addCategory(Intent.CATEGORY_OPENABLE), requestCode)
return
return false
} catch (_: ActivityNotFoundException) { } catch (_: SecurityException) { }
Toast.makeText(requireContext(), R.string.file_manager_missing, Toast.LENGTH_SHORT).show()
return true
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
......@@ -314,6 +319,16 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
Toast.makeText(context, e.readableMessage, Toast.LENGTH_SHORT).show()
}
}
REQUEST_HOSTS -> {
if (resultCode != Activity.RESULT_OK) return
val context = requireContext()
try {
// we read and persist all its content here to avoid content URL permission issues
hosts.text = context.contentResolver.openInputStream(data!!.data!!)!!.bufferedReader().readText()
} catch (e: RuntimeException) {
Toast.makeText(context, e.readableMessage, Toast.LENGTH_SHORT).show()
}
}
else -> super.onActivityResult(requestCode, resultCode, data)
}
}
......
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:title="@string/app_name">
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto"
app:title="@string/app_name">
<!-- profileId is stored in privateStore so its value needs to be populated manually -->
<ListPreference
android:key="profileId"
android:title="@string/connect"
android:summary="%s"
android:persistent="false"/>
app:key="profileId"
app:title="@string/connect"
app:persistent="false"
app:useSimpleSummaryProvider="true"/>
<Preference
android:key="control.stats"
android:title="@string/connection_test_pending"
android:summary="@string/stat_summary"/>
app:key="control.stats"
app:title="@string/connection_test_pending"
app:summary="@string/stat_summary"/>
<Preference
android:key="control.import"
android:title="@string/action_replace_file"/>
app:key="control.import"
app:title="@string/action_replace_file"/>
<Preference
android:key="control.export"
android:title="@string/action_export_file"/>
app:key="control.export"
app:title="@string/action_export_file"/>
<PreferenceCategory
android:key="settings"
android:title="@string/settings"
app:initialExpandedChildrenCount="2">
app:key="settings"
app:title="@string/settings"
app:initialExpandedChildrenCount="3">
<SwitchPreference
android:key="isAutoConnect"
android:summary="@string/auto_connect_summary"
android:title="@string/auto_connect"/>
app:key="isAutoConnect"
app:summary="@string/auto_connect_summary"
app:title="@string/auto_connect"/>
<SwitchPreference
android:key="tcp_fastopen"
android:summary="@string/tcp_fastopen_summary"
android:title="TCP Fast Open"/>
app:key="tcp_fastopen"
app:summary="@string/tcp_fastopen_summary"
app:title="TCP Fast Open"/>
<EditTextPreference
app:key="hosts"
app:title="hosts"/>
<ListPreference
android:key="serviceMode"
android:entries="@array/service_modes"
android:entryValues="@array/service_mode_values"
android:defaultValue="vpn"
android:summary="%s"
android:title="@string/service_mode"/>
app:key="serviceMode"
app:entries="@array/service_modes"
app:entryValues="@array/service_mode_values"
app:defaultValue="vpn"
app:title="@string/service_mode"
app:useSimpleSummaryProvider="true"/>
<SwitchPreference
android:key="shareOverLan"
android:title="@string/share_over_lan"/>
<com.takisoft.preferencex.AutoSummaryEditTextPreference
android:key="portProxy"
android:inputType="number"
android:maxLength="5"
app:pref_summaryHasText="%s"
android:singleLine="true"
android:title="@string/port_proxy"/>
<com.takisoft.preferencex.AutoSummaryEditTextPreference
android:key="portLocalDns"
android:inputType="number"
android:maxLength="5"
app:pref_summaryHasText="%s"
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"/>
app:key="shareOverLan"
app:title="@string/share_over_lan"/>
<EditTextPreference
app:key="portProxy"
app:title="@string/port_proxy"
app:useSimpleSummaryProvider="true"/>
<EditTextPreference
app:key="portLocalDns"
app:title="@string/port_local_dns"
app:useSimpleSummaryProvider="true"/>
<EditTextPreference
app:key="portTransproxy"
app:title="@string/port_transproxy"
app:useSimpleSummaryProvider="true"/>
</PreferenceCategory>
<Preference
android:key="about"
android:title="@string/about"/>
app:key="about"
app:title="@string/about"/>
</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