From 114456a1f21883a752374436ff6d1ff41839ee59 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Wed, 22 Jan 2020 13:23:27 +0800 Subject: [PATCH] [WIP] refactor UI --- app/build.gradle | 3 +- .../com/github/kr328/clash/BaseActivity.kt | 162 +++++------------- .../com/github/kr328/clash/MainApplication.kt | 4 +- .../github/kr328/clash/remote/Broadcasts.kt | 65 +++++++ .../com/github/kr328/clash/remote/Calls.kt | 7 + .../com/github/kr328/clash/remote/Channels.kt | 33 ++++ .../github/kr328/clash/remote/ClashClient.kt | 95 ++++++++++ .../clash/service/ClashProfileManager.kt | 2 +- .../kr328/clash/service/ClashService.kt | 2 +- .../com/github/kr328/clash/service/Intents.kt | 4 +- 10 files changed, 256 insertions(+), 121 deletions(-) create mode 100644 app/src/main/java/com/github/kr328/clash/remote/Broadcasts.kt create mode 100644 app/src/main/java/com/github/kr328/clash/remote/Calls.kt create mode 100644 app/src/main/java/com/github/kr328/clash/remote/Channels.kt create mode 100644 app/src/main/java/com/github/kr328/clash/remote/ClashClient.kt diff --git a/app/build.gradle b/app/build.gradle index 4b7af1d..7b3c3e9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -40,7 +40,8 @@ dependencies { implementation project(":core") implementation project(":service") implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - implementation "androidx.lifecycle:lifecycle-livedata:2.1.0" + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3' + implementation "androidx.lifecycle:lifecycle-extensions:2.1.0" implementation "androidx.lifecycle:lifecycle-common-java8:2.1.0" implementation 'androidx.browser:browser:1.2.0' implementation 'androidx.preference:preference:1.1.0' diff --git a/app/src/main/java/com/github/kr328/clash/BaseActivity.kt b/app/src/main/java/com/github/kr328/clash/BaseActivity.kt index 928eacb..bcfc251 100644 --- a/app/src/main/java/com/github/kr328/clash/BaseActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/BaseActivity.kt @@ -1,103 +1,65 @@ package com.github.kr328.clash -import android.content.ComponentName +import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import android.content.ServiceConnection -import android.os.Bundle -import android.os.IBinder import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar -import com.github.kr328.clash.core.event.* -import com.github.kr328.clash.core.utils.Log +import com.github.kr328.clash.remote.Broadcasts import com.github.kr328.clash.service.ClashService -import com.github.kr328.clash.service.IClashEventObserver -import com.github.kr328.clash.service.IClashService -import java.util.concurrent.LinkedBlockingQueue -import kotlin.concurrent.thread +import com.github.kr328.clash.service.data.ClashProfileEntity +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch -abstract class BaseActivity : AppCompatActivity(), IClashEventObserver { - companion object { - private var activityCount: Int = 0 +abstract class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope() { + class EmptyBroadcastReceiver : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) {} + } - private val paddingRequest = LinkedBlockingQueue<(IClashService) -> Unit>() - private var clashConnection: ClashConnection? = null - - class ClashConnection : ServiceConnection { - private var requestHandler: Thread? = null - private var clash: IClashService? = null - - override fun onServiceDisconnected(name: ComponentName?) { - synchronized(BaseActivity::class.java) { - Log.i("ClashService disconnected") - - requestHandler?.interrupt() - requestHandler = null - } + private val receiver = object : Broadcasts.Receiver { + override fun onStarted() { + launch { + onClashStarted() } + } - override fun onServiceConnected(name: ComponentName?, service: IBinder?) { - synchronized(BaseActivity::class) { - Log.i("ClashService connected") + override fun onStopped(cause: String?) { + launch { + onClashStopped(cause) + } + } - clash = IClashService.Stub.asInterface(service) - requestHandler = thread { - try { - while (!Thread.currentThread().isInterrupted) { - val block = paddingRequest.take() - - clash?.run(block) - } - } catch (e: InterruptedException) { - } - - Log.i("ClashConnect exited") - - synchronized(BaseActivity::class.java) { - clash = null - requestHandler = null - } - } - } + override fun onProfileChanged(active: ClashProfileEntity?) { + launch { + onClashProfileChanged(active) } } } - protected fun runClash(block: (IClashService) -> Unit) { - paddingRequest.offer(block) - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - synchronized(BaseActivity::class.java) { - if (clashConnection == null) { - clashConnection = ClashConnection() - - applicationContext.bindService( - Intent(this, ClashService::class.java), - clashConnection!!, - Context.BIND_AUTO_CREATE - ) - } - - activityCount++ - } - } - - override fun onDestroy() { - synchronized(BaseActivity::class.java) { - if (--activityCount <= 0) { - if (clashConnection != null) { - applicationContext.unbindService(clashConnection!!) - clashConnection?.onServiceDisconnected(null) - - clashConnection = null - } - } + val isClashRunning: Boolean + get() { + return EmptyBroadcastReceiver().peekService( + this, + Intent(this, ClashService::class.java) + ) != null } - super.onDestroy() + open suspend fun onClashStarted() {} + open suspend fun onClashStopped(reason: String?) {} + open suspend fun onClashProfileChanged(active: ClashProfileEntity?) {} + + override fun onStart() { + super.onStart() + + Broadcasts.register(receiver) + } + + override fun onStop() { + super.onStop() + + Broadcasts.unregister(receiver) } override fun setSupportActionBar(toolbar: Toolbar?) { @@ -116,39 +78,9 @@ abstract class BaseActivity : AppCompatActivity(), IClashEventObserver { return true } - private val observerBinder = object : IClashEventObserver.Stub() { - override fun onLogEvent(event: LogEvent?) = - this@BaseActivity.onLogEvent(event) + override fun onDestroy() { + cancel() - override fun onProcessEvent(event: ProcessEvent?) = - this@BaseActivity.onProcessEvent(event) - - override fun onErrorEvent(event: ErrorEvent?) = - this@BaseActivity.onErrorEvent(event) - - override fun onTrafficEvent(event: TrafficEvent?) = - this@BaseActivity.onTrafficEvent(event) - - override fun onBandwidthEvent(event: BandwidthEvent?) { - this@BaseActivity.onBandwidthEvent(event) - } - - override fun onProfileChanged(event: ProfileChangedEvent?) = - this@BaseActivity.onProfileChanged(event) - - override fun onProfileReloaded(event: ProfileReloadEvent?) { - this@BaseActivity.onProfileReloaded(event) - } - } - - override fun onLogEvent(event: LogEvent?) {} - override fun onErrorEvent(event: ErrorEvent?) {} - override fun onProfileChanged(event: ProfileChangedEvent?) {} - override fun onProcessEvent(event: ProcessEvent?) {} - override fun onTrafficEvent(event: TrafficEvent?) {} - override fun onBandwidthEvent(event: BandwidthEvent?) {} - override fun onProfileReloaded(event: ProfileReloadEvent?) {} - override fun asBinder(): IBinder { - return observerBinder + super.onDestroy() } } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/MainApplication.kt b/app/src/main/java/com/github/kr328/clash/MainApplication.kt index 7ff19a7..a8d49ef 100644 --- a/app/src/main/java/com/github/kr328/clash/MainApplication.kt +++ b/app/src/main/java/com/github/kr328/clash/MainApplication.kt @@ -7,6 +7,7 @@ import com.crashlytics.android.Crashlytics import com.github.kr328.clash.core.Constants import com.github.kr328.clash.core.utils.ByteFormatter import com.github.kr328.clash.core.utils.Log +import com.github.kr328.clash.remote.ClashClient import com.google.firebase.FirebaseApp import io.fabric.sdk.android.Fabric import java.io.File @@ -15,7 +16,6 @@ import java.security.MessageDigest import java.util.zip.ZipFile import kotlin.concurrent.thread - class MainApplication : Application() { companion object { const val KEY_PROXY_MODE = "key_proxy_mode" @@ -68,6 +68,8 @@ class MainApplication : Application() { Crashlytics.setBool(CRASHLYTICS_SPLIT_APK_KEY, detectSplitArchive()) Crashlytics.setString(CRASHLYTICS_BASE_SIZE_KEY, getBaseApkSize()) Crashlytics.setUserIdentifier(userIdentifier) + + ClashClient.init(this) } private fun detectFromPlay(): Boolean { diff --git a/app/src/main/java/com/github/kr328/clash/remote/Broadcasts.kt b/app/src/main/java/com/github/kr328/clash/remote/Broadcasts.kt new file mode 100644 index 0000000..c99997e --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/remote/Broadcasts.kt @@ -0,0 +1,65 @@ +package com.github.kr328.clash.remote + +import android.app.Application +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ProcessLifecycleOwner +import com.github.kr328.clash.service.Intents +import com.github.kr328.clash.service.data.ClashProfileEntity + +object Broadcasts { + interface Receiver { + fun onStarted() + fun onStopped(cause: String?) + fun onProfileChanged(active: ClashProfileEntity?) + } + + private val receivers = mutableListOf() + + private val broadcastReceiver = object: BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + when ( intent?.action ) { + Intents.INTENT_ACTION_CLASH_STARTED -> + receivers.forEach { + it.onStarted() + } + Intents.INTENT_ACTION_CLASH_STOPPED -> + receivers.forEach { + it.onStopped(intent.getStringExtra(Intents.INTENT_EXTRA_CLASH_STOP_REASON)) + } + Intents.INTENT_ACTION_PROFILE_CHANGED -> + receivers.forEach { + it.onProfileChanged(intent.getParcelableExtra(Intents.INTENT_EXTRA_PROFILE_ACTIVE)) + } + } + } + } + + fun register(receiver: Receiver) { + receivers.add(receiver) + } + + fun unregister(receiver: Receiver) { + receivers.remove(receiver) + } + + fun init(application: Application) { + ProcessLifecycleOwner.get().lifecycle.addObserver(object: DefaultLifecycleObserver { + override fun onStart(owner: LifecycleOwner) { + application.registerReceiver(broadcastReceiver, IntentFilter().apply { + addAction(Intents.INTENT_ACTION_PROFILE_CHANGED) + addAction(Intents.INTENT_ACTION_CLASH_STOPPED) + addAction(Intents.INTENT_ACTION_CLASH_STARTED) + }) + } + + override fun onStop(owner: LifecycleOwner) { + application.unregisterReceiver(broadcastReceiver) + } + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/remote/Calls.kt b/app/src/main/java/com/github/kr328/clash/remote/Calls.kt new file mode 100644 index 0000000..d55d776 --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/remote/Calls.kt @@ -0,0 +1,7 @@ +package com.github.kr328.clash.remote + +suspend fun withClash(block: suspend (ClashClient) -> Unit) { + val client = ClashClient.instance ?: return + + block(client) +} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/remote/Channels.kt b/app/src/main/java/com/github/kr328/clash/remote/Channels.kt new file mode 100644 index 0000000..8b38289 --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/remote/Channels.kt @@ -0,0 +1,33 @@ +package com.github.kr328.clash.remote + +import com.github.kr328.clash.core.event.BandwidthEvent +import com.github.kr328.clash.core.event.LogEvent +import com.github.kr328.clash.service.ipc.ParcelablePipe +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.channels.Channel + +class LogChannel(private val pipe: ParcelablePipe): Channel by Channel(Channel.CONFLATED) { + init { + pipe.onReceive { + offer(it as LogEvent) + } + } + + override fun cancel(cause: CancellationException?) { + pipe.close() + close(cause) + } +} + +class BandwidthChannel(private val pipe: ParcelablePipe): Channel by Channel(Channel.CONFLATED) { + init { + pipe.onReceive { + offer(it as BandwidthEvent) + } + } + + override fun cancel(cause: CancellationException?) { + pipe.close() + close(cause) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/remote/ClashClient.kt b/app/src/main/java/com/github/kr328/clash/remote/ClashClient.kt new file mode 100644 index 0000000..44496bc --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/remote/ClashClient.kt @@ -0,0 +1,95 @@ +package com.github.kr328.clash.remote + +import android.app.Application +import android.content.* +import android.os.IBinder +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ProcessLifecycleOwner +import com.github.kr328.clash.MainApplication +import com.github.kr328.clash.core.event.BandwidthEvent +import com.github.kr328.clash.core.event.LogEvent +import com.github.kr328.clash.core.model.General +import com.github.kr328.clash.core.model.ProxyGroup +import com.github.kr328.clash.service.ClashManagerService +import com.github.kr328.clash.service.IClashManager +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.withContext + +class ClashClient(val service: IClashManager) { + companion object { + var instance: ClashClient? = null + + private val connection = object : ServiceConnection { + override fun onServiceDisconnected(name: ComponentName?) { + instance = null + } + + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + if (service != null) + instance = ClashClient(IClashManager.Stub.asInterface(service)) + } + } + + fun init(application: Application) { + ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver { + override fun onStart(owner: LifecycleOwner) { + application.bindService( + Intent(application, ClashManagerService::class.java), + connection, + Context.BIND_AUTO_CREATE + ) + } + override fun onStop(owner: LifecycleOwner) { + application.unbindService(connection) + } + }) + } + } + + private val openedChannel: MutableList> = mutableListOf() + + suspend fun setSelectProxy(name: String, proxy: String): Boolean = withContext(Dispatchers.IO) { + return@withContext service.setSelectProxy(name, proxy) + } + + suspend fun startHealthCheck(group: String) = withContext(Dispatchers.IO) { + CompletableDeferred().apply { + service.startHealthCheck(group).whenComplete { _, u -> + if (u != null) + completeExceptionally(u) + else + complete(Unit) + } + } + }.await() + + suspend fun queryAllProxyGroups(): Array = withContext(Dispatchers.IO) { + service.queryAllProxies() + } + + suspend fun queryGeneral(): General = withContext(Dispatchers.IO) { + service.queryGeneral() + } + + suspend fun openLogChannel(): ReceiveChannel = withContext(Dispatchers.IO) { + LogChannel(service.openLogEvent()).also { + openedChannel.add(it) + } + } + + suspend fun openBandwidthChannel(): ReceiveChannel = + withContext(Dispatchers.IO) { + BandwidthChannel(service.openBandwidthEvent()).also { + openedChannel.add(it) + } + } + + fun close() { + for (channel in openedChannel) + channel.cancel() + } +} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashProfileManager.kt b/service/src/main/java/com/github/kr328/clash/service/ClashProfileManager.kt index 8e56bf1..50d8454 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashProfileManager.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashProfileManager.kt @@ -125,7 +125,7 @@ class ClashProfileManager(private val context: Context, private val database: Cl context.sendBroadcastSelf( Intent(Intents.INTENT_ACTION_PROFILE_CHANGED) - .putExtra(Intents.INTENT_ACTION_PROFILE_ACTIVE_NAME, active?.name) + .putExtra(Intents.INTENT_EXTRA_PROFILE_ACTIVE, active) ) } } \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashService.kt b/service/src/main/java/com/github/kr328/clash/service/ClashService.kt index 6ce6d83..682e981 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashService.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashService.kt @@ -85,7 +85,7 @@ class ClashService : Service() { sendBroadcastSelf( Intent(Intents.INTENT_ACTION_CLASH_STOPPED) - .putExtra(Intents.INTENT_ACTION_CLASH_STOP_REASON, stopReason) + .putExtra(Intents.INTENT_EXTRA_CLASH_STOP_REASON, stopReason) ) unregisterReceiver(profileObserver) diff --git a/service/src/main/java/com/github/kr328/clash/service/Intents.kt b/service/src/main/java/com/github/kr328/clash/service/Intents.kt index d89fe18..551fe8f 100644 --- a/service/src/main/java/com/github/kr328/clash/service/Intents.kt +++ b/service/src/main/java/com/github/kr328/clash/service/Intents.kt @@ -10,8 +10,8 @@ object Intents { const val INTENT_ACTION_PROFILE_CHANGED = "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.action.profile.changed" - const val INTENT_ACTION_CLASH_STOP_REASON = + const val INTENT_EXTRA_CLASH_STOP_REASON = "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.extra.clash.stop.reason" - const val INTENT_ACTION_PROFILE_ACTIVE_NAME = + const val INTENT_EXTRA_PROFILE_ACTIVE = "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.extra.profile.active.name" } \ No newline at end of file