feat(push): 支持解绑手机号与设备策略推送

- 新增 contentType=18 解绑标记,上报 otherInfo 时不再携带 SIM 卡手机号
- 新增 contentType=19 设备策略解析并落库到 MMKV
- 屏幕采集服务改为按需启动,远程会话结束后停止并释放资源
- 增加通知心跳刷新,避免静止画面不推帧导致黑屏
This commit is contained in:
TongTongStudio
2026-08-27 11:37:29 +08:00
parent 86bcc93131
commit e163e4533a
4 changed files with 295 additions and 21 deletions

View File

@@ -73,4 +73,53 @@ public class CommonConfig {
@Deprecated
public static final String CURRENT_LOCATION_ID_KEY = "current_map_location_ID";
/**
* 设备解绑标记1=已解除手机号绑定0=未解绑)。
* <p>
* 由服务端下发「解除绑定」推送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";
}

View File

@@ -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 {
// 工具方法
// ==================================================================
/**
* 是否已解除手机号绑定。
* <p>
* 由服务端「解除绑定」推送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 使用的取值接口,用于统一包裹异常。
*/

View File

@@ -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
* <p>
@@ -776,6 +772,156 @@ public class PushExecutor {
Logger.e(TAG, "screenLock: 收到屏幕锁定推送,功能待实现");
}
/**
* 解除设备手机号绑定contentType = 18, unbindMobile
* <p>
* 由后端 {@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
* <p>
* 由后端在策略全量保存 / 单独设置后推送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 权限。

View File

@@ -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);
}