Commit 98ed1f78 authored by Mygod's avatar Mygod

Merge branch 'master' into q-beta

parents ebda36ee 5e4e82b3
...@@ -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)
......
...@@ -29,4 +29,4 @@ ...@@ -29,4 +29,4 @@
branch = stable branch = stable
[submodule "core/src/main/jni/libev"] [submodule "core/src/main/jni/libev"]
path = core/src/main/jni/libev path = core/src/main/jni/libev
url = https://github.com/shadowsocks/libev.git url = https://git.lighttpd.net/libev.git
...@@ -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.30'
minSdkVersion = 21 minSdkVersion = 21
sdkVersion = 28 sdkVersion = 28
compileSdkVersion = 'android-Q' compileSdkVersion = 'android-Q'
...@@ -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']
} }
......
...@@ -44,28 +44,25 @@ androidExtensions { ...@@ -44,28 +44,25 @@ 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.0.0' api 'androidx.preference:preference:1.0.0'
api "androidx.room:room-runtime:$roomVersion" 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.crashlytics.sdk.android:crashlytics:2.9.9'
api 'com.google.firebase:firebase-config:16.4.1' api 'com.google.firebase:firebase-config:16.5.0'
api 'com.google.firebase:firebase-core:16.0.8' api 'com.google.firebase:firebase-core:16.0.8'
api "com.takisoft.preferencex:preferencex:$preferencexVersion" api "com.takisoft.preferencex:preferencex:$preferencexVersion"
api 'dnsjava:dnsjava:2.1.8' api 'dnsjava:dnsjava:2.1.8'
api 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1' api 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.0'
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'
} }
...@@ -51,6 +51,8 @@ import com.github.shadowsocks.utils.* ...@@ -51,6 +51,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
...@@ -99,6 +101,8 @@ object Core { ...@@ -99,6 +101,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
......
...@@ -255,7 +255,7 @@ object BaseService { ...@@ -255,7 +255,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() } }
} }
} }
...@@ -40,7 +40,7 @@ object LocalDnsService { ...@@ -40,7 +40,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()
......
...@@ -131,11 +131,11 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface { ...@@ -131,11 +131,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() } }
} }
} }
...@@ -61,7 +61,7 @@ object DefaultNetworkListener { ...@@ -61,7 +61,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()
......
...@@ -73,8 +73,7 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAd ...@@ -73,8 +73,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)
...@@ -170,8 +169,8 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAd ...@@ -170,8 +169,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()
} }
} }
......
...@@ -114,7 +114,7 @@ include $(CLEAR_VARS) ...@@ -114,7 +114,7 @@ include $(CLEAR_VARS)
LIBEVENT_SOURCES := \ LIBEVENT_SOURCES := \
buffer.c bufferevent.c event.c \ buffer.c bufferevent.c event.c \
bufferevent_sock.c bufferevent_ratelim.c \ bufferevent_sock.c bufferevent_ratelim.c \
evthread.c log.c evutil.c evutil_time.c evmap.c epoll.c poll.c signal.c select.c evthread.c log.c evutil.c evutil_rand.c evutil_time.c evmap.c epoll.c poll.c signal.c select.c
LOCAL_MODULE := event LOCAL_MODULE := event
LOCAL_SRC_FILES := $(addprefix libevent/, $(LIBEVENT_SOURCES)) LOCAL_SRC_FILES := $(addprefix libevent/, $(LIBEVENT_SOURCES))
......
Subproject commit e8660cfae85762d3d3eaf72c47b8478f1c9f0e22 Subproject commit 6e14849aa213cbb028aeb0c75905438260fd867c
libev @ 31ca40b7
Subproject commit 5213419011ec5de302f8bc1716f348de1fb53b12 Subproject commit 31ca40b7a18ed424213a4ecba0d57706ed3e56a0
Subproject commit fec4c92d81189e0394ad94e14f238a837c81385e Subproject commit b732443c442239c2e0184820e9b23cca0de0828c
Subproject commit 4f0929189ab43e020b0d441919abf6bab02baf11 Subproject commit 9f4f8eec93dd1f32d78e0bcceddbef0ca570e66f
Subproject commit 274334f14839431ae003774d99c3d1de337afff4 Subproject commit d9954892ba96627748efab2901f38d7e20d6d0a2
Subproject commit b430124ed82973d4edf345dfadbe556f93377484 Subproject commit b0c6ba494509473e269f7ae5b5e769e09bb9b6ee
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.3.1-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
...@@ -28,7 +28,7 @@ APP_NAME="Gradle" ...@@ -28,7 +28,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"'
# 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"
......
...@@ -14,7 +14,7 @@ set APP_BASE_NAME=%~n0 ...@@ -14,7 +14,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"
@rem Find java.exe @rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome if defined JAVA_HOME goto findJavaFromJavaHome
......
...@@ -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
......
...@@ -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))
......
...@@ -33,6 +33,7 @@ import androidx.recyclerview.widget.LinearLayoutManager ...@@ -33,6 +33,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.github.shadowsocks.database.Profile import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.plugin.PluginConfiguration
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.resolveResourceId import com.github.shadowsocks.utils.resolveResourceId
...@@ -61,7 +62,7 @@ class UdpFallbackProfileActivity : AppCompatActivity() { ...@@ -61,7 +62,7 @@ class UdpFallbackProfileActivity : AppCompatActivity() {
inner class ProfilesAdapter : RecyclerView.Adapter<ProfileViewHolder>() { inner class ProfilesAdapter : RecyclerView.Adapter<ProfileViewHolder>() {
internal val profiles = (ProfileManager.getAllProfiles()?.toMutableList() ?: mutableListOf()) internal val profiles = (ProfileManager.getAllProfiles()?.toMutableList() ?: mutableListOf())
.filter { it.id != editingId && it.plugin.isNullOrEmpty() } .filter { it.id != editingId && PluginConfiguration(it.plugin ?: "").selected.isEmpty() }
override fun onBindViewHolder(holder: ProfileViewHolder, position: Int) = override fun onBindViewHolder(holder: ProfileViewHolder, position: Int) =
holder.bind(if (position == 0) null else profiles[position - 1]) holder.bind(if (position == 0) null else profiles[position - 1])
......
...@@ -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,14 +169,20 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener, ...@@ -170,14 +169,20 @@ 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 {
when (Template.values()[templateSelector.selectedItemPosition]) { AclEditResult(editText.text.toString().let { text ->
Template.Generic -> AclItem(text) when (Template.values()[templateSelector.selectedItemPosition]) {
Template.Domain -> AclItem(TEMPLATE_REGEX_DOMAIN.format(Locale.ENGLISH, IDN.toASCII(text, Template.Generic -> AclItem(text)
IDN.ALLOW_UNASSIGNED or IDN.USE_STD3_ASCII_RULES).replace(".", "\\."))) Template.Domain -> AclItem(IDN.toASCII(text, IDN.ALLOW_UNASSIGNED or IDN.USE_STD3_ASCII_RULES)
Template.Url -> AclItem(text, true) .replace(".", "\\.").let { "(^|\\.)$it\$" })
} 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),
......
...@@ -49,7 +49,7 @@ mavenPublish { ...@@ -49,7 +49,7 @@ 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-alpha04' api 'com.google.android.material:material:1.1.0-alpha05'
api "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" api "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
testImplementation "junit:junit:$junitVersion" testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test:runner:$androidTestVersion" androidTestImplementation "androidx.test:runner:$androidTestVersion"
......
...@@ -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,25 +42,22 @@ class PluginOptions : HashMap<String, String?> { ...@@ -42,25 +42,22 @@ 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() "\\" -> current.append(tokenizer.nextToken())
when (nextToken) { "=" -> if (key == null) {
"\\" -> current.append(tokenizer.nextToken()) key = current.toString()
"=" -> if (key == null) { current.setLength(0)
key = current.toString() } else current.append(nextToken)
current.setLength(0) ";" -> {
} else current.append(nextToken) if (key != null) {
";" -> { put(key, current.toString())
if (key != null) { key = null
put(key, current.toString()) } else if (current.isNotEmpty())
key = null if (parseId) id = current.toString() else put(current.toString(), null)
} else if (current.isNotEmpty()) current.setLength(0)
if (parseId) id = current.toString() else put(current.toString(), null) parseId = false
current.setLength(0)
parseId = false
}
else -> current.append(nextToken)
} }
else -> current.append(nextToken)
} }
} }
......
...@@ -37,7 +37,7 @@ android { ...@@ -37,7 +37,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
......
...@@ -92,7 +92,7 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo ...@@ -92,7 +92,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)),
...@@ -102,6 +102,7 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo ...@@ -102,6 +102,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
...@@ -116,9 +117,9 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo ...@@ -116,9 +117,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
if (state == BaseService.State.Stopped) { if (state == BaseService.State.Stopped) {
controlImport.isEnabled = true controlImport.isEnabled = true
......
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