Commit 2e289597 authored by Mygod's avatar Mygod

Refactor using coroutines

parent c0ad5841
......@@ -5,6 +5,7 @@ apply plugin: 'com.github.ben-manes.versions'
buildscript {
ext {
kotlinVersion = '1.3.11'
kotlinCoroutinesVersion = '1.1.0'
minSdkVersion = 21
sdkVersion = 28
compileSdkVersion = 28
......
......@@ -62,13 +62,15 @@ dependencies {
api "android.arch.work:work-runtime-ktx:$workVersion"
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.0.0'
api "androidx.room:room-runtime:$roomVersion"
api 'com.crashlytics.sdk.android:crashlytics:2.9.8'
api 'com.google.firebase:firebase-config:16.1.3'
api 'com.google.firebase:firebase-core:16.0.6'
api 'com.squareup.okhttp3:okhttp:3.12.1'
api "com.takisoft.preferencex:preferencex:$preferencexVersion"
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinCoroutinesVersion"
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinCoroutinesVersion"
kapt "androidx.lifecycle:lifecycle-compiler:$lifecycleVersion"
kapt "androidx.room:room-compiler:$roomVersion"
testImplementation "androidx.arch.core:core-testing:$lifecycleVersion"
......
......@@ -51,6 +51,8 @@ import com.github.shadowsocks.utils.*
import com.google.firebase.FirebaseApp
import com.google.firebase.analytics.FirebaseAnalytics
import io.fabric.sdk.android.Fabric
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import java.io.File
import java.io.IOException
import kotlin.reflect.KClass
......@@ -106,7 +108,7 @@ object Core {
// handle data restored/crash
if (Build.VERSION.SDK_INT >= 24 && DataStore.directBootAware &&
app.getSystemService<UserManager>()?.isUserUnlocked == true) DirectBoot.flushTrafficStats()
if (DataStore.tcpFastOpen && !TcpFastOpen.sendEnabled) TcpFastOpen.enableAsync()
if (DataStore.tcpFastOpen && !TcpFastOpen.sendEnabled) TcpFastOpen.enableTimeout()
if (DataStore.publicStore.getLong(Key.assetUpdateTime, -1) != packageInfo.lastUpdateTime) {
val assetManager = app.assets
for (dir in arrayOf("acl", "overture"))
......
......@@ -39,8 +39,8 @@ import com.github.shadowsocks.plugin.PluginManager
import com.github.shadowsocks.utils.Action
import com.github.shadowsocks.utils.broadcastReceiver
import com.github.shadowsocks.utils.printLog
import com.github.shadowsocks.utils.thread
import com.google.firebase.analytics.FirebaseAnalytics
import kotlinx.coroutines.*
import java.io.File
import java.net.UnknownHostException
import java.util.*
......@@ -62,16 +62,16 @@ object BaseService {
const val CONFIG_FILE_UDP = "shadowsocks-udp.conf"
class Data internal constructor(private val service: Interface) {
@Volatile var state = STOPPED
val processes = GuardedProcessPool()
@Volatile var proxy: ProxyInstance? = null
@Volatile var udpFallback: ProxyInstance? = null
var state = STOPPED
var processes: GuardedProcessPool? = null
var proxy: ProxyInstance? = null
var udpFallback: ProxyInstance? = null
var notification: ServiceNotification? = null
val closeReceiver = broadcastReceiver { _, intent ->
when (intent.action) {
Action.RELOAD -> service.forceLoad()
else -> service.stopRunner(true)
else -> service.stopRunner()
}
}
var closeReceiverRegistered = false
......@@ -101,12 +101,13 @@ object BaseService {
}
private fun broadcast(work: (IShadowsocksServiceCallback) -> Unit) {
val n = callbacks.beginBroadcast()
for (i in 0 until n) try {
work(callbacks.getBroadcastItem(i))
repeat(callbacks.beginBroadcast()) {
try {
work(callbacks.getBroadcastItem(it))
} catch (e: Exception) {
printLog(e)
}
}
callbacks.finishBroadcast()
}
......@@ -193,26 +194,23 @@ object BaseService {
fun forceLoad() {
val (profile, fallback) = Core.currentProfile
?: return stopRunner(true, (this as Context).getString(R.string.profile_empty))
?: return stopRunner(false, (this as Context).getString(R.string.profile_empty))
if (profile.host.isEmpty() || profile.password.isEmpty() ||
fallback != null && (fallback.host.isEmpty() || fallback.password.isEmpty())) {
stopRunner(true, (this as Context).getString(R.string.proxy_empty))
stopRunner(false, (this as Context).getString(R.string.proxy_empty))
return
}
val s = data.state
when (s) {
STOPPED -> startRunner()
CONNECTED -> {
stopRunner(false)
startRunner()
}
CONNECTED -> stopRunner(true)
else -> Crashlytics.log(Log.WARN, tag, "Illegal state when invoking use: $s")
}
}
fun buildAdditionalArguments(cmd: ArrayList<String>): ArrayList<String> = cmd
fun startNativeProcesses() {
suspend fun startProcesses() {
val configRoot = (if (Build.VERSION.SDK_INT < 24 || app.getSystemService<UserManager>()
?.isUserUnlocked != false) app else Core.deviceStorage).noBackupFilesDir
val udpFallback = data.udpFallback
......@@ -233,19 +231,24 @@ object BaseService {
else startService(Intent(this, javaClass))
}
fun killProcesses() = data.processes.killAll()
suspend fun killProcesses() {
data.processes?.run {
close()
data.processes = null
}
}
fun stopRunner(stopService: Boolean, msg: String? = null) {
fun stopRunner(restart: Boolean = false, msg: String? = null) {
// channge the state
val data = data
data.changeState(STOPPING)
GlobalScope.launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED) {
Core.analytics.logEvent("stop", bundleOf(Pair(FirebaseAnalytics.Param.METHOD, tag)))
killProcesses()
// clean up recevier
this as Service
this@Interface as Service
val data = data
if (data.closeReceiverRegistered) {
unregisterReceiver(data.closeReceiver)
data.closeReceiverRegistered = false
......@@ -265,7 +268,8 @@ object BaseService {
data.changeState(STOPPED, msg)
// stop the service if nothing has bound to it
if (stopService) stopSelf()
if (restart) startRunner() else stopSelf()
}
}
fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
......@@ -276,7 +280,7 @@ object BaseService {
if (profilePair == null) {
// gracefully shutdown: https://stackoverflow.com/q/47337857/2245107
data.notification = createNotification("")
stopRunner(true, getString(R.string.profile_empty))
stopRunner(false, getString(R.string.profile_empty))
return Service.START_NOT_STICKY
}
val (profile, fallback) = profilePair
......@@ -299,14 +303,14 @@ object BaseService {
data.changeState(CONNECTING)
thread("$tag-Connecting") {
GlobalScope.launch(Dispatchers.Main) {
try {
proxy.init()
data.udpFallback?.init()
// Clean up
killProcesses()
startNativeProcesses()
killProcesses()
data.processes = GuardedProcessPool()
startProcesses()
proxy.scheduleUpdate()
data.udpFallback?.scheduleUpdate()
......@@ -314,12 +318,12 @@ object BaseService {
data.changeState(CONNECTED)
} catch (_: UnknownHostException) {
stopRunner(true, getString(R.string.invalid_server))
stopRunner(false, getString(R.string.invalid_server))
} catch (exc: Throwable) {
if (exc !is PluginManager.PluginNotFoundException && exc !is VpnService.NullConnectionException) {
printLog(exc)
}
stopRunner(true, "${getString(R.string.service_failed)}: ${exc.localizedMessage}")
stopRunner(false, "${getString(R.string.service_failed)}: ${exc.localizedMessage}")
}
}
return Service.START_NOT_STICKY
......
......@@ -26,76 +26,69 @@ import android.system.ErrnoException
import android.system.Os
import android.system.OsConstants
import android.util.Log
import androidx.annotation.MainThread
import com.crashlytics.android.Crashlytics
import com.github.shadowsocks.Core
import com.github.shadowsocks.utils.Commandline
import com.github.shadowsocks.utils.thread
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.selects.select
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.util.concurrent.ArrayBlockingQueue
import java.util.concurrent.atomic.AtomicReference
import java.util.concurrent.TimeUnit
import kotlin.concurrent.thread
class GuardedProcessPool {
companion object Dummy : IOException("Oopsie the developer has made a no-no") {
class GuardedProcessPool : CoroutineScope {
companion object {
private const val TAG = "GuardedProcessPool"
private val ProcessImpl by lazy { Class.forName("java.lang.ProcessManager\$ProcessImpl") }
private val pid by lazy { ProcessImpl.getDeclaredField("pid").apply { isAccessible = true } }
private val exitValueMutex by lazy {
ProcessImpl.getDeclaredField("exitValueMutex").apply { isAccessible = true }
private val pid by lazy {
Class.forName("java.lang.ProcessManager\$ProcessImpl").getDeclaredField("pid").apply { isAccessible = true }
}
}
private inner class Guard(private val cmd: List<String>, private val onRestartCallback: (() -> Unit)?) {
val cmdName = File(cmd.first()).nameWithoutExtension
val excQueue = ArrayBlockingQueue<IOException>(1) // ArrayBlockingQueue doesn't want null
private var pushed = false
private inner class Guard(private val cmd: List<String>) {
val abortChannel = Channel<Unit>()
private val exitChannel = Channel<Int>()
private val cmdName = File(cmd.first()).nameWithoutExtension
private var startTime: Long = -1
private lateinit var process: Process
private fun streamLogger(input: InputStream, logger: (String, String) -> Int) =
thread("StreamLogger-$cmdName") {
try {
private fun streamLogger(input: InputStream, logger: (String, String) -> Int) = try {
input.bufferedReader().forEachLine { logger(TAG, it) }
} catch (_: IOException) { } // ignore
}
private fun pushException(ioException: IOException?) {
if (pushed) return
excQueue.put(ioException ?: Dummy)
pushed = true
fun start() {
startTime = SystemClock.elapsedRealtime()
process = ProcessBuilder(cmd).directory(Core.deviceStorage.noBackupFilesDir).start()
}
fun looper(host: HashSet<Thread>) {
var process: Process? = null
suspend fun looper(onRestartCallback: (() -> Unit)?) {
var running = true
try {
var callback: (() -> Unit)? = null
while (guardThreads.get() === host) {
Crashlytics.log(Log.DEBUG, TAG, "start process: " + Commandline.toString(cmd))
val startTime = SystemClock.elapsedRealtime()
process = ProcessBuilder(cmd)
.redirectErrorStream(true)
.directory(Core.deviceStorage.noBackupFilesDir)
.start()
while (true) {
thread(name = "stderr-$cmdName") { streamLogger(process.errorStream, Log::e) }
thread(name = "stdout-$cmdName") {
runBlocking {
streamLogger(process.inputStream, Log::i)
streamLogger(process.errorStream, Log::e)
if (callback == null) callback = onRestartCallback else callback()
pushException(null)
process.waitFor()
exitChannel.send(process.waitFor()) // this thread also acts as a daemon thread for waitFor
}
}
if (select {
abortChannel.onReceive { true } // prefer abort to save work
exitChannel.onReceive { false }
}) break
running = false
if (SystemClock.elapsedRealtime() - startTime < 1000) {
Crashlytics.log(Log.WARN, TAG, "process exit too fast, stop guard: $cmdName")
break
}
Crashlytics.log(Log.DEBUG, TAG, "restart process: " + Commandline.toString(cmd))
start()
running = true
onRestartCallback?.invoke()
}
} catch (_: InterruptedException) {
Crashlytics.log(Log.DEBUG, TAG, "thread interrupt, destroy process: $cmdName")
} catch (e: IOException) {
pushException(e)
} finally {
if (process != null) {
if (!running) return // process already exited, nothing to be done
if (Build.VERSION.SDK_INT < 24) {
val pid = pid.get(process) as Int
try {
......@@ -103,58 +96,34 @@ class GuardedProcessPool {
} catch (e: ErrnoException) {
if (e.errno != OsConstants.ESRCH) throw e
}
val mutex = exitValueMutex.get(process) as Object
synchronized(mutex) {
try {
process.exitValue()
} catch (e: IllegalThreadStateException) {
mutex.wait(500)
if (withTimeoutOrNull(500) { exitChannel.receive() } != null) return
}
}
}
process.destroy() // kill the process
if (Build.VERSION.SDK_INT >= 26) {
val isKilled = process.waitFor(1L, TimeUnit.SECONDS) // wait for 1 second
if (!isKilled) {
if (withTimeoutOrNull(1000) { exitChannel.receive() } != null) return
process.destroyForcibly() // Force to kill the process if it's still alive
}
}
process.waitFor() // ensure the process is destroyed
}
pushException(null)
exitChannel.receive()
}
}
}
/**
* This is an indication of which thread pool is being active.
* Reading/writing this collection still needs an additional lock to prevent concurrent modification.
*/
private val guardThreads = AtomicReference<HashSet<Thread>>(HashSet())
private val supervisor = SupervisorJob()
override val coroutineContext get() = Dispatchers.Main + supervisor
private val guards = ArrayList<Guard>()
fun start(cmd: List<String>, onRestartCallback: (() -> Unit)? = null): GuardedProcessPool {
val guard = Guard(cmd, onRestartCallback)
val guardThreads = guardThreads.get()
synchronized(guardThreads) {
guardThreads.add(thread("GuardThread-${guard.cmdName}") {
guard.looper(guardThreads)
})
}
val ioException = guard.excQueue.take()
if (ioException !== Dummy) throw ioException
return this
@MainThread
suspend fun start(cmd: List<String>, onRestartCallback: (() -> Unit)? = null) {
Crashlytics.log(Log.DEBUG, TAG, "start process: " + Commandline.toString(cmd))
val guard = Guard(cmd)
guard.start()
guards += guard
launch(start = CoroutineStart.UNDISPATCHED) { guard.looper(onRestartCallback) }
}
fun killAll() {
val guardThreads = guardThreads.getAndSet(HashSet())
synchronized(guardThreads) {
guardThreads.forEach { it.interrupt() }
try {
guardThreads.forEach { it.join() }
} catch (_: InterruptedException) { }
}
@MainThread
suspend fun close() {
guards.forEach { it.abortChannel.send(Unit) }
supervisor.children.forEach { it.join() }
}
}
......@@ -32,8 +32,8 @@ import org.json.JSONObject
object LocalDnsService {
interface Interface : BaseService.Interface {
override fun startNativeProcesses() {
super.startNativeProcesses()
override suspend fun startProcesses() {
super.startProcesses()
val data = data
val profile = data.proxy!!.profile
......@@ -86,7 +86,7 @@ object LocalDnsService {
})
}
if (!profile.udpdns) data.processes.start(buildAdditionalArguments(arrayListOf(
if (!profile.udpdns) data.processes!!.start(buildAdditionalArguments(arrayListOf(
File(app.applicationInfo.nativeLibraryDir, Executable.OVERTURE).absolutePath,
"-c", buildOvertureConfig("overture.conf"))))
}
......
......@@ -30,7 +30,12 @@ import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.plugin.PluginConfiguration
import com.github.shadowsocks.plugin.PluginManager
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.*
import com.github.shadowsocks.utils.DirectBoot
import com.github.shadowsocks.utils.parseNumericAddress
import com.github.shadowsocks.utils.signaturesCompat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import okhttp3.FormBody
import okhttp3.OkHttpClient
import okhttp3.Request
......@@ -39,7 +44,6 @@ import java.io.IOException
import java.net.InetAddress
import java.net.UnknownHostException
import java.security.MessageDigest
import java.util.concurrent.TimeUnit
/**
* This class sets up environment for ss-local.
......@@ -50,13 +54,9 @@ class ProxyInstance(val profile: Profile, private val route: String = profile.ro
private val plugin = PluginConfiguration(profile.plugin ?: "").selectedOptions
val pluginPath by lazy { PluginManager.init(plugin) }
fun init() {
suspend fun init() {
if (profile.host == "198.199.101.152") {
val client = OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
val client = OkHttpClient.Builder().build()
val mdg = MessageDigest.getInstance("SHA-1")
mdg.update(Core.packageInfo.signaturesCompat.first().toByteArray())
val requestBody = FormBody.Builder()
......@@ -67,8 +67,9 @@ class ProxyInstance(val profile: Profile, private val route: String = profile.ro
.post(requestBody)
.build()
val proxies = client.newCall(request).execute()
.body()!!.string().split('|').toMutableList()
val proxies = withTimeout(30_000) {
withContext(Dispatchers.IO) { client.newCall(request).execute().body()!!.string() }
}.split('|').toMutableList()
proxies.shuffle()
val proxy = proxies.first().split(':')
profile.host = proxy[0].trim()
......@@ -80,22 +81,16 @@ class ProxyInstance(val profile: Profile, private val route: String = profile.ro
if (route == Acl.CUSTOM_RULES) Acl.save(Acl.CUSTOM_RULES, Acl.customRules.flatten(10))
// it's hard to resolve DNS on a specific interface so we'll do it here
if (profile.host.parseNumericAddress() == null) {
thread("ProxyInstance-resolve") {
// A WAR fix for Huawei devices that UnknownHostException cannot be caught correctly
try {
profile.host = InetAddress.getByName(profile.host).hostAddress ?: ""
} catch (_: UnknownHostException) { }
}.join(10 * 1000)
if (profile.host.parseNumericAddress() == null) throw UnknownHostException()
}
if (profile.host.parseNumericAddress() == null) profile.host = withTimeout(10_000) {
withContext(Dispatchers.IO) { InetAddress.getByName(profile.host).hostAddress }
} ?: throw UnknownHostException()
}
/**
* Sensitive shadowsocks configuration file requires extra protection. It may be stored in encrypted storage or
* device storage, depending on which is currently available.
*/
fun start(service: BaseService.Interface, stat: File, configFile: File, extraFlag: String? = null) {
suspend fun start(service: BaseService.Interface, stat: File, configFile: File, extraFlag: String? = null) {
trafficMonitor = TrafficMonitor(stat)
this.configFile = configFile
......@@ -122,7 +117,7 @@ class ProxyInstance(val profile: Profile, private val route: String = profile.ro
if (DataStore.tcpFastOpen) cmd += "--fast-open"
service.data.processes.start(cmd)
service.data.processes!!.start(cmd)
}
fun scheduleUpdate() {
......
......@@ -36,7 +36,7 @@ class TransproxyService : Service(), LocalDnsService.Interface {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int =
super<LocalDnsService.Interface>.onStartCommand(intent, flags, startId)
private fun startDNSTunnel() {
private suspend fun startDNSTunnel() {
val proxy = data.proxy!!
val cmd = arrayListOf(File(applicationInfo.nativeLibraryDir, Executable.SS_TUNNEL).absolutePath,
"-t", "10",
......@@ -47,10 +47,10 @@ class TransproxyService : Service(), LocalDnsService.Interface {
// config is already built by BaseService.Interface
"-c", (data.udpFallback ?: proxy).configFile!!.absolutePath)
if (DataStore.tcpFastOpen) cmd += "--fast-open"
data.processes.start(cmd)
data.processes!!.start(cmd)
}
private fun startRedsocksDaemon() {
private suspend fun startRedsocksDaemon() {
File(Core.deviceStorage.noBackupFilesDir, "redsocks.conf").writeText("""base {
log_debug = off;
log_info = off;
......@@ -66,13 +66,13 @@ redsocks {
type = socks5;
}
""")
data.processes.start(listOf(
data.processes!!.start(listOf(
File(applicationInfo.nativeLibraryDir, Executable.REDSOCKS).absolutePath, "-c", "redsocks.conf"))
}
override fun startNativeProcesses() {
override suspend fun startProcesses() {
startRedsocksDaemon()
super.startNativeProcesses()
super.startProcesses()
if (data.proxy!!.profile.udpdns) startDNSTunnel()
}
......
......@@ -139,9 +139,9 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
else -> super<LocalDnsService.Interface>.onBind(intent)
}
override fun onRevoke() = stopRunner(true)
override fun onRevoke() = stopRunner()
override fun killProcesses() {
override suspend fun killProcesses() {
if (listeningForDefaultNetwork) {
connectivity.unregisterNetworkCallback(defaultNetworkCallback)
listeningForDefaultNetwork = false
......@@ -159,16 +159,16 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
startActivity(Intent(this, VpnRequestActivity::class.java)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
else return super<LocalDnsService.Interface>.onStartCommand(intent, flags, startId)
stopRunner(true)
stopRunner()
return Service.START_NOT_STICKY
}
override fun startNativeProcesses() {
override suspend fun startProcesses() {
val worker = ProtectWorker()
worker.start()
this.worker = worker
super.startNativeProcesses()
super.startProcesses()
sendFd(startVpn())
}
......@@ -178,7 +178,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
return cmd
}
private fun startVpn(): FileDescriptor {
private suspend fun startVpn(): FileDescriptor {
val profile = data.proxy!!.profile
val builder = Builder()
.setConfigureIntent(Core.configureIntent(this))
......@@ -247,11 +247,11 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
cmd += "--dnsgw"
cmd += "127.0.0.1:${DataStore.portLocalDns}"
}
data.processes.start(cmd, onRestartCallback = {
data.processes!!.start(cmd, onRestartCallback = {
try {
sendFd(conn.fileDescriptor)
} catch (e: ErrnoException) {
stopRunner(true, e.message)
stopRunner(false, e.message)
}
})
return conn.fileDescriptor
......
......@@ -26,9 +26,9 @@ import androidx.lifecycle.ViewModel
import com.github.shadowsocks.Core
import com.github.shadowsocks.Core.app
import com.github.shadowsocks.acl.Acl
import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.core.R
import com.github.shadowsocks.preference.DataStore
import kotlinx.coroutines.*
import java.io.IOException
import java.net.HttpURLConnection
import java.net.InetSocketAddress
......@@ -72,14 +72,12 @@ class HttpsTest : ViewModel() {
}
}
private var testCount = 0
private var running: Pair<HttpURLConnection, Job>? = null
val status = MutableLiveData<Status>().apply { value = Status.Idle }
fun testConnection() {
++testCount
cancelTest()
status.value = Status.Testing
val id = testCount // it would change by other code
thread("ConnectionTest") {
val url = URL("https", when (Core.currentProfile!!.first.route) {
Acl.CHINALIST -> "www.qualcomm.cn"
else -> "www.google.com"
......@@ -90,7 +88,9 @@ class HttpsTest : ViewModel() {
conn.setRequestProperty("Connection", "close")
conn.instanceFollowRedirects = false
conn.useCaches = false
val result = try {
running = conn to GlobalScope.launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED) {
status.value = withContext(Dispatchers.IO) {
try {
val start = SystemClock.elapsedRealtime()
val code = conn.responseCode
val elapsed = SystemClock.elapsedRealtime() - start
......@@ -101,12 +101,18 @@ class HttpsTest : ViewModel() {
} finally {
conn.disconnect()
}
if (testCount == id) status.postValue(result)
}
}
}
private fun cancelTest() = running?.let { (conn, job) ->
job.cancel() // ensure job is cancelled before interrupting
conn.disconnect()
running = null
}
fun invalidate() {
++testCount
cancelTest()
status.value = Status.Idle
}
}
......@@ -20,6 +20,8 @@
package com.github.shadowsocks.utils
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeoutOrNull
import java.io.File
import java.io.IOException
......@@ -58,5 +60,5 @@ object TcpFastOpen {
e.localizedMessage
}
}
fun enableAsync() = thread("TcpFastOpen") { enable() }.join(1000)
fun enableTimeout() = runBlocking { withTimeoutOrNull(1000) { enable() } }
}
......@@ -54,17 +54,6 @@ fun broadcastReceiver(callback: (Context, Intent) -> Unit): BroadcastReceiver =
override fun onReceive(context: Context, intent: Intent) = callback(context, intent)
}
/**
* Wrapper for kotlin.concurrent.thread that tracks uncaught exceptions.
*/
fun thread(name: String? = null, start: Boolean = true, isDaemon: Boolean = false,
contextClassLoader: ClassLoader? = null, priority: Int = -1, block: () -> Unit): Thread {
val thread = kotlin.concurrent.thread(false, isDaemon, contextClassLoader, name, priority, block)
thread.setUncaughtExceptionHandler { _, t -> printLog(t) }
if (start) thread.start()
return thread
}
val URLConnection.responseLength: Long
get() = if (Build.VERSION.SDK_INT >= 24) contentLengthLong else contentLength.toLong()
......
......@@ -32,10 +32,10 @@ import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.os.Handler
import android.view.*
import android.widget.ImageView
import android.widget.Switch
import androidx.annotation.UiThread
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.core.content.getSystemService
......@@ -47,9 +47,8 @@ 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.thread
import com.google.android.material.snackbar.Snackbar
import java.util.concurrent.atomic.AtomicBoolean
import kotlinx.coroutines.*
class AppManager : AppCompatActivity() {
companion object {
......@@ -58,13 +57,13 @@ class AppManager : AppCompatActivity() {
private var receiver: BroadcastReceiver? = null
private var cachedApps: List<PackageInfo>? = null
private fun getApps(pm: PackageManager) = synchronized(AppManager) {
private suspend fun getApps(pm: PackageManager) = synchronized(AppManager) {
if (receiver == null) receiver = Core.listenForPackageChanges {
synchronized(AppManager) {
receiver = null
cachedApps = null
}
AppManager.instance?.reloadApps()
AppManager.instance?.loadApps()
}
// Labels and icons can change on configuration (locale, etc.) changes, therefore they are not cached.
val cachedApps = cachedApps ?: pm.getInstalledPackages(PackageManager.GET_PERMISSIONS)
......@@ -72,7 +71,10 @@ class AppManager : AppCompatActivity() {
it.requestedPermissions?.contains(Manifest.permission.INTERNET) ?: false }
this.cachedApps = cachedApps
cachedApps
}.map { ProxiedApp(pm, it.applicationInfo, it.packageName) }
}.map {
yield()
ProxiedApp(pm, it.applicationInfo, it.packageName)
}
}
private class ProxiedApp(private val pm: PackageManager, private val appInfo: ApplicationInfo,
......@@ -106,17 +108,15 @@ class AppManager : AppCompatActivity() {
proxiedApps.add(item.packageName)
check.isChecked = true
}
if (!appsLoading.get()) {
DataStore.individual = proxiedApps.joinToString("\n")
DataStore.dirty = true
}
}
}
private inner class AppsAdapter : RecyclerView.Adapter<AppViewHolder>() {
private var apps = listOf<ProxiedApp>()
fun reload() {
suspend fun reload() {
apps = getApps(packageManager)
.sortedWith(compareBy({ !proxiedApps.contains(it.packageName) }, { it.name.toString() }))
}
......@@ -132,38 +132,36 @@ class AppManager : AppCompatActivity() {
private lateinit var bypassSwitch: Switch
private lateinit var appListView: RecyclerView
private lateinit var loadingView: View
private val appsLoading = AtomicBoolean()
private val handler = Handler()
private val clipboard by lazy { getSystemService<ClipboardManager>()!! }
private var loader: Job? = null
private val shortAnimTime by lazy { resources.getInteger(android.R.integer.config_shortAnimTime).toLong() }
private fun View.crossFadeFrom(other: View) {
clearAnimation()
other.clearAnimation()
if (visibility == View.VISIBLE && other.visibility == View.GONE) return
alpha = 0F
visibility = View.VISIBLE
animate().alpha(1F).duration = shortAnimTime
other.animate().alpha(0F).setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
other.visibility = View.GONE
}
}).duration = shortAnimTime
}
private fun initProxiedApps(str: String = DataStore.individual) {
proxiedApps = str.split('\n').toHashSet()
}
private fun reloadApps() {
if (!appsLoading.compareAndSet(true, false)) loadAppsAsync()
}
private fun loadAppsAsync() {
if (!appsLoading.compareAndSet(false, true)) return
appListView.visibility = View.GONE
loadingView.visibility = View.VISIBLE
thread("AppManager-loader") {
@UiThread
private fun loadApps() {
loader?.cancel()
loader = GlobalScope.launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED) {
loadingView.crossFadeFrom(appListView)
val adapter = appListView.adapter as AppsAdapter
do {
appsLoading.set(true)
adapter.reload()
} while (!appsLoading.compareAndSet(true, false))
handler.post {
withContext(Dispatchers.IO) { adapter.reload() }
adapter.notifyDataSetChanged()
val shortAnimTime = resources.getInteger(android.R.integer.config_shortAnimTime)
appListView.alpha = 0F
appListView.visibility = View.VISIBLE
appListView.animate().alpha(1F).duration = shortAnimTime.toLong()
loadingView.animate().alpha(0F).setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
loadingView.visibility = View.GONE
}
}).duration = shortAnimTime.toLong()
}
appListView.crossFadeFrom(loadingView)
}
}
......@@ -199,7 +197,7 @@ class AppManager : AppCompatActivity() {
appListView.adapter = AppsAdapter()
instance = this
loadAppsAsync()
loadApps()
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
......@@ -239,7 +237,7 @@ class AppManager : AppCompatActivity() {
DataStore.dirty = true
Snackbar.make(appListView, R.string.action_import_msg, Snackbar.LENGTH_LONG).show()
initProxiedApps(apps)
reloadApps()
loadApps()
return true
} catch (_: IllegalArgumentException) { }
}
......@@ -255,7 +253,7 @@ class AppManager : AppCompatActivity() {
override fun onDestroy() {
instance = null
handler.removeCallbacksAndMessages(null)
loader?.cancel()
super.onDestroy()
}
}
......@@ -55,11 +55,13 @@
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:id="@+id/list"
android:visibility="gone"
app:fastScrollEnabled="true"
app:fastScrollHorizontalThumbDrawable="@drawable/fastscroll_thumb"
app:fastScrollHorizontalTrackDrawable="@drawable/fastscroll_line"
app:fastScrollVerticalThumbDrawable="@drawable/fastscroll_thumb"
app:fastScrollVerticalTrackDrawable="@drawable/fastscroll_line"
tools:listitem="@layout/layout_apps_item"/>
tools:listitem="@layout/layout_apps_item"
tools:visibility="visible"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</LinearLayout>
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