feat: 模拟点击抽象,增加检测工具类
This commit is contained in:
@@ -7,8 +7,7 @@ import com.google.gson.JsonObject;
|
||||
|
||||
/**
|
||||
* 处理控制端发来的输入指令(触摸、按键)。
|
||||
* 注意:注入输入事件需要 INJECT_EVENTS 权限,
|
||||
* 通常需要系统签名或ADB授权才能使用。
|
||||
* 解析指令并委托给 {@link InputExecutor} 接口执行。
|
||||
*/
|
||||
public class InputCommandHandler {
|
||||
|
||||
@@ -16,10 +15,13 @@ public class InputCommandHandler {
|
||||
private final Gson gson = new Gson();
|
||||
private final int screenWidth;
|
||||
private final int screenHeight;
|
||||
private final InputExecutor inputExecutor;
|
||||
|
||||
public InputCommandHandler(int screenWidth, int screenHeight) {
|
||||
this.screenWidth = screenWidth;
|
||||
this.screenHeight = screenHeight;
|
||||
// 使用系统签名权限下的 SystemInputUtils 进行事件注入
|
||||
this.inputExecutor = new SystemInputUtils();
|
||||
}
|
||||
|
||||
public void handleCommand(String commandJson) {
|
||||
@@ -55,18 +57,13 @@ public class InputCommandHandler {
|
||||
int y = (int) (relY * screenHeight);
|
||||
|
||||
Log.d(TAG, "Touch at: " + x + ", " + y);
|
||||
injectTap(x, y);
|
||||
inputExecutor.injectTap(x, y);
|
||||
}
|
||||
|
||||
private void handleKey(JsonObject command) {
|
||||
int keyCode = command.get("keyCode").getAsInt();
|
||||
Log.d(TAG, "Key press: " + keyCode);
|
||||
// 使用 Runtime.exec 模拟按键(需要root或ADB权限)
|
||||
try {
|
||||
Runtime.getRuntime().exec("input keyevent " + keyCode);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error injecting key event", e);
|
||||
}
|
||||
inputExecutor.injectKeyEvent(keyCode);
|
||||
}
|
||||
|
||||
private void handleSwipe(JsonObject command) {
|
||||
@@ -82,7 +79,7 @@ public class InputCommandHandler {
|
||||
int y2 = (int) (relY2 * screenHeight);
|
||||
|
||||
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) {
|
||||
@@ -92,31 +89,6 @@ public class InputCommandHandler {
|
||||
int y = (int) (relY * screenHeight);
|
||||
|
||||
Log.d(TAG, "Long press at: " + x + ", " + y);
|
||||
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);
|
||||
}
|
||||
inputExecutor.injectLongPress(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user