diff --git a/app/src/main/java/com/ttstd/dialer/config/CommonConfig.java b/app/src/main/java/com/ttstd/dialer/config/CommonConfig.java index cceb0ae..6691560 100644 --- a/app/src/main/java/com/ttstd/dialer/config/CommonConfig.java +++ b/app/src/main/java/com/ttstd/dialer/config/CommonConfig.java @@ -73,4 +73,53 @@ public class CommonConfig { @Deprecated public static final String CURRENT_LOCATION_ID_KEY = "current_map_location_ID"; + /** + * 设备解绑标记(1=已解除手机号绑定,0=未解绑)。 + *

+ * 由服务端下发「解除绑定」推送(contentType = 18)置为 1。 + * 置为 1 后,设备上报 {@code otherInfo} 时不再携带手机号, + * 从而避免服务端把 SIM 卡手机号重新写回 {@code sys_sn.snMobile}。 + */ + public static final String KEY_DEVICE_UNBIND_MOBILE = "device_unbind_mobile_key"; + + // ===================== 设备策略开关(由服务端策略推送 contentType=19 下发) ===================== + /** USB偏好设置(1开 0关) */ + public static final String KEY_POLICY_USB_DATA = "policy_usb_data_key"; + /** 时间设置管控(1开 0关) */ + public static final String KEY_POLICY_TIME_SETTING = "policy_time_setting_key"; + /** 存储卡(1开 0关) */ + public static final String KEY_POLICY_STORAGE_CARD = "policy_storage_card_key"; + /** 恢复出厂(1开 0关) */ + public static final String KEY_POLICY_FACTORY_RESET = "policy_factory_reset_key"; + /** WiFi热点开关(1开 0关) */ + public static final String KEY_POLICY_WIFI_HOTSPOT = "policy_wifi_hotspot_key"; + /** 蓝牙开关(1开 0关) */ + public static final String KEY_POLICY_BLUETOOTH_SWITCH = "policy_bluetooth_switch_key"; + /** 壁纸功能(1开 0关) */ + public static final String KEY_POLICY_WALLPAPER = "policy_wallpaper_key"; + /** 通知栏显示(1开 0关) */ + public static final String KEY_POLICY_NOTIFICATION_BAR = "policy_notification_bar_key"; + /** 状态栏下拉开关(1开 0关) */ + public static final String KEY_POLICY_STATUS_BAR_PULL_DOWN = "policy_status_bar_pull_down_key"; + /** 系统导航条显示(1开 0关) */ + public static final String KEY_POLICY_SYSTEM_NAV_BAR_SHOW = "policy_system_nav_bar_show_key"; + /** 系统导航栏设置(1开 0关) */ + public static final String KEY_POLICY_SYSTEM_NAV_BAR_SETTING = "policy_system_nav_bar_setting_key"; + /** 导航条选项(1开 0关) */ + public static final String KEY_POLICY_NAV_BAR_OPTIONS = "policy_nav_bar_options_key"; + /** 蓝牙功能开关(1开 0关) */ + public static final String KEY_POLICY_BLUETOOTH_FUNCTION = "policy_bluetooth_function_key"; + /** OTA升级管控(1开 0关) */ + public static final String KEY_POLICY_OTA_UPGRADE = "policy_ota_upgrade_key"; + /** 应用安装开关(1开 0关) */ + public static final String KEY_POLICY_APP_INSTALL = "policy_app_install_key"; + /** 自动旋转(1开 0关) */ + public static final String KEY_POLICY_AUTO_ROTATE = "policy_auto_rotate_key"; + /** 自动亮度(1开 0关) */ + public static final String KEY_POLICY_AUTO_BRIGHTNESS = "policy_auto_brightness_key"; + /** 护眼模式(1开 0关) */ + public static final String KEY_POLICY_EYE_PROTECTION = "policy_eye_protection_key"; + /** 深色模式(1开 0关) */ + public static final String KEY_POLICY_DARK_MODE = "policy_dark_mode_key"; + } diff --git a/app/src/main/java/com/ttstd/dialer/mdm/DeviceInfoCollector.java b/app/src/main/java/com/ttstd/dialer/mdm/DeviceInfoCollector.java index af01103..05aa5ad 100644 --- a/app/src/main/java/com/ttstd/dialer/mdm/DeviceInfoCollector.java +++ b/app/src/main/java/com/ttstd/dialer/mdm/DeviceInfoCollector.java @@ -34,9 +34,11 @@ import com.ttstd.dialer.bean.req.SnNetworkInfoReq; import com.ttstd.dialer.bean.req.SnOtherInfoReq; import com.ttstd.dialer.bean.req.SnSecurityInfoReq; import com.ttstd.dialer.bean.req.SnSystemInfoReq; +import com.ttstd.dialer.config.CommonConfig; import com.ttstd.dialer.utils.Logger; import com.ttstd.dialer.utils.SystemUtils; +import com.tencent.mmkv.MMKV; import cn.jpush.android.api.JPushInterface; import java.io.BufferedReader; @@ -625,7 +627,10 @@ public class DeviceInfoCollector { (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); if (tm != null) { req.setIccid(safeString(tm::getSimSerialNumber)); - req.setPhoneNumber(safeString(tm::getLine1Number)); + // 已解除绑定:不再上报 SIM 卡手机号,避免服务端重新写回 sys_sn.snMobile + if (!isUnbindMobile()) { + req.setPhoneNumber(safeString(tm::getLine1Number)); + } } } } catch (Throwable t) { @@ -728,6 +733,22 @@ public class DeviceInfoCollector { // 工具方法 // ================================================================== + /** + * 是否已解除手机号绑定。 + *

+ * 由服务端「解除绑定」推送(contentType = 18)在 {@link CommonConfig#KEY_DEVICE_UNBIND_MOBILE} + * 写入 1。置位后上报 {@code otherInfo} 不再携带 SIM 卡手机号。 + */ + private boolean isUnbindMobile() { + try { + MMKV mmkv = MMKV.mmkvWithID(CommonConfig.MMKV_ID, MMKV.MULTI_PROCESS_MODE); + return mmkv.decodeInt(CommonConfig.KEY_DEVICE_UNBIND_MOBILE, 0) == 1; + } catch (Throwable t) { + Logger.e(TAG, "isUnbindMobile failed: " + t.getMessage()); + return false; + } + } + /** * 供 lambda 使用的取值接口,用于统一包裹异常。 */ diff --git a/app/src/main/java/com/ttstd/dialer/push/PushExecutor.java b/app/src/main/java/com/ttstd/dialer/push/PushExecutor.java index c0fe576..c33d0cf 100644 --- a/app/src/main/java/com/ttstd/dialer/push/PushExecutor.java +++ b/app/src/main/java/com/ttstd/dialer/push/PushExecutor.java @@ -91,6 +91,8 @@ public class PushExecutor { private static final String DEVICE_CLEAN = "15"; private static final String DEVICE_MIRROR = "16"; private static final String DEVICE_SCREEN_LOCK = "17"; + private static final String DEVICE_UNBIND_MOBILE = "18"; + private static final String DEVICE_POLICY = "19"; /** @@ -198,6 +200,12 @@ public class PushExecutor { case DEVICE_SCREEN_LOCK: screenLock(); break; + case DEVICE_UNBIND_MOBILE: + unbindMobile(); + break; + case DEVICE_POLICY: + setDevicePolicy(extra); + break; default: Logger.e(TAG, "setPushMessage: default"); } @@ -710,7 +718,7 @@ public class PushExecutor { return; } - if (acceptKey == 1 || isAutoRemoteAssistanceEnabled()) { + if (acceptKey == 1) { // 自动接收并采集窗口串流(推送指定自动,或本地已开启自动接收开关) Logger.d(TAG, "mirror: 自动接收远程协助,启动采集"); ScreenCaptureActivity.start(mContext); @@ -754,18 +762,6 @@ public class PushExecutor { }); } - /** - * 读取本地"自动接收远程协助"开关(与设置页 siAutoRemoteAssistance 同步)。 - */ - private boolean isAutoRemoteAssistanceEnabled() { - try { - MMKV mmkv = MMKV.mmkvWithID("MMKV_DEFAULT"); - return mmkv != null && mmkv.decodeBool("AutoRemoteAssistanceEnable", false); - } catch (Exception e) { - return false; - } - } - /** * 屏幕锁定(contentType = 17, screenLock)。 *

@@ -776,6 +772,156 @@ public class PushExecutor { Logger.e(TAG, "screenLock: 收到屏幕锁定推送,功能待实现"); } + /** + * 解除设备手机号绑定(contentType = 18, unbindMobile)。 + *

+ * 由后端 {@code DeviceService.unbindMobile} 下发。收到后在本地标记「已解绑」, + * 之后上报 {@code otherInfo} 不再携带 SIM 卡手机号,避免服务端重新建立绑定。 + */ + private void unbindMobile() { + Logger.e(TAG, "unbindMobile: 收到解除绑定推送"); + try { + mMMKV.encode(CommonConfig.KEY_DEVICE_UNBIND_MOBILE, 1); + Logger.e(TAG, "unbindMobile: 已本地标记解除绑定"); + } catch (Throwable t) { + Logger.e(TAG, "unbindMobile failed: " + t.getMessage()); + } + } + + /** + * 设置设备策略开关(contentType = 19, policy)。 + *

+ * 由后端在策略全量保存 / 单独设置后推送,extra 承载全部策略开关状态(0关闭 1开启)。 + * 解析后写入本地 MMKV,并对已有设备控制能力的开关即时应用;暂无能力的开关先落库并记录日志。 + */ + private void setDevicePolicy(String extra) { + Logger.e(TAG, "setDevicePolicy: 收到策略推送, extra=" + extra); + if (TextUtils.isEmpty(extra)) { + Logger.e(TAG, "setDevicePolicy: extra 为空"); + return; + } + try { + JsonObject json = JsonParser.parseString(extra).getAsJsonObject(); + DeviceManagerService mdm = DeviceManagerService.getInstance(); + + // USB偏好设置 + int usbData = getIntField(json, "usbData", 1); + mMMKV.encode(CommonConfig.KEY_POLICY_USB_DATA, usbData); + + // 时间设置管控 + int timeSetting = getIntField(json, "timeSetting", 1); + mMMKV.encode(CommonConfig.KEY_POLICY_TIME_SETTING, timeSetting); + + // 存储卡 + int storageCard = getIntField(json, "storageCard", 1); + mMMKV.encode(CommonConfig.KEY_POLICY_STORAGE_CARD, storageCard); + + // 恢复出厂 + int factoryReset = getIntField(json, "factoryReset", 1); + mMMKV.encode(CommonConfig.KEY_POLICY_FACTORY_RESET, factoryReset); + + // WiFi热点开关 + int wifiHotspot = getIntField(json, "wifiHotspot", 1); + mMMKV.encode(CommonConfig.KEY_POLICY_WIFI_HOTSPOT, wifiHotspot); + + // 蓝牙开关 + int bluetoothSwitch = getIntField(json, "bluetoothSwitch", 1); + mMMKV.encode(CommonConfig.KEY_POLICY_BLUETOOTH_SWITCH, bluetoothSwitch); + + // 壁纸功能 + int wallpaper = getIntField(json, "wallpaper", 1); + mMMKV.encode(CommonConfig.KEY_POLICY_WALLPAPER, wallpaper); + + // 通知栏显示 + int notificationBar = getIntField(json, "notificationBar", 1); + mMMKV.encode(CommonConfig.KEY_POLICY_NOTIFICATION_BAR, notificationBar); + + // 状态栏下拉开关 + int statusBarPullDown = getIntField(json, "statusBarPullDown", 1); + mMMKV.encode(CommonConfig.KEY_POLICY_STATUS_BAR_PULL_DOWN, statusBarPullDown); + + // 系统导航条显示 + int systemNavBarShow = getIntField(json, "systemNavBarShow", 1); + mMMKV.encode(CommonConfig.KEY_POLICY_SYSTEM_NAV_BAR_SHOW, systemNavBarShow); + + // 系统导航栏设置 + int systemNavBarSetting = getIntField(json, "systemNavBarSetting", 1); + mMMKV.encode(CommonConfig.KEY_POLICY_SYSTEM_NAV_BAR_SETTING, systemNavBarSetting); + + // 导航条选项 + int navBarOptions = getIntField(json, "navBarOptions", 1); + mMMKV.encode(CommonConfig.KEY_POLICY_NAV_BAR_OPTIONS, navBarOptions); + + // 蓝牙功能开关 + int bluetoothFunction = getIntField(json, "bluetoothFunction", 1); + mMMKV.encode(CommonConfig.KEY_POLICY_BLUETOOTH_FUNCTION, bluetoothFunction); + + // OTA升级管控 + int otaUpgrade = getIntField(json, "otaUpgrade", 1); + mMMKV.encode(CommonConfig.KEY_POLICY_OTA_UPGRADE, otaUpgrade); + + // 应用安装开关(对应 deviceController.installPackage / uninstallPackage 的放行控制) + int appInstall = getIntField(json, "appInstall", 1); + mMMKV.encode(CommonConfig.KEY_POLICY_APP_INSTALL, appInstall); + + // 自动旋转 + int autoRotate = getIntField(json, "autoRotate", 1); + mMMKV.encode(CommonConfig.KEY_POLICY_AUTO_ROTATE, autoRotate); + + // 自动亮度 + int autoBrightness = getIntField(json, "autoBrightness", 1); + mMMKV.encode(CommonConfig.KEY_POLICY_AUTO_BRIGHTNESS, autoBrightness); + + // 护眼模式 + int eyeProtection = getIntField(json, "eyeProtection", 1); + mMMKV.encode(CommonConfig.KEY_POLICY_EYE_PROTECTION, eyeProtection); + + // 深色模式 + int darkMode = getIntField(json, "darkMode", 1); + mMMKV.encode(CommonConfig.KEY_POLICY_DARK_MODE, darkMode); + + Logger.e(TAG, "setDevicePolicy: 策略解析完成并落库, usbData=" + usbData + + ", timeSetting=" + timeSetting + + ", storageCard=" + storageCard + + ", factoryReset=" + factoryReset + + ", wifiHotspot=" + wifiHotspot + + ", bluetoothSwitch=" + bluetoothSwitch + + ", wallpaper=" + wallpaper + + ", notificationBar=" + notificationBar + + ", statusBarPullDown=" + statusBarPullDown + + ", systemNavBarShow=" + systemNavBarShow + + ", systemNavBarSetting=" + systemNavBarSetting + + ", navBarOptions=" + navBarOptions + + ", bluetoothFunction=" + bluetoothFunction + + ", otaUpgrade=" + otaUpgrade + + ", appInstall=" + appInstall + + ", autoRotate=" + autoRotate + + ", autoBrightness=" + autoBrightness + + ", eyeProtection=" + eyeProtection + + ", darkMode=" + darkMode); + + // TODO: 针对各策略开关的 OS 级能力(导航栏/通知栏/WiFi热点/蓝牙/壁纸等)已具备落地数据, + // 接入对应 DeviceManagerService / DeviceController 能力后在此即时应用。 + + } catch (Exception e) { + Logger.e(TAG, "setDevicePolicy: 解析策略失败: " + e.getMessage()); + } + } + + /** + * 从推送 extra JSON 中读取整型字段;字段缺失或解析失败时返回默认值。 + */ + private int getIntField(JsonObject json, String field, int defaultValue) { + try { + if (json.has(field)) { + return json.get(field).getAsInt(); + } + } catch (Exception e) { + Logger.e(TAG, "getIntField: " + field + " 解析失败: " + e.getMessage()); + } + return defaultValue; + } + /** * 重启(重启系统)(contentType = 12, reboot)。 * 通过 {@code reboot} 命令实现,需要设备具备 root / shell 权限。 diff --git a/app/src/main/java/com/ttstd/dialer/webrtc/service/ScreenCaptureService.java b/app/src/main/java/com/ttstd/dialer/webrtc/service/ScreenCaptureService.java index 48e2391..662d4e9 100644 --- a/app/src/main/java/com/ttstd/dialer/webrtc/service/ScreenCaptureService.java +++ b/app/src/main/java/com/ttstd/dialer/webrtc/service/ScreenCaptureService.java @@ -281,7 +281,8 @@ public class ScreenCaptureService extends Service { } if (isInitialized) { - return START_STICKY; + // 服务已在运行:不自动重启(远程控制会话结束后应保持停止,等待下一次命令拉起) + return START_NOT_STICKY; } int resultCode = (sResultCode != Activity.RESULT_CANCELED) @@ -301,7 +302,9 @@ public class ScreenCaptureService extends Service { isInitialized = true; startScreenCapture(resultCode, resultData); - return START_STICKY; + // 按需启动:远程控制会话结束(onControllerDisconnected)即 stopSelf, + // 且不随系统重启自动复活,避免空转占用前台服务与 MediaProjection。 + return START_NOT_STICKY; } private void startScreenCapture(int resultCode, Intent resultData) { @@ -365,6 +368,11 @@ public class ScreenCaptureService extends Service { }); webRtcClient.setVideoCapturer(screenCapturer, width, height, fps); + + // 启动心脏起搏器:周期刷新前台通知,强制 MediaProjection 持续产帧, + // 避免静止画面(无触摸/无变化)下不推帧导致控制端黑屏。 + mainHandler.removeCallbacks(heartbeatRunnable); + mainHandler.post(heartbeatRunnable); } /** 解析信令服务器地址(ws:// 或 wss://)。 */ @@ -496,9 +504,16 @@ public class ScreenCaptureService extends Service { private void onSignalServerDisconnected() { if (isShuttingDown) return; - Log.i(TAG, "Signal server disconnected, will auto-reconnect"); - updateNotification("信令断开,正在重连..."); + Log.i(TAG, "Signal server disconnected"); if (stateListener != null) stateListener.onSignalDisconnected(); + // 信令已断开:若当前无控制端在观看,采集服务已无意义,直接停止; + // 全局信令单例会自行重连,待下次命令再拉起本服务。 + if (controllingName == null || controllingName.isEmpty()) { + Log.i(TAG, "No active controller, stopping screen capture service"); + stopSelf(); + } else { + updateNotification("信令断开,正在重连..."); + } } private void onControllerDisconnected(String controllerId) { @@ -506,9 +521,11 @@ public class ScreenCaptureService extends Service { String name = (controllingName != null && !controllingName.isEmpty()) ? controllingName : controllerId; Log.i(TAG, "Remote controller disconnected: " + name); - updateNotification("远程控制服务正在运行"); if (stateListener != null) stateListener.onControllerDisconnected(name); controllingName = null; + // 远程控制会话已结束:停止前台采集服务,释放 MediaProjection / WebRTC 等资源。 + // 全局信令 WebSocket 由 siRemoteAssistance 开关控制,不受本服务停止影响。 + stopSelf(); } private void sendConnectionRejected(String controllerId) { @@ -717,6 +734,8 @@ public class ScreenCaptureService extends Service { public void onDestroy() { super.onDestroy(); isShuttingDown = true; + // 停止心脏起搏器 + mainHandler.removeCallbacks(heartbeatRunnable); sResultCode = Activity.RESULT_CANCELED; sResultData = null; if (instance == this) instance = null; @@ -754,6 +773,40 @@ public class ScreenCaptureService extends Service { manager.notify(NOTIFICATION_ID, createNotification(contentText)); } + /** + * 心脏起搏器:周期刷新前台通知,强制 MediaProjection 持续产帧, + * 解决静止画面(无触摸/无变化的屏幕)下 MediaProjection 不推帧、 + * 控制端看到黑屏或"等待画面"的问题。 + * + * 优化点(避免打扰用户): + * - 通知渠道为 IMPORTANCE_LOW + setSilent(true):周期刷新不响铃/不振铃/不弹横幅。 + * - 仅在「有远端控制端正在观看」时(controllingName != null)才刷新通知扰动推帧; + * 空闲(无控制端连接)时不扰动、也不反复刷新,既省电又安静。 + * - 每 1 秒刷新一次通知文案(叠加动态圆点),状态栏变化即触发采集管线产出一帧。 + */ + private final Runnable heartbeatRunnable = new Runnable() { + @Override + public void run() { + if (!isShuttingDown) { + updateNotificationHeartbeat(); + mainHandler.postDelayed(this, 1000); + } + } + }; + + private int heartbeatCount = 0; + + private void updateNotificationHeartbeat() { + // 空闲时(无控制端在观看)不做任何扰动,避免无谓刷新/打扰。 + if (controllingName == null || controllingName.isEmpty()) { + return; + } + StringBuilder dots = new StringBuilder(); + for (int i = 0; i < (heartbeatCount % 3) + 1; i++) dots.append("."); + heartbeatCount++; + updateNotification("远程控制服务运行中" + dots); + } + private Notification createNotification() { return createNotification("远程控制服务正在运行"); } @@ -768,16 +821,21 @@ public class ScreenCaptureService extends Service { .setContentText(contentText) .setSmallIcon(android.R.drawable.ic_menu_camera) .setContentIntent(pendingIntent) - .setPriority(NotificationCompat.PRIORITY_HIGH) + // 静默:即便周期刷新也不响铃/不振铃/不弹横幅(setOnlyAlertOnce 保证重复 notify 不再提醒) + .setSilent(true) + .setOnlyAlertOnce(true) .setOngoing(true) .build(); } private void createNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // IMPORTANCE_LOW:静默通知,不响铃/不振铃/不弹横幅,仅在通知栏安静展示, + // 避免"心脏起搏器"周期刷新通知时反复打扰用户。 NotificationChannel channel = new NotificationChannel( - CHANNEL_ID, "屏幕共享服务", NotificationManager.IMPORTANCE_DEFAULT); + CHANNEL_ID, "屏幕共享服务", NotificationManager.IMPORTANCE_LOW); channel.setDescription("远程控制屏幕采集前台服务"); + channel.setShowBadge(false); NotificationManager manager = getSystemService(NotificationManager.class); manager.createNotificationChannel(channel); }