闹钟与健康提醒
本文档介绍 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 弹出提醒对话框。
整个能力围绕三个核心角色展开:
- RTCOpImpl(RTC 操作实现):
com.jieli.jl_rcspSDK 中负责 RTC(实时时钟)与闹钟相关命令的封装,提供readAlarmList、addOrModifyAlarm、deleteAlarm、syncTime、stopAlarmBell、readAlarmBellArgs、setAlarmBellArg等接口。App 侧所有闹钟操作最终都通过它下发到设备。 - AlarmViewModel:闹钟业务逻辑的编排层,向 Fragment 暴露读列表、增删改、铃声参数读写等方法,并监听设备主动上报的闹钟列表变化事件,通过
MutableLiveData<AlarmListInfo>驱动 UI 刷新。 - 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<AlarmListInfo>"]
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() 的字段初始化可以还原其关键属性:
| 字段 | 类型 | 含义 | 初始化值 |
|---|---|---|---|
index | byte | 闹钟槽位索引(0~4,最多 5 个) | 首个空闲槽位 |
hour / min | byte | 触发时间(24 小时制) | 当前系统时间 |
name | String | 闹钟名称 | 默认名称字符串资源 |
open | boolean | 是否启用 | true |
version | int | 版本号 | 1 |
bellName | String | 铃声名称 | 默认铃声 1 |
bellType | byte | 铃声类型 | 0 |
bellCluster | int | 铃声簇 | 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:手表闹钟开始响铃。此时:- 获取当前 Activity(
ActivityManager.getInstance().getCurrentActivity()); - 通过
findFragmentByTag("alarm_notify")做防重入检查——若弹窗已存在则直接返回; - 构建
Jl_Dialog:标题"提示"、内容"闹钟正在响铃"(R.string.alarm_running)、右侧按钮"确定"、宽度 0.8、cancel(false)禁止点击外部取消; - 点击"确定"后调用
mRTCOp.stopAlarmBell(device, null)下发停铃命令并关闭弹窗。
- 获取当前 Activity(
@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_name | AlarmViewModel.createNewAlarm() |
| 新增默认铃声 | R.string.alarm_bell_1,bellType=0,bellCluster=0 | AlarmViewModel.createNewAlarm() |
| 新增默认时间 | 当前系统时间(Calendar) | AlarmViewModel.createNewAlarm() |
| 闹钟版本号 | 1 | AlarmViewModel.createNewAlarm() |
| 扩展闹钟能力标志 | DeviceInfo.getAlarmExpandFlag() & 0x01 | AlarmViewModel.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() 守卫 |
| 响铃时当前无 Activity | onAlarmNotify 直接返回,不弹窗(设备继续响铃) | AlarmHandleTask 判空 |
| 读取铃声参数返回空 | 以 ERR_INVALID_PARAMETER 构造 BaseError 走失败分支,回调 null | readExpandArg |
边界情况
- 闹钟数量上限:
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 资源。
扩展点
- 周期时间同步:
AlarmHandleTask.updateSysTime()中被注释的postDelayed(..., 5000)展示了"定期重复同步"的预留方案,如需增强时钟可靠性可启用并加断连保护。 - 铃声扩展(AlarmExpand):
AlarmExpandCmd.BellArg、DefaultAlarmBell、AlarmBellContainerFragment(铃声选择容器页)构成了扩展闹钟的完整链路;新增铃声类型只需扩展BellArg字段与铃声资源。 - 重复星期描述:
Util.getRepeatDescModify()集中处理星期文案,新增语言/文案格式只需修改该工具方法。 - 事件透传:
onAlarmDefaultBellListChange目前仅super透传,是预留的设备默认铃声变更钩子,可在其中增加默认铃声列表的 UI 刷新。
Related Links
- 设备管理相关页面:设备连接、WatchManager 生命周期(见目录 5-device-management 其他条目)
- SDK 协议模型:
AlarmBean、AlarmListInfo、AlarmExpandCmd、DefaultAlarmBell(com.jieli.jl_rcsp.model,SDK 模块) - 相关源文件: