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>
<service
android:name=".service.GkdAbService"
android:name=".service.A11yService"
android:exported="false"
android:label="@string/app_name"
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.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

View File

@ -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<MainViewModel>()
@ -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() {

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

View File

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

View File

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

View File

@ -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<GkdAction>()
call.respond(GkdAbService.execAction(gkdAction))
call.respond(A11yService.execAction(gkdAction))
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<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(
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<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)
}
private fun A11yService.useMatchRule() {
val context = this
var lastTriggerShizukuTime = 0L
var lastContentEventTime = 0L
val events = mutableListOf<AccessibilityNodeInfo>()
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
}
}
}
}
}
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() {
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()
}

View File

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

View File

@ -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<ActivityManager
// 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(
scope: CoroutineScope,
shizukuGrantFlow: StateFlow<Boolean>,
shizukuAliveFlow: StateFlow<Boolean>,
shizukuEnableFlow: StateFlow<Boolean>,
enableFlow: StateFlow<Boolean>,
): StateFlow<Boolean> {
return combine(
shizukuAliveFlow, shizukuGrantFlow, shizukuEnableFlow
) { shizukuAlive, shizukuGrant, enableShizuku ->
enableShizuku && shizukuAlive && shizukuGrant
shizukuOkState.stateFlow,
enableFlow
) { shizukuOk, enableShizuku ->
shizukuOk && enableShizuku
}.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.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<AuthA11yVm>()
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 = {

View File

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

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