Android(minSDK30)跨应用模拟屏幕点击输入的实现方案咨询
背景
我正在开发一款类似AnyDesk的远程访问应用,支持通过浏览器在局域网内访问并操控移动设备。目前已实现通过WebSocket将设备画面实时传输至PC浏览器,但无法模拟跨应用的屏幕点击输入——PC端通过WebSocket发送的坐标能在Android代码中接收,但无法触发点击事件。
开发环境
- Android Studio Hedgehog
- Java Gradle项目
- minSDK版本:30
已尝试的无效方案
1. 使用Instrumentation
代码实现:
public class InputSimulator { public static void simulateTap (int x, int y) { Instrumentation inst = new Instrumentation(); long timeDown = SystemClock.uptimeMillis(); long eventTime = timeDown + 100; MotionEvent motionEventDown = MotionEvent.obtain(timeDown,eventTime,MotionEvent.ACTION_DOWN,x,y,0); inst.sendPointerSync(motionEventDown); MotionEvent motionEventUP = MotionEvent.obtain(timeDown,eventTime,MotionEvent.ACTION_UP,x,y,0); inst.sendPointerSync(motionEventUp); motionEventDown.recycle(); motionEventUp.recycle(); } }
问题:因Android安全限制(Android 8.0以上禁止)抛出异常:
java.lang.SecurityException: Injecting to another application requires INJECT_EVENTS permission
在Manifest中添加该权限时,提示:
Permissions with the protection level signature, privileged or signatureOrSystem are only granted to system apps
该权限属于系统级权限,普通应用无法获取。
2. 使用Runtime执行input命令
代码实现:
public class InputSimulator { public static void simulateTap (int x, int y) { try { Process process = Runtime.getRuntime().exec("input " + x + " " + y); int exitCode = process.waitFor(); Log.d("INPUT", "COMMAND CODE: " + exitCode); // exitCode is 0 but no tap } catch (IOException | InterruptedException e) { e.printStackTrace(); } } }
问题:命令执行返回码为0,但未触发任何点击操作。(注:原代码缺少tap指令,这是可能的原因之一,但即使补充后,非Root设备下普通应用也无法执行该命令)
3. 尝试AccessibilityService但不知如何实现
已创建AccessibilityService的基础框架,但不清楚如何通过它实现坐标点击功能。
核心疑问
在Android 11+(minSDK30)的环境下,实现跨应用的虚拟屏幕点击,是否有无需Root、无需安装为系统应用的可行方案?如果必须Root或系统应用,有没有其他替代思路?
解决方案
方案一:正确实现AccessibilityService(非Root/非系统应用可行)
这是当前高版本Android中,普通应用实现跨应用模拟输入的唯一合法途径,步骤如下:
- 创建AccessibilityService类
public class RemoteControlAccessibilityService extends AccessibilityService { @Override public void onAccessibilityEvent(AccessibilityEvent event) { // 无需处理事件,仅需保持服务运行 } @Override public void onInterrupt() { // 服务中断处理 } // 对外提供模拟点击的方法 public void simulateTap(int x, int y) { // 创建GestureDescription.Builder GestureDescription.Builder builder = new GestureDescription.Builder(); // 构建点击路径:按下->抬起,持续100ms Path path = new Path(); path.moveTo(x, y); // 添加按下动作(0ms时) builder.addStroke(new GestureDescription.StrokeDescription( path, 0, 100 )); // 执行手势 dispatchGesture(builder.build(), new GestureResultCallback() { @Override public void onCompleted(GestureDescription gestureDescription) { super.onCompleted(gestureDescription); Log.d("ACCESSIBILITY", "点击执行完成"); } @Override public void onCancelled(GestureDescription gestureDescription) { super.onCancelled(gestureDescription); Log.e("ACCESSIBILITY", "点击被取消"); } }, null); } }
- 在Manifest中注册服务
<service android:name=".RemoteControlAccessibilityService" android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"> <intent-filter> <action android:name="android.accessibilityservice.AccessibilityService" /> </intent-filter> <meta-data android:name="android.accessibilityservice" android:resource="@xml/accessibility_service_config" /> </service>
- 创建AccessibilityService配置文件
在res/xml目录下创建accessibility_service_config.xml:
<?xml version="1.0" encoding="utf-8"?> <accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" android:accessibilityEventTypes="typeAllMask" android:accessibilityFeedbackType="feedbackGeneric" android:accessibilityFlags="flagDefault|flagIncludeNotImportantViews|flagRequestTouchExplorationMode" android:canPerformGestures="true" android:description="@string/accessibility_service_description" />
在strings.xml中添加描述:
<string name="accessibility_service_description">远程控制需要开启无障碍权限,用于模拟屏幕点击操作</string>
- 引导用户开启无障碍权限
普通应用无法自动获取无障碍权限,必须引导用户手动开启:
// 检查无障碍权限是否开启 private boolean isAccessibilityServiceEnabled() { int accessibilityEnabled = 0; final String serviceName = getPackageName() + "/" + RemoteControlAccessibilityService.class.getName(); try { accessibilityEnabled = Settings.Secure.getInt( getContentResolver(), Settings.Secure.ACCESSIBILITY_ENABLED ); } catch (Settings.SettingNotFoundException e) { e.printStackTrace(); } TextUtils.SimpleStringSplitter splitter = new TextUtils.SimpleStringSplitter(':'); if (accessibilityEnabled == 1) { String settingValue = Settings.Secure.getString( getContentResolver(), Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES ); if (settingValue != null) { splitter.setString(settingValue); while (splitter.hasNext()) { String accessibilityService = splitter.next(); if (accessibilityService.equalsIgnoreCase(serviceName)) { return true; } } } } return false; } // 引导用户开启权限 private void requestAccessibilityPermission() { Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS); startActivity(intent); }
方案二:Root设备下使用input命令(补充)
如果你的应用目标用户包含Root设备用户,可以修正input命令的执行方式:
public static void simulateTapRoot(int x, int y) { try { // 需要以root身份执行 Process process = Runtime.getRuntime().exec("su"); OutputStream outputStream = process.getOutputStream(); outputStream.write(("input tap " + x + " " + y + "\n").getBytes()); outputStream.flush(); outputStream.close(); process.waitFor(); } catch (IOException | InterruptedException e) { e.printStackTrace(); } }
注意:原代码缺少tap指令,这是之前执行无效果的原因之一,但非Root设备下普通应用无法执行input命令。
方案三:系统应用(仅适合定制ROM场景)
如果你的应用可以作为系统应用安装(通过签名或放入/system/app目录),可以使用Instrumentation或直接调用InputManager,但这仅适用于定制ROM或企业级场景,普通开发者无法实现。
内容的提问来源于stack exchange,提问作者ppepa

