feat: 模拟点击抽象,增加检测工具类

This commit is contained in:
2026-07-14 18:06:24 +08:00
parent 5c279d1ba6
commit 590d14c85e
5 changed files with 318 additions and 36 deletions

View File

@@ -7,8 +7,7 @@ import com.google.gson.JsonObject;
/** /**
* 处理控制端发来的输入指令(触摸、按键)。 * 处理控制端发来的输入指令(触摸、按键)。
* 注意:注入输入事件需要 INJECT_EVENTS 权限, * 解析指令并委托给 {@link InputExecutor} 接口执行。
* 通常需要系统签名或ADB授权才能使用。
*/ */
public class InputCommandHandler { public class InputCommandHandler {
@@ -16,10 +15,13 @@ public class InputCommandHandler {
private final Gson gson = new Gson(); private final Gson gson = new Gson();
private final int screenWidth; private final int screenWidth;
private final int screenHeight; private final int screenHeight;
private final InputExecutor inputExecutor;
public InputCommandHandler(int screenWidth, int screenHeight) { public InputCommandHandler(int screenWidth, int screenHeight) {
this.screenWidth = screenWidth; this.screenWidth = screenWidth;
this.screenHeight = screenHeight; this.screenHeight = screenHeight;
// 使用系统签名权限下的 SystemInputUtils 进行事件注入
this.inputExecutor = new SystemInputUtils();
} }
public void handleCommand(String commandJson) { public void handleCommand(String commandJson) {
@@ -55,18 +57,13 @@ public class InputCommandHandler {
int y = (int) (relY * screenHeight); int y = (int) (relY * screenHeight);
Log.d(TAG, "Touch at: " + x + ", " + y); Log.d(TAG, "Touch at: " + x + ", " + y);
injectTap(x, y); inputExecutor.injectTap(x, y);
} }
private void handleKey(JsonObject command) { private void handleKey(JsonObject command) {
int keyCode = command.get("keyCode").getAsInt(); int keyCode = command.get("keyCode").getAsInt();
Log.d(TAG, "Key press: " + keyCode); Log.d(TAG, "Key press: " + keyCode);
// 使用 Runtime.exec 模拟按键需要root或ADB权限 inputExecutor.injectKeyEvent(keyCode);
try {
Runtime.getRuntime().exec("input keyevent " + keyCode);
} catch (Exception e) {
Log.e(TAG, "Error injecting key event", e);
}
} }
private void handleSwipe(JsonObject command) { private void handleSwipe(JsonObject command) {
@@ -82,7 +79,7 @@ public class InputCommandHandler {
int y2 = (int) (relY2 * screenHeight); int y2 = (int) (relY2 * screenHeight);
Log.d(TAG, "Swipe from (" + x1 + "," + y1 + ") to (" + x2 + "," + y2 + ")"); Log.d(TAG, "Swipe from (" + x1 + "," + y1 + ") to (" + x2 + "," + y2 + ")");
injectSwipe(x1, y1, x2, y2, duration); inputExecutor.injectSwipe(x1, y1, x2, y2, duration);
} }
private void handleLongPress(JsonObject command) { private void handleLongPress(JsonObject command) {
@@ -92,31 +89,6 @@ public class InputCommandHandler {
int y = (int) (relY * screenHeight); int y = (int) (relY * screenHeight);
Log.d(TAG, "Long press at: " + x + ", " + y); Log.d(TAG, "Long press at: " + x + ", " + y);
injectLongPress(x, y); inputExecutor.injectLongPress(x, y);
}
private void injectTap(int x, int y) {
try {
Runtime.getRuntime().exec("input tap " + x + " " + y);
} catch (Exception e) {
Log.e(TAG, "Error injecting tap event", e);
}
}
private void injectLongPress(int x, int y) {
try {
// 用起点和终点相同的 swipe 模拟长按,持续 1000ms
Runtime.getRuntime().exec("input swipe " + x + " " + y + " " + x + " " + y + " 1000");
} catch (Exception e) {
Log.e(TAG, "Error injecting long press event", e);
}
}
private void injectSwipe(int x1, int y1, int x2, int y2, long duration) {
try {
Runtime.getRuntime().exec("input swipe " + x1 + " " + y1 + " " + x2 + " " + y2 + " " + duration);
} catch (Exception e) {
Log.e(TAG, "Error injecting swipe event", e);
}
} }
} }

View File

@@ -0,0 +1,11 @@
package com.ttstd.controlled.input;
/**
* 输入执行器接口,定义了注入输入事件的标准操作。
*/
public interface InputExecutor {
void injectTap(int x, int y);
void injectLongPress(int x, int y);
void injectSwipe(int x1, int y1, int x2, int y2, long duration);
void injectKeyEvent(int keyCode);
}

View File

@@ -0,0 +1,40 @@
package com.ttstd.controlled.input;
import android.util.Log;
import java.util.Locale;
/**
* 通过 Shell 命令Runtime.exec执行注入操作的实现类。
*/
public class ShellInputUtils implements InputExecutor {
private static final String TAG = "ShellInputUtils";
@Override
public void injectTap(int x, int y) {
executeShellCommand(String.format(Locale.US, "input tap %d %d", x, y));
}
@Override
public void injectLongPress(int x, int y) {
injectSwipe(x, y, x, y, 1000);
}
@Override
public void injectSwipe(int x1, int y1, int x2, int y2, long duration) {
executeShellCommand(String.format(Locale.US, "input swipe %d %d %d %d %d", x1, y1, x2, y2, duration));
}
@Override
public void injectKeyEvent(int keyCode) {
executeShellCommand(String.format(Locale.US, "input keyevent %d", keyCode));
}
private void executeShellCommand(String command) {
try {
Log.d(TAG, "Executing shell command: " + command);
Runtime.getRuntime().exec(command);
} catch (Exception e) {
Log.e(TAG, "Error executing shell command: " + command, e);
}
}
}

View File

@@ -0,0 +1,96 @@
package com.ttstd.controlled.input;
import android.os.SystemClock;
import android.util.Log;
import android.view.InputDevice;
import android.view.InputEvent;
import android.view.KeyEvent;
import android.view.MotionEvent;
import java.lang.reflect.Method;
/**
* 使用系统隐藏 API android.hardware.input.InputManager 进行事件注入的实现类。
* 需要应用具有系统签名 (android:sharedUserId="android.uid.system") 和 INJECT_EVENTS 权限。
*/
public class SystemInputUtils implements InputExecutor {
private static final String TAG = "SystemInputUtils";
private Object mInputManager;
private Method mInjectInputEventMethod;
private static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2;
public SystemInputUtils() {
try {
// 反射获取 InputManager 实例
Class<?> inputManagerClass = Class.forName("android.hardware.input.InputManager");
Method getInstanceMethod = inputManagerClass.getDeclaredMethod("getInstance");
mInputManager = getInstanceMethod.invoke(null);
// 获取 injectInputEvent 方法
mInjectInputEventMethod = inputManagerClass.getMethod("injectInputEvent", InputEvent.class, int.class);
} catch (Exception e) {
Log.e(TAG, "Failed to initialize SystemInputUtils via reflection", e);
}
}
@Override
public void injectTap(int x, int y) {
long downTime = SystemClock.uptimeMillis();
injectMotionEvent(downTime, downTime, MotionEvent.ACTION_DOWN, x, y);
injectMotionEvent(downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, x, y);
}
@Override
public void injectLongPress(int x, int y) {
long downTime = SystemClock.uptimeMillis();
injectMotionEvent(downTime, downTime, MotionEvent.ACTION_DOWN, x, y);
// 模拟长按等待
SystemClock.sleep(1000);
injectMotionEvent(downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, x, y);
}
@Override
public void injectSwipe(int x1, int y1, int x2, int y2, long duration) {
long downTime = SystemClock.uptimeMillis();
injectMotionEvent(downTime, downTime, MotionEvent.ACTION_DOWN, x1, y1);
int steps = 10;
for (int i = 1; i <= steps; i++) {
SystemClock.sleep(duration / steps);
int x = x1 + (x2 - x1) * i / steps;
int y = y1 + (y2 - y1) * i / steps;
injectMotionEvent(downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_MOVE, x, y);
}
injectMotionEvent(downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, x2, y2);
}
@Override
public void injectKeyEvent(int keyCode) {
long now = SystemClock.uptimeMillis();
injectKeyEvent(now, now, KeyEvent.ACTION_DOWN, keyCode);
injectKeyEvent(now, SystemClock.uptimeMillis(), KeyEvent.ACTION_UP, keyCode);
}
private void injectMotionEvent(long downTime, long eventTime, int action, float x, float y) {
MotionEvent event = MotionEvent.obtain(downTime, eventTime, action, x, y, 0);
event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
try {
mInjectInputEventMethod.invoke(mInputManager, event, INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH);
} catch (Exception e) {
Log.e(TAG, "Error injecting motion event", e);
} finally {
event.recycle();
}
}
private void injectKeyEvent(long downTime, long eventTime, int action, int keyCode) {
KeyEvent event = new KeyEvent(downTime, eventTime, action, keyCode, 0);
try {
mInjectInputEventMethod.invoke(mInputManager, event, INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH);
} catch (Exception e) {
Log.e(TAG, "Error injecting key event", e);
}
}
}

View File

@@ -0,0 +1,163 @@
package com.ttstd.controlled.utils;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.util.Log;
import java.io.File;
/**
* 签名与系统属性工具类
*/
public class SignatureUtils {
private static final String TAG = "SignatureUtils";
/**
* 检查当前应用是否为系统应用(安装在系统分区)
*
* @param context 上下文
* @return true 如果是系统应用
*/
public static boolean isSystemApp(Context context) {
try {
ApplicationInfo ai = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0);
return (ai.flags & ApplicationInfo.FLAG_SYSTEM) != 0 ||
(ai.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0;
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "Package not found", e);
return false;
}
}
/**
* 检查当前应用是否具有系统签名
* 通过比较应用签名与系统核心包 "android" 的签名是否一致来判断
*
* @param context 上下文
* @return true 如果应用具有系统签名
*/
public static boolean isSystemSignature(Context context) {
PackageManager pm = context.getPackageManager();
try {
// 与系统核心包 "android" 比较签名
int result = pm.checkSignatures(context.getPackageName(), "android");
return result == PackageManager.SIGNATURE_MATCH;
} catch (Exception e) {
Log.e(TAG, "Error checking system signature", e);
return false;
}
}
/**
* 检查是否声明并获得了系统级权限(如 INJECT_EVENTS
*
* @param context 上下文
* @param permission 权限名称
* @return true 如果权限已授予
*/
public static boolean hasPermission(Context context, String permission) {
return context.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED;
}
/**
* 检查当前应用是否配置了 android:sharedUserId="android.uid.system"
*
* @param context 上下文
* @return true 如果配置了系统共享 UID
*/
public static boolean isSharedSystemUid(Context context) {
try {
PackageInfo pi = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
return "android.uid.system".equals(pi.sharedUserId);
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "Package not found", e);
return false;
}
}
/**
* 检查当前进程是否正在以系统 UID (1000) 运行
*
* @return true 如果当前 UID 为 1000
*/
public static boolean isSystemUid() {
// android.os.Process.SYSTEM_UID 为 1000
return android.os.Process.myUid() == 1000;
}
/**
* 检查系统是否已 Root
*
* @return true 如果检测到 Root 特征
*/
public static boolean isDeviceRooted() {
// 1. 检查 Build.TAGS
String buildTags = Build.TAGS;
if (buildTags != null && buildTags.contains("test-keys")) {
return true;
}
// 2. 检查常见的 su 二进制文件路径
String[] paths = {
"/system/app/Superuser.apk",
"/sbin/su",
"/system/bin/su",
"/system/xbin/su",
"/data/local/xbin/su",
"/data/local/bin/su",
"/system/sd/xbin/su",
"/system/bin/failsafe/su",
"/data/local/su",
"/su/bin/su"
};
for (String path : paths) {
if (new File(path).exists()) {
return true;
}
}
// 3. 尝试执行 su 命令(虽然这更像是检测应用是否有权限,但也说明系统已 Root
return checkRootMethod3();
}
private static boolean checkRootMethod3() {
Process process = null;
try {
process = Runtime.getRuntime().exec(new String[]{"/system/xbin/which", "su"});
return process.waitFor() == 0;
} catch (Throwable t) {
return false;
} finally {
if (process != null) process.destroy();
}
}
/**
* 检查当前应用自身是否已被授予 Root 权限
*
* @return true 如果应用可以执行 su 并获得 root 身份
*/
public static boolean isAppHasRootPermission() {
Process process = null;
try {
// 尝试请求 su
process = Runtime.getRuntime().exec("su");
// 写入 exit 确保进程能正常结束
process.getOutputStream().write("exit\n".getBytes());
process.getOutputStream().flush();
int exitValue = process.waitFor();
return exitValue == 0;
} catch (Exception e) {
// 执行 su 失败,通常说明没有 root 权限或未安装 su
return false;
} finally {
if (process != null) {
process.destroy();
}
}
}
}