mirror of
https://github.com/gkd-kit/gkd.git
synced 2024-11-16 11:42:22 +08:00
refactor: simplify code
This commit is contained in:
parent
97857f505b
commit
c8a1e00077
|
@ -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">
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
package li.songe.gkd.composition
|
||||
|
||||
interface CanConfigBubble {
|
||||
fun configBubble(f: ConfigBubbleHook)
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
package li.songe.gkd.composition
|
||||
|
||||
import android.view.accessibility.AccessibilityEvent
|
||||
|
||||
interface CanOnAccessibilityEvent {
|
||||
fun onAccessibilityEvent(f: (AccessibilityEvent) -> Unit): Boolean
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
package li.songe.gkd.composition
|
||||
|
||||
import android.content.res.Configuration
|
||||
|
||||
interface CanOnConfigurationChanged {
|
||||
fun onConfigurationChanged(f: (newConfig: Configuration) -> Unit):Boolean
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
package li.songe.gkd.composition
|
||||
|
||||
interface CanOnDestroy {
|
||||
fun onDestroy(f: () -> Unit): Boolean
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
package li.songe.gkd.composition
|
||||
|
||||
interface CanOnInterrupt {
|
||||
fun onInterrupt(f: () -> Unit):Boolean
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
package li.songe.gkd.composition
|
||||
|
||||
import android.view.KeyEvent
|
||||
|
||||
interface CanOnKeyEvent {
|
||||
fun onKeyEvent(f: (KeyEvent?) -> Unit): Unit
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
package li.songe.gkd.composition
|
||||
|
||||
interface CanOnServiceConnected {
|
||||
fun onServiceConnected(f: () -> Unit):Boolean
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
package li.songe.gkd.composition
|
||||
|
||||
interface CanOnStartCommand {
|
||||
fun onStartCommand(f: StartCommandHook): Boolean
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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() }
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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,
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
})
|
||||
|
||||
|
|
63
app/src/main/kotlin/li/songe/gkd/util/LifecycleCallbacks.kt
Normal file
63
app/src/main/kotlin/li/songe/gkd/util/LifecycleCallbacks.kt
Normal 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() }
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user