Unverified Commit 3cfc17c2 authored by Mygod's avatar Mygod Committed by GitHub

Merge pull request #2089 from Mygod/coroutines

Refactor using coroutines
parents 1658ac57 2241c5aa
......@@ -4,7 +4,7 @@ apply plugin: 'com.github.ben-manes.versions'
buildscript {
ext {
kotlinVersion = '1.3.11'
kotlinVersion = '1.3.20'
minSdkVersion = 21
sdkVersion = 28
compileSdkVersion = 28
......
......@@ -62,13 +62,13 @@ 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:1.1.0'
kapt "androidx.lifecycle:lifecycle-compiler:$lifecycleVersion"
kapt "androidx.room:room-compiler:$roomVersion"
testImplementation "androidx.arch.core:core-testing:$lifecycleVersion"
......
......@@ -32,8 +32,6 @@ import android.content.IntentFilter
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.UserManager
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
......@@ -60,7 +58,6 @@ object Core {
lateinit var app: Application
lateinit var configureIntent: (Context) -> PendingIntent
val handler by lazy { Handler(Looper.getMainLooper()) }
val packageInfo: PackageInfo by lazy { getPackageInfo(app.packageName) }
val deviceStorage by lazy { if (Build.VERSION.SDK_INT < 24) app else DeviceStorageApp(app) }
val analytics: FirebaseAnalytics by lazy { FirebaseAnalytics.getInstance(deviceStorage) }
......@@ -106,7 +103,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"))
......
......@@ -25,9 +25,9 @@ import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.DeadObjectException
import android.os.Handler
import android.os.IBinder
import android.os.RemoteException
import com.github.shadowsocks.Core
import com.github.shadowsocks.bg.ProxyService
import com.github.shadowsocks.bg.TransproxyService
import com.github.shadowsocks.bg.VpnService
......@@ -38,7 +38,8 @@ import com.github.shadowsocks.utils.Key
/**
* This object should be compact as it will not get GC-ed.
*/
class ShadowsocksConnection(private var listenForDeath: Boolean = false) : ServiceConnection, IBinder.DeathRecipient {
class ShadowsocksConnection(private val handler: Handler = Handler(), private var listenForDeath: Boolean = false) :
ServiceConnection, IBinder.DeathRecipient {
companion object {
val serviceClass get() = when (DataStore.serviceMode) {
Key.modeProxy -> ProxyService::class
......@@ -66,13 +67,13 @@ class ShadowsocksConnection(private var listenForDeath: Boolean = false) : Servi
private var callback: Callback? = null
private val serviceCallback = object : IShadowsocksServiceCallback.Stub() {
override fun stateChanged(state: Int, profileName: String?, msg: String?) {
Core.handler.post { callback!!.stateChanged(state, profileName, msg) }
handler.post { callback!!.stateChanged(state, profileName, msg) }
}
override fun trafficUpdated(profileId: Long, stats: TrafficStats) {
Core.handler.post { callback!!.trafficUpdated(profileId, stats) }
handler.post { callback!!.trafficUpdated(profileId, stats) }
}
override fun trafficPersisted(profileId: Long) {
Core.handler.post { callback!!.trafficPersisted(profileId) }
handler.post { callback!!.trafficPersisted(profileId) }
}
}
private var binder: IBinder? = null
......@@ -110,7 +111,7 @@ class ShadowsocksConnection(private var listenForDeath: Boolean = false) : Servi
override fun binderDied() {
service = null
callback!!.onBinderDied()
handler.post(callback!!::onBinderDied)
}
private fun unregisterCallback() {
......
......@@ -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,21 +62,22 @@ 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
val binder = Binder(this)
var connectingJob: Job? = null
fun changeState(s: Int, msg: String? = null) {
if (state == s && msg == null) return
......@@ -92,6 +93,7 @@ object BaseService {
class Binder(private var data: Data? = null) : IShadowsocksService.Stub(), AutoCloseable {
val callbacks = RemoteCallbackList<IShadowsocksServiceCallback>()
private val bandwidthListeners = HashSet<IBinder>() // the binder is the real identifier
private val handler = Handler()
override fun getState(): Int = data!!.state
override fun getProfileName(): String = data!!.proxy?.profile?.name ?: "Idle"
......@@ -101,17 +103,17 @@ object BaseService {
}
private fun broadcast(work: (IShadowsocksServiceCallback) -> Unit) {
val n = callbacks.beginBroadcast()
for (i in 0 until n) try {
work(callbacks.getBroadcastItem(i))
} catch (e: Exception) {
printLog(e)
repeat(callbacks.beginBroadcast()) {
try {
work(callbacks.getBroadcastItem(it))
} catch (e: Exception) {
printLog(e)
}
}
callbacks.finishBroadcast()
}
private fun registerTimeout() =
Core.handler.postAtTime(this::onTimeout, this, SystemClock.uptimeMillis() + 1000)
private fun registerTimeout() = handler.postDelayed(this::onTimeout, 1000)
private fun onTimeout() {
val proxies = listOfNotNull(data!!.proxy, data!!.udpFallback)
val stats = proxies
......@@ -157,7 +159,7 @@ object BaseService {
override fun stopListeningForBandwidth(cb: IShadowsocksServiceCallback) {
if (bandwidthListeners.remove(cb.asBinder()) && bandwidthListeners.isEmpty()) {
Core.handler.removeCallbacksAndMessages(this)
handler.removeCallbacksAndMessages(null)
}
}
......@@ -179,7 +181,7 @@ object BaseService {
override fun close() {
callbacks.kill()
Core.handler.removeCallbacksAndMessages(this)
handler.removeCallbacksAndMessages(null)
data = null
}
}
......@@ -193,26 +195,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,39 +232,46 @@ 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) {
if (data.state == STOPPING) return
// 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)))
Core.analytics.logEvent("stop", bundleOf(Pair(FirebaseAnalytics.Param.METHOD, tag)))
killProcesses()
killProcesses()
// clean up recevier
this as Service
if (data.closeReceiverRegistered) {
unregisterReceiver(data.closeReceiver)
data.closeReceiverRegistered = false
}
// clean up recevier
this@Interface as Service
val data = data
if (data.closeReceiverRegistered) {
unregisterReceiver(data.closeReceiver)
data.closeReceiverRegistered = false
}
data.notification?.destroy()
data.notification = null
data.notification?.destroy()
data.notification = null
val ids = listOfNotNull(data.proxy, data.udpFallback).map {
it.close()
it.profile.id
}
data.proxy = null
data.binder.trafficPersisted(ids)
val ids = listOfNotNull(data.proxy, data.udpFallback).map {
it.close()
it.profile.id
}
data.proxy = null
data.binder.trafficPersisted(ids)
// change the state
data.changeState(STOPPED, msg)
// change the state
data.changeState(STOPPED, msg)
// stop the service if nothing has bound to it
if (stopService) stopSelf()
// stop the service if nothing has bound to it
if (restart) startRunner() else stopSelf()
}
}
fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
......@@ -276,7 +282,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
......@@ -298,15 +304,18 @@ object BaseService {
Core.analytics.logEvent("start", bundleOf(Pair(FirebaseAnalytics.Param.METHOD, tag)))
data.changeState(CONNECTING)
thread("$tag-Connecting") {
data.connectingJob = GlobalScope.launch(Dispatchers.Main) {
try {
proxy.init()
data.udpFallback?.init()
// Clean up
killProcesses()
startNativeProcesses()
killProcesses()
data.processes = GuardedProcessPool {
printLog(it)
data.connectingJob?.apply { runBlocking { cancelAndJoin() } }
stopRunner(false, it.localizedMessage)
}
startProcesses()
proxy.scheduleUpdate()
data.udpFallback?.scheduleUpdate()
......@@ -314,12 +323,14 @@ 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}")
} finally {
data.connectingJob = null
}
}
return Service.START_NOT_STICKY
......
......@@ -26,135 +26,109 @@ 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(private val onFatal: (IOException) -> Unit) : 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>) {
private val abortChannel = Channel<Unit>()
private var startTime: Long = -1
private lateinit var process: Process
private fun streamLogger(input: InputStream, logger: (String, String) -> Int) =
thread("StreamLogger-$cmdName") {
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 looper(host: HashSet<Thread>) {
var process: Process? = null
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()
private fun streamLogger(input: InputStream, logger: (String) -> Unit) = try {
input.bufferedReader().forEachLine(logger)
} catch (_: IOException) { } // ignore
streamLogger(process.inputStream, Log::i)
streamLogger(process.errorStream, Log::e)
if (callback == null) callback = onRestartCallback else callback()
fun start() {
startTime = SystemClock.elapsedRealtime()
process = ProcessBuilder(cmd).directory(Core.deviceStorage.noBackupFilesDir).start()
}
pushException(null)
process.waitFor()
suspend fun abort() {
if (!abortChannel.isClosedForSend) abortChannel.send(Unit)
}
if (SystemClock.elapsedRealtime() - startTime < 1000) {
Crashlytics.log(Log.WARN, TAG, "process exit too fast, stop guard: $cmdName")
break
suspend fun looper(onRestartCallback: (() -> Unit)?) {
var running = true
val cmdName = File(cmd.first()).nameWithoutExtension
val exitChannel = Channel<Int>()
try {
while (true) {
thread(name = "stderr-$cmdName") { streamLogger(process.errorStream) { Log.e(cmdName, it) } }
thread(name = "stdout-$cmdName") {
runBlocking {
streamLogger(process.inputStream) { Log.i(cmdName, it) }
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) throw IOException("$cmdName exits too fast")
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)
Crashlytics.log(Log.WARN, TAG, "error occurred. stop guard: " + Commandline.toString(cmd))
// calling callback without closing channel first will cause deadlock, therefore we defer it
GlobalScope.launch(Dispatchers.Main) { onFatal(e) }
} finally {
if (process != null) {
if (Build.VERSION.SDK_INT < 24) {
val pid = pid.get(process) as Int
try {
Os.kill(pid, OsConstants.SIGTERM)
} 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)
}
}
}
process.destroy() // kill the process
if (Build.VERSION.SDK_INT >= 26) {
val isKilled = process.waitFor(1L, TimeUnit.SECONDS) // wait for 1 second
if (!isKilled) {
process.destroyForcibly() // Force to kill the process if it's still alive
}
abortChannel.close()
if (!running) return // process already exited, nothing to be done
if (Build.VERSION.SDK_INT < 24) {
try {
Os.kill(pid.get(process) as Int, OsConstants.SIGTERM)
} catch (e: ErrnoException) {
if (e.errno != OsConstants.ESRCH) throw e
}
process.waitFor() // ensure the process is destroyed
if (withTimeoutOrNull(500) { exitChannel.receive() } != null) return
}
process.destroy() // kill the process
if (Build.VERSION.SDK_INT >= 26) {
if (withTimeoutOrNull(1000) { exitChannel.receive() } != null) return
process.destroyForcibly() // Force to kill the process if it's still alive
}
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())
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
private val supervisor = SupervisorJob()
override val coroutineContext get() = Dispatchers.Main + supervisor
private val guards = ArrayList<Guard>()
@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.abort() }
supervisor.children.forEach { it.join() } // we can't cancel the supervisor as we need it to do clean up
}
}
......@@ -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"))))
}
......
......@@ -23,16 +23,19 @@ package com.github.shadowsocks.bg
import android.net.LocalServerSocket
import android.net.LocalSocket
import android.net.LocalSocketAddress
import android.system.ErrnoException
import android.system.Os
import android.system.OsConstants
import com.github.shadowsocks.utils.printLog
import java.io.File
import java.io.IOException
abstract class LocalSocketListener(protected val tag: String) : Thread(tag) {
init {
setUncaughtExceptionHandler { _, t -> printLog(t) }
abstract class LocalSocketListener(name: String, socketFile: File) : Thread(name), AutoCloseable {
private val localSocket = LocalSocket().apply {
socketFile.delete() // It's a must-have to close and reuse previous local socket.
bind(LocalSocketAddress(socketFile.absolutePath, LocalSocketAddress.Namespace.FILESYSTEM))
}
protected abstract val socketFile: File
private val serverSocket = LocalServerSocket(localSocket.fileDescriptor)
@Volatile
private var running = true
......@@ -40,29 +43,25 @@ abstract class LocalSocketListener(protected val tag: String) : Thread(tag) {
* Inherited class do not need to close input/output streams as they will be closed automatically.
*/
protected abstract fun accept(socket: LocalSocket)
final override fun run() {
socketFile.delete() // It's a must-have to close and reuse previous local socket.
LocalSocket().use { localSocket ->
val serverSocket = try {
localSocket.bind(LocalSocketAddress(socketFile.absolutePath, LocalSocketAddress.Namespace.FILESYSTEM))
LocalServerSocket(localSocket.fileDescriptor)
final override fun run() = localSocket.use {
while (running) {
try {
accept(serverSocket.accept())
} catch (e: IOException) {
printLog(e)
return
}
while (running) {
try {
serverSocket.accept()
} catch (e: IOException) {
printLog(e)
continue
}?.use(this::accept)
if (running) printLog(e)
continue
}
}
}
fun stopThread() {
override fun close() {
running = false
interrupt()
// see also: https://issuetracker.google.com/issues/36945762#comment15
try {
Os.shutdown(localSocket.fileDescriptor, OsConstants.SHUT_RDWR)
} catch (e: ErrnoException) {
if (e.errno != OsConstants.EBADF) throw e // suppress fd already closed
}
join()
}
}
......@@ -30,16 +30,18 @@ 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 okhttp3.FormBody
import okhttp3.OkHttpClient
import okhttp3.Request
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 java.io.File
import java.io.IOException
import java.net.HttpURLConnection
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,25 +52,26 @@ 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 mdg = MessageDigest.getInstance("SHA-1")
mdg.update(Core.packageInfo.signaturesCompat.first().toByteArray())
val requestBody = FormBody.Builder()
.add("sig", String(Base64.encode(mdg.digest(), 0)))
.build()
val request = Request.Builder()
.url(RemoteConfig.proxyUrl)
.post(requestBody)
.build()
val proxies = client.newCall(request).execute()
.body()!!.string().split('|').toMutableList()
val conn = RemoteConfig.proxyUrl.openConnection() as HttpURLConnection
conn.requestMethod = "POST"
conn.doOutput = true
val proxies = try {
withTimeout(30_000) {
withContext(Dispatchers.IO) {
conn.outputStream.bufferedWriter().use {
it.write("sig=" + String(Base64.encode(mdg.digest(), Base64.DEFAULT)))
}
conn.inputStream.bufferedReader().readText()
}
}
} finally {
conn.disconnect()
}.split('|').toMutableList()
proxies.shuffle()
val proxy = proxies.first().split(':')
profile.host = proxy[0].trim()
......@@ -80,22 +83,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 +119,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() {
......
......@@ -25,11 +25,12 @@ import androidx.core.os.bundleOf
import com.github.shadowsocks.Core
import com.github.shadowsocks.core.R
import com.google.firebase.remoteconfig.FirebaseRemoteConfig
import java.net.URL
object RemoteConfig {
private val config = FirebaseRemoteConfig.getInstance().apply { setDefaults(R.xml.default_configs) }
val proxyUrl get() = config.getString("proxy_url")
val proxyUrl get() = URL(config.getString("proxy_url"))
fun fetch() = config.fetch().addOnCompleteListener {
if (it.isSuccessful) config.activateFetched() else {
......
......@@ -23,33 +23,26 @@ package com.github.shadowsocks.bg
import android.net.LocalSocket
import android.os.SystemClock
import com.github.shadowsocks.aidl.TrafficStats
import com.github.shadowsocks.utils.printLog
import java.io.File
import java.io.IOException
import java.nio.ByteBuffer
import java.nio.ByteOrder
class TrafficMonitor(statFile: File) : AutoCloseable {
private val thread = object : LocalSocketListener("TrafficMonitor") {
override val socketFile = statFile
override fun accept(socket: LocalSocket) {
try {
val buffer = ByteArray(16)
if (socket.inputStream.read(buffer) != 16) throw IOException("Unexpected traffic stat length")
val stat = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN)
val tx = stat.getLong(0)
val rx = stat.getLong(8)
if (current.txTotal != tx) {
current.txTotal = tx
dirty = true
}
if (current.rxTotal != rx) {
current.rxTotal = rx
dirty = true
}
} catch (e: IOException) {
printLog(e)
private val thread = object : LocalSocketListener("TrafficMonitor", statFile) {
override fun accept(socket: LocalSocket) = socket.use {
val buffer = ByteArray(16)
if (socket.inputStream.read(buffer) != 16) throw IOException("Unexpected traffic stat length")
val stat = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN)
val tx = stat.getLong(0)
val rx = stat.getLong(8)
if (current.txTotal != tx) {
current.txTotal = tx
dirty = true
}
if (current.rxTotal != rx) {
current.rxTotal = rx
dirty = true
}
}
}.apply { start() }
......@@ -57,7 +50,6 @@ class TrafficMonitor(statFile: File) : AutoCloseable {
val current = TrafficStats()
var out = TrafficStats()
private var timestampLast = 0L
@Volatile
private var dirty = false
fun requestUpdate(): Pair<TrafficStats, Boolean> {
......@@ -87,5 +79,5 @@ class TrafficMonitor(statFile: File) : AutoCloseable {
return Pair(out, updated)
}
override fun close() = thread.stopThread()
override fun close() = thread.close()
}
......@@ -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()
}
......
......@@ -39,6 +39,7 @@ import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.Subnet
import com.github.shadowsocks.utils.parseNumericAddress
import com.github.shadowsocks.utils.printLog
import kotlinx.coroutines.*
import java.io.Closeable
import java.io.File
import java.io.FileDescriptor
......@@ -78,26 +79,37 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
override fun close() = Os.close(fd)
}
private inner class ProtectWorker : LocalSocketListener("ShadowsocksVpnThread") {
override val socketFile: File = File(Core.deviceStorage.noBackupFilesDir, "protect_path")
private inner class ProtectWorker :
LocalSocketListener("ShadowsocksVpnThread", File(Core.deviceStorage.noBackupFilesDir, "protect_path")),
CoroutineScope {
private val job = SupervisorJob()
override val coroutineContext get() = Dispatchers.IO + job + CoroutineExceptionHandler { _, t -> printLog(t) }
override fun accept(socket: LocalSocket) = try {
socket.inputStream.read()
val fd = socket.ancillaryFileDescriptors!!.single()!!
CloseableFd(fd).use {
socket.outputStream.write(if (underlyingNetwork.let { network ->
if (network != null && Build.VERSION.SDK_INT >= 23) try {
network.bindSocket(fd)
true
} catch (e: IOException) {
// suppress ENONET (Machine is not on the network)
if ((e.cause as? ErrnoException)?.errno != 64) printLog(e)
false
} else protect(getInt.invoke(fd) as Int)
}) 0 else 1)
override fun accept(socket: LocalSocket) {
launch {
socket.use {
socket.inputStream.read()
val fd = socket.ancillaryFileDescriptors!!.single()!!
CloseableFd(fd).use {
socket.outputStream.write(if (underlyingNetwork.let { network ->
if (network != null && Build.VERSION.SDK_INT >= 23) try {
network.bindSocket(fd)
true
} catch (e: IOException) {
// suppress ENONET (Machine is not on the network)
if ((e.cause as? ErrnoException)?.errno != 64) printLog(e)
false
} else protect(getInt.invoke(fd) as Int)
}) 0 else 1)
}
}
}
} catch (e: IOException) {
printLog(e)
}
suspend fun shutdown() {
job.cancel()
close()
job.join()
}
}
inner class NullConnectionException : NullPointerException() {
......@@ -139,14 +151,14 @@ 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
}
worker?.stopThread()
worker?.shutdown()
worker = null
super.killProcesses()
conn?.close()
......@@ -159,16 +171,14 @@ 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() {
val worker = ProtectWorker()
worker.start()
this.worker = worker
override suspend fun startProcesses() {
worker = ProtectWorker().apply { start() }
super.startNativeProcesses()
super.startProcesses()
sendFd(startVpn())
}
......@@ -178,7 +188,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 +257,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,41 +72,47 @@ 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"
}, "/generate_204")
val conn = (if (DataStore.serviceMode == Key.modeVpn) url.openConnection() else
url.openConnection(Proxy(Proxy.Type.SOCKS, InetSocketAddress("127.0.0.1", DataStore.portProxy))))
as HttpURLConnection
conn.setRequestProperty("Connection", "close")
conn.instanceFollowRedirects = false
conn.useCaches = false
val result = try {
val start = SystemClock.elapsedRealtime()
val code = conn.responseCode
val elapsed = SystemClock.elapsedRealtime() - start
if (code == 204 || code == 200 && conn.responseLength == 0L) Status.Success(elapsed)
else Status.Error.UnexpectedResponseCode(code)
} catch (e: IOException) {
Status.Error.IOFailure(e)
} finally {
conn.disconnect()
val url = URL("https", when (Core.currentProfile!!.first.route) {
Acl.CHINALIST -> "www.qualcomm.cn"
else -> "www.google.com"
}, "/generate_204")
val conn = (if (DataStore.serviceMode == Key.modeVpn) url.openConnection() else
url.openConnection(Proxy(Proxy.Type.SOCKS, InetSocketAddress("127.0.0.1", DataStore.portProxy))))
as HttpURLConnection
conn.setRequestProperty("Connection", "close")
conn.instanceFollowRedirects = false
conn.useCaches = false
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
if (code == 204 || code == 200 && conn.responseLength == 0L) Status.Success(elapsed)
else Status.Error.UnexpectedResponseCode(code)
} catch (e: IOException) {
Status.Error.IOFailure(e)
} 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()
......
......@@ -22,5 +22,3 @@
#-renamesourcefileattribute SourceFile
-dontwarn com.google.android.gms.internal.**
-dontwarn okhttp3.**
-dontwarn okio.**
......@@ -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
}
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 {
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()
}
withContext(Dispatchers.IO) { adapter.reload() }
adapter.notifyDataSetChanged()
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()
}
}
......@@ -29,6 +29,7 @@ import android.nfc.NdefMessage
import android.nfc.NfcAdapter
import android.os.Bundle
import android.os.DeadObjectException
import android.os.Handler
import android.util.Log
import android.view.KeyCharacterMap
import android.view.KeyEvent
......@@ -124,7 +125,8 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
else -> Core.startService()
}
private val connection = ShadowsocksConnection(true)
private val handler = Handler()
private val connection = ShadowsocksConnection(handler, true)
override fun onServiceConnected(service: IShadowsocksService) = changeState(try {
service.state
} catch (_: DeadObjectException) {
......@@ -132,11 +134,9 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
})
override fun onServiceDisconnected() = changeState(BaseService.IDLE)
override fun onBinderDied() {
Core.handler.post {
connection.disconnect(this)
Executable.killAll()
connection.connect(this, this)
}
connection.disconnect(this)
Executable.killAll()
connection.connect(this, this)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
......@@ -167,7 +167,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
fab.setOnClickListener { toggle() }
changeState(BaseService.IDLE) // reset everything to init state
Core.handler.post { connection.connect(this, this) }
connection.connect(this, this)
DataStore.publicStore.registerChangeListener(this)
val intent = this.intent
......@@ -205,7 +205,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String?) {
when (key) {
Key.serviceMode -> Core.handler.post {
Key.serviceMode -> handler.post {
connection.disconnect(this)
connection.connect(this, this)
}
......@@ -280,6 +280,6 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
DataStore.publicStore.unregisterChangeListener(this)
connection.disconnect(this)
BackupManager(this).dataChanged()
Core.handler.removeCallbacksAndMessages(null)
handler.removeCallbacksAndMessages(null)
}
}
......@@ -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>
......@@ -22,5 +22,3 @@
#-renamesourcefileattribute SourceFile
-dontwarn com.google.android.gms.internal.**
-dontwarn okhttp3.**
-dontwarn okio.**
......@@ -27,6 +27,7 @@ import android.content.Intent
import android.net.VpnService
import android.os.Bundle
import android.os.DeadObjectException
import android.os.Handler
import android.text.format.Formatter
import android.util.Log
import android.widget.Toast
......@@ -135,7 +136,8 @@ class MainPreferenceFragment : LeanbackPreferenceFragment(), ShadowsocksConnecti
}
}
private val connection = ShadowsocksConnection(true)
private val handler = Handler()
private val connection = ShadowsocksConnection(handler, true)
override fun onServiceConnected(service: IShadowsocksService) = changeState(try {
service.state
} catch (_: DeadObjectException) {
......@@ -143,11 +145,9 @@ class MainPreferenceFragment : LeanbackPreferenceFragment(), ShadowsocksConnecti
})
override fun onServiceDisconnected() = changeState(BaseService.IDLE)
override fun onBinderDied() {
Core.handler.post {
connection.disconnect(activity)
Executable.killAll()
connection.connect(activity, this)
}
connection.disconnect(activity)
Executable.killAll()
connection.connect(activity, this)
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
......@@ -232,7 +232,7 @@ class MainPreferenceFragment : LeanbackPreferenceFragment(), ShadowsocksConnecti
override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String?) {
when (key) {
Key.serviceMode -> Core.handler.post {
Key.serviceMode -> handler.post {
connection.disconnect(activity)
connection.connect(activity, this)
}
......
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