杰理 SDK 文档中心
首页
首页
  • 快速开始

    • 环境要求与权限配置
    • 集成SDK依赖
    • 运行示例应用
  • 核心架构与协议

    • RCSP协议与数据通道
    • 蓝牙连接与设备管理
    • TWS双耳功能
    • 基础功能接口与自定义命令
  • 设备功能控制

    • 设备音乐控制与ID3信息
    • 文件浏览与传输
    • FM收音与发射
    • 灯光控制
    • 闹钟与时间管理
    • 查找设备与防丢
    • ANC与噪声处理
    • 按键功能设置
    • 彩屏仓控制
    • AI翻译
  • 音效与音频处理

    • 均衡器音效调节
    • 录音与语音控制
    • Line-in、SPDIF与声卡功能
    • 音频编解码库
  • 扩展功能库

    • OTA固件升级
    • 数据加密与解密
    • 图片与动图格式转换
  • 示例应用 btsmart

    • 应用架构与界面导航
    • 设备功能适配与数据层
    • 设备配置JSON与资源文件
  • 参考与版本

    • 错误码参考
    • 版本历史与更新日志
    • 开发文档中心导航

文件浏览与传输

设备端文件浏览与传输能力,由 UI 层 FilesFragment(设备文件浏览界面)驱动 com.jieli.filebrowse SDK 中的 FileBrowseManager 完成对设备存储(SD 卡)的目录浏览、文件列表分页加载、文件夹进出与文件点播播放,是"设备文件管理"这一完整用户链路的入口。

Purpose and Scope

本页面向 device-functions.file-browser(文件浏览与传输)能力,说明:

  • 设备文件浏览界面 FilesFragment 的完整生命周期与交互逻辑(目录导航、文件/文件夹点击、分页加载);
  • 底层 FileBrowseManager(位于 com.jieli.filebrowse SDK 包)经由调用方代码呈现的公开接口、返回码与行为约定;
  • 文件点击后触发"暂停媒体 → 延迟 → 点播播放"的传输/播放链路,以及与 RCSPController/DeviceInfo 的联动;
  • 典型的失败模式(读取中、设备离线、目录深度超限)与并发约束。

以下内容刻意留给兄弟页面:音乐播放状态机与 PlayControlImpl 细节归"音乐播放"页面;设备连接/状态回调(BTRcspEventCallback)归"设备连接管理"页面;铃声文件(FileBellFragment)、充电仓资源文件(ResourceFile)与日志文件(LogFileFragment)虽复用部分文件列表控件,但属各自独立能力。

说明:com.jieli.filebrowse SDK(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 式节奏:

  1. 初始化两个 RecyclerView:rv_device_files(文件列表,垂直)与 rv_file_path_nav(路径导航,水平);
  2. 用 mFileRouterAdapter 做导航栏适配器,并绑定点击回调 handleFileRouterClick;
  3. 通过 createFileAdapter() 创建列表适配器(子类可覆写以扩展),并从 BaseLoadMoreModule 取得加载更多模块,绑定 onLoadMoreRequested 回调;
  4. 若 mSdCardBean == null 直接返回空视图(无存储设备时不渲染);
  5. 若设备已连接,用 DeviceInfo.getCurrentDevIndex() 与 getCluster() 设置列表当前选中项(即当前正在播放的文件高亮);
  6. 调用 initWithCurrentPath() 恢复上次浏览的目录缓存;
  7. 注册 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,其逻辑体现了两层保护:

  1. 读取互斥:FileBrowseManager.isReading() 为真时直接 Toast 拒绝,避免在目录读取过程中插入播放指令导致设备侧协议错乱;
  2. 播放冲突规避:若本地媒体播放器正在播放,先 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 埋点,线上问题可通过日志还原用户浏览轨迹。

扩展点

  1. 适配器覆写:createFileAdapter() 与 getFileListAdapter() 为 protected,子类 Fragment 可替换 FileListAdapter 以定制条目 UI(如铃声文件的 BellFileListAdapter 即复用该模式的同类实现思路)。
  2. FileObserver 观察者模式:任何组件都可 addFileObserver 订阅目录读取事件,与 UI 解耦;多个观察者可并行注册(单例分发)。
  3. SDCardBean 多介质:浏览 API 以 SDCardBean 为键,天然支持多存储介质(内置闪存、SD 卡、U 盘)并存浏览,切换介质只需换 bean。
  4. 结果码驱动的错误文案:错误提示通过 R.string.msg_read_file_err_* 资源引用,本地化与文案调整无需改代码逻辑。

相关链接

  • FilesFragment.java — 设备文件浏览界面(核心实现)
  • FileListAdapter.java — 文件列表适配器
  • FileRouterAdapter.java — 路径导航适配器
  • FileUtil.java — 文件工具
  • SelectFileDialog.java — 本地文件选择对话框
  • 相关能力页:音乐播放(PlayControlImpl 模式切换)、设备连接管理(BTRcspEventCallback 生命周期)、铃声文件(FileBellFragment)、日志文件(LogFileFragment)
Prev
设备音乐控制与ID3信息
Next
FM收音与发射