perf: shizuku/clickCenter

This commit is contained in:
lisonge 2024-03-15 20:54:20 +08:00
parent b820d90527
commit 57a8684e98
8 changed files with 225 additions and 39 deletions

View File

@ -130,6 +130,7 @@ android {
buildFeatures {
buildConfig = true
compose = true
aidl = true
}
composeOptions {
kotlinCompilerExtensionVersion = libs.versions.compose.compilerVersion.get()

View File

@ -0,0 +1,9 @@
package li.songe.gkd.shizuku;
interface IUserService {
void destroy() = 16777114; // Destroy method defined by Shizuku server
void exit() = 1; // Exit method defined by user
String execCommand(String command) = 2;
}

View File

@ -45,7 +45,7 @@ import li.songe.gkd.debug.SnapshotExt
import li.songe.gkd.shizuku.getShizukuCanUsedFlow
import li.songe.gkd.shizuku.shizukuIsSafeOK
import li.songe.gkd.shizuku.useSafeGetTasksFc
import li.songe.gkd.shizuku.useSafeInjectClickEventFc
import li.songe.gkd.shizuku.useSafeInputTapFc
import li.songe.gkd.shizuku.useShizukuAliveState
import li.songe.gkd.util.VOLUME_CHANGED_ACTION
import li.songe.gkd.util.checkSubsUpdate
@ -104,7 +104,7 @@ class GkdAbService : CompositionAbService({
shizukuAliveFlow,
storeFlow.map(scope) { s -> s.enableShizukuClick }
)
val safeInjectClickEventFc = useSafeInjectClickEventFc(scope, shizukuClickCanUsedFlow)
val safeInjectClickEventFc = useSafeInputTapFc(scope, shizukuClickCanUsedFlow)
injectClickEventFc = safeInjectClickEventFc
onDestroy {
injectClickEventFc = null

View File

@ -0,0 +1,10 @@
package li.songe.gkd.shizuku
import kotlinx.serialization.Serializable
@Serializable
data class CommandResult(
val code: Int,
val result: String,
val error: String?
)

View File

@ -3,8 +3,12 @@ package li.songe.gkd.shizuku
import android.app.ActivityManager
import android.app.IActivityTaskManager
import android.content.ComponentName
import android.content.Context
import android.content.ServiceConnection
import android.content.pm.PackageManager
import android.hardware.input.IInputManager
import android.os.IBinder
import android.os.SystemClock
import android.view.Display
import android.view.MotionEvent
@ -15,16 +19,29 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import li.songe.gkd.BuildConfig
import li.songe.gkd.composition.CanOnDestroy
import li.songe.gkd.data.DeviceInfo
import li.songe.gkd.util.json
import li.songe.gkd.util.map
import li.songe.gkd.util.toast
import rikka.shizuku.Shizuku
import rikka.shizuku.ShizukuBinderWrapper
import rikka.shizuku.SystemServiceHelper
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlin.reflect.full.declaredMemberFunctions
import kotlin.reflect.typeOf
fun shizukuIsSafeOK(): Boolean {
return try {
Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED
} catch (e: Exception) {
false
}
}
/**
* https://github.com/gkd-kit/gkd/issues/44
*/
@ -38,8 +55,7 @@ fun newActivityTaskManager(): IActivityTaskManager? {
LogUtils.d("shizuku 无法获取 $ACTIVITY_TASK_SERVICE")
return null
}
return service.let(::ShizukuBinderWrapper)
.let(IActivityTaskManager.Stub::asInterface)
return service.let(::ShizukuBinderWrapper).let(IActivityTaskManager.Stub::asInterface)
}
/**
@ -114,9 +130,7 @@ fun getShizukuCanUsedFlow(
shizukuEnableFlow: StateFlow<Boolean>,
): StateFlow<Boolean> {
return combine(
shizukuAliveFlow,
shizukuGrantFlow,
shizukuEnableFlow
shizukuAliveFlow, shizukuGrantFlow, shizukuEnableFlow
) { shizukuAlive, shizukuGrant, enableShizuku ->
enableShizuku && shizukuAlive && shizukuGrant
}.stateIn(scope, SharingStarted.Eagerly, false)
@ -144,18 +158,10 @@ fun IInputManager.safeClick(x: Float, y: Float): Boolean? {
// 下面除了 pressure 的常量来自 MotionEvent obtain 方法
val downTime = SystemClock.uptimeMillis()
val downEvent = MotionEvent.obtain(
downTime,
downTime,
MotionEvent.ACTION_DOWN,
x, y, 1.0f, 1.0f, 0,
1.0f, 1.0f, 0, 0
downTime, downTime, MotionEvent.ACTION_DOWN, x, y, 1.0f, 1.0f, 0, 1.0f, 1.0f, 0, 0
) // pressure=1.0f
val upEvent = MotionEvent.obtain(
downTime,
downTime,
MotionEvent.ACTION_UP,
x, y, 0f, 1.0f, 0,
1.0f, 1.0f, 0, 0
downTime, downTime, MotionEvent.ACTION_UP, x, y, 0f, 1.0f, 0, 1.0f, 1.0f, 0, 0
) // pressure=0f
return try {
val r1 = injectInputEvent(downEvent, 2)
@ -172,14 +178,105 @@ fun IInputManager.safeClick(x: Float, y: Float): Boolean? {
fun useSafeInjectClickEventFc(
scope: CoroutineScope,
shizukuCanUsedFlow: StateFlow<Boolean>,
usedFlow: StateFlow<Boolean>,
): (x: Float, y: Float) -> Boolean? {
val inputManagerFlow = shizukuCanUsedFlow.map(scope) { if (it) newInputManager() else null }
val inputManagerFlow = usedFlow.map(scope) { if (it) newInputManager() else null }
return { x, y ->
if (shizukuCanUsedFlow.value) {
if (usedFlow.value) {
inputManagerFlow.value?.safeClick(x, y)
} else {
null
}
}
}
// 在 大麦 https://i.gkd.li/i/14605104 上测试产生如下 3 种情况
// 1. 点击不生效: 使用传统无障碍屏幕点击, 此种点击可被 大麦 通过 View.setAccessibilityDelegate 屏蔽
// 2. 点击概率生效: 使用 Shizuku 获取到的 InputManager.injectInputEvent 发出点击, 概率失效/生效, 原因未知
// 3. 点击生效: 使用 Shizuku 获取到的 shell input tap x y 发出点击 by useSafeInputTapFc, 暂未找到屏蔽方案
fun useSafeInputTapFc(
scope: CoroutineScope,
usedFlow: StateFlow<Boolean>,
): (x: Float, y: Float) -> Boolean? {
val serviceWrapperFlow = MutableStateFlow<UserServiceWrapper?>(null)
scope.launch {
usedFlow.collect {
if (it) {
val serviceWrapper = newUserService()
serviceWrapperFlow.value = serviceWrapper
} else {
serviceWrapperFlow.value?.destroy()
serviceWrapperFlow.value = null
}
}
}
return { x, y ->
if (usedFlow.value) {
try {
val result = serviceWrapperFlow.value?.userService?.execCommand("input tap $x $y")
if (result != null) {
json.decodeFromString<CommandResult>(result).code == 0
} else {
null
}
} catch (e: Exception) {
e.printStackTrace()
null
}
} else {
null
}
}
}
data class UserServiceWrapper(
val userService: IUserService,
val connection: ServiceConnection,
val serviceArgs: Shizuku.UserServiceArgs
) {
fun destroy() {
try {
Shizuku.unbindUserService(serviceArgs, connection, false)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
suspend fun newUserService(): UserServiceWrapper = suspendCoroutine { continuation ->
val serviceArgs = Shizuku.UserServiceArgs(
ComponentName(
BuildConfig.APPLICATION_ID,
UserService::class.java.name
)
).daemon(false).processNameSuffix(
"service-for-${if (BuildConfig.DEBUG) "gkd-debug" else "gkd-release"}"
).debuggable(BuildConfig.DEBUG).version(BuildConfig.VERSION_CODE)
var resumeFc: ((UserServiceWrapper) -> Unit)? = { continuation.resume(it) }
val connection = object : ServiceConnection {
override fun onServiceConnected(componentName: ComponentName, binder: IBinder?) {
// Shizuku.unbindUserService 并不会移除 connection, 导致后续调用此函数时 此方法仍然被调用
LogUtils.d("onServiceConnected", componentName)
resumeFc ?: return
if (binder?.pingBinder() == true) {
resumeFc?.invoke(
UserServiceWrapper(
IUserService.Stub.asInterface(binder),
this,
serviceArgs
)
)
resumeFc = null
} else {
LogUtils.d("invalid binder for $componentName received")
}
}
override fun onServiceDisconnected(componentName: ComponentName) {
LogUtils.d("onServiceDisconnected", componentName)
}
}
Shizuku.bindUserService(serviceArgs, connection)
}

View File

@ -1,12 +0,0 @@
package li.songe.gkd.shizuku
import android.content.pm.PackageManager
import rikka.shizuku.Shizuku
fun shizukuIsSafeOK(): Boolean {
return try {
Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED
} catch (e: Exception) {
false
}
}

View File

@ -0,0 +1,79 @@
package li.songe.gkd.shizuku
import android.content.Context
import android.os.RemoteException
import android.util.Log
import androidx.annotation.Keep
import kotlinx.serialization.encodeToString
import li.songe.gkd.util.json
import java.io.DataOutputStream
class UserService : IUserService.Stub {
/**
* Constructor is required.
*/
constructor() {
Log.i("UserService", "constructor")
}
@Keep
constructor(context: Context) {
Log.i("UserService", "constructor with Context: context=$context")
}
/**
* Reserved destroy method
*/
override fun destroy() {
Log.i("UserService", "destroy")
}
override fun exit() {
destroy()
}
@Throws(RemoteException::class)
override fun execCommand(command: String): String {
val process = Runtime.getRuntime().exec("sh")
val outputStream = DataOutputStream(process.outputStream)
val commandResult = try {
command.split('\n').filter { it.isNotBlank() }.forEach {
outputStream.write(it.toByteArray())
outputStream.writeBytes('\n'.toString())
outputStream.flush()
}
outputStream.writeBytes("exit\n")
outputStream.flush()
CommandResult(
code = process.waitFor(),
result = process.inputStream.bufferedReader().readText(),
error = process.errorStream.bufferedReader().readText(),
)
} catch (e: Exception) {
e.printStackTrace()
val message = e.message
val aimErrStr = "error="
val index = message?.indexOf(aimErrStr)
val code = if (index != null) {
message.substring(index + aimErrStr.length)
.takeWhile { c -> c.isDigit() }
.toIntOrNull()
} else {
null
} ?: 1
CommandResult(
code = code,
result = "",
error = e.message,
)
} finally {
outputStream.close()
process.inputStream.close()
process.outputStream.close()
process.destroy()
}
return json.encodeToString(commandResult)
}
}

View File

@ -56,14 +56,15 @@ import com.dylanc.activityresult.launcher.launchForResult
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootNavGraph
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.update
import li.songe.gkd.MainActivity
import li.songe.gkd.appScope
import li.songe.gkd.debug.FloatingService
import li.songe.gkd.debug.HttpService
import li.songe.gkd.debug.ScreenshotService
import li.songe.gkd.shizuku.CommandResult
import li.songe.gkd.shizuku.newActivityTaskManager
import li.songe.gkd.shizuku.newInputManager
import li.songe.gkd.shizuku.safeClick
import li.songe.gkd.shizuku.newUserService
import li.songe.gkd.shizuku.safeGetTasks
import li.songe.gkd.shizuku.shizukuIsSafeOK
import li.songe.gkd.ui.component.AuthCard
@ -76,6 +77,7 @@ import li.songe.gkd.util.ProfileTransitions
import li.songe.gkd.util.authActionFlow
import li.songe.gkd.util.canDrawOverlaysAuthAction
import li.songe.gkd.util.checkOrRequestNotifPermission
import li.songe.gkd.util.json
import li.songe.gkd.util.launchAsFn
import li.songe.gkd.util.launchTry
import li.songe.gkd.util.navigate
@ -166,11 +168,11 @@ fun DebugPage() {
onCheckedChange = { enableShizuku ->
if (enableShizuku) {
appScope.launchTry(Dispatchers.IO) {
val result = newInputManager()?.safeClick(0f, 0f)
if (result != null) {
storeFlow.value = store.copy(
enableShizukuClick = true
)
val service = newUserService()
val result = service.userService.execCommand("input tap 0 0")
service.destroy()
if (json.decodeFromString<CommandResult>(result).code == 0) {
storeFlow.update { it.copy(enableShizukuClick = true) }
} else {
toast("Shizuku-模拟点击校验失败,无法使用")
}