Skip to content

Commit

Permalink
perf: a11y mutex, shizuku userService
Browse files Browse the repository at this point in the history
  • Loading branch information
lisonge committed Nov 29, 2024
1 parent 624568c commit 4654a11
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 89 deletions.
100 changes: 54 additions & 46 deletions app/src/main/kotlin/li/songe/gkd/service/GkdTileService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import li.songe.gkd.accessRestrictedSettingsShowFlow
import li.songe.gkd.app
import li.songe.gkd.appScope
Expand Down Expand Up @@ -106,62 +108,68 @@ private fun enableA11yService() {
)
}

val modifyA11yMutex by lazy { Mutex() }

fun switchA11yService() = appScope.launchTry(Dispatchers.IO) {
if (!writeSecureSettingsState.updateAndGet()) {
toast("请先授予[写入安全设置权限]")
return@launchTry
}
val names = getServiceNames()
storeFlow.update { it.copy(enableService = !A11yService.isRunning.value) }
if (A11yService.isRunning.value) {
names.remove(a11yClsName)
updateServiceNames(names)
toast("关闭无障碍")
} else {
enableA11yService()
if (names.contains(a11yClsName)) { // 当前无障碍异常, 重启服务
modifyA11yMutex.withLock {
if (!writeSecureSettingsState.updateAndGet()) {
toast("请先授予[写入安全设置权限]")
return@launchTry
}
val names = getServiceNames()
storeFlow.update { it.copy(enableService = !A11yService.isRunning.value) }
if (A11yService.isRunning.value) {
names.remove(a11yClsName)
updateServiceNames(names)
delay(500)
}
names.add(a11yClsName)
updateServiceNames(names)
delay(300)
if (!A11yService.isRunning.value) {
toast("开启无障碍失败")
accessRestrictedSettingsShowFlow.value = true
return@launchTry
toast("关闭无障碍")
} else {
enableA11yService()
if (names.contains(a11yClsName)) { // 当前无障碍异常, 重启服务
names.remove(a11yClsName)
updateServiceNames(names)
delay(500)
}
names.add(a11yClsName)
updateServiceNames(names)
delay(300)
if (!A11yService.isRunning.value) {
toast("开启无障碍失败")
accessRestrictedSettingsShowFlow.value = true
return@launchTry
}
toast("开启无障碍")
}
toast("开启无障碍")
}
}

fun fixRestartService() = appScope.launchTry(Dispatchers.IO) {
// 1. 服务没有运行
// 2. 用户配置开启了服务
// 3. 有写入系统设置权限
if (!A11yService.isRunning.value && storeFlow.value.enableService && writeSecureSettingsState.updateAndGet()) {
val t = System.currentTimeMillis()
if (t - lastRestartA11yServiceTimeFlow.value < 5_000) return@launchTry
lastRestartA11yServiceTimeFlow.value = t
val names = getServiceNames()
val a11yBroken = names.contains(a11yClsName)
if (a11yBroken) {
// 无障碍出现故障, 重启服务
names.remove(a11yClsName)
modifyA11yMutex.withLock {
// 1. 服务没有运行
// 2. 用户配置开启了服务
// 3. 有写入系统设置权限
if (!A11yService.isRunning.value && storeFlow.value.enableService && writeSecureSettingsState.updateAndGet()) {
val t = System.currentTimeMillis()
if (t - lastRestartA11yServiceTimeFlow.value < 3_000) return@launchTry
lastRestartA11yServiceTimeFlow.value = t
val names = getServiceNames()
val a11yBroken = names.contains(a11yClsName)
if (a11yBroken) {
// 无障碍出现故障, 重启服务
names.remove(a11yClsName)
updateServiceNames(names)
// 必须等待一段时间, 否则概率不会触发系统重启无障碍服务
delay(500)
}
names.add(a11yClsName)
updateServiceNames(names)
// 必须等待一段时间, 否则概率不会触发系统重启无障碍服务
delay(500)
}
names.add(a11yClsName)
updateServiceNames(names)
delay(300)
if (!A11yService.isRunning.value) {
toast("重启无障碍失败")
accessRestrictedSettingsShowFlow.value = true
return@launchTry
delay(300)
if (!A11yService.isRunning.value) {
toast("重启无障碍失败")
accessRestrictedSettingsShowFlow.value = true
return@launchTry
}
toast("重启无障碍")
}
toast("重启无障碍")
}
}

Expand Down
91 changes: 50 additions & 41 deletions app/src/main/kotlin/li/songe/gkd/shizuku/ShizukuApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.flow.updateAndGet
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeoutOrNull
import li.songe.gkd.META
import li.songe.gkd.appScope
import li.songe.gkd.data.DeviceInfo
Expand Down Expand Up @@ -153,45 +156,48 @@ fun safeGetTopActivity(): TopActivity? {
// return service.let(::ShizukuBinderWrapper).let(IPackageManager.Stub::asInterface)
//}

private fun unbindUserService(serviceArgs: Shizuku.UserServiceArgs, connection: ServiceConnection) {
LogUtils.d("unbindUserService", serviceArgs)
Shizuku.unbindUserService(serviceArgs, connection, false)
Shizuku.unbindUserService(serviceArgs, connection, true)
}

data class UserServiceWrapper(
val userService: IUserService,
val connection: ServiceConnection,
val serviceArgs: Shizuku.UserServiceArgs
) {
fun destroy() {
try {
LogUtils.d("unbindUserService", serviceArgs)
userService.exit()
Shizuku.unbindUserService(serviceArgs, connection, true)
} catch (e: Exception) {
e.printStackTrace()
}
unbindUserService(serviceArgs, connection)
}

fun execCommandForResult(command: String): Boolean? {
return userService.execCommandForResult(command)
}
}

private suspend fun serviceWrapper(): UserServiceWrapper = suspendCoroutine { continuation ->
private suspend fun buildServiceWrapper(): UserServiceWrapper? {
val serviceArgs = Shizuku
.UserServiceArgs(UserService::class.componentName)
.daemon(false)
.processNameSuffix("shizuku-user-service")
.debuggable(META.debuggable)
.version(META.versionCode)

var resumeFc: ((UserServiceWrapper) -> Unit)? = { continuation.resume(it) }

LogUtils.d("buildServiceWrapper", serviceArgs)
var resumeCallback: ((UserServiceWrapper) -> Unit)? = null
val connection = object : ServiceConnection {
override fun onServiceConnected(componentName: ComponentName, binder: IBinder?) {
LogUtils.d("onServiceConnected", componentName)
resumeFc ?: return
resumeCallback ?: return
if (binder?.pingBinder() == true) {
resumeFc?.invoke(
resumeCallback?.invoke(
UserServiceWrapper(
IUserService.Stub.asInterface(binder),
this,
serviceArgs
)
)
resumeFc = null
resumeCallback = null
} else {
LogUtils.d("invalid binder for $componentName received")
}
Expand All @@ -201,27 +207,16 @@ private suspend fun serviceWrapper(): UserServiceWrapper = suspendCoroutine { co
LogUtils.d("onServiceDisconnected", componentName)
}
}
Shizuku.bindUserService(serviceArgs, connection)
}

suspend fun execCommandForResult(command: String): Boolean {
serviceWrapperFlow.value?.userService?.let {
return it.execCommandForResult(command) == true
}
val wrapper = serviceWrapper()
return try {
wrapper.userService.execCommandForResult(command) == true
} finally {
wrapper.destroy()
}
}

suspend fun shizukuCheckUserService(): Boolean {
return safeTap(0f, 0f) == true || try {
execCommandForResult("input tap 0 0")
} catch (e: Exception) {
e.printStackTrace()
false
return withTimeoutOrNull(1000) {
suspendCoroutine { continuation ->
resumeCallback = { continuation.resume(it) }
Shizuku.bindUserService(serviceArgs, connection)
}
}.apply {
if (this == null) {
toast("Shizuku获取绑定服务超时失败")
unbindUserService(serviceArgs, connection)
}
}
}

Expand All @@ -231,27 +226,41 @@ private val shizukuServiceUsedFlow by lazy {
}.stateIn(appScope, SharingStarted.Eagerly, false)
}

val serviceWrapperFlow by lazy<StateFlow<UserServiceWrapper?>> {
val serviceWrapperFlow by lazy {
val stateFlow = MutableStateFlow<UserServiceWrapper?>(null)
appScope.launch(Dispatchers.IO) {
shizukuServiceUsedFlow.collect {
stateFlow.value?.destroy()
stateFlow.value = null
if (it) {
stateFlow.value = serviceWrapper()
stateFlow.update { it ?: buildServiceWrapper() }
} else {
stateFlow.update { it?.destroy(); null }
}
}
}
stateFlow
}

suspend fun shizukuCheckUserService(): Boolean {
return try {
execCommandForResult("input tap 0 0")
} catch (e: Exception) {
e.printStackTrace()
false
}
}

suspend fun execCommandForResult(command: String): Boolean {
return serviceWrapperFlow.updateAndGet {
it ?: buildServiceWrapper()
}?.execCommandForResult(command) == true
}

// 在 大麦 https://i.gkd.li/i/14605104 上测试产生如下 3 种情况
// 1. 点击不生效: 使用传统无障碍屏幕点击, 此种点击可被 大麦 通过 View.setAccessibilityDelegate 屏蔽
// 2. 点击概率生效: 使用 Shizuku 获取到的 InputManager.injectInputEvent 发出点击, 概率失效/生效, 原因未知
// 3. 点击生效: 使用 Shizuku 获取到的 shell input tap x y 发出点击 by safeTap, 暂未找到屏蔽方案
fun safeTap(x: Float, y: Float): Boolean? {
val userService = serviceWrapperFlow.value?.userService ?: return null
return userService.execCommandForResult("input tap $x $y")
return serviceWrapperFlow.value?.execCommandForResult("input tap $x $y")
}

private fun IUserService.execCommandForResult(command: String): Boolean? {
Expand Down
7 changes: 6 additions & 1 deletion app/src/main/kotlin/li/songe/gkd/ui/AuthA11yPage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,12 @@ fun AuthA11yPage() {
}
}

private val innerCommandText by lazy { "pm grant ${META.appId} android.permission.WRITE_SECURE_SETTINGS; appops set ${META.appId} ACCESS_RESTRICTED_SETTINGS allow" }
private val innerCommandText by lazy {
arrayOf(
"appops set ${META.appId} ACCESS_RESTRICTED_SETTINGS allow",
"pm grant ${META.appId} android.permission.WRITE_SECURE_SETTINGS"
).joinToString("; ")
}
private val commandText by lazy { "adb shell \"${innerCommandText}\"" }

private fun successAuthExec() {
Expand Down
3 changes: 2 additions & 1 deletion app/src/main/kotlin/li/songe/gkd/ui/AuthA11yVm.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package li.songe.gkd.ui

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.isActive
Expand All @@ -10,7 +11,7 @@ import li.songe.gkd.permission.writeSecureSettingsState

class AuthA11yVm : ViewModel() {
init {
viewModelScope.launch {
viewModelScope.launch(Dispatchers.IO) {
while (isActive) {
writeSecureSettingsState.updateAndGet()
delay(1000)
Expand Down

0 comments on commit 4654a11

Please sign in to comment.