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

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

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

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

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

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

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

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

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

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

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

表盘管理

表盘管理是 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 将表盘能力组织为"商店 → 我的 → 自定义"三条路径:

  1. 表盘商店(DialShopFragment):从服务端按设备型号(VID/PID)与固件匹配版本拉取可下载的表盘列表,支持分页加载、下载、购买记录查看;
  2. 我的表盘(MyDialsFragment):展示已下载/已购买的表盘,支持应用到设备;
  3. 自定义表盘(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,是整个表盘管理的宿主页。它的职责有三:

  1. 搭建三 Tab 容器:initUI() 中把 ViewPager2(vp2DialContainer)与 TabLayout(tbDialHeader)双向绑定,并设置 setUserInputEnabled(false) —— 即禁止用户滑动切换,只能点击 Tab 切换,避免表盘列表滚动与页面切换手势冲突;
  2. 持有共享 ViewModel:通过 DialShopViewModel.DialShopViewModelFactory(requireContext()) 创建,并在 onDestroyView() 中调用 mViewModel.release() 释放设备监听;
  3. 注册支付联动广播: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

这段代码揭示了表盘商店请求的前置条件链(设计意图:避免无效网络请求):

  1. 设备必须在线:isConnectedDevice(mTargetDev) 失败直接报 ERR_REMOTE_NOT_CONNECT;
  2. 列表必须未加载完:DialListMsg.isLoadFinish() 为 true 时不再请求(PAGE_NUM = 15 一页);
  3. 必须能拿到设备型号:DeviceInfo 的 getUid()(VID)与 getPid()(PID)用于服务端筛选机型;
  4. 必须存在固件匹配版本:从 mWatchManager.getExternalFlashMsg(mTargetDev) 的 getMatchVersions() 读取支持的表盘版本列表,为空则报 ERR_NOT_SUPPORT_VERSION;
  5. 防重入: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

其关键流程与状态机如下:

  1. 进入页面时从 HealthConstant.EXTRA_WATCH_INFO 取出当前 WatchInfo(即用户选择要更换背景的表盘),计算背景保存路径 HealthUtil.createFilePath(...) + HealthConstant.DIR_WATCH + bgName;
  2. 点击"选择图片"弹出 ChoosePhotoDialog(拍照 / 相册),拍照走 FileProvider,相册走 MediaStore,随后统一进入 CropPhotoActivity / UCrop 裁剪;
  3. mViewModel.mCustomBgStatusMLD 以 CustomBgStatus(STATUS_START / STATUS_PROGRESS / ...)驱动 UpdateResourceDialog 展示传输进度;
  4. 点击"恢复默认"调用 mViewModel.restoreCustomBg();
  5. 监听 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_RECORDint-表盘列表类型:购买/支付记录
HealthConstant.EXTRA_WATCH_INFOString-页面间传递 WatchInfo 的 Bundle key
HealthConstant.DIR_WATCHString-表盘/背景文件本地保存目录名
WatchDialFragment.REQUEST_CODE_DIAL_OPint0x6666打开记录列表的 ActivityResult 请求码
CustomWatchBgFragment.REQUEST_CODE_TAKE_PHOTOint0x1458拍照请求码
CustomWatchBgFragment.REQUEST_CODE_ALBUMint0x1459相册请求码
CustomWatchBgFragment.REQUEST_CODE_CROP_PHOTOint0x1460裁剪请求码
RemovePaymentReceiver.ACTION_REMOVE_PAYMENTStringcom.jieli.healthaide.watch.remove_payment支付变更广播 Action
WatchMarketViewModel.PAGE_NUMint15服务端表盘列表每页条数
WatchServerCacheHelper.ERR_LOAD_FINISHint-列表已加载完毕错误码
WatchServerCacheHelper.ERR_REMOTE_NOT_CONNECTint-设备未连接错误码
WatchServerCacheHelper.ERR_NOT_SUPPORT_VERSIONint-固件无匹配表盘版本错误码

说明:以上常量定义于 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)按页请求服务端列表(含设备/版本校验)
mWatchMarketResultMLDMutableLiveData<WatchListResult>列表结果 LiveData,UI 订阅刷新

CustomWatchBgFragment(自定义背景)

成员签名说明
newInstance()public static CustomWatchBgFragment newInstance()工厂方法创建实例
onActivityCreated(...)@Override public void onActivityCreated(...)读取 WatchInfo、建立保存路径、订阅状态
showChoosePhotoDialog()private void showChoosePhotoDialog()弹出拍照/相册选择对话框(触发权限流程)

RemovePaymentReceiver(支付联动广播)

成员签名说明
ACTION_REMOVE_PAYMENTpublic 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 行为。

扩展点

  1. 新增表盘列表类型:DialListFragment 通过 EXTRA_DIAL_TYPE(如 HealthConstant.DIAL_TYPE_RECORD)区分列表内容,新增"收藏/推荐"等类型只需扩展 HealthConstant 常量并复用该 Fragment;
  2. 自定义表盘文件生成:CustomDialFragment.newInstance(filePath -> viewModel.listWatchList()) 接受文件路径回调,第三方自定义表盘生成器(如导入外部资源)只需回调同一接口即可接入列表刷新;
  3. 支付联动:任何支付/退款成功点发送 ACTION_REMOVE_PAYMENT 广播即可触发表盘列表与缓存刷新,无需修改 WatchDialFragment;
  4. 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_Watch AAR(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 文档。

Next
闹钟与健康提醒