Commit 8dd24d69 authored by Mygod's avatar Mygod

Merge branch 'master' into preference-1.1

parents 576ab96c 723b2b4d
...@@ -35,6 +35,7 @@ The exclamation mark in the Wi-Fi/cellular icon appears because the system fails ...@@ -35,6 +35,7 @@ The exclamation mark in the Wi-Fi/cellular icon appears because the system fails
* Related to Xposed: [#1414](https://github.com/shadowsocks/shadowsocks-android/issues/1414) * Related to Xposed: [#1414](https://github.com/shadowsocks/shadowsocks-android/issues/1414)
* Samsung and/or Brevent: [#1410](https://github.com/shadowsocks/shadowsocks-android/issues/1410) * Samsung and/or Brevent: [#1410](https://github.com/shadowsocks/shadowsocks-android/issues/1410)
* Another Samsung: [#1712](https://github.com/shadowsocks/shadowsocks-android/issues/1712) * Another Samsung: [#1712](https://github.com/shadowsocks/shadowsocks-android/issues/1712)
* Samsung with GMS: [#2138](https://github.com/shadowsocks/shadowsocks-android/issues/2138)
* Don't install this app on SD card because of permission issues: [#1124 (comment)](https://github.com/shadowsocks/shadowsocks-android/issues/1124#issuecomment-307556453) * Don't install this app on SD card because of permission issues: [#1124 (comment)](https://github.com/shadowsocks/shadowsocks-android/issues/1124#issuecomment-307556453)
* `INTERACT_ACROSS_USERS` permission missing: [#1184](https://github.com/shadowsocks/shadowsocks-android/issues/1184) * `INTERACT_ACROSS_USERS` permission missing: [#1184](https://github.com/shadowsocks/shadowsocks-android/issues/1184)
......
...@@ -4,7 +4,7 @@ apply plugin: 'com.github.ben-manes.versions' ...@@ -4,7 +4,7 @@ apply plugin: 'com.github.ben-manes.versions'
buildscript { buildscript {
ext { ext {
kotlinVersion = '1.3.21' kotlinVersion = '1.3.31'
minSdkVersion = 21 minSdkVersion = 21
sdkVersion = 28 sdkVersion = 28
compileSdkVersion = 28 compileSdkVersion = 28
...@@ -12,8 +12,8 @@ buildscript { ...@@ -12,8 +12,8 @@ buildscript {
junitVersion = '4.12' junitVersion = '4.12'
androidTestVersion = '1.1.1' androidTestVersion = '1.1.1'
androidEspressoVersion = '3.1.1' androidEspressoVersion = '3.1.1'
versionCode = 4070350 versionCode = 4070450
versionName = '4.7.3-nightly' versionName = '4.7.4-nightly'
resConfigs = ['es', 'fa', 'fr', 'ja', 'ko', 'ru', 'tr', 'zh-rCN', 'zh-rTW'] resConfigs = ['es', 'fa', 'fr', 'ja', 'ko', 'ru', 'tr', 'zh-rCN', 'zh-rTW']
} }
...@@ -26,7 +26,7 @@ buildscript { ...@@ -26,7 +26,7 @@ buildscript {
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.3.2' classpath 'com.android.tools.build:gradle:3.4.0'
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'
......
...@@ -45,27 +45,24 @@ androidExtensions { ...@@ -45,27 +45,24 @@ androidExtensions {
def lifecycleVersion = '2.0.0' def lifecycleVersion = '2.0.0'
def roomVersion = '2.0.0' def roomVersion = '2.0.0'
def workVersion = '1.0.0'
dependencies { dependencies {
api project(':plugin') api project(':plugin')
api "android.arch.work:work-runtime-ktx:$workVersion"
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.1.0-alpha04' api 'androidx.preference:preference:1.1.0-alpha04'
api "androidx.room:room-runtime:$roomVersion" api "androidx.room:room-runtime:$roomVersion"
api 'com.crashlytics.sdk.android:crashlytics:2.9.9' api 'androidx.work:work-runtime-ktx:2.0.1'
api 'com.google.firebase:firebase-config:16.4.1' api 'com.crashlytics.sdk.android:crashlytics:2.10.0'
api 'com.google.firebase:firebase-core:16.0.8' 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 'dnsjava:dnsjava:2.1.8'
api 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1' 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.lifecycle:lifecycle-compiler:$lifecycleVersion"
kapt "androidx.room:room-compiler:$roomVersion" kapt "androidx.room:room-compiler:$roomVersion"
testImplementation "androidx.arch.core:core-testing:$lifecycleVersion"
testImplementation "junit:junit:$junitVersion" testImplementation "junit:junit:$junitVersion"
androidTestImplementation "android.arch.work:work-testing:$workVersion"
androidTestImplementation "androidx.room:room-testing:$roomVersion" androidTestImplementation "androidx.room:room-testing:$roomVersion"
androidTestImplementation "androidx.test:runner:$androidTestVersion" androidTestImplementation "androidx.test:runner:$androidTestVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidEspressoVersion" androidTestImplementation "androidx.test.espresso:espresso-core:$androidEspressoVersion"
androidTestImplementation "androidx.test.ext:junit-ktx:1.1.0" androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.0'
} }
...@@ -50,6 +50,8 @@ import com.github.shadowsocks.utils.* ...@@ -50,6 +50,8 @@ import com.github.shadowsocks.utils.*
import com.google.firebase.FirebaseApp import com.google.firebase.FirebaseApp
import com.google.firebase.analytics.FirebaseAnalytics import com.google.firebase.analytics.FirebaseAnalytics
import io.fabric.sdk.android.Fabric import io.fabric.sdk.android.Fabric
import kotlinx.coroutines.DEBUG_PROPERTY_NAME
import kotlinx.coroutines.DEBUG_PROPERTY_VALUE_ON
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import kotlin.reflect.KClass import kotlin.reflect.KClass
...@@ -97,6 +99,8 @@ object Core { ...@@ -97,6 +99,8 @@ object Core {
} }
} }
// overhead of debug mode is minimal: https://github.com/Kotlin/kotlinx.coroutines/blob/f528898/docs/debugging.md#debug-mode
System.setProperty(DEBUG_PROPERTY_NAME, DEBUG_PROPERTY_VALUE_ON)
Fabric.with(deviceStorage, Crashlytics()) // multiple processes needs manual set-up Fabric.with(deviceStorage, Crashlytics()) // multiple processes needs manual set-up
FirebaseApp.initializeApp(deviceStorage) FirebaseApp.initializeApp(deviceStorage)
WorkManager.initialize(deviceStorage, Configuration.Builder().build()) WorkManager.initialize(deviceStorage, Configuration.Builder().build())
......
...@@ -121,8 +121,7 @@ class Acl { ...@@ -121,8 +121,7 @@ class Acl {
val blocks = (line as java.lang.String).split("#", 2) val blocks = (line as java.lang.String).split("#", 2)
val url = networkAclParser.matchEntire(blocks.getOrElse(1) { "" })?.groupValues?.getOrNull(1) val url = networkAclParser.matchEntire(blocks.getOrElse(1) { "" })?.groupValues?.getOrNull(1)
if (url != null) urls.add(URL(url)) if (url != null) urls.add(URL(url))
val input = blocks[0].trim() when (val input = blocks[0].trim()) {
when (input) {
"[outbound_block_list]" -> { "[outbound_block_list]" -> {
hostnames = null hostnames = null
subnets = null subnets = null
......
...@@ -254,7 +254,7 @@ object BaseService { ...@@ -254,7 +254,7 @@ object BaseService {
if (data.state == State.Stopping) return if (data.state == State.Stopping) return
// channge the state // channge the state
data.changeState(State.Stopping) data.changeState(State.Stopping)
GlobalScope.launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED) { GlobalScope.launch(Dispatchers.Main.immediate) {
Core.analytics.logEvent("stop", bundleOf(Pair(FirebaseAnalytics.Param.METHOD, tag))) Core.analytics.logEvent("stop", bundleOf(Pair(FirebaseAnalytics.Param.METHOD, tag)))
data.connectingJob?.cancelAndJoin() // ensure stop connecting first data.connectingJob?.cancelAndJoin() // ensure stop connecting first
this@Interface as Service this@Interface as Service
......
...@@ -104,8 +104,7 @@ class GuardedProcessPool(private val onFatal: suspend (IOException) -> Unit) : C ...@@ -104,8 +104,7 @@ class GuardedProcessPool(private val onFatal: suspend (IOException) -> Unit) : C
} }
} }
private val job = Job() override val coroutineContext = Dispatchers.Main.immediate + Job()
override val coroutineContext get() = Dispatchers.Main + job
@MainThread @MainThread
fun start(cmd: List<String>, onRestartCallback: (suspend () -> Unit)? = null) { fun start(cmd: List<String>, onRestartCallback: (suspend () -> Unit)? = null) {
...@@ -118,7 +117,7 @@ class GuardedProcessPool(private val onFatal: suspend (IOException) -> Unit) : C ...@@ -118,7 +117,7 @@ class GuardedProcessPool(private val onFatal: suspend (IOException) -> Unit) : C
@MainThread @MainThread
fun close(scope: CoroutineScope) { fun close(scope: CoroutineScope) {
job.cancel() cancel()
scope.launch { job.join() } coroutineContext[Job]!!.also { job -> scope.launch { job.join() } }
} }
} }
...@@ -42,7 +42,7 @@ object LocalDnsService { ...@@ -42,7 +42,7 @@ object LocalDnsService {
.lineSequence().map(Subnet.Companion::fromString).filterNotNull().toList() .lineSequence().map(Subnet.Companion::fromString).filterNotNull().toList()
} }
private val servers = WeakHashMap<LocalDnsService.Interface, LocalDnsServer>() private val servers = WeakHashMap<Interface, LocalDnsServer>()
interface Interface : BaseService.Interface { interface Interface : BaseService.Interface {
override suspend fun startProcesses() { override suspend fun startProcesses() {
......
...@@ -37,13 +37,13 @@ object RemoteConfig { ...@@ -37,13 +37,13 @@ object RemoteConfig {
} }
fun scheduleFetch() = config.fetch().addOnCompleteListener { fun scheduleFetch() = config.fetch().addOnCompleteListener {
if (it.isSuccessful) config.activateFetched() else it.exception?.log() if (it.isSuccessful) config.activate() else it.exception?.log()
} }
suspend fun fetch() = suspendCancellableCoroutine<Pair<FirebaseRemoteConfig, Boolean>> { cont -> suspend fun fetch() = suspendCancellableCoroutine<Pair<FirebaseRemoteConfig, Boolean>> { cont ->
config.fetch().addOnCompleteListener { config.fetch().addOnCompleteListener {
if (it.isSuccessful) { if (it.isSuccessful) {
config.activateFetched() config.activate()
cont.resume(config to true) cont.resume(config to true)
} else { } else {
it.exception?.log() it.exception?.log()
......
...@@ -129,11 +129,11 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface { ...@@ -129,11 +129,11 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (DataStore.serviceMode == Key.modeVpn) if (DataStore.serviceMode == Key.modeVpn) {
if (BaseVpnService.prepare(this) != null) if (prepare(this) != null) {
startActivity(Intent(this, VpnRequestActivity::class.java) startActivity(Intent(this, VpnRequestActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) } else return super<LocalDnsService.Interface>.onStartCommand(intent, flags, startId)
else return super<LocalDnsService.Interface>.onStartCommand(intent, flags, startId) }
stopRunner() stopRunner()
return Service.START_NOT_STICKY return Service.START_NOT_STICKY
} }
......
...@@ -244,12 +244,12 @@ data class Profile( ...@@ -244,12 +244,12 @@ data class Profile(
} }
fun toUri(): Uri { fun toUri(): Uri {
val auth = Base64.encodeToString("$method:$password".toByteArray(),
Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE)
val wrappedHost = if (host.contains(':')) "[$host]" else host
val builder = Uri.Builder() val builder = Uri.Builder()
.scheme("ss") .scheme("ss")
.encodedAuthority("%s@%s:%d".format(Locale.ENGLISH, .encodedAuthority("$auth@$wrappedHost:$remotePort")
Base64.encodeToString("$method:$password".toByteArray(),
Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE),
if (host.contains(':')) "[$host]" else host, remotePort))
val configuration = PluginConfiguration(plugin ?: "") val configuration = PluginConfiguration(plugin ?: "")
if (configuration.selected.isNotEmpty()) if (configuration.selected.isNotEmpty())
builder.appendQueryParameter(Key.plugin, configuration.selectedOptions.toString(false)) builder.appendQueryParameter(Key.plugin, configuration.selectedOptions.toString(false))
......
...@@ -27,8 +27,7 @@ import java.io.File ...@@ -27,8 +27,7 @@ import java.io.File
abstract class ConcurrentLocalSocketListener(name: String, socketFile: File) : LocalSocketListener(name, socketFile), abstract class ConcurrentLocalSocketListener(name: String, socketFile: File) : LocalSocketListener(name, socketFile),
CoroutineScope { CoroutineScope {
private val job = SupervisorJob() override val coroutineContext = Dispatchers.IO + SupervisorJob() + CoroutineExceptionHandler { _, t -> printLog(t) }
override val coroutineContext get() = Dispatchers.IO + job + CoroutineExceptionHandler { _, t -> printLog(t) }
override fun accept(socket: LocalSocket) { override fun accept(socket: LocalSocket) {
launch { super.accept(socket) } launch { super.accept(socket) }
...@@ -36,8 +35,8 @@ abstract class ConcurrentLocalSocketListener(name: String, socketFile: File) : L ...@@ -36,8 +35,8 @@ abstract class ConcurrentLocalSocketListener(name: String, socketFile: File) : L
override fun shutdown(scope: CoroutineScope) { override fun shutdown(scope: CoroutineScope) {
running = false running = false
job.cancel() cancel()
super.shutdown(scope) super.shutdown(scope)
scope.launch { job.join() } coroutineContext[Job]!!.also { job -> scope.launch { job.join() } }
} }
} }
...@@ -62,7 +62,7 @@ object DefaultNetworkListener { ...@@ -62,7 +62,7 @@ object DefaultNetworkListener {
check(listeners.isNotEmpty()) { "Getting network without any listeners is not supported" } check(listeners.isNotEmpty()) { "Getting network without any listeners is not supported" }
if (network == null) pendingRequests += message else message.response.complete(network) if (network == null) pendingRequests += message else message.response.complete(network)
} }
is NetworkMessage.Stop -> if (!listeners.isEmpty() && // was not empty is NetworkMessage.Stop -> if (listeners.isNotEmpty() && // was not empty
listeners.remove(message.key) != null && listeners.isEmpty()) { listeners.remove(message.key) != null && listeners.isEmpty()) {
network = null network = null
unregister() unregister()
......
...@@ -91,7 +91,7 @@ class HttpsTest : ViewModel() { ...@@ -91,7 +91,7 @@ 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, CoroutineStart.UNDISPATCHED) { running = conn to GlobalScope.launch(Dispatchers.Main.immediate) {
status.value = withContext(Dispatchers.IO) { status.value = withContext(Dispatchers.IO) {
try { try {
val start = SystemClock.elapsedRealtime() val start = SystemClock.elapsedRealtime()
......
...@@ -86,8 +86,7 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAd ...@@ -86,8 +86,7 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAd
private val monitor = ChannelMonitor() private val monitor = ChannelMonitor()
private val job = SupervisorJob() override val coroutineContext = SupervisorJob() + CoroutineExceptionHandler { _, t -> printLog(t) }
override val coroutineContext = job + CoroutineExceptionHandler { _, t -> printLog(t) }
suspend fun start(listen: SocketAddress) = DatagramChannel.open().run { suspend fun start(listen: SocketAddress) = DatagramChannel.open().run {
configureBlocking(false) configureBlocking(false)
...@@ -182,8 +181,8 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAd ...@@ -182,8 +181,8 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAd
} }
fun shutdown(scope: CoroutineScope) { fun shutdown(scope: CoroutineScope) {
job.cancel() cancel()
monitor.close(scope) monitor.close(scope)
scope.launch { job.join() } coroutineContext[Job]!!.also { job -> scope.launch { job.join() } }
} }
} }
...@@ -44,7 +44,7 @@ object DataStore : OnPreferenceDataStoreChangeListener { ...@@ -44,7 +44,7 @@ object DataStore : OnPreferenceDataStoreChangeListener {
override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String) { override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String) {
when (key) { when (key) {
Key.id -> if (DataStore.directBootAware) DirectBoot.update() Key.id -> if (directBootAware) DirectBoot.update()
} }
} }
......
Subproject commit e8660cfae85762d3d3eaf72c47b8478f1c9f0e22 Subproject commit 6e14849aa213cbb028aeb0c75905438260fd867c
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
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-all.zip
#!/usr/bin/env sh #!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
############################################################################## ##############################################################################
## ##
## Gradle start up script for UN*X ## Gradle start up script for UN*X
...@@ -28,7 +44,7 @@ APP_NAME="Gradle" ...@@ -28,7 +44,7 @@ APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"` APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS="" DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum" MAX_FD="maximum"
......
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem http://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off @if "%DEBUG%" == "" @echo off
@rem ########################################################################## @rem ##########################################################################
@rem @rem
...@@ -14,7 +30,7 @@ set APP_BASE_NAME=%~n0 ...@@ -14,7 +30,7 @@ set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME% set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS= set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe @rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome if defined JAVA_HOME goto findJavaFromJavaHome
......
...@@ -39,7 +39,7 @@ android { ...@@ -39,7 +39,7 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
} }
} }
lintOptions.checkReleaseBuilds false packagingOptions.exclude '**/*.kotlin_*'
splits { splits {
abi { abi {
enable true enable true
......
...@@ -53,6 +53,7 @@ import com.google.android.material.snackbar.Snackbar ...@@ -53,6 +53,7 @@ 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.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlin.coroutines.coroutineContext
class AppManager : AppCompatActivity() { class AppManager : AppCompatActivity() {
companion object { companion object {
...@@ -68,7 +69,7 @@ class AppManager : AppCompatActivity() { ...@@ -68,7 +69,7 @@ class AppManager : AppCompatActivity() {
receiver = null receiver = null
cachedApps = null cachedApps = null
} }
AppManager.instance?.loadApps() instance?.loadApps()
} }
// Labels and icons can change on configuration (locale, etc.) changes, therefore they are not cached. // Labels and icons can change on configuration (locale, etc.) changes, therefore they are not cached.
val cachedApps = cachedApps ?: pm.getInstalledPackages(PackageManager.GET_PERMISSIONS) val cachedApps = cachedApps ?: pm.getInstalledPackages(PackageManager.GET_PERMISSIONS)
...@@ -108,7 +109,7 @@ class AppManager : AppCompatActivity() { ...@@ -108,7 +109,7 @@ class AppManager : AppCompatActivity() {
} }
fun handlePayload(payloads: List<String>) { fun handlePayload(payloads: List<String>) {
if (payloads.contains(AppManager.SWITCH)) itemView.itemcheck.isChecked = isProxiedApp(item) if (payloads.contains(SWITCH)) itemView.itemcheck.isChecked = isProxiedApp(item)
} }
override fun onClick(v: View?) { override fun onClick(v: View?) {
...@@ -116,7 +117,7 @@ class AppManager : AppCompatActivity() { ...@@ -116,7 +117,7 @@ class AppManager : AppCompatActivity() {
DataStore.individual = apps.filter { isProxiedApp(it) }.joinToString("\n") { it.packageName } DataStore.individual = apps.filter { isProxiedApp(it) }.joinToString("\n") { it.packageName }
DataStore.dirty = true DataStore.dirty = true
appsAdapter.notifyItemRangeChanged(0, appsAdapter.itemCount, AppManager.SWITCH) appsAdapter.notifyItemRangeChanged(0, appsAdapter.itemCount, SWITCH)
} }
} }
...@@ -125,7 +126,7 @@ class AppManager : AppCompatActivity() { ...@@ -125,7 +126,7 @@ class AppManager : AppCompatActivity() {
suspend fun reload() { suspend fun reload() {
apps = getCachedApps(packageManager).map { (packageName, packageInfo) -> apps = getCachedApps(packageManager).map { (packageName, packageInfo) ->
yield() coroutineContext[Job]!!.ensureActive()
ProxiedApp(packageManager, packageInfo.applicationInfo, packageName) ProxiedApp(packageManager, packageInfo.applicationInfo, packageName)
}.sortedWith(compareBy({ !isProxiedApp(it) }, { it.name.toString() })) }.sortedWith(compareBy({ !isProxiedApp(it) }, { it.name.toString() }))
} }
...@@ -196,7 +197,7 @@ class AppManager : AppCompatActivity() { ...@@ -196,7 +197,7 @@ class AppManager : AppCompatActivity() {
@UiThread @UiThread
private fun loadApps() { private fun loadApps() {
loader?.cancel() loader?.cancel()
loader = GlobalScope.launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED) { loader = GlobalScope.launch(Dispatchers.Main.immediate) {
loading.crossFadeFrom(list) loading.crossFadeFrom(list)
val adapter = list.adapter as AppsAdapter val adapter = list.adapter as AppsAdapter
withContext(Dispatchers.IO) { adapter.reload() } withContext(Dispatchers.IO) { adapter.reload() }
...@@ -280,7 +281,7 @@ class AppManager : AppCompatActivity() { ...@@ -280,7 +281,7 @@ class AppManager : AppCompatActivity() {
DataStore.dirty = true DataStore.dirty = true
Snackbar.make(list, R.string.action_import_msg, Snackbar.LENGTH_LONG).show() Snackbar.make(list, R.string.action_import_msg, Snackbar.LENGTH_LONG).show()
initProxiedUids(apps) initProxiedUids(apps)
appsAdapter.notifyItemRangeChanged(0, appsAdapter.itemCount, AppManager.SWITCH) appsAdapter.notifyItemRangeChanged(0, appsAdapter.itemCount, SWITCH)
return true return true
} catch (_: IllegalArgumentException) { } } catch (_: IllegalArgumentException) { }
} }
......
...@@ -41,7 +41,7 @@ class QuickToggleShortcut : Activity(), ShadowsocksConnection.Callback { ...@@ -41,7 +41,7 @@ class QuickToggleShortcut : Activity(), ShadowsocksConnection.Callback {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
if (intent.action == Intent.ACTION_CREATE_SHORTCUT) { if (intent.action == Intent.ACTION_CREATE_SHORTCUT) {
setResult(Activity.RESULT_OK, ShortcutManagerCompat.createShortcutResultIntent(this, setResult(RESULT_OK, ShortcutManagerCompat.createShortcutResultIntent(this,
ShortcutInfoCompat.Builder(this, "toggle") ShortcutInfoCompat.Builder(this, "toggle")
.setIntent(Intent(this, QuickToggleShortcut::class.java).setAction(Intent.ACTION_MAIN)) .setIntent(Intent(this, QuickToggleShortcut::class.java).setAction(Intent.ACTION_MAIN))
.setIcon(IconCompat.createWithResource(this, R.drawable.ic_qu_shadowsocks_launcher)) .setIcon(IconCompat.createWithResource(this, R.drawable.ic_qu_shadowsocks_launcher))
......
...@@ -63,7 +63,6 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener, ...@@ -63,7 +63,6 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener,
companion object { companion object {
private const val REQUEST_CODE_ADD = 1 private const val REQUEST_CODE_ADD = 1
private const val REQUEST_CODE_EDIT = 2 private const val REQUEST_CODE_EDIT = 2
private const val TEMPLATE_REGEX_DOMAIN = "(^|\\.)%s$"
private const val SELECTED_SUBNETS = "com.github.shadowsocks.acl.CustomRulesFragment.SELECTED_SUBNETS" private const val SELECTED_SUBNETS = "com.github.shadowsocks.acl.CustomRulesFragment.SELECTED_SUBNETS"
private const val SELECTED_HOSTNAMES = "com.github.shadowsocks.acl.CustomRulesFragment.SELECTED_HOSTNAMES" private const val SELECTED_HOSTNAMES = "com.github.shadowsocks.acl.CustomRulesFragment.SELECTED_HOSTNAMES"
...@@ -170,16 +169,22 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener, ...@@ -170,16 +169,22 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener,
inputLayout.error = message inputLayout.error = message
} }
override val ret get() = AclEditResult(editText.text.toString().let { text -> override fun ret(which: Int) = if (which != DialogInterface.BUTTON_POSITIVE) null else {
AclEditResult(editText.text.toString().let { text ->
when (Template.values()[templateSelector.selectedItemPosition]) { when (Template.values()[templateSelector.selectedItemPosition]) {
Template.Generic -> AclItem(text) Template.Generic -> AclItem(text)
Template.Domain -> AclItem(TEMPLATE_REGEX_DOMAIN.format(Locale.ENGLISH, IDN.toASCII(text, Template.Domain -> AclItem(IDN.toASCII(text, IDN.ALLOW_UNASSIGNED or IDN.USE_STD3_ASCII_RULES)
IDN.ALLOW_UNASSIGNED or IDN.USE_STD3_ASCII_RULES).replace(".", "\\."))) .replace(".", "\\.").let { "(^|\\.)$it\$" })
Template.Url -> AclItem(text, true) Template.Url -> AclItem(text, true)
} }
}, arg) }, arg)
} }
override fun onClick(dialog: DialogInterface?, which: Int) {
if (which != DialogInterface.BUTTON_NEGATIVE) super.onClick(dialog, which)
}
}
private inner class AclRuleViewHolder(view: View) : RecyclerView.ViewHolder(view), private inner class AclRuleViewHolder(view: View) : RecyclerView.ViewHolder(view),
View.OnClickListener, View.OnLongClickListener { View.OnClickListener, View.OnLongClickListener {
lateinit var item: Any lateinit var item: Any
......
...@@ -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"
......
...@@ -50,8 +50,8 @@ mavenPublish { ...@@ -50,8 +50,8 @@ mavenPublish {
dependencies { dependencies {
api 'androidx.core:core-ktx:1.0.1' api 'androidx.core:core-ktx:1.0.1'
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"
......
...@@ -43,21 +43,21 @@ abstract class AlertDialogFragment<Arg : Parcelable, Ret : Parcelable> : ...@@ -43,21 +43,21 @@ abstract class AlertDialogFragment<Arg : Parcelable, Ret : Parcelable> :
protected abstract fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) protected abstract fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener)
protected val arg by lazy { arguments!!.getParcelable<Arg>(KEY_ARG)!! } protected val arg by lazy { arguments!!.getParcelable<Arg>(KEY_ARG)!! }
protected open val ret: Ret? get() = null protected open fun ret(which: Int): Ret? = null
fun withArg(arg: Arg) = apply { arguments = Bundle().apply { putParcelable(KEY_ARG, arg) } } fun withArg(arg: Arg) = apply { arguments = Bundle().apply { putParcelable(KEY_ARG, arg) } }
override fun onCreateDialog(savedInstanceState: Bundle?): AlertDialog = override fun onCreateDialog(savedInstanceState: Bundle?): AlertDialog =
AlertDialog.Builder(requireContext()).also { it.prepare(this) }.create() AlertDialog.Builder(requireContext()).also { it.prepare(this) }.create()
override fun onClick(dialog: DialogInterface?, which: Int) { override fun onClick(dialog: DialogInterface?, which: Int) {
targetFragment?.onActivityResult(targetRequestCode, which, ret?.let { targetFragment?.onActivityResult(targetRequestCode, which, ret(which)?.let {
Intent().replaceExtras(Bundle().apply { putParcelable(KEY_RET, it) }) Intent().replaceExtras(Bundle().apply { putParcelable(KEY_RET, it) })
}) })
} }
override fun onDismiss(dialog: DialogInterface) { override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog) super.onDismiss(dialog)
targetFragment?.onActivityResult(targetRequestCode, Activity.RESULT_CANCELED, null) onClick(dialog, Activity.RESULT_CANCELED)
} }
fun show(target: Fragment, requestCode: Int = 0, tag: String = javaClass.simpleName) { fun show(target: Fragment, requestCode: Int = 0, tag: String = javaClass.simpleName) {
......
...@@ -42,9 +42,7 @@ class PluginOptions : HashMap<String, String?> { ...@@ -42,9 +42,7 @@ class PluginOptions : HashMap<String, String?> {
val tokenizer = StringTokenizer("$options;", "\\=;", true) val tokenizer = StringTokenizer("$options;", "\\=;", true)
val current = StringBuilder() val current = StringBuilder()
var key: String? = null var key: String? = null
while (tokenizer.hasMoreTokens()) { while (tokenizer.hasMoreTokens()) when (val nextToken = tokenizer.nextToken()) {
val nextToken = tokenizer.nextToken()
when (nextToken) {
"\\" -> current.append(tokenizer.nextToken()) "\\" -> current.append(tokenizer.nextToken())
"=" -> if (key == null) { "=" -> if (key == null) {
key = current.toString() key = current.toString()
...@@ -62,7 +60,6 @@ class PluginOptions : HashMap<String, String?> { ...@@ -62,7 +60,6 @@ class PluginOptions : HashMap<String, String?> {
else -> current.append(nextToken) else -> current.append(nextToken)
} }
} }
}
constructor(options: String?) : this(options, true) constructor(options: String?) : this(options, true)
constructor(id: String, options: String?) : this(options, false) { constructor(id: String, options: String?) : this(options, false) {
......
<?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"
......
...@@ -38,7 +38,7 @@ android { ...@@ -38,7 +38,7 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
} }
} }
lintOptions.checkReleaseBuilds false packagingOptions.exclude '**/*.kotlin_*'
splits { splits {
abi { abi {
enable true enable true
......
...@@ -94,7 +94,7 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo ...@@ -94,7 +94,7 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
private set private set
override fun stateChanged(state: BaseService.State, profileName: String?, msg: String?) = changeState(state, msg) override fun stateChanged(state: BaseService.State, profileName: String?, msg: String?) = changeState(state, msg)
override fun trafficUpdated(profileId: Long, stats: TrafficStats) { override fun trafficUpdated(profileId: Long, stats: TrafficStats) {
if (profileId == 0L) requireContext().let { context -> if (profileId == 0L) context?.let { context ->
this.stats.summary = getString(R.string.stat_summary, this.stats.summary = getString(R.string.stat_summary,
getString(R.string.speed, Formatter.formatFileSize(context, stats.txRate)), getString(R.string.speed, Formatter.formatFileSize(context, stats.txRate)),
getString(R.string.speed, Formatter.formatFileSize(context, stats.rxRate)), getString(R.string.speed, Formatter.formatFileSize(context, stats.rxRate)),
...@@ -104,6 +104,7 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo ...@@ -104,6 +104,7 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
} }
private fun changeState(state: BaseService.State, msg: String? = null) { private fun changeState(state: BaseService.State, msg: String? = null) {
val context = context ?: return
fab.isEnabled = state.canStop || state == BaseService.State.Stopped fab.isEnabled = state.canStop || state == BaseService.State.Stopped
fab.setTitle(when (state) { fab.setTitle(when (state) {
BaseService.State.Connecting -> R.string.connecting BaseService.State.Connecting -> R.string.connecting
...@@ -118,9 +119,9 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo ...@@ -118,9 +119,9 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
tester.status.removeObservers(this) tester.status.removeObservers(this)
if (state != BaseService.State.Idle) tester.invalidate() if (state != BaseService.State.Idle) tester.invalidate()
} else tester.status.observe(this, Observer { } else tester.status.observe(this, Observer {
it.retrieve(stats::setTitle) { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() } it.retrieve(stats::setTitle) { Toast.makeText(context, it, Toast.LENGTH_LONG).show() }
}) })
if (msg != null) Toast.makeText(requireContext(), 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
val stopped = state == BaseService.State.Stopped val stopped = state == BaseService.State.Stopped
controlImport.isEnabled = stopped controlImport.isEnabled = stopped
......
...@@ -18,6 +18,7 @@ ...@@ -18,6 +18,7 @@
app:key="control.export" app:key="control.export"
app:title="@string/action_export_file"/> app:title="@string/action_export_file"/>
<PreferenceCategory <PreferenceCategory
app:key="settings"
app:title="@string/settings" app:title="@string/settings"
app:initialExpandedChildrenCount="3"> app:initialExpandedChildrenCount="3">
<SwitchPreference <SwitchPreference
......
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