enabledServices = am.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_ALL_MASK);
+
+ // 构建你服务的完整组件标识符
+ String expectedServiceId = new ComponentName(context, serviceClass).flattenToShortString();
+ Log.e(TAG, "isAccessibilityServiceEnabled: expectedServiceId = " + expectedServiceId);
+ for (AccessibilityServiceInfo serviceInfo : enabledServices) {
+ Log.e(TAG, "isAccessibilityServiceEnabled: serviceInfo.getId() = " + serviceInfo.getId());
+ if (expectedServiceId.equals(serviceInfo.getId())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 跳转到系统的无障碍服务设置页面
+ *
+ * @param context 上下文对象
+ */
+ public static void jumpToAccessibilitySettings(Context context) {
+ try {
+ Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); // 通常从应用上下文启动时需要此标志
+ String str = context.getPackageName() + "/" + KeyboardAccessibilityService.class.getName();
+ Bundle bundle = new Bundle();
+ bundle.putString(":settings:fragment_args_key", str);
+ intent.putExtra(":settings:fragment_args_key", str);
+ intent.putExtra(":settings:show_fragment_args", bundle);
+ context.startActivity(intent);
+ } catch (Exception e) {
+ // 处理跳转失败的情况,例如某些设备可能没有标准的设置界面
+ e.printStackTrace();
+ // 可以尝试跳转到系统通用设置作为备选方案
+ Intent fallbackIntent = new Intent(Settings.ACTION_SETTINGS);
+ fallbackIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(fallbackIntent);
+ }
+ }
+
+
+}
diff --git a/WebRTCControlled/app/src/main/java/com/ttstd/controlled/accessibility/KeyboardAccessibilityService.java b/WebRTCControlled/app/src/main/java/com/ttstd/controlled/accessibility/KeyboardAccessibilityService.java
new file mode 100644
index 0000000..e459dc3
--- /dev/null
+++ b/WebRTCControlled/app/src/main/java/com/ttstd/controlled/accessibility/KeyboardAccessibilityService.java
@@ -0,0 +1,112 @@
+package com.ttstd.controlled.accessibility;
+
+import android.accessibilityservice.AccessibilityService;
+import android.accessibilityservice.AccessibilityServiceInfo;
+import android.content.Context;
+import android.os.Build;
+import android.provider.Settings;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.accessibility.AccessibilityEvent;
+
+/**
+ * 键盘无障碍服务。
+ *
+ * 用于在应用没有系统签名、也没有 root 权限时,通过无障碍能力模拟系统级全局按键。
+ * 具体能力:
+ * - {@link #pressKey(int)}:将部分 KeyEvent 按键码映射为 AccessibilityService 的全局动作
+ * (返回 / 主页 / 多任务 / 通知 / 音量等),通过 {@link #performGlobalAction(int)} 触发。
+ * - {@link #dispatchGesture(GestureDescription, ...)}(继承自父类):供
+ * {@code AccessibilityInputUtils} 模拟触摸/滑动/长按等手势。
+ *
+ * 注意:无障碍服务无法直接模拟任意字符按键(如字母、数字),只能触发上述全局动作。
+ */
+public class KeyboardAccessibilityService extends AccessibilityService {
+
+ private static final String TAG = "KeyboardA11yService";
+ private static KeyboardAccessibilityService sInstance;
+
+ /** 获取当前正在运行的实例(未启用时返回 null)。 */
+ public static KeyboardAccessibilityService getInstance() {
+ return sInstance;
+ }
+
+ /** 无障碍服务是否已经启用并运行。 */
+ public static boolean isRunning() {
+ return sInstance != null;
+ }
+
+ @Override
+ protected void onServiceConnected() {
+ super.onServiceConnected();
+ sInstance = this;
+
+ // 也可以通过 res/xml/accessibility_service_config.xml 配置,这里做一次兜底设置。
+ AccessibilityServiceInfo info = new AccessibilityServiceInfo();
+ info.eventTypes = AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
+ | AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED;
+ info.feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC;
+ info.notificationTimeout = 100;
+ setServiceInfo(info);
+
+ Log.i(TAG, "Accessibility service connected");
+ }
+
+ @Override
+ public void onAccessibilityEvent(AccessibilityEvent event) {
+ // 本服务仅用于模拟输入,无需处理具体无障碍事件。
+ }
+
+ @Override
+ public void onInterrupt() {
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (sInstance == this) {
+ sInstance = null;
+ }
+ Log.i(TAG, "Accessibility service destroyed");
+ }
+
+ /**
+ * 通过无障碍服务的全局动作模拟按键。
+ * 仅支持系统级全局按键,对于无障碍无法直接模拟的按键码返回 false。
+ *
+ * @param keyCode Android {@link KeyEvent} 按键码
+ * @return 是否成功模拟(true 表示已触发对应全局动作)
+ */
+ public boolean pressKey(int keyCode) {
+ int globalAction = mapKeyToGlobalAction(keyCode);
+ if (globalAction >= 0) {
+ return performGlobalAction(globalAction);
+ }
+ Log.w(TAG, "Unsupported key code for accessibility: " + keyCode);
+ return false;
+ }
+
+ private static int mapKeyToGlobalAction(int keyCode) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_BACK:
+ return GLOBAL_ACTION_BACK;
+ case KeyEvent.KEYCODE_HOME:
+ return GLOBAL_ACTION_HOME;
+ case KeyEvent.KEYCODE_APP_SWITCH:
+ return GLOBAL_ACTION_RECENTS;
+ case KeyEvent.KEYCODE_NOTIFICATION:
+ return GLOBAL_ACTION_NOTIFICATIONS;
+ case KeyEvent.KEYCODE_VOLUME_UP:
+ // GLOBAL_ACTION_VOLUME_UP = 11 (API 29)
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ? 11 : -1;
+ case KeyEvent.KEYCODE_VOLUME_DOWN:
+ // GLOBAL_ACTION_VOLUME_DOWN = 12 (API 29)
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ? 12 : -1;
+ case KeyEvent.KEYCODE_VOLUME_MUTE:
+ // GLOBAL_ACTION_VOLUME_MUTE = 13 (API 29)
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ? 13 : -1;
+ default:
+ return -1;
+ }
+ }
+}
diff --git a/WebRTCControlled/app/src/main/java/com/ttstd/controlled/activity/main/MainActivity.java b/WebRTCControlled/app/src/main/java/com/ttstd/controlled/activity/main/MainActivity.java
index 52f1201..4af7188 100644
--- a/WebRTCControlled/app/src/main/java/com/ttstd/controlled/activity/main/MainActivity.java
+++ b/WebRTCControlled/app/src/main/java/com/ttstd/controlled/activity/main/MainActivity.java
@@ -4,11 +4,13 @@ import android.Manifest;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.app.AlertDialog;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.media.projection.MediaProjectionManager;
import android.os.Build;
import android.os.IBinder;
+import android.provider.Settings;
import android.util.Log;
import android.widget.Toast;
@@ -18,9 +20,11 @@ import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import com.ttstd.controlled.R;
+import com.ttstd.controlled.accessibility.AccessibilityServiceHelper;
import com.ttstd.controlled.base.BaseMvvmActivity;
import com.ttstd.controlled.databinding.ActivityMainBinding;
import com.ttstd.controlled.service.ScreenCaptureService;
+import com.ttstd.controlled.accessibility.KeyboardAccessibilityService;
import com.ttstd.controlled.utils.DeviceUtils;
import com.ttstd.controlled.utils.SignatureUtils;
import com.ttstd.controlled.webrtc.WebRtcClient;
@@ -39,6 +43,10 @@ public class MainActivity extends BaseMvvmActivity startScreenSharing());
binding.btnStop.setOnClickListener(v -> stopScreenSharing());
@@ -84,9 +92,45 @@ public class MainActivity extends BaseMvvmActivity
-
+ android:layout_height="wrap_content">
-
-
-
-
-
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:padding="24dp">
-
+
-
+
-
+
-
+
-
+
-
+
-
+
+
+
+
+
+
+
+
diff --git a/WebRTCControlled/app/src/main/res/values/strings.xml b/WebRTCControlled/app/src/main/res/values/strings.xml
index dc49b8b..449236f 100644
--- a/WebRTCControlled/app/src/main/res/values/strings.xml
+++ b/WebRTCControlled/app/src/main/res/values/strings.xml
@@ -7,4 +7,10 @@
拒绝
与信令服务器断开,屏幕共享已停止
控制端(%s)已断开远程控制
+ 用于远程控制时模拟键盘与触摸输入。开启后,控制端可通过本服务向本机发送返回、主页、音量等系统按键及触摸手势。
+ 未使用系统签名:如需在本机模拟键盘/触摸,请在系统设置中开启本应用的无障碍服务。
+ 需要开启无障碍服务
+ 当前应用未使用系统签名,也未获取 Root 权限。如需在远程控制时模拟键盘与触摸输入,请前往系统设置开启本应用的无障碍服务。
+ 去开启
+ 取消
diff --git a/WebRTCControlled/app/src/main/res/xml/accessibility_service_config.xml b/WebRTCControlled/app/src/main/res/xml/accessibility_service_config.xml
new file mode 100644
index 0000000..77d58c3
--- /dev/null
+++ b/WebRTCControlled/app/src/main/res/xml/accessibility_service_config.xml
@@ -0,0 +1,8 @@
+
+
diff --git a/webrtc_controller_flutter/lib/utils/device_utils.dart b/webrtc_controller_flutter/lib/utils/device_utils.dart
index 7360bde..e4e5e8f 100644
--- a/webrtc_controller_flutter/lib/utils/device_utils.dart
+++ b/webrtc_controller_flutter/lib/utils/device_utils.dart
@@ -1,5 +1,6 @@
import 'dart:io';
+import 'package:android_id/android_id.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:uuid/uuid.dart';
@@ -8,15 +9,16 @@ import 'package:uuid/uuid.dart';
/// 与 WebRTCControlled 的 DeviceUtils 对应,但控制端没有系统签名,
/// 因此无法稳定获取真实的硬件序列号,这里采用「不要求准确」的兜底方案:
/// 1. 优先使用官方插件([DeviceInfoPlugin])提供的 serialNumber;
-/// 2. 失败则使用 iOS 的 identifierForVendor;
-/// 3. 再失败则生成随机 UUID。
+/// 2. 如果 serialNumber 为 unknown,则尝试获取 Android ID;
+/// 3. 失败则使用 iOS 的 identifierForVendor;
+/// 4. 再失败则生成随机 UUID。
class DeviceUtils {
DeviceUtils._();
/// 获取设备标识(适用于普通应用)。
///
- /// 兼容各平台:Android 使用官方序列号,iOS 使用 identifierForVendor,
- /// 均不可用时回退到随机 UUID。
+ /// 兼容各平台:Android 优先使用官方序列号,其次使用 Android ID,
+ /// iOS 使用 identifierForVendor,均不可用时回退到随机 UUID。
static Future getSerialNumber() async {
const uuid = Uuid();
try {
@@ -27,6 +29,13 @@ class DeviceUtils {
if (serial.isNotEmpty && serial.toLowerCase() != 'unknown') {
return serial;
}
+
+ // 如果 serialNumber 获取失败,尝试获取 Android ID
+ const androidIdPlugin = AndroidId();
+ final androidId = await androidIdPlugin.getId();
+ if (androidId != null && androidId.isNotEmpty) {
+ return androidId;
+ }
} else if (Platform.isIOS) {
final iosInfo = await deviceInfo.iosInfo;
final id = iosInfo.identifierForVendor;
diff --git a/webrtc_controller_flutter/pubspec.lock b/webrtc_controller_flutter/pubspec.lock
index bdf7f8e..f7de2e5 100644
--- a/webrtc_controller_flutter/pubspec.lock
+++ b/webrtc_controller_flutter/pubspec.lock
@@ -1,6 +1,14 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
+ android_id:
+ dependency: "direct main"
+ description:
+ name: android_id
+ sha256: "543bbfcf316de69d3ac36601d74eeaacd0248178a2671b00ad30d09f35bd3581"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "0.5.2+1"
args:
dependency: transitive
description:
diff --git a/webrtc_controller_flutter/pubspec.yaml b/webrtc_controller_flutter/pubspec.yaml
index 55177c5..75eeec4 100644
--- a/webrtc_controller_flutter/pubspec.yaml
+++ b/webrtc_controller_flutter/pubspec.yaml
@@ -48,6 +48,7 @@ dependencies:
device_info_plus: ^11.0.0
protobuf: ^6.0.0
fixnum: ^1.1.1
+ android_id: ^0.5.2+1
dev_dependencies:
flutter_test: