设备发现与连接界面
本页介绍 Android-JL_OTA Demo 应用(BroadcastBox)中用于发现、扫描并连接 JL 系列低功耗蓝牙(BLE)设备的用户界面,包括宿主 Activity、连接页 Fragment、连接状态 LiveData 以及底层扫描调用链。
Purpose and Scope
本页覆盖 Demo 应用「设备发现与连接」这条完整链路的界面侧实现:
- 宿主界面
BroadcastBoxActivity如何组织「连接 / 文件 / 升级」三个页签; - 连接页
ConnectFragment的职责与它在整体界面中的位置(其内部实现细节在本次源码采集中未完整读取,文档会明确标注); UpgradeFragment如何通过viewModel.deviceConnectionMLD观察连接状态并刷新「已连接设备」列表;- BLE 扫描的调用方式(
scanner.startScan(ScanCallback))。
以下主题属于兄弟页面,不在本页展开:
- OTA 升级流程与升级进度界面(
UpgradeFragment的升级逻辑、UpgradeProgressAdapter); - 升级文件选择(
UpgradeFilePickerAdapter、DialogUpgradeFilePicker); - BLE 底层 SDK 实现(
BleManager、BleEventCallbackManager); - 多设备广播盒(BroadcastBox)OTA 状态机模型(
model/ota/*)。
Overview
BroadcastBox Demo 应用以「连接 → 选择文件 → 升级」作为核心用户旅程。应用启动后首先进入的是设备发现与连接界面:用户在此处扫描附近的 JL BLE 设备、查看设备列表、发起连接,并实时看到连接状态的变化。
从源码可见,整个界面由 BroadcastBoxActivity 作为单一宿主,通过 ViewPager2 挂载三个 Fragment(ConnectFragment、FilesFragment、UpgradeFragment),并用工具栏菜单在三个页签之间切换标题。连接状态通过 ViewModel 暴露的 deviceConnectionMLD(LiveData)在 Fragment 间共享:连接页负责触发发现与连接,升级页负责展示「已连接设备」列表并驱动后续升级。底层 BLE 扫描则通过 SDK 提供的 Scanner(如 ReconnectDemo 所示)以 startScan(ScanCallback) 方式启动。
设计意图:把「发现/连接」与「升级」拆成独立 Fragment、用 ViewModel + LiveData 共享连接状态,是为了让连接流程与升级流程解耦——连接页只关心建立链路,升级页只关心链路上有哪些设备可用,二者通过同一份连接状态数据协同,避免 UI 层直接互相调用。
Architecture
flowchart TD
subgraph sg_UI["UI Layer (otasdk · broadcastbox)"]
Activity["BroadcastBoxActivity<br/>(ViewPager2 宿主)"]
Connect["ConnectFragment<br/>(设备发现与连接)"]
Files["FilesFragment<br/>(文件选择)"]
Upgrade["UpgradeFragment<br/>(升级 + 已连接设备列表)"]
Adapter["UpgradeProgressAdapter / UpgradeFilePickerAdapter"]
end
subgraph sg_State["状态层"]
ViewModel["ViewModel<br/>(deviceConnectionMLD: LiveData)"]
end
subgraph sg_BLE["BLE 层"]
Scanner["Scanner<br/>(startScan + ScanCallback)"]
BleManager["BleManager / BleEventCallbackManager"]
end
Activity -->|"new Fragment[]{...}"| Connect
Activity -->|"new Fragment[]{...}"| Files
Activity -->|"new Fragment[]{...}"| Upgrade
Activity -->|"ViewPager2 展示"| Connect
Upgrade -->|"observe"| ViewModel
Connect -->|"触发扫描/连接"| Scanner
Scanner --> BleManager
ViewModel -->|"连接状态回调"| Scanner
Upgrade --> Adapter
架构说明:
BroadcastBoxActivity是唯一入口,持有Fragment[] fragments = new Fragment[]{new ConnectFragment(), new FilesFragment(), new UpgradeFragment()}(BroadcastBoxActivity.java#L22-L23),页签切换通过工具栏菜单menu_toolbar完成,标题随菜单项切换(如R.string.connect,BroadcastBoxActivity.java#L35-L36)。ConnectFragment承担本页主题——设备发现与连接。它由 Activity 直接实例化并放入 ViewPager2;其在本次采集中仅确认存在与挂载位置,Fragment 内部按钮/列表的具体布局代码未读取,详见下文「已知边界」。UpgradeFragment通过viewModel.deviceConnectionMLD.observe(...)订阅连接状态(UpgradeFragment.java#L61-L64),并维护MSG_UPDATE_DEVICE_LIST消息驱动的设备列表刷新,体现「状态驱动 UI」的模式。- BLE 扫描入口在
ReconnectDemo(测试代码)中以scanner.startScan(new ScanCallback() {...})形式给出(ReconnectDemo.java#L36-L38),说明扫描回调采用标准ScanCallback模型,发现结果经 SDK 汇聚后最终反映到连接状态。
主要实现详解
1. 宿主界面 BroadcastBoxActivity
BroadcastBoxActivity 是 Demo 应用的主 Activity,职责是组装三个业务页签并统一管理标题栏。核心代码:
Fragment[] fragments = new Fragment[]{new ConnectFragment(), new FilesFragment(), new UpgradeFragment()};
随后通过 ViewPager2(findViewById(R.id.vp2_container))承载这些 Fragment,并把工具栏标题设为「连接」:
ViewPager2 viewPager2 = findViewById(R.id.vp2_container);
tvTitle.setText(getString(R.string.connect));
toolbar.inflateMenu(R.menu.menu_toolbar);
菜单点击处理中,通过 item.getTitle() 与 R.string.connect 等字符串比较来决定切换哪个页签并更新标题(BroadcastBoxActivity.java#L65)。这里使用「标题字符串即页签标识」的约定,简化了菜单→页签的映射逻辑,代价是标题文案不能随意修改。
另外该类声明了 public boolean isSkipDestroyViewModel = false;,用于控制 ViewModel 是否在销毁时被跳过清理,属于调试/生命周期控制的开关。
2. 连接状态共享:deviceConnectionMLD
连接状态不保存在 Fragment 内部,而是放在 ViewModel 的 LiveData deviceConnectionMLD 中,升级页通过 observe 订阅:
viewModel.deviceConnectionMLD.observe(getViewLifecycleOwner(), deviceConnection -> {
JL_Log.i(TAG, ">>>> deviceConnectionMLD >> " + deviceConnection);
if (deviceConnection.getState() != BluetoothProfile.STATE_CONNECTING) {
uiHandler.removeMessages(MSG_UPDATE_DEVICE_LIST);
}
});
Source: UpgradeFragment.java#L61-L64
这段代码透露了关键设计:
- LiveData + ViewModel 是连接状态的单一事实来源,连接页写入、升级页读取,解耦两个页签;
deviceConnection.getState()使用 Android 标准BluetoothProfile.STATE_CONNECTING等常量描述连接阶段;- 当状态离开
STATE_CONNECTING(即连接成功或失败)时,移除待处理的MSG_UPDATE_DEVICE_LIST消息,随后立即刷新列表——避免在「连接中」的中间态反复触发 UI 更新。
3. 已连接设备列表刷新
UpgradeFragment 通过 Handler 消息 MSG_UPDATE_DEVICE_LIST 触发 updateConnectedDeviceList():
private void updateConnectedDeviceList() {
List<BroadcastBoxInfo> connectedDevices = viewModel.getConnectedBleDevices(); //已连接设备列表
List<BroadcastBoxInfo> realTimeList = new ArrayList<>(); //实时更新列表
JL_Log.i(TAG, "updateConnectedDeviceList >> connectedDevices = " + connectedDevices.size() + ", cacheSelected = " + cacheSelected.size());
boolean isEmpty = connectedDevices.isEmpty();
binding.groupConnected.setVisibility(isEmpty ? View.GONE : View.VISIBLE);
binding.groupNoconnect.setVisibility(isEmpty ? View.VISIBLE : View.GONE);
for (BroadcastBoxInfo boxInfo : connectedDevices) {
for (BroadcastBoxInfo selected : cacheSelected) {
// ... 标记已选设备
}
}
JL_Log.i(TAG, "updateConnectedDeviceList >> realTimeList = " + realTimeList.size());
adapter.setList(connectedDevices);
}
Source: UpgradeFragment.java#L161-L181
要点:
- 数据来源是
viewModel.getConnectedBleDevices(),返回List<BroadcastBoxInfo>,与连接状态同源; - 空态处理:
groupConnected/groupNoconnect两个 ViewGroup 根据列表是否为空互斥显示,提示用户当前没有已连接设备; - 与本地
cacheSelected缓存做比对,用于还原用户之前在列表上的勾选状态; - 最终
adapter.setList(...)一次性提交数据,属于「列表整体替换」模式,配合UpgradeProgressAdapter渲染。
4. BLE 扫描调用链
设备发现的核心是 BLE 扫描。ReconnectDemo(测试示例)展示了标准调用方式:
//开始搜索设备
scanner.startScan(new ScanCallback() {
// ...
});
Source: ReconnectDemo.java#L36-L38
startScan 接收 ScanCallback 回调对象,扫描结果(设备发现、广播数据、RSSI 等)通过回调上报;SDK 层再由 BleManager / BleEventCallbackManager 汇总成连接事件,最终写入 deviceConnectionMLD,完成「扫描 → 发现 → 连接 → 状态通知 → UI 刷新」的闭环。Demo 界面侧只需订阅状态即可,无需关心蓝牙协议细节。
已知边界(诚实说明)
本次采集中 ConnectFragment.java 的完整实现(扫描按钮、设备列表 Adapter、连接点击处理的具体代码)未在源工具预算内读取。其存在性、挂载位置(BroadcastBoxActivity 的 fragments 数组首位)与职责(对应 R.string.connect 页签)均已由宿主代码证实;如需 Fragment 内部逐行细节,请直接阅读 ConnectFragment.java 源文件。
Core Flow
sequenceDiagram
participant U as 用户
participant A as BroadcastBoxActivity
participant C as ConnectFragment
participant S as Scanner (BLE)
participant VM as ViewModel
participant F as UpgradeFragment
U->>A: 启动应用
A->>A: new Fragment[]{Connect, Files, Upgrade}
A->>C: 通过 ViewPager2 展示连接页
U->>C: 点击「搜索设备」
C->>S: startScan(ScanCallback)
S-->>C: onDeviceFound(设备列表)
U->>C: 点击设备发起连接
C->>S: connect(device)
S-->>VM: 连接状态回调
VM-->>F: deviceConnectionMLD 变化
F->>F: 状态 != STATE_CONNECTING → 刷新列表
F->>F: updateConnectedDeviceList() 展示已连接设备
F-->>U: 界面显示已连接设备,可进入升级
流程要点:
- 应用启动后 Activity 一次性创建三个 Fragment,首个可见页签即连接页;
- 用户触发扫描,
Scanner.startScan(ScanCallback)异步发现设备; - 连接结果经 SDK → ViewModel 的 LiveData 链路发布;
UpgradeFragment观察到非STATE_CONNECTING状态后移除待刷新消息、立即重建设备列表,并切换groupConnected/groupNoconnect空态视图;- 列表数据与勾选缓存合并后由 Adapter 渲染,用户即可继续文件选择与升级。
配置选项
设备发现与连接界面本身没有独立的配置文件,其「配置」体现在资源与常量层面:
| 配置项 | 类型 | 默认/取值 | 说明 |
|---|---|---|---|
| 页签菜单 | Menu 资源 | R.menu.menu_toolbar | 定义 connect / files / upgrade 三个菜单项 |
| 页签标题 | String 资源 | R.string.connect 等 | 同时用作菜单项匹配与标题栏文案 |
| 页签集合 | Fragment[] | {ConnectFragment, FilesFragment, UpgradeFragment} | Activity 内硬编码,决定页签顺序(连接页在首位) |
| ViewPager2 容器 | View ID | R.id.vp2_container | 承载三个页签的容器 |
| 连接状态来源 | LiveData | viewModel.deviceConnectionMLD | 全应用共享的连接状态单一事实来源 |
| 设备列表来源 | 方法 | viewModel.getConnectedBleDevices() | 返回 List<BroadcastBoxInfo> |
| 列表刷新消息 | Handler 消息 | MSG_UPDATE_DEVICE_LIST | 驱动 updateConnectedDeviceList() 的异步刷新 |
| 生命周期开关 | boolean | isSkipDestroyViewModel = false | 控制是否跳过 ViewModel 销毁清理 |
失败模式、边界情况与并发
以下行为均来自源码证据:
- 连接中间态处理:
UpgradeFragment在deviceConnection.getState() == BluetoothProfile.STATE_CONNECTING时不刷新设备列表,而是保留/移除MSG_UPDATE_DEVICE_LIST消息;只有在状态离开「连接中」才刷新。这避免了连接过程中列表反复跳动,也说明失败/成功都会落入同一刷新路径。 - 空态与无设备:
updateConnectedDeviceList()通过connectedDevices.isEmpty()决定groupConnected与groupNoconnect的互斥显隐,保证无设备时界面有明确提示而非空白列表。 - 列表并发更新:列表更新通过 Handler 消息串行化,避免 BLE 回调线程直接触碰 UI;
adapter.setList()整体替换而非增量修改,天然规避了「边遍历边修改」的并发问题。 - 勾选状态还原:
cacheSelected与实时列表做嵌套比对,若设备在重连/刷新后仍在线,可恢复用户之前的勾选;若设备已离线则从结果中剔除。 - 已知边界:扫描未开启、蓝牙权限缺失等系统级失败路径的具体弹窗/重试逻辑位于
ConnectFragment/ SDK 内部,本次未读取到对应实现,不在此臆断。
性能与运维注意
- 避免主线程阻塞:扫描与连接均为异步(
ScanCallback回调模型),界面通过 Handler 消息与 LiveData 观察更新,主线程只做列表渲染。 - 日志可观测性:关键路径均有
JL_Log.i日志(如deviceConnectionMLD、updateConnectedDeviceList的设备数量),排查连接问题时可直接按 TAG 过滤。 - 列表规模:
adapter.setList()全量替换在设备数量较大时会有轻微重建开销,但 Demo 场景(个位数广播盒设备)下可忽略。
扩展点
- 新增页签:修改
BroadcastBoxActivity的fragments数组并补充对应菜单项即可扩展新的业务页签。 - 自定义连接状态展示:任何页面均可
observe(viewModel.deviceConnectionMLD)获得同一份连接状态,可在不侵入连接页的情况下增加状态展示(如顶部连接指示灯)。 - 替换设备来源:
viewModel.getConnectedBleDevices()是唯一列表入口,如需接入非 BLE 设备或过滤设备,可在此方法层做适配,UI 层无需改动。
相关链接
- BroadcastBoxActivity.java — 宿主 Activity,页签组装与标题管理
- UpgradeFragment.java — 连接状态观察与已连接设备列表刷新
- ReconnectDemo.java — BLE 扫描调用示例(
startScan(ScanCallback)) - 相关兄弟主题:OTA 升级界面(
UpgradeFragment升级逻辑)、文件选择(UpgradeFilePickerAdapter)、BLE 底层实现(BleManager/BleEventCallbackManager)