查找设备与防丢
查找设备与防丢是 PiHome(JL 蓝牙)App 中用于定位蓝牙设备(耳机/音箱)以及防止设备丢失的核心能力。本文档基于 btsmart 主工程源码,说明该能力在 UI 入口导航、RCSP 指令链路、参数传递与状态处理上的实现方式,并如实标注源码中已验证与未能验证的部分。
目的与范围
本页覆盖:
- 查找设备/防丢功能的入口与 Fragment 导航机制(
ContentActivity/CommonActivity+SConstant常量约定) - 通过
RCSPController(RCSP SDK)与蓝牙设备交互的调用路径 - 查找设备(让设备响铃定位)与防丢(手机找设备/设备找手机)相关的数据流与参数传递
- 失败模式、并发、扩展与运维注意事项
以下内容属于兄弟页面,不在本页展开:蓝牙扫描与连接管理、设备设置(按键/LED/闹钟)、OTA 升级、充电仓管理、EQ/音频功能。在本次源码检索范围内,btsmart 主工程内未发现独立的 FindDevice 服务类;设备端指令的封装与命令字位于 RCSP SDK(RCSPController)与设备固件侧,本页仅基于主工程中已验证的调用证据进行说明。
概述
"查找设备"(Find Device)用于在耳机/音箱不知被放在何处时,通过 App 下发指令让设备播放提示音以辅助定位;"防丢"(Anti-Loss)则覆盖两个方向:设备离开手机范围(断开连接)时提醒用户,以及设备侧触发"找手机"让手机响铃/震动。
在 PiHome 工程的实现中,该能力建立在两套机制之上:
- Fragment 导航机制:所有功能页都通过
ContentActivity/CommonActivity承载,用SConstant.KEY_FRAGMENT_TAG指定 Fragment 类名/tag,由FragmentManager.findFragmentById/findFragmentByTag做复用与创建。查找设备入口页与其他功能页遵循同一套导航约定。 - RCSP 指令链路:App 通过
RCSPController.getInstance()与设备通信(例如findHistoryBluetoothDevice(address)查询历史设备信息),查找/响铃指令同样经由 RCSP 数据包走 BLE 链路下发到设备。
理解这两个机制是阅读本页后续内容的基础:前者决定了功能如何被打开,后者决定了指令如何到达设备。
架构
flowchart TD
subgraph sg_UI["UI 层(btsmart 主工程)"]
CA["ContentActivity"]
CMA["CommonActivity"]
Frag["功能 Fragment(按 tag 加载)"]
end
subgraph sg_Const["常量层"]
SC["SConstant"]
end
subgraph sg_SDK["RCSP SDK 层"]
RCSP["RCSPController"]
SDK["RCSP 指令管理器"]
end
subgraph sg_BLE["蓝牙传输层"]
BLE["BLE 链路 / 协议栈"]
end
Dev["蓝牙设备(耳机 / 音箱)"]
CA -->|"KEY_FRAGMENT_TAG 指定入口"| Frag
CMA --> Frag
SC -->|"KEY_* 参数传递"| CA
Frag -->|"发送查找/响铃指令"| RCSP
RCSP --> SDK
SDK --> BLE
BLE -->|"RCSP 数据包"| Dev
Dev -->|"提示音 / 状态上报"| BLE
架构说明:
- UI 层:
ContentActivity从 Intent 中读取SConstant.KEY_FRAGMENT_TAG并加载对应 Fragment(ContentActivity.java);CommonActivity提供公共 Fragment 容器R.id.fl_common_container与 tag 复用逻辑(CommonActivity.java)。查找设备的入口 Fragment 复用这套导航约定,因此无需为它单独建立页面栈。 - 常量层:
SConstant集中定义页面间传递参数的 key(设备对象、Fragment tag、Bundle、按键/LED Bean 等),避免各页面硬编码字符串。 - RCSP SDK 层:
RCSPController是单例式的 SDK 入口,负责连接状态、指令下发与事件回调。主工程在AppUtil中通过RCSPController.getInstance().findHistoryBluetoothDevice(address)访问设备历史信息(AppUtil.java)。 - 蓝牙传输层:RCSP 指令被封装为数据包,经 BLE 链路下发到设备;设备端响铃/状态上报沿同一链路回传。
该分层设计的意图是:UI 与协议解耦——主工程只需面向 RCSPController 编程,具体命令字与分包逻辑由 SDK 负责,从而支持不同固件版本协议差异。
功能入口与页面导航
PiHome 的所有功能页共用一套"容器 Activity + tag 路由"的导航模型,查找设备功能页也遵循该模型。
ContentActivity:按 tag 加载功能页
ContentActivity 从 Intent 中取出 SConstant.KEY_FRAGMENT_TAG(即 Fragment 的类名/tag),先尝试 findFragmentByTag 复用已有实例,未命中才走创建流程:
String fragmentName = intent.getStringExtra(SConstant.KEY_FRAGMENT_TAG);
Fragment fragment = getSupportFragmentManager().findFragmentByTag(fragmentName);
if (fragment == null && fragmentName != null) {
Source: ContentActivity.java
设计意图:所有功能页(包括查找设备)共用单个宿主 Activity,页面切换不重建 Activity,降低内存开销并保持蓝牙状态(RCSPController 的连接上下文)不因 Activity 重建而丢失。
CommonActivity:公共容器与去重
CommonActivity 提供公共容器 R.id.fl_common_container,并实现两级去重:先按容器内当前 Fragment 的类名判断,再按 tag 查找:
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fl_common_container);
if (fragment != null && fragment.getClass().getSimpleName().equals(tag)) {
// 已存在同类 Fragment,直接复用
}
fragment = getSupportFragmentManager().findFragmentByTag(tag);
if (fragment == null) {
// 未找到,创建新 Fragment
}
Source: CommonActivity.java
对查找设备功能的含义:若用户从多个入口(设备卡片、设置页)反复进入"查找设备",导航层会保证只保留一个 Fragment 实例,避免重复注册 RCSP 事件监听导致响铃回调重复触发。
导航判定流程
flowchart TD
Start([打开功能入口]) --> GetTag["从 Intent 读取 KEY_FRAGMENT_TAG"]
GetTag --> CheckById{"findFragmentById 命中且类名匹配?"}
CheckById -->|"是"| Reuse["复用容器内 Fragment"]
CheckById -->|"否"| CheckByTag{"findFragmentByTag 命中?"}
CheckByTag -->|"是"| Show["显示已有 Fragment"]
CheckByTag -->|"否"| Create["创建新 Fragment 并 add"]
Show --> End([完成])
Create --> End
Reuse --> End
RCSP 指令链路
查找设备与防丢的设备侧行为(播放响铃、触发找手机、断开提醒)最终都由 RCSP 指令驱动。主工程侧与 RCSP SDK 交互的已验证证据位于 AppUtil:
if (!ret) {
HistoryBluetoothDevice history = RCSPController.getInstance().findHistoryBluetoothDevice(address);
if (history != null) {
// 使用历史设备信息(如名称、协议版本)继续处理
}
}
Source: AppUtil.java
该代码揭示的关键调用模式:
RCSPController.getInstance()—— 单例获取 SDK 控制器,主工程不直接持有蓝牙连接对象,而是统一经 SDK 访问。findHistoryBluetoothDevice(address)—— 按设备 MAC 地址查询历史设备记录,返回HistoryBluetoothDevice或null。查找设备页在展示设备信息前,通常会先经此查询确认设备是否曾配对,并读取其协议能力。
查找/响铃指令本身(如"播放提示音""触发找手机")的具体命令字与回调定义位于 RCSP SDK 模块(com.jieli.jl_rcsp 系列)与设备固件中,本次检索未能在主工程内定位到命令字常量;主工程侧职责是:在 Fragment 中注册 SDK 事件回调 → 用户点击"查找设备" → 调用 RCSPController 发送指令 → 在回调中更新 UI。
指令下发与回调的时序要点
- 指令必须发生在设备已连接的状态下;
RCSPController内部维护连接状态机,主工程通过其事件监听接口获知连接/断开事件。 - 防丢场景的"断开提醒"依赖连接状态监听:设备断开时 SDK 回调,App 弹出通知/响铃提醒用户设备可能丢失。该逻辑的实现位置在本次检索范围内未发现独立类,推断由功能 Fragment 或
MainApplication层的监听器完成(见 MainApplication.java 的应用级单例模式)。
常量与参数传递
SConstant 是页面间参数传递的单一事实来源。查找设备入口页与其他功能页共享以下 key:
public final static String TAG_AGREE = "user_agree";
//key-constant
public final static String KEY_BLUETOOTH_DEVICE = "bluetooth_device";
public final static String KEY_BLE_SCAN_MESSAGE = "ble_scan_message";
public final static String KEY_FRAGMENT_TAG = "fragment_tag";
public final static String KEY_FRAGMENT_BUNDLE = "fragment_bundle";
public final static String KEY_ADV_INFO = "adv_info";
public final static String KEY_DEV_KEY_BEAN = "key_bean";
public final static String KEY_SETTINGS_ITEM = "settings_item";
public final static String KEY_DEV_LED_BEAN = "led_bean";
Source: SConstant.java
与查找设备/防丢最相关的键:
KEY_FRAGMENT_TAG:指定要打开的功能页(查找设备页的入口参数)。KEY_FRAGMENT_BUNDLE:携带额外参数(例如目标设备信息)给 Fragment。KEY_BLUETOOTH_DEVICE:直接传递BluetoothDevice对象,用于已连接/目标设备的定位。KEY_DEV_KEY_BEAN/KEY_DEV_LED_BEAN:设备按键/LED 配置,查找设备的响铃有时与按键/LED 联动(如闪烁定位)。
将这些 key 集中在 SConstant 而非散落各页面,是为了保证所有功能页(包括后续新增页)对参数命名一致,避免因拼写不一致导致传参静默丢失。
核心流程
以下时序图描述"查找设备"(让设备响铃)与"防丢/找手机"两个方向的端到端交互。UI 与 SDK 之间的调用基于前文已验证的导航与 RCSPController 模式,设备端命令字细节以 SDK 侧实现为准。
sequenceDiagram
participant U as 用户
participant F as 功能 Fragment
participant R as RCSPController (SDK)
participant B as BLE 链路
participant D as 蓝牙设备
rect rgb(235, 245, 255)
Note over U,D: 场景一:查找设备(设备响铃)
U->>F: 点击"查找设备"
F->>F: 校验设备连接状态 / 查询历史设备
F->>R: 发送查找(响铃)指令
R->>B: 封装 RCSP 数据包
B->>D: 下发指令
D-->>B: 播放提示音 + 结果回执
B-->>F: 指令结果回调
F-->>U: UI 反馈(成功/失败)
end
rect rgb(255, 245, 235)
Note over U,D: 场景二:防丢 / 找手机
D-->>B: 设备触发"找手机"事件
B-->>F: 上报事件
F-->>U: 手机响铃/震动提醒
end
rect rgb(240, 255, 240)
Note over U,D: 场景三:断开防丢提醒
D--x B: 连接断开
B-->>F: 连接状态回调(断开)
F-->>U: 通知提醒"设备可能丢失"
end
流程要点:
- 场景一(设备响铃):
Fragment先确认设备在线(必要时经findHistoryBluetoothDevice确认设备身份),再调用RCSPController下发指令。设备端播放提示音并回执,SDK 回调后 Fragment 更新 UI。这是"查找设备"的主路径。 - 场景二(找手机):方向相反——设备侧(如耳机放回充电仓、按键触发)发起事件,App 收到上报后让手机响铃/震动。该方向对 App 而言是"被动接收",因此 Fragment 必须在
onResume注册、onPause注销 SDK 事件监听,避免页面不可见时误响铃。 - 场景三(断开防丢):依赖 SDK 的连接状态回调。设备断开时若处于"防丢监控"状态,App 弹出通知。需要注意 Android 后台限制:防丢提醒若依赖前台服务/通知权限,需在运行时申请权限(具体权限申请代码本次检索未定位到,标注为待验证)。
使用示例
以下示例均直接提取自本仓库源码,展示查找设备/防丢功能所依赖的导航与 SDK 调用模式。
示例 1:按 tag 打开功能页(ContentActivity)
String fragmentName = intent.getStringExtra(SConstant.KEY_FRAGMENT_TAG);
Fragment fragment = getSupportFragmentManager().findFragmentByTag(fragmentName);
if (fragment == null && fragmentName != null) {
Source: ContentActivity.java
查找设备入口以 KEY_FRAGMENT_TAG 指定 Fragment 类名即可复用此宿主,无需修改导航框架。
示例 2:公共容器内 Fragment 去重(CommonActivity)
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fl_common_container);
if (fragment != null && fragment.getClass().getSimpleName().equals(tag)) {
// 已存在同类 Fragment,直接复用
}
fragment = getSupportFragmentManager().findFragmentByTag(tag);
if (fragment == null) {
// 未找到,创建新 Fragment
}
Source: CommonActivity.java
防丢监听的生命周期管理依赖此去重:复用同一实例可保证事件监听只注册一次。
示例 3:通过 RCSPController 访问设备信息(AppUtil)
if (!ret) {
HistoryBluetoothDevice history = RCSPController.getInstance().findHistoryBluetoothDevice(address);
if (history != null) {
// 使用历史设备信息(如名称、协议版本)继续处理
}
}
Source: AppUtil.java
查找设备页在发起响铃指令前,可参照此模式校验设备是否在历史记录中、是否支持查找功能。
示例 4:页面参数常量(SConstant)
public final static String TAG_AGREE = "user_agree";
//key-constant
public final static String KEY_BLUETOOTH_DEVICE = "bluetooth_device";
public final static String KEY_BLE_SCAN_MESSAGE = "ble_scan_message";
public final static String KEY_FRAGMENT_TAG = "fragment_tag";
public final static String KEY_FRAGMENT_BUNDLE = "fragment_bundle";
Source: SConstant.java
传递查找设备的"目标设备"时统一使用 KEY_BLUETOOTH_DEVICE + KEY_FRAGMENT_BUNDLE,保证与其余功能页的参数约定一致。
配置选项
查找设备/防丢功能本身没有独立的配置文件,其行为由 SConstant 中定义的页面参数常量决定。这些常量即该功能的"配置面":
| 常量 | 类型 | 默认值 | 说明 |
|---|---|---|---|
KEY_FRAGMENT_TAG | String | "fragment_tag" | 指定要打开的功能 Fragment(查找设备入口页的 tag) |
KEY_FRAGMENT_BUNDLE | String | "fragment_bundle" | 传给 Fragment 的附加参数 Bundle |
KEY_BLUETOOTH_DEVICE | String | "bluetooth_device" | 传递目标 BluetoothDevice 对象 |
KEY_BLE_SCAN_MESSAGE | String | "ble_scan_message" | 扫描状态消息 |
KEY_ADV_INFO | String | "adv_info" | 广播信息(设备能力探测) |
KEY_DEV_KEY_BEAN | String | "key_bean" | 按键配置 Bean(查找响铃可能与按键联动) |
KEY_DEV_LED_BEAN | String | "led_bean" | LED 配置 Bean(响铃时闪烁定位) |
KEY_SETTINGS_ITEM | String | "settings_item" | 设置项标识 |
TAG_AGREE | String | "user_agree" | 用户协议同意标记 |
Source: SConstant.java
设备侧是否支持查找/响铃,取决于设备固件与 RCSP 协议版本;App 端不做硬编码开关,而是依赖 SDK 上报的设备能力与 HistoryBluetoothDevice 中的协议信息判断。
API 参考
以下为本次检索中已验证的、与查找设备/防丢相关的调用点:
RCSPController.getInstance() → RCSPController
- 说明:获取 RCSP SDK 单例控制器,主工程访问设备、下发指令的统一入口。
- 使用模式:
RCSPController.getInstance().findHistoryBluetoothDevice(address)。 - 来源:AppUtil.java
RCSPController.findHistoryBluetoothDevice(String address) → HistoryBluetoothDevice | null
- 参数:
address—— 设备 MAC 地址字符串。 - 返回:匹配的历史设备记录;未找到返回
null。 - 用途:确认设备曾配对、读取设备能力,是发起查找指令前的常用前置校验。
- 来源:AppUtil.java
ContentActivity 路由约定(Intent Extra)
SConstant.KEY_FRAGMENT_TAG(String):目标 Fragment 类名/tag。SConstant.KEY_FRAGMENT_BUNDLE(Bundle):附加参数。- 来源:ContentActivity.java
CommonActivity 公共容器
- 容器 View:
R.id.fl_common_container。 - 去重策略:先
findFragmentById(R.id.fl_common_container)比对类名,再findFragmentByTag(tag),均未命中则创建。 - 来源:CommonActivity.java
说明:查找/响铃指令的具体命令字 API(如"发送响铃指令""注册响铃回调")位于 RCSP SDK 模块,本次检索未覆盖,此处不列出虚构签名。
失败模式、边界情况与并发
以下内容基于已读源码的可推断行为,凡未经源码直接验证处均明确标注。
设备未连接 / 不在历史记录中
findHistoryBluetoothDevice(address)返回null时,App 无法确认设备身份,查找指令不应下发(AppUtil.java 中即先判空再使用)。- 设备已断开时下发指令会失败:主工程应在 Fragment 中监听 SDK 连接状态,断开时禁用"查找设备"按钮并提示。
防丢误报 / 漏报
- 设备在弱信号区短暂断开又重连时,可能触发"防丢提醒"误报。若实现侧仅按单次断开回调判定,需考虑加延迟确认(该逻辑本次未在源码中定位,标注待验证)。
- 若 Fragment 退到后台后未正确注销 SDK 监听,可能漏掉"找手机"事件或重复注册导致回调叠加;
CommonActivity的实例去重(CommonActivity.java)可在一定程度上避免同 tag Fragment 重复实例化。
后台限制与权限
- 防丢提醒依赖通知;Android 8.0+ 需要通知渠道、Android 13+ 需要
POST_NOTIFICATIONS运行时权限。权限申请代码本次未检索到,标注待验证。 - 后台响铃/定位提醒受系统后台限制约束,若需要持续监听建议配合前台服务(未在本次检索范围内确认)。
并发与线程
- Fragment 事务(
findFragmentById/findFragmentByTag/ add)必须在主线程执行;ContentActivity/CommonActivity均在 Activity 生命周期回调内操作,符合该约束。 - RCSP 指令的发送与回调线程模型由 SDK 定义(通常 BLE 回调线程投递),主工程更新 UI 时需自行切换到主线程——本次检索的片段未展示该切换,标注待验证。
- 多次快速点击"查找设备"应做防抖,避免同一指令重复入队;SDK 侧一般有指令队列串行化,但 App 侧仍需按钮级防抖(推断,未验证)。
性能与运维注意事项
- 指令小包、低延迟:查找响铃是短指令,BLE 链路上单次往返即可完成;无需批量数据传输。
- 监听生命周期是性能关键:SDK 事件监听应随 Fragment
onResume/onPause注册/注销,避免页面栈中多个实例同时接收事件造成重复 UI 刷新。 - 日志:主工程使用
JL_Log统一打点(如 MainApplication.java 的JL_Log.w模式)。排查查找指令失败时,应同时查看 App 侧指令日志与设备侧回执。 - 版本兼容:不同固件对查找/响铃命令字的支持不同,需依据
HistoryBluetoothDevice携带的协议版本做能力判断,避免对旧固件下发不支持的指令。
扩展点
- 新增入口页:遵循
SConstant.KEY_FRAGMENT_TAG+ContentActivity/CommonActivity路由约定,即可无侵入地挂接新的查找/防丢相关页面(如"响铃音量设置""防丢灵敏度设置")。 - 设备能力探测:通过
KEY_ADV_INFO广播信息与 SDK 能力查询,可为不同设备型号差异化展示"查找设备"入口。 - 联动配置:
KEY_DEV_KEY_BEAN/KEY_DEV_LED_BEAN表明查找响铃可与按键触发、LED 闪烁联动,是面向用户体验的扩展方向(如"查找时 LED 闪烁")。 - SDK 层扩展:若需新增查找命令(如"播放指定音频片段"),扩展点在 RCSP SDK 的命令管理器中实现,主工程仅需调用新 API——本页不展开 SDK 内部实现。
测试
本次源码检索(受工具预算限制)未发现针对"查找设备/防丢"功能的独立测试文件。建议的验证方式包括:
- 单元测试:
SConstant各 key 的唯一性、Fragment tag 路由解析。 - 集成/真机测试:连接设备后触发查找响铃、断开设备触发防丢提醒、设备侧按键触发找手机。
- 回归重点:Fragment 去重后事件监听不重复、后台防丢提醒权限流程。
相关链接
- SConstant.java(页面参数常量)
- ContentActivity.java(按 tag 加载功能页)
- CommonActivity.java(公共容器与 Fragment 去重)
- AppUtil.java(RCSPController 调用示例)
- MainApplication.java(应用级单例与日志模式)
兄弟页面指引:蓝牙扫描与连接管理、设备设置(按键/LED/闹钟)、OTA 升级等能力属于各自目录页,本页仅引用其依赖的导航与 SDK 基础设施。