refactor: simplify code

This commit is contained in:
lisonge 2024-09-24 13:00:53 +08:00
parent 97857f505b
commit c8a1e00077
35 changed files with 591 additions and 740 deletions

View File

@ -114,7 +114,7 @@
</provider> </provider>
<service <service
android:name=".service.GkdAbService" android:name=".service.A11yService"
android:exported="false" android:exported="false"
android:label="@string/app_name" android:label="@string/app_name"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"> android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">

View File

@ -21,8 +21,8 @@ import kotlinx.coroutines.flow.MutableStateFlow
import li.songe.gkd.data.selfAppInfo import li.songe.gkd.data.selfAppInfo
import li.songe.gkd.debug.clearHttpSubs import li.songe.gkd.debug.clearHttpSubs
import li.songe.gkd.notif.initChannel import li.songe.gkd.notif.initChannel
import li.songe.gkd.permission.updatePermissionState import li.songe.gkd.permission.shizukuOkState
import li.songe.gkd.service.GkdAbService import li.songe.gkd.service.A11yService
import li.songe.gkd.util.SafeR import li.songe.gkd.util.SafeR
import li.songe.gkd.util.initAppState import li.songe.gkd.util.initAppState
import li.songe.gkd.util.initFolder 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.launchTry
import li.songe.gkd.util.setReactiveToastStyle import li.songe.gkd.util.setReactiveToastStyle
import org.lsposed.hiddenapibypass.HiddenApiBypass import org.lsposed.hiddenapibypass.HiddenApiBypass
import rikka.shizuku.Shizuku
val appScope by lazy { MainScope() } 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) { appScope.launchTry(Dispatchers.IO) {
initStore() initStore()
initAppState() initAppState()
initSubsState() initSubsState()
initChannel() initChannel()
clearHttpSubs() clearHttpSubs()
updatePermissionState() syncFixState()
} }
} }
} }
@ -154,7 +163,7 @@ private fun getA11yServiceEnabled(): Boolean {
if (value.isNullOrEmpty()) return false if (value.isNullOrEmpty()) return false
val colonSplitter = TextUtils.SimpleStringSplitter(':') val colonSplitter = TextUtils.SimpleStringSplitter(':')
colonSplitter.setString(value) colonSplitter.setString(value)
val name = ComponentName(app, GkdAbService::class.java) val name = ComponentName(app, A11yService::class.java)
while (colonSplitter.hasNext()) { while (colonSplitter.hasNext()) {
if (ComponentName.unflattenFromString(colonSplitter.next()) == name) { if (ComponentName.unflattenFromString(colonSplitter.next()) == name) {
return true return true

View File

@ -3,6 +3,7 @@ package li.songe.gkd
import android.app.Activity import android.app.Activity
import android.app.ActivityManager import android.app.ActivityManager
import android.content.ComponentName import android.content.ComponentName
import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Color import android.graphics.Color
import android.os.Bundle import android.os.Bundle
@ -23,7 +24,6 @@ import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.blankj.utilcode.util.BarUtils import com.blankj.utilcode.util.BarUtils
import com.blankj.utilcode.util.ServiceUtils
import com.dylanc.activityresult.launcher.PickContentLauncher import com.dylanc.activityresult.launcher.PickContentLauncher
import com.dylanc.activityresult.launcher.StartActivityLauncher import com.dylanc.activityresult.launcher.StartActivityLauncher
import com.ramcosta.composedestinations.DestinationsNavHost import com.ramcosta.composedestinations.DestinationsNavHost
@ -32,12 +32,14 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch 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.FloatingService
import li.songe.gkd.debug.HttpService import li.songe.gkd.debug.HttpService
import li.songe.gkd.debug.ScreenshotService import li.songe.gkd.debug.ScreenshotService
import li.songe.gkd.permission.AuthDialog import li.songe.gkd.permission.AuthDialog
import li.songe.gkd.permission.updatePermissionState 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.ManageService
import li.songe.gkd.service.fixRestartService import li.songe.gkd.service.fixRestartService
import li.songe.gkd.service.updateLauncherAppId 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.openApp
import li.songe.gkd.util.openUri import li.songe.gkd.util.openUri
import li.songe.gkd.util.storeFlow import li.songe.gkd.util.storeFlow
import kotlin.reflect.KClass
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
val mainVm by viewModels<MainViewModel>() val mainVm by viewModels<MainViewModel>()
@ -63,7 +66,10 @@ class MainActivity : ComponentActivity() {
enableEdgeToEdge() enableEdgeToEdge()
fixTopPadding() fixTopPadding()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
mainVm
launcher
pickContentLauncher
ManageService.autoStart()
lifecycleScope.launch { lifecycleScope.launch {
storeFlow.map(lifecycleScope) { s -> s.excludeFromRecents }.collect { storeFlow.map(lifecycleScope) { s -> s.excludeFromRecents }.collect {
(app.getSystemService(ACTIVITY_SERVICE) as ActivityManager).let { manager -> (app.getSystemService(ACTIVITY_SERVICE) as ActivityManager).let { manager ->
@ -73,12 +79,6 @@ class MainActivity : ComponentActivity() {
} }
} }
} }
mainVm
launcher
pickContentLauncher
ManageService.autoStart(this)
setContent { setContent {
val navController = rememberNavController() val navController = rememberNavController()
AppTheme { AppTheme {
@ -102,26 +102,7 @@ class MainActivity : ComponentActivity() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
syncFixState()
// 每次切换页面更新记录桌面 appId
appScope.launchTry(Dispatchers.IO) {
updateLauncherAppId()
}
// 在某些机型由于未知原因创建失败, 在此保证每次界面切换都能重新检测创建
appScope.launchTry(Dispatchers.IO) {
initFolder()
}
// 用户在系统权限设置中切换权限后再切换回应用时能及时更新状态
appScope.launchTry(Dispatchers.IO) {
updatePermissionState()
}
// 由于某些机型的进程存在 安装缓存/崩溃缓存 导致服务状态可能不正确, 在此保证每次界面切换都能重新刷新状态
appScope.launchTry(Dispatchers.IO) {
updateServiceRunning()
}
} }
override fun onStart() { override fun onStart() {
@ -162,14 +143,45 @@ fun Activity.navToMainActivity() {
finish() finish()
} }
@Suppress("DEPRECATION")
private fun updateServiceRunning() { private fun updateServiceRunning() {
ManageService.isRunning.value = ServiceUtils.isServiceRunning(ManageService::class.java) val list = try {
GkdAbService.isRunning.value = ServiceUtils.isServiceRunning(GkdAbService::class.java) val manager = app.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
FloatingService.isRunning.value = ServiceUtils.isServiceRunning(FloatingService::class.java) manager.getRunningServices(Int.MAX_VALUE) ?: emptyList()
ScreenshotService.isRunning.value = ServiceUtils.isServiceRunning(ScreenshotService::class.java) } catch (_: Exception) {
HttpService.isRunning.value = ServiceUtils.isServiceRunning(HttpService::class.java) emptyList()
fixRestartService() }
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() { private fun Activity.fixTopPadding() {

View File

@ -1,5 +0,0 @@
package li.songe.gkd.composition
interface CanConfigBubble {
fun configBubble(f: ConfigBubbleHook)
}

View File

@ -1,7 +0,0 @@
package li.songe.gkd.composition
import android.view.accessibility.AccessibilityEvent
interface CanOnAccessibilityEvent {
fun onAccessibilityEvent(f: (AccessibilityEvent) -> Unit): Boolean
}

View File

@ -1,7 +0,0 @@
package li.songe.gkd.composition
import android.content.res.Configuration
interface CanOnConfigurationChanged {
fun onConfigurationChanged(f: (newConfig: Configuration) -> Unit):Boolean
}

View File

@ -1,5 +0,0 @@
package li.songe.gkd.composition
interface CanOnDestroy {
fun onDestroy(f: () -> Unit): Boolean
}

View File

@ -1,5 +0,0 @@
package li.songe.gkd.composition
interface CanOnInterrupt {
fun onInterrupt(f: () -> Unit):Boolean
}

View File

@ -1,7 +0,0 @@
package li.songe.gkd.composition
import android.view.KeyEvent
interface CanOnKeyEvent {
fun onKeyEvent(f: (KeyEvent?) -> Unit): Unit
}

View File

@ -1,5 +0,0 @@
package li.songe.gkd.composition
interface CanOnServiceConnected {
fun onServiceConnected(f: () -> Unit):Boolean
}

View File

@ -1,5 +0,0 @@
package li.songe.gkd.composition
interface CanOnStartCommand {
fun onStartCommand(f: StartCommandHook): Boolean
}

View File

@ -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<StartCommandHook>() }
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)
}
}

View File

@ -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)
}
}
}
}

View File

@ -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<ConfigBubbleHook>() }
override fun configBubble(f: ConfigBubbleHook) {
configBubbleHooks.add(f)
}
override fun configBubble(): BubbleBuilder? {
var result: BubbleBuilder? = null
configBubbleHooks.forEach { f ->
f {
result = it
}
}
return result
}
}

View File

@ -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<StartCommandHook>() }
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() }
}
}

View File

@ -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

View File

@ -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

View File

@ -3,7 +3,7 @@ package li.songe.gkd.data
import com.blankj.utilcode.util.ScreenUtils import com.blankj.utilcode.util.ScreenUtils
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import li.songe.gkd.app 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.getAndUpdateCurrentRules
import li.songe.gkd.service.safeActiveWindow import li.songe.gkd.service.safeActiveWindow
@ -34,7 +34,7 @@ data class ComplexSnapshot(
fun createComplexSnapshot(): ComplexSnapshot { fun createComplexSnapshot(): ComplexSnapshot {
val currentAbNode = GkdAbService.service?.safeActiveWindow val currentAbNode = A11yService.instance?.safeActiveWindow
val appId = currentAbNode?.packageName?.toString() val appId = currentAbNode?.packageName?.toString()
val currentActivityId = getAndUpdateCurrentRules().topActivity.activityId val currentActivityId = getAndUpdateCurrentRules().topActivity.activityId

View File

@ -5,7 +5,7 @@ import android.util.Log
import android.view.accessibility.AccessibilityNodeInfo import android.view.accessibility.AccessibilityNodeInfo
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import li.songe.gkd.META 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.createCacheTransform
import li.songe.gkd.service.createNoCacheTransform import li.songe.gkd.service.createNoCacheTransform
import li.songe.gkd.service.lastTriggerRule import li.songe.gkd.service.lastTriggerRule
@ -153,7 +153,7 @@ sealed class ResolvedRule(
val rootNode = (if (isRootNode) { val rootNode = (if (isRootNode) {
node node
} else { } else {
GkdAbService.service?.safeActiveWindow A11yService.instance?.safeActiveWindow
}) ?: return null }) ?: return null
rootNode.apply { rootNode.apply {
transform.cache.rootNode = this transform.cache.rootNode = this

View File

@ -12,23 +12,27 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.torrydo.floatingbubbleview.FloatingBubbleListener import com.torrydo.floatingbubbleview.FloatingBubbleListener
import com.torrydo.floatingbubbleview.service.expandable.BubbleBuilder import com.torrydo.floatingbubbleview.service.expandable.BubbleBuilder
import kotlinx.coroutines.Dispatchers import com.torrydo.floatingbubbleview.service.expandable.ExpandableBubbleService
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import li.songe.gkd.app import li.songe.gkd.app
import li.songe.gkd.appScope 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.data.Tuple3
import li.songe.gkd.notif.createNotif import li.songe.gkd.notif.createNotif
import li.songe.gkd.notif.floatingChannel import li.songe.gkd.notif.floatingChannel
import li.songe.gkd.notif.floatingNotif import li.songe.gkd.notif.floatingNotif
import li.songe.gkd.util.launchTry import li.songe.gkd.util.launchTry
import li.songe.gkd.util.toast
import kotlin.math.sqrt import kotlin.math.sqrt
class FloatingService : CompositionFbService({ class FloatingService : ExpandableBubbleService() {
useLifeCycleLog() override fun configExpandedBubble() = null
configBubble { resolve ->
override fun onCreate() {
super.onCreate()
isRunning.value = true
minimize()
}
override fun configBubble(): BubbleBuilder {
val builder = BubbleBuilder(this).bubbleCompose { val builder = BubbleBuilder(this).bubbleCompose {
Icon( Icon(
imageVector = Icons.Default.CenterFocusWeak, imageVector = Icons.Default.CenterFocusWeak,
@ -64,31 +68,25 @@ class FloatingService : CompositionFbService({
override fun onFingerUp(x: Float, y: Float) { override fun onFingerUp(x: Float, y: Float) {
if (System.currentTimeMillis() - fingerDownData.t0 < ViewConfiguration.getTapTimeout()) { if (System.currentTimeMillis() - fingerDownData.t0 < ViewConfiguration.getTapTimeout()) {
// is onClick // is onClick
appScope.launchTry(Dispatchers.IO) { appScope.launchTry {
SnapshotExt.captureSnapshot() SnapshotExt.captureSnapshot()
toast("快照成功")
} }
} }
} }
}) })
resolve(builder) return builder
} }
isRunning.value = true
onDestroy {
isRunning.value = false
}
}) {
override fun onCreate() {
super.onCreate()
minimize()
}
override fun startNotificationForeground() { override fun startNotificationForeground() {
createNotif(this, floatingChannel.id, floatingNotif) createNotif(this, floatingChannel.id, floatingNotif)
} }
override fun onDestroy() {
super.onDestroy()
isRunning.value = false
}
companion object { companion object {
val isRunning = MutableStateFlow(false) val isRunning = MutableStateFlow(false)
fun stop(context: Context = app) { fun stop(context: Context = app) {

View File

@ -43,7 +43,7 @@ import li.songe.gkd.debug.SnapshotExt.captureSnapshot
import li.songe.gkd.notif.createNotif import li.songe.gkd.notif.createNotif
import li.songe.gkd.notif.httpChannel import li.songe.gkd.notif.httpChannel
import li.songe.gkd.notif.httpNotif 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.LOCAL_HTTP_SUBS_ID
import li.songe.gkd.util.SERVER_SCRIPT_URL import li.songe.gkd.util.SERVER_SCRIPT_URL
import li.songe.gkd.util.getIpAddressInLocalNetwork import li.songe.gkd.util.getIpAddressInLocalNetwork
@ -58,7 +58,7 @@ import java.io.File
class HttpService : Service() { class HttpService : Service() {
private val scope = CoroutineScope(Dispatchers.IO) private val scope = CoroutineScope(Dispatchers.Default)
private var server: CIOApplicationEngine? = null private var server: CIOApplicationEngine? = null
override fun onCreate() { override fun onCreate() {
@ -236,11 +236,11 @@ private fun createServer(port: Int): CIOApplicationEngine {
call.respond(RpcOk()) call.respond(RpcOk())
} }
post("/execSelector") { post("/execSelector") {
if (!GkdAbService.isRunning.value) { if (!A11yService.isRunning.value) {
throw RpcError("无障碍没有运行") throw RpcError("无障碍没有运行")
} }
val gkdAction = call.receive<GkdAction>() val gkdAction = call.receive<GkdAction>()
call.respond(GkdAbService.execAction(gkdAction)) call.respond(A11yService.execAction(gkdAction))
} }
} }
} }

View File

@ -1,39 +1,46 @@
package li.songe.gkd.debug package li.songe.gkd.debug
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Service
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import com.blankj.utilcode.util.LogUtils import com.blankj.utilcode.util.LogUtils
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import li.songe.gkd.app 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.createNotif
import li.songe.gkd.notif.screenshotChannel import li.songe.gkd.notif.screenshotChannel
import li.songe.gkd.notif.screenshotNotif import li.songe.gkd.notif.screenshotNotif
import li.songe.gkd.util.ScreenshotUtil import li.songe.gkd.util.ScreenshotUtil
class ScreenshotService : CompositionService({ class ScreenshotService : Service() {
useLifeCycleLog() override fun onBind(intent: Intent?) = null
createNotif(this, screenshotChannel.id, screenshotNotif)
onStartCommand { intent, _, _ -> override fun onCreate() {
if (intent == null) return@onStartCommand super.onCreate()
screenshotUtil?.destroy() isRunning.value = true
screenshotUtil = ScreenshotUtil(this, intent) createNotif(this, screenshotChannel.id, screenshotNotif)
LogUtils.d("screenshot restart")
} }
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?.destroy()
screenshotUtil = null screenshotUtil = null
} }
isRunning.value = true
onDestroy {
isRunning.value = false
}
}) {
companion object { companion object {
suspend fun screenshot() = screenshotUtil?.execute() suspend fun screenshot() = screenshotUtil?.execute()

View File

@ -18,7 +18,7 @@ import li.songe.gkd.data.RpcError
import li.songe.gkd.data.createComplexSnapshot import li.songe.gkd.data.createComplexSnapshot
import li.songe.gkd.data.toSnapshot import li.songe.gkd.data.toSnapshot
import li.songe.gkd.db.DbSet 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.appInfoCacheFlow
import li.songe.gkd.util.keepNullJson import li.songe.gkd.util.keepNullJson
import li.songe.gkd.util.snapshotFolder import li.songe.gkd.util.snapshotFolder
@ -82,7 +82,7 @@ object SnapshotExt {
private val captureLoading = MutableStateFlow(false) private val captureLoading = MutableStateFlow(false)
suspend fun captureSnapshot(skipScreenshot: Boolean = false): ComplexSnapshot { suspend fun captureSnapshot(skipScreenshot: Boolean = false): ComplexSnapshot {
if (!GkdAbService.isRunning.value) { if (!A11yService.isRunning.value) {
throw RpcError("无障碍不可用") throw RpcError("无障碍不可用")
} }
if (captureLoading.value) { if (captureLoading.value) {
@ -105,7 +105,7 @@ object SnapshotExt {
Bitmap.Config.ARGB_8888 Bitmap.Config.ARGB_8888
) )
} else { } else {
GkdAbService.currentScreenshot() ?: withTimeoutOrNull(3_000) { A11yService.currentScreenshot() ?: withTimeoutOrNull(3_000) {
if (!ScreenshotService.isRunning.value) { if (!ScreenshotService.isRunning.value) {
return@withTimeoutOrNull null return@withTimeoutOrNull null
} }
@ -141,7 +141,7 @@ object SnapshotExt {
File(getSnapshotPath(snapshot.id)).writeText(text) File(getSnapshotPath(snapshot.id)).writeText(text)
DbSet.snapshotDao.insert(snapshot.toSnapshot()) DbSet.snapshotDao.insert(snapshot.toSnapshot())
} }
toast("快照捕获成功") toast("快照成功")
return snapshot return snapshot
} finally { } finally {
captureLoading.value = false captureLoading.value = false

View File

@ -7,9 +7,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import li.songe.gkd.appScope import li.songe.gkd.appScope
import li.songe.gkd.debug.SnapshotExt.captureSnapshot import li.songe.gkd.debug.SnapshotExt.captureSnapshot
import li.songe.gkd.service.GkdAbService import li.songe.gkd.service.A11yService
import li.songe.gkd.service.GkdAbService.Companion.eventExecutor
import li.songe.gkd.service.GkdAbService.Companion.shizukuTopActivityGetter
import li.songe.gkd.service.TopActivity import li.songe.gkd.service.TopActivity
import li.songe.gkd.service.getAndUpdateCurrentRules import li.songe.gkd.service.getAndUpdateCurrentRules
import li.songe.gkd.service.safeActiveWindow import li.songe.gkd.service.safeActiveWindow
@ -21,7 +19,7 @@ class SnapshotTileService : TileService() {
override fun onClick() { override fun onClick() {
super.onClick() super.onClick()
LogUtils.d("SnapshotTileService::onClick") LogUtils.d("SnapshotTileService::onClick")
val service = GkdAbService.service val service = A11yService.instance
if (service == null) { if (service == null) {
toast("无障碍没有开启") toast("无障碍没有开启")
return return
@ -47,9 +45,11 @@ class SnapshotTileService : TileService() {
} }
} else if (latestAppId != oldAppId) { } else if (latestAppId != oldAppId) {
LogUtils.d("SnapshotTileService::eventExecutor.execute") LogUtils.d("SnapshotTileService::eventExecutor.execute")
eventExecutor.execute { A11yService.eventExecutor.execute {
updateTopActivity( updateTopActivity(
shizukuTopActivityGetter?.invoke() ?: TopActivity(appId = latestAppId) A11yService.instance?.getShizukuTopActivity?.invoke() ?: TopActivity(
appId = latestAppId
)
) )
getAndUpdateCurrentRules() getAndUpdateCurrentRules()
appScope.launchTry(Dispatchers.IO) { appScope.launchTry(Dispatchers.IO) {

View File

@ -11,11 +11,8 @@ import com.hjq.permissions.Permission
import com.hjq.permissions.XXPermissions import com.hjq.permissions.XXPermissions
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.updateAndGet import kotlinx.coroutines.flow.updateAndGet
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import li.songe.gkd.app import li.songe.gkd.app
import li.songe.gkd.appScope import li.songe.gkd.appScope
import li.songe.gkd.service.fixRestartService
import li.songe.gkd.shizuku.newActivityTaskManager import li.songe.gkd.shizuku.newActivityTaskManager
import li.songe.gkd.shizuku.safeGetTasks import li.songe.gkd.shizuku.safeGetTasks
import li.songe.gkd.shizuku.shizukuIsSafeOK import li.songe.gkd.shizuku.shizukuIsSafeOK
@ -175,23 +172,17 @@ val shizukuOkState by lazy {
) )
} }
private val checkAuthMutex by lazy { Mutex() }
suspend fun updatePermissionState() { suspend fun updatePermissionState() {
if (checkAuthMutex.isLocked) return arrayOf(
checkAuthMutex.withLock { notificationState,
arrayOf( canDrawOverlaysState,
notificationState, canWriteExternalStorage,
canDrawOverlaysState, writeSecureSettingsState,
canWriteExternalStorage, shizukuOkState,
).forEach { it.updateAndGet() } ).forEach { it.updateAndGet() }
if (canQueryPkgState.stateFlow.value != canQueryPkgState.updateAndGet()) { if (canQueryPkgState.stateFlow.value != canQueryPkgState.updateAndGet()) {
appScope.launchTry { appScope.launchTry {
initOrResetAppInfoCache() initOrResetAppInfoCache()
}
} }
if (writeSecureSettingsState.stateFlow.value != writeSecureSettingsState.updateAndGet()) {
fixRestartService()
}
shizukuOkState.updateAndGet()
} }
} }

View File

@ -2,15 +2,15 @@ package li.songe.gkd.service
import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityEvent
data class AbEvent( data class A11yEvent(
val type: Int, val type: Int,
val time: Long, val time: Long,
val appId: String, val appId: String,
val className: String, val className: String,
) )
fun AccessibilityEvent.toAbEvent(): AbEvent? { fun AccessibilityEvent.toA11yEvent(): A11yEvent? {
return AbEvent( return A11yEvent(
type = eventType, type = eventType,
time = System.currentTimeMillis(), time = System.currentTimeMillis(),
appId = packageName?.toString() ?: return null, appId = packageName?.toString() ?: return null,

View File

@ -83,7 +83,7 @@ fun AccessibilityNodeInfo.querySelector(
val root = if (isRootNode) { val root = if (isRootNode) {
return this return this
} else { } else {
GkdAbService.service?.safeActiveWindow ?: return null A11yService.instance?.safeActiveWindow ?: return null
} }
return selector.match(root, transform, option) return selector.match(root, transform, option)
} }
@ -266,7 +266,7 @@ class NodeCache {
fun getRoot(node: AccessibilityNodeInfo): AccessibilityNodeInfo? { fun getRoot(node: AccessibilityNodeInfo): AccessibilityNodeInfo? {
if (rootNode == null) { if (rootNode == null) {
rootNode = GkdAbService.service?.safeActiveWindow rootNode = A11yService.instance?.safeActiveWindow
} }
if (node == rootNode) return null if (node == rootNode) return null
return rootNode return rootNode

View File

@ -1,5 +1,7 @@
package li.songe.gkd.service package li.songe.gkd.service
import android.accessibilityservice.AccessibilityService
import android.accessibilityservice.AccessibilityService.WINDOW_SERVICE
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
@ -17,9 +19,11 @@ import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo import android.view.accessibility.AccessibilityNodeInfo
import com.blankj.utilcode.util.LogUtils import com.blankj.utilcode.util.LogUtils
import com.blankj.utilcode.util.ScreenUtils import com.blankj.utilcode.util.ScreenUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
@ -27,10 +31,8 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import li.songe.gkd.META import li.songe.gkd.META
import li.songe.gkd.app
import li.songe.gkd.appScope 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.ActionPerformer
import li.songe.gkd.data.ActionResult import li.songe.gkd.data.ActionResult
import li.songe.gkd.data.AppRule 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.RuleStatus
import li.songe.gkd.data.clearNodeCache import li.songe.gkd.data.clearNodeCache
import li.songe.gkd.debug.SnapshotExt import li.songe.gkd.debug.SnapshotExt
import li.songe.gkd.permission.shizukuOkState
import li.songe.gkd.shizuku.getShizukuCanUsedFlow import li.songe.gkd.shizuku.getShizukuCanUsedFlow
import li.songe.gkd.shizuku.shizukuIsSafeOK
import li.songe.gkd.shizuku.useSafeGetTasksFc import li.songe.gkd.shizuku.useSafeGetTasksFc
import li.songe.gkd.shizuku.useSafeInputTapFc 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.UpdateTimeOption
import li.songe.gkd.util.checkSubsUpdate import li.songe.gkd.util.checkSubsUpdate
import li.songe.gkd.util.launchTry 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.gkd.util.toast
import li.songe.selector.MatchOption import li.songe.selector.MatchOption
import li.songe.selector.Selector import li.songe.selector.Selector
import java.lang.ref.WeakReference
import java.util.concurrent.Executors import java.util.concurrent.Executors
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
class GkdAbService : CompositionAbService({ class A11yService : AccessibilityService(), OnCreate, OnA11yConnected, OnA11yEvent, OnDestroy {
useLifeCycleLog() override fun onCreate() {
updateLauncherAppId() super.onCreate()
onCreated()
val context = this as GkdAbService
val scope = useScope()
service = context
onDestroy {
service = null
} }
val shizukuAliveFlow = useShizukuAliveState() override fun onServiceConnected() {
val shizukuGrantFlow = MutableStateFlow(false) super.onServiceConnected()
var lastCheckShizukuTime = 0L onA11yConnected()
onAccessibilityEvent { // 借助无障碍轮询校验 shizuku 权限, 因为 shizuku 可能无故被关闭 }
if ((storeFlow.value.enableShizukuActivity || storeFlow.value.enableShizukuClick) && it.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {// 筛选降低判断频率
val t = System.currentTimeMillis() override fun onAccessibilityEvent(event: AccessibilityEvent?) {
if (t - lastCheckShizukuTime > 60_000L) { if (event == null || !event.isUseful()) return
lastCheckShizukuTime = t onA11yEvent(event)
scope.launchTry(Dispatchers.IO) { }
shizukuGrantFlow.value = if (shizukuAliveFlow.value) {
shizukuIsSafeOK() override fun onInterrupt() {}
} else { override fun onDestroy() {
false 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<A11yService>(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( private fun A11yService.useMatchRule() {
scope, val context = this
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<Pair<String, String>, Boolean>(128) {
override fun create(key: Pair<String, String>): 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)
}
var lastTriggerShizukuTime = 0L var lastTriggerShizukuTime = 0L
var lastContentEventTime = 0L var lastContentEventTime = 0L
val events = mutableListOf<AccessibilityNodeInfo>() val events = mutableListOf<AccessibilityNodeInfo>()
var queryTaskJob: Job? = null var queryTaskJob: Job? = null
fun newQueryTask( fun newQueryTask(
byEvent: Boolean = false, byEvent: Boolean = false, byForced: Boolean = false, delayRule: ResolvedRule? = null
byForced: Boolean = false,
delayRule: ResolvedRule? = null
) { ) {
if (!storeFlow.value.enableMatch) return if (!storeFlow.value.enableMatch) return
queryTaskJob = scope.launchTry(queryThread) { queryTaskJob = scope.launchTry(A11yService.queryThread) {
var latestEvent = if (delayRule != null) {// 延迟规则不消耗事件 var latestEvent = if (delayRule != null) {// 延迟规则不消耗事件
null null
} else { } else {
@ -180,7 +211,7 @@ class GkdAbService : CompositionAbService({
if (delayRule != null && delayRule !== rule) continue if (delayRule != null && delayRule !== rule) continue
val statusCode = rule.status val statusCode = rule.status
if (statusCode == RuleStatus.Status3 && rule.matchDelayJob == null) { if (statusCode == RuleStatus.Status3 && rule.matchDelayJob == null) {
rule.matchDelayJob = scope.launch(actionThread) { rule.matchDelayJob = scope.launch(A11yService.actionThread) {
delay(rule.matchDelay) delay(rule.matchDelay)
rule.matchDelayJob = null rule.matchDelayJob = null
newQueryTask(delayRule = rule) newQueryTask(delayRule = rule)
@ -207,7 +238,7 @@ class GkdAbService : CompositionAbService({
rightAppId rightAppId
) )
if (topActivityFlow.value.appId != rightAppId || (!matchApp && rule is AppRule)) { if (topActivityFlow.value.appId != rightAppId || (!matchApp && rule is AppRule)) {
eventExecutor.execute { A11yService.eventExecutor.execute {
if (topActivityFlow.value.appId != rightAppId) { if (topActivityFlow.value.appId != rightAppId) {
val shizukuTop = getShizukuTopActivity() val shizukuTop = getShizukuTopActivity()
if (shizukuTop?.appId == rightAppId) { if (shizukuTop?.appId == rightAppId) {
@ -216,7 +247,7 @@ class GkdAbService : CompositionAbService({
updateTopActivity(TopActivity(appId = rightAppId)) updateTopActivity(TopActivity(appId = rightAppId))
} }
getAndUpdateCurrentRules() getAndUpdateCurrentRules()
scope.launch(actionThread) { scope.launch(A11yService.actionThread) {
delay(300) delay(300)
if (queryTaskJob?.isActive != true) { if (queryTaskJob?.isActive != true) {
newQueryTask() newQueryTask()
@ -230,7 +261,7 @@ class GkdAbService : CompositionAbService({
val target = rule.query(nodeVal, latestEvent == null) ?: continue val target = rule.query(nodeVal, latestEvent == null) ?: continue
if (activityRule !== getAndUpdateCurrentRules()) break if (activityRule !== getAndUpdateCurrentRules()) break
if (rule.checkDelay() && rule.actionDelayJob == null) { if (rule.checkDelay() && rule.actionDelayJob == null) {
rule.actionDelayJob = scope.launch(actionThread) { rule.actionDelayJob = scope.launch(A11yService.actionThread) {
delay(rule.actionDelay) delay(rule.actionDelay)
rule.actionDelayJob = null rule.actionDelayJob = null
newQueryTask(delayRule = rule) newQueryTask(delayRule = rule)
@ -238,10 +269,10 @@ class GkdAbService : CompositionAbService({
continue continue
} }
if (rule.status != RuleStatus.StatusOk) break if (rule.status != RuleStatus.StatusOk) break
val actionResult = rule.performAction(context, target, safeInjectClickEventFc) val actionResult = rule.performAction(context, target, getShizukuClick)
if (actionResult.result) { if (actionResult.result) {
rule.trigger() rule.trigger()
scope.launch(actionThread) { scope.launch(A11yService.actionThread) {
delay(300) delay(300)
if (queryTaskJob?.isActive != true) { if (queryTaskJob?.isActive != true) {
newQueryTask() newQueryTask()
@ -251,16 +282,14 @@ class GkdAbService : CompositionAbService({
appScope.launchTry(Dispatchers.IO) { appScope.launchTry(Dispatchers.IO) {
insertClickLog(rule) insertClickLog(rule)
LogUtils.d( LogUtils.d(
rule.statusText(), rule.statusText(), AttrInfo.info2data(target, 0, 0), actionResult
AttrInfo.info2data(target, 0, 0),
actionResult
) )
} }
} }
} }
val t = System.currentTimeMillis() val t = System.currentTimeMillis()
if (t - lastTriggerTime < 3000L || t - appChangeTime < 5000L) { if (t - lastTriggerTime < 3000L || t - appChangeTime < 5000L) {
scope.launch(actionThread) { scope.launch(A11yService.actionThread) {
delay(300) delay(300)
if (queryTaskJob?.isActive != true) { if (queryTaskJob?.isActive != true) {
newQueryTask() newQueryTask()
@ -268,7 +297,7 @@ class GkdAbService : CompositionAbService({
} }
} else { } else {
if (activityRule.currentRules.any { r -> r.checkForced() && r.status.let { s -> s == RuleStatus.StatusOk || s == RuleStatus.Status5 } }) { 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) delay(300)
if (queryTaskJob?.isActive != true) { if (queryTaskJob?.isActive != true) {
newQueryTask(byForced = true) newQueryTask(byForced = true)
@ -280,17 +309,18 @@ class GkdAbService : CompositionAbService({
} }
val skipAppIds = setOf("com.android.systemui") val skipAppIds = setOf("com.android.systemui")
onAccessibilityEvent { event -> onA11yEvent { event ->
if (event.eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED && if (event.eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED && skipAppIds.contains(
skipAppIds.contains(event.packageName.toString()) 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.type == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) {
if (fixedEvent.time - lastContentEventTime < 100 && fixedEvent.time - appChangeTime > 5000 && fixedEvent.time - lastTriggerTime > 3000) { if (fixedEvent.time - lastContentEventTime < 100 && fixedEvent.time - appChangeTime > 5000 && fixedEvent.time - lastTriggerTime > 3000) {
return@onAccessibilityEvent return@onA11yEvent
} }
lastContentEventTime = fixedEvent.time lastContentEventTime = fixedEvent.time
} }
@ -306,7 +336,7 @@ class GkdAbService : CompositionAbService({
val evAppId = fixedEvent.appId val evAppId = fixedEvent.appId
val evActivityId = fixedEvent.className val evActivityId = fixedEvent.className
eventExecutor.execute launch@{ A11yService.eventExecutor.execute launch@{
val oldAppId = topActivityFlow.value.appId val oldAppId = topActivityFlow.value.appId
val rightAppId = if (oldAppId == evAppId) { val rightAppId = if (oldAppId == evAppId) {
oldAppId oldAppId
@ -372,20 +402,9 @@ class GkdAbService : CompositionAbService({
newQueryTask(eventNode != null) newQueryTask(eventNode != null)
} }
} }
}
var lastUpdateSubsTime = System.currentTimeMillis() - 25000 private fun A11yService.useRuleChangedLog() {
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()
}
}
}
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
activityRuleFlow.debounce(300).collect { activityRuleFlow.debounce(300).collect {
if (storeFlow.value.enableMatch && it.currentRules.isNotEmpty()) { if (storeFlow.value.enableMatch && it.currentRules.isNotEmpty()) {
@ -395,190 +414,218 @@ class GkdAbService : CompositionAbService({
} }
} }
} }
}
var aliveView: View? = null private fun A11yService.useRunningState() {
val wm by lazy { context.getSystemService(WINDOW_SERVICE) as WindowManager } onCreated {
onServiceConnected { A11yService.weakInstance = WeakReference(this)
scope.launchTry { A11yService.isRunning.value = true
storeFlow.map(scope) { s -> s.enableAbFloatWindow }.collect { ManageService.autoStart()
if (aliveView != null) { }
withContext(Dispatchers.Main) { onDestroyed {
wm.removeView(aliveView) A11yService.weakInstance = WeakReference(null)
} A11yService.isRunning.value = false
} }
if (it) { }
val tempView = View(context)
val lp = WindowManager.LayoutParams().apply { private fun A11yService.useGetShizukuActivity(): () -> TopActivity? {
type = WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY val enableShizukuActivityFlow = storeFlow.map(scope) { s -> s.enableShizukuActivity }
format = PixelFormat.TRANSLUCENT val shizukuActivityUsedFlow = getShizukuCanUsedFlow(scope, enableShizukuActivityFlow)
flags = val safeGetTasksFc by lazy { useSafeGetTasksFc(scope, shizukuActivityUsedFlow) }
flags or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE // 当锁屏/下拉通知栏时, safeActiveWindow 没有 activityId, 但是此时 shizuku 获取到是前台 app 的 appId 和 activityId
width = 1 return fun(): TopActivity? {
height = 1 if (!storeFlow.value.enableShizukuActivity) return null
packageName = context.packageName // 平均耗时 5 ms
} val top = safeGetTasksFc()?.lastOrNull()?.topActivity ?: return null
withContext(Dispatchers.Main) { return TopActivity(appId = top.packageName, activityId = top.className)
try { }
// 在某些机型创建失败, 原因未知 }
wm.addView(tempView, lp)
aliveView = tempView private fun A11yService.useGetShizukuClick(): (Float, Float) -> Boolean? {
} catch (e: Exception) { val shizukuClickUsedFlow =
LogUtils.d("创建无障碍悬浮窗失败", e) getShizukuCanUsedFlow(scope, storeFlow.map(scope) { s -> s.enableShizukuClick })
toast("创建无障碍悬浮窗失败") val safeInjectClickEventFc = useSafeInputTapFc(scope, shizukuClickUsedFlow)
storeFlow.update { store -> return safeInjectClickEventFc
store.copy(enableAbFloatWindow = false) }
}
} private fun A11yService.useAutoCheckShizuku() {
} var lastCheckShizukuTime = 0L
} else { onA11yEvent {
aliveView = null // 借助无障碍轮询校验 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) { if (aliveView != null) {
wm.removeView(aliveView) wm.removeView(aliveView)
} }
} }
}
val volumeChangedAction = "android.media.VOLUME_CHANGED_ACTION" private fun A11yService.useAutoUpdateSubs() {
fun createVolumeReceiver() = object : BroadcastReceiver() { var lastUpdateSubsTime = System.currentTimeMillis() - 25000
var lastTriggerTime = -1L onA11yEvent {// 借助 无障碍事件 触发自动检测更新
override fun onReceive(context: Context?, intent: Intent?) { if (it.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {// 筛选降低判断频率
if (intent?.action == volumeChangedAction) { val i = storeFlow.value.updateSubsInterval
val t = System.currentTimeMillis() if (i <= 0) return@onA11yEvent
if (t - lastTriggerTime > 3000 && !ScreenUtils.isScreenLock()) { val t = System.currentTimeMillis()
lastTriggerTime = t if (t - lastUpdateSubsTime > i.coerceAtLeast(UpdateTimeOption.Everyday.value)) {
scope.launchTry(Dispatchers.IO) { lastUpdateSubsTime = t
SnapshotExt.captureSnapshot() checkSubsUpdate()
toast("快照成功") }
} }
}
}
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 var captureVolumeReceiver: BroadcastReceiver? = null
scope.launch { onCreated {
storeFlow.map(scope) { s -> s.captureVolumeChange }.collect { scope.launch {
if (captureVolumeReceiver != null) { storeFlow.map(scope) { s -> s.captureVolumeChange }.collect {
context.unregisterReceiver(captureVolumeReceiver) if (captureVolumeReceiver != null) {
} unregisterReceiver(captureVolumeReceiver)
captureVolumeReceiver = if (it) { }
createVolumeReceiver().apply { captureVolumeReceiver = if (it) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { createVolumeReceiver().apply {
context.registerReceiver( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
this, IntentFilter(volumeChangedAction), Context.RECEIVER_EXPORTED registerReceiver(
) this, IntentFilter(volumeChangedAction), Context.RECEIVER_EXPORTED
} else { )
context.registerReceiver(this, IntentFilter(volumeChangedAction)) } 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 { } else {
it.resume(null) null
} }
} }
} }
} }
} 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<Pair<String, String>, Boolean>(128) {
override fun create(key: Pair<String, String>): 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)
}
}
}

View File

@ -16,7 +16,7 @@ import li.songe.gkd.util.toast
class GkdTileService : TileService() { class GkdTileService : TileService() {
private fun updateTile(): Boolean { private fun updateTile(): Boolean {
val oldState = qsTile.state val oldState = qsTile.state
val newState = if (GkdAbService.isRunning.value) { val newState = if (A11yService.isRunning.value) {
Tile.STATE_ACTIVE Tile.STATE_ACTIVE
} else { } else {
Tile.STATE_INACTIVE Tile.STATE_INACTIVE
@ -93,7 +93,7 @@ fun switchA11yService(): Boolean {
return false return false
} }
val names = getServiceNames() val names = getServiceNames()
if (GkdAbService.isRunning.value) { if (A11yService.isRunning.value) {
names.remove(a11yClsName) names.remove(a11yClsName)
updateServiceNames(names) updateServiceNames(names)
storeFlow.update { it.copy(enableService = false) } storeFlow.update { it.copy(enableService = false) }
@ -116,7 +116,7 @@ fun fixRestartService(): Boolean {
// 1. 服务没有运行 // 1. 服务没有运行
// 2. 用户配置开启了服务 // 2. 用户配置开启了服务
// 3. 有写入系统设置权限 // 3. 有写入系统设置权限
if (!GkdAbService.isRunning.value && storeFlow.value.enableService && writeSecureSettingsState.updateAndGet()) { if (!A11yService.isRunning.value && storeFlow.value.enableService && writeSecureSettingsState.updateAndGet()) {
val t = System.currentTimeMillis() val t = System.currentTimeMillis()
if (t - lastRestartA11yServiceTimeFlow.value < 10_000) return false if (t - lastRestartA11yServiceTimeFlow.value < 10_000) return false
lastRestartA11yServiceTimeFlow.value = t lastRestartA11yServiceTimeFlow.value = t
@ -136,5 +136,5 @@ fun fixRestartService(): Boolean {
} }
private val a11yClsName by lazy { private val a11yClsName by lazy {
ComponentName(app, GkdAbService::class.java).flattenToShortString() ComponentName(app, A11yService::class.java).flattenToShortString()
} }

View File

@ -1,8 +1,9 @@
package li.songe.gkd.service package li.songe.gkd.service
import android.content.Context import android.app.Service
import android.content.Intent 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.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
@ -10,70 +11,73 @@ import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import li.songe.gkd.app 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.abNotif
import li.songe.gkd.notif.createNotif import li.songe.gkd.notif.createNotif
import li.songe.gkd.notif.defaultChannel import li.songe.gkd.notif.defaultChannel
import li.songe.gkd.permission.notificationState
import li.songe.gkd.util.actionCountFlow import li.songe.gkd.util.actionCountFlow
import li.songe.gkd.util.getSubsStatus import li.songe.gkd.util.getSubsStatus
import li.songe.gkd.util.ruleSummaryFlow import li.songe.gkd.util.ruleSummaryFlow
import li.songe.gkd.util.storeFlow import li.songe.gkd.util.storeFlow
class ManageService : CompositionService({ class ManageService : Service() {
useLifeCycleLog() override fun onBind(intent: Intent?) = null
val context = this val scope = CoroutineScope(Dispatchers.Default)
createNotif(context, defaultChannel.id, abNotif)
val scope = useScope() override fun onCreate() {
scope.launch { super.onCreate()
combine( isRunning.value = true
GkdAbService.isRunning, createNotif(this, defaultChannel.id, abNotif)
storeFlow, scope.launch {
ruleSummaryFlow, combine(
actionCountFlow, A11yService.isRunning,
) { abRunning, store, ruleSummary, count -> storeFlow,
if (!abRunning) return@combine "无障碍未授权" ruleSummaryFlow,
if (!store.enableMatch) return@combine "暂停规则匹配" actionCountFlow,
if (store.useCustomNotifText) { ) { abRunning, store, ruleSummary, count ->
return@combine store.customNotifText if (!abRunning) return@combine "无障碍未授权"
.replace("\${i}", ruleSummary.globalGroups.size.toString()) if (!store.enableMatch) return@combine "暂停规则匹配"
.replace("\${k}", ruleSummary.appSize.toString()) if (store.useCustomNotifText) {
.replace("\${u}", ruleSummary.appGroupSize.toString()) return@combine store.customNotifText
.replace("\${n}", count.toString()) .replace("\${i}", ruleSummary.globalGroups.size.toString())
} .replace("\${k}", ruleSummary.appSize.toString())
return@combine getSubsStatus(ruleSummary, count) .replace("\${u}", ruleSummary.appGroupSize.toString())
}.debounce(500L).stateIn(scope, SharingStarted.Eagerly, "").collect { text -> .replace("\${n}", count.toString())
createNotif( }
context, defaultChannel.id, abNotif.copy( return@combine getSubsStatus(ruleSummary, count)
text = text }.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 isRunning.value = false
} }
}) {
companion object {
fun start(context: Context = app) {
context.startForegroundService(Intent(context, ManageService::class.java))
}
companion object {
val isRunning = MutableStateFlow(false) val isRunning = MutableStateFlow(false)
fun stop(context: Context = app) { fun start() {
context.stopService(Intent(context, ManageService::class.java)) 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 && if (storeFlow.value.enableStatusService
NotificationManagerCompat.from(context).areNotificationsEnabled() && && !isRunning.value
!isRunning.value && notificationState.updateAndGet()
) { ) {
start(context) start()
} }
} }
} }

View File

@ -20,8 +20,8 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import li.songe.gkd.META import li.songe.gkd.META
import li.songe.gkd.app import li.songe.gkd.app
import li.songe.gkd.composition.CanOnDestroy
import li.songe.gkd.data.DeviceInfo import li.songe.gkd.data.DeviceInfo
import li.songe.gkd.permission.shizukuOkState
import li.songe.gkd.util.json import li.songe.gkd.util.json
import li.songe.gkd.util.map import li.songe.gkd.util.map
import li.songe.gkd.util.toast import li.songe.gkd.util.toast
@ -119,30 +119,15 @@ fun IActivityTaskManager.safeGetTasks(log: Boolean = true): List<ActivityManager
// return service.let(::ShizukuBinderWrapper).let(IInputManager.Stub::asInterface) // return service.let(::ShizukuBinderWrapper).let(IInputManager.Stub::asInterface)
//} //}
fun CanOnDestroy.useShizukuAliveState(): StateFlow<Boolean> {
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( fun getShizukuCanUsedFlow(
scope: CoroutineScope, scope: CoroutineScope,
shizukuGrantFlow: StateFlow<Boolean>, enableFlow: StateFlow<Boolean>,
shizukuAliveFlow: StateFlow<Boolean>,
shizukuEnableFlow: StateFlow<Boolean>,
): StateFlow<Boolean> { ): StateFlow<Boolean> {
return combine( return combine(
shizukuAliveFlow, shizukuGrantFlow, shizukuEnableFlow shizukuOkState.stateFlow,
) { shizukuAlive, shizukuGrant, enableShizuku -> enableFlow
enableShizuku && shizukuAlive && shizukuGrant ) { shizukuOk, enableShizuku ->
shizukuOk && enableShizuku
}.stateIn(scope, SharingStarted.Eagerly, false) }.stateIn(scope, SharingStarted.Eagerly, false)
} }

View File

@ -42,7 +42,7 @@ import li.songe.gkd.META
import li.songe.gkd.MainActivity import li.songe.gkd.MainActivity
import li.songe.gkd.permission.shizukuOkState import li.songe.gkd.permission.shizukuOkState
import li.songe.gkd.permission.writeSecureSettingsState 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.service.fixRestartService
import li.songe.gkd.shizuku.newPackageManager import li.songe.gkd.shizuku.newPackageManager
import li.songe.gkd.ui.component.updateDialogOptions import li.songe.gkd.ui.component.updateDialogOptions
@ -66,7 +66,7 @@ fun AuthA11yPage() {
val vm = viewModel<AuthA11yVm>() val vm = viewModel<AuthA11yVm>()
val showCopyDlg by vm.showCopyDlgFlow.collectAsState() val showCopyDlg by vm.showCopyDlgFlow.collectAsState()
val writeSecureSettings by writeSecureSettingsState.stateFlow.collectAsState() val writeSecureSettings by writeSecureSettingsState.stateFlow.collectAsState()
val a11yRunning by GkdAbService.isRunning.collectAsState() val a11yRunning by A11yService.isRunning.collectAsState()
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
Scaffold(modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { Scaffold(modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = {

View File

@ -37,7 +37,7 @@ import li.songe.gkd.a11yServiceEnabledFlow
import li.songe.gkd.permission.notificationState import li.songe.gkd.permission.notificationState
import li.songe.gkd.permission.requiredPermission import li.songe.gkd.permission.requiredPermission
import li.songe.gkd.permission.writeSecureSettingsState 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.ManageService
import li.songe.gkd.service.switchA11yService import li.songe.gkd.service.switchA11yService
import li.songe.gkd.ui.component.AuthCard import li.songe.gkd.ui.component.AuthCard
@ -93,7 +93,7 @@ fun useControlPage(): ScaffoldExt {
val store by storeFlow.collectAsState() val store by storeFlow.collectAsState()
val ruleSummary by ruleSummaryFlow.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 manageRunning by ManageService.isRunning.collectAsState()
val a11yServiceEnabled by a11yServiceEnabledFlow.collectAsState() val a11yServiceEnabled by a11yServiceEnabledFlow.collectAsState()
@ -133,12 +133,12 @@ fun useControlPage(): ScaffoldExt {
storeFlow.value = store.copy( storeFlow.value = store.copy(
enableStatusService = true enableStatusService = true
) )
ManageService.start(context) ManageService.start()
} else { } else {
storeFlow.value = store.copy( storeFlow.value = store.copy(
enableStatusService = false enableStatusService = false
) )
ManageService.stop(context) ManageService.stop()
} }
}) })

View File

@ -0,0 +1,63 @@
package li.songe.gkd.util
import android.view.accessibility.AccessibilityEvent
import java.util.WeakHashMap
private val callbacksMap by lazy { WeakHashMap<Any, HashMap<Int, MutableList<Any>>>() }
@Suppress("UNCHECKED_CAST")
private fun <T> Any.getCallbacks(method: Int): MutableList<T> {
return callbacksMap.getOrPut(this) { hashMapOf() }
.getOrPut(method) { mutableListOf() } as MutableList<T>
}
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() }
}
}