From c8a1e0007731e5350775776b77374d4fbc25247d Mon Sep 17 00:00:00 2001 From: lisonge Date: Tue, 24 Sep 2024 13:00:53 +0800 Subject: [PATCH] refactor: simplify code --- app/src/main/AndroidManifest.xml | 2 +- app/src/main/kotlin/li/songe/gkd/App.kt | 17 +- .../main/kotlin/li/songe/gkd/MainActivity.kt | 84 +-- .../songe/gkd/composition/CanConfigBubble.kt | 5 - .../composition/CanOnAccessibilityEvent.kt | 7 - .../composition/CanOnConfigurationChanged.kt | 7 - .../li/songe/gkd/composition/CanOnDestroy.kt | 5 - .../songe/gkd/composition/CanOnInterrupt.kt | 5 - .../li/songe/gkd/composition/CanOnKeyEvent.kt | 7 - .../gkd/composition/CanOnServiceConnected.kt | 5 - .../gkd/composition/CanOnStartCommand.kt | 5 - .../gkd/composition/CompositionAbService.kt | 72 --- .../songe/gkd/composition/CompositionExt.kt | 55 -- .../gkd/composition/CompositionFbService.kt | 41 -- .../gkd/composition/CompositionService.kt | 30 - .../li/songe/gkd/composition/InvokeMessage.kt | 13 - .../li/songe/gkd/composition/Typealias.kt | 8 - .../li/songe/gkd/data/ComplexSnapshot.kt | 4 +- .../kotlin/li/songe/gkd/data/ResolvedRule.kt | 4 +- .../li/songe/gkd/debug/FloatingService.kt | 38 +- .../kotlin/li/songe/gkd/debug/HttpService.kt | 8 +- .../li/songe/gkd/debug/ScreenshotService.kt | 39 +- .../kotlin/li/songe/gkd/debug/SnapshotExt.kt | 8 +- .../li/songe/gkd/debug/SnapshotTileService.kt | 12 +- .../songe/gkd/permission/PermissionState.kt | 29 +- .../gkd/service/{AbEvent.kt => A11yEvent.kt} | 6 +- .../gkd/service/{AbExt.kt => A11yExt.kt} | 4 +- .../{GkdAbService.kt => A11yService.kt} | 605 ++++++++++-------- .../gkd/service/{AbState.kt => A11yState.kt} | 0 .../li/songe/gkd/service/GkdTileService.kt | 8 +- .../li/songe/gkd/service/ManageService.kt | 96 +-- .../kotlin/li/songe/gkd/shizuku/ShizukuApi.kt | 27 +- .../kotlin/li/songe/gkd/ui/AuthA11yPage.kt | 4 +- .../li/songe/gkd/ui/home/ControlPage.kt | 8 +- .../li/songe/gkd/util/LifecycleCallbacks.kt | 63 ++ 35 files changed, 591 insertions(+), 740 deletions(-) delete mode 100644 app/src/main/kotlin/li/songe/gkd/composition/CanConfigBubble.kt delete mode 100644 app/src/main/kotlin/li/songe/gkd/composition/CanOnAccessibilityEvent.kt delete mode 100644 app/src/main/kotlin/li/songe/gkd/composition/CanOnConfigurationChanged.kt delete mode 100644 app/src/main/kotlin/li/songe/gkd/composition/CanOnDestroy.kt delete mode 100644 app/src/main/kotlin/li/songe/gkd/composition/CanOnInterrupt.kt delete mode 100644 app/src/main/kotlin/li/songe/gkd/composition/CanOnKeyEvent.kt delete mode 100644 app/src/main/kotlin/li/songe/gkd/composition/CanOnServiceConnected.kt delete mode 100644 app/src/main/kotlin/li/songe/gkd/composition/CanOnStartCommand.kt delete mode 100644 app/src/main/kotlin/li/songe/gkd/composition/CompositionAbService.kt delete mode 100644 app/src/main/kotlin/li/songe/gkd/composition/CompositionExt.kt delete mode 100644 app/src/main/kotlin/li/songe/gkd/composition/CompositionFbService.kt delete mode 100644 app/src/main/kotlin/li/songe/gkd/composition/CompositionService.kt delete mode 100644 app/src/main/kotlin/li/songe/gkd/composition/InvokeMessage.kt delete mode 100644 app/src/main/kotlin/li/songe/gkd/composition/Typealias.kt rename app/src/main/kotlin/li/songe/gkd/service/{AbEvent.kt => A11yEvent.kt} (79%) rename app/src/main/kotlin/li/songe/gkd/service/{AbExt.kt => A11yExt.kt} (99%) rename app/src/main/kotlin/li/songe/gkd/service/{GkdAbService.kt => A11yService.kt} (60%) rename app/src/main/kotlin/li/songe/gkd/service/{AbState.kt => A11yState.kt} (100%) create mode 100644 app/src/main/kotlin/li/songe/gkd/util/LifecycleCallbacks.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0a7cbd5..d2efc26 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -114,7 +114,7 @@ diff --git a/app/src/main/kotlin/li/songe/gkd/App.kt b/app/src/main/kotlin/li/songe/gkd/App.kt index 352d7a5..c8d74bd 100644 --- a/app/src/main/kotlin/li/songe/gkd/App.kt +++ b/app/src/main/kotlin/li/songe/gkd/App.kt @@ -21,8 +21,8 @@ import kotlinx.coroutines.flow.MutableStateFlow import li.songe.gkd.data.selfAppInfo import li.songe.gkd.debug.clearHttpSubs import li.songe.gkd.notif.initChannel -import li.songe.gkd.permission.updatePermissionState -import li.songe.gkd.service.GkdAbService +import li.songe.gkd.permission.shizukuOkState +import li.songe.gkd.service.A11yService import li.songe.gkd.util.SafeR import li.songe.gkd.util.initAppState import li.songe.gkd.util.initFolder @@ -31,6 +31,7 @@ import li.songe.gkd.util.initSubsState import li.songe.gkd.util.launchTry import li.songe.gkd.util.setReactiveToastStyle import org.lsposed.hiddenapibypass.HiddenApiBypass +import rikka.shizuku.Shizuku val appScope by lazy { MainScope() } @@ -130,13 +131,21 @@ class App : Application() { } } ) + Shizuku.addBinderReceivedListener { + appScope.launchTry(Dispatchers.IO) { + shizukuOkState.updateAndGet() + } + } + Shizuku.addBinderDeadListener { + shizukuOkState.stateFlow.value = false + } appScope.launchTry(Dispatchers.IO) { initStore() initAppState() initSubsState() initChannel() clearHttpSubs() - updatePermissionState() + syncFixState() } } } @@ -154,7 +163,7 @@ private fun getA11yServiceEnabled(): Boolean { if (value.isNullOrEmpty()) return false val colonSplitter = TextUtils.SimpleStringSplitter(':') colonSplitter.setString(value) - val name = ComponentName(app, GkdAbService::class.java) + val name = ComponentName(app, A11yService::class.java) while (colonSplitter.hasNext()) { if (ComponentName.unflattenFromString(colonSplitter.next()) == name) { return true diff --git a/app/src/main/kotlin/li/songe/gkd/MainActivity.kt b/app/src/main/kotlin/li/songe/gkd/MainActivity.kt index 4f16208..16b33bd 100644 --- a/app/src/main/kotlin/li/songe/gkd/MainActivity.kt +++ b/app/src/main/kotlin/li/songe/gkd/MainActivity.kt @@ -3,6 +3,7 @@ package li.songe.gkd import android.app.Activity import android.app.ActivityManager import android.content.ComponentName +import android.content.Context import android.content.Intent import android.graphics.Color import android.os.Bundle @@ -23,7 +24,6 @@ import androidx.core.view.WindowInsetsCompat import androidx.lifecycle.lifecycleScope import androidx.navigation.compose.rememberNavController import com.blankj.utilcode.util.BarUtils -import com.blankj.utilcode.util.ServiceUtils import com.dylanc.activityresult.launcher.PickContentLauncher import com.dylanc.activityresult.launcher.StartActivityLauncher import com.ramcosta.composedestinations.DestinationsNavHost @@ -32,12 +32,14 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import li.songe.gkd.debug.FloatingService import li.songe.gkd.debug.HttpService import li.songe.gkd.debug.ScreenshotService import li.songe.gkd.permission.AuthDialog import li.songe.gkd.permission.updatePermissionState -import li.songe.gkd.service.GkdAbService +import li.songe.gkd.service.A11yService import li.songe.gkd.service.ManageService import li.songe.gkd.service.fixRestartService import li.songe.gkd.service.updateLauncherAppId @@ -52,6 +54,7 @@ import li.songe.gkd.util.map import li.songe.gkd.util.openApp import li.songe.gkd.util.openUri import li.songe.gkd.util.storeFlow +import kotlin.reflect.KClass class MainActivity : ComponentActivity() { val mainVm by viewModels() @@ -63,7 +66,10 @@ class MainActivity : ComponentActivity() { enableEdgeToEdge() fixTopPadding() super.onCreate(savedInstanceState) - + mainVm + launcher + pickContentLauncher + ManageService.autoStart() lifecycleScope.launch { storeFlow.map(lifecycleScope) { s -> s.excludeFromRecents }.collect { (app.getSystemService(ACTIVITY_SERVICE) as ActivityManager).let { manager -> @@ -73,12 +79,6 @@ class MainActivity : ComponentActivity() { } } } - - mainVm - launcher - pickContentLauncher - ManageService.autoStart(this) - setContent { val navController = rememberNavController() AppTheme { @@ -102,26 +102,7 @@ class MainActivity : ComponentActivity() { override fun onResume() { super.onResume() - - // 每次切换页面更新记录桌面 appId - appScope.launchTry(Dispatchers.IO) { - updateLauncherAppId() - } - - // 在某些机型由于未知原因创建失败, 在此保证每次界面切换都能重新检测创建 - appScope.launchTry(Dispatchers.IO) { - initFolder() - } - - // 用户在系统权限设置中切换权限后再切换回应用时能及时更新状态 - appScope.launchTry(Dispatchers.IO) { - updatePermissionState() - } - - // 由于某些机型的进程存在 安装缓存/崩溃缓存 导致服务状态可能不正确, 在此保证每次界面切换都能重新刷新状态 - appScope.launchTry(Dispatchers.IO) { - updateServiceRunning() - } + syncFixState() } override fun onStart() { @@ -162,14 +143,45 @@ fun Activity.navToMainActivity() { finish() } - +@Suppress("DEPRECATION") private fun updateServiceRunning() { - ManageService.isRunning.value = ServiceUtils.isServiceRunning(ManageService::class.java) - GkdAbService.isRunning.value = ServiceUtils.isServiceRunning(GkdAbService::class.java) - FloatingService.isRunning.value = ServiceUtils.isServiceRunning(FloatingService::class.java) - ScreenshotService.isRunning.value = ServiceUtils.isServiceRunning(ScreenshotService::class.java) - HttpService.isRunning.value = ServiceUtils.isServiceRunning(HttpService::class.java) - fixRestartService() + val list = try { + val manager = app.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + manager.getRunningServices(Int.MAX_VALUE) ?: emptyList() + } catch (_: Exception) { + emptyList() + } + + fun checkRunning(cls: KClass<*>): Boolean { + return list.any { it.service.className == cls.java.name } + } + ManageService.isRunning.value = checkRunning(ManageService::class) + A11yService.isRunning.value = checkRunning(A11yService::class) + FloatingService.isRunning.value = checkRunning(FloatingService::class) + ScreenshotService.isRunning.value = checkRunning(ScreenshotService::class) + HttpService.isRunning.value = checkRunning(HttpService::class) +} + +private val syncStateMutex = Mutex() +fun syncFixState() { + appScope.launchTry(Dispatchers.IO) { + syncStateMutex.withLock { + // 每次切换页面更新记录桌面 appId + updateLauncherAppId() + + // 在某些机型由于未知原因创建失败, 在此保证每次界面切换都能重新检测创建 + initFolder() + + // 由于某些机型的进程存在 安装缓存/崩溃缓存 导致服务状态可能不正确, 在此保证每次界面切换都能重新刷新状态 + updateServiceRunning() + + // 用户在系统权限设置中切换权限后再切换回应用时能及时更新状态 + updatePermissionState() + + // 自动重启无障碍服务 + fixRestartService() + } + } } private fun Activity.fixTopPadding() { diff --git a/app/src/main/kotlin/li/songe/gkd/composition/CanConfigBubble.kt b/app/src/main/kotlin/li/songe/gkd/composition/CanConfigBubble.kt deleted file mode 100644 index 8b2e1ad..0000000 --- a/app/src/main/kotlin/li/songe/gkd/composition/CanConfigBubble.kt +++ /dev/null @@ -1,5 +0,0 @@ -package li.songe.gkd.composition - -interface CanConfigBubble { - fun configBubble(f: ConfigBubbleHook) -} \ No newline at end of file diff --git a/app/src/main/kotlin/li/songe/gkd/composition/CanOnAccessibilityEvent.kt b/app/src/main/kotlin/li/songe/gkd/composition/CanOnAccessibilityEvent.kt deleted file mode 100644 index bf3b4a2..0000000 --- a/app/src/main/kotlin/li/songe/gkd/composition/CanOnAccessibilityEvent.kt +++ /dev/null @@ -1,7 +0,0 @@ -package li.songe.gkd.composition - -import android.view.accessibility.AccessibilityEvent - -interface CanOnAccessibilityEvent { - fun onAccessibilityEvent(f: (AccessibilityEvent) -> Unit): Boolean -} \ No newline at end of file diff --git a/app/src/main/kotlin/li/songe/gkd/composition/CanOnConfigurationChanged.kt b/app/src/main/kotlin/li/songe/gkd/composition/CanOnConfigurationChanged.kt deleted file mode 100644 index 47258f8..0000000 --- a/app/src/main/kotlin/li/songe/gkd/composition/CanOnConfigurationChanged.kt +++ /dev/null @@ -1,7 +0,0 @@ -package li.songe.gkd.composition - -import android.content.res.Configuration - -interface CanOnConfigurationChanged { - fun onConfigurationChanged(f: (newConfig: Configuration) -> Unit):Boolean -} \ No newline at end of file diff --git a/app/src/main/kotlin/li/songe/gkd/composition/CanOnDestroy.kt b/app/src/main/kotlin/li/songe/gkd/composition/CanOnDestroy.kt deleted file mode 100644 index 0d0abff..0000000 --- a/app/src/main/kotlin/li/songe/gkd/composition/CanOnDestroy.kt +++ /dev/null @@ -1,5 +0,0 @@ -package li.songe.gkd.composition - -interface CanOnDestroy { - fun onDestroy(f: () -> Unit): Boolean -} \ No newline at end of file diff --git a/app/src/main/kotlin/li/songe/gkd/composition/CanOnInterrupt.kt b/app/src/main/kotlin/li/songe/gkd/composition/CanOnInterrupt.kt deleted file mode 100644 index c987e94..0000000 --- a/app/src/main/kotlin/li/songe/gkd/composition/CanOnInterrupt.kt +++ /dev/null @@ -1,5 +0,0 @@ -package li.songe.gkd.composition - -interface CanOnInterrupt { - fun onInterrupt(f: () -> Unit):Boolean -} \ No newline at end of file diff --git a/app/src/main/kotlin/li/songe/gkd/composition/CanOnKeyEvent.kt b/app/src/main/kotlin/li/songe/gkd/composition/CanOnKeyEvent.kt deleted file mode 100644 index 69fe587..0000000 --- a/app/src/main/kotlin/li/songe/gkd/composition/CanOnKeyEvent.kt +++ /dev/null @@ -1,7 +0,0 @@ -package li.songe.gkd.composition - -import android.view.KeyEvent - -interface CanOnKeyEvent { - fun onKeyEvent(f: (KeyEvent?) -> Unit): Unit -} \ No newline at end of file diff --git a/app/src/main/kotlin/li/songe/gkd/composition/CanOnServiceConnected.kt b/app/src/main/kotlin/li/songe/gkd/composition/CanOnServiceConnected.kt deleted file mode 100644 index 5d77e5f..0000000 --- a/app/src/main/kotlin/li/songe/gkd/composition/CanOnServiceConnected.kt +++ /dev/null @@ -1,5 +0,0 @@ -package li.songe.gkd.composition - -interface CanOnServiceConnected { - fun onServiceConnected(f: () -> Unit):Boolean -} \ No newline at end of file diff --git a/app/src/main/kotlin/li/songe/gkd/composition/CanOnStartCommand.kt b/app/src/main/kotlin/li/songe/gkd/composition/CanOnStartCommand.kt deleted file mode 100644 index e91105d..0000000 --- a/app/src/main/kotlin/li/songe/gkd/composition/CanOnStartCommand.kt +++ /dev/null @@ -1,5 +0,0 @@ -package li.songe.gkd.composition - -interface CanOnStartCommand { - fun onStartCommand(f: StartCommandHook): Boolean -} \ No newline at end of file diff --git a/app/src/main/kotlin/li/songe/gkd/composition/CompositionAbService.kt b/app/src/main/kotlin/li/songe/gkd/composition/CompositionAbService.kt deleted file mode 100644 index 76742bc..0000000 --- a/app/src/main/kotlin/li/songe/gkd/composition/CompositionAbService.kt +++ /dev/null @@ -1,72 +0,0 @@ -package li.songe.gkd.composition - -import android.accessibilityservice.AccessibilityService -import android.content.Intent -import android.view.KeyEvent -import android.view.accessibility.AccessibilityEvent - -open class CompositionAbService( - private val block: CompositionAbService.() -> Unit, -) : AccessibilityService(), CanOnDestroy, CanOnStartCommand, CanOnAccessibilityEvent, - CanOnServiceConnected, CanOnInterrupt, CanOnKeyEvent { - override fun onCreate() { - super.onCreate() - block(this) - } - - private val onStartCommandHooks by lazy { linkedSetOf() } - override fun onStartCommand(f: StartCommandHook) = onStartCommandHooks.add(f) - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - onStartCommandHooks.forEach { f -> f(intent, flags, startId) } - return super.onStartCommand(intent, flags, startId) - } - - private val destroyHooks by lazy { linkedSetOf<() -> Unit>() } - override fun onDestroy(f: () -> Unit) = destroyHooks.add(f) - override fun onDestroy() { - super.onDestroy() - destroyHooks.forEach { f -> f() } - } - - private val onAccessibilityEventHooks by lazy { linkedSetOf<(AccessibilityEvent) -> Unit>() } - private val interestedEvents = - AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED or AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED - - override fun onAccessibilityEvent(f: (AccessibilityEvent) -> Unit) = - onAccessibilityEventHooks.add(f) - - override fun onAccessibilityEvent(event: AccessibilityEvent?) { - if (event != null && - event.packageName != null && - event.className != null && - event.eventType.and(interestedEvents) != 0 - ) { - onAccessibilityEventHooks.forEach { f -> f(event) } - } - } - - - private val onInterruptHooks by lazy { linkedSetOf<() -> Unit>() } - override fun onInterrupt(f: () -> Unit) = onInterruptHooks.add(f) - override fun onInterrupt() { - onInterruptHooks.forEach { f -> f() } - } - - private val onServiceConnectedHooks by lazy { linkedSetOf<() -> Unit>() } - override fun onServiceConnected(f: () -> Unit) = onServiceConnectedHooks.add(f) - override fun onServiceConnected() { - super.onServiceConnected() - onServiceConnectedHooks.forEach { f -> f() } - } - - - private val onKeyEventHooks by lazy { linkedSetOf<(KeyEvent?) -> Unit>() } - override fun onKeyEvent(event: KeyEvent?): Boolean { - onKeyEventHooks.forEach { f -> f(event) } - return super.onKeyEvent(event) - } - - override fun onKeyEvent(f: (KeyEvent?) -> Unit) { - onKeyEventHooks.add(f) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/li/songe/gkd/composition/CompositionExt.kt b/app/src/main/kotlin/li/songe/gkd/composition/CompositionExt.kt deleted file mode 100644 index b9a1ab7..0000000 --- a/app/src/main/kotlin/li/songe/gkd/composition/CompositionExt.kt +++ /dev/null @@ -1,55 +0,0 @@ -package li.songe.gkd.composition - -import android.app.Activity -import android.app.Service -import android.content.Context -import com.blankj.utilcode.util.LogUtils -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.cancel -import kotlin.coroutines.CoroutineContext - -object CompositionExt { - fun CanOnDestroy.useScope(context: CoroutineContext = Dispatchers.Default): CoroutineScope { - val scope = CoroutineScope(context) - onDestroy { scope.cancel() } - return scope - } - - fun Context.useLifeCycleLog() { - val simpleName = this::class.simpleName - when (this) { - is Activity, is Service -> { - LogUtils.d(simpleName, "onCreate") - } - - else -> { - LogUtils.w("current context is not the one of Activity, Service", this) - } - } - - if (this is CanOnDestroy) { - onDestroy { - LogUtils.d(simpleName, "onDestroy") - } - } - if (this is CanOnInterrupt) { - onInterrupt { - LogUtils.d(simpleName, "onInterrupt") - } - } - - if (this is CanOnServiceConnected) { - onServiceConnected { - LogUtils.d(simpleName, "onServiceConnected") - } - } - - if (this is CanOnConfigurationChanged) { - onConfigurationChanged { - LogUtils.d(simpleName, "onConfigurationChanged", it) - } - } - - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/li/songe/gkd/composition/CompositionFbService.kt b/app/src/main/kotlin/li/songe/gkd/composition/CompositionFbService.kt deleted file mode 100644 index 3ac9671..0000000 --- a/app/src/main/kotlin/li/songe/gkd/composition/CompositionFbService.kt +++ /dev/null @@ -1,41 +0,0 @@ -package li.songe.gkd.composition - -import com.torrydo.floatingbubbleview.service.expandable.BubbleBuilder -import com.torrydo.floatingbubbleview.service.expandable.ExpandableBubbleService - -open class CompositionFbService( - private val block: CompositionFbService.() -> Unit, -) : ExpandableBubbleService(), CanOnDestroy, CanConfigBubble { - - - override fun configExpandedBubble() = null - - override fun onCreate() { - block() - super.onCreate() - } - - private val destroyHooks by lazy { linkedSetOf<() -> Unit>() } - override fun onDestroy(f: () -> Unit) = destroyHooks.add(f) - override fun onDestroy() { - super.onDestroy() - destroyHooks.forEach { f -> f() } - } - - - private val configBubbleHooks by lazy { linkedSetOf() } - override fun configBubble(f: ConfigBubbleHook) { - configBubbleHooks.add(f) - } - - override fun configBubble(): BubbleBuilder? { - var result: BubbleBuilder? = null - configBubbleHooks.forEach { f -> - f { - result = it - } - } - return result - } - -} \ No newline at end of file diff --git a/app/src/main/kotlin/li/songe/gkd/composition/CompositionService.kt b/app/src/main/kotlin/li/songe/gkd/composition/CompositionService.kt deleted file mode 100644 index c364a34..0000000 --- a/app/src/main/kotlin/li/songe/gkd/composition/CompositionService.kt +++ /dev/null @@ -1,30 +0,0 @@ -package li.songe.gkd.composition - -import android.app.Service -import android.content.Intent -import android.os.IBinder - -open class CompositionService( - private val block: CompositionService.() -> Unit, -) : Service(), CanOnDestroy, CanOnStartCommand { - override fun onBind(intent: Intent?): IBinder? = null - override fun onCreate() { - super.onCreate() - block() - } - - private val onStartCommandHooks by lazy { linkedSetOf() } - override fun onStartCommand(f: StartCommandHook) = onStartCommandHooks.add(f) - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - onStartCommandHooks.forEach { f -> f(intent, flags, startId) } - return super.onStartCommand(intent, flags, startId) - } - - private val destroyHooks by lazy { linkedSetOf<() -> Unit>() } - override fun onDestroy(f: () -> Unit) = destroyHooks.add(f) - override fun onDestroy() { - super.onDestroy() - destroyHooks.forEach { f -> f() } - } - -} \ No newline at end of file diff --git a/app/src/main/kotlin/li/songe/gkd/composition/InvokeMessage.kt b/app/src/main/kotlin/li/songe/gkd/composition/InvokeMessage.kt deleted file mode 100644 index 34be770..0000000 --- a/app/src/main/kotlin/li/songe/gkd/composition/InvokeMessage.kt +++ /dev/null @@ -1,13 +0,0 @@ -package li.songe.gkd.composition - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Parcelize -@Serializable -data class InvokeMessage( - @SerialName("name") val name: String?, - @SerialName("method") val method: String, -) : Parcelable diff --git a/app/src/main/kotlin/li/songe/gkd/composition/Typealias.kt b/app/src/main/kotlin/li/songe/gkd/composition/Typealias.kt deleted file mode 100644 index 5d145f3..0000000 --- a/app/src/main/kotlin/li/songe/gkd/composition/Typealias.kt +++ /dev/null @@ -1,8 +0,0 @@ -package li.songe.gkd.composition - -import android.content.Intent -import com.torrydo.floatingbubbleview.service.expandable.BubbleBuilder - -typealias StartCommandHook = (intent: Intent?, flags: Int, startId: Int) -> Unit - -typealias ConfigBubbleHook = ((BubbleBuilder) -> Unit) -> Unit \ No newline at end of file diff --git a/app/src/main/kotlin/li/songe/gkd/data/ComplexSnapshot.kt b/app/src/main/kotlin/li/songe/gkd/data/ComplexSnapshot.kt index db3c41e..798b4a4 100644 --- a/app/src/main/kotlin/li/songe/gkd/data/ComplexSnapshot.kt +++ b/app/src/main/kotlin/li/songe/gkd/data/ComplexSnapshot.kt @@ -3,7 +3,7 @@ package li.songe.gkd.data import com.blankj.utilcode.util.ScreenUtils import kotlinx.serialization.Serializable import li.songe.gkd.app -import li.songe.gkd.service.GkdAbService +import li.songe.gkd.service.A11yService import li.songe.gkd.service.getAndUpdateCurrentRules import li.songe.gkd.service.safeActiveWindow @@ -34,7 +34,7 @@ data class ComplexSnapshot( fun createComplexSnapshot(): ComplexSnapshot { - val currentAbNode = GkdAbService.service?.safeActiveWindow + val currentAbNode = A11yService.instance?.safeActiveWindow val appId = currentAbNode?.packageName?.toString() val currentActivityId = getAndUpdateCurrentRules().topActivity.activityId diff --git a/app/src/main/kotlin/li/songe/gkd/data/ResolvedRule.kt b/app/src/main/kotlin/li/songe/gkd/data/ResolvedRule.kt index dcac4cc..3c33bd9 100644 --- a/app/src/main/kotlin/li/songe/gkd/data/ResolvedRule.kt +++ b/app/src/main/kotlin/li/songe/gkd/data/ResolvedRule.kt @@ -5,7 +5,7 @@ import android.util.Log import android.view.accessibility.AccessibilityNodeInfo import kotlinx.coroutines.Job import li.songe.gkd.META -import li.songe.gkd.service.GkdAbService +import li.songe.gkd.service.A11yService import li.songe.gkd.service.createCacheTransform import li.songe.gkd.service.createNoCacheTransform import li.songe.gkd.service.lastTriggerRule @@ -153,7 +153,7 @@ sealed class ResolvedRule( val rootNode = (if (isRootNode) { node } else { - GkdAbService.service?.safeActiveWindow + A11yService.instance?.safeActiveWindow }) ?: return null rootNode.apply { transform.cache.rootNode = this diff --git a/app/src/main/kotlin/li/songe/gkd/debug/FloatingService.kt b/app/src/main/kotlin/li/songe/gkd/debug/FloatingService.kt index a98f3d2..22f9ff9 100644 --- a/app/src/main/kotlin/li/songe/gkd/debug/FloatingService.kt +++ b/app/src/main/kotlin/li/songe/gkd/debug/FloatingService.kt @@ -12,23 +12,27 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.torrydo.floatingbubbleview.FloatingBubbleListener import com.torrydo.floatingbubbleview.service.expandable.BubbleBuilder -import kotlinx.coroutines.Dispatchers +import com.torrydo.floatingbubbleview.service.expandable.ExpandableBubbleService import kotlinx.coroutines.flow.MutableStateFlow import li.songe.gkd.app import li.songe.gkd.appScope -import li.songe.gkd.composition.CompositionExt.useLifeCycleLog -import li.songe.gkd.composition.CompositionFbService import li.songe.gkd.data.Tuple3 import li.songe.gkd.notif.createNotif import li.songe.gkd.notif.floatingChannel import li.songe.gkd.notif.floatingNotif import li.songe.gkd.util.launchTry -import li.songe.gkd.util.toast import kotlin.math.sqrt -class FloatingService : CompositionFbService({ - useLifeCycleLog() - configBubble { resolve -> +class FloatingService : ExpandableBubbleService() { + override fun configExpandedBubble() = null + + override fun onCreate() { + super.onCreate() + isRunning.value = true + minimize() + } + + override fun configBubble(): BubbleBuilder { val builder = BubbleBuilder(this).bubbleCompose { Icon( imageVector = Icons.Default.CenterFocusWeak, @@ -64,31 +68,25 @@ class FloatingService : CompositionFbService({ override fun onFingerUp(x: Float, y: Float) { if (System.currentTimeMillis() - fingerDownData.t0 < ViewConfiguration.getTapTimeout()) { // is onClick - appScope.launchTry(Dispatchers.IO) { + appScope.launchTry { SnapshotExt.captureSnapshot() - toast("快照成功") } } } }) - resolve(builder) + return builder } - isRunning.value = true - onDestroy { - isRunning.value = false - } -}) { - - override fun onCreate() { - super.onCreate() - minimize() - } override fun startNotificationForeground() { createNotif(this, floatingChannel.id, floatingNotif) } + override fun onDestroy() { + super.onDestroy() + isRunning.value = false + } + companion object { val isRunning = MutableStateFlow(false) fun stop(context: Context = app) { diff --git a/app/src/main/kotlin/li/songe/gkd/debug/HttpService.kt b/app/src/main/kotlin/li/songe/gkd/debug/HttpService.kt index 45ced7e..58fdf8f 100644 --- a/app/src/main/kotlin/li/songe/gkd/debug/HttpService.kt +++ b/app/src/main/kotlin/li/songe/gkd/debug/HttpService.kt @@ -43,7 +43,7 @@ import li.songe.gkd.debug.SnapshotExt.captureSnapshot import li.songe.gkd.notif.createNotif import li.songe.gkd.notif.httpChannel import li.songe.gkd.notif.httpNotif -import li.songe.gkd.service.GkdAbService +import li.songe.gkd.service.A11yService import li.songe.gkd.util.LOCAL_HTTP_SUBS_ID import li.songe.gkd.util.SERVER_SCRIPT_URL import li.songe.gkd.util.getIpAddressInLocalNetwork @@ -58,7 +58,7 @@ import java.io.File class HttpService : Service() { - private val scope = CoroutineScope(Dispatchers.IO) + private val scope = CoroutineScope(Dispatchers.Default) private var server: CIOApplicationEngine? = null override fun onCreate() { @@ -236,11 +236,11 @@ private fun createServer(port: Int): CIOApplicationEngine { call.respond(RpcOk()) } post("/execSelector") { - if (!GkdAbService.isRunning.value) { + if (!A11yService.isRunning.value) { throw RpcError("无障碍没有运行") } val gkdAction = call.receive() - call.respond(GkdAbService.execAction(gkdAction)) + call.respond(A11yService.execAction(gkdAction)) } } } diff --git a/app/src/main/kotlin/li/songe/gkd/debug/ScreenshotService.kt b/app/src/main/kotlin/li/songe/gkd/debug/ScreenshotService.kt index febb64f..9c82331 100644 --- a/app/src/main/kotlin/li/songe/gkd/debug/ScreenshotService.kt +++ b/app/src/main/kotlin/li/songe/gkd/debug/ScreenshotService.kt @@ -1,39 +1,46 @@ package li.songe.gkd.debug import android.annotation.SuppressLint +import android.app.Service import android.content.ComponentName import android.content.Context import android.content.Intent import com.blankj.utilcode.util.LogUtils import kotlinx.coroutines.flow.MutableStateFlow import li.songe.gkd.app -import li.songe.gkd.composition.CompositionExt.useLifeCycleLog -import li.songe.gkd.composition.CompositionService import li.songe.gkd.notif.createNotif import li.songe.gkd.notif.screenshotChannel import li.songe.gkd.notif.screenshotNotif import li.songe.gkd.util.ScreenshotUtil -class ScreenshotService : CompositionService({ - useLifeCycleLog() - createNotif(this, screenshotChannel.id, screenshotNotif) +class ScreenshotService : Service() { + override fun onBind(intent: Intent?) = null - onStartCommand { intent, _, _ -> - if (intent == null) return@onStartCommand - screenshotUtil?.destroy() - screenshotUtil = ScreenshotUtil(this, intent) - LogUtils.d("screenshot restart") + override fun onCreate() { + super.onCreate() + isRunning.value = true + createNotif(this, screenshotChannel.id, screenshotNotif) } - onDestroy { + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + try { + return super.onStartCommand(intent, flags, startId) + } finally { + intent?.let { + screenshotUtil?.destroy() + screenshotUtil = ScreenshotUtil(this, intent) + LogUtils.d("screenshot restart") + } + } + } + + override fun onDestroy() { + super.onDestroy() + isRunning.value = false screenshotUtil?.destroy() screenshotUtil = null } - isRunning.value = true - onDestroy { - isRunning.value = false - } -}) { companion object { suspend fun screenshot() = screenshotUtil?.execute() diff --git a/app/src/main/kotlin/li/songe/gkd/debug/SnapshotExt.kt b/app/src/main/kotlin/li/songe/gkd/debug/SnapshotExt.kt index a741a87..ccbc3bb 100644 --- a/app/src/main/kotlin/li/songe/gkd/debug/SnapshotExt.kt +++ b/app/src/main/kotlin/li/songe/gkd/debug/SnapshotExt.kt @@ -18,7 +18,7 @@ import li.songe.gkd.data.RpcError import li.songe.gkd.data.createComplexSnapshot import li.songe.gkd.data.toSnapshot import li.songe.gkd.db.DbSet -import li.songe.gkd.service.GkdAbService +import li.songe.gkd.service.A11yService import li.songe.gkd.util.appInfoCacheFlow import li.songe.gkd.util.keepNullJson import li.songe.gkd.util.snapshotFolder @@ -82,7 +82,7 @@ object SnapshotExt { private val captureLoading = MutableStateFlow(false) suspend fun captureSnapshot(skipScreenshot: Boolean = false): ComplexSnapshot { - if (!GkdAbService.isRunning.value) { + if (!A11yService.isRunning.value) { throw RpcError("无障碍不可用") } if (captureLoading.value) { @@ -105,7 +105,7 @@ object SnapshotExt { Bitmap.Config.ARGB_8888 ) } else { - GkdAbService.currentScreenshot() ?: withTimeoutOrNull(3_000) { + A11yService.currentScreenshot() ?: withTimeoutOrNull(3_000) { if (!ScreenshotService.isRunning.value) { return@withTimeoutOrNull null } @@ -141,7 +141,7 @@ object SnapshotExt { File(getSnapshotPath(snapshot.id)).writeText(text) DbSet.snapshotDao.insert(snapshot.toSnapshot()) } - toast("快照捕获成功") + toast("快照成功") return snapshot } finally { captureLoading.value = false diff --git a/app/src/main/kotlin/li/songe/gkd/debug/SnapshotTileService.kt b/app/src/main/kotlin/li/songe/gkd/debug/SnapshotTileService.kt index 1bdfdcf..ad94766 100644 --- a/app/src/main/kotlin/li/songe/gkd/debug/SnapshotTileService.kt +++ b/app/src/main/kotlin/li/songe/gkd/debug/SnapshotTileService.kt @@ -7,9 +7,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import li.songe.gkd.appScope import li.songe.gkd.debug.SnapshotExt.captureSnapshot -import li.songe.gkd.service.GkdAbService -import li.songe.gkd.service.GkdAbService.Companion.eventExecutor -import li.songe.gkd.service.GkdAbService.Companion.shizukuTopActivityGetter +import li.songe.gkd.service.A11yService import li.songe.gkd.service.TopActivity import li.songe.gkd.service.getAndUpdateCurrentRules import li.songe.gkd.service.safeActiveWindow @@ -21,7 +19,7 @@ class SnapshotTileService : TileService() { override fun onClick() { super.onClick() LogUtils.d("SnapshotTileService::onClick") - val service = GkdAbService.service + val service = A11yService.instance if (service == null) { toast("无障碍没有开启") return @@ -47,9 +45,11 @@ class SnapshotTileService : TileService() { } } else if (latestAppId != oldAppId) { LogUtils.d("SnapshotTileService::eventExecutor.execute") - eventExecutor.execute { + A11yService.eventExecutor.execute { updateTopActivity( - shizukuTopActivityGetter?.invoke() ?: TopActivity(appId = latestAppId) + A11yService.instance?.getShizukuTopActivity?.invoke() ?: TopActivity( + appId = latestAppId + ) ) getAndUpdateCurrentRules() appScope.launchTry(Dispatchers.IO) { diff --git a/app/src/main/kotlin/li/songe/gkd/permission/PermissionState.kt b/app/src/main/kotlin/li/songe/gkd/permission/PermissionState.kt index 1492032..c1dafa1 100644 --- a/app/src/main/kotlin/li/songe/gkd/permission/PermissionState.kt +++ b/app/src/main/kotlin/li/songe/gkd/permission/PermissionState.kt @@ -11,11 +11,8 @@ import com.hjq.permissions.Permission import com.hjq.permissions.XXPermissions import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.updateAndGet -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock import li.songe.gkd.app import li.songe.gkd.appScope -import li.songe.gkd.service.fixRestartService import li.songe.gkd.shizuku.newActivityTaskManager import li.songe.gkd.shizuku.safeGetTasks import li.songe.gkd.shizuku.shizukuIsSafeOK @@ -175,23 +172,17 @@ val shizukuOkState by lazy { ) } -private val checkAuthMutex by lazy { Mutex() } suspend fun updatePermissionState() { - if (checkAuthMutex.isLocked) return - checkAuthMutex.withLock { - arrayOf( - notificationState, - canDrawOverlaysState, - canWriteExternalStorage, - ).forEach { it.updateAndGet() } - if (canQueryPkgState.stateFlow.value != canQueryPkgState.updateAndGet()) { - appScope.launchTry { - initOrResetAppInfoCache() - } + arrayOf( + notificationState, + canDrawOverlaysState, + canWriteExternalStorage, + writeSecureSettingsState, + shizukuOkState, + ).forEach { it.updateAndGet() } + if (canQueryPkgState.stateFlow.value != canQueryPkgState.updateAndGet()) { + appScope.launchTry { + initOrResetAppInfoCache() } - if (writeSecureSettingsState.stateFlow.value != writeSecureSettingsState.updateAndGet()) { - fixRestartService() - } - shizukuOkState.updateAndGet() } } \ No newline at end of file diff --git a/app/src/main/kotlin/li/songe/gkd/service/AbEvent.kt b/app/src/main/kotlin/li/songe/gkd/service/A11yEvent.kt similarity index 79% rename from app/src/main/kotlin/li/songe/gkd/service/AbEvent.kt rename to app/src/main/kotlin/li/songe/gkd/service/A11yEvent.kt index 93e7cb2..d202731 100644 --- a/app/src/main/kotlin/li/songe/gkd/service/AbEvent.kt +++ b/app/src/main/kotlin/li/songe/gkd/service/A11yEvent.kt @@ -2,15 +2,15 @@ package li.songe.gkd.service import android.view.accessibility.AccessibilityEvent -data class AbEvent( +data class A11yEvent( val type: Int, val time: Long, val appId: String, val className: String, ) -fun AccessibilityEvent.toAbEvent(): AbEvent? { - return AbEvent( +fun AccessibilityEvent.toA11yEvent(): A11yEvent? { + return A11yEvent( type = eventType, time = System.currentTimeMillis(), appId = packageName?.toString() ?: return null, diff --git a/app/src/main/kotlin/li/songe/gkd/service/AbExt.kt b/app/src/main/kotlin/li/songe/gkd/service/A11yExt.kt similarity index 99% rename from app/src/main/kotlin/li/songe/gkd/service/AbExt.kt rename to app/src/main/kotlin/li/songe/gkd/service/A11yExt.kt index 87dd78f..68b511e 100644 --- a/app/src/main/kotlin/li/songe/gkd/service/AbExt.kt +++ b/app/src/main/kotlin/li/songe/gkd/service/A11yExt.kt @@ -83,7 +83,7 @@ fun AccessibilityNodeInfo.querySelector( val root = if (isRootNode) { return this } else { - GkdAbService.service?.safeActiveWindow ?: return null + A11yService.instance?.safeActiveWindow ?: return null } return selector.match(root, transform, option) } @@ -266,7 +266,7 @@ class NodeCache { fun getRoot(node: AccessibilityNodeInfo): AccessibilityNodeInfo? { if (rootNode == null) { - rootNode = GkdAbService.service?.safeActiveWindow + rootNode = A11yService.instance?.safeActiveWindow } if (node == rootNode) return null return rootNode diff --git a/app/src/main/kotlin/li/songe/gkd/service/GkdAbService.kt b/app/src/main/kotlin/li/songe/gkd/service/A11yService.kt similarity index 60% rename from app/src/main/kotlin/li/songe/gkd/service/GkdAbService.kt rename to app/src/main/kotlin/li/songe/gkd/service/A11yService.kt index bb05a5d..7862845 100644 --- a/app/src/main/kotlin/li/songe/gkd/service/GkdAbService.kt +++ b/app/src/main/kotlin/li/songe/gkd/service/A11yService.kt @@ -1,5 +1,7 @@ package li.songe.gkd.service +import android.accessibilityservice.AccessibilityService +import android.accessibilityservice.AccessibilityService.WINDOW_SERVICE import android.content.BroadcastReceiver import android.content.ComponentName import android.content.Context @@ -17,9 +19,11 @@ import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityNodeInfo import com.blankj.utilcode.util.LogUtils import com.blankj.utilcode.util.ScreenUtils +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.debounce @@ -27,10 +31,8 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import li.songe.gkd.META +import li.songe.gkd.app import li.songe.gkd.appScope -import li.songe.gkd.composition.CompositionAbService -import li.songe.gkd.composition.CompositionExt.useLifeCycleLog -import li.songe.gkd.composition.CompositionExt.useScope import li.songe.gkd.data.ActionPerformer import li.songe.gkd.data.ActionResult import li.songe.gkd.data.AppRule @@ -41,11 +43,14 @@ import li.songe.gkd.data.RpcError import li.songe.gkd.data.RuleStatus import li.songe.gkd.data.clearNodeCache import li.songe.gkd.debug.SnapshotExt +import li.songe.gkd.permission.shizukuOkState import li.songe.gkd.shizuku.getShizukuCanUsedFlow -import li.songe.gkd.shizuku.shizukuIsSafeOK import li.songe.gkd.shizuku.useSafeGetTasksFc import li.songe.gkd.shizuku.useSafeInputTapFc -import li.songe.gkd.shizuku.useShizukuAliveState +import li.songe.gkd.util.OnA11yConnected +import li.songe.gkd.util.OnA11yEvent +import li.songe.gkd.util.OnCreate +import li.songe.gkd.util.OnDestroy import li.songe.gkd.util.UpdateTimeOption import li.songe.gkd.util.checkSubsUpdate import li.songe.gkd.util.launchTry @@ -55,104 +60,130 @@ import li.songe.gkd.util.storeFlow import li.songe.gkd.util.toast import li.songe.selector.MatchOption import li.songe.selector.Selector +import java.lang.ref.WeakReference import java.util.concurrent.Executors import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine -class GkdAbService : CompositionAbService({ - useLifeCycleLog() - updateLauncherAppId() - - val context = this as GkdAbService - val scope = useScope() - - service = context - onDestroy { - service = null +class A11yService : AccessibilityService(), OnCreate, OnA11yConnected, OnA11yEvent, OnDestroy { + override fun onCreate() { + super.onCreate() + onCreated() } - val shizukuAliveFlow = useShizukuAliveState() - val shizukuGrantFlow = MutableStateFlow(false) - var lastCheckShizukuTime = 0L - onAccessibilityEvent { // 借助无障碍轮询校验 shizuku 权限, 因为 shizuku 可能无故被关闭 - if ((storeFlow.value.enableShizukuActivity || storeFlow.value.enableShizukuClick) && it.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {// 筛选降低判断频率 - val t = System.currentTimeMillis() - if (t - lastCheckShizukuTime > 60_000L) { - lastCheckShizukuTime = t - scope.launchTry(Dispatchers.IO) { - shizukuGrantFlow.value = if (shizukuAliveFlow.value) { - shizukuIsSafeOK() - } else { - false + override fun onServiceConnected() { + super.onServiceConnected() + onA11yConnected() + } + + override fun onAccessibilityEvent(event: AccessibilityEvent?) { + if (event == null || !event.isUseful()) return + onA11yEvent(event) + } + + override fun onInterrupt() {} + override fun onDestroy() { + super.onDestroy() + onDestroyed() + scope.cancel() + } + + val scope = CoroutineScope(Dispatchers.Default) + val getShizukuTopActivity by lazy { useGetShizukuActivity() } + val getShizukuClick by lazy { useGetShizukuClick() } + + init { + useRunningState() + useAliveView() + useCaptureVolume() + onA11yEvent(::handleCaptureScreenshot) + useAutoUpdateSubs() + useRuleChangedLog() + useAutoCheckShizuku() + getShizukuTopActivity + getShizukuClick + useMatchRule() + } + + companion object { + internal var weakInstance = WeakReference(null) + val instance: A11yService? + get() = weakInstance.get() + val isRunning = MutableStateFlow(false) + + // AccessibilityInteractionClient.getInstanceForThread(threadId) + val queryThread by lazy { Executors.newSingleThreadExecutor().asCoroutineDispatcher() } + val eventExecutor by lazy { Executors.newSingleThreadExecutor()!! } + val actionThread by lazy { Executors.newSingleThreadExecutor().asCoroutineDispatcher() } + + fun execAction(gkdAction: GkdAction): ActionResult { + val serviceVal = instance ?: throw RpcError("无障碍没有运行") + val selector = Selector.parseOrNull(gkdAction.selector) ?: throw RpcError("非法选择器") + selector.checkSelector()?.let { + throw RpcError(it) + } + val targetNode = serviceVal.safeActiveWindow?.querySelector( + selector, MatchOption( + quickFind = gkdAction.quickFind, + fastQuery = gkdAction.fastQuery, + ), createCacheTransform().transform, isRootNode = true + ) ?: throw RpcError("没有查询到节点") + + if (gkdAction.action == null) { + // 仅查询 + return ActionResult( + action = null, + result = true + ) + } + + return ActionPerformer + .getAction(gkdAction.action) + .perform(serviceVal, targetNode, gkdAction.position, instance?.getShizukuClick) + } + + + suspend fun currentScreenshot(): Bitmap? = suspendCoroutine { + if (instance == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + it.resume(null) + } else { + val callback = object : TakeScreenshotCallback { + override fun onSuccess(screenshot: ScreenshotResult) { + try { + it.resume( + Bitmap.wrapHardwareBuffer( + screenshot.hardwareBuffer, screenshot.colorSpace + ) + ) + } finally { + screenshot.hardwareBuffer.close() + } } + + override fun onFailure(errorCode: Int) = it.resume(null) } + instance!!.takeScreenshot( + Display.DEFAULT_DISPLAY, + instance!!.application.mainExecutor, + callback + ) } } } - val shizukuCanUsedFlow = getShizukuCanUsedFlow( - scope, - shizukuGrantFlow, - shizukuAliveFlow, - storeFlow.map(scope) { s -> s.enableShizukuActivity } - ) - val safeGetTasksFc by lazy { useSafeGetTasksFc(scope, shizukuCanUsedFlow) } +} - val shizukuClickCanUsedFlow = getShizukuCanUsedFlow( - scope, - shizukuGrantFlow, - shizukuAliveFlow, - storeFlow.map(scope) { s -> s.enableShizukuClick } - ) - val safeInjectClickEventFc = useSafeInputTapFc(scope, shizukuClickCanUsedFlow) - injectClickEventFc = safeInjectClickEventFc - onDestroy { - injectClickEventFc = null - } - - // 当锁屏/上拉通知栏时, safeActiveWindow 没有 activityId, 但是此时 shizuku 获取到是前台 app 的 appId 和 activityId - fun getShizukuTopActivity(): TopActivity? { - if (!storeFlow.value.enableShizukuActivity) return null - // 平均耗时 5 ms - val top = safeGetTasksFc()?.lastOrNull()?.topActivity ?: return null - return TopActivity(appId = top.packageName, activityId = top.className) - } - shizukuTopActivityGetter = ::getShizukuTopActivity - onDestroy { - shizukuTopActivityGetter = null - } - - val activityCache = object : LruCache, Boolean>(128) { - override fun create(key: Pair): Boolean { - return kotlin.runCatching { - packageManager.getActivityInfo( - ComponentName( - key.first, key.second - ), 0 - ) - }.getOrNull() != null - } - } - - fun isActivity( - appId: String, - activityId: String, - ): Boolean { - if (appId == topActivityFlow.value.appId && activityId == topActivityFlow.value.activityId) return true - val cacheKey = Pair(appId, activityId) - return activityCache.get(cacheKey) - } +private fun A11yService.useMatchRule() { + val context = this var lastTriggerShizukuTime = 0L var lastContentEventTime = 0L val events = mutableListOf() var queryTaskJob: Job? = null fun newQueryTask( - byEvent: Boolean = false, - byForced: Boolean = false, - delayRule: ResolvedRule? = null + byEvent: Boolean = false, byForced: Boolean = false, delayRule: ResolvedRule? = null ) { if (!storeFlow.value.enableMatch) return - queryTaskJob = scope.launchTry(queryThread) { + queryTaskJob = scope.launchTry(A11yService.queryThread) { var latestEvent = if (delayRule != null) {// 延迟规则不消耗事件 null } else { @@ -180,7 +211,7 @@ class GkdAbService : CompositionAbService({ if (delayRule != null && delayRule !== rule) continue val statusCode = rule.status if (statusCode == RuleStatus.Status3 && rule.matchDelayJob == null) { - rule.matchDelayJob = scope.launch(actionThread) { + rule.matchDelayJob = scope.launch(A11yService.actionThread) { delay(rule.matchDelay) rule.matchDelayJob = null newQueryTask(delayRule = rule) @@ -207,7 +238,7 @@ class GkdAbService : CompositionAbService({ rightAppId ) if (topActivityFlow.value.appId != rightAppId || (!matchApp && rule is AppRule)) { - eventExecutor.execute { + A11yService.eventExecutor.execute { if (topActivityFlow.value.appId != rightAppId) { val shizukuTop = getShizukuTopActivity() if (shizukuTop?.appId == rightAppId) { @@ -216,7 +247,7 @@ class GkdAbService : CompositionAbService({ updateTopActivity(TopActivity(appId = rightAppId)) } getAndUpdateCurrentRules() - scope.launch(actionThread) { + scope.launch(A11yService.actionThread) { delay(300) if (queryTaskJob?.isActive != true) { newQueryTask() @@ -230,7 +261,7 @@ class GkdAbService : CompositionAbService({ val target = rule.query(nodeVal, latestEvent == null) ?: continue if (activityRule !== getAndUpdateCurrentRules()) break if (rule.checkDelay() && rule.actionDelayJob == null) { - rule.actionDelayJob = scope.launch(actionThread) { + rule.actionDelayJob = scope.launch(A11yService.actionThread) { delay(rule.actionDelay) rule.actionDelayJob = null newQueryTask(delayRule = rule) @@ -238,10 +269,10 @@ class GkdAbService : CompositionAbService({ continue } if (rule.status != RuleStatus.StatusOk) break - val actionResult = rule.performAction(context, target, safeInjectClickEventFc) + val actionResult = rule.performAction(context, target, getShizukuClick) if (actionResult.result) { rule.trigger() - scope.launch(actionThread) { + scope.launch(A11yService.actionThread) { delay(300) if (queryTaskJob?.isActive != true) { newQueryTask() @@ -251,16 +282,14 @@ class GkdAbService : CompositionAbService({ appScope.launchTry(Dispatchers.IO) { insertClickLog(rule) LogUtils.d( - rule.statusText(), - AttrInfo.info2data(target, 0, 0), - actionResult + rule.statusText(), AttrInfo.info2data(target, 0, 0), actionResult ) } } } val t = System.currentTimeMillis() if (t - lastTriggerTime < 3000L || t - appChangeTime < 5000L) { - scope.launch(actionThread) { + scope.launch(A11yService.actionThread) { delay(300) if (queryTaskJob?.isActive != true) { newQueryTask() @@ -268,7 +297,7 @@ class GkdAbService : CompositionAbService({ } } else { if (activityRule.currentRules.any { r -> r.checkForced() && r.status.let { s -> s == RuleStatus.StatusOk || s == RuleStatus.Status5 } }) { - scope.launch(actionThread) { + scope.launch(A11yService.actionThread) { delay(300) if (queryTaskJob?.isActive != true) { newQueryTask(byForced = true) @@ -280,17 +309,18 @@ class GkdAbService : CompositionAbService({ } val skipAppIds = setOf("com.android.systemui") - onAccessibilityEvent { event -> - if (event.eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED && - skipAppIds.contains(event.packageName.toString()) + onA11yEvent { event -> + if (event.eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED && skipAppIds.contains( + event.packageName.toString() + ) ) { - return@onAccessibilityEvent + return@onA11yEvent } - val fixedEvent = event.toAbEvent() ?: return@onAccessibilityEvent + val fixedEvent = event.toA11yEvent() ?: return@onA11yEvent if (fixedEvent.type == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) { if (fixedEvent.time - lastContentEventTime < 100 && fixedEvent.time - appChangeTime > 5000 && fixedEvent.time - lastTriggerTime > 3000) { - return@onAccessibilityEvent + return@onA11yEvent } lastContentEventTime = fixedEvent.time } @@ -306,7 +336,7 @@ class GkdAbService : CompositionAbService({ val evAppId = fixedEvent.appId val evActivityId = fixedEvent.className - eventExecutor.execute launch@{ + A11yService.eventExecutor.execute launch@{ val oldAppId = topActivityFlow.value.appId val rightAppId = if (oldAppId == evAppId) { oldAppId @@ -372,20 +402,9 @@ class GkdAbService : CompositionAbService({ newQueryTask(eventNode != null) } } +} - var lastUpdateSubsTime = System.currentTimeMillis() - 25000 - onAccessibilityEvent {// 借助 无障碍事件 触发自动检测更新 - if (it.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {// 筛选降低判断频率 - val i = storeFlow.value.updateSubsInterval - if (i <= 0) return@onAccessibilityEvent - val t = System.currentTimeMillis() - if (t - lastUpdateSubsTime > i.coerceAtLeast(UpdateTimeOption.Everyday.value)) { - lastUpdateSubsTime = t - checkSubsUpdate() - } - } - } - +private fun A11yService.useRuleChangedLog() { scope.launch(Dispatchers.IO) { activityRuleFlow.debounce(300).collect { if (storeFlow.value.enableMatch && it.currentRules.isNotEmpty()) { @@ -395,190 +414,218 @@ class GkdAbService : CompositionAbService({ } } } +} - var aliveView: View? = null - val wm by lazy { context.getSystemService(WINDOW_SERVICE) as WindowManager } - onServiceConnected { - scope.launchTry { - storeFlow.map(scope) { s -> s.enableAbFloatWindow }.collect { - if (aliveView != null) { - withContext(Dispatchers.Main) { - wm.removeView(aliveView) - } - } - if (it) { - val tempView = View(context) - val lp = WindowManager.LayoutParams().apply { - type = WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY - format = PixelFormat.TRANSLUCENT - flags = - flags or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE - width = 1 - height = 1 - packageName = context.packageName - } - withContext(Dispatchers.Main) { - try { - // 在某些机型创建失败, 原因未知 - wm.addView(tempView, lp) - aliveView = tempView - } catch (e: Exception) { - LogUtils.d("创建无障碍悬浮窗失败", e) - toast("创建无障碍悬浮窗失败") - storeFlow.update { store -> - store.copy(enableAbFloatWindow = false) - } - } - } - } else { - aliveView = null +private fun A11yService.useRunningState() { + onCreated { + A11yService.weakInstance = WeakReference(this) + A11yService.isRunning.value = true + ManageService.autoStart() + } + onDestroyed { + A11yService.weakInstance = WeakReference(null) + A11yService.isRunning.value = false + } +} + +private fun A11yService.useGetShizukuActivity(): () -> TopActivity? { + val enableShizukuActivityFlow = storeFlow.map(scope) { s -> s.enableShizukuActivity } + val shizukuActivityUsedFlow = getShizukuCanUsedFlow(scope, enableShizukuActivityFlow) + val safeGetTasksFc by lazy { useSafeGetTasksFc(scope, shizukuActivityUsedFlow) } + // 当锁屏/下拉通知栏时, safeActiveWindow 没有 activityId, 但是此时 shizuku 获取到是前台 app 的 appId 和 activityId + return fun(): TopActivity? { + if (!storeFlow.value.enableShizukuActivity) return null + // 平均耗时 5 ms + val top = safeGetTasksFc()?.lastOrNull()?.topActivity ?: return null + return TopActivity(appId = top.packageName, activityId = top.className) + } +} + +private fun A11yService.useGetShizukuClick(): (Float, Float) -> Boolean? { + val shizukuClickUsedFlow = + getShizukuCanUsedFlow(scope, storeFlow.map(scope) { s -> s.enableShizukuClick }) + val safeInjectClickEventFc = useSafeInputTapFc(scope, shizukuClickUsedFlow) + return safeInjectClickEventFc +} + +private fun A11yService.useAutoCheckShizuku() { + var lastCheckShizukuTime = 0L + onA11yEvent { + // 借助无障碍轮询校验 shizuku 权限, 因为 shizuku 可能无故被关闭 + if ((storeFlow.value.enableShizukuActivity || storeFlow.value.enableShizukuClick) && it.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {// 筛选降低判断频率 + val t = System.currentTimeMillis() + if (t - lastCheckShizukuTime > 10 * 60_000L) { + lastCheckShizukuTime = t + scope.launchTry(Dispatchers.IO) { + shizukuOkState.updateAndGet() } } } } - onDestroy { +} + +private fun A11yService.useAliveView() { + val context = this + var aliveView: View? = null + val wm by lazy { getSystemService(WINDOW_SERVICE) as WindowManager } + suspend fun removeA11View() { + if (aliveView != null) { + withContext(Dispatchers.Main) { + wm.removeView(aliveView) + } + aliveView = null + } + } + + suspend fun addA11View() { + removeA11View() + val tempView = View(context) + val lp = WindowManager.LayoutParams().apply { + type = WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY + format = PixelFormat.TRANSLUCENT + flags = + flags or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + width = 1 + height = 1 + packageName = context.packageName + } + withContext(Dispatchers.Main) { + try { + // 在某些机型创建失败, 原因未知 + wm.addView(tempView, lp) + aliveView = tempView + } catch (e: Exception) { + LogUtils.d("创建无障碍悬浮窗失败", e) + toast("创建无障碍悬浮窗失败") + storeFlow.update { store -> + store.copy(enableAbFloatWindow = false) + } + } + } + } + + onA11yConnected { + scope.launchTry { + storeFlow.map(scope) { s -> s.enableAbFloatWindow }.collect { + if (it) { + addA11View() + } else { + removeA11View() + } + } + } + } + onDestroyed { if (aliveView != null) { wm.removeView(aliveView) } } +} - val volumeChangedAction = "android.media.VOLUME_CHANGED_ACTION" - fun createVolumeReceiver() = object : BroadcastReceiver() { - var lastTriggerTime = -1L - override fun onReceive(context: Context?, intent: Intent?) { - if (intent?.action == volumeChangedAction) { - val t = System.currentTimeMillis() - if (t - lastTriggerTime > 3000 && !ScreenUtils.isScreenLock()) { - lastTriggerTime = t - scope.launchTry(Dispatchers.IO) { - SnapshotExt.captureSnapshot() - toast("快照成功") - } +private fun A11yService.useAutoUpdateSubs() { + var lastUpdateSubsTime = System.currentTimeMillis() - 25000 + onA11yEvent {// 借助 无障碍事件 触发自动检测更新 + if (it.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {// 筛选降低判断频率 + val i = storeFlow.value.updateSubsInterval + if (i <= 0) return@onA11yEvent + val t = System.currentTimeMillis() + if (t - lastUpdateSubsTime > i.coerceAtLeast(UpdateTimeOption.Everyday.value)) { + lastUpdateSubsTime = t + checkSubsUpdate() + } + } + } +} + +private const val volumeChangedAction = "android.media.VOLUME_CHANGED_ACTION" +private fun createVolumeReceiver() = object : BroadcastReceiver() { + var lastTriggerTime = -1L + override fun onReceive(context: Context?, intent: Intent?) { + if (intent?.action == volumeChangedAction) { + val t = System.currentTimeMillis() + if (t - lastTriggerTime > 3000 && !ScreenUtils.isScreenLock()) { + lastTriggerTime = t + appScope.launchTry { + SnapshotExt.captureSnapshot() } } } } +} +private fun A11yService.useCaptureVolume() { var captureVolumeReceiver: BroadcastReceiver? = null - scope.launch { - storeFlow.map(scope) { s -> s.captureVolumeChange }.collect { - if (captureVolumeReceiver != null) { - context.unregisterReceiver(captureVolumeReceiver) - } - captureVolumeReceiver = if (it) { - createVolumeReceiver().apply { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - context.registerReceiver( - this, IntentFilter(volumeChangedAction), Context.RECEIVER_EXPORTED - ) - } else { - context.registerReceiver(this, IntentFilter(volumeChangedAction)) + onCreated { + scope.launch { + storeFlow.map(scope) { s -> s.captureVolumeChange }.collect { + if (captureVolumeReceiver != null) { + unregisterReceiver(captureVolumeReceiver) + } + captureVolumeReceiver = if (it) { + createVolumeReceiver().apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + registerReceiver( + this, IntentFilter(volumeChangedAction), Context.RECEIVER_EXPORTED + ) + } else { + registerReceiver(this, IntentFilter(volumeChangedAction)) + } } - } - } else { - null - } - } - } - onDestroy { - if (captureVolumeReceiver != null) { - context.unregisterReceiver(captureVolumeReceiver) - } - } - - onAccessibilityEvent { e -> - if (!storeFlow.value.captureScreenshot) return@onAccessibilityEvent - val appId = e.packageName ?: return@onAccessibilityEvent - val appCls = e.className ?: return@onAccessibilityEvent - if (appId.contentEquals("com.miui.screenshot") && e.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED && !e.isFullScreen && appCls.contentEquals( - "android.widget.RelativeLayout" - ) && e.text.firstOrNull()?.contentEquals("截屏缩略图") == true // [截屏缩略图, 截长屏, 发送] - ) { - LogUtils.d("captureScreenshot", e) - scope.launchTry(Dispatchers.IO) { - SnapshotExt.captureSnapshot(skipScreenshot = true) - } - } - } - - isRunning.value = true - onDestroy { - isRunning.value = false - } - - ManageService.autoStart(context) - onDestroy { - if (!storeFlow.value.enableStatusService && ManageService.isRunning.value) { - ManageService.stop() - } - } -}) { - - companion object { - // AccessibilityInteractionClient.getInstanceForThread(threadId) - val queryThread by lazy { Executors.newSingleThreadExecutor().asCoroutineDispatcher() } - val eventExecutor by lazy { Executors.newSingleThreadExecutor()!! } - val actionThread by lazy { Executors.newSingleThreadExecutor().asCoroutineDispatcher() } - - var shizukuTopActivityGetter: (() -> TopActivity?)? = null - private var injectClickEventFc: ((x: Float, y: Float) -> Boolean?)? = null - var service: GkdAbService? = null - - val isRunning = MutableStateFlow(false) - - fun execAction(gkdAction: GkdAction): ActionResult { - val serviceVal = service ?: throw RpcError("无障碍没有运行") - val selector = Selector.parseOrNull(gkdAction.selector) ?: throw RpcError("非法选择器") - selector.checkSelector()?.let { - throw RpcError(it) - } - val targetNode = serviceVal.safeActiveWindow?.querySelector( - selector, - MatchOption( - quickFind = gkdAction.quickFind, - fastQuery = gkdAction.fastQuery, - ), - createCacheTransform().transform, - isRootNode = true - ) ?: throw RpcError("没有查询到节点") - - if (gkdAction.action == null) { - // 仅查询 - return ActionResult( - action = null, - result = true - ) - } - - return ActionPerformer.getAction(gkdAction.action) - .perform(serviceVal, targetNode, gkdAction.position, injectClickEventFc) - } - - - suspend fun currentScreenshot() = service?.run { - suspendCoroutine { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - takeScreenshot(Display.DEFAULT_DISPLAY, - application.mainExecutor, - object : TakeScreenshotCallback { - override fun onSuccess(screenshot: ScreenshotResult) { - try { - it.resume( - Bitmap.wrapHardwareBuffer( - screenshot.hardwareBuffer, screenshot.colorSpace - ) - ) - } finally { - screenshot.hardwareBuffer.close() - } - } - - override fun onFailure(errorCode: Int) = it.resume(null) - }) } else { - it.resume(null) + null } } } } -} \ No newline at end of file + onDestroyed { + if (captureVolumeReceiver != null) { + unregisterReceiver(captureVolumeReceiver) + } + } +} + +private const val interestedEvents = + AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED or AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED + +private fun AccessibilityEvent.isUseful(): Boolean { + return packageName != null && className != null && eventType.and(interestedEvents) != 0 +} + +private val activityCache = object : LruCache, Boolean>(128) { + override fun create(key: Pair): Boolean { + return runCatching { + app.packageManager.getActivityInfo( + ComponentName( + key.first, key.second + ), 0 + ) + }.getOrNull() != null + } +} + +private fun isActivity( + appId: String, + activityId: String, +): Boolean { + if (appId == topActivityFlow.value.appId && activityId == topActivityFlow.value.activityId) return true + val cacheKey = Pair(appId, activityId) + return activityCache.get(cacheKey) +} + +private fun handleCaptureScreenshot(event: AccessibilityEvent) { + if (!storeFlow.value.captureScreenshot) return + val appId = event.packageName!! + val appCls = event.className!! + if (event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED && !event.isFullScreen && appId.contentEquals( + "com.miui.screenshot" + ) && appCls.contentEquals( + "android.widget.RelativeLayout" + ) && event.text.firstOrNull() + ?.contentEquals("截屏缩略图") == true // [截屏缩略图, 截长屏, 发送] + ) { + LogUtils.d("captureScreenshot", event) + appScope.launchTry { + SnapshotExt.captureSnapshot(skipScreenshot = true) + } + } +} diff --git a/app/src/main/kotlin/li/songe/gkd/service/AbState.kt b/app/src/main/kotlin/li/songe/gkd/service/A11yState.kt similarity index 100% rename from app/src/main/kotlin/li/songe/gkd/service/AbState.kt rename to app/src/main/kotlin/li/songe/gkd/service/A11yState.kt diff --git a/app/src/main/kotlin/li/songe/gkd/service/GkdTileService.kt b/app/src/main/kotlin/li/songe/gkd/service/GkdTileService.kt index 7e31453..c3db991 100644 --- a/app/src/main/kotlin/li/songe/gkd/service/GkdTileService.kt +++ b/app/src/main/kotlin/li/songe/gkd/service/GkdTileService.kt @@ -16,7 +16,7 @@ import li.songe.gkd.util.toast class GkdTileService : TileService() { private fun updateTile(): Boolean { val oldState = qsTile.state - val newState = if (GkdAbService.isRunning.value) { + val newState = if (A11yService.isRunning.value) { Tile.STATE_ACTIVE } else { Tile.STATE_INACTIVE @@ -93,7 +93,7 @@ fun switchA11yService(): Boolean { return false } val names = getServiceNames() - if (GkdAbService.isRunning.value) { + if (A11yService.isRunning.value) { names.remove(a11yClsName) updateServiceNames(names) storeFlow.update { it.copy(enableService = false) } @@ -116,7 +116,7 @@ fun fixRestartService(): Boolean { // 1. 服务没有运行 // 2. 用户配置开启了服务 // 3. 有写入系统设置权限 - if (!GkdAbService.isRunning.value && storeFlow.value.enableService && writeSecureSettingsState.updateAndGet()) { + if (!A11yService.isRunning.value && storeFlow.value.enableService && writeSecureSettingsState.updateAndGet()) { val t = System.currentTimeMillis() if (t - lastRestartA11yServiceTimeFlow.value < 10_000) return false lastRestartA11yServiceTimeFlow.value = t @@ -136,5 +136,5 @@ fun fixRestartService(): Boolean { } private val a11yClsName by lazy { - ComponentName(app, GkdAbService::class.java).flattenToShortString() + ComponentName(app, A11yService::class.java).flattenToShortString() } \ No newline at end of file diff --git a/app/src/main/kotlin/li/songe/gkd/service/ManageService.kt b/app/src/main/kotlin/li/songe/gkd/service/ManageService.kt index e574e84..4c602b0 100644 --- a/app/src/main/kotlin/li/songe/gkd/service/ManageService.kt +++ b/app/src/main/kotlin/li/songe/gkd/service/ManageService.kt @@ -1,8 +1,9 @@ package li.songe.gkd.service -import android.content.Context +import android.app.Service import android.content.Intent -import androidx.core.app.NotificationManagerCompat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine @@ -10,70 +11,73 @@ import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import li.songe.gkd.app -import li.songe.gkd.composition.CompositionExt.useLifeCycleLog -import li.songe.gkd.composition.CompositionExt.useScope -import li.songe.gkd.composition.CompositionService import li.songe.gkd.notif.abNotif import li.songe.gkd.notif.createNotif import li.songe.gkd.notif.defaultChannel +import li.songe.gkd.permission.notificationState import li.songe.gkd.util.actionCountFlow import li.songe.gkd.util.getSubsStatus import li.songe.gkd.util.ruleSummaryFlow import li.songe.gkd.util.storeFlow -class ManageService : CompositionService({ - useLifeCycleLog() - val context = this - createNotif(context, defaultChannel.id, abNotif) - val scope = useScope() - scope.launch { - combine( - GkdAbService.isRunning, - storeFlow, - ruleSummaryFlow, - actionCountFlow, - ) { abRunning, store, ruleSummary, count -> - if (!abRunning) return@combine "无障碍未授权" - if (!store.enableMatch) return@combine "暂停规则匹配" - if (store.useCustomNotifText) { - return@combine store.customNotifText - .replace("\${i}", ruleSummary.globalGroups.size.toString()) - .replace("\${k}", ruleSummary.appSize.toString()) - .replace("\${u}", ruleSummary.appGroupSize.toString()) - .replace("\${n}", count.toString()) - } - return@combine getSubsStatus(ruleSummary, count) - }.debounce(500L).stateIn(scope, SharingStarted.Eagerly, "").collect { text -> - createNotif( - context, defaultChannel.id, abNotif.copy( - text = text +class ManageService : Service() { + override fun onBind(intent: Intent?) = null + val scope = CoroutineScope(Dispatchers.Default) + + override fun onCreate() { + super.onCreate() + isRunning.value = true + createNotif(this, defaultChannel.id, abNotif) + scope.launch { + combine( + A11yService.isRunning, + storeFlow, + ruleSummaryFlow, + actionCountFlow, + ) { abRunning, store, ruleSummary, count -> + if (!abRunning) return@combine "无障碍未授权" + if (!store.enableMatch) return@combine "暂停规则匹配" + if (store.useCustomNotifText) { + return@combine store.customNotifText + .replace("\${i}", ruleSummary.globalGroups.size.toString()) + .replace("\${k}", ruleSummary.appSize.toString()) + .replace("\${u}", ruleSummary.appGroupSize.toString()) + .replace("\${n}", count.toString()) + } + return@combine getSubsStatus(ruleSummary, count) + }.debounce(500L).stateIn(scope, SharingStarted.Eagerly, "").collect { text -> + createNotif( + this@ManageService, + defaultChannel.id, + abNotif.copy(text = text) ) - ) + } } } - isRunning.value = true - onDestroy { + + override fun onDestroy() { + super.onDestroy() isRunning.value = false } -}) { - companion object { - fun start(context: Context = app) { - context.startForegroundService(Intent(context, ManageService::class.java)) - } + companion object { val isRunning = MutableStateFlow(false) - fun stop(context: Context = app) { - context.stopService(Intent(context, ManageService::class.java)) + fun start() { + app.startForegroundService(Intent(app, ManageService::class.java)) } - fun autoStart(context: Context) { + fun stop() { + app.stopService(Intent(app, ManageService::class.java)) + } + + fun autoStart() { // 在[系统重启]/[被其它高权限应用重启]时自动打开通知栏状态服务 - if (storeFlow.value.enableStatusService && - NotificationManagerCompat.from(context).areNotificationsEnabled() && - !isRunning.value + if (storeFlow.value.enableStatusService + && !isRunning.value + && notificationState.updateAndGet() ) { - start(context) + start() } } } diff --git a/app/src/main/kotlin/li/songe/gkd/shizuku/ShizukuApi.kt b/app/src/main/kotlin/li/songe/gkd/shizuku/ShizukuApi.kt index 62b63ce..e35a201 100644 --- a/app/src/main/kotlin/li/songe/gkd/shizuku/ShizukuApi.kt +++ b/app/src/main/kotlin/li/songe/gkd/shizuku/ShizukuApi.kt @@ -20,8 +20,8 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import li.songe.gkd.META import li.songe.gkd.app -import li.songe.gkd.composition.CanOnDestroy import li.songe.gkd.data.DeviceInfo +import li.songe.gkd.permission.shizukuOkState import li.songe.gkd.util.json import li.songe.gkd.util.map import li.songe.gkd.util.toast @@ -119,30 +119,15 @@ fun IActivityTaskManager.safeGetTasks(log: Boolean = true): List { - val shizukuAliveFlow = MutableStateFlow(Shizuku.pingBinder()) - val receivedListener = Shizuku.OnBinderReceivedListener { shizukuAliveFlow.value = true } - val deadListener = Shizuku.OnBinderDeadListener { shizukuAliveFlow.value = false } - Shizuku.addBinderReceivedListener(receivedListener) - Shizuku.addBinderDeadListener(deadListener) - onDestroy { - Shizuku.removeBinderReceivedListener(receivedListener) - Shizuku.removeBinderDeadListener(deadListener) - } - return shizukuAliveFlow -} - fun getShizukuCanUsedFlow( scope: CoroutineScope, - shizukuGrantFlow: StateFlow, - shizukuAliveFlow: StateFlow, - shizukuEnableFlow: StateFlow, + enableFlow: StateFlow, ): StateFlow { return combine( - shizukuAliveFlow, shizukuGrantFlow, shizukuEnableFlow - ) { shizukuAlive, shizukuGrant, enableShizuku -> - enableShizuku && shizukuAlive && shizukuGrant + shizukuOkState.stateFlow, + enableFlow + ) { shizukuOk, enableShizuku -> + shizukuOk && enableShizuku }.stateIn(scope, SharingStarted.Eagerly, false) } diff --git a/app/src/main/kotlin/li/songe/gkd/ui/AuthA11yPage.kt b/app/src/main/kotlin/li/songe/gkd/ui/AuthA11yPage.kt index c519123..b8101c3 100644 --- a/app/src/main/kotlin/li/songe/gkd/ui/AuthA11yPage.kt +++ b/app/src/main/kotlin/li/songe/gkd/ui/AuthA11yPage.kt @@ -42,7 +42,7 @@ import li.songe.gkd.META import li.songe.gkd.MainActivity import li.songe.gkd.permission.shizukuOkState import li.songe.gkd.permission.writeSecureSettingsState -import li.songe.gkd.service.GkdAbService +import li.songe.gkd.service.A11yService import li.songe.gkd.service.fixRestartService import li.songe.gkd.shizuku.newPackageManager import li.songe.gkd.ui.component.updateDialogOptions @@ -66,7 +66,7 @@ fun AuthA11yPage() { val vm = viewModel() val showCopyDlg by vm.showCopyDlgFlow.collectAsState() val writeSecureSettings by writeSecureSettingsState.stateFlow.collectAsState() - val a11yRunning by GkdAbService.isRunning.collectAsState() + val a11yRunning by A11yService.isRunning.collectAsState() val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() Scaffold(modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { diff --git a/app/src/main/kotlin/li/songe/gkd/ui/home/ControlPage.kt b/app/src/main/kotlin/li/songe/gkd/ui/home/ControlPage.kt index 710baaf..aba0197 100644 --- a/app/src/main/kotlin/li/songe/gkd/ui/home/ControlPage.kt +++ b/app/src/main/kotlin/li/songe/gkd/ui/home/ControlPage.kt @@ -37,7 +37,7 @@ import li.songe.gkd.a11yServiceEnabledFlow import li.songe.gkd.permission.notificationState import li.songe.gkd.permission.requiredPermission import li.songe.gkd.permission.writeSecureSettingsState -import li.songe.gkd.service.GkdAbService +import li.songe.gkd.service.A11yService import li.songe.gkd.service.ManageService import li.songe.gkd.service.switchA11yService import li.songe.gkd.ui.component.AuthCard @@ -93,7 +93,7 @@ fun useControlPage(): ScaffoldExt { val store by storeFlow.collectAsState() val ruleSummary by ruleSummaryFlow.collectAsState() - val a11yRunning by GkdAbService.isRunning.collectAsState() + val a11yRunning by A11yService.isRunning.collectAsState() val manageRunning by ManageService.isRunning.collectAsState() val a11yServiceEnabled by a11yServiceEnabledFlow.collectAsState() @@ -133,12 +133,12 @@ fun useControlPage(): ScaffoldExt { storeFlow.value = store.copy( enableStatusService = true ) - ManageService.start(context) + ManageService.start() } else { storeFlow.value = store.copy( enableStatusService = false ) - ManageService.stop(context) + ManageService.stop() } }) diff --git a/app/src/main/kotlin/li/songe/gkd/util/LifecycleCallbacks.kt b/app/src/main/kotlin/li/songe/gkd/util/LifecycleCallbacks.kt new file mode 100644 index 0000000..ac70d02 --- /dev/null +++ b/app/src/main/kotlin/li/songe/gkd/util/LifecycleCallbacks.kt @@ -0,0 +1,63 @@ +package li.songe.gkd.util + + +import android.view.accessibility.AccessibilityEvent +import java.util.WeakHashMap + +private val callbacksMap by lazy { WeakHashMap>>() } + +@Suppress("UNCHECKED_CAST") +private fun Any.getCallbacks(method: Int): MutableList { + return callbacksMap.getOrPut(this) { hashMapOf() } + .getOrPut(method) { mutableListOf() } as MutableList +} + +interface CanOnCallback + +interface OnCreate : CanOnCallback { + fun onBeforeCreate(f: () -> Unit) { + getCallbacks<() -> Unit>(1).add(f) + } + + fun onBeforeCreate() { + getCallbacks<() -> Unit>(1).forEach { it() } + } + + fun onCreated(f: () -> Unit) { + getCallbacks<() -> Unit>(2).add(f) + } + + fun onCreated() { + getCallbacks<() -> Unit>(2).forEach { it() } + } +} + +interface OnDestroy : CanOnCallback { + fun onDestroyed(f: () -> Unit) { + getCallbacks<() -> Unit>(4).add(f) + } + + fun onDestroyed() { + getCallbacks<() -> Unit>(4).forEach { it() } + } +} + +interface OnA11yEvent : CanOnCallback { + fun onA11yEvent(f: (AccessibilityEvent) -> Unit) { + getCallbacks<(AccessibilityEvent) -> Unit>(6).add(f) + } + + fun onA11yEvent(event: AccessibilityEvent) { + getCallbacks<(AccessibilityEvent) -> Unit>(6).forEach { it(event) } + } +} + +interface OnA11yConnected : CanOnCallback { + fun onA11yConnected(f: () -> Unit) { + getCallbacks<() -> Unit>(8).add(f) + } + + fun onA11yConnected() { + getCallbacks<() -> Unit>(8).forEach { it() } + } +}