feat: 被控端增加无障碍检查,flutter固定获取Android_Id
This commit is contained in:
@@ -35,6 +35,19 @@
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="mediaProjection" />
|
||||
|
||||
<!-- 键盘/输入模拟无障碍服务(无需系统签名/root,用于模拟全局按键与手势) -->
|
||||
<service
|
||||
android:name=".accessibility.KeyboardAccessibilityService"
|
||||
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.accessibilityservice.AccessibilityService" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.accessibilityservice"
|
||||
android:resource="@xml/accessibility_service_config" />
|
||||
</service>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -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<AccessibilityServiceInfo> 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* 键盘无障碍服务。
|
||||
* <p>
|
||||
* 用于在应用没有系统签名、也没有 root 权限时,通过无障碍能力模拟系统级全局按键。
|
||||
* 具体能力:
|
||||
* - {@link #pressKey(int)}:将部分 KeyEvent 按键码映射为 AccessibilityService 的全局动作
|
||||
* (返回 / 主页 / 多任务 / 通知 / 音量等),通过 {@link #performGlobalAction(int)} 触发。
|
||||
* - {@link #dispatchGesture(GestureDescription, ...)}(继承自父类):供
|
||||
* {@code AccessibilityInputUtils} 模拟触摸/滑动/长按等手势。
|
||||
* <p>
|
||||
* 注意:无障碍服务无法直接模拟任意字符按键(如字母、数字),只能触发上述全局动作。
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<MainViewModel, ActivityMainBi
|
||||
private boolean isBound = false;
|
||||
private boolean isLocalViewInitialized = false;
|
||||
|
||||
/** 是否为特权应用(系统签名或无 root 权限),缓存后避免重复在主线程执行 su 检测。 */
|
||||
private boolean isPrivileged = false;
|
||||
private AlertDialog accessibilityDialog;
|
||||
|
||||
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
@@ -67,7 +75,7 @@ public class MainActivity extends BaseMvvmActivity<MainViewModel, ActivityMainBi
|
||||
// 默认服务器地址
|
||||
binding.etServerUrl.setText("ws://175.178.213.60:8088/ws/signal");
|
||||
// 生成设备ID
|
||||
binding.etDeviceId.setText(DeviceUtils.getSystemSerialNumber());
|
||||
// binding.etDeviceId.setText(DeviceUtils.getSystemSerialNumber());
|
||||
|
||||
binding.btnStart.setOnClickListener(v -> startScreenSharing());
|
||||
binding.btnStop.setOnClickListener(v -> stopScreenSharing());
|
||||
@@ -84,9 +92,45 @@ public class MainActivity extends BaseMvvmActivity<MainViewModel, ActivityMainBi
|
||||
Log.e(TAG, "initData: isSystemUid = " + SignatureUtils.isSystemUid());
|
||||
Log.e(TAG, "initData: isDeviceRooted = " + SignatureUtils.isDeviceRooted());
|
||||
Log.e(TAG, "initData: isAppHasRootPermission = " + SignatureUtils.isAppHasRootPermission());
|
||||
|
||||
// 特权应用判定:系统签名(含共享系统 UID / 系统 UID)或已获取 Root 权限。
|
||||
// 特权应用无需依赖无障碍服务即可模拟输入,缓存结果避免每次 onResume 在主线程检测 su。
|
||||
boolean systemSigned = SignatureUtils.isSystemSignature(this)
|
||||
|| SignatureUtils.isSharedSystemUid(this)
|
||||
|| SignatureUtils.isSystemUid();
|
||||
boolean rooted = SignatureUtils.isDeviceRooted() && SignatureUtils.isAppHasRootPermission();
|
||||
isPrivileged = systemSigned || rooted;
|
||||
Log.e(TAG, "initData: isPrivileged = " + isPrivileged);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
}
|
||||
|
||||
/** 弹窗提示开启无障碍服务;同一运行周期内仅提示一次。 */
|
||||
private void showAccessibilityPermissionDialog() {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setTitle(R.string.accessibility_dialog_title)
|
||||
.setMessage(R.string.accessibility_dialog_message)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.accessibility_dialog_go, (dialog, which) -> {
|
||||
AccessibilityServiceHelper.jumpToAccessibilitySettings(this);
|
||||
})
|
||||
.setNegativeButton(R.string.accessibility_dialog_cancel, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
});
|
||||
accessibilityDialog = builder.create();
|
||||
accessibilityDialog.show();
|
||||
}
|
||||
|
||||
private void startScreenSharing() {
|
||||
// 非特权应用且未开启无障碍服务时,弹窗提示用户前往系统设置开启。
|
||||
if (!isPrivileged && !AccessibilityServiceHelper.isAccessibilityEnabled(this)) {
|
||||
showAccessibilityPermissionDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查通知权限 (Android 13+)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
|
||||
@@ -185,6 +229,10 @@ public class MainActivity extends BaseMvvmActivity<MainViewModel, ActivityMainBi
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (accessibilityDialog != null && accessibilityDialog.isShowing()) {
|
||||
accessibilityDialog.dismiss();
|
||||
accessibilityDialog = null;
|
||||
}
|
||||
if (isBound) {
|
||||
if (screenCaptureService != null) {
|
||||
screenCaptureService.setStateListener(null);
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.ttstd.controlled.input;
|
||||
|
||||
import android.accessibilityservice.GestureDescription;
|
||||
import android.graphics.Path;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import com.ttstd.controlled.accessibility.KeyboardAccessibilityService;
|
||||
|
||||
/**
|
||||
* 基于无障碍服务(AccessibilityService)的输入执行器。
|
||||
* <p>
|
||||
* 适用于没有系统签名、也没有 root 权限的普通应用:
|
||||
* - 触摸 / 滑动 / 长按 / 连续手势:通过 AccessibilityService.dispatchGesture 模拟;
|
||||
* - 按键:通过 KeyboardAccessibilityService.pressKey 模拟系统级全局按键。
|
||||
* <p>
|
||||
* 注意:无障碍无法模拟任意字符按键,仅支持返回 / 主页 / 多任务 / 通知 / 音量等全局动作。
|
||||
*/
|
||||
public class AccessibilityInputUtils implements InputExecutor {
|
||||
|
||||
private static final String TAG = "AccessibilityInputUtils";
|
||||
|
||||
// 连续手势(MOTION_EVENT)的累积路径
|
||||
private Path motionPath;
|
||||
private long motionStartTime;
|
||||
|
||||
private KeyboardAccessibilityService getService() {
|
||||
return KeyboardAccessibilityService.getInstance();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void injectTap(int x, int y) {
|
||||
Path path = new Path();
|
||||
path.moveTo(x, y);
|
||||
// 同一点、短时长即模拟“按下-抬起”的点击
|
||||
dispatch(path, 1, 50);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void injectLongPress(int x, int y) {
|
||||
Path path = new Path();
|
||||
path.moveTo(x, y);
|
||||
dispatch(path, 1, 1000);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void injectSwipe(int x1, int y1, int x2, int y2, long duration) {
|
||||
Path path = new Path();
|
||||
path.moveTo(x1, y1);
|
||||
path.lineTo(x2, y2);
|
||||
dispatch(path, 0, Math.max(50, duration));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void injectKeyEvent(int keyCode) {
|
||||
KeyboardAccessibilityService svc = getService();
|
||||
if (svc == null) {
|
||||
Log.w(TAG, "Accessibility service not running, cannot inject key");
|
||||
return;
|
||||
}
|
||||
boolean handled = svc.pressKey(keyCode);
|
||||
if (!handled) {
|
||||
Log.w(TAG, "Key code " + keyCode + " is not supported via accessibility");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void injectMotionEvent(int action, int x, int y) {
|
||||
switch (action) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
motionPath = new Path();
|
||||
motionPath.moveTo(x, y);
|
||||
motionStartTime = SystemClock.uptimeMillis();
|
||||
break;
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
if (motionPath != null) {
|
||||
motionPath.lineTo(x, y);
|
||||
}
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
if (motionPath != null) {
|
||||
long duration = Math.max(1, SystemClock.uptimeMillis() - motionStartTime);
|
||||
dispatch(motionPath, 0, duration);
|
||||
motionPath = null;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
Log.w(TAG, "Unknown motion action: " + action);
|
||||
}
|
||||
}
|
||||
|
||||
private void dispatch(Path path, long startOffset, long duration) {
|
||||
KeyboardAccessibilityService svc = getService();
|
||||
if (svc == null) {
|
||||
Log.w(TAG, "Accessibility service not running, cannot dispatch gesture");
|
||||
return;
|
||||
}
|
||||
GestureDescription.Builder builder = new GestureDescription.Builder();
|
||||
builder.addStroke(new GestureDescription.StrokeDescription(path, startOffset, duration));
|
||||
boolean result = svc.dispatchGesture(builder.build(), null, null);
|
||||
if (!result) {
|
||||
Log.w(TAG, "dispatchGesture failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,10 +19,20 @@ public class InputCommandHandler {
|
||||
private final InputExecutor inputExecutor;
|
||||
|
||||
public InputCommandHandler(int screenWidth, int screenHeight) {
|
||||
this(screenWidth, screenHeight, new SystemInputUtils());
|
||||
}
|
||||
|
||||
/**
|
||||
* 允许调用方根据权限/环境选择具体的输入执行器。
|
||||
*
|
||||
* @param screenWidth 真实屏幕宽度(用于坐标映射)
|
||||
* @param screenHeight 真实屏幕高度(用于坐标映射)
|
||||
* @param inputExecutor 输入执行器(系统注入 / Shell / 无障碍 等)
|
||||
*/
|
||||
public InputCommandHandler(int screenWidth, int screenHeight, InputExecutor inputExecutor) {
|
||||
this.screenWidth = screenWidth;
|
||||
this.screenHeight = screenHeight;
|
||||
// 使用系统签名权限下的 SystemInputUtils 进行事件注入
|
||||
this.inputExecutor = new SystemInputUtils();
|
||||
this.inputExecutor = inputExecutor;
|
||||
}
|
||||
|
||||
public void handleCommand(byte[] data) {
|
||||
|
||||
@@ -28,7 +28,13 @@ import androidx.core.app.NotificationCompat;
|
||||
import com.ttstd.controlled.R;
|
||||
import com.ttstd.controlled.activity.connection.ConnectionRequestActivity;
|
||||
import com.ttstd.controlled.activity.main.MainActivity;
|
||||
import com.ttstd.controlled.accessibility.KeyboardAccessibilityService;
|
||||
import com.ttstd.controlled.input.AccessibilityInputUtils;
|
||||
import com.ttstd.controlled.input.InputCommandHandler;
|
||||
import com.ttstd.controlled.input.InputExecutor;
|
||||
import com.ttstd.controlled.input.ShellInputUtils;
|
||||
import com.ttstd.controlled.input.SystemInputUtils;
|
||||
import com.ttstd.controlled.utils.SignatureUtils;
|
||||
import com.ttstd.controlled.signaling.SignalMessage;
|
||||
import com.ttstd.controlled.signaling.WebSocketClient;
|
||||
import com.ttstd.controlled.webrtc.WebRtcClient;
|
||||
@@ -153,13 +159,32 @@ public class ScreenCaptureService extends Service {
|
||||
Log.i(TAG, "Capture resolution: " + captureWidth + "x" + captureHeight + " @ " + fps + "fps");
|
||||
|
||||
// 输入处理器必须使用真实分辨率进行坐标映射
|
||||
inputHandler = new InputCommandHandler(realWidth, realHeight);
|
||||
inputHandler = new InputCommandHandler(realWidth, realHeight, createInputExecutor());
|
||||
|
||||
// 开启屏幕捕获,使用计算出的 capture 分辨率
|
||||
startScreenCapture(resultCode, resultData, serverUrl, deviceId, captureWidth, captureHeight, fps);
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据当前应用权限/环境选择最合适的输入执行器:
|
||||
* 1. 系统签名应用优先使用隐藏 API 注入(支持精确触摸与任意按键);
|
||||
* 2. 已开启无障碍服务时使用 AccessibilityInputUtils(无需 root/系统签名);
|
||||
* 3. 兜底使用 root shell 方式。
|
||||
*/
|
||||
private InputExecutor createInputExecutor() {
|
||||
if (SignatureUtils.isSystemSignature(this) || SignatureUtils.isSharedSystemUid(this)) {
|
||||
Log.i(TAG, "Input executor: SystemInputUtils (system signature)");
|
||||
return new SystemInputUtils();
|
||||
}
|
||||
if (KeyboardAccessibilityService.getInstance() != null) {
|
||||
Log.i(TAG, "Input executor: AccessibilityInputUtils (accessibility enabled)");
|
||||
return new AccessibilityInputUtils();
|
||||
}
|
||||
Log.i(TAG, "Input executor: ShellInputUtils (fallback)");
|
||||
return new ShellInputUtils();
|
||||
}
|
||||
|
||||
private final IBinder binder = new LocalBinder();
|
||||
|
||||
public class LocalBinder extends Binder {
|
||||
|
||||
@@ -4,78 +4,84 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:context=".activity.main.MainActivity">
|
||||
|
||||
<LinearLayout
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp">
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:text="WebRTC 被控端"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="信令服务器地址:"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/et_server_url"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:hint="ws://175.178.213.60:8088/ws/signal"
|
||||
android:inputType="textUri" />
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="设备ID:"
|
||||
android:textSize="14sp" />
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:text="WebRTC 被控端"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/et_device_id"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:hint="设备ID"
|
||||
android:inputType="text"
|
||||
android:text="981964879" />
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="信令服务器地址:"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_status"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:text="状态: 已停止"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
<EditText
|
||||
android:id="@+id/et_server_url"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:hint="ws://175.178.213.60:8088/ws/signal"
|
||||
android:inputType="textUri" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_start"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="开始屏幕共享" />
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="设备ID:"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_stop"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:enabled="false"
|
||||
android:text="停止屏幕共享" />
|
||||
<EditText
|
||||
android:id="@+id/et_device_id"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:hint="设备ID"
|
||||
android:inputType="text"
|
||||
android:text="981964879" />
|
||||
|
||||
<org.webrtc.SurfaceViewRenderer
|
||||
android:id="@+id/local_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
<TextView
|
||||
android:id="@+id/tv_status"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:text="状态: 已停止"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
</LinearLayout>
|
||||
<Button
|
||||
android:id="@+id/btn_start"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="开始屏幕共享" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_stop"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:enabled="false"
|
||||
android:text="停止屏幕共享" />
|
||||
|
||||
<org.webrtc.SurfaceViewRenderer
|
||||
android:id="@+id/local_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="500dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</layout>
|
||||
|
||||
@@ -7,4 +7,10 @@
|
||||
<string name="connection_reject">拒绝</string>
|
||||
<string name="signal_disconnected_message">与信令服务器断开,屏幕共享已停止</string>
|
||||
<string name="controller_disconnected_message">控制端(%s)已断开远程控制</string>
|
||||
<string name="accessibility_service_description">用于远程控制时模拟键盘与触摸输入。开启后,控制端可通过本服务向本机发送返回、主页、音量等系统按键及触摸手势。</string>
|
||||
<string name="accessibility_hint">未使用系统签名:如需在本机模拟键盘/触摸,请在系统设置中开启本应用的无障碍服务。</string>
|
||||
<string name="accessibility_dialog_title">需要开启无障碍服务</string>
|
||||
<string name="accessibility_dialog_message">当前应用未使用系统签名,也未获取 Root 权限。如需在远程控制时模拟键盘与触摸输入,请前往系统设置开启本应用的无障碍服务。</string>
|
||||
<string name="accessibility_dialog_go">去开启</string>
|
||||
<string name="accessibility_dialog_cancel">取消</string>
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:description="@string/accessibility_service_description"
|
||||
android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged"
|
||||
android:accessibilityFeedbackType="feedbackGeneric"
|
||||
android:notificationTimeout="100"
|
||||
android:canPerformGestures="true"
|
||||
android:canRetrieveWindowContent="false" />
|
||||
@@ -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<String> 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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user