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

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

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

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

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

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

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

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

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

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

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

闹钟与健康提醒

本文档介绍 Android-JL_Health 应用中"闹钟与健康提醒"能力的完整实现:从 UI 层的闹钟列表/设置界面,到 ViewModel 层的业务编排,再到基于 RCSP 协议(RTCOpImpl)与智能手表设备进行闹钟增删改查、时间同步与响铃通知处理的端到端机制。

Purpose and Scope

本页覆盖与闹钟及健康提醒相关的全部实现环节:

  • 闹钟列表页(AlarmListFragment)与新增/编辑/删除/开关闹钟的交互逻辑
  • AlarmViewModel 中通过 RCSP 协议与设备通信的增删改查编排
  • 设备侧闹钟任务处理(AlarmHandleTask):系统时间同步、响铃弹窗通知、停止铃声
  • 闹钟铃声扩展能力(BellArg、默认铃声列表、铃声音量/时长参数读写)

以下相关主题属于兄弟页面,本页不做展开:

  • 设备连接管理与 WatchManager 生命周期:参见设备管理(Device Management)相关页面
  • RCSP 协议底层命令帧解析(AlarmExpandCmd 等模型定义):属于 SDK 协议层,见 SDK 相关文档
  • 其他健康数据(心率、睡眠、运动)采集能力:属于各健康数据页面

Overview

智能手表上的闹钟是"健康提醒"的主要载体:用户可以为手表配置最多 5 个 闹钟(例如起床闹钟、喝水提醒、服药提醒),每个闹钟包含时间(时/分)、重复星期、名称、开关状态以及铃声参数。手机端 App 负责配置下发与读取,手表端在到达设定时间后本地响铃,并通过 RCSP 事件通知 App 弹出提醒对话框。

整个能力围绕三个核心角色展开:

  1. RTCOpImpl(RTC 操作实现):com.jieli.jl_rcsp SDK 中负责 RTC(实时时钟)与闹钟相关命令的封装,提供 readAlarmList、addOrModifyAlarm、deleteAlarm、syncTime、stopAlarmBell、readAlarmBellArgs、setAlarmBellArg 等接口。App 侧所有闹钟操作最终都通过它下发到设备。
  2. AlarmViewModel:闹钟业务逻辑的编排层,向 Fragment 暴露读列表、增删改、铃声参数读写等方法,并监听设备主动上报的闹钟列表变化事件,通过 MutableLiveData<AlarmListInfo> 驱动 UI 刷新。
  3. AlarmHandleTask:设备侧事件监听器,负责在系统初始化后同步设备时间,在设备响铃(onAlarmNotify)时弹出提醒对话框,在停止响铃(onAlarmStop)时关闭对话框。

设计上,App 与设备的通信全部走异步回调(OnOperationCallback),UI 线程通过 LiveData 与主线程 Handler 更新,确保蓝牙操作不阻塞界面;闹钟数据本身存储在设备端(手表本地),App 仅在需要时读取或下发,因此断连后设备仍能按时响铃。

Architecture

flowchart TD
    subgraph sg_UI["UI 层 (com.jieli.healthaide.ui.device.alarm)"]
        AlarmListFragment["AlarmListFragment<br/>闹钟列表"]
        AlarmSettingFragment["AlarmSettingFragment<br/>闹钟设置"]
        AlarmBellContainerFragment["AlarmBellContainerFragment<br/>铃声选择"]
        EditAlarmNameFragment["EditAlarmNameFragment<br/>名称编辑"]
    end

    subgraph sg_VM["ViewModel 层"]
        AlarmViewModel["AlarmViewModel<br/>extends WatchViewModel"]
        alarmsMLD["alarmsMutableLiveData<br/>MutableLiveData&lt;AlarmListInfo&gt;"]
    end

    subgraph sg_SDK["SDK 层 (com.jieli.jl_rcsp)"]
        RTCOp["RTCOpImpl<br/>RTC 闹钟命令封装"]
        WatchManager["WatchManager"]
    end

    subgraph sg_Device["设备侧任务 (tool.watch)"]
        AlarmHandleTask["AlarmHandleTask<br/>extends OnWatchCallback"]
        EventListener["EventListener<br/>extends OnRcspEventListener"]
    end

    subgraph sg_Watch["智能手表"]
        Watch["设备本地闹钟存储<br/>响铃/震动"]
    end

    AlarmListFragment -->|"observe / 用户操作"| AlarmViewModel
    AlarmSettingFragment -->|"observe / 编辑保存"| AlarmViewModel
    AlarmBellContainerFragment -->|"铃声参数读写"| AlarmViewModel
    EditAlarmNameFragment -->|"名称修改"| AlarmViewModel
    AlarmViewModel --> alarmsMLD
    AlarmViewModel -->|"readAlarmList / addOrModifyAlarm /<br/>deleteAlarm / setAlarmBellArg"| RTCOp
    RTCOp -->|"RCSP 协议 BLE 透传"| Watch
    AlarmHandleTask -->|"注册监听"| WatchManager
    Watch -->|"onAlarmNotify / onAlarmStop 事件"| EventListener
    EventListener -->|"Jl_Dialog 弹窗 + stopAlarmBell"| RTCOp
    AlarmHandleTask -->|"syncTime (系统初始化)"| RTCOp

架构说明

  • UI 层:AlarmListFragment 是入口页面,展示闹钟卡片列表(时间、名称、重复星期、开关、删除按钮);点击卡片进入 AlarmSettingFragment 编辑;AlarmBellContainerFragment 用于选择铃声与设置响铃参数;EditAlarmNameFragment 用于修改闹钟名称。Fragment 间通过 Bundle 传递 JSON 序列化的 AlarmBean。
  • ViewModel 层:AlarmViewModel 继承自 WatchViewModel,持有 RTCOpImpl 实例并注册 OnRcspEventListener。它不直接操作 UI,而是通过 MutableLiveData<AlarmListInfo> 将设备上报的闹钟列表推送给 Fragment 观察者。
  • SDK 层:RTCOpImpl 是 SDK 提供的命令封装,与 WatchManager 协作完成 BLE 命令收发;AlarmBean、AlarmListInfo、AlarmExpandCmd.BellArg、DefaultAlarmBell 等模型定义在 com.jieli.jl_rcsp.model 包中。
  • 设备侧任务:AlarmHandleTask 生命周期跟随 WatchManager,在系统初始化回调中同步设备时间,保证闹钟按手机时间触发;同时监听响铃通知事件,在 App 前台弹出不可取消的提醒对话框,用户确认后调用 stopAlarmBell 停止设备铃声。

设计意图:闹钟数据以设备端为唯一数据源(single source of truth),App 只是配置面板。这样即使手机与手表断开连接,闹钟依然由手表本地触发,符合穿戴设备"离线可用"的产品预期。所有读写均异步执行,失败通过 Toast/回调反馈,避免 UI 卡顿。

闹钟数据模型

闹钟的核心模型是 SDK 中的 AlarmBean(com.jieli.jl_rcsp.model.device.AlarmBean)。从 AlarmViewModel.createNewAlarm() 的字段初始化可以还原其关键属性:

字段类型含义初始化值
indexbyte闹钟槽位索引(0~4,最多 5 个)首个空闲槽位
hour / minbyte触发时间(24 小时制)当前系统时间
nameString闹钟名称默认名称字符串资源
openboolean是否启用true
versionint版本号1
bellNameString铃声名称默认铃声 1
bellTypebyte铃声类型0
bellClusterint铃声簇0

createNewAlarm() 的实现逻辑展示了"最多 5 个闹钟"的产品约束:遍历当前列表,用布尔数组 has[5] 标记已占用的槽位,找到第一个空闲 index 分配给新闹钟;同时以当前本地时间(Calendar.HOUR_OF_DAY / Calendar.MINUTE)作为默认触发时间,便于用户快速创建"马上生效"的提醒。

列表数据的聚合模型为 AlarmListInfo,由 AlarmViewModel.alarmsMutableLiveData 承载,通过 getAlarmBeans() 取出闹钟集合。

UI 层:AlarmListFragment

AlarmListFragment 是闹钟功能的入口页面,继承 BaseFragment,使用 ViewBinding(FragmentAlarmListBinding)渲染布局。其生命周期与职责如下:

  • onViewCreated:通过 ViewModelProvider(this).get(AlarmViewModel.class) 获取 ViewModel,初始化顶部栏(标题"闹钟"、右侧"+"新增按钮)与 RecyclerView 适配器。
  • onResume:每次回到页面都调用 mViewModel.readAlarmList(),确保列表与设备端同步——这是"设备为数据源"设计的体现:任何设备端变化(包括其他入口修改)都会在页面可见时被重新拉取。
  • 顶部栏新增按钮 → addAlarm():先检查 alarmAdapter.getData().size() > 4,若已达上限则提示"闹钟数量已满"(R.string.alarm_set_num_is_full);否则创建新闹钟并跳转设置页。
private void addAlarm() {
    if (alarmAdapter.getData().size() > 4) {
        showTips(R.string.alarm_set_num_is_full);
        return;
    }
    editAlarm(mViewModel.createNewAlarm(), false);
}

Source: AlarmListFragment.java

  • 列表适配器 AlarmAdapter(继承 BaseQuickAdapter):每个列表项展示:

    • tv_alarm_time:CalendarUtil.formatString("%02d:%02d", hour, min) 格式化的时间;
    • tv_alarm_name:闹钟名称;
    • tv_alarm_week:通过 Util.getRepeatDescModify() 生成的重复星期描述;
    • sw_default_alarm(SwitchButton):开关闹钟,用 setCheckedImmediatelyNoEvent 避免回环触发,切换时调用 mViewModel.updateAlarm() 并提示保存成功/失败;
    • btn_del_alarm:删除按钮,调用 mViewModel.deleteAlarm();
    • 卡片背景点击:携带 JSON 序列化的 AlarmBean 进入 AlarmSettingFragment 编辑。
  • 数据观察:addObserver() 注册两个观察者——alarmsMutableLiveData 将闹钟列表按 hour * 60 + min 升序排序后刷新适配器;mConnectionDataMLD 在设备断开连接时直接 finish() 关闭页面,防止在无设备状态下误操作。

业务层:AlarmViewModel

AlarmViewModel extends WatchViewModel 承担全部闹钟业务编排,注释明确其职责为"闹钟功能逻辑处理"。

生命周期管理

构造时注册 OnRcspEventListener 并创建 RTCOpImpl;onCleared() 时反注册监听并调用 mRTCOp.destroy() 释放资源——这是典型的"跟随 ViewModel 生命周期绑定设备监听"模式,避免 Fragment 销毁后仍收到设备事件导致内存泄漏。

闹钟 CRUD

方法底层命令说明
readAlarmList()mRTCOp.readAlarmList请求设备上报全部闹钟;失败 Toast 提示"闹钟列表读取失败"
updateAlarm(AlarmBean, OpCallback<Boolean>)mRTCOp.addOrModifyAlarm新增或修改闹钟;通过回调回传成功状态
deleteAlarm(AlarmBean) / deleteAlarm(AlarmBean, OperatCallback)mRTCOp.deleteAlarm删除闹钟;成功后 Toast + 重新 readAlarmList() 刷新
public void updateAlarm(AlarmBean alarmBean, OpCallback<Boolean> callback) {
    JL_Log.i(tag, "updateAlarm", "alarmBean : " + alarmBean);
    mRTCOp.addOrModifyAlarm(getConnectedDevice(), alarmBean, new OnOperationCallback<Boolean>() {
        @Override
        public void onSuccess(Boolean result) {
            if (callback != null) callback.back(true);
        }

        @Override
        public void onFailed(BaseError error) {
            JL_Log.w(tag, "updateAlarm", "onFailed ---> " + error);
            if (callback != null) callback.back(false);
        }
    });
}

Source: AlarmViewModel.java

设备事件监听

eventHandler(OnRcspEventListener 匿名实现)监听两个事件:

  • onAlarmListChange(device, AlarmListInfo):设备主动推送闹钟列表变化(例如用户直接在手表上修改),alarmsMutableLiveData.postValue(alarmListInfo) 用 postValue(而非 setValue)保证可从蓝牙回调线程安全地更新主线程 LiveData;
  • onAlarmDefaultBellListChange(device, bells):默认铃声列表变化,此处直接透传 super,为扩展预留。

设备侧任务:AlarmHandleTask

AlarmHandleTask extends OnWatchCallback 是运行在工具层(com.jieli.healthaide.tool.watch)的设备侧监听器,负责闹钟功能与设备的"握手":

时间同步

onWatchSystemInit(int code) 在手表系统初始化完成后回调;当 code == 0 时调用 updateSysTime():若 DeviceInfo.isRTCEnable() 为真,则通过 mRTCOp.syncTime() 将手机时间同步到手表。这是闹钟按时触发的前置条件——手表端 RTC(实时时钟)必须与手机一致,闹钟才会在用户期望的时刻响铃。

响铃通知弹窗

构造函数中延迟 10ms 注册 EventListener(OnRcspEventListener),处理两个关键事件:

  • onAlarmNotify:手表闹钟开始响铃。此时:
    1. 获取当前 Activity(ActivityManager.getInstance().getCurrentActivity());
    2. 通过 findFragmentByTag("alarm_notify") 做防重入检查——若弹窗已存在则直接返回;
    3. 构建 Jl_Dialog:标题"提示"、内容"闹钟正在响铃"(R.string.alarm_running)、右侧按钮"确定"、宽度 0.8、cancel(false) 禁止点击外部取消;
    4. 点击"确定"后调用 mRTCOp.stopAlarmBell(device, null) 下发停铃命令并关闭弹窗。
@Override
public void onAlarmNotify(BluetoothDevice device, AlarmBean alarmBean) {
    super.onAlarmNotify(device, alarmBean);
    FragmentActivity activity = (FragmentActivity) ActivityManager.getInstance().getCurrentActivity();
    if (activity == null) return;
    Fragment fragment = activity.getSupportFragmentManager().findFragmentByTag("alarm_notify");
    if (fragment != null) return;
    Jl_Dialog jl_dialog = new Jl_Dialog.Builder()
            .title(activity.getString(R.string.tips))
            .content(activity.getString(R.string.alarm_running))
            .right(activity.getString(R.string.sure))
            .width(0.8f)
            .cancel(false)
            .rightClickListener((v, dialogFragment) -> {
                mRTCOp.stopAlarmBell(device, null);
                dialogFragment.dismiss();
            })
            .build();
    jl_dialog.show(activity.getSupportFragmentManager(), "alarm_notify");
}

Source: AlarmHandleTask.java

  • onAlarmStop:手表停止响铃(可能由设备端超时或用户操作触发),查找 alarm_notify 标签的 Fragment,若存在则强制 dismiss(),保证弹窗与设备响铃状态严格同步,避免残留对话框。

连接状态处理

onConnectStateChange 在状态非 CONNECTION_OK 时调用 mUIHandler.removeCallbacksAndMessages(null) 清空主线程 Handler 上所有待执行任务,防止断连后残留的延迟任务(如重复同步时间)在重连时误执行。

闹钟铃声扩展能力

部分设备支持"扩展闹钟"(AlarmExpand),AlarmViewModel 提供以下能力:

  • hasBellArgs():读取 DeviceInfo.getAlarmExpandFlag(),通过 (flag & 0x01) == 0x01 判断设备是否支持扩展闹钟参数——这是所有铃声扩展操作的前置守卫。
  • saveBellArgs(AlarmExpandCmd.BellArg, OpCallback<Boolean>):调用 mRTCOp.setAlarmBellArg() 下发铃声参数;若设备不支持扩展(!hasBellArgs())直接回调成功,若参数为 null 回调失败。
  • readExpandArg(AlarmBean, OpCallback<AlarmExpandCmd.BellArg>):以位掩码 (byte) (0x01 << alarmBean.getIndex()) 指定目标闹钟槽位,调用 mRTCOp.readAlarmBellArgs() 读取该闹钟的铃声参数;结果为空时以 ERR_INVALID_PARAMETER 走失败分支。

Core Flow

流程一:新建/编辑闹钟(App → 设备)

sequenceDiagram
    participant U as 用户
    participant F as AlarmListFragment
    participant VM as AlarmViewModel
    participant R as RTCOpImpl
    participant W as 手表设备

    U->>F: 点击"+"新增 / 点击卡片编辑
    F->>F: addAlarm(): size > 4 ? 提示已满 : createNewAlarm()
    F->>F: editAlarm(): AlarmBean 转 JSON 放入 Bundle
    F->>F: 跳转 AlarmSettingFragment
    U->>F: 设置时间/重复/名称/铃声后点保存
    F->>VM: updateAlarm(alarmBean, callback)
    VM->>R: addOrModifyAlarm(device, alarmBean)
    R->>W: RCSP 命令下发 (BLE)
    W-->>R: 命令应答
    R-->>VM: onSuccess / onFailed
    VM-->>F: callback.back(true/false)
    F->>U: Toast 保存成功/失败

关键设计点:闹钟编辑页通过 JSON(Gson)传递 AlarmBean,跨 Fragment 解耦;保存操作是"全量覆盖"语义——addOrModifyAlarm 同时承担新增与修改,设备端按 index 决定是创建还是更新槽位。

流程二:闹钟响铃通知(设备 → App)

sequenceDiagram
    participant W as 手表设备
    participant EL as EventListener (AlarmHandleTask)
    participant AM as ActivityManager
    participant D as Jl_Dialog
    participant R as RTCOpImpl

    W->>EL: onAlarmNotify(device, alarmBean)
    EL->>AM: getCurrentActivity()
    AM-->>EL: FragmentActivity
    EL->>EL: findFragmentByTag("alarm_notify") 防重入
    EL->>D: show(manager, "alarm_notify") 弹窗
    U->>D: 点击"确定"
    D->>R: stopAlarmBell(device)
    R->>W: 停铃命令 (BLE)
    D->>D: dismiss()
    W->>EL: onAlarmStop(device, alarmBean)
    EL->>EL: findFragmentByTag 存在则 dismiss()

关键设计点:弹窗使用 cancel(false) 强制用户确认,避免误触导致铃声继续;onAlarmNotify/onAlarmStop 成对出现,配合 findFragmentByTag 幂等处理,保证弹窗生命周期与设备响铃状态一一对应。

流程三:系统初始化时间同步

AlarmHandleTask.onWatchSystemInit(0) → updateSysTime() → 检查 DeviceInfo.isRTCEnable() → mRTCOp.syncTime(device, callback) 将手机时间写入手表 RTC。时间同步是闹钟可靠性的根基:若手表 RTC 与手机不一致,所有闹钟触发时刻都会偏移。源码中注释掉了一段 postDelayed(..., 5000) 的周期同步代码,说明设计上考虑过"重复同步兜底",最终采用单次同步 + 失败回调的简化策略。

配置与约束

配置/约束值来源
闹钟数量上限5 个AlarmListFragment.addAlarm() 的 size() > 4 判断
新增默认名称R.string.default_alarm_nameAlarmViewModel.createNewAlarm()
新增默认铃声R.string.alarm_bell_1,bellType=0,bellCluster=0AlarmViewModel.createNewAlarm()
新增默认时间当前系统时间(Calendar)AlarmViewModel.createNewAlarm()
闹钟版本号1AlarmViewModel.createNewAlarm()
扩展闹钟能力标志DeviceInfo.getAlarmExpandFlag() & 0x01AlarmViewModel.hasBellArgs()
弹窗防重入标签"alarm_notify"AlarmHandleTask.EventListener
弹窗宽度0.8f,不可取消AlarmHandleTask.EventListener

相关文案资源:R.string.alarm(标题)、R.string.alarm_set_num_is_full(数量已满)、R.string.alarm_running(响铃中)、R.string.alarm_list_read_failed(列表读取失败)、R.string.save_alarm_success/failed(保存结果)、R.string.alarm_delete_success/failure(删除结果)。

API 参考

本页涉及的对外接口集中在 AlarmViewModel(App 侧)与 RTCOpImpl(SDK 侧)。

AlarmViewModel 方法

方法签名说明回调
readAlarmList()请求设备上报闹钟列表,结果经 onAlarmListChange 事件驱动 LiveData失败时 Toast
updateAlarm(AlarmBean alarmBean, OpCallback<Boolean> callback)新增或修改闹钟callback.back(true/false)
deleteAlarm(AlarmBean alarmBean)删除闹钟(无回调版本)Toast 成功/失败
deleteAlarm(AlarmBean alarmBean, OperatCallback opCallback)删除闹钟(带回调版本)onSuccess() / onError(subCode)
createNewAlarm()创建默认配置的新 AlarmBean,自动分配空闲槽位返回 AlarmBean
saveBellArgs(AlarmExpandCmd.BellArg bellArg, OpCallback<Boolean> callback)保存铃声参数,设备不支持时直接成功callback.back(result)
readExpandArg(AlarmBean alarmBean, OpCallback<AlarmExpandCmd.BellArg> callback)按槽位读取铃声参数callback.back(arg/null)
hasBellArgs()判断设备是否支持扩展闹钟参数返回 boolean

RTCOpImpl 底层命令(SDK)

方法用途
readAlarmList(device, callback)读取闹钟列表
addOrModifyAlarm(device, alarmBean, callback)新增/修改闹钟
deleteAlarm(device, alarmBean, callback)删除闹钟
syncTime(device, callback)同步手机时间到设备 RTC
stopAlarmBell(device, callback)停止设备响铃
readAlarmBellArgs(device, indexMask, callback)按位掩码读取铃声参数
setAlarmBellArg(device, bellArg, callback)下发铃声参数
destroy()释放 SDK 资源(ViewModel 销毁时调用)

注:RTCOpImpl 位于 SDK 模块 com.jieli.jl_rcsp.impl,其具体命令帧编码不在本仓库 App 代码内,此处仅列出 AlarmViewModel/AlarmHandleTask 实际调用到的接口。

回调类型

  • OnOperationCallback<Boolean>:onSuccess(Boolean result) / onFailed(BaseError error),SDK 统一异步回调;
  • OpCallback<Boolean>:back(boolean),App 内自定义回调,用于把 SDK 回调映射为 UI 提示;
  • OperatCallback:onSuccess() / onError(int subCode),删除操作的扩展回调,subCode 来自 BaseError.getSubCode()。

Usage Examples

示例一:读取并刷新闹钟列表

Fragment 在 onResume 时请求设备上报闹钟列表,并通过 LiveData 观察者接收设备推送的数据,按时间排序后展示:

@Override
public void onResume() {
    super.onResume();
    mViewModel.readAlarmList();
}

private void addObserver(){
    mViewModel.alarmsMutableLiveData.observe(getViewLifecycleOwner(), alarmListInfo -> {
        List<AlarmBean> sorts = alarmListInfo.getAlarmBeans();
        Collections.sort(sorts, (o1, o2) -> Integer.compare(o1.getHour() * 60 + o1.getMin(), o2.getHour() * 60 + o2.getMin()));
        if(sorts.isEmpty()){
            binding.tvTips.setVisibility(View.GONE);
        }else {
            binding.tvTips.setVisibility(View.VISIBLE);
        }
        alarmAdapter.setList(sorts);
    });
}

Source: AlarmListFragment.java

注意:列表排序在 UI 观察者中执行(hour * 60 + min 数值比较),是因为设备返回的顺序不保证按时间排列;postValue 保证了蓝牙线程 → 主线程的线程安全。

示例二:创建默认闹钟并自动分配槽位

//新建一个闹钟
public AlarmBean createNewAlarm() {
    AlarmBean alarmBean = new AlarmBean();
    AlarmListInfo info = alarmsMutableLiveData.getValue();

    if (info != null) {
        List<AlarmBean> list = info.getAlarmBeans();
        boolean[] has = new boolean[5];//如果有闹钟则设置为true,则空余位为false,闹钟个数最大5个
        for (AlarmBean bean : list) {
            has[bean.getIndex()] = true;
        }
        for (byte i = 0; i < has.length; i++) {
            if (!has[i]) {
                alarmBean.setIndex(i);
                break;
            }
        }
    }
    alarmBean.setName(HealthApplication.getAppViewModel().getApplication().getString(R.string.default_alarm_name));
    Calendar calendar = Calendar.getInstance();
    alarmBean.setHour((byte) calendar.get(Calendar.HOUR_OF_DAY));
    alarmBean.setMin((byte) calendar.get(Calendar.MINUTE));
    alarmBean.setVersion(1);
    alarmBean.setOpen(true);
    String bellName = HealthApplication.getAppViewModel().getApplication().getString(R.string.alarm_bell_1);
    alarmBean.setBellName(bellName)
            .setBellType((byte) 0)
            .setBellCluster(0);
    return alarmBean;
}

Source: AlarmViewModel.java

示例三:列表项开关闹钟

使用 SwitchButton 的 setCheckedImmediatelyNoEvent 避免程序设置状态触发监听器,用户手动切换时才提交到设备:

SwitchButton switchButton = holder.getView(R.id.sw_default_alarm);
switchButton.setCheckedImmediatelyNoEvent(alarmBean.isOpen());
switchButton.setOnCheckedChangeListener((buttonView, isChecked) -> {
    alarmBean.setOpen(isChecked);
    mViewModel.updateAlarm(alarmBean, success -> showTips(success ? R.string.save_alarm_success : R.string.save_alarm_failed));
});

Source: AlarmListFragment.java

失败模式、边界情况与并发

失败模式

场景处理方式源码依据
读取闹钟列表失败Toast"闹钟列表读取失败",界面停留原状态AlarmViewModel.readAlarmList() 的 onFailed
保存闹钟失败回调 false,Toast"保存失败"updateAlarm 的 onFailed + Fragment 回调
删除闹钟失败Toast"删除失败",opCallback.onError(subCode)deleteAlarm 的 onFailed
设备不支持扩展闹钟saveBellArgs 直接回调成功(空操作),readExpandArg 直接返回hasBellArgs() 守卫
响铃时当前无 ActivityonAlarmNotify 直接返回,不弹窗(设备继续响铃)AlarmHandleTask 判空
读取铃声参数返回空以 ERR_INVALID_PARAMETER 构造 BaseError 走失败分支,回调 nullreadExpandArg

边界情况

  • 闹钟数量上限:addAlarm() 在 size() > 4 时拒绝新增并提示,这是设备端存储能力(5 个槽位)在 App 侧的同步约束;createNewAlarm() 的槽位分配算法也以 5 为硬编码上限。
  • 断连保护:AlarmListFragment 观察 mConnectionDataMLD,设备断开立即 finish() 关闭页面;AlarmHandleTask 断连时清空 UI Handler 的延迟任务。这说明闹钟配置功能严格依赖在线状态——离线时只能依赖设备本地已配置的闹钟。
  • 弹窗重入:onAlarmNotify 通过 findFragmentByTag("alarm_notify") 防重入,避免多个闹钟同时触发时弹出多个对话框。
  • 时间槽位越界:readExpandArg 使用 0x01 << index 位掩码,若 index 越界(>7)会得到无效掩码,SDK 层通过 ERR_INVALID_PARAMETER 兜底。

并发与线程安全

  • 蓝牙回调(OnOperationCallback、OnRcspEventListener)运行在 SDK 的 BLE 线程,更新 UI 状态时:LiveData 用 postValue(可在任意线程调用),弹窗操作通过 ActivityManager 获取主线程 Activity 后直接在主线程执行,Handler 使用主线程 Looper。
  • 同一时刻的多次写操作(如快速连续开关多个闹钟)串行下发到设备,SDK 内部负责命令排队;App 侧未做额外互斥,依赖设备端按命令顺序处理。
  • ViewModel 在 onCleared() 中 destroy() RTC 操作并反注册监听,防止 Fragment 销毁后回调泄漏。

性能与运维注意事项

  • 蓝牙命令开销:每次 onResume 都 readAlarmList() 一次全量读取,属于低频、小数据量操作(最多 5 条闹钟),对 BLE 带宽影响可忽略;此设计换取的是"始终与设备一致"的可靠性。
  • 时间同步:仅在系统初始化时同步一次,若同步失败仅记录日志(onFailed 分支为空),不重试——在弱网/断连场景下可能导致设备时间偏移,运维上建议在首次配对后检查设备时间。
  • 内存管理:AlarmHandleTask 持有的 mUIHandler 在断连时被清空任务,避免延迟任务持有 Activity 引用;AlarmViewModel 通过 onCleared 释放 SDK 资源。

扩展点

  1. 周期时间同步:AlarmHandleTask.updateSysTime() 中被注释的 postDelayed(..., 5000) 展示了"定期重复同步"的预留方案,如需增强时钟可靠性可启用并加断连保护。
  2. 铃声扩展(AlarmExpand):AlarmExpandCmd.BellArg、DefaultAlarmBell、AlarmBellContainerFragment(铃声选择容器页)构成了扩展闹钟的完整链路;新增铃声类型只需扩展 BellArg 字段与铃声资源。
  3. 重复星期描述:Util.getRepeatDescModify() 集中处理星期文案,新增语言/文案格式只需修改该工具方法。
  4. 事件透传:onAlarmDefaultBellListChange 目前仅 super 透传,是预留的设备默认铃声变更钩子,可在其中增加默认铃声列表的 UI 刷新。

Related Links

  • 设备管理相关页面:设备连接、WatchManager 生命周期(见目录 5-device-management 其他条目)
  • SDK 协议模型:AlarmBean、AlarmListInfo、AlarmExpandCmd、DefaultAlarmBell(com.jieli.jl_rcsp.model,SDK 模块)
  • 相关源文件:
    • AlarmListFragment.java
    • AlarmViewModel.java
    • AlarmHandleTask.java
    • AlarmSettingFragment.java
    • AlarmBellContainerFragment.java
    • EditAlarmNameFragment.java
Prev
表盘管理
Next
消息与联系人同步