mirror of
https://github.com/gkd-kit/gkd.git
synced 2024-11-16 11:42:22 +08:00
perf: shizuku/clickCenter
This commit is contained in:
parent
b820d90527
commit
57a8684e98
|
@ -130,6 +130,7 @@ android {
|
|||
buildFeatures {
|
||||
buildConfig = true
|
||||
compose = true
|
||||
aidl = true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = libs.versions.compose.compilerVersion.get()
|
||||
|
|
9
app/src/main/aidl/li/songe/gkd/shizuku/IUserService.aidl
Normal file
9
app/src/main/aidl/li/songe/gkd/shizuku/IUserService.aidl
Normal 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;
|
||||
}
|
|
@ -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
|
||||
|
|
10
app/src/main/kotlin/li/songe/gkd/shizuku/CommandResult.kt
Normal file
10
app/src/main/kotlin/li/songe/gkd/shizuku/CommandResult.kt
Normal 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?
|
||||
)
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
79
app/src/main/kotlin/li/songe/gkd/shizuku/UserService.kt
Normal file
79
app/src/main/kotlin/li/songe/gkd/shizuku/UserService.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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-模拟点击校验失败,无法使用")
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user