消息与联系人同步
消息与联系人同步是健康助手 App 将手机端应用通知(微信、QQ、短信等)与联系人信息同步到已连接手表/手环设备的能力。本文档基于 HealthAide_V1.1.0_SDK_V1.14.0 模块中的 UI 层、ViewModel、通知辅助类与演示代码,说明消息同步从开关配置、通知捕获到蓝牙下发的完整机制。
Purpose and Scope
本页面覆盖"消息与联系人同步"这一能力在 App 端的完整实现链路:
- 消息同步设置页(
MessageSyncFragment)的用户交互与状态管理; MessageSyncViewModel对NotificationHelper单例的封装与读写接口;- 通过
WatchManager.pushMessageInfo / removeMessageInfo将NotificationMsg下发到设备的协议流程; - 关键常量(App 包名)与演示代码。
以下主题属于兄弟页面,本文不展开:设备蓝牙连接管理(见"设备连接"相关页面)、AI 云端消息历史(AICloudHistoryMessage* 系列,属于 AI 云消息独立能力)、通知权限的系统级申请流程。联系人数据本身在当前源码中以消息内容字符串(如"帅小伙:[1条] ...")的方式内嵌于 NotificationMsg,并未发现独立的联系人实体持久化,故本文将其作为消息载荷的一部分说明。
Overview
智能手表/手环屏幕有限,无法直接运行微信、QQ 等应用。因此 App 通过 Android 通知监听机制捕获手机上已安装应用的通知,再经 JL RCSP 蓝牙协议把消息标题、内容、时间、来源 App 等字段同步到设备端,让用户在手腕上即可阅读和撤销消息。
从代码可以看出该能力分为三层职责:
- 设置层(UI):
MessageSyncFragment提供总开关(消息同步)与四个子开关(微信、QQ、短信、其他应用),并通过setCheckedImmediatelyNoEvent在初始化时回显已保存状态。 - 状态层(ViewModel):
MessageSyncViewModel继承自WatchViewModel,将 UI 与NotificationHelper单例解耦,提供isOpenMessageSync、isSyncWeChat、addAppPackage等薄封装方法。 - 执行层(Helper + Watch):
NotificationHelper管理"是否开启通知同步、监听哪些包名、是否允许其他应用";WatchManager负责把NotificationMsg经蓝牙写入设备,并通过OnWatchOpCallback回调成败。
消息的增删通过 NotificationMsg.op 字段区分:op = 0 表示推送/新增一条消息,op = 1 表示撤销一条消息。这种"以撤销补偿推送"的设计,使设备端可以基于 appName + time 等字段做消息去重与撤回展示。
Architecture
下图展示了消息同步功能的分层架构与调用关系(节点名均取自实际源码类名)。
flowchart TD
subgraph sg_UI["UI 层 (ui/device/more)"]
MessageSyncFragment["MessageSyncFragment"]
MessageSyncViewModel["MessageSyncViewModel"]
end
subgraph sg_Notify["通知状态层 (tool/notification)"]
NotificationHelper["NotificationHelper<br/>(单例)"]
end
subgraph sg_Watch["设备操作层 (tool/watch)"]
WatchManager["WatchManager"]
end
subgraph sg_SDK["JL RCSP SDK 模型"]
NotificationMsg["NotificationMsg"]
OnWatchOpCallback["OnWatchOpCallback"]
end
subgraph sg_Device["蓝牙设备"]
WatchDevice["手表 / 手环"]
end
MessageSyncFragment -->|"开关事件 / 状态回显"| MessageSyncViewModel
MessageSyncViewModel -->|"查询 / 增删包名 / 总开关"| NotificationHelper
NotificationHelper -->|"isEnableNotification / getPackageObserverList"| WatchManager
NotificationHelper -->|"构建 NotificationMsg"| NotificationMsg
WatchManager -->|"pushMessageInfo / removeMessageInfo"| NotificationMsg
WatchManager -->|"蓝牙 RCSP 协议下发"| WatchDevice
WatchManager -->|"onSuccess / onFailed"| OnWatchOpCallback
各层职责说明:
- MessageSyncFragment:界面容器。四个
Switch的监听器直接调用 ViewModel 的对应方法;同时观察mConnectionDataMLD,一旦设备断开连接(非CONNECT_STATE_CONNECTED)立即关闭页面,避免在无设备状态下做无意义的配置。 - MessageSyncViewModel:薄封装层。所有方法均委托给
NotificationHelper.getInstance(),本身不持有业务状态,是"UI ↔ Helper"之间的翻译层。 - NotificationHelper:全局单例,持久化保存用户偏好(是否开启、选中包名列表、是否放行其他应用),并向外提供查询接口。
- WatchManager / NotificationMsg / OnWatchOpCallback:与 SDK 的边界。
WatchManager是WatchOpImpl的子类,负责把消息模型序列化为 RCSP 指令写入蓝牙设备。
核心组件与实现细节
MessageSyncFragment:设置页交互
MessageSyncFragment 继承 BaseFragment,使用 ViewBinding(FragmentMessageSyncBinding)渲染 fragment_message_sync.xml 布局。其初始化流程在 onActivityCreated 中完成:
- 设置顶部栏标题为"通知"(
R.string.alert),左上角返回键关闭页面; - 为 4 个开关注册
OnCheckedChangeListener; - 通过
ViewModelProvider(this).get(MessageSyncViewModel.class)获取 ViewModel(而非手动 new,确保与 Fragment 生命周期绑定); - 观察
mConnectionDataMLD连接状态,设备断开即finish(); - 使用
setCheckedImmediatelyNoEvent回显已保存的开关状态(该方法不触发监听器,避免初始化时产生写入副作用); - 调用
handleMessageSyncUI根据总开关状态启用/禁用子开关。
mBinding.sbtnMessageSyncSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
mViewModel.setEnableNotification(isChecked);
handleMessageSyncUI(isChecked);
});
mBinding.sbtnMessageWechatSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (isChecked) {
mViewModel.addAppPackage(HealthConstant.PACKAGE_NAME_WECHAT);
} else {
mViewModel.removeAppPackage(HealthConstant.PACKAGE_NAME_WECHAT);
}
});
Source: MessageSyncFragment.java
设计意图:总开关与子开关是"主从"关系。handleMessageSyncUI(boolean) 用 setEnabled 控制子开关可用性,保证总开关关闭时用户无法单独开启某个 App 的同步,避免"总开关关了但微信还在同步"的歧义状态:
private void handleMessageSyncUI(boolean isOpen){
mBinding.sbtnMessageWechatSwitch.setEnabled(isOpen);
mBinding.sbtnMessageQqSwitch.setEnabled(isOpen);
mBinding.sbtnMessageSmsSwitch.setEnabled(isOpen);
mBinding.sbtnMessageOtherSwitch.setEnabled(isOpen);
}
Source: MessageSyncFragment.java
MessageSyncViewModel:状态翻译层
MessageSyncViewModel 继承 WatchViewModel,内部持有 NotificationHelper 单例。所有公共方法都是对 Helper 的一对一委托,并做了空值防护(TextUtils.isEmpty(packageName) 时直接 return)。这种设计让 UI 层只依赖 ViewModel,将来若切换通知实现(如系统 API 变化)只需改动 ViewModel 内部。
public boolean isOpenMessageSync() {
return mNotificationHelper.isEnableNotification();
}
public boolean isSyncWeChat() {
return mNotificationHelper.isSelectedApp(HealthConstant.PACKAGE_NAME_WECHAT);
}
public void addAppPackage(String packageName) {
if (TextUtils.isEmpty(packageName)) return;
mNotificationHelper.addPackageName(packageName);
}
Source: MessageSyncViewModel.java
可观察到三类委托方法:
| 类别 | 方法 | 底层 Helper 调用 |
|---|---|---|
| 查询 | isOpenMessageSync() | isEnableNotification() |
| 查询 | isSyncWeChat() / isSyncQQ() / isSyncSms() / isSyncOther() | isSelectedApp(包名) / isAllowOther() |
| 写入 | addAppPackage / removeAppPackage | addPackageName / removePackageName |
| 写入 | setEnableNotification(boolean) | setEnableNotification(enable) |
| 写入 | setNotFilter(boolean) | setAllowOther(value) |
NotificationHelper:通知同步状态中枢
NotificationHelper(com.jieli.healthaide.tool.notification)是全局单例(NotificationHelper.getInstance()),职责包括:
- 维护总开关
enableNotification; - 维护监听包名集合(
packageObserverList,由getPackageObserverList()返回); - 维护"是否放行其他应用"标记(
allowOther); - 提供
getNotificationFlag(appName)生成消息标志位(见演示代码中的使用)。
它同时是 Android 通知监听器(NotificationListenerService)与 UI 状态之间的数据源:通知监听器捕获到新通知后,依据包名集合决定是否构造 NotificationMsg 并交给 WatchManager 下发。
WatchManager 与 NotificationMsg:设备下发边界
WatchManager(com.jieli.healthaide.tool.watch)是 WatchOpImpl 的子类,封装了与 JL RCSP SDK 的交互。消息同步涉及两个核心方法:
pushMessageInfo(NotificationMsg msg, OnWatchOpCallback<Boolean> callback):新增/推送一条消息;removeMessageInfo(NotificationMsg msg, OnWatchOpCallback<Boolean> callback):撤销一条消息。
NotificationMsg 是 SDK 模型(com.jieli.jl_rcsp.model.NotificationMsg),采用 Builder 风格链式赋值,关键字段:
| 字段 | 类型 | 含义 |
|---|---|---|
appName | String | 来源 App 包名(如 com.tencent.mm) |
flag | int | 通知标志(由 NotificationHelper.getNotificationFlag(appName) 生成) |
content | String | 消息正文内容 |
title | String | 消息标题 |
time | long | 消息时间戳(毫秒) |
op | int | 操作类型:0 = 推送,1 = 撤销 |
op 字段是整个同步协议的关键设计:同一 NotificationMsg 既可作为新增载荷(op=0)也可作为撤销指令(op=1),设备端据此决定"展示消息"还是"撤回消息",从而支持微信等应用的"撤回"能力同步到手表端。
包名常量:支持的默认应用
HealthConstant 定义了同步支持的默认应用包名:
//default package name
public final static String PACKAGE_NAME_SYS_MESSAGE = "com.android.mms";
public final static String PACKAGE_NAME_WECHAT = "com.tencent.mm";
public final static String PACKAGE_NAME_QQ = "com.tencent.mobileqq";
public final static String PACKAGE_NAME_DING_DING = "com.alibaba.android.rimet";
Source: HealthConstant.java
设计意图:UI 层只针对微信/QQ/短信提供专用开关,而"其他应用"开关(isAllowOther)提供兜底放行能力,使系统能适配任意新应用,无需为每个 App 硬编码开关。
核心流程
消息同步配置流程(用户操作视角)
sequenceDiagram
participant U as 用户
participant F as MessageSyncFragment
participant V as MessageSyncViewModel
participant N as NotificationHelper
U->>F: 打开"消息同步"总开关
F->>V: setEnableNotification(true)
V->>N: setEnableNotification(true)
N-->>N: 持久化总开关状态
F->>F: handleMessageSyncUI(true)
Note over F: 启用微信/QQ/短信/其他子开关
U->>F: 打开"微信同步"子开关
F->>V: addAppPackage("com.tencent.mm")
V->>N: addPackageName("com.tencent.mm")
N-->>N: 持久化监听包名集合
消息推送到设备流程(通知捕获视角)
sequenceDiagram
participant OS as Android 系统
participant NL as 通知监听器 (NotificationListenerService)
participant N as NotificationHelper
participant W as WatchManager
participant CB as OnWatchOpCallback
participant D as 手表设备
OS->>NL: 微信收到新通知
NL->>N: 查询 isEnableNotification / 包名集合
N-->>NL: 命中 com.tencent.mm
NL->>N: getNotificationFlag(appName) 生成 flag
NL->>W: pushMessageInfo(NotificationMsg{op=0})
W->>D: 蓝牙 RCSP 指令写入
D-->>W: 写入结果
W-->>CB: onSuccess(result)
alt 写入失败
W-->>CB: onFailed(BaseError error)
end
消息撤销流程
消息撤销与推送共用同一结构,仅 op 不同。当用户在手机上撤回一条微信消息时,通知监听器以相同 appName、time 构造 op=1 的 NotificationMsg 调用 removeMessageInfo,设备端据此定位并移除已展示的对应消息。
使用示例
推送一条消息到设备(模拟微信消息)
以下代码来自 SyncMessageDemo,演示如何构造 NotificationMsg 并通过 WatchManager 推送:
@Test
void addSyncMessage() {
//WatchManager是WatchOpImpl的子类,须在1.3配置好sdk
WatchManager watchManager = WatchManager.getInstance();
//模拟微信消息
String appName = HealthConstant.PACKAGE_NAME_WECHAT;
NotificationMsg msg = new NotificationMsg()
.setAppName(appName)
.setFlag(NotificationHelper.getNotificationFlag(appName))
.setContent("帅小伙:[1条] 你好,欢迎使用健康助手,乐享运动,助力健康!")
.setTitle("测试消息")
.setTime(time)
.setOp(0);
watchManager.pushMessageInfo(msg, new OnWatchOpCallback<Boolean>() {
@Override
public void onSuccess(Boolean result) {
//回调成功
}
@Override
public void onFailed(BaseError error) {
//回调失败
//error: 错误信息
}
});
}
Source: SyncMessageDemo.java
要点说明:
time来自Calendar.getInstance().getTimeInMillis(),同一消息推送与撤销必须使用一致的time,设备端才可正确匹配撤回;flag通过NotificationHelper.getNotificationFlag(appName)生成,调用方无需关心标志位的具体位运算规则;setOp(0)表示新增;回调中result为Boolean表示写入是否成功。
撤销一条消息
@Test
void removeSyncMessage() {
//WatchManager是WatchOpImpl的子类,须在1.3配置好sdk
WatchManager watchManager = WatchManager.getInstance();
//模拟撤销微信消息
String appName = HealthConstant.PACKAGE_NAME_WECHAT;
NotificationMsg msg = new NotificationMsg()
.setAppName(appName)
.setFlag(NotificationHelper.getNotificationFlag(appName))
.setTime(time)
.setOp(1);
watchManager.removeMessageInfo(msg, new OnWatchOpCallback<Boolean>() {
@Override
public void onSuccess(Boolean result) {
//回调成功
}
@Override
public void onFailed(BaseError error) {
//回调失败
//error: 错误信息
}
});
}
Source: SyncMessageDemo.java
与推送的区别仅在于 setOp(1) 且不携带 content/title——撤销指令只需 appName + flag + time 即可唯一定位消息。
初始化回显开关状态
MessageSyncFragment 在页面创建后从 ViewModel 读取持久化状态并回显,使用 setCheckedImmediatelyNoEvent 避免回显动作触发写操作:
mBinding.sbtnMessageSyncSwitch.setCheckedImmediatelyNoEvent(mViewModel.isOpenMessageSync());
mBinding.sbtnMessageWechatSwitch.setCheckedImmediatelyNoEvent(mViewModel.isSyncWeChat());
mBinding.sbtnMessageQqSwitch.setCheckedImmediatelyNoEvent(mViewModel.isSyncQQ());
mBinding.sbtnMessageSmsSwitch.setCheckedImmediatelyNoEvent(mViewModel.isSyncSms());
mBinding.sbtnMessageOtherSwitch.setCheckedImmediatelyNoEvent(mViewModel.isSyncOther());
handleMessageSyncUI(mViewModel.isOpenMessageSync());
Source: MessageSyncFragment.java
配置选项
消息同步的配置全部由 NotificationHelper 单例持久化,UI 通过 MessageSyncViewModel 读写。关键配置项如下:
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| 消息同步总开关 | boolean | 由用户设置 | isEnableNotification() / setEnableNotification(boolean) |
| 微信同步 | boolean | 由用户设置 | 包名 com.tencent.mm,isSelectedApp / addPackageName |
| QQ 同步 | boolean | 由用户设置 | 包名 com.tencent.mobileqq |
| 短信同步 | boolean | 由用户设置 | 包名 com.android.mms |
| 其他应用放行 | boolean | 由用户设置 | isAllowOther() / setAllowOther(boolean),兜底放行未列出的应用 |
| 监听包名集合 | List<String> | 空集合 | getPackageObserverList(),通知监听器据此过滤 |
| 消息标志位 | int | 按包名生成 | getNotificationFlag(appName),随 NotificationMsg 下发 |
包名常量定义于 HealthConstant.java(含钉钉 com.alibaba.android.rimet,为未来扩展预留)。
API 参考
MessageSyncViewModel(com.jieli.healthaide.ui.device.more)
| 方法签名 | 说明 |
|---|---|
boolean isOpenMessageSync() | 总开关是否开启,委托 NotificationHelper.isEnableNotification() |
boolean isSyncWeChat() | 微信包名是否在监听集合中 |
boolean isSyncQQ() | QQ 包名是否在监听集合中 |
boolean isSyncSms() | 短信包名是否在监听集合中 |
boolean isSyncOther() | 是否放行其他应用(isAllowOther()) |
List<String> getSelectedApp() | 返回当前监听的包名列表 |
void addAppPackage(String packageName) | 加入监听集合;packageName 为空时直接返回 |
void removeAppPackage(String packageName) | 移出监听集合;packageName 为空时直接返回 |
void setEnableNotification(boolean enable) | 设置总开关 |
void setNotFilter(boolean value) | 设置是否放行其他应用 |
WatchManager(com.jieli.healthaide.tool.watch)
pushMessageInfo(NotificationMsg msg, OnWatchOpCallback<Boolean> callback)
- 描述:将一条新消息推送到已连接设备。
- 参数:
msg为待推送消息(op应设为0);callback为操作结果回调。 - 回调:
onSuccess(Boolean result)成功;onFailed(BaseError error)失败,error携带错误信息。
removeMessageInfo(NotificationMsg msg, OnWatchOpCallback<Boolean> callback)
- 描述:撤销设备上已展示的一条消息。
- 参数:
msg的op应设为1,需与推送时保持一致的appName/flag/time。 - 回调:同
pushMessageInfo。
NotificationMsg(com.jieli.jl_rcsp.model,SDK 模型)
Builder 风格链式 setter:setAppName(String)、setFlag(int)、setContent(String)、setTitle(String)、setTime(long)、setOp(int)。op = 0 推送,op = 1 撤销。
失败模式、边界情况与并发
设备断开
MessageSyncFragment 通过 mConnectionDataMLD.observe 监听连接状态,一旦状态不等于 BluetoothConstant.CONNECT_STATE_CONNECTED(BluetoothConstant 来自 com.jieli.bluetooth_connect.constant)立即 finish()。这意味着所有同步配置界面都要求设备在线,避免配置写入"悬空"。副作用是用户在编辑过程中设备断连会被直接踢出页面,未保存的开关操作可能丢失。
空包名防护
addAppPackage / removeAppPackage 对空字符串做了 TextUtils.isEmpty 前置校验,防止空值污染监听集合。
消息匹配的一致性要求
撤销(op=1)必须与推送(op=0)使用相同的 appName、flag、time。若两端时间戳或包名不一致(如系统通知携带的时间与 Calendar.getInstance().getTimeInMillis() 有偏差),设备端将无法定位消息,撤销会静默失败。这是该协议最容易出错的边界点。
并发与去重
NotificationHelper 是进程内单例,包名集合的读写发生在 UI 线程(开关回调)与通知监听线程(通知捕获)两个上下文。源码中未发现显式同步锁;由于 addPackageName 等操作由系统通知逐个触发且频率有限,实践中竞争窗口极小,但高并发通知风暴场景下仍存在丢失包名更新的理论风险。消息去重依赖设备端按 appName + time 处理。
其他应用兜底开关的语义
isAllowOther 是布尔兜底而非白名单追加:开启后所有未显式列出的应用通知都会被同步,这可能造成隐私信息(如银行验证码)被推到手表端,属于产品层面的取舍。
性能与运维
- 消息同步路径为"系统通知 → 监听器 → RCSP 蓝牙指令",单条消息开销极小;
NotificationMsg仅携带标题、内容、时间等少量文本字段,无大对象传输。 - 蓝牙写入为异步回调模型(
OnWatchOpCallback),UI 线程不会被阻塞;回调不保证在主线程执行,涉及 UI 更新时需自行切线程。 - 通知风暴场景下(如群消息轰炸),每条通知都会触发一次蓝牙写入,建议在通知监听器侧做节流/合并(当前源码未体现,属于可扩展点)。
MessageSyncFragment.onDestroy中会mViewModel.release()并置空mBinding,避免 ViewBinding 泄漏。
扩展点
- 新增默认支持的应用:在
HealthConstant追加包名常量,并在 UI 层增加对应开关(参考微信/QQ/短信开关的对称实现)。 - 放行任意应用:利用"其他应用"开关(
setNotFilter/isAllowOther),无需代码改动即可同步任意已安装应用。 - 消息节流/聚合:可在通知监听器与
WatchManager之间增加聚合层,将短时间内的多条同类通知合并为一条摘要消息下发。 - 联系人结构化:当前联系人信息内嵌于
content字符串(如"帅小伙:[1条] ...");如需结构化联系人,可在NotificationMsg之外扩展模型并在设备端协议中增加字段。
测试
仓库内提供 SyncMessageDemo(app/src/test 下的 JUnit 测试类),覆盖两种核心场景:
addSyncMessage():构造微信消息并调用pushMessageInfo推送;removeSyncMessage():构造撤销消息并调用removeMessageInfo撤销。
测试以注释形式明确说明前置条件:"WatchManager 是 WatchOpImpl 的子类,须在 1.3 配置好 SDK",即测试依赖 SDK 初始化完成与设备连接,属于集成级演示而非纯单元测试。UI 层开关联动(总开关与子开关的启用/禁用)目前未见独立测试用例。
Related Links
- MessageSyncFragment.java — 消息同步设置页 UI 与交互
- MessageSyncViewModel.java — ViewModel 状态封装
- SyncMessageDemo.java — 消息推送/撤销演示
- HealthConstant.java — 包名常量定义
- 设备连接状态管理见"设备管理"相关页面;AI 云端消息历史(
AICloudHistoryMessageFragment等)为独立能力,见"AI 云消息"页面