From 45ee9ef60cf8a8f20d285b9e67f7bf65335f45da Mon Sep 17 00:00:00 2001 From: tongtongstudio Date: Thu, 16 Jul 2026 21:56:12 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=A2=AB=E6=8E=A7=E7=AB=AF=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E6=97=A0=E9=9A=9C=E7=A2=8D=E6=A3=80=E6=9F=A5=EF=BC=8C?= =?UTF-8?q?flutter=E5=9B=BA=E5=AE=9A=E8=8E=B7=E5=8F=96Android=5FId?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/src/main/AndroidManifest.xml | 13 ++ .../AccessibilityServiceHelper.java | 82 +++++++++++ .../KeyboardAccessibilityService.java | 112 +++++++++++++++ .../activity/main/MainActivity.java | 50 ++++++- .../input/AccessibilityInputUtils.java | 106 ++++++++++++++ .../controlled/input/InputCommandHandler.java | 14 +- .../service/ScreenCaptureService.java | 27 +++- .../app/src/main/res/layout/activity_main.xml | 134 +++++++++--------- .../app/src/main/res/values/strings.xml | 6 + .../res/xml/accessibility_service_config.xml | 8 ++ .../lib/utils/device_utils.dart | 17 ++- webrtc_controller_flutter/pubspec.lock | 8 ++ webrtc_controller_flutter/pubspec.yaml | 1 + 13 files changed, 506 insertions(+), 72 deletions(-) create mode 100644 WebRTCControlled/app/src/main/java/com/ttstd/controlled/accessibility/AccessibilityServiceHelper.java create mode 100644 WebRTCControlled/app/src/main/java/com/ttstd/controlled/accessibility/KeyboardAccessibilityService.java create mode 100644 WebRTCControlled/app/src/main/java/com/ttstd/controlled/input/AccessibilityInputUtils.java create mode 100644 WebRTCControlled/app/src/main/res/xml/accessibility_service_config.xml diff --git a/WebRTCControlled/app/src/main/AndroidManifest.xml b/WebRTCControlled/app/src/main/AndroidManifest.xml index 4dafa1a..66bb3a1 100644 --- a/WebRTCControlled/app/src/main/AndroidManifest.xml +++ b/WebRTCControlled/app/src/main/AndroidManifest.xml @@ -35,6 +35,19 @@ android:exported="false" android:foregroundServiceType="mediaProjection" /> + + + + + + + + diff --git a/WebRTCControlled/app/src/main/java/com/ttstd/controlled/accessibility/AccessibilityServiceHelper.java b/WebRTCControlled/app/src/main/java/com/ttstd/controlled/accessibility/AccessibilityServiceHelper.java new file mode 100644 index 0000000..6b6701f --- /dev/null +++ b/WebRTCControlled/app/src/main/java/com/ttstd/controlled/accessibility/AccessibilityServiceHelper.java @@ -0,0 +1,82 @@ +package com.ttstd.controlled.accessibility; + +import android.accessibilityservice.AccessibilityServiceInfo; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.provider.Settings; +import android.util.Log; +import android.view.accessibility.AccessibilityManager; + +import java.util.List; + +public class AccessibilityServiceHelper { + private static final String TAG = "AccessibilityHelper"; + + /** 检查本应用在系统设置中是否已开启该无障碍服务。 */ + public static boolean isAccessibilityEnabled(Context context) { + String enabled = Settings.Secure.getString( + context.getContentResolver(), Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES); + if (enabled == null || enabled.isEmpty()) { + return false; + } + String component = context.getPackageName() + "/" + KeyboardAccessibilityService.class.getName(); + return enabled.contains(component); + } + /** + * 检查特定的无障碍服务是否已启用 + * + * @param context 上下文对象 + * @param serviceClass 你的无障碍服务类(例如 MyAccessibilityService.class) + * @return true 已开启,false 未开启 + */ + public static boolean isAccessibilityServiceEnabled(Context context, Class serviceClass) { + // 获取 AccessibilityManager 系统服务 + AccessibilityManager am = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); + if (am == null) { + return false; + } + + // 获取所有已启用的无障碍服务列表 + List 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"> - + - + - + -