表盘管理
表盘管理是 Android-JL_Health 应用中面向杰理(Jieli)智能手表的核心功能模块,负责表盘商店浏览与下载、已购表盘管理、自定义表盘制作与背景更换、表盘文件传输与持久化,以及与设备端(jl_rcsp)、文件系统(jl_fatfs)和服务端(jl_health_http)的完整联动。
Purpose and Scope
本页面系统性地介绍"表盘管理"能力的端到端实现,涵盖:
- 表盘管理入口
WatchDialFragment及其三个 Tab(表盘商店、我的表盘、自定义表盘)的组织方式; - 新一代
DialShopViewModel与旧版WatchMarketViewModel(已废弃)的服务端表盘列表分页加载机制; - 自定义表盘背景
CustomWatchBgFragment/CustomWatchBgViewModel的拍照、相册、裁剪、传输与恢复流程; - 表盘数据传输(
CustomDialManager、CustomWatchBgTransferCallback、FatFs 文件系统)与服务器缓存(WatchServerCacheHelper); - 相关数据模型(
WatchInfo、WatchOpData、DialListMsg、DialParam、WatchFileList等)与广播联动(RemovePaymentReceiver)。
本页面不覆盖:设备连接与蓝牙管理(见"设备管理"相关页面)、健康数据同步、OTA 升级等与表盘无关的能力;表盘仅作为这些能力的消费方被引用。
概述
表盘(Watch Dial)是智能手表上最直观的个性化载体。Android-JL_Health 将表盘能力组织为"商店 → 我的 → 自定义"三条路径:
- 表盘商店(DialShopFragment):从服务端按设备型号(VID/PID)与固件匹配版本拉取可下载的表盘列表,支持分页加载、下载、购买记录查看;
- 我的表盘(MyDialsFragment):展示已下载/已购买的表盘,支持应用到设备;
- 自定义表盘(CustomDialFragment / CustomWatchBgFragment):用户从相册选择或拍摄照片,经 UCrop 裁剪后通过 FatFs 文件系统写入设备外部 Flash,并可随时恢复默认背景。
模块的架构决策体现了"界面-视图模型-设备服务-文件系统"的分层思想:UI 层通过 ViewModel 与设备通信层(WatchManager)解耦;文件传输复用 jl_fatfs 的进度回调与错误码体系;服务端列表通过 jl_health_http 模型(WatchFileList、WatchProduct、DialParam)分页拉取,并用 WatchServerCacheHelper 统一错误码。
关键设计意图:表盘文件体积较大(通常数 MB),必须走 FatFs 外部 Flash 通道而非普通 RCSP 数据通道;因此模块同时依赖
jl_rcsp(获取设备信息、匹配版本)、jl_health_http(拉取表盘资源元数据)、jl_fatfs(传输表盘二进制文件),三者协作完成"元数据 + 文件"两条数据链路。
架构
flowchart TD
subgraph sg_UI["UI 层 (ui/device)"]
DialFragment["WatchDialFragment<br/>表盘入口容器"]
ShopFragment["DialShopFragment<br/>表盘商店"]
MyDialsFragment["MyDialsFragment<br/>我的表盘"]
CustomDialFragment["CustomDialFragment<br/>自定义表盘"]
DialListFragment["DialListFragment<br/>表盘列表(记录/收藏)"]
CustomBgFragment["CustomWatchBgFragment<br/>自定义背景"]
end
subgraph sg_VM["ViewModel 层"]
DialShopVM["DialShopViewModel"]
WatchMarketVM["WatchMarketViewModel<br/>(@Deprecated 旧版)"]
CustomBgVM["CustomWatchBgViewModel"]
end
subgraph sg_Service["设备/工具层 (tool)"]
WatchManager["WatchManager"]
CustomDialMgr["CustomDialManager"]
BgCallback["CustomWatchBgTransferCallback"]
ServerCache["WatchServerCacheHelper"]
end
subgraph sg_SDK["SDK 依赖"]
RCSP["jl_rcsp<br/>设备信息/操作"]
FATFS["jl_fatfs<br/>FatFs 文件传输"]
HTTP["jl_health_http<br/>服务端列表"]
end
DialFragment --> ShopFragment
DialFragment --> MyDialsFragment
DialFragment --> CustomDialFragment
DialFragment --> DialListFragment
DialFragment --> DialShopVM
ShopFragment --> DialShopVM
MyDialsFragment --> DialShopVM
CustomDialFragment --> DialShopVM
CustomBgFragment --> CustomBgVM
DialShopVM --> WatchManager
DialShopVM --> ServerCache
WatchMarketVM --> WatchManager
WatchMarketVM --> ServerCache
CustomBgVM --> CustomDialMgr
CustomDialMgr --> BgCallback
CustomBgVM --> WatchManager
WatchManager --> RCSP
WatchManager --> FATFS
DialShopVM --> HTTP
CustomBgVM --> FATFS
架构说明
- WatchDialFragment 是表盘能力的唯一入口容器,内部使用
ViewPager2+TabLayout承载三个子页面(商店/我的/自定义),并持有全局共享的DialShopViewModel,保证三个 Tab 之间数据一致(例如购买后"我的表盘"立即刷新)。 - DialShopViewModel(新)与 WatchMarketViewModel(旧,
@Deprecated)封装了服务端列表的获取逻辑:先通过WatchManager拿到已连接设备的DeviceInfo(UID/PID)与ExternalFlashMsgResponse匹配版本,再构造DialParam请求jl_health_http分页数据。 - CustomWatchBgViewModel 负责自定义背景的完整生命周期:选图 → 裁剪 → 写入文件 → 通过
CustomDialManager与CustomWatchBgTransferCallback传输到设备 FatFs → 状态/进度通过MutableLiveData回传 UI。 - WatchServerCacheHelper 统一了服务器交互的错误码(如
ERR_LOAD_FINISH、ERR_REMOTE_NOT_CONNECT、ERR_NOT_SUPPORT_VERSION),使 ViewModel 与 UI 的错误文案处理一致。
核心组件与实现
表盘管理入口:WatchDialFragment
WatchDialFragment 继承 BaseFragment,是整个表盘管理的宿主页。它的职责有三:
- 搭建三 Tab 容器:
initUI()中把ViewPager2(vp2DialContainer)与TabLayout(tbDialHeader)双向绑定,并设置setUserInputEnabled(false)—— 即禁止用户滑动切换,只能点击 Tab 切换,避免表盘列表滚动与页面切换手势冲突; - 持有共享 ViewModel:通过
DialShopViewModel.DialShopViewModelFactory(requireContext())创建,并在onDestroyView()中调用mViewModel.release()释放设备监听; - 注册支付联动广播:
RemovePaymentReceiver监听ACTION_REMOVE_PAYMENT = "com.jieli.healthaide.watch.remove_payment",收到广播后cleanCache()+listWatchList()刷新列表(例如用户在表盘商店完成购买后,由支付模块发出该广播)。
三个 Tab 页通过内部静态类 CustomFragmentStateAdapter 组装:
private static class CustomFragmentStateAdapter extends FragmentStateAdapter {
private final List<Fragment> fragmentList = new ArrayList<>();
public CustomFragmentStateAdapter(@NonNull Fragment fragment, @NonNull DialShopViewModel viewModel) {
super(fragment);
fragmentList.add(DialShopFragment.newInstance(viewModel));
fragmentList.add(MyDialsFragment.newInstance(viewModel));
fragmentList.add(CustomDialFragment.newInstance(filePath -> viewModel.listWatchList()));
}
@NonNull
@Override
public Fragment createFragment(int position) {
return fragmentList.get(position);
}
@Override
public int getItemCount() {
return fragmentList.size();
}
}
Source: WatchDialFragment.java
设计要点:
CustomDialFragment.newInstance(filePath -> viewModel.listWatchList())传入回调,当用户完成一次自定义表盘(生成文件)后自动刷新整个表盘列表,使"我的表盘"页即时出现新表盘;- 右上角"支付记录"按钮调用
toDialListFragment(HealthConstant.DIAL_TYPE_RECORD),以ContentActivity.startContentActivityForResult(...)打开DialListFragment,请求码为REQUEST_CODE_DIAL_OP = 0x6666; onActivityResult中只要请求码匹配REQUEST_CODE_DIAL_OP就重新listWatchList(),保证从表盘详情/记录页返回后数据是最新的。
表盘商店列表加载:DialShopViewModel 与旧版 WatchMarketViewModel
旧版 WatchMarketViewModel(标注 @Deprecated,由 DialShopViewModel 取代)完整展示了服务端表盘列表的拉取算法:
public void getServiceWatchList(int page) {
if (!isConnectedDevice(mTargetDev)) {
publishWatchMarketFail(WatchServerCacheHelper.ERR_REMOTE_NOT_CONNECT, mFragment.getString(R.string.device_is_disconnected));
return;
}
if (mDialListMsg.isLoadFinish()) {
publishWatchMarketFail(WatchServerCacheHelper.ERR_LOAD_FINISH, mFragment.getString(R.string.last_page));
return;
}
DeviceInfo deviceInfo = getDeviceInfo(mTargetDev);
if (null == deviceInfo) return;
int vid = deviceInfo.getUid();//2
int pid = deviceInfo.getPid();//49
List<String> versionList = new ArrayList<>();
DialParam param = new DialParam();
ExternalFlashMsgResponse externalFlashMsg = mWatchManager.getExternalFlashMsg(mTargetDev);
if (null != externalFlashMsg) {
String[] versions = externalFlashMsg.getMatchVersions();
if (versions != null) {
versionList.addAll(Arrays.asList(versions));
}
}
if (versionList.isEmpty()) {
publishWatchMarketFail(WatchServerCacheHelper.ERR_NOT_SUPPORT_VERSION, mFragment.getString(R.string.server_none_device_support_version));
return;
}
if (isRequestServer) {
return;
}
...
}
Source: WatchMarketViewModel.java
这段代码揭示了表盘商店请求的前置条件链(设计意图:避免无效网络请求):
- 设备必须在线:
isConnectedDevice(mTargetDev)失败直接报ERR_REMOTE_NOT_CONNECT; - 列表必须未加载完:
DialListMsg.isLoadFinish()为 true 时不再请求(PAGE_NUM = 15一页); - 必须能拿到设备型号:
DeviceInfo的getUid()(VID)与getPid()(PID)用于服务端筛选机型; - 必须存在固件匹配版本:从
mWatchManager.getExternalFlashMsg(mTargetDev)的getMatchVersions()读取支持的表盘版本列表,为空则报ERR_NOT_SUPPORT_VERSION; - 防重入:
isRequestServer标志防止同一时刻并发请求。
分页状态由 DialListMsg 维护(resetServiceParam() 重置 size/currentPage/list),loadServiceWatchList() 每次请求 currentPage + 1 页。请求成功后将新页数据追加进列表,并通过 mWatchMarketResultMLD(MutableLiveData<WatchListResult>)通知 UI。
自定义表盘背景:CustomWatchBgFragment 与 CustomWatchBgViewModel
自定义背景是表盘管理中最复杂的交互链路,涉及权限申请、图片选择、裁剪、文件写入与设备传输。CustomWatchBgFragment 使用 permissions.dispatcher 注解驱动运行时权限:
@RuntimePermissions
public class CustomWatchBgFragment extends BaseFragment {
...
private final static int REQUEST_CODE_TAKE_PHOTO = 0x1458;
private final static int REQUEST_CODE_ALBUM = 0x1459;
private final static int REQUEST_CODE_CROP_PHOTO = 0x1460;
private Uri mSrcImageUri;
public static CustomWatchBgFragment newInstance() {
return new CustomWatchBgFragment();
}
...
}
Source: CustomWatchBgFragment.java
其关键流程与状态机如下:
- 进入页面时从
HealthConstant.EXTRA_WATCH_INFO取出当前WatchInfo(即用户选择要更换背景的表盘),计算背景保存路径HealthUtil.createFilePath(...) + HealthConstant.DIR_WATCH + bgName; - 点击"选择图片"弹出
ChoosePhotoDialog(拍照 / 相册),拍照走FileProvider,相册走MediaStore,随后统一进入CropPhotoActivity/UCrop裁剪; mViewModel.mCustomBgStatusMLD以CustomBgStatus(STATUS_START/STATUS_PROGRESS/ ...)驱动UpdateResourceDialog展示传输进度;- 点击"恢复默认"调用
mViewModel.restoreCustomBg(); - 监听
mConnectionDataMLD(设备断开即关闭页面)与mChangeWatchMLD(表盘切换即关闭页面),保证自定义背景操作始终处于"已连接且当前表盘未变"的上下文。
传输层由 CustomDialManager 与 CustomWatchBgTransferCallback 完成:前者封装 FatFs 写入的编排,后者实现 OnFatFileProgressListener 报告进度,失败时返回 FatFsErrCode。错误码体系贯穿 jl_fatfs(如空间不足、路径错误)与 jl_rcsp(设备未连接),UI 通过 ResultDialog 统一展示。
核心流程
表盘商店列表加载流程
sequenceDiagram
participant U as 用户
participant F as WatchDialFragment
participant VM as DialShopViewModel
participant WM as WatchManager
participant RCSP as jl_rcsp(设备)
participant HTTP as jl_health_http(服务端)
U->>F: 进入表盘管理页
F->>VM: 创建 DialShopViewModel
F->>VM: listWatchList() / loadServiceWatchList()
VM->>WM: getConnectedDevice() / isConnectedDevice()
alt 设备未连接
VM-->>F: ERR_REMOTE_NOT_CONNECT
else 设备已连接
VM->>WM: getDeviceInfo() → uid/pid
VM->>WM: getExternalFlashMsg() → matchVersions
alt 无匹配版本
VM-->>F: ERR_NOT_SUPPORT_VERSION
else 有匹配版本
VM->>HTTP: DialParam(vid, pid, versions, page)
HTTP-->>VM: WatchListResult(分页数据)
VM-->>F: mWatchMarketResultMLD 更新
F-->>U: 渲染表盘列表
end
end
自定义表盘背景传输流程
flowchart TD
Start([用户点击选择图片]) --> Perm{"运行时权限<br/>相机/存储"}
Perm -->|"拒绝"| Denied["PermissionDialog 引导"]
Perm -->|"允许"| Choice{"ChoosePhotoDialog"}
Choice -->|"拍照"| Camera["FileProvider 拍摄"]
Choice -->|"相册"| Album["MediaStore 选择"]
Camera --> Crop["UCrop / CropPhotoActivity 裁剪"]
Album --> Crop
Crop --> Save["保存到 DIR_WATCH 目录"]
Save --> Send["CustomDialManager 传输"]
Send --> Progress{"CustomWatchBgTransferCallback<br/>进度回调"}
Progress -->|"STATUS_PROGRESS"| Dialog["UpdateResourceDialog 显示进度"]
Progress -->|"完成"| Done["刷新表盘列表"]
Progress -->|"失败"| Fail["FatFsErrCode → ResultDialog"]
Done --> End([结束])
Fail --> End
支付联动与返回刷新
RemovePaymentReceiver 是模块间解耦的典型:表盘商店的购买/退款动作发生在其它页面(如 DialListFragment 记录页),通过系统广播 ACTION_REMOVE_PAYMENT 通知 WatchDialFragment 刷新。同样,onActivityResult(REQUEST_CODE_DIAL_OP)在从记录页返回时也会触发 listWatchList()。这两条路径共同保证"购买 → 我的表盘立即可见"、"删除/退款 → 列表即时同步"。
数据模型
表盘管理涉及两类数据:服务端列表模型(jl_health_http 提供)与设备侧模型(ui/device/bean 提供)。
erDiagram
WATCHINFO {
string name
string bitmapUri
string filePath
}
WATCHOPDATA {
string name
string opType
string result
}
DIALLISTMSG {
int size
int currentPage
bool loadFinish
list list
}
WATCHLISTRESULT {
int code
string message
list data
}
DIALPARAM {
int vid
int pid
list matchVersions
int page
}
WATCHFILEMSG {
string fileId
string fileName
string fileUrl
int fileSize
}
WATCHINFO ||--o| WATCHOPDATA : "操作结果"
DIALLISTMSG ||--o{ WATCHFILEMSG : "分页包含"
WATCHLISTRESULT ||--o{ WATCHFILEMSG : "结果包含"
DIALPARAM ||--|| WATCHFILEMSG : "请求筛选"
- WatchInfo:当前选中表盘的信息(名称、位图 URI),通过
HealthConstant.EXTRA_WATCH_INFO在页面间传递,CustomWatchBgFragment据此确定背景保存文件名getCustomBgName(watchInfo.getName()); - WatchOpData:设备操作(如应用表盘、删除表盘)的请求/响应载体,由
WatchViewModel统一管理; - DialListMsg:内存中的分页状态机(
size、currentPage、loadFinish),resetServiceParam()负责重置; - DialParam:构造服务端请求的参数对象,携带 VID、PID、固件匹配版本与页码;
- WatchFileList / WatchFileMsg / WatchProduct:
jl_health_http的服务端表盘文件元数据模型(文件 ID、名称、下载 URL、大小)。
持久化方面,自定义背景图片保存在 HealthUtil.createFilePath(requireContext(), HealthConstant.DIR_WATCH) 目录下;已下载表盘由 FatFs 写入设备外部 Flash,App 本地只保留元数据与缩略图(bitmapUri)。
使用示例
示例一:启动表盘管理页(三个 Tab 的组装)
WatchDialFragment 通过 FragmentStateAdapter 一次性组装"商店 / 我的 / 自定义"三个页面,并把共享 ViewModel 传入:
private static class CustomFragmentStateAdapter extends FragmentStateAdapter {
private final List<Fragment> fragmentList = new ArrayList<>();
public CustomFragmentStateAdapter(@NonNull Fragment fragment, @NonNull DialShopViewModel viewModel) {
super(fragment);
fragmentList.add(DialShopFragment.newInstance(viewModel));
fragmentList.add(MyDialsFragment.newInstance(viewModel));
fragmentList.add(CustomDialFragment.newInstance(filePath -> viewModel.listWatchList()));
}
@NonNull
@Override
public Fragment createFragment(int position) {
return fragmentList.get(position);
}
@Override
public int getItemCount() {
return fragmentList.size();
}
}
Source: WatchDialFragment.java
使用场景:任何需要进入表盘功能的入口(设备详情页"表盘"按钮)只需以 Fragment 形式加载 WatchDialFragment 即可。
示例二:打开表盘记录列表并等待结果
表盘管理页右上角"支付记录"通过 ContentActivity 打开 DialListFragment,并使用请求码 0x6666 等待返回后刷新:
public static final int REQUEST_CODE_DIAL_OP = 0x6666;
...
private void toDialListFragment(int dialType) {
Bundle bundle = new Bundle();
bundle.putInt(DialListFragment.EXTRA_DIAL_TYPE, dialType);
ContentActivity.startContentActivityForResult(WatchDialFragment.this, DialListFragment.class.getCanonicalName(), bundle, REQUEST_CODE_DIAL_OP);
}
@Override
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_DIAL_OP) {
mViewModel.listWatchList();
}
}
Source: WatchDialFragment.java
使用场景:查看购买记录/收藏列表后返回,页面自动同步最新表盘状态。
示例三:请求服务端表盘列表(含前置校验)
旧版 WatchMarketViewModel.getServiceWatchList(int page) 演示了完整的服务端列表请求模式(新版 DialShopViewModel 复用同一套校验链):
public void getServiceWatchList(int page) {
if (!isConnectedDevice(mTargetDev)) {
publishWatchMarketFail(WatchServerCacheHelper.ERR_REMOTE_NOT_CONNECT, mFragment.getString(R.string.device_is_disconnected));
return;
}
if (mDialListMsg.isLoadFinish()) {
publishWatchMarketFail(WatchServerCacheHelper.ERR_LOAD_FINISH, mFragment.getString(R.string.last_page));
return;
}
DeviceInfo deviceInfo = getDeviceInfo(mTargetDev);
if (null == deviceInfo) return;
int vid = deviceInfo.getUid();//2
int pid = deviceInfo.getPid();//49
List<String> versionList = new ArrayList<>();
DialParam param = new DialParam();
ExternalFlashMsgResponse externalFlashMsg = mWatchManager.getExternalFlashMsg(mTargetDev);
if (null != externalFlashMsg) {
String[] versions = externalFlashMsg.getMatchVersions();
if (versions != null) {
versionList.addAll(Arrays.asList(versions));
}
}
if (versionList.isEmpty()) {
publishWatchMarketFail(WatchServerCacheHelper.ERR_NOT_SUPPORT_VERSION, mFragment.getString(R.string.server_none_device_support_version));
return;
}
if (isRequestServer) {
return;
}
...
}
Source: WatchMarketViewModel.java
示例四:自定义表盘背景页的初始化与观察
CustomWatchBgFragment 展示了如何从 Intent 读取 WatchInfo、建立保存路径并订阅状态 LiveData:
mViewModel = new ViewModelProvider(this).get(CustomWatchBgViewModel.class);
if (getArguments() != null) {
mViewModel.mWatchInfo = getArguments().getParcelable(HealthConstant.EXTRA_WATCH_INFO);
}
final WatchInfo watchInfo = mViewModel.mWatchInfo;
if (watchInfo == null) {
requireActivity().finish();
return;
}
String bgName = mViewModel.getCustomBgName(watchInfo.getName());
mViewModel.mPhotoSavePath = new File(HealthUtil.createFilePath(requireContext(), HealthConstant.DIR_WATCH)
+ File.separator + bgName);
HealthUtil.updateWatchImg(requireContext(), mBinding.ivCustomWatchBgImg, watchInfo.getBitmapUri());
Source: CustomWatchBgFragment.java
配置选项
表盘管理模块的"配置"主要体现在 HealthConstant 常量与 WatchServerCacheHelper 错误码上,关键项如下:
| 配置/常量 | 类型 | 值/默认 | 说明 |
|---|---|---|---|
HealthConstant.DIAL_TYPE_RECORD | int | - | 表盘列表类型:购买/支付记录 |
HealthConstant.EXTRA_WATCH_INFO | String | - | 页面间传递 WatchInfo 的 Bundle key |
HealthConstant.DIR_WATCH | String | - | 表盘/背景文件本地保存目录名 |
WatchDialFragment.REQUEST_CODE_DIAL_OP | int | 0x6666 | 打开记录列表的 ActivityResult 请求码 |
CustomWatchBgFragment.REQUEST_CODE_TAKE_PHOTO | int | 0x1458 | 拍照请求码 |
CustomWatchBgFragment.REQUEST_CODE_ALBUM | int | 0x1459 | 相册请求码 |
CustomWatchBgFragment.REQUEST_CODE_CROP_PHOTO | int | 0x1460 | 裁剪请求码 |
RemovePaymentReceiver.ACTION_REMOVE_PAYMENT | String | com.jieli.healthaide.watch.remove_payment | 支付变更广播 Action |
WatchMarketViewModel.PAGE_NUM | int | 15 | 服务端表盘列表每页条数 |
WatchServerCacheHelper.ERR_LOAD_FINISH | int | - | 列表已加载完毕错误码 |
WatchServerCacheHelper.ERR_REMOTE_NOT_CONNECT | int | - | 设备未连接错误码 |
WatchServerCacheHelper.ERR_NOT_SUPPORT_VERSION | int | - | 固件无匹配表盘版本错误码 |
说明:以上常量定义于 HealthConstant.java、WatchDialFragment.java、WatchServerCacheHelper.java;具体数值以源码为准。
API 参考
WatchDialFragment(表盘管理入口)
| 成员 | 签名 | 说明 |
|---|---|---|
initUI() | private void initUI() | 初始化顶栏、TabLayout、ViewPager2 与 FragmentStateAdapter |
toDialListFragment(int) | private void toDialListFragment(int dialType) | 打开 DialListFragment(记录/收藏列表) |
registerPaymentReceiver() | private void registerPaymentReceiver() | 注册 RemovePaymentReceiver 广播 |
unregisterPaymentReceiver() | private void unregisterPaymentReceiver() | 注销广播,避免泄漏 |
onActivityResult(int,int,Intent) | @Override public void onActivityResult(...) | 请求码为 REQUEST_CODE_DIAL_OP 时刷新表盘列表 |
onDestroyView() | @Override public void onDestroyView() | 注销广播、解绑页面回调、释放 ViewModel |
WatchMarketViewModel(旧版,@Deprecated)
| 成员 | 签名 | 说明 |
|---|---|---|
resetServiceParam() | public void resetServiceParam() | 重置 DialListMsg 分页状态 |
loadServiceWatchList() | public void loadServiceWatchList() | 加载下一页服务端表盘列表 |
getServiceWatchList(int) | public void getServiceWatchList(int page) | 按页请求服务端列表(含设备/版本校验) |
mWatchMarketResultMLD | MutableLiveData<WatchListResult> | 列表结果 LiveData,UI 订阅刷新 |
CustomWatchBgFragment(自定义背景)
| 成员 | 签名 | 说明 |
|---|---|---|
newInstance() | public static CustomWatchBgFragment newInstance() | 工厂方法创建实例 |
onActivityCreated(...) | @Override public void onActivityCreated(...) | 读取 WatchInfo、建立保存路径、订阅状态 |
showChoosePhotoDialog() | private void showChoosePhotoDialog() | 弹出拍照/相册选择对话框(触发权限流程) |
RemovePaymentReceiver(支付联动广播)
| 成员 | 签名 | 说明 |
|---|---|---|
ACTION_REMOVE_PAYMENT | public static final String | 广播 Action:com.jieli.healthaide.watch.remove_payment |
onReceive(Context, Intent) | @Override public void onReceive(...) | 收到广播后 cleanCache() + listWatchList() |
失败模式、边界情况与并发
设备状态相关的失败
表盘管理强依赖设备连接状态,源码中有多处防护:
- 设备未连接:
getServiceWatchList首先检查isConnectedDevice(mTargetDev),失败即返回ERR_REMOTE_NOT_CONNECT(文案device_is_disconnected),不会发起无效网络请求; - 设备中途断开:
CustomWatchBgFragment订阅mConnectionDataMLD,一旦BluetoothConstant.CONNECT_STATE_CONNECTED不成立立即finish()当前页面,防止在断连状态下继续传输损坏文件; - 表盘被切换:订阅
mChangeWatchMLD,用户切到其它表盘时关闭自定义背景页,避免背景写入错误的表盘上下文。
服务端与分页边界
- 版本不匹配:设备固件无匹配版本(
getMatchVersions()为空)时返回ERR_NOT_SUPPORT_VERSION,UI 提示"服务端无该设备支持版本"。这是重要的产品约束:表盘资源按固件版本分发,防止下载后无法使用; - 末页:
DialListMsg.isLoadFinish()为 true 后不再发起请求,loadServiceWatchList()直接返回ERR_LOAD_FINISH("已经是最后一页"),避免无限滚动请求; - 防重入:
isRequestServer布尔标志保证同一时刻只有一个服务端请求在途,配合分页状态避免重复页数据叠加。
并发与资源泄漏防护
- ViewModel 释放:
WatchDialFragment.onDestroyView()调用mViewModel.release()(基类WatchViewModel释放设备操作监听),防止 Fragment 销毁后回调更新已销毁 UI; - 广播生命周期:
registerPaymentReceiver使用ContextCompat.registerReceiver(..., RECEIVER_EXPORTED)注册,onDestroyView()中成对unregisterPaymentReceiver(),避免泄漏;RemovePaymentReceiver持有DialShopViewModel引用,其生命周期与 Fragment 绑定; - 列表缓存:收到
ACTION_REMOVE_PAYMENT后先cleanCache()再listWatchList(),确保退款/删除后本地缓存与服务端一致。
文件与权限边界
- 运行时权限:拍照/相册读取依赖
permissions.dispatcher,拒绝与"不再询问"分别走PermissionDialog引导(isUserNeverAskAgain标志区分场景); - 图片来源:拍照走
FileProvider(BuildConfig配置 authority),相册走MediaStore,统一交给UCrop/CropPhotoActivity裁剪,规避不同 Android 版本相册 URI 差异; - 保存路径:背景文件写入
HealthUtil.createFilePath(requireContext(), HealthConstant.DIR_WATCH),由工具类保证目录存在;mPhotoSavePath为空或WatchInfo缺失时直接关闭页面(requireActivity().finish())。
性能与运维注意事项
- 分页加载:服务端列表按
PAGE_NUM = 15分页,每次只请求一页,ViewPager2设置setOffscreenPageLimit(getItemCount())将三个 Tab 全部预加载,换取切换零延迟(内存换响应速度); - 列表防抖:
isRequestServer与isLoadFinish双重防重入,避免用户快速上滑触发多个并发 HTTP 请求; - 传输进度:FatFs 传输通过
OnFatFileProgressListener回调进度,UI 层UpdateResourceDialog展示;传输大文件时建议保持设备连接稳定,弱网/断连场景由错误码FatFsErrCode驱动ResultDialog提示; - 调试工具:仓库
apk/tool/下提供WatchTestTool调试包,可用于独立验证设备侧表盘/FatFs 行为。
扩展点
- 新增表盘列表类型:
DialListFragment通过EXTRA_DIAL_TYPE(如HealthConstant.DIAL_TYPE_RECORD)区分列表内容,新增"收藏/推荐"等类型只需扩展HealthConstant常量并复用该 Fragment; - 自定义表盘文件生成:
CustomDialFragment.newInstance(filePath -> viewModel.listWatchList())接受文件路径回调,第三方自定义表盘生成器(如导入外部资源)只需回调同一接口即可接入列表刷新; - 支付联动:任何支付/退款成功点发送
ACTION_REMOVE_PAYMENT广播即可触发表盘列表与缓存刷新,无需修改WatchDialFragment; - SDK 替换:设备通信全部收敛在
WatchManager/WatchViewModel层,更换jl_rcsp/jl_fatfs版本或替换厂商 SDK 时,UI 与 ViewModel 层无需改动。
测试
仓库当前未提供表盘管理专门的单元测试文件(ui/device/market、ui/device/watch 目录下未见对应 *Test.java)。相关验证依赖:
WatchTestTool(apk/tool/WatchTestTool_V0.9.0_619_20260202-debug.apk)用于设备端功能验证;JL_WatchAAR(JL_Watch_V1.14.0_11307-release.aar)提供 SDK 层保证。
建议在扩展本模块时补充:分页边界(末页/重置)、设备断连时序、支付广播触发刷新、权限拒绝/不再询问等场景的自动化测试。
相关链接
- WatchDialFragment.java — 表盘管理入口容器
- WatchMarketViewModel.java — 旧版服务端列表 ViewModel
- CustomWatchBgFragment.java — 自定义表盘背景页
- CustomWatchBgViewModel.java — 自定义背景 ViewModel
- CustomWatchBgTransferCallback.java — FatFs 传输回调
- WatchManager.java — 设备操作服务
- WatchServerCacheHelper.java — 服务端缓存与错误码
- WatchMarketFragment.java — 旧版市场页(已被
WatchDialFragment取代)
设备连接与通信的底层机制请参见"设备管理"目录下其它页面;表盘文件传输复用的
jl_fatfs能力详见 SDK 文档。