杰理 SDK 文档中心
首页
首页
  • 项目概述

    • 项目简介与核心能力
    • 运行环境与SDK版本
  • 快速开始

    • 工程导入与依赖配置
    • 权限配置与示例运行
  • 平台架构

    • SDK分层架构与RCSP协议
    • 蓝牙连接库
    • 健康SDK核心库 JL_Watch
    • 健康服务器与云端服务
  • 健康与运动数据

    • 健康数据同步
    • 运动数据同步
    • 本地数据持久化
  • 设备管理功能

    • 表盘管理
    • 闹钟与健康提醒
    • 消息与联系人同步
    • 天气同步
    • 设备查找
    • 支付宝集成
  • 传输与媒体处理

    • 文件传输与文件管理
    • 音乐传输与播放控制
    • 图像转换库
    • 音频编解码与解密
  • OTA 升级

    • 固件空中升级流程
    • 4G模块与差分升级
  • AI 能力

    • AI表盘与云服务
    • AI语音助手
  • 示例应用

    • HealthAide 健康助手应用
    • WatchTestTool 测试工具
  • 开发者指南

    • 自定义命令扩展
    • 调试技巧与问题排查
    • 版本历史与兼容性

消息与联系人同步

消息与联系人同步是健康助手 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 等字段同步到设备端,让用户在手腕上即可阅读和撤销消息。

从代码可以看出该能力分为三层职责:

  1. 设置层(UI):MessageSyncFragment 提供总开关(消息同步)与四个子开关(微信、QQ、短信、其他应用),并通过 setCheckedImmediatelyNoEvent 在初始化时回显已保存状态。
  2. 状态层(ViewModel):MessageSyncViewModel 继承自 WatchViewModel,将 UI 与 NotificationHelper 单例解耦,提供 isOpenMessageSync、isSyncWeChat、addAppPackage 等薄封装方法。
  3. 执行层(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 中完成:

  1. 设置顶部栏标题为"通知"(R.string.alert),左上角返回键关闭页面;
  2. 为 4 个开关注册 OnCheckedChangeListener;
  3. 通过 ViewModelProvider(this).get(MessageSyncViewModel.class) 获取 ViewModel(而非手动 new,确保与 Fragment 生命周期绑定);
  4. 观察 mConnectionDataMLD 连接状态,设备断开即 finish();
  5. 使用 setCheckedImmediatelyNoEvent 回显已保存的开关状态(该方法不触发监听器,避免初始化时产生写入副作用);
  6. 调用 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 / removeAppPackageaddPackageName / 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 风格链式赋值,关键字段:

字段类型含义
appNameString来源 App 包名(如 com.tencent.mm)
flagint通知标志(由 NotificationHelper.getNotificationFlag(appName) 生成)
contentString消息正文内容
titleString消息标题
timelong消息时间戳(毫秒)
opint操作类型: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 泄漏。

扩展点

  1. 新增默认支持的应用:在 HealthConstant 追加包名常量,并在 UI 层增加对应开关(参考微信/QQ/短信开关的对称实现)。
  2. 放行任意应用:利用"其他应用"开关(setNotFilter / isAllowOther),无需代码改动即可同步任意已安装应用。
  3. 消息节流/聚合:可在通知监听器与 WatchManager 之间增加聚合层,将短时间内的多条同类通知合并为一条摘要消息下发。
  4. 联系人结构化:当前联系人信息内嵌于 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 云消息"页面
Prev
闹钟与健康提醒
Next
天气同步