文件浏览与传输
设备端文件浏览与传输能力,由 UI 层 FilesFragment(设备文件浏览界面)驱动 com.jieli.filebrowse SDK 中的 FileBrowseManager 完成对设备存储(SD 卡)的目录浏览、文件列表分页加载、文件夹进出与文件点播播放,是"设备文件管理"这一完整用户链路的入口。
Purpose and Scope
本页面向 device-functions.file-browser(文件浏览与传输)能力,说明:
- 设备文件浏览界面
FilesFragment的完整生命周期与交互逻辑(目录导航、文件/文件夹点击、分页加载); - 底层
FileBrowseManager(位于com.jieli.filebrowseSDK 包)经由调用方代码呈现的公开接口、返回码与行为约定; - 文件点击后触发"暂停媒体 → 延迟 → 点播播放"的传输/播放链路,以及与
RCSPController/DeviceInfo的联动; - 典型的失败模式(读取中、设备离线、目录深度超限)与并发约束。
以下内容刻意留给兄弟页面:音乐播放状态机与 PlayControlImpl 细节归"音乐播放"页面;设备连接/状态回调(BTRcspEventCallback)归"设备连接管理"页面;铃声文件(FileBellFragment)、充电仓资源文件(ResourceFile)与日志文件(LogFileFragment)虽复用部分文件列表控件,但属各自独立能力。
说明:
com.jieli.filebrowseSDK(FileBrowseManager、FileStruct、Folder、SDCardBean、FileObserver等)在本仓库btsmart模块中以依赖形式引用,其内部实现不在仓库源码树内。本文档基于调用方FilesFragment的实际源码(本仓库可验证)描述该 SDK 的接口与行为约定;凡属调用点可推断的签名均以"调用点证据"标注,未经源码验证的内部细节不做虚构。
Overview
在 JL 蓝牙生态(PiHome App + RCSP 协议)中,"设备文件"指蓝牙音箱/耳机等设备内置或外插存储(SD 卡、U 盘)上的音乐文件。App 通过 RCSP(Remote Control & Status Protocol)通道向设备下发命令,设备返回目录结构,App 以"文件浏览器"形式呈现,并支持点播设备上任意一首歌曲。
该能力解决的核心问题是:设备存储无法像本地文件系统一样直接挂载,目录树的读取是异步、分页、且被设备侧串行限制的。因此:
FilesFragment采用"路径导航栏(FileRouterAdapter)+ 文件列表(FileListAdapter)"双列表 UI,模拟资源管理器式浏览体验;- 每次进入/返回目录都通过
FileBrowseManager发起异步读取,读取期间以isReading()标志禁止并发操作; - 列表采用上拉加载更多(
BaseLoadMoreModule)分页拉取,防止一次性拉取整棵目录树; - 文件点击不直接播放,而是先暂停本地媒体播放器,再经主线程延迟后调用
playFile下发点播指令,避免与本地播放冲突。
flowchart TD
subgraph sg_UI["UI 层(btsmart 模块)"]
FilesFragment["FilesFragment"]
FileListAdapter["FileListAdapter"]
FileRouterAdapter["FileRouterAdapter"]
end
subgraph sg_SDK["SDK 层(com.jieli.filebrowse)"]
FileBrowseManager["FileBrowseManager"]
FileObserver["FileObserver"]
FileStruct["FileStruct"]
Folder["Folder"]
SDCardBean["SDCardBean"]
end
subgraph sg_BT["连接与控制层"]
RCSPController["RCSPController"]
DeviceInfo["DeviceInfo"]
PlayControlImpl["PlayControlImpl"]
end
FilesFragment -->|"getInstance / appenBrowse / backBrowse / playFile"| FileBrowseManager
FileBrowseManager -->|"回调通知"| FileObserver
FilesFragment -.->|"实现"| FileObserver
FileListAdapter -->|"条目类型"| FileStruct
FileRouterAdapter -->|"导航条目"| Folder
FilesFragment -->|"isDeviceConnected / getDeviceInfo"| RCSPController
RCSPController --> DeviceInfo
FilesFragment -->|"updateMode 切音乐模式"| PlayControlImpl
架构要点:FilesFragment 是唯一的用户入口,它同时依赖三条链——FileBrowseManager(目录数据)、RCSPController(连接状态与设备信息)、PlayControlImpl(播放模式切换)。FileObserver 是 SDK 向 UI 推送读取进度/结果的回调接口,FilesFragment 通过实现并注册它获得目录读取完成通知(读取回调在文件第 240 行之后的剩余代码中实现,本页依据调用点证据描述其注册/注销模式)。
主要文件
- FilesFragment.java — 设备文件浏览界面(本页核心证据文件)
- FileListAdapter.java — 文件列表适配器(支持加载更多模块)
- FileRouterAdapter.java — 路径导航栏适配器
- FileUtil.java — 文件工具类
- SelectFileDialog.java — 文件选择对话框(本地选文件场景复用)
实现深入分析
FilesFragment 的角色与创建
FilesFragment 继承自 DeviceControlFragment 并实现 FileObserver 接口,通过 newInstance(SDCardBean) 工厂方法携带目标存储设备(SD 卡)创建,SDCardBean 是 SDK 层对"某个存储介质"的描述对象,作为所有 FileBrowseManager 调用的第一参数(即"在哪个设备上浏览"的上下文键):
public static FilesFragment newInstance(SDCardBean sdCardBean) {
Bundle args = new Bundle();
FilesFragment fragment = new FilesFragment();
fragment.mSdCardBean = sdCardBean;
fragment.setArguments(args);
return fragment;
}
Source: FilesFragment.java
FileBrowseManager 采用单例模式按需获取(懒加载),并在 onDestroyView 时反注册观察者、置空引用,保证 Fragment 销毁后不泄漏 SDK 回调:
private FileBrowseManager getFileBrowseManager() {
if (mFileBrowseManager == null) {
mFileBrowseManager = FileBrowseManager.getInstance();
}
return mFileBrowseManager;
}
Source: FilesFragment.java
界面初始化与缓存恢复(onCreateView)
onCreateView 的初始化顺序体现了"先建 UI,再绑数据"的 MVVM 式节奏:
- 初始化两个 RecyclerView:
rv_device_files(文件列表,垂直)与rv_file_path_nav(路径导航,水平); - 用
mFileRouterAdapter做导航栏适配器,并绑定点击回调handleFileRouterClick; - 通过
createFileAdapter()创建列表适配器(子类可覆写以扩展),并从BaseLoadMoreModule取得加载更多模块,绑定onLoadMoreRequested回调; - 若
mSdCardBean == null直接返回空视图(无存储设备时不渲染); - 若设备已连接,用
DeviceInfo.getCurrentDevIndex()与getCluster()设置列表当前选中项(即当前正在播放的文件高亮); - 调用
initWithCurrentPath()恢复上次浏览的目录缓存; - 注册
BTRcspEventCallback(连接状态变化时 UI 需要响应)与FileObserver(目录读取结果回调)。
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_files, container, false);
RecyclerView rvDeviceFiles = view.findViewById(R.id.rv_device_files);
rvFilePathNav = view.findViewById(R.id.rv_file_path_nav);
//初始化文件路径导航栏
rvFilePathNav.setLayoutManager(new LinearLayoutManager(requireContext(), RecyclerView.HORIZONTAL, false));
rvFilePathNav.setAdapter(mFileRouterAdapter);
mFileRouterAdapter.setOnItemClickListener((adapter, view12, position) -> handleFileRouterClick(position));
//初始化文件列表ui
mFileListAdapter = createFileAdapter();
rvDeviceFiles.setAdapter(mFileListAdapter);
rvDeviceFiles.setLayoutManager(new LinearLayoutManager(getContext()));
mLoadMoreModule = mFileListAdapter.getLoadMoreModule();
mLoadMoreModule.setOnLoadMoreListener(this::onLoadMoreRequested);
...
//读取当前浏览信息
initWithCurrentPath();
mRCSPController.addBTRcspEventCallback(mBTEventCallback);
getFileBrowseManager().addFileObserver(this);
return view;
}
Source: FilesFragment.java
initWithCurrentPath() 是"浏览位置记忆"的关键:SDK 按 SDCardBean 缓存当前读取的目录(Folder),Fragment 重建时直接取缓存恢复列表,避免用户每次进入都从根目录重新加载。若缓存目录存在但数据为空且未读完,则自动触发一次 onLoadMoreRequested() 续拉;若已读完则调用 loadMoreEnd() 并启用空视图:
private void initWithCurrentPath() {
//获取存储设备的缓存数据
Folder currentFolder = getFileBrowseManager().getCurrentReadFile(mSdCardBean);
if (currentFolder != null) {
refreshFileRouterView(getFileBrowseManager().getCurrentReadFile(mSdCardBean));
mFileListAdapter.setNewInstance(currentFolder.getChildFileStructs()); //缓存的文件列表
//判断当前目录是否读取完毕
if (mFileListAdapter.getData().size() < 1 && !currentFolder.isLoadFinished(false)) {
onLoadMoreRequested(); //未读取完毕,进行加载操作
} else if (currentFolder.isLoadFinished(false)) {
mFileListAdapter.setUseEmpty(true);
mLoadMoreModule.loadMoreEnd(); //读取完毕
}
}
}
Source: FilesFragment.java
设计意图:isLoadFinished(false) 的布尔参数表明 SDK 对"读取完成"状态可能有不同口径(如忽略错误重试或含子目录未读状态),UI 据此决定是继续加载还是显示空态——这种"缓存 + 断点续拉"策略对蓝牙低带宽链路至关重要,能显著减少重复的目录请求。
点击文件夹:进入目录(appenBrowse)
文件夹点击走 handleFolderClick:先隐藏空视图,再调用 appenBrowse(fileStruct, mSdCardBean) 请求进入该文件夹。返回值决定 UI 行为——SUCCESS 时清空列表并刷新导航栏(新目录数据由 FileObserver 回调异步填充);三种错误码分别弹出对应 Toast:
//点击文件夹
protected void handleFolderClick(FileStruct fileStruct) {
showEmptyView(false);
int ret = getFileBrowseManager().appenBrowse(fileStruct, mSdCardBean);
JL_Log.i(TAG, AppUtil.formatString("appenBrowse :[%s], ret : %d", mSdCardBean, ret));
if (ret == FileBrowseConstant.ERR_READING) {
ToastUtil.showToastShort(R.string.msg_read_file_err_reading);
} else if (ret == FileBrowseConstant.ERR_OFFLINE) {
ToastUtil.showToastShort(R.string.msg_read_file_err_offline);
} else if (ret == FileBrowseConstant.ERR_BEYOND_MAX_DEPTH) {
ToastUtil.showToastShort(R.string.msg_read_file_err_beyond_max_depth);
} else if (ret == FileBrowseConstant.SUCCESS) {
mFileListAdapter.setNewInstance(new ArrayList<>());
refreshFileRouterView(getFileBrowseManager().getCurrentReadFile(mSdCardBean));
}
}
Source: FilesFragment.java
注意:进入目录成功但列表为空是预期状态——真正的内容通过 FileObserver 异步到达。这正是"蓝牙文件浏览"与本地文件系统的本质差异:请求与结果解耦。
点击文件:点播播放(playFile)
文件点击(fileStruct.isFile() 为真)走 handleFileClick,其逻辑体现了两层保护:
- 读取互斥:
FileBrowseManager.isReading()为真时直接 Toast 拒绝,避免在目录读取过程中插入播放指令导致设备侧协议错乱; - 播放冲突规避:若本地媒体播放器正在播放,先
pause(),再通过主线程 Handler 延迟(播放中延迟 100ms,否则 0ms)执行真正的播放动作;延迟后再次校验设备连接状态,断开则放弃。
//点击文件
protected void handleFileClick(FileStruct fileStruct) {
if (getFileBrowseManager().isReading()) {
ToastUtil.showToastShort(R.string.msg_read_file_err_reading);
return;
}
boolean isPlaying = JL_MediaPlayerServiceManager.getInstance().getJl_mediaPlayer().isPlaying();
if (isPlaying) {
JL_MediaPlayerServiceManager.getInstance().getJl_mediaPlayer().pause();
}
HandlerManager.getInstance().getMainHandler().postDelayed(() -> {
if (!mRCSPController.isDeviceConnected()) return;
getFileBrowseManager().playFile(fileStruct, mSdCardBean);
PlayControlImpl.getInstance().updateMode(PlayControlImpl.MODE_MUSIC);//切换到音乐模式
mRCSPController.getDeviceInfo().setCurrentDevIndex(fileStruct.getDevIndex());
mRCSPController.getDeviceInfo().setCluster(fileStruct.getCluster());
mFileListAdapter.setSelected(fileStruct.getDevIndex(), fileStruct.getCluster());
}, isPlaying ? 100 : 0);
}
Source: FilesFragment.java
播放动作包含四个有序步骤:playFile(SDK 下发点播命令)→ updateMode(MODE_MUSIC)(App 播放状态切到音乐模式)→ 更新 DeviceInfo 的当前索引与簇号(getDevIndex()/getCluster() 是文件在设备存储中的定位坐标)→ 更新列表选中态。100ms 延迟的目的是让暂停操作先落盘/生效,避免本地播放器与设备播放在同一时刻竞争音频焦点。
路径导航栏:返回上级目录(backBrowse)
handleFileRouterClick 实现"点击面包屑任意层级直接跳回"。其算法是:从当前目录沿 getParent() 链向上遍历,逐级 backBrowse(mSdCardBean, false)(静默回退,不刷新 UI),直到父目录簇号与目标一致;再执行一次 backBrowse(mSdCardBean, true)(刷新)并重建列表与导航栏:
private void handleFileRouterClick(int position) {
FileStruct fileStruct = mFileRouterAdapter.getItem(position);
Folder current = getFileBrowseManager().getCurrentReadFile(mSdCardBean);
if (current == null) {
return;
}
//当前文件返回
if (current.getCluster() == fileStruct.getCluster()) {
return;
}
//遍历父文件夹,直到父文件夹是选中文件夹。
while (current.getParent() != null && current.getParent().getCluster() != fileStruct.getCluster()) {
//不判断结果,可能会导致死循环
if(getFileBrowseManager().backBrowse(mSdCardBean, false) != FileBrowseConstant.SUCCESS) break;
current = getFileBrowseManager().getCurrentReadFile(mSdCardBean);
}
//清空文件列表
mFileListAdapter.setNewInstance(new ArrayList<>());
getFileBrowseManager().backBrowse(mSdCardBean, true);
refreshFileRouterView(getFileBrowseManager().getCurrentReadFile(mSdCardBean));
}
Source: FilesFragment.java
这段代码注释里明确点出设计约束:"不判断结果,可能会导致死循环"——backBrowse 返回值被用作循环出口保护,防止 SDK 因异常而永远停在某个目录。导航栏本身由 refreshFileRouterView 从当前 Folder 沿 getParent() 链向上收集全部祖先,构成"根 → … → 当前"的顺序列表,并自动滚动到末尾:
private void refreshFileRouterView(Folder folder) {
if (folder == null) {
return;
}
List<FileStruct> list = new ArrayList<>();
list.add(folder);
while (folder.getParent() != null) {
folder = folder.getParent();
list.add(0, folder);
}
JL_Log.d(TAG, "file path count-->" + list.size());
mFileRouterAdapter.setNewInstance(list);
rvFilePathNav.scrollToPosition(mFileRouterAdapter.getData().size() - 1);
}
Source: FilesFragment.java
设计意图:Folder 本身就是一棵"已展开的父链"节点(含 getParent() 引用),导航栏数据零成本派生,无需额外的路径栈;簇号(cluster)作为目录唯一标识贯穿 FileStruct/Folder/DeviceInfo,是设备侧定位文件与目录的协议坐标。
核心流程
进入目录流程(异步读取)
sequenceDiagram
participant U as 用户
participant F as FilesFragment
participant M as FileBrowseManager
participant D as 蓝牙设备
U->>F: 点击文件夹
F->>M: appenBrowse(fileStruct, sdCardBean)
M-->>F: 返回 int(同步判定)
alt ret == SUCCESS
F->>F: 清空文件列表 + 刷新导航栏
M->>D: 下发读取目录命令(RCSP 通道)
D-->>M: 目录数据(分页)
M-->>F: FileObserver 回调(读取开始/完成/失败)
F->>F: 填充列表 / 触发 loadMore
else ret == ERR_READING
F->>F: Toast「正在读取中」
else ret == ERR_OFFLINE
F->>F: Toast「设备离线」
else ret == ERR_BEYOND_MAX_DEPTH
F->>F: Toast「超出最大目录深度」
end
关键点:appenBrowse 的返回值是同步的准入判定(是否允许发起请求),真正的目录数据经 FileObserver 异步回传。UI 在返回 SUCCESS 后立即置空列表并等待回调,这是蓝牙协议"请求-响应分离"的必然结果。
文件点播播放流程
flowchart TD
Start([点击文件条目]) --> IsFile{"isFile 为真?"}
IsFile -->|"否"| FolderPath["handleFolderClick 进入目录"]
IsFile -->|"是"| CheckReading{"FileBrowseManager<br/>isReading?"}
CheckReading -->|"是"| ToastReading["Toast: 正在读取"]
CheckReading -->|"否"| CheckPlaying{"本地媒体播放器<br/>正在播放?"}
CheckPlaying -->|"是"| Pause["pause 暂停本地播放"]
CheckPlaying -->|"否"| Delay0["延迟 0ms"]
Pause --> Delay100["延迟 100ms"]
Delay100 --> CheckConn{"设备已连接?"}
Delay0 --> CheckConn
CheckConn -->|"否"| Abort([放弃播放])
CheckConn -->|"是"| Play["playFile(fileStruct, sdCardBean)"]
Play --> Mode["updateMode(MODE_MUSIC)"]
Mode --> Index["setCurrentDevIndex / setCluster"]
Index --> Select["列表选中态高亮"]
Select --> Done([结束])
分页加载(loadMore)
列表滚动到底部时触发 onLoadMoreRequested(绑定于 mLoadMoreModule.setOnLoadMoreListener)。与目录进入相同,加载请求与结果通过 FileObserver 解耦;Folder.isLoadFinished(false) 与 mLoadMoreModule.loadMoreEnd() 共同标记"该目录已全量拉取",此后不再触发加载。分页大小与步进由 SDK 侧协议决定(调用点证据:UI 仅订阅加载事件,不直接传入页码)。
SDK 接口约定(基于调用点证据)
以下签名与语义均来自 FilesFragment 的实际调用方式;SDK 内部实现位于外部依赖 com.jieli.filebrowse,本仓库未包含其源码,故不虚构内部细节。
FileBrowseManager(单例)
| 方法 | 签名(调用点推断) | 说明 |
|---|---|---|
| 获取实例 | static FileBrowseManager getInstance() | 全局单例,懒加载 |
| 当前目录 | Folder getCurrentReadFile(SDCardBean) | 返回指定存储设备当前浏览的目录缓存,可能为 null |
| 读取状态 | boolean isReading() | 是否有目录读取任务进行中 |
| 进入目录 | int appenBrowse(FileStruct, SDCardBean) | 请求进入文件夹,返回 FileBrowseConstant 结果码 |
| 返回上级 | int backBrowse(SDCardBean, boolean) | 返回上一级目录;第二个参数控制是否刷新 UI/通知 |
| 播放文件 | void playFile(FileStruct, SDCardBean) | 下发点播指令,让设备播放指定文件 |
| 注册观察者 | void addFileObserver(FileObserver) | 订阅目录读取事件 |
| 注销观察者 | void removeFileObserver(FileObserver) | 取消订阅,Fragment 销毁时必须调用 |
FileBrowseConstant 结果码
| 常量 | 含义 | UI 行为 |
|---|---|---|
SUCCESS | 请求受理成功 | 清空列表、刷新导航栏,等待异步回调 |
ERR_READING | 已有读取任务进行中,禁止并发 | Toast「正在读取」 |
ERR_OFFLINE | 设备未连接/离线 | Toast「设备离线」 |
ERR_BEYOND_MAX_DEPTH | 目录层级超过设备侧上限 | Toast「超出最大目录深度」 |
数据模型(bean)
SDCardBean:存储介质描述(SD 卡/内置存储等),作为所有浏览操作的上下文键,FilesFragment通过newInstance(SDCardBean)传入。FileStruct:单个条目(文件或目录)。关键方法:isFile()(区分文件/目录)、getDevIndex()(设备存储索引)、getCluster()(簇号,定位坐标)。Folder:目录节点,构成父链树。关键方法:getChildFileStructs()(子条目列表)、getParent()(父目录)、getCluster()(目录簇号)、isLoadFinished(boolean)(目录是否读取完毕)。FileObserver:回调接口,FilesFragment实现并注册;承载读取开始/完成/失败等事件(本页依据注册/注销调用点确认其存在与生命周期,具体回调方法名未在已读代码中出现,不作虚构)。
DeviceInfo 联动
RCSPController.getDeviceInfo() 返回的 DeviceInfo 提供 getCurrentDevIndex()/getCluster() 读取与 setCurrentDevIndex()/setCluster() 写入,用于记忆/展示"当前正在播放的文件"坐标;RCSPController.isDeviceConnected() 是播放与初始化前的连接门禁。
失败模式、边界情况与并发
并发约束:读取互斥
设备侧对目录读取是串行的——同一时刻只允许一个读取任务。SDK 通过 isReading() 暴露该状态,UI 在文件点击(handleFileClick)和文件夹进入(appenBrowse 返回 ERR_READING)两处都做了防护。该设计意图明确:蓝牙链路带宽与设备协议栈都不支持并发目录操作,违反会导致协议乱序或设备无响应。
设备离线
- 进入目录时:
appenBrowse返回ERR_OFFLINE,UI 仅 Toast 提示,不进入加载流程; - 播放文件时:延迟回调内重新校验
isDeviceConnected(),断连则静默放弃——这是"延迟执行 + 二次校验"模式,防止用户在点击后、执行前拔线导致向空连接下发指令。
目录深度上限
ERR_BEYOND_MAX_DEPTH 表明设备侧限制最大嵌套目录层数(如 8 层)。UI 不自行限制,完全依赖 SDK 返回码;这与 backBrowse 循环中"不判断结果可能死循环"的注释互为印证:目录树的合法性由设备端协议约束。
Fragment 生命周期
onDestroyView 中依次 removeBTRcspEventCallback、removeFileObserver、置空 mFileBrowseManager 引用。若漏掉反注册,单例 FileBrowseManager 会持有已销毁 Fragment 的引用,造成内存泄漏与回调空指针;若漏掉 addBTRcspEventCallback 的移除,则设备状态变化(如断开)会触发已销毁 UI 的刷新逻辑。
边界:空目录与未读完目录
initWithCurrentPath 区分三种状态:缓存非空(直接展示)、缓存为空但未读完(自动续拉)、已读完(loadMoreEnd + 空视图)。setUseEmpty(true) 控制空态视图的展示,避免"空列表误判为加载失败"。
播放器竞争
本地媒体播放器正在播放时,先 pause() 再延迟 100ms 下发设备播放,避免音频焦点竞争与播放状态不同步(App 侧播放器与设备播放器的状态机必须互斥切换)。
性能与运维考量
- 分页加载:目录列表按需分页(
BaseLoadMoreModule),避免一次性拉取大目录导致蓝牙通道拥塞与 UI 卡顿; - 缓存复用:
getCurrentReadFile(SDCardBean)按存储介质缓存目录树,Fragment 重建零成本恢复浏览位置,显著降低重复请求; - 主线程调度:所有 UI 操作(列表刷新、Toast、导航栏滚动)均在主线程;SDK 回调经
FileObserver到达后由 Fragment 处理,未在已读代码中看到跨线程屏障,可推断 SDK 负责回调线程归拢(内部实现未验证,不作断言); - 日志:关键路径(
appenBrowse返回值、路径导航节点数、列表点击)均有JL_Log埋点,线上问题可通过日志还原用户浏览轨迹。
扩展点
- 适配器覆写:
createFileAdapter()与getFileListAdapter()为protected,子类 Fragment 可替换FileListAdapter以定制条目 UI(如铃声文件的BellFileListAdapter即复用该模式的同类实现思路)。 - FileObserver 观察者模式:任何组件都可
addFileObserver订阅目录读取事件,与 UI 解耦;多个观察者可并行注册(单例分发)。 - SDCardBean 多介质:浏览 API 以
SDCardBean为键,天然支持多存储介质(内置闪存、SD 卡、U 盘)并存浏览,切换介质只需换 bean。 - 结果码驱动的错误文案:错误提示通过
R.string.msg_read_file_err_*资源引用,本地化与文案调整无需改代码逻辑。
相关链接
- FilesFragment.java — 设备文件浏览界面(核心实现)
- FileListAdapter.java — 文件列表适配器
- FileRouterAdapter.java — 路径导航适配器
- FileUtil.java — 文件工具
- SelectFileDialog.java — 本地文件选择对话框
- 相关能力页:音乐播放(
PlayControlImpl模式切换)、设备连接管理(BTRcspEventCallback生命周期)、铃声文件(FileBellFragment)、日志文件(LogFileFragment)