文件传输与文件管理
本页面介绍 Android-JL_Health 健康助手 App 与智能手表(穿戴设备)之间的文件传输与文件管理能力:包括设备端文件系统的浏览、文件下载(传输到手机)、文件删除、存储卡格式化、设备存储信息查询,以及手机本地文件的路径解析与存储管理。整个能力基于杰理(Jieli)jl_filebrowse 与 jl_rcsp SDK 通过 BLE 通道实现。
Purpose and Scope
本页面覆盖以下内容:
- 设备文件浏览:通过
FileBrowseManager获取在线设备(SD 卡)列表、读取目录、加载更多、进入/返回子目录; - 文件传输:通过
GetFileByClusterTask/GetFileByNameTask将设备端文件按簇或按名称下载到手机本地路径; - 文件管理操作:删除设备文件(
deleteFile与DeleteFileByNameCmd)、格式化存储卡(FormatTask)、点播设备文件(playFile); - 设备存储信息:
DevStorageInfo/DevStorageState对应的 RCSP 命令(GetSysInfoCmd)与属性读取; - 手机本地文件处理:
UriTool将content:///file://URI 解析为真实路径,HealthUtil基于getExternalFilesDir管理应用私有外部目录。
不在本页面范围内(由其他目录页负责)的内容:BLE 连接建立与 WatchManager 的整体初始化、RCSP 协议命令的底层编解码细节、健康数据(心率/血氧等)的业务解析。本页面聚焦"文件"这一传输媒体本身。
Overview
在健康手表类应用中,手机 App 与设备之间的文件传输是最基础的能力之一:固件/资源升级文件需要下发到设备,设备的录音、运动记录等数据需要上传到手机,SD 卡上的媒体文件需要被浏览、点播或删除。Android-JL_Health 通过封装杰理 jl_rcsp(RCSP 双向通信协议)与 jl_filebrowse(文件浏览模块)两个 SDK 组件实现上述能力。
关键设计思想:
- 分层解耦:App 层(
WatchManager及 Demo 测试)只面向高层 API,如FileBrowseManager.getInstance()、GetFileByClusterTask;真正的协议交互(RCSP 命令构造、BLE 分包收发、重传与应答)由 SDK 内部完成。Demo 注释明确要求"须在 1.3 配置好 sdk"(即先完成WatchManager/RCSP 的初始化与连接)。 - 观察者模式:文件浏览采用
FileObserver观察者回调,SD 卡在线状态变化、文件列表读取开始/结束/失败、歌曲点播结果都通过回调通知上层,避免轮询。 - 任务模式:文件下载(
GetFileByClusterTask/GetFileByNameTask)与格式化(FormatTask)被建模为可启动、带进度与取消语义的任务,通过TaskListener上报onBegin/onProgress/onFinish/onError/onCancel。 - 文件句柄模型:设备端文件通过
SDCardBean.getDevHandler()(设备句柄)与FileStruct.getCluster()(簇号)唯一定位,传输参数即(devHandle, offset, cluster, path)。
Architecture
flowchart TD
subgraph sg_App["App 层(HealthAide)"]
WatchManager["WatchManager<br/>(WatchOpImpl 子类)"]
Demo["FileManagerDemo / 业务 UI"]
UriTool["UriTool 本地路径解析"]
HealthUtil["HealthUtil 外部存储管理"]
end
subgraph sg_Sdk["SDK 层(jl_rcsp / jl_filebrowse)"]
FBM["FileBrowseManager"]
FO["FileObserver 回调接口"]
TaskGetCluster["GetFileByClusterTask"]
TaskGetName["GetFileByNameTask"]
TaskFormat["FormatTask"]
CmdDelete["DeleteFileByNameCmd"]
CmdSysInfo["GetSysInfoCmd"]
DevStorage["DevStorageInfo / DevStorageState"]
end
subgraph sg_Transport["传输层"]
RCSP["RCSP 协议引擎<br/>(CommandBuilder / RcspCommandCallback)"]
BLE["BLE GATT 通道"]
end
subgraph sg_Device["设备端"]
FileSystem["设备文件系统 / SD 卡"]
end
Demo --> FBM
Demo --> TaskGetCluster
Demo --> TaskGetName
Demo --> TaskFormat
Demo --> WatchManager
WatchManager --> RCSP
FBM --> FO
FBM --> RCSP
TaskGetCluster --> RCSP
TaskGetName --> RCSP
TaskFormat --> RCSP
CmdDelete --> RCSP
CmdSysInfo --> DevStorage
CmdSysInfo --> RCSP
RCSP --> BLE
BLE --> FileSystem
UriTool --> HealthUtil
架构说明:
FileBrowseManager(单例)是设备文件浏览的入口,FileObserver是上层订阅浏览事件与 SD 卡在线状态的回调接口;Demo 中的brow()展示了"注册观察者 → 获取在线设备 → 读取当前目录 → 目录导航 → 点播"的标准流程。- 文件下载、格式化、删除分别由
GetFileByClusterTask、GetFileByNameTask、FormatTask、DeleteFileByNameCmd承载,它们最终都依赖WatchManager(WatchOpImpl子类)驱动 RCSP 命令。 DevStorageInfo/DevStorageState是GetSysInfoCmd的响应模型,用于获取设备存储容量与状态。- 手机侧,
UriTool.getPath()将系统返回的 URI(如content://media/...、content://com.android.providers.downloads/...)转换为真实文件路径,供 SDK 保存下载文件;HealthUtil提供基于context.getExternalFilesDir(null)的应用私有目录定位能力。
设备端文件浏览与目录管理
FileBrowseManager 与 FileObserver
FileBrowseManager 是 jl_filebrowse 模块对外暴露的单例门面(FileBrowseManager.getInstance()),负责维护在线设备(SD 卡)列表、当前浏览目录状态以及向观察者派发事件。FileObserver 是上层必须实现的观察者接口,Demo 中展示了全部回调:
Source: FileManagerDemo.java
FileObserver fileObserver = new FileObserver() {
@Override
public void onFileReceiver(List<FileStruct> fileStructs) {
// 读取到文件列表,仅仅回调本次读取的文件列表
}
@Override
public void onFileReadStop(boolean isEnd) {
// 文件列表读取结束
}
@Override
public void onFileReadStart() {
// 开始文件列表读取
}
@Override
public void onFileReadFailed(int reason) {
// 文件列表读取失败
}
@Override
public void onSdCardStatusChange(List<SDCardBean> onLineCards) {
//在线设备有变化
}
@Override
public void OnFlayCallback(boolean success) {
//歌曲点播回调
}
};
回调语义与设计意图:
| 回调 | 触发时机 | 业务用途 |
|---|---|---|
onFileReceiver(List<FileStruct>) | 每次读到一批文件列表条目 | 增量刷新列表 UI("仅仅回调本次读取的文件列表",需自行累积) |
onFileReadStart() | 开始读取目录 | 显示加载中状态 |
onFileReadStop(boolean isEnd) | 列表读取结束 | isEnd 标记是否为最后一批,用于隐藏加载动画 |
onFileReadFailed(int reason) | 读取失败 | 展示错误提示;reason 为失败码 |
onSdCardStatusChange(List<SDCardBean>) | 在线 SD 卡集合变化(插拔/连接断开) | 重新拉取设备列表 |
OnFlayCallback(boolean success) | 歌曲点播结果 | 点播成功/失败提示 |
标准浏览流程
Demo brow() 给出了五个步骤的完整调用序列:
Source: FileManagerDemo.java
//第一步:注册观察者
manager.addFileObserver(fileObserver);
// 第2步:获取在线设备列表,可以通过fileObserver处理设备状态变化
List<SDCardBean> list = manager.getOnlineDev();
if (list.size() < 1) {
//没有在线设备
return;
}
SDCardBean sdCardBean = list.get(0);//获取设备,如果有多个设备,请根据需求获取相应的设备
Folder currentFolder = manager.getCurrentReadFile(sdCardBean);
//第4步:获取当前目录下已经读了但在缓存中的子文件
List<FileStruct> fileStructs = currentFolder.getChildFileStructs();
//第5步:浏览操作
//加载更多
manager.loadMore(sdCardBean);
//进入下一级目录
FileStruct fileStruct = currentFolder.getChildFileStructs().get(0);//根据需要获取需要读取的文件夹
manager.appenBrowse(fileStruct, sdCardBean);
//返回上一级目录没有列表回调
boolean hasEvent = true;//是否需要FileObserver的事件回调
manager.backBrowse(sdCardBean, hasEvent);
//点播文件
manager.playFile(fileStruct, sdCardBean);
关键点解读:
- 多设备支持:
getOnlineDev()返回List<SDCardBean>,App 需要自行决定操作哪个设备(Demo 取第一个)。SDCardBean是设备/SD 卡的会话模型,后续所有浏览与删除操作都要以它为上下文参数传入。 - 目录状态缓存:
getCurrentReadFile(sdCardBean)返回当前目录的Folder对象,getChildFileStructs()取出已缓存的子文件条目。这避免了每次 UI 刷新都重新走一遍协议读取。 - 目录导航:
appenBrowse(fileStruct, sdCardBean)进入下一级目录;backBrowse(sdCardBean, hasEvent)返回上一级,hasEvent控制是否需要触发FileObserver回调(返回上级时通常不需要列表回调,因此 Demo 特意说明"返回上一级目录没有列表回调")。 - 加载更多:
loadMore(sdCardBean)用于目录条目较多时分批读取,与onFileReadStop(boolean isEnd)配合判断是否还有更多数据。 - 点播:
playFile(fileStruct, sdCardBean)让设备播放指定媒体文件,结果通过OnFlayCallback(boolean success)回调。
文件删除
删除设备文件有两层 API:FileBrowseManager.deleteFile() 是面向批量的高层封装,DeleteFileByNameCmd 是面向单文件的 RCSP 命令。
deleteFile 批量删除
Source: FileManagerDemo.java
List<FileStruct> fileStructs = new ArrayList<>();//注意fileStructs一定要是在sdCardBean中
boolean withEnv = false;//准备环境,一般使用false
manager.deleteFile(sdCardBean, fileStructs, withEnv, new DeleteCallback() {
@Override
public void onSuccess(FileStruct fileStruct) {
//成功
}
@Override
public void onError(int code, FileStruct fileStruct) {
//fileStruct 删除失败
}
@Override
public void onFinish() {
//删除结束,通过onError判断是否有删除失败的文件
}
});
要点:
fileStructs必须是属于目标sdCardBean的文件条目(注释强调"注意fileStructs一定要是在sdCardBean中"),删除操作的协议上下文依赖设备句柄。withEnv控制是否为删除操作准备运行环境,一般传false(避免额外的环境准备开销)。DeleteCallback对每个文件分别回调onSuccess/onError(code, fileStruct),全部处理完统一回调onFinish();上层应在onFinish中汇总失败项并给出整体结果。
DeleteFileByNameCmd 按名称删除
SDK 还提供按文件名删除的命令模型 DeleteFileByNameCmd(位于 com.jieli.jl_rcsp.model.command.file_op 包),配合 CommandBuilder 构造、通过 RCSP 命令回调(RcspCommandCallback)发送。适用于已知文件名的精准删除场景,例如清理设备上某个固定名称的日志或临时文件。
存储卡格式化
FormatTask 对设备存储卡执行格式化,是一个典型的任务型操作:
Source: FileManagerDemo.java
//WatchManager是WatchOpImpl的子类,须在1.3配置好sdk
WatchManager watchManager = WatchManager.getInstance();
List<SDCardBean> list = FileBrowseManager.getInstance().getOnlineDev();
if (list.size() < 1) {
//没有在线设备
return;
}
SDCardBean sdCardBean = list.get(0);//获取设备,如果有多个设备,请根据需求获取相应的设备
FormatTask task = new FormatTask(watchManager, context, sdCardBean);
task.setListener(new SimpleTaskListener() {
@Override
public void onBegin() {
//开始
}
// ... onProgress / onFinish / onError / onCancel
});
设计意图:格式化属于高风险、耗时长的操作,因此被建模为带监听器的 Task,而不是一次性回调。上层用 SimpleTaskListener(TaskListener 的便捷实现)订阅 onBegin / onProgress / onFinish / onError / onCancel 生命周期事件,可在 onBegin 时锁定 UI 防误操作。
设备存储信息查询
存储信息通过 RCSP 系统信息命令 GetSysInfoCmd 获取,响应模型为 SysInfoResponse,其中包含设备存储状态模型 DevStorageInfo 与 DevStorageState(com.jieli.jl_rcsp.model.device 包)。FileManagerDemo 中引入了:
GetSysInfoCmd/GetSysInfoParam/SysInfoResponse:系统信息命令的参数与响应;DevStorageInfo:存储设备信息(如 SD 卡总容量、剩余容量);DevStorageState:存储状态(如是否挂载、是否可写);AttrAndFunCode:属性/功能码常量,用于按属性码读取设备能力;BooleanRcspActionCallback:布尔结果回调,用于"成功/失败"类命令的快捷回调封装。
典型用途:在文件传输前查询设备剩余空间,若空间不足则提示用户清理或格式化后再传输;或在文件浏览器中展示容量占用。
文件传输任务(下载到手机)
GetFileByClusterTask 按簇下载
设备文件系统以"簇"(cluster)为最小分配单位,因此按簇下载是最通用的文件读取方式。Demo read() 展示了完整用法:
Source: FileManagerDemo.java
//WatchManager是WatchOpImpl的子类,须在1.3配置好sdk
WatchManager watchManager = WatchManager.getInstance();
List<SDCardBean> list = FileBrowseManager.getInstance().getOnlineDev();
if (list.size() < 1) {
//没有在线设备
return;
}
SDCardBean sdCardBean = list.get(0);//获取设备,如果有多个设备,请根据需求获取相应的设备
FileStruct fileStruct = null;//注意fileStructs一定要是在sdCardBean中
int devHandle = sdCardBean.getDevHandler();
int cluster = fileStruct.getCluster();
int offset = 0;
String path = "读取内容的保存路径";
GetFileByClusterTask.Param param = new GetFileByClusterTask.Param(devHandle, 0, cluster, path);
GetFileByClusterTask task = new GetFileByClusterTask(watchManager, param);
task.setListener(new TaskListener() {
@Override
public void onBegin() {
//开始
}
@Override
public void onProgress(int progress) {
//进度回调
}
@Override
public void onFinish() {
//成功
}
@Override
public void onError(int code, String msg) {
//失败
}
@Override
public void onCancel(int reason) {
//取消
}
});
task.start();
参数模型与生命周期:
| 参数 | 来源 | 含义 |
|---|---|---|
devHandle | sdCardBean.getDevHandler() | 设备句柄,标识目标设备/SD 卡会话 |
0(第二个构造参数) | 固定 | 起始偏移(Demo 从 0 开始;若文件较大可分多次续传) |
cluster | fileStruct.getCluster() | 文件起始簇号,由浏览阶段获得的 FileStruct 提供 |
path | 上层指定 | 下载内容保存到手机的文件路径 |
TaskListener 与 GetFileByClusterTask.Param 共同构成了"先构造参数 → 再注册监听 → 最后 start()"的启动约定。onProgress(int progress) 为百分比进度,可用于驱动传输进度条;onCancel(int reason) 说明任务支持主动取消(如用户中断大文件传输)。
GetFileByNameTask 按名称下载
当已知设备端文件名时,可使用 GetFileByNameTask(同样位于 com.jieli.jl_rcsp.task 包,Demo 已 import)直接下载,免去先浏览定位簇号的步骤。适合下载设备上名称固定的文件(如固定命名的运动记录、日志文件)。其参数模型与监听器结构与 GetFileByClusterTask 一致,只是定位方式从簇号变为文件名。
传输任务与 RCSP 的关系
WatchManager 是 WatchOpImpl 的子类,是 RCSP 协议操作能力的门面。所有 Task(GetFileByClusterTask、GetFileByNameTask、FormatTask)都持有 WatchManager 引用,通过它向 RCSP 协议引擎发送命令、接收应答;底层命令构造依赖 CommandBuilder,应答通过 RcspCommandCallback 分发。大文件传输在 BLE 上会被拆分为多个数据包按序发送,SDK 内部处理重传与完整性校验,上层只感知 onProgress 与 onFinish。
Core Flow
sequenceDiagram
participant UI as 业务层(FileManagerDemo)
participant FBM as FileBrowseManager
participant WM as WatchManager (WatchOpImpl)
participant RCSP as RCSP协议引擎
participant DEV as 设备端文件系统
UI->>FBM: addFileObserver(observer)
UI->>FBM: getOnlineDev()
FBM-->>UI: List<SDCardBean>
UI->>FBM: getCurrentReadFile(sdCardBean)
FBM-->>UI: Folder (含缓存的FileStruct列表)
UI->>FBM: appenBrowse(fileStruct, sdCardBean)
FBM->>RCSP: 读取目录命令
RCSP->>DEV: BLE 请求
DEV-->>RCSP: 目录条目数据
RCSP-->>FBM: 解析为FileStruct批次
FBM-->>UI: onFileReceiver(List<FileStruct>)
FBM-->>UI: onFileReadStop(isEnd)
UI->>WM: new GetFileByClusterTask(watchManager, param)
UI->>WM: task.start()
WM->>RCSP: 按簇读取文件命令 (devHandle, cluster)
RCSP->>DEV: BLE 分包传输
DEV-->>RCSP: 文件数据包
RCSP-->>WM: 进度/数据
WM-->>UI: onProgress(percent)
WM-->>UI: onFinish() (文件已写入 path)
流程要点:
- 浏览阶段:先注册
FileObserver,再取在线设备;目录读取是异步的,结果通过onFileReceiver分批发回,onFileReadStop标记结束。目录导航(appenBrowse/backBrowse/loadMore)均以SDCardBean为上下文。 - 传输阶段:从
FileStruct取得cluster、从SDCardBean取得devHandle,构造GetFileByClusterTask.Param并启动任务;RCSP 通过 BLE 分包读取设备文件,边收边写入path指定的手机文件,onProgress上报进度,onFinish表示完整落盘。 - 失败路径:任何一步出错都会走对应回调——浏览失败走
onFileReadFailed(reason),传输失败走onError(code, msg),用户中断走onCancel(reason)。
手机本地文件路径管理
UriTool:URI → 真实路径
系统文件选择器(ACTION_OPEN_DOCUMENT / ACTION_GET_CONTENT)返回的是 content:// URI,而不是可直接使用的文件路径。UriTool.getPath() 针对 Android 4.4(KitKat)及以上系统,把各种 Provider 的 URI 还原为文件系统路径:
Source: UriTool.java
public static String getPath(final Context context, final Uri uri) {
// DocumentProvider
if (DocumentsContract.isDocumentUri(context, uri)) {
// ExternalStorageProvider
if (isExternalStorageDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
return Environment.getExternalStorageDirectory() + "/" + split[1];
}
// DownloadsProvider
else if (isDownloadsDocument(uri)) {
final String id = DocumentsContract.getDocumentId(uri);
final Uri contentUri = ContentUris.withAppendedId(
Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
return getDataColumn(context, contentUri, null, null);
}
// MediaProvider
else if (isMediaDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
Uri contentUri = null;
if ("image".equals(type)) {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
} else if ("video".equals(type)) {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
} else if ("audio".equals(type)) {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
}
final String selection = "_id=?";
final String[] selectionArgs = new String[] { split[1] };
return getDataColumn(context, contentUri, selection, selectionArgs);
}
}
// MediaStore (and general)
else if ("content".equalsIgnoreCase(uri.getScheme())) {
// Return the remote address
if (isGooglePhotosUri(uri))
return uri.getLastPathSegment();
return getDataColumn(context, uri, null, null);
}
// File
else if ("file".equalsIgnoreCase(uri.getScheme())) {
return uri.getPath();
}
return null;
}
支持的分支与设计意图:
- ExternalStorageProvider(
primary:xxx型 docId):直接拼接Environment.getExternalStorageDirectory(),这是最常见的选择"内部存储文件"路径; - DownloadsProvider:通过
ContentUris.withAppendedId构造content://downloads/public_downloads/<id>再查MediaStore的DATA列; - MediaProvider:按
image/video/audio类型选择对应MediaStore表,用_id=?查询真实路径; - content 通用分支:查询
getDataColumn(MediaStore.Images.Media.DATA列),并对 Google Photos 类 URI 直接取getLastPathSegment(); - file 分支:直接返回
uri.getPath()。
该方法在文件选择器选中"要发送到设备或保存到本地的文件"时被调用,解析出的真实路径随后传给 SDK(例如作为传输目标或作为待上传文件源)。
HealthUtil:应用私有外部目录
HealthUtil 提供基于 context.getExternalFilesDir(null) 的应用专属目录定位(Grep 命中见下):
Source: HealthUtil.java
if (context == null || dirNames == null || dirNames.length == 0) return null;
File file = context.getExternalFilesDir(null);
if (file == null || !file.exists()) return null;
该方法以 dirNames 为参数、返回目录 File 对象,用于在 Android/data/<package>/files 下组织应用私有文件(下载的固件、录音、日志等)。使用应用专属目录的优势是无需申请存储权限即可读写,且卸载应用时自动清理。当目录不存在或 getExternalFilesDir 返回 null(存储不可用)时返回 null,调用方需处理该空值。
Usage Examples
以下综合示例串联"浏览 → 下载 → 删除"三段流程,演示本页面所涉及 API 的组合使用方式(片段均提取自 FileManagerDemo.java 的 brow()、read()、delete() 三个测试方法):
// 1. 注册观察者并获取在线设备
FileBrowseManager manager = FileBrowseManager.getInstance();
manager.addFileObserver(fileObserver);
List<SDCardBean> list = manager.getOnlineDev();
if (list.size() < 1) return; // 没有在线设备
SDCardBean sdCardBean = list.get(0);
// 2. 读取当前目录(缓存的子文件)
Folder currentFolder = manager.getCurrentReadFile(sdCardBean);
List<FileStruct> fileStructs = currentFolder.getChildFileStructs();
// 3. 按簇下载到手机
int devHandle = sdCardBean.getDevHandler();
int cluster = fileStructs.get(0).getCluster(); // 取第一个文件
GetFileByClusterTask.Param param = new GetFileByClusterTask.Param(devHandle, 0, cluster, "保存路径");
GetFileByClusterTask task = new GetFileByClusterTask(WatchManager.getInstance(), param);
task.setListener(taskListener); // onProgress / onFinish / onError / onCancel
task.start();
// 4. 批量删除设备文件
boolean withEnv = false;
manager.deleteFile(sdCardBean, fileStructs, withEnv, deleteCallback);
注意:fileStructs 必须来自目标 sdCardBean 的浏览结果(FileStruct 携带簇号与设备上下文),否则删除/下载会定位失败;传输前应先查询 DevStorageInfo 确认设备剩余空间。
API Reference
FileBrowseManager(com.jieli.jl_filebrowse.FileBrowseManager)
| 方法 | 说明 |
|---|---|
static FileBrowseManager getInstance() | 获取单例 |
void addFileObserver(FileObserver observer) | 注册浏览事件观察者 |
List<SDCardBean> getOnlineDev() | 获取在线设备/SD 卡列表 |
Folder getCurrentReadFile(SDCardBean bean) | 获取设备当前读取目录(含缓存子文件) |
void loadMore(SDCardBean bean) | 加载更多目录条目 |
void appenBrowse(FileStruct fs, SDCardBean bean) | 进入下一级目录 |
void backBrowse(SDCardBean bean, boolean hasEvent) | 返回上一级目录,hasEvent 控制是否回调 |
void playFile(FileStruct fs, SDCardBean bean) | 点播设备媒体文件 |
void deleteFile(SDCardBean bean, List<FileStruct> list, boolean withEnv, DeleteCallback cb) | 批量删除设备文件 |
GetFileByClusterTask(com.jieli.jl_rcsp.task.GetFileByClusterTask)
- 构造:
GetFileByClusterTask(WatchManager manager, GetFileByClusterTask.Param param) Param构造:Param(int devHandle, int offset, int cluster, String savePath)void setListener(TaskListener listener)/void start()- 回调:
onBegin()、onProgress(int progress)、onFinish()、onError(int code, String msg)、onCancel(int reason)
DeleteCallback(com.jieli.jl_filebrowse.interfaces.DeleteCallback)
onSuccess(FileStruct fileStruct):单个文件删除成功onError(int code, FileStruct fileStruct):单个文件删除失败onFinish():全部文件处理结束
UriTool(com.jieli.healthaide.util.UriTool)
static String getPath(Context context, Uri uri):将 DocumentProvider / MediaStore / file 类型 URI 还原为真实路径,无法解析时返回null。
Failure Modes、边界情况与并发注意
- 无在线设备:
getOnlineDev()返回空列表时必须提前返回(Demo 中list.size() < 1即退出),否则后续调用会以空设备上下文执行。SD 卡热插拔通过onSdCardStatusChange通知,UI 应及时刷新。 - 浏览失败:
onFileReadFailed(int reason)返回失败码,常见原因为设备端读目录错误或 BLE 断连;onFileReadStop(boolean isEnd)的isEnd=false表示数据未读完,不应提前隐藏加载状态。 - 删除部分失败:
DeleteCallback对每个文件独立回调onError,onFinish()只表示流程结束而非全部成功,汇总结果必须以逐文件回调为准。 - 传输中断/取消:大文件传输中用户取消会触发
onCancel(int reason);断连等错误触发onError(code, msg)。任务无自动重试语义(源码注释与回调模型中未见重试机制),上层需按业务决定是否重建任务重试。 - 并发限制:同一时刻对同一
SDCardBean发起多个浏览/删除/下载操作可能造成协议串扰;SDK 以设备句柄为上下文隔离不同设备,但同设备多任务建议串行化(等上一个任务onFinish/onError后再启动下一个)。 - 路径解析空值:
UriTool.getPath()对未知 Provider 返回null,HealthUtil在存储不可用时也返回null,调用方必须判空,否则后续File构造会抛NullPointerException。
性能与运维考量
- 分页浏览:设备目录可能包含大量文件,
loadMore+onFileReceiver分批发回的设计避免了单次回调携带超大列表导致的 UI 卡顿,上层应增量追加而非整体替换。 - 大文件传输进度:
onProgress(int progress)以百分比上报,UI 应节流刷新(如每 1% 或每 200ms 刷新一次),避免进度条动画抢占主线程。 - BLE 吞吐限制:RCSP 经 BLE 传输吞吐有限,超大文件(固件/长录音)耗时较长,建议传输期间保持屏幕常亮、提示用户不要断开连接,并在
onBegin时锁定关键操作。 - 本地目录策略:下载文件写入
getExternalFilesDir(null)子目录,无需存储权限且随应用卸载清理;如需长期保留给用户,应再复制到公共目录(此时需申请存储权限并处理 Android 10+ 分区存储)。
Extension Points
- 自定义观察者:实现
FileObserver即可扩展浏览 UI(多设备选择、断连重连提示、点播结果播报),无需修改 SDK。 - 自定义传输任务:参照
GetFileByClusterTask/GetFileByNameTask的任务模式,基于WatchManager与CommandBuilder可构造新的 RCSP 任务(如批量下载、断点续传),并复用TaskListener生命周期约定。 - 设备存储策略:基于
DevStorageInfo/DevStorageState+GetSysInfoCmd可扩展"剩余空间不足预警""自动清理旧文件"等业务策略。 - 文件组织:
HealthUtil的目录工具可扩展为统一的文件分类管理器(按固件/录音/日志分目录),为传输任务提供一致的路径解析入口。
Related Links
- WatchManager 与设备连接管理(BLE 连接与
WatchManager初始化,本页面的前置依赖) - HealthUtil.java —— 应用外部目录工具
- UriTool.java —— URI 路径解析工具
- FileManagerDemo.java —— 文件管理功能完整测试用例
- 健康数据解析与存储(运动记录、血氧等数据的解析/入库),参见数据层相关目录页