diff --git a/WebRTCControlled/app/src/main/java/com/ttstd/controlled/input/InputCommandHandler.java b/WebRTCControlled/app/src/main/java/com/ttstd/controlled/input/InputCommandHandler.java index a9ced1e..96aee80 100644 --- a/WebRTCControlled/app/src/main/java/com/ttstd/controlled/input/InputCommandHandler.java +++ b/WebRTCControlled/app/src/main/java/com/ttstd/controlled/input/InputCommandHandler.java @@ -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); } } diff --git a/WebRTCControlled/app/src/main/java/com/ttstd/controlled/input/InputExecutor.java b/WebRTCControlled/app/src/main/java/com/ttstd/controlled/input/InputExecutor.java new file mode 100644 index 0000000..b6abca8 --- /dev/null +++ b/WebRTCControlled/app/src/main/java/com/ttstd/controlled/input/InputExecutor.java @@ -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); +} diff --git a/WebRTCControlled/app/src/main/java/com/ttstd/controlled/input/ShellInputUtils.java b/WebRTCControlled/app/src/main/java/com/ttstd/controlled/input/ShellInputUtils.java new file mode 100644 index 0000000..7cc952a --- /dev/null +++ b/WebRTCControlled/app/src/main/java/com/ttstd/controlled/input/ShellInputUtils.java @@ -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); + } + } +} diff --git a/WebRTCControlled/app/src/main/java/com/ttstd/controlled/input/SystemInputUtils.java b/WebRTCControlled/app/src/main/java/com/ttstd/controlled/input/SystemInputUtils.java new file mode 100644 index 0000000..10e9ae3 --- /dev/null +++ b/WebRTCControlled/app/src/main/java/com/ttstd/controlled/input/SystemInputUtils.java @@ -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); + } + } +} diff --git a/WebRTCControlled/app/src/main/java/com/ttstd/controlled/utils/SignatureUtils.java b/WebRTCControlled/app/src/main/java/com/ttstd/controlled/utils/SignatureUtils.java new file mode 100644 index 0000000..f83ec0b --- /dev/null +++ b/WebRTCControlled/app/src/main/java/com/ttstd/controlled/utils/SignatureUtils.java @@ -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(); + } + } + } +}