文件选择与升级界面
本文档介绍 Android-JL_OTA 演示应用(demo-app)中「文件选择与升级界面」的完整实现,涵盖设备列表展示、升级文件选择对话框、多设备升级进度对话框及其与 ViewModel 层和 OTA SDK 的协作方式。
Purpose and Scope
本页面聚焦于广播盒子演示应用(com.jieli.broadcastbox 包)中升级相关的 UI 层实现,包括:
UpgradeFragment—— 升级主页面(已连接设备列表、设备勾选、文件选择入口、升级按钮状态控制)DialogUpgradeFilePicker—— 固件文件选择对话框(含按芯片 uid/pid 过滤逻辑)DialogUpgradeDevice—— 多设备升级执行与进度展示对话框UpgradeFilePickerAdapter—— 文件列表适配器与选中态高亮
本页面不涉及以下内容(属于其他目录页的边界):
- OTA SDK 底层蓝牙传输与固件升级协议(见
otasdk中tool/ota相关页面) MultiOTAViewModel内部的多设备 OTA 状态机与队列调度机制(状态机模型类MultiOTAState、MultiOTAStart等仅作引用说明)FileOpViewModel如何扫描/监听升级文件目录(见文件管理相关页面)- 连接页
ConnectFragment与文件管理页FilesFragment的 UI 实现
Overview
广播盒子演示应用通过 BroadcastBoxActivity 承载三个 Fragment 标签页:连接(ConnectFragment)、文件(FilesFragment)、升级(UpgradeFragment),见 BroadcastBoxActivity.java#L23。
升级界面的核心职责是打通「已连接设备 → 选择固件文件 → 发起多设备升级 → 展示进度」的完整用户链路:
- 展示当前已连接(广播盒)设备列表;
- 用户为每个设备选择对应的升级文件(
.ufw等); - 用户勾选至少一台设备后,升级按钮才会启用;
- 点击升级弹出
DialogUpgradeDevice,由MultiOTAViewModel驱动 OTA SDK 依次为多台设备执行升级,并在对话框中实时刷新每台设备的状态与进度; - 升级结束后对话框展示结果并提供「确定」入口。
关键设计意图:
- 状态与 UI 分离:设备连接状态、选中设备列表、升级状态均存放在
BroadcastBoxViewModel/MultiOTAViewModel中,Fragment 与 Dialog 只做观察与渲染,即使页面重建也能恢复状态。 - 文件过滤前置:文件选择对话框在后台线程读取每个候选文件头部字节,并用
ParseDataUtil.filterFile(bytes, uid, pid)校验固件是否匹配目标芯片,避免把错误固件推给设备。 - 升级过程不可打断:升级对话框拦截返回键并保持屏幕常亮(
FLAG_KEEP_SCREEN_ON),降低升级中途因意外操作或息屏导致失败的概率。
Architecture
下面展示升级界面相关组件及其与 ViewModel / SDK 层的依赖关系:
flowchart TD
subgraph sg_UI["UI 层 (com.jieli.broadcastbox)"]
UF["UpgradeFragment"]
Picker["DialogUpgradeFilePicker"]
DevDlg["DialogUpgradeDevice"]
PickerAdapter["UpgradeFilePickerAdapter"]
ProgAdapter["UpgradeProgressAdapter"]
end
subgraph sg_VM["ViewModel 层"]
BCVM["BroadcastBoxViewModel"]
FileVM["FileOpViewModel"]
OTAVM["MultiOTAViewModel"]
end
subgraph sg_SDK["OTA SDK 层"]
SDK["JL_OTAManager / 蓝牙通道"]
Filter["ParseDataUtil"]
end
UF -->|"readFileList()"| FileVM
UF -->|"deviceConnectionMLD 观察<br/>getSelectedDeviceList()"| BCVM
UF -->|"newInstance(path, uid, pid)"| Picker
UF -->|"new DialogUpgradeDevice(list, cb)"| DevDlg
Picker -->|"getUpgradeFiles()"| FileVM
Picker -->|"filterFile(bytes, uid, pid)"| Filter
Picker --> PickerAdapter
DevDlg -->|"startMultiOTA(ctx, list)"| OTAVM
DevDlg --> ProgAdapter
OTAVM -->|"multiOtaStateMLD 观察"| DevDlg
OTAVM -->|"发起/控制升级"| SDK
组件职责说明:
| 组件 | 职责 | 关键依赖 |
|---|---|---|
UpgradeFragment | 主页面:设备列表渲染、勾选/取消设备、打开文件选择器、控制升级按钮可用性 | BroadcastBoxViewModel、FileOpViewModel、UpgradeDeviceAdapter |
DialogUpgradeFilePicker | 弹窗列出候选固件文件;按 uid/pid 过滤;点击文件回调 OnResultListener | FileOpViewModel、ParseDataUtil、UpgradeFilePickerAdapter |
DialogUpgradeDevice | 弹窗执行多设备升级;监听 MultiOTAViewModel.multiOtaStateMLD 刷新进度;拦截返回键、保持屏幕常亮 | MultiOTAViewModel、UpgradeProgressAdapter |
UpgradeFilePickerAdapter | 文件列表单项渲染;对已选文件显示蓝色勾选图标 | 布局 R.layout.upgrade_file_picker_item |
UpgradeProgressAdapter | 升级进度列表渲染(每台设备一个条目) | 布局 R.layout.upgrade_progress_item |
架构要点:UI 层只通过
ViewModelProvider(requireActivity())或ViewModelProvider(this)获取共享 ViewModel,不直接持有 SDK 实例。这使得升级页面与 SDK 解耦,也保证了旋转屏幕等配置变更时设备选择状态与升级任务不丢失。
主界面:UpgradeFragment
UpgradeFragment 继承自 SDK 提供的 BaseFragment,使用 ViewBinding(FragmentUpgradeBinding)渲染布局。它在 onCreateView 中先禁用升级按钮,待有设备被勾选后才启用:
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
binding = FragmentUpgradeBinding.inflate(inflater, container, false);
binding.btnUpgrade.setEnabled(false);
return binding.getRoot();
}
Source: UpgradeFragment.java#L48-L53
设备连接状态观察与列表刷新
onViewCreated 中通过 ViewModelProvider(requireActivity()) 获取 Activity 级共享的 BroadcastBoxViewModel 与 FileOpViewModel,并立即触发 fileOpViewModel.readFileList() 预加载升级文件列表。
界面通过 deviceConnectionMLD(LiveData)观察连接状态变化:当设备连接状态不再是 STATE_CONNECTING 时,使用 uiHandler 延迟 300ms 刷新设备列表——延迟的目的在于等待连接状态稳定,避免在 BLE 连接建立瞬间频繁刷新列表:
viewModel.deviceConnectionMLD.observe(getViewLifecycleOwner(), deviceConnection -> {
JL_Log.i(TAG, ">>>> deviceConnectionMLD >> " + deviceConnection);
if (deviceConnection.getState() != BluetoothProfile.STATE_CONNECTING) {
uiHandler.removeMessages(MSG_UPDATE_DEVICE_LIST);
uiHandler.sendEmptyMessageDelayed(MSG_UPDATE_DEVICE_LIST, 300);
}
});
Source: UpgradeFragment.java#L61-L67
updateConnectedDeviceList() 是列表刷新的核心:它把「已连接设备」与「之前缓存的选中设备」做匹配,将用户之前的选择(勾选状态、文件、强制升级标记)迁移到新的设备对象上,从而在设备重连/列表刷新后保留用户的选择:
private void updateConnectedDeviceList() {
List<BroadcastBoxInfo> connectedDevices = viewModel.getConnectedBleDevices(); //已连接设备列表
List<BroadcastBoxInfo> cacheSelected = viewModel.getSelectedDeviceList(); //缓存的选择设备列表
List<BroadcastBoxInfo> realTimeList = new ArrayList<>(); //实时更新列表
...
for (BroadcastBoxInfo boxInfo : connectedDevices) {
for (BroadcastBoxInfo selected : cacheSelected) {
if (boxInfo.equals(selected)) {
boxInfo.setChosen(true);
boxInfo.setSelectFile(selected.getSelectFile());
boxInfo.setForceUpdate(selected.isForceUpdate());
realTimeList.add(boxInfo);
break;
}
}
}
adapter.setList(connectedDevices);
viewModel.getSelectedDeviceList().clear();
viewModel.getSelectedDeviceList().addAll(realTimeList);
updateUpgradeButton();
}
Source: UpgradeFragment.java#L161-L185
设备勾选与文件选择交互
UpgradeDeviceAdapter 注册了两个可点击子视图:R.id.iv_hook(勾选框)与 R.id.tv_choose_file(选择文件入口),通过 addChildClickViewIds 声明后由 setOnItemChildClickListener 统一处理:
- 点击勾选框:若该设备尚未选择文件则提示「请先选择文件」(
ToastUtil.showToastLong);否则切换BroadcastBoxInfo.isChosen状态,并同步增删viewModel.getSelectedDeviceList(),随后notifyItemChanged(position)局部刷新该行。 - 点击选择文件:先检查
fileOpViewModel.getUpgradeFiles()是否为空,为空则弹出showNoFileDialog()(NotifyDialog提示"请先添加升级文件");否则读取当前设备已选文件路径、pid、uid,构造DialogUpgradeFilePicker.newInstance(filepath, uid, pid)并设置结果回调——回调中把用户选中的File写回设备的selectFile字段并局部刷新列表。
upgradeFilePicker = DialogUpgradeFilePicker.newInstance(filepath, uid, pid);
upgradeFilePicker.setOnResultListener(file1 -> {
adapter.getItem(position).setSelectFile(file1);
adapter.notifyItemChanged(position);
});
upgradeFilePicker.show(getParentFragmentManager(), upgradeFilePicker.getClass().getSimpleName());
Source: UpgradeFragment.java#L134-L143
升级按钮状态机
updateUpgradeButton() 依据「选中设备列表是否为空」决定升级按钮的可用性与背景资源(启用 bg_btn_upgrade / 禁用 dbg_btn_unenable):
private void updateUpgradeButton() {
if (!viewModel.getSelectedDeviceList().isEmpty()) {
binding.btnUpgrade.setEnabled(true);
binding.btnUpgrade.setBackground(ResourcesCompat.getDrawable(getResources(), R.drawable.bg_btn_upgrade, null));
} else {
binding.btnUpgrade.setEnabled(false);
binding.btnUpgrade.setBackground(ResourcesCompat.getDrawable(getResources(), R.drawable.dbg_btn_unenable, null));
}
}
Source: UpgradeFragment.java#L187-L195
点击升级按钮时还会做二次校验:遍历所有选中设备,只要有一台 getSelectFile() == null 就放弃弹窗并用 Snackbar 提示 ota_please_chose_file。校验通过后,通过 child FragmentManager 复用已存在的 DialogUpgradeDevice 实例(防止重复弹窗),不存在才新建并传入选中设备列表,升级完成回调会再次延迟刷新设备列表:
binding.btnUpgrade.setOnClickListener(v -> {
boolean ready = true;
for (BroadcastBoxInfo info : viewModel.getSelectedDeviceList()) {
if (info.getSelectFile() == null) {
ready = false;
break;
}
}
if (ready) {
DialogUpgradeDevice upgradeDevice = null;
Fragment fragment = getChildFragmentManager().findFragmentByTag(DialogUpgradeDevice.class.getSimpleName());
if (fragment instanceof DialogUpgradeDevice) {
upgradeDevice = (DialogUpgradeDevice) fragment;
}
if (null == upgradeDevice) {
upgradeDevice = new DialogUpgradeDevice(viewModel.getSelectedDeviceList(), infos -> {
uiHandler.removeMessages(MSG_UPDATE_DEVICE_LIST);
uiHandler.sendEmptyMessageDelayed(MSG_UPDATE_DEVICE_LIST, 300);
});
}
if (!upgradeDevice.isShow()) {
upgradeDevice.show(getChildFragmentManager(), DialogUpgradeDevice.class.getSimpleName());
}
} else {
showSnackBar(getString(R.string.ota_please_chose_file));
}
});
Source: UpgradeFragment.java#L69-L95
文件选择对话框:DialogUpgradeFilePicker
DialogUpgradeFilePicker 继承 DialogFragment,负责让用户从 FileOpViewModel 已扫描到的升级文件集合中挑选一个文件,并在展示前完成固件与设备的匹配校验。
参数传递与初始化
静态工厂 newInstance(path, uid, pid) 通过 Bundle 携带三个参数:当前已选文件路径(用于高亮)、目标设备芯片 uid 与 pid(用于过滤)。onCreate 中还原参数并设置无标题样式、允许点击外部取消;onStart 中设置 0.5 遮罩:
public static DialogUpgradeFilePicker newInstance(String path, int uid, int pid) {
DialogUpgradeFilePicker filePicker = new DialogUpgradeFilePicker();
Bundle bundle = new Bundle();
bundle.putString("file_path", path);
bundle.putInt("uid", uid);
bundle.putInt("pid", pid);
filePicker.setArguments(bundle);
return filePicker;
}
public interface OnResultListener {
void onResult(File file);
}
Source: DialogUpgradeFilePicker.java#L51-L67
屏幕适配
onViewCreated 中根据屏幕方向动态计算对话框尺寸:竖屏时宽高各取屏幕宽度的 4/5 与 5/6;横屏时取屏幕高度的 4/5 与 5/6,并水平垂直居中。这种自适应保证了大文件列表在两种屏幕方向下都有足够的可视区域。
文件过滤(后台线程)
addFile() 在子线程中遍历 viewModel.getUpgradeFiles()(HashSet<File>),这是关键设计:
- 当
uid <= 0 && pid <= 0(未指定芯片信息)时直接添加所有文件; - 否则逐文件读取字节,调用
ParseDataUtil.filterFile(bytes, uid, pid)校验。返回 0 表示匹配,才通过runOnUiThread添加到适配器;非 0 记录Filter file error日志并跳过。
private void addFile() {
new Thread(() -> {
HashSet<File> fileHashSet = viewModel.getUpgradeFiles();
for (File file : fileHashSet) {
if (uid <= 0 && pid <= 0) {
requireActivity().runOnUiThread(() -> adapter.addData(file));
continue;
}
byte[] bytes = readFile(file);
if (bytes == null) {
JL_Log.e(tag, "read file error");
break;
}
int ret = ParseDataUtil.filterFile(bytes, uid, pid);
if (ret == 0) {
requireActivity().runOnUiThread(() -> adapter.addData(file));
} else {
JL_Log.e(tag, "Filter file error: " + ret);
}
}
}).start();
}
Source: DialogUpgradeFilePicker.java#L157-L181
把过滤放到后台线程的原因:读取整个文件(FileInputStream.read)属于 I/O 操作,若在 UI 线程执行会造成卡顿与 ANR;而过滤本身必须读取文件头部完整内容才能解析固件信息,因此是"读全文件 + 头部校验"的组合,耗时不可忽略。
结果回调
列表项点击后立即 dismiss() 关闭对话框,并通过 OnResultListener.onResult(File) 把选中的 File 返回给 UpgradeFragment,由 Fragment 写回对应设备的 selectFile:
adapter.setOnItemClickListener((adapter1, view1, position) -> {
dismiss();
JL_Log.e(tag, "onItemClick=" + adapter.getItem(position).getName());
if (onResultListener != null) {
onResultListener.onResult(adapter.getItem(position));
}
});
Source: DialogUpgradeFilePicker.java#L148-L154
列表适配器:UpgradeFilePickerAdapter
UpgradeFilePickerAdapter 继承 BaseQuickAdapter,convert 中设置文件名,并将当前选中的文件路径与条目对比——匹配则显示蓝色勾选图标(ic_blue_hook),否则清空图标,实现了"当前设备已选文件"在文件列表中的视觉反馈:
@Override
protected void convert(@NonNull BaseViewHolder holder, File file) {
holder.setText(R.id.tv_file_name, file.getName());
if (!TextUtils.isEmpty(selectedFile) && selectedFile.equals(file.getAbsolutePath())) {
holder.setImageResource(R.id.iv_check, R.drawable.ic_blue_hook);
} else {
holder.setImageResource(R.id.iv_check, 0);
}
}
Source: UpgradeFilePickerAdapter.java#L32-L40
升级执行对话框:DialogUpgradeDevice
DialogUpgradeDevice 继承 SDK 的 BaseDialogFragment,是升级动作的真正执行入口。它由 UpgradeFragment 传入"选中设备列表 + 完成回调",并在 onViewCreated 中调用 MultiOTAViewModel.startMultiOTA(requireContext(), deviceInfoList) 启动多设备升级。
升级期间的防打扰保护
对话框在 onCreateView 中拦截系统返回键(setOnKeyListener 恒返回 true),并在 onViewCreated 中为窗口添加 FLAG_KEEP_SCREEN_ON,确保升级过程中用户无法通过返回键退出、屏幕不会自动熄灭;onDestroyView 中清除该标志位:
requireDialog().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
...
@Override
public void onDestroyView() {
if (requireDialog().getWindow() != null) {
requireDialog().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
super.onDestroyView();
}
Source: DialogUpgradeDevice.java#L105 与 DialogUpgradeDevice.java#L137-L142
设计意图:固件升级一旦中断(断电、断开连接、进程被杀)可能导致设备变砖,因此 UI 层主动阻止一切非预期退出路径,把中断风险降到最低。
尺寸与布局策略
竖屏时对话框高度按设备数量动态计算:h = 100 + 60 * deviceInfoList.size()(dp),即每个设备条目占 60dp 高,底部对齐(Gravity.BOTTOM)并上移 28dp。横屏时则取屏幕高度的 9/10。这种"按内容定高"的做法让进度列表始终完整可见,无需滚动即可一览所有设备的升级状态。
状态机观察与 UI 刷新
addObserver() 观察 viewModel.multiOtaStateMLD(MultiOTAViewModel 的状态 LiveData),根据 MultiOTAState.getState() 分发处理:
private void addObserver() {
viewModel.multiOtaStateMLD.observe(this, multiOTAState -> {
switch (multiOTAState.getState()) {
case MultiOTAState.STATE_IDLE: {
MultiOTAEnd otaEnd = (MultiOTAEnd) multiOTAState;
binding.tvUpgradeWarning.setVisibility(View.GONE);
binding.viewLine.setVisibility(View.VISIBLE);
binding.tvOkButton.setVisibility(View.VISIBLE);
binding.tvTitle.setText(requireActivity().getString(R.string.ota_finish));
...
}
}
});
}
Source: DialogUpgradeDevice.java#L152-L160
状态机模型类位于 model/ota 包:MultiOTAState(基类/状态常量)、MultiOTAStart、MultiOTAWorking(进度)、MultiOTAReconnect(重连)、MultiOTAEnd(结束)、MultiOTAStop(停止)。对话框主要关心结束态(STATE_IDLE 包装 MultiOTAEnd)——此时隐藏警告、显示分割线与"确定"按钮、标题切换为"升级完成",随后展示每台设备的结果。
生命周期与资源释放
onDestroy 中调用 viewModel.release() 释放 MultiOTAViewModel 持有的 OTA 资源。注意 ViewModel 作用域是 ViewModelProvider(this)(对话框自身),因此每个升级对话框实例拥有独立的升级任务上下文,与主界面的 BroadcastBoxViewModel 互不干扰。
@Override
public void onDestroy() {
super.onDestroy();
JL_Log.i(tag, "on Destroy");
viewModel.release();
}
Source: DialogUpgradeDevice.java#L144-L149
Core Flow:从选文件到升级完成
下图完整描述一次典型升级的用户操作与内部调用链:
sequenceDiagram
participant U as 用户
participant F as UpgradeFragment
participant P as DialogUpgradeFilePicker
participant FVM as FileOpViewModel
participant D as DialogUpgradeDevice
participant MVM as MultiOTAViewModel
participant SDK as OTA SDK/蓝牙
U->>F: 点击设备行"选择文件"
F->>FVM: getUpgradeFiles() 判空
FVM-->>F: 文件集合
F->>P: newInstance(filepath, uid, pid).show()
P->>FVM: 读取 getUpgradeFiles()
P->>P: 后台线程 filterFile(bytes, uid, pid)
P-->>U: 展示匹配的固件列表
U->>P: 点击某个文件
P-->>F: onResult(File) → setSelectFile()
U->>F: 点击勾选框选择设备
F->>F: updateUpgradeButton() 启用按钮
U->>F: 点击"开始升级"
F->>D: new DialogUpgradeDevice(list, cb).show()
D->>MVM: startMultiOTA(ctx, deviceInfoList)
MVM->>SDK: 逐台发起升级
SDK-->>MVM: 状态/进度回调
MVM-->>D: multiOtaStateMLD 通知
D->>D: 刷新 UpgradeProgressAdapter 进度
D-->>U: 展示"升级完成"与确定按钮
U->>D: 点击确定关闭
D-->>F: 回调 → 延迟刷新设备列表
关键步骤解读:
- 选文件:文件选择器在展示前就完成了 uid/pid 匹配过滤,用户只能看到适用于该设备的固件,从源头避免"选错固件"。
- 选设备:勾选行为与
BroadcastBoxViewModel.getSelectedDeviceList()双向同步,升级按钮状态随选中集合变化。 - 二次校验:点击升级时再次遍历确认每台设备都有文件,防止竞态条件下(如文件刚被清理)携带空文件升级。
- 升级执行:
DialogUpgradeDevice通过MultiOTAViewModel发起任务,UI 只响应状态 LiveData 刷新进度,不直接接触蓝牙协议。 - 收尾:结束态展示结果与确定按钮;关闭后回调主界面延迟 300ms 刷新设备列表,使状态回到可再次升级的初始形态。
数据模型与状态流转
升级界面涉及的关键实体:
erDiagram
BroadcastBoxInfo {
string deviceName
string address PK
int uid
int pid
bool chosen
File selectFile
bool forceUpdate
int state
}
File {
string name
string absolutePath
long length
}
MultiOTAState {
int state
string message
}
BroadcastBoxInfo ||--o| File : "selectFile"
MultiOTAState ||--|| MultiOTAEnd : "STATE_IDLE 包装结束信息"
BroadcastBoxInfo是设备列表条目,同时承载 UI 选择状态(chosen、selectFile、forceUpdate)与设备身份(address、uid、pid),是连接页与升级页共享的模型。UpgradeFragment.updateConnectedDeviceList()通过equals匹配新连接对象与缓存选择,完成选择状态的"迁移"。MultiOTAViewModel的状态对象(MultiOTAState及其子类)驱动DialogUpgradeDevice的界面阶段:进行中(隐藏确定按钮)→ 完成(STATE_IDLE+MultiOTAEnd,显示确定按钮)。
API 参考
UpgradeFragment(主界面)
| 方法 | 说明 |
|---|---|
onCreateView(inflater, container, savedInstanceState): View | 初始化 ViewBinding,默认禁用升级按钮 |
onViewCreated(view, savedInstanceState) | 获取共享 ViewModel、观察连接状态、注册子视图点击监听、触发首次设备列表刷新 |
updateConnectedDeviceList() | 合并已连接设备与缓存选择状态,刷新 RecyclerView 与选中列表 |
updateUpgradeButton() | 依据选中设备集合切换升级按钮可用性与背景 |
showNoFileDialog() | 通过 NotifyDialog 提示"请先添加升级文件" |
showSnackBar(msg) | 使用 Snackbar 展示提示信息(如"请选择升级文件") |
DialogUpgradeFilePicker(文件选择对话框)
| 方法 | 说明 |
|---|---|
newInstance(path: String, uid: int, pid: int): DialogUpgradeFilePicker | 静态工厂,打包参数 |
setOnResultListener(listener: OnResultListener) | 注册文件选中回调 |
onCreate(savedInstanceState) | 还原 file_path/uid/pid 参数,配置无标题样式与外部点击取消 |
onViewCreated(view, savedInstanceState) | 按屏幕方向计算窗口尺寸、初始化适配器、启动后台文件过滤 |
addFile() | 子线程遍历 getUpgradeFiles() 并按 uid/pid 过滤(ParseDataUtil.filterFile) |
readFile(file: File): byte[] | 读取文件全部字节;读取失败返回 null |
interface OnResultListener { void onResult(File file); } | 选中文件回调契约 |
DialogUpgradeDevice(升级执行对话框)
| 方法 | 说明 |
|---|---|
DialogUpgradeDevice(infos: List<BroadcastBoxInfo>, listener: OnResultListener<List<BroadcastBoxInfo>>) | 构造:持有选中设备列表与完成回调 |
onViewCreated(view, savedInstanceState) | 保持屏幕常亮、按设备数计算高度、调用 startMultiOTA 启动升级 |
addObserver() | 观察 multiOtaStateMLD,按 MultiOTAState.getState() 分发界面更新 |
onDestroyView() | 清除 FLAG_KEEP_SCREEN_ON |
onDestroy() | 调用 viewModel.release() 释放 OTA 资源 |
UpgradeFilePickerAdapter
| 方法 | 说明 |
|---|---|
setSelectedFile(filepath: String) | 记录当前设备已选文件路径(用于高亮) |
convert(holder: BaseViewHolder, file: File) | 渲染文件名;匹配 selectedFile 时显示蓝色勾选图标 |
失败模式、边界情况与并发
用户操作层面的防护
| 场景 | 处理方式 | 源码位置 |
|---|---|---|
| 点击勾选框时设备未选文件 | ToastUtil.showToastLong(no_file_selected) 并返回,不改变勾选状态 | UpgradeFragment 点击监听 |
| 升级文件列表为空时点击"选择文件" | 弹出 NotifyDialog 提示"请先添加升级文件" | showNoFileDialog() |
| 点击升级但存在设备未选文件 | Snackbar 提示 ota_please_chose_file,不弹升级对话框 | 升级按钮监听 |
| 升级进行中按返回键 | setOnKeyListener 恒返回 true,禁止返回 | DialogUpgradeDevice.onCreateView |
数据与 I/O 边界
- 文件读取失败:
DialogUpgradeFilePicker.readFile()在FileNotFoundException/IOException时打印堆栈并返回 null;addFile()检测到 null 后break停止过滤剩余文件。这意味着某文件损坏会导致其后的候选文件也不被展示——属于已知的保守策略(宁可少显示,不可错显示)。 - 过滤失败:
ParseDataUtil.filterFile返回非 0 时仅记录日志并跳过该文件,不影响其他文件。 - 线程安全:文件集合来自
FileOpViewModel.getUpgradeFiles()(HashSet<File>),后台线程只读遍历;UI 更新通过runOnUiThread提交,符合 Android 单线程 UI 模型。updateConnectedDeviceList()全部在主线程执行,通过ArrayList与clear/addAll原子替换选中列表,避免迭代时结构性修改。 - 重复弹窗防护:升级对话框通过
getChildFragmentManager().findFragmentByTag(...)复用实例,文件选择对话框每次新建但由 FragmentManager 去重(同 tag 覆盖),防止连点产生多个弹窗。
连接状态竞态
deviceConnectionMLD 观察中显式排除 STATE_CONNECTING,并用 300ms 延迟合并频繁的连接事件(去除抖动)。升级完成回调同样延迟 300ms 刷新,给设备状态稳定留出时间窗。
性能与运维注意事项
- 文件过滤在后台线程:读取完整固件文件属于高成本 I/O,
addFile()使用new Thread而非 UI 线程,避免列表卡顿。注意未使用线程池,文件较多时每次打开选择器都会新建线程,属轻量可接受场景。 - 局部刷新:勾选设备、选择文件后均使用
notifyItemChanged(position)而非notifyDataSetChanged(),减少列表整体重绘。 - 界面防中断:升级对话框保持屏幕常亮并屏蔽返回键,建议运维侧同时保证 BLE 连接稳定(信号、系统省电策略),SDK 侧支持断线重连(
MultiOTAReconnect状态)。 - 资源释放:
DialogUpgradeDevice.onDestroy调用MultiOTAViewModel.release(),避免升级任务在对话框关闭后仍持有蓝牙资源。
扩展点
- 文件过滤策略:
DialogUpgradeFilePicker.addFile()中的过滤条件(uid <= 0 && pid <= 0时全部放行)是默认策略;如需支持多芯片通用固件或按文件扩展名过滤,可在此处扩展ParseDataUtil之外的判断逻辑。 - 升级按钮启用条件:
updateUpgradeButton()仅以"选中集合非空"为准;可在此基础上增加"所有选中设备均完成文件选择"的强校验(点击时已做,按钮态可进一步收紧)。 - 状态展示:
DialogUpgradeDevice对multiOtaStateMLD的 switch 只完整处理了STATE_IDLE(结束态);MultiOTAWorking、MultiOTAReconnect等状态的 UI 分支可在addObserver()中按需补充(如展示重连提示、单设备进度条)。
Related Links
- UpgradeFragment.java — 升级主界面实现
- DialogUpgradeFilePicker.java — 文件选择对话框
- DialogUpgradeDevice.java — 多设备升级进度对话框
- UpgradeFilePickerAdapter.java — 文件列表适配器
- UpgradeProgressAdapter.java — 升级进度列表适配器
- BroadcastBoxActivity.java — 承载三个 Fragment 标签页的宿主 Activity
- 相关页面:连接页
ConnectFragment、文件管理页FilesFragment、MultiOTAViewModel多设备升级状态机