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