当前位置: 首页 > news >正文

安卓低功耗蓝牙BLE官方开发例程(JAVA)翻译注释版

官方原文链接

https://developer.android.com/develop/connectivity/bluetooth/ble/ble-overview?hl=zh-cn


目录

低功耗蓝牙 

基础知识

关键术语和概念

角色和职责

查找 BLE 设备 

连接到 GATT 服务器 

设置绑定服务

设置 BluetoothAdapter

连接到设备

声明 GATT 回调

连接到 GATT 服务

广播动态

在活动中监听更新

关闭 GATT 连接

传输 BLE 数据 

发现服务

读取 BLE 特性

接收 GATT 通知

在后台交流 

查找设备

在后台

连接到设备

在后台

保持与设备的连接

在应用之间切换时

收听外围设备通知时




低功耗蓝牙 

bookmark_border

  • 本页内容
  • 基础知识
  • 关键术语和概念
    • 角色和职责

Android 为发挥核心作用的蓝牙低功耗 (BLE) 提供内置平台支持,并提供可供应用用于发现设备、查询服务和传输信息的 API。

常见用例包括:

  • 在临近设备间传输少量数据。
  • 与近程传感器交互,以便为用户提供基于其当前位置的自定义体验。

与传统蓝牙不同,BLE 旨在显著降低功耗。这样一来,应用便可与功率要求更严格的 BLE 设备(例如近程传感器、心率监测器和健身设备)进行通信。

注意:当用户使用 BLE 将其设备与其他设备配对时,用户设备上的所有应用都可以访问在这两个设备间传输的数据。

因此,如果您的应用捕获敏感数据,您应实现应用层安全以保护此类数据的私密性。

基础知识

为了让支持 BLE 的设备能够在彼此之间传输数据,它们必须先形成通信通道。若要使用 Bluetooth LE API,您需要在清单文件中声明多项权限。您的应用获得使用蓝牙的权限后,需要访问 BluetoothAdapter 并确定设备上是否支持蓝牙。如果支持蓝牙,设备将扫描附近的 BLE 设备。找到设备后,通过连接到 BLE 设备上的 GATT 服务器来发现 BLE 设备的功能。建立连接后,可以根据可用服务和特性与已连接的设备传输数据。

关键术语和概念

以下是对 BLE 关键术语和概念的总结:

  • 通用属性配置文件 (GATT)

    GATT 配置文件是一种通用规范,内容针对在 BLE 链路上发送和接收称为“属性”的简短数据片段。所有最新的 BLE 应用配置文件都基于 GATT。如需了解详情,请查看 GitHub 上的 Android BluetoothLeGatt 示例。

  • 配置文件

    蓝牙特别兴趣小组 (Bluetooth SIG) 为 BLE 设备定义了许多配置文件。配置文件是描述设备如何在特定应用中工作的规范。请注意,一台设备可以实现多个配置文件。例如,一台设备可能包含心率监测仪和电池电量检测器。

  • 属性协议 (ATT)

    GATT 以属性协议 (ATT) 为基础构建而成。二者的关系也被称为 GATT/ATT。ATT 经过优化,可在 BLE 设备上运行。为此,该协议尽可能少地使用字节。每个属性均由通用唯一标识符 (UUID) 进行唯一标识,后者是用于对信息进行唯一标识的字符串 ID 的 128 位标准化格式。ATT 传输的属性采用特征服务格式。

  • 特征

    特征包含一个值和 0 至多个描述特征值的描述符。您可将特征理解为类型,后者与类类似。

  • 描述符

    描述符是描述特征值的已定义属性。例如,描述符可指定人类可读的描述、特征值的可接受范围或特定于特征值的度量单位。

  • 服务

    服务是一系列特征。例如,您可能有一项名为“心率监测器”的服务,其中包含“心率测量”等特征。您可以在 bluetooth.org 上找到基于 GATT 的现有配置文件和服务的列表。

角色和职责

当设备与 BLE 设备交互时,角色和职责会以两种不同的方式划分:

  • 中央与外围。这适用于 BLE 连接本身:担任中央角色的设备进行扫描、寻找广播;外围设备发出广播。如果两个设备都仅支持外围角色,则无法相互通信;如果两个设备都仅支持中央角色,也无法相互通信。

  • GATT 服务器与 GATT 客户端。这决定两个设备建立连接后如何相互通信。处于客户端角色的设备发送数据请求,处于服务器角色的设备执行这些请求。

如需了解中心-外围设备角色划分与服务器-客户端角色划分的区别,请考虑以下示例:您有一台 Android 手机和一台支持 BLE 的活动追踪器,该追踪器会将传感器数据报告回手机。

  • 手机(中央设备)会主动扫描 BLE 设备。活动追踪器(即外围设备)会进行广告宣传,并等待收到连接请求。

  • 手机与活动追踪器建立连接后,它们便开始相互传送 GATT 元数据。在本例中,手机上运行的应用会发送数据请求,因此它充当 GATT 客户端。活动追踪器会执行这些请求,因此它充当 GATT 服务器

应用的另一种设计可能使手机扮演 GATT 服务器角色。如需了解详情,请参阅 BluetoothGattServer。




查找 BLE 设备 

bookmark_border

要查找 BLE 设备,您可以使用 startScan() 方法。此方法采用 ScanCallback 作为参数。 您必须实现此回调,因为这是返回扫描结果的方式。 由于扫描非常耗电,因此您应该注意以下事项: 指南:

  • 找到所需设备后,立即停止扫描。
  • 永不循环扫描,并始终设置扫描时间限制。具有如下特征的设备: 可能已经超出有效范围, 很耗电。

在以下示例中,BLE 应用提供了一个 activity (DeviceScanActivity),用于扫描可用的蓝牙 LE 设备和显示屏 向用户列出它们以下代码段展示了如何启动和停止 扫描:

// 定义一个 BluetoothLeScanner 实例,用于执行 BLE 扫描。
private BluetoothLeScanner bluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner();// 定义一个布尔变量来跟踪当前是否正在进行扫描。
private boolean scanning;// 创建一个 Handler 实例,用于在主线程上发布消息或运行代码块。
private Handler handler = new Handler();// 定义常量 SCAN_PERIOD 为 10 秒(10000 毫秒),表示扫描持续的时间。
private static final long SCAN_PERIOD = 10000;/*** 开始或停止 BLE 设备的扫描。*/
private void scanLeDevice() {// 如果当前没有进行扫描,则开始新的扫描。if (!scanning) {// 在指定的扫描周期后,通过 Handler 延迟执行一个 Runnable,以停止扫描。handler.postDelayed(new Runnable() {@Overridepublic void run() {// 将扫描状态设置为 false,表示不再扫描。scanning = false;// 调用 stopScan 方法并传入 leScanCallback,停止当前的 BLE 扫描。bluetoothLeScanner.stopScan(leScanCallback);}}, SCAN_PERIOD); // 设置延迟时间,即扫描周期。// 更新扫描状态为 true,表示正在扫描。scanning = true;// 调用 startScan 方法并传入 leScanCallback,开始 BLE 扫描。bluetoothLeScanner.startScan(leScanCallback);} else {// 如果当前正在进行扫描,则停止扫描。scanning = false;// 调用 stopScan 方法并传入 leScanCallback,立即停止当前的 BLE 扫描。bluetoothLeScanner.stopScan(leScanCallback);}
}
 

注意 : BluetoothLeScanner是 只能通过 BluetoothAdapter(如果蓝牙) 目前在设备上处于启用状态。如果未启用蓝牙,则 getBluetoothLeScanner() 会返回 null。

要仅扫描特定类型的外围设备,您可以改为调用 startScan(List<ScanFilter>, ScanSettings, ScanCallback)、 提供一系列 ScanFilter 对象,这些对象限制了扫描要查找的设备, ScanSettings 对象, 指定有关扫描的参数。

以下代码示例是 ScanCallback、 该接口是用于提供 BLE 扫描结果的接口。找到结果后, 它们会添加到 DeviceScanActivity 中的列表适配器中,以显示给 用户。

// 创建一个 LeDeviceListAdapter 实例,用于管理 BLE 设备列表的适配器。
private LeDeviceListAdapter leDeviceListAdapter = new LeDeviceListAdapter();// 定义一个 ScanCallback 的匿名内部类实例,作为 BLE 设备扫描的回调接口。
private ScanCallback leScanCallback =new ScanCallback() {// 当扫描到一个新的设备时,系统会调用此方法。@Overridepublic void onScanResult(int callbackType, ScanResult result) {super.onScanResult(callbackType, result); // 调用父类的方法,确保默认行为被执行。// 将扫描结果中的 BluetoothDevice 添加到适配器中。leDeviceListAdapter.addDevice(result.getDevice());// 通知适配器数据集已更改,以便更新 UI 显示最新的设备列表。leDeviceListAdapter.notifyDataSetChanged();}};

连接到 GATT 服务器 

bookmark_border

  • 本页内容
  • 设置绑定服务
  • 设置 BluetoothAdapter
  • 连接到设备
  • 声明 GATT 回调
  • 连接到 GATT 服务

与 BLE 设备交互的第一步是连接该设备。更多 具体来说就是连接到设备上的 GATT 服务器。关联 GATT 服务器,请使用 connectGatt() 方法。此方法采用三个参数: Context 对象,autoConnect(一个布尔值) 指示是否在 BLE 设备完成后立即自动连接到 可用),并且引用了 BluetoothGattCallback:

KotlinJava

bluetoothGatt = device.connectGatt(this, false, gattCallback);
// 建立与指定 BLE 设备的 GATT 连接。
bluetoothGatt = device.connectGatt(this, false, gattCallback);

这将连接到由 BLE 设备托管的 GATT 服务器,并返回 BluetoothGatt 实例, 然后,您可以使用它执行 GATT 客户端操作。调用方(Android 应用) 是 GATT 客户端通过 BluetoothGattCallback 用于向客户端传递结果,例如 连接状态,以及任何进一步的 GATT 客户端操作。

设置绑定服务

在以下示例中,BLE 应用提供了一个 activity (DeviceControlActivity) 可连接到蓝牙设备、显示设备数据、 并显示设备支持的 GATT 服务和特征。位于 该活动会与 Service 调用了 BluetoothLeService, 通过 BLE API 与 BLE 设备进行交互。沟通是 使用绑定服务执行,这样, 要连接到 BluetoothLeService 并调用函数的 activity 连接到设备BluetoothLeService需要 Binder 实现,可提供对 服务。

// 定义一个名为 BluetoothLeService 的类,它继承自 Service。
class BluetoothLeService extends Service {// 创建一个 LocalBinder 类的实例,并将其赋值给名为 binder 的成员变量。private Binder binder = new LocalBinder();// 重写 onBind 方法,它是 Service 类的一部分。当有组件(如 Activity)绑定到服务时调用此方法。@Nullable@Overridepublic IBinder onBind(Intent intent) {// 返回 binder 对象,使调用者可以通过它与服务进行交互。return binder;}// 定义一个内部类 LocalBinder,它扩展了 Binder 类。class LocalBinder extends Binder {// 提供一个公共方法 getService,用于返回当前 BluetoothLeService 实例。public BluetoothLeService getService() {// 返回 BluetoothLeService 的当前实例,允许客户端直接访问其公共方法。return BluetoothLeService.this;}}
}

activity 可以使用以下代码启动服务: bindService()、 传入 Intent 以启动 服务,即ServiceConnection 用于监听连接和断开连接事件的实现,以及一个用于 以指定其他连接选项。

// 定义一个名为 DeviceControlActivity 的类,它继承自 AppCompatActivity。
class DeviceControlActivity extends AppCompatActivity {// 声明一个 BluetoothLeService 类型的成员变量 bluetoothService,用于与蓝牙服务交互。private BluetoothLeService bluetoothService;// 创建一个 ServiceConnection 实例,用于监听服务绑定和解绑事件。private ServiceConnection serviceConnection = new ServiceConnection() {// 当服务成功绑定时调用此方法。@Overridepublic void onServiceConnected(ComponentName name, IBinder service) {// 将传入的 IBinder 转换为 LocalBinder,并通过它获取 BluetoothLeService 实例。bluetoothService = ((LocalBinder) service).getService();if (bluetoothService != null) {// 如果成功获取到服务实例,可以在这里调用服务上的方法来检查连接状态或连接设备。}}// 当服务断开连接时调用此方法。@Overridepublic void onServiceDisconnected(ComponentName name) {// 将 bluetoothService 设置为 null,表示服务已断开。bluetoothService = null;}};// 重写 onCreate 方法,它是 Activity 生命周期的一部分,在 Activity 第一次创建时调用。@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState); // 调用父类的 onCreate 方法。setContentView(R.layout.gatt_services_characteristics); // 设置 Activity 的布局文件。// 创建一个 Intent 对象,用于启动 BluetoothLeService 服务。Intent gattServiceIntent = new Intent(this, BluetoothLeService.class);// 使用 bindService 方法将 Activity 绑定到 BluetoothLeService。// 参数 serviceConnection 是回调接口,用于处理服务连接的状态变化。// Context.BIND_AUTO_CREATE 标志表示如果服务未运行,则自动创建该服务。bindService(gattServiceIntent, serviceConnection, Context.BIND_AUTO_CREATE);}
}

设置 BluetoothAdapter

服务被绑定后,需要访问 BluetoothAdapter。它应该 检查适配器在设备上是否可用。请参阅设置 蓝牙,详细了解 BluetoothAdapter。以下示例将此设置代码封装在 initialize() 函数,用于返回表示成功的 Boolean 值。

KotlinJava

// 定义一个名为 BluetoothLeService 的类,它继承自 Service。
class BluetoothLeService extends Service {// 定义一个静态常量字符串 TAG,用于日志记录的标签。这有助于在日志中识别来自此服务的消息。public static final String TAG = "BluetoothLeService";// 声明一个 BluetoothAdapter 类型的私有成员变量 bluetoothAdapter,用于管理蓝牙适配器(本地蓝牙硬件)。private BluetoothAdapter bluetoothAdapter;// 定义一个公共方法 initialize(),用于初始化蓝牙适配器并检查是否可用。public boolean initialize() {// 获取默认的蓝牙适配器实例,并将其赋值给 bluetoothAdapter 成员变量。bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();// 检查是否成功获取到蓝牙适配器。if (bluetoothAdapter == null) {// 如果没有找到蓝牙适配器,则记录错误信息,并返回 false 表示初始化失败。Log.e(TAG, "Unable to obtain a BluetoothAdapter.");return false;}// 如果成功获取到了蓝牙适配器,则返回 true 表示初始化成功。return true;}...
}

此 activity 将在其 ServiceConnection 实现中调用此函数。 处理 initialize() 函数的 false 返回值取决于 应用。您可以向用户显示一条错误消息 当前设备不支持蓝牙操作或停用任何功能 需要蓝牙才能工作在以下示例中, 系统会对 activity 调用 finish() 可将用户带回上一屏幕。

// 定义一个名为 DeviceControlsActivity 的类,它继承自 AppCompatActivity。
class DeviceControlsActivity extends AppCompatActivity {// 创建一个 ServiceConnection 实例,用于监听服务绑定和解绑事件。private ServiceConnection serviceConnection = new ServiceConnection() {// 当服务成功绑定时调用此方法。@Overridepublic void onServiceConnected(ComponentName name, IBinder service) {// 将传入的 IBinder 转换为 LocalBinder,并通过它获取 BluetoothLeService 实例。bluetoothService = ((LocalBinder) service).getService();// 检查是否成功获取到蓝牙服务实例。if (bluetoothService != null) {// 尝试初始化蓝牙服务。if (!bluetoothService.initialize()) {// 如果初始化失败,记录错误信息并结束 Activity。Log.e(TAG, "Unable to initialize Bluetooth");finish(); // 结束当前 Activity。}// 在这里可以执行设备连接逻辑,例如尝试连接到特定的 BLE 设备。// perform device connection}}// 当服务意外断开连接时调用此方法。@Overridepublic void onServiceDisconnected(ComponentName name) {// 将 bluetoothService 设置为 null,表示服务已断开。bluetoothService = null;}};...
}
 

连接到设备

初始化 BluetoothLeService 实例后,它可以连接到 BLE 设备。activity 需要将设备地址发送给服务,然后才能 发起连接。该服务将首先调用 getRemoteDevice() 在 BluetoothAdapter 上访问该设备。如果适配器找不到 在具有该地址的设备时,getRemoteDevice() 会抛出一个 IllegalArgumentException。

public boolean connect(final String address) {// 检查蓝牙适配器是否已初始化以及提供的地址是否为空。if (bluetoothAdapter == null || address == null) {// 如果蓝牙适配器未初始化或地址未指定,则记录警告信息并返回 false 表示连接失败。Log.w(TAG, "BluetoothAdapter not initialized or unspecified address.");return false;}try {// 使用给定的地址获取远程蓝牙设备对象。这一步骤可能会抛出 IllegalArgumentException,// 如果提供的地址格式不正确或不存在对应的蓝牙设备。final BluetoothDevice device = bluetoothAdapter.getRemoteDevice(address);} catch (IllegalArgumentException exception) {// 捕获 IllegalArgumentException 异常,表示使用提供的地址找不到设备。Log.w(TAG, "Device not found with provided address.");return false; // 返回 false 表示连接失败。}// 连接到设备上的 GATT 服务器(这部分代码在原始片段中被省略)。// 下面是可能的后续代码,用于实际连接到 GATT 服务器:/*if (device != null) {// 尝试连接到 GATT 服务器,并传入回调接口以处理连接状态变化等事件。bluetoothGatt = device.connectGatt(this, false, gattCallback);// 可能需要在这里添加额外的逻辑来处理连接结果,例如等待连接完成或设置超时。return true; // 假设连接操作成功启动。}*/// 注意:原始代码片段在此处结束,没有提供完整的连接逻辑。
}

当服务被触发后,DeviceControlActivity 会调用此 connect() 函数。 初始化。activity 需要传入 BLE 设备的地址。在 在以下示例中,设备地址将作为 intent 传递给 activity extra。

// 定义一个 ServiceConnection 的匿名内部类实例,用于监听服务绑定和解绑事件。
private ServiceConnection serviceConnection = new ServiceConnection() {// 当服务成功绑定时调用此方法。@Overridepublic void onServiceConnected(ComponentName name, IBinder service) {// 将传入的 IBinder 转换为 LocalBinder,并通过它获取 BluetoothLeService 实例。bluetoothService = ((LocalBinder) service).getService();// 检查是否成功获取到蓝牙服务实例。if (bluetoothService != null) {// 尝试初始化蓝牙服务。if (!bluetoothService.initialize()) {// 如果初始化失败,记录错误信息并结束当前 Activity。Log.e(TAG, "Unable to initialize Bluetooth");finish(); // 结束当前 Activity。}// 尝试连接到指定地址的设备。// perform device connectionbluetoothService.connect(deviceAddress); // 使用预先定义的 deviceAddress 进行连接。}}// 当服务意外断开连接时调用此方法。@Overridepublic void onServiceDisconnected(ComponentName name) {// 将 bluetoothService 设置为 null,表示服务已断开。bluetoothService = null;}
};
 

声明 GATT 回调

一旦该 activity 告知服务要连接到哪个设备和该服务 连接到设备时,服务需要连接到 BLE 设备。此连接需要 BluetoothGattCallback 才能接收 有关连接状态、服务发现、特征 读取和特征通知

本主题重点介绍连接状态通知。请参阅传输 BLE 数据来了解如何 服务发现、特征读取和请求特征 通知。

通过 onConnectionStateChange() 函数。 在以下示例中,回调是在 Service 类中定义的,因此它 可以搭配 BluetoothDevice 发生 服务就会连接到该网络。

// 定义一个 BluetoothGattCallback 的匿名内部类实例,用于处理与 GATT 服务器交互的回调事件。
private final BluetoothGattCallback bluetoothGattCallback = new BluetoothGattCallback() {// 当 GATT 服务器连接状态发生变化时调用此方法。@Overridepublic void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {// 检查新的连接状态是否为已连接。if (newState == BluetoothProfile.STATE_CONNECTED) {// 成功连接到 GATT 服务器,在这里可以执行进一步的操作,// 例如开始服务发现或初始化设备交互逻辑。Log.i(TAG, "Successfully connected to the GATT Server");} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {// 连接断开,从 GATT 服务器断开连接。// 可能需要在这里清理资源或通知用户连接已丢失。Log.w(TAG, "Disconnected from the GATT Server");}// 注意:通常还需要检查 status 参数以确保操作成功。status 为 BluetoothGatt.GATT_SUCCESS 表示操作成功。// 如果 status 不是 GATT_SUCCESS,可能需要处理错误情况。}
};

连接到 GATT 服务

声明 BluetoothGattCallback 后,该服务便可使用 connect() 函数中的 BluetoothDevice 对象,用于连接到 GATT 服务。

通过 connectGatt() 函数。这需要一个 Context 对象,这是一个 autoConnect 布尔值 标志和 BluetoothGattCallback。在此示例中,应用直接 连接到 BLE 设备,因此系统会为 autoConnect 传递 false

此外,还添加了 BluetoothGatt 属性。这样,该服务就可以关闭 。

// 定义一个名为 BluetoothLeService 的类,它继承自 Service。
class BluetoothLeService extends Service {...// 声明一个 BluetoothGatt 类型的私有成员变量 bluetoothGatt,// 用于管理与远程 GATT 服务器(即 BLE 设备)之间的连接。private BluetoothGatt bluetoothGatt;...// 定义一个公共方法 connect,用于尝试连接到指定地址的 BLE 设备。public boolean connect(final String address) {// 检查蓝牙适配器是否已初始化以及提供的地址是否为空。if (bluetoothAdapter == null || address == null) {// 如果蓝牙适配器未初始化或地址未指定,则记录警告信息并返回 false 表示连接失败。Log.w(TAG, "BluetoothAdapter not initialized or unspecified address.");return false;}try {// 使用给定的地址获取远程蓝牙设备对象。final BluetoothDevice device = bluetoothAdapter.getRemoteDevice(address);// 尝试连接到该设备上的 GATT 服务器,并传入回调接口以处理连接状态变化等事件。// 参数 `false` 表示不使用自动连接模式,即如果设备当前不在范围内,则不会重试连接。bluetoothGatt = device.connectGatt(this, false, bluetoothGattCallback);// 返回 true 表示连接操作成功启动。请注意,这并不意味着连接已经建立成功,// 连接结果将通过 bluetoothGattCallback 中的方法来通知。return true;} catch (IllegalArgumentException exception) {// 如果提供的地址无效或不符合预期格式,则捕获 IllegalArgumentException 异常。// 记录警告信息并返回 false 表示连接失败。Log.w(TAG, "Device not found with provided address. Unable to connect.");return false;}}
}

广播动态

当服务器与 GATT 服务器连接或断开连接时,需要通知 新状态的 activity。您可以通过多种方式实现这一目标。通过 以下示例使用广播 从服务传递到 activity 的信息。

该服务会声明一个函数来广播新状态。此函数将 操作字符串中,该字符串在广播之前会传递到 Intent 对象 发送到系统。

// 定义一个名为 broadcastUpdate 的私有方法,用于广播特定动作的意图。
private void broadcastUpdate(final String action) {// 创建一个新的 Intent 对象,指定其动作(action)参数为传入的方法参数。final Intent intent = new Intent(action);// 使用 sendBroadcast 方法发送该 Intent,通知所有注册了对应 action 的广播接收者。sendBroadcast(intent);
}
 

广播函数设置好后,即可用在 BluetoothGattCallback,用于发送 GATT 服务器。声明常量和服务的当前连接状态 。Intent

// 定义一个名为 BluetoothLeService 的类,它继承自 Service。
class BluetoothLeService extends Service {// 定义两个静态字符串常量,用于表示广播动作(Intent action),分别是连接和断开连接。public final static String ACTION_GATT_CONNECTED ="com.example.bluetooth.le.ACTION_GATT_CONNECTED";public final static String ACTION_GATT_DISCONNECTED ="com.example.bluetooth.le.ACTION_GATT_DISCONNECTED";// 定义两个静态整型常量,用于内部跟踪蓝牙 GATT 服务器的连接状态。private static final int STATE_DISCONNECTED = 0;private static final int STATE_CONNECTED = 2;// 声明一个私有整型变量 connectionState,用于保存当前的连接状态。private int connectionState;...// 定义一个 BluetoothGattCallback 的匿名内部类实例,用于处理与 GATT 服务器交互的回调事件。private final BluetoothGattCallback bluetoothGattCallback = new BluetoothGattCallback() {@Overridepublic void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {// 当 GATT 服务器连接状态发生变化时调用此方法。// 检查新的连接状态是否为已连接。if (newState == BluetoothProfile.STATE_CONNECTED) {// 成功连接到 GATT 服务器,更新连接状态为已连接。connectionState = STATE_CONNECTED;// 广播通知其他组件连接成功。broadcastUpdate(ACTION_GATT_CONNECTED);} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {// 连接断开,从 GATT 服务器断开连接,更新连接状态为断开。connectionState = STATE_DISCONNECTED;// 广播通知其他组件连接断开。broadcastUpdate(ACTION_GATT_DISCONNECTED);}// 注意:通常还需要检查 status 参数以确保操作成功。status 为 BluetoothGatt.GATT_SUCCESS 表示操作成功。// 如果 status 不是 GATT_SUCCESS,可能需要处理错误情况。}};…
}

 

在活动中监听更新

服务广播连接更新后,activity 需要 实现 BroadcastReceiver。 在设置 activity 时注册此接收器,并在 activity 正在离开屏幕。通过监听来自该服务的事件, activity 能够根据当前的 BLE 设备的连接状态。

// 定义一个名为 DeviceControlsActivity 的类,它继承自 AppCompatActivity。
class DeviceControlsActivity extends AppCompatActivity {...// 定义一个 BroadcastReceiver 的匿名内部类实例,用于监听蓝牙 GATT 事件的广播更新。private final BroadcastReceiver gattUpdateReceiver = new BroadcastReceiver() {@Overridepublic void onReceive(Context context, Intent intent) {// 获取广播的意图动作(action),用于识别广播类型。final String action = intent.getAction();// 检查广播动作是否为 ACTION_GATT_CONNECTED。if (BluetoothLeService.ACTION_GATT_CONNECTED.equals(action)) {// 如果是连接成功的广播,则设置 connected 标志为 true,// 并调用 updateConnectionState 方法更新 UI 显示已连接状态。connected = true;updateConnectionState(R.string.connected);} else if (BluetoothLeService.ACTION_GATT_DISCONNECTED.equals(action)) {// 如果是断开连接的广播,则设置 connected 标志为 false,// 并调用 updateConnectionState 方法更新 UI 显示已断开状态。connected = false;updateConnectionState(R.string.disconnected);}}};// 重写 onResume 方法,在活动恢复时注册广播接收器并尝试连接到设备。@Overrideprotected void onResume() {super.onResume(); // 调用父类方法,确保正常的生命周期管理。// 注册 gattUpdateReceiver,以便它可以接收蓝牙 GATT 状态变化的广播。registerReceiver(gattUpdateReceiver, makeGattUpdateIntentFilter());// 如果 bluetoothService 不为空,尝试连接到指定地址的设备,并记录操作结果。if (bluetoothService != null) {final boolean result = bluetoothService.connect(deviceAddress);Log.d(TAG, "Connect request result=" + result); // 记录连接请求的结果。}}// 重写 onPause 方法,在活动暂停时取消注册广播接收器以节省资源。@Overrideprotected void onPause() {super.onPause(); // 调用父类方法,确保正常的生命周期管理。// 取消注册 gattUpdateReceiver,避免内存泄漏。unregisterReceiver(gattUpdateReceiver);}// 定义一个静态方法用于创建一个 IntentFilter,该过滤器包含两个动作:// ACTION_GATT_CONNECTED 和 ACTION_GATT_DISCONNECTED。private static IntentFilter makeGattUpdateIntentFilter() {final IntentFilter intentFilter = new IntentFilter();// 向 intentFilter 添加 ACTION_GATT_CONNECTED 动作。intentFilter.addAction(BluetoothLeService.ACTION_GATT_CONNECTED);// 向 intentFilter 添加 ACTION_GATT_DISCONNECTED 动作。intentFilter.addAction(BluetoothLeService.ACTION_GATT_DISCONNECTED);return intentFilter; // 返回配置好的 IntentFilter。}
}
 

在传输 BLE 数据中,执行以下操作: BroadcastReceiver 也用于以如下形式传达服务发现: 以及来自设备的特征数据。

关闭 GATT 连接

处理蓝牙连接时,一个重要的步骤是关闭 使用它们。为此,请调用 close() 针对 BluetoothGatt 对象调用函数。在以下示例中,服务 存储对 BluetoothGatt 的引用。当 activity 与 服务,连接会关闭,以免消耗设备电池电量。

// 定义一个名为 BluetoothLeService 的类,它继承自 Service。
class BluetoothLeService extends Service {...// 重写 onUnbind 方法,在所有客户端解除绑定时调用。@Overridepublic boolean onUnbind(Intent intent) {// 调用 close 方法来关闭与 GATT 服务器的连接并释放资源。close();// 调用父类的 onUnbind 方法,并返回其结果。这通常用于通知系统是否应保留服务实例以供将来使用。return super.onUnbind(intent);}// 定义一个私有方法 close,用于安全地关闭蓝牙 GATT 连接并清理相关资源。private void close() {// 检查 bluetoothGatt 是否为 null,以避免尝试关闭一个已经关闭或从未初始化的连接。if (bluetoothGatt == null) {// 如果 bluetoothGatt 为 null,则直接返回,不执行任何操作。return;}// 调用 bluetoothGatt 的 close 方法来断开与远程设备的 GATT 连接,并释放所有相关的资源。bluetoothGatt.close();// 将 bluetoothGatt 设置为 null,表示连接已关闭并且不再持有对 GATT 对象的引用。bluetoothGatt = null;}
}




传输 BLE 数据 

bookmark_border

  • 本页内容
  • 发现服务
  • 读取 BLE 特性
  • 接收 GATT 通知

连接到 BLE GATT 后, 服务器,您可以使用 以了解设备上提供哪些服务、查询数据 ,并在特定 GATT 特征时请求通知 更改。

发现服务

在 BLE 设备上连接到 GATT 服务器后,首先要做的是 来执行服务发现。此信息可提供有关您所用服务 以及服务特征及其 描述符。在以下示例中,服务成功连接到 设备(通过对 onConnectionStateChange() 的 BluetoothGattCallback), 该 discoverServices() 函数从 BLE 设备中查询信息。

该服务需要覆盖 onServicesDiscovered() 函数 BluetoothGattCallback。 此函数在设备报告其可用服务时被调用。

// 定义一个名为 BluetoothLeService 的类,它继承自 Service。
class BluetoothLeService extends Service {// 定义一个静态字符串常量 ACTION_GATT_SERVICES_DISCOVERED,// 用于标识广播动作,当 GATT 服务被成功发现时发送此广播。public final static String ACTION_GATT_SERVICES_DISCOVERED ="com.example.bluetooth.le.ACTION_GATT_SERVICES_DISCOVERED";...// 定义一个 BluetoothGattCallback 的匿名内部类实例,用于处理与 GATT 服务器交互的回调事件。private final BluetoothGattCallback bluetoothGattCallback = new BluetoothGattCallback() {@Overridepublic void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {// 当 GATT 服务器连接状态发生变化时调用此方法。// 检查新的连接状态是否为已连接。if (newState == BluetoothProfile.STATE_CONNECTED) {// 成功连接到 GATT 服务器,更新连接状态为已连接。connectionState = STATE_CONNECTED;// 广播通知其他组件连接成功。broadcastUpdate(ACTION_GATT_CONNECTED);// 在成功连接后尝试发现 GATT 服务器上的服务。bluetoothGatt.discoverServices();} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {// 连接断开,从 GATT 服务器断开连接,更新连接状态为断开。connectionState = STATE_DISCONNECTED;// 广播通知其他组件连接断开。broadcastUpdate(ACTION_GATT_DISCONNECTED);}// 注意:通常还需要检查 status 参数以确保操作成功。status 为 BluetoothGatt.GATT_SUCCESS 表示操作成功。// 如果 status 不是 GATT_SUCCESS,可能需要处理错误情况。}@Overridepublic void onServicesDiscovered(BluetoothGatt gatt, int status) {// 当 GATT 服务器的服务被发现时调用此方法。// 检查服务发现的状态是否为成功。if (status == BluetoothGatt.GATT_SUCCESS) {// 如果服务发现成功,则广播通知其他组件服务已被发现。broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED);} else {// 如果服务发现失败,记录警告信息,说明服务发现的结果不是成功。Log.w(TAG, "onServicesDiscovered received: " + status);}}};
}
 

该服务使用广播来通知 活动。发现服务后,服务便可调用 getServices()至 获取报告的数据。

KotlinJava

// 定义一个名为 BluetoothLeService 的类,它继承自 Service。
class BluetoothLeService extends Service {...// 定义一个公共方法 getSupportedGattServices,用于获取支持的 GATT 服务列表。public List<BluetoothGattService> getSupportedGattServices() {// 检查 bluetoothGatt 是否为 null,以确保 GATT 连接已建立。if (bluetoothGatt == null) return null;// 调用 bluetoothGatt 的 getServices 方法来获取所有支持的服务列表,// 并将其返回给调用者。此列表包含所有由远程设备提供的 GATT 服务。return bluetoothGatt.getServices();}
}

然后,activity 在收到广播 intent 时可以调用此函数, 表示服务发现已完成。

// 定义一个名为 DeviceControlsActivity 的类,它继承自 AppCompatActivity。
class DeviceControlsActivity extends AppCompatActivity {...// 定义一个 BroadcastReceiver 的匿名内部类实例 gattUpdateReceiver,// 用于监听蓝牙 GATT 事件的广播更新。private final BroadcastReceiver gattUpdateReceiver = new BroadcastReceiver() {@Overridepublic void onReceive(Context context, Intent intent) {// 获取广播的意图动作(action),用于识别广播类型。final String action = intent.getAction();// 检查广播动作是否为 ACTION_GATT_CONNECTED。if (BluetoothLeService.ACTION_GATT_CONNECTED.equals(action)) {// 如果是连接成功的广播,则设置 connected 标志为 true,// 并调用 updateConnectionState 方法更新 UI 显示已连接状态。connected = true;updateConnectionState(R.string.connected);} else if (BluetoothLeService.ACTION_GATT_DISCONNECTED.equals(action)) {// 如果是断开连接的广播,则设置 connected 标志为 false,// 并调用 updateConnectionState 方法更新 UI 显示已断开状态。connected = false;updateConnectionState(R.string.disconnected);} else if (BluetoothLeService.ACTION_GATT_SERVICES_DISCOVERED.equals(action)) {// 如果是服务发现完成的广播,则调用 displayGattServices 方法显示所有支持的服务和特性。// 这个方法会更新用户界面以展示远程设备提供的所有 GATT 服务及其特征。displayGattServices(bluetoothService.getSupportedGattServices());}}};
}

读取 BLE 特性

一旦您的应用连接到 GATT 服务器并发现服务, 可以读取和写入属性(在支持的情况下)。例如,以下 该代码段会循环访问服务器的服务和特征,并显示 在界面中执行以下操作:

public class DeviceControlActivity extends Activity {...// 展示如何遍历支持的 GATT 服务和特征。// 在这个例子中,我们填充一个与 UI 中的 ExpandableListView 绑定的数据结构。private void displayGattServices(List<BluetoothGattService> gattServices) {if (gattServices == null) return; // 如果传入的服务列表为空,则直接返回。// 获取未知服务和特征的字符串资源,用于在找不到匹配项时显示。String unknownServiceString = getResources().getString(R.string.unknown_service);String unknownCharaString = getResources().getString(R.string.unknown_characteristic);// 创建三个列表来存储服务、特征的数据以及原始的 BluetoothGattCharacteristic 对象。ArrayList<HashMap<String, String>> gattServiceData = new ArrayList<HashMap<String, String>>();ArrayList<ArrayList<HashMap<String, String>>> gattCharacteristicData = new ArrayList<ArrayList<HashMap<String, String>>>();mGattCharacteristics = new ArrayList<ArrayList<BluetoothGattCharacteristic>>();// 遍历所有可用的 GATT 服务。for (BluetoothGattService gattService : gattServices) {HashMap<String, String> currentServiceData = new HashMap<String, String>(); // 创建一个哈希表来存储当前服务的信息。String uuid = gattService.getUuid().toString(); // 获取服务的 UUID 并转换为字符串格式。// 将服务名称和服务 UUID 存储到哈希表中。使用 SampleGattAttributes.lookup 方法查找已知服务名称,// 如果找不到则使用默认的未知服务字符串。currentServiceData.put(LIST_NAME, SampleGattAttributes.lookup(uuid, unknownServiceString));currentServiceData.put(LIST_UUID, uuid);gattServiceData.add(currentServiceData); // 将当前服务的数据添加到服务数据列表中。// 创建一个列表来存储当前服务下的所有特征数据。ArrayList<HashMap<String, String>> gattCharacteristicGroupData = new ArrayList<HashMap<String, String>>();List<BluetoothGattCharacteristic> gattCharacteristics = gattService.getCharacteristics(); // 获取该服务下的所有特征。ArrayList<BluetoothGattCharacteristic> charas = new ArrayList<BluetoothGattCharacteristic>(); // 创建一个列表来保存原始的特征对象。// 遍历所有可用的特征。for (BluetoothGattCharacteristic gattCharacteristic : gattCharacteristics) {charas.add(gattCharacteristic); // 将每个特征对象添加到原始特征列表中。HashMap<String, String> currentCharaData = new HashMap<String, String>(); // 创建一个哈希表来存储当前特征的信息。uuid = gattCharacteristic.getUuid().toString(); // 获取特征的 UUID 并转换为字符串格式。// 将特征名称和特征 UUID 存储到哈希表中。使用 SampleGattAttributes.lookup 方法查找已知特征名称,// 如果找不到则使用默认的未知特征字符串。currentCharaData.put(LIST_NAME, SampleGattAttributes.lookup(uuid, unknownCharaString));currentCharaData.put(LIST_UUID, uuid);gattCharacteristicGroupData.add(currentCharaData); // 将当前特征的数据添加到特征数据列表中。}// 将当前服务下的所有特征对象列表添加到全局特征列表中。mGattCharacteristics.add(charas);// 将当前服务下的所有特征数据列表添加到全局特征数据列表中。gattCharacteristicData.add(gattCharacteristicGroupData);}}
}
 

GATT 服务提供了一系列特征,您可以从 设备。要查询数据,请调用 readCharacteristic() 函数 BluetoothGatt,传入 BluetoothGattCharacteristic 想要阅读的内容

class BluetoothLeService extends Service {...// 定义一个公共方法 readCharacteristic,用于读取指定的 GATT 特征值。public void readCharacteristic(BluetoothGattCharacteristic characteristic) {// 检查 bluetoothGatt 是否为 null,以确保 GATT 连接已初始化。if (bluetoothGatt == null) {// 如果 bluetoothGatt 未初始化,则记录警告信息并直接返回,不执行任何操作。Log.w(TAG, "BluetoothGatt not initialized");return;}// 调用 bluetoothGatt 的 readCharacteristic 方法来读取指定特征的值。// 这将向远程设备发送请求,要求它返回该特征的当前值。bluetoothGatt.readCharacteristic(characteristic);}
}

在此示例中,服务实现了一个用于调用 readCharacteristic()。 这是一个异步调用。系统会将结果发送到 BluetoothGattCallback 函数 onCharacteristicRead()。

class BluetoothLeService extends Service {...// 定义一个 BluetoothGattCallback 的匿名内部类实例,用于处理与 GATT 服务器交互的回调事件。private final BluetoothGattCallback bluetoothGattCallback = new BluetoothGattCallback() {...// 重写 onCharacteristicRead 方法,当从 GATT 服务器读取特征值完成时调用。@Overridepublic void onCharacteristicRead(BluetoothGatt gatt,                    // 表示连接到的 GATT 服务器(即远程设备)。BluetoothGattCharacteristic characteristic, // 表示已读取其值的特征。int status                             // 操作状态码,指示读取操作是否成功。) {// 检查读取操作的状态是否为成功。if (status == BluetoothGatt.GATT_SUCCESS) {// 如果读取成功,则通过广播更新通知其他组件数据可用,// 并将读取到的特征对象作为参数传递给广播接收器。broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);} else {// 如果读取失败,可以在这里添加错误处理逻辑或记录日志。Log.w(TAG, "onCharacteristicRead failed with status: " + status);}}};
}

当特定回调被触发时,它会调用相应的 broadcastUpdate() 辅助方法,并向其传递操作。请注意, 本部分根据蓝牙心率执行解析 测量配置文件规范。

private void broadcastUpdate(final String action,final BluetoothGattCharacteristic characteristic) {// 创建一个意图(Intent)对象,用于广播更新。final Intent intent = new Intent(action);// 特殊处理心率测量特征。根据心率测量规范解析数据。if (UUID_HEART_RATE_MEASUREMENT.equals(characteristic.getUuid())) {// 获取特征属性标志位。int flag = characteristic.getProperties();int format = -1; // 初始化格式变量为 -1,表示未知格式。// 根据标志位判断心率值的格式是 16 位无符号整数还是 8 位无符号整数。if ((flag & 0x01) != 0) {format = BluetoothGattCharacteristic.FORMAT_UINT16;Log.d(TAG, "Heart rate format UINT16.");} else {format = BluetoothGattCharacteristic.FORMAT_UINT8;Log.d(TAG, "Heart rate format UINT8.");}// 从特征中读取心率值,偏移量为 1,因为心率值通常位于特征值的第一个字节之后。final int heartRate = characteristic.getIntValue(format, 1);Log.d(TAG, String.format("Received heart rate: %d", heartRate));// 将心率值作为额外数据附加到意图中,以便接收者可以获取该信息。intent.putExtra(EXTRA_DATA, String.valueOf(heartRate));} else {// 对于所有其他特征,以十六进制格式写入数据。final byte[] data = characteristic.getValue(); // 获取特征值。if (data != null && data.length > 0) {// 如果有数据,则创建一个字符串构建器来构建十六进制表示形式。final StringBuilder stringBuilder = new StringBuilder(data.length);for(byte byteChar : data)stringBuilder.append(String.format("%02X ", byteChar)); // 每个字节转换为两位十六进制字符,并加空格分隔。// 将原始数据和十六进制表示的数据都作为额外数据附加到意图中。intent.putExtra(EXTRA_DATA, new String(data) + "\n" +stringBuilder.toString());}}// 发送广播,通知其他组件有新的数据可用。sendBroadcast(intent);
}
 

接收 GATT 通知

当出现特定特征时,BLE 应用通常会要求接收通知 更改在以下示例中,服务将实现 函数来调用 setCharacteristicNotification() 方法:

class BluetoothLeService extends Service {...// 定义一个公共方法 setCharacteristicNotification,用于启用或禁用 GATT 特征的通知/指示。public void setCharacteristicNotification(BluetoothGattCharacteristic characteristic, boolean enabled) {// 检查 bluetoothGatt 是否为 null,以确保 GATT 连接已初始化。if (bluetoothGatt == null) {// 如果 bluetoothGatt 未初始化,则记录警告信息并直接返回,不执行任何操作。Log.w(TAG, "BluetoothGatt not initialized");return; // 注意:这里应使用小写的 'return' 而不是大写的 'Return'}// 调用 bluetoothGatt 的 setCharacteristicNotification 方法来启用或禁用特征通知。// 此调用会向远程设备发送请求,要求它在特征值变化时发送通知(如果启用)或停止发送通知(如果禁用)。bluetoothGatt.setCharacteristicNotification(characteristic, enabled);// 下面的代码是特定于心率测量特征的处理逻辑。// 如果当前特征是心率测量特征,则需要进一步配置其描述符。if (UUID_HEART_RATE_MEASUREMENT.equals(characteristic.getUuid())) {// 获取心率测量特征的 Client Characteristic Configuration 描述符。// 这个描述符控制是否接收来自远程设备的通知。BluetoothGattDescriptor descriptor = characteristic.getDescriptor(UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG));// 设置描述符的值为启用通知。// 注意:这里假设我们总是启用通知。如果需要支持指示(indication),则需要额外的逻辑来区分。descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);// 将修改后的描述符值写回到远程设备。// 这将实际启用或禁用远程设备上的通知机制。bluetoothGatt.writeDescriptor(descriptor);}}
}

为某个特征启用通知后, onCharacteristicChanged() 回调将触发回调函数:

class BluetoothLeService extends Service {...// 定义一个 BluetoothGattCallback 的匿名内部类实例,用于处理与 GATT 服务器交互的回调事件。private final BluetoothGattCallback bluetoothGattCallback = new BluetoothGattCallback() {...// 重写 onCharacteristicChanged 方法,当 GATT 服务器上的特征值发生变化时调用。@Overridepublic void onCharacteristicChanged(BluetoothGatt gatt,                    // 表示连接到的 GATT 服务器(即远程设备)。BluetoothGattCharacteristic characteristic // 表示其值已更改的特征。) {// 当远程设备发送通知或指示时,这个方法会被调用,并且会包含更新后的特征对象。// 调用广播更新方法来通知其他组件有新的数据可用,// 并将变更的特征对象作为参数传递给广播接收器。broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);}};
}



在后台交流 

bookmark_border

  • 本页内容
  • 查找设备
    • 在后台
  • 连接到设备
    • 在后台
  • 保持与设备的连接
    • 在应用之间切换时
    • 收听外围设备通知时

本指南简要介绍了当您的应用在后台运行时,如何为与外围设备通信的关键用例提供支持:

  • 查找设备
  • 连接到设备
  • 保持设备连接状态

这些用例都有多个支持选项。每种方法各有优缺点,可能使其更适合或不太符合您的具体需求。

下图为本页指南的简化视图:

注意 :Android 上针对后台工作的一般指南也适用于蓝牙相关工作。

查找设备

首先,您的应用需要查找要连接的设备。如需查找 BLE 设备,您可以使用以下任一 API:

  • BluetoothLeScanner(如查找 BLE 设备中所述)。 (示例)
  • CompanionDeviceManager(如配套设备配对中所述)。 (示例)

注意 :CompanionDeviceManager 具有某些限制(例如过滤功能有限且不支持随机 MAC 地址),这些限制可能无法满足您的需求,具体取决于外围设备的实现。

在后台

在应用不可见时使用其中任一 API 不受限制,但它们都需要您的应用进程保持活跃状态。如果应用进程未运行,您可以使用以下解决方法:

  • 对于 BluetoothLeScanner:使用 PendingIntent 对象(而不是 ScanCallback 对象)调用 startScan(),以便在扫描到与您的过滤条件匹配的设备时收到通知。(示例)
  • 对于 CompanionDeviceManager:请按照让配套应用保持唤醒状态中的指南来唤醒应用,并在之前关联的设备在范围内时使其保持唤醒状态。(示例)

注意 :不建议安排定期扫描来查找设备。这种方法效率较低,因为无论设备是否在范围内,它都会定期启动应用进程。本指南中介绍的方法可确保设备在唤醒应用进程之前位于感应范围内。

连接到设备

如需在找到设备后连接到该设备,您需要从以下来源之一获取该设备的 BluetoothDevice 实例:

  • BluetoothLeScanner 扫描结果(如上一部分中所述)。
  • 从 BluetoothAdapter.getBondedDevices() 检索到的绑定设备列表。
  • 使用 BluetoothAdapter.getRemoteLeDevice() 的 BluetoothAdapter 缓存。

有了 BluetoothDevice 实例后,您便可以通过调用 connectGatt() 方法之一向相应设备发起连接请求。您传递到 autoConnect 布尔值的值决定了 GATT 客户端使用以下两种连接模式中的哪一种:

  • Direct connect (autoconnect = false):尝试直接连接到外围设备,如果设备不可用,则尝试连接失败。如果断开连接,GATT 客户端不会自动尝试重新连接。
  • 自动连接 (autoconnect = true):在外围设备可用时尝试自动连接。如果外围设备发起断开连接或外围设备不在覆盖范围内,GATT 客户端会在外围设备可用时自动尝试重新连接。

注意 :低于 10 的 Android 版本一次只能有一个连接请求,并将所有后续请求加入队列。在 Android 10 及更高版本中,系统会将连接请求分组以便批量执行。

在后台

当应用在后台运行时,连接设备不受限制,但如果您的进程被终止,连接会关闭。此外,从后台启动 activity(在 Android 10 及更高版本中)或前台服务(在 Android 12 及更高版本中)存在限制。

因此,如需在后台建立连接,应用可以使用以下解决方案:

  • 使用 WorkManager 连接到设备。
    • 您可以设置 PeriodicWorkRequest 或 OneTimeWorkRequest 来执行定义的操作,不过可能会受到应用限制。
    • 此外,您还可以受益于工作约束条件、加急工作、重试政策等 WorkManager 功能。
    • 如果需要尽可能让连接保持活跃状态以执行任务(例如同步数据或从外围设备进行轮询),则需要按照对长时间运行的 worker 的支持中的指南启动前台服务。不过,从 Android 12 开始,将适用前台服务启动限制。
  • 启动一项具有 connectedDevice 类型的前台服务。
    • 如果需要尽可能让连接保持活跃状态以执行任务(例如同步数据或从外围设备进行轮询),则需要按照对长时间运行的 worker 的支持中的指南启动前台服务。不过,从 Android 12 开始,将适用前台服务启动限制。
  • 如查找设备中所述,使用 PendingIntent 对象调用 startScan(),以在设备存在时唤醒您的进程。外围设备必须进行广播。
    • 我们建议您启动一个 worker 和一个 Job。此操作可能会被系统中断,因此仅支持短时间的通信。
    • 在 Android 12 之前的版本中,您可以直接从 PendingIntent 对象启动前台服务。
  • 使用 CompanionDeviceService 以及 REQUEST_COMPANION_RUN_IN_BACKGROUND 或 REQUEST_COMPANION_START_FOREGROUND_SERVICES_FROM_BACKGROUND 权限即可在后台启动服务。

保持与设备的连接

理想情况下,应用应仅在必要时保持与外围设备的连接,并在任务完成后断开连接。不过,在以下两种情况下,应用可能需要无限期地保持连接的活动:

  • 在应用之间切换时。
  • 在收听外围设备通知时。

在这两种情况下,都可以使用以下选项:

  • 请结合使用 CompanionDeviceService、REQUEST_COMPANION_RUN_IN_BACKGROUND 权限和 CompanionDeviceManager.startObservingDevicePresence() 方法。
  • 当应用在前台(或在某个豁免范围内)且前台类型为 connectedDevice 时,请启动前台服务。

在应用之间切换时

查找设备、连接到设备并传输数据既耗时又耗费资源。为避免每次用户切换应用或同时执行任务时连接中断且必须执行完整流程,您应使连接保持活跃状态,直到操作完成。您可以使用 connectedDevice 类型的前台服务或配套设备在线状态 API。

收听外围设备通知时

如需监听外围设备通知,应用必须调用 setCharacteristicNotification(),使用 onCharacteristicChanged() 监听回调,并使连接保持活跃状态。对于大多数应用来说,最好通过 CompanionDeviceService 来支持此用例,因为应用可能需要长时间保持监听。不过,您也可以使用前台服务。

无论是哪种情况,您都可以在终止进程后按照连接到设备部分中的说明重新连接。

相关文章:

安卓低功耗蓝牙BLE官方开发例程(JAVA)翻译注释版

官方原文链接 https://developer.android.com/develop/connectivity/bluetooth/ble/ble-overview?hlzh-cn 目录 低功耗蓝牙 基础知识 关键术语和概念 角色和职责 查找 BLE 设备 连接到 GATT 服务器 设置绑定服务 设置 BluetoothAdapter 连接到设备 声明 GATT 回…...

C++ 异常处理机制与自定义异常体系

目录 1.C语言传统的处理错误的方式 ?? 1. 终止程序 2. 返回错误码 3.实际使用中的情况 2. C异常概念?? 2.1 C异常的基本概念 2.2异常的抛出和匹配原则 2.3?异常的重新抛出 2.4 异常安全 2.5 异常规范 3. 自定义异常体系 ??? 3.1??自定义异常类 3.2??自…...

【动态库.so | 头文件.hpp】基于CMake与CMakeList编写C++自定义库

前言 最近比较忙&#xff0c;其他系列教程得等到年后一起更&#xff01;请大家多多包涵&#xff01;&#xff01;相信各位在配置C环境和各类库的时候一定经常看到如下小连招 git clone https://github.com/opencv/opencv.git cd opencv mkdir build && cd build cma…...

三、nginx实现lnmp+discuz论坛

lnmp l&#xff1a;linux操作系统 n&#xff1a;nginx前端页面 m&#xff1a;mysql数据库&#xff0c;账号密码&#xff0c;数据库等等都保存在这个数据库里面 p&#xff1a;php——nginx擅长处理的是静态页面&#xff0c;页面登录账户&#xff0c;需要请求到数据库&#…...

OpenCV相机标定与3D重建(15)计算给定图像点对应的极线(epipolar lines)函数computeCorrespondEpilines()的使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 计算给定图像点对应的极线&#xff08;epipolar lines&#xff09;。 对于立体图像对中一个图像的点&#xff0c;计算这些点在另一个图像中对应的…...

【MySQL 进阶之路】了解 性能优化 与 设计原则

1.B树的优势 “矮胖”结构&#xff1a; 矮&#xff1a;B树的每个节点存储更多的关键字&#xff0c;从而减少了树的层级&#xff08;最多三层&#xff09;&#xff0c;减少了磁盘I/O操作&#xff0c;提高了查询效率。胖&#xff1a;叶子节点存储实际的数据&#xff0c;并使用双…...

【银河麒麟高级服务器操作系统】修改容器中journal服务日志存储位置无效—分析及解决方案

了解更多银河麒麟操作系统全新产品&#xff0c;请点击访问 麒麟软件产品专区&#xff1a;https://product.kylinos.cn 开发者专区&#xff1a;https://developer.kylinos.cn 文档中心&#xff1a;https://documentkylinos.cn 服务器环境以及配置 【机型】 整机类型/架构&am…...

轻量级的 HTML 模板引擎

Mustache 简介&#xff1a;Mustache 是一个非常简单的逻辑少的模板引擎&#xff0c;支持 HTML 文件中的占位符替换。它不会执行复杂的逻辑&#xff0c;只支持简单的变量替换。 安装&#xff1a; npm install mustache示例&#xff1a; const Mustache require(mustache);c…...

SQL 查询秘籍:提升你数据库技能的实用指南

目录 一、常用表表达式&#xff08;CTEs&#xff09; &#xff08;一&#xff09;快速了解 &#xff08;二&#xff09;上手演练 练习一&#xff1a;使用 CTE 来分解一般查询逻辑 练习二&#xff1a;使用 CTE 来分解复杂的查询逻辑 二、递归CTEs &#xff08;一&#xf…...

Unity背包道具拖拽(极简版实现)

&#xff08;感觉Csdn代码页面可以再大一点或者加个放大功能 不然得划着看不太舒服&#xff09; 1.关键接口&#xff0c;三个拖拽相关的 2.关键参数&#xff0c;PointerEventData 一直没仔细看过&#xff0c;其实有包含鼠标相关的很多参数&#xff0c;鼠标点击次数&#xff…...

Vins_Fusion_gpu中source setup.bash

文章目录 source setup.bashsetup.bashsetup.sh脚本的主要功能脚本的详细解释1. **初始化和检查**2. **检测操作系统**3. **设置环境变量**4. **记住 shell 类型**5. **调用 Python 脚本生成环境变量**6. **加载环境钩子**7. **清理** 总结 _setup_util.py_setup_util.py 的完整…...

appium学习之二:adb命令

1、查看设备 adb devices 2、连接 adb connect IP:端口 3、安装 adb install xxx.apk 4、卸载 adb uninstall 【包名】 5、把对应目录下的1.txt文件传到手机sdcard下 adb push 1.txt /sdcard 6、进入对应的设备里 adb shell 7、切入sdcard目录 cd /sdcard 8、ls 查…...

Docker 安装 sentinel

Docker 安装系列 1、拉取 [rootTseng ~]# docker pull bladex/sentinel-dashboard Using default tag: latest latest: Pulling from bladex/sentinel-dashboard 4abcf2066143: Pull complete 1ec1e81da383: Pull complete 56bccb36a894: Pull complete 7cc80011dc6f: Pull…...

selinux

项目使用&#xff1a; neverallow 报错&#xff1a; 改法&#xff1a;typeattribute system_server system_writes_mnt_vendor_violators; selinux目录&#xff1a; Android/lagvm/LINUX/android/device/qcom/sepolicy_vndr Android/lagvm/LINUX/android/vendor/jidu/sepoli…...

C++11新特性之线程std::atomic

C std::atomic C 中 std::atomic 的定义及功能 std::atomic 是 C 标准库中提供的一种模板类&#xff0c;主要用于实现 原子操作&#xff0c;以保证多线程环境下对共享变量的安全操作。它提供了一种无需使用互斥锁即可安全访问共享数据的机制&#xff0c;避免了竞争条件。 特…...

MySQL中的通配符

1. 百分号 % 内部工作原理&#xff1a; 代表零个、一个或多个任意字符。LIKE A%&#xff1a;MySQL会从索引&#xff08;如果存在&#xff09;中查找所有以 "A" 开头的记录。如果没有索引&#xff0c;则需要扫描整个表。MySQL解析器将模式转换为可执行的形式&#x…...

【ETCD】【源码阅读】stepWithWaitOption方法解析

在分布式系统中&#xff0c;ETCD 作为一个强一致性、高可用的 key-value 存储系统&#xff0c;广泛应用于服务发现、配置管理等场景。ETCD 在内部采用了 Raft 协议来保证集群的一致性&#xff0c;而日志预提案&#xff08;log proposal&#xff09;是 Raft 协议中至关重要的一部…...

F12抓包01:启动、面板功能介绍、语言设置、前端样式调试

浏览器检查工具通常用来作为浏览器web服务测试过程中&#xff0c;辅助测试、排查问题、定位缺陷的工具。 本文以mac系统下&#xff0c;当前比较常用的Chrome浏览器为例&#xff0c;讲解“检查”工具的常用功能操作方法。 一、打开方式 **1、****鼠标操作&#xff1a;**浏览器…...

用前端html如何实现2024烟花效果

用HTML、CSS和JavaScript编写的网页&#xff0c;主要用于展示“2024新年快乐&#xff01;”的文字形式烟花效果。下面是对代码主要部分的分析&#xff1a; HTML结构 包含三个<canvas>元素&#xff0c;用于绘制动画。引入百度统计的脚本。 CSS样式 设置body的背景为黑…...

Maven学习(传统Jar包管理、Maven依赖管理(导入坐标)、快速下载指定jar包)

目录 一、传统Jar包管理。 &#xff08;1&#xff09;基本介绍。 &#xff08;2&#xff09;传统的Jar包导入方法。 1、手动寻找Jar包。并放置到指定目录下。 2、使用IDEA的库管理功能。 3、配置环境变量。 &#xff08;3&#xff09;传统的Jar包管理缺点。 二、Maven。 &#…...

mac电脑吧iso文件制作成u盘启动

1 查看u盘的具体路径 diskutil list 根据容量确认路径 /dev/disk2 2、卸载u盘 diskutil unmountDisk /dev/disk2 3、把iso文件写入u盘 sudo dd if/Users/dengjinshan/Downloads/Win11_24H2_Chinese_Simplified_x64.iso of/dev/disk2 bs1m 4、弹出u盘 diskutil eject /dev/dis…...

MySQL数据库的数据类型

个人主页&#xff1a;C忠实粉丝 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 C忠实粉丝 原创 MySQL数据库的数据类型 收录于专栏[MySQL] 本专栏旨在分享学习MySQL的一点学习笔记&#xff0c;欢迎大家在评论区交流讨论&#x1f48c; 目录 数据类型分类 ​…...

【golang】匿名内部协程,值传递与参数传递

代码例子 下面代码的区别是直接调用循环变量&#xff0c;这里使用的就是这个变量的引用&#xff0c;而不是将参数的副本传递给协程执行 for task : range taskChan {wg.Add(1)go func() {defer wg.Done()task.Do() // 使用外部循环变量}() }func DistributeTasks(taskChan &…...

ByteByteGo-Top 9 HTTP Request Methods 9种HTTP请求方法

更详细准确的描述直接参考 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Methods/CONNECT ​​​​​本片文章出自 EP85: Top 9 HTTP Request Methods - ByteByteGo Newsletter http request methods 1&#xff0c;GET 从服务器获取资源。 具有幂等性&#xff08;idempo…...

vue3实际案例分析:展示Vue拖拽功能的实现和效果

在现代前端开发中&#xff0c;交互式的用户界面是提升用户体验的关键因素之一。Vue.js&#xff0c;作为一个渐进式JavaScript框架&#xff0c;提供了灵活的组件系统和响应式数据绑定&#xff0c;使得实现复杂的用户交互变得简单。拖拽功能是其中一个常见的交互模式&#xff0c;…...

EasyExcel使用管道流连接InputStream和OutputStream

前言 Java中的InputSteam 是程序从其中读取数据&#xff0c; OutputSteam是程序可以往里面写入数据。 如果我们有在项目中读取数据库的记录&#xff0c; 在转存成Excel文件, 再把文件转存到OSS中。 生成Excel使用的是阿里的EasyExcel 。 他支持Output的方式写出文件内容。 而…...

源码分析之Openlayers中的ZoomToExtent控件

概述 在 Openlayers 中&#xff0c;ZoomToExtent控件可能用的不是很多&#xff0c;它主要用于将地图视图缩放到指定范围&#xff0c;应用场景就是重置地图、恢复初始状态。但是一般情况下&#xff0c;重置地图可能还会有其它操作&#xff0c;比如清除地图上绘制的点线面或者显…...

Acwing 算法基础课 数学知识 线性筛

线性筛素数 也叫欧拉筛。 int pr[maxn]; bool flg[maxn]; int main() {for (int i 2; i < maxn; i) {if (!flg[i]) pr[pr[0]] i;for (int j 1; i * pr[j] < n && j < pr[0]; j) {flg[i * pr[j]] 1;if (i % pr[j] 0) break; // 重点}} }这样筛的话&…...

K8s驱逐阈值调整

要在 Kubernetes 中调整 kubelet 驱逐阈值&#xff0c;特别是针对 imagefs 和 nodefs 的大小阈值&#xff0c;你可以通过修改 kubelet 的启动参数来实现。这些参数定义了在触发 Pod 驱逐之前&#xff0c;节点上的资源使用情况必须满足的硬性条件。 根据你提供的文件内容&#…...

【密码学】BUUCTF Crypto 1 - 12 题 WriteUp

今天&#xff0c;我在 BUUCTF 网站的 crypto section 开启了一场充满挑战的密码学之旅。 这次我一口气完成了 12 个板块的任务&#xff0c;虽然耗时较长&#xff0c;但每一次解密成功的瞬间都让我无比满足&#xff0c;那种沉浸在密码世界里的感觉真的很棒。 接下来&#xff0…...

【文献阅读】使用深度语音后验改进独立于说话者的构音障碍可懂度分类

原文名称: IMPROVED SPEAKER INDEPENDENT DYSARTHRIA INTELLIGIBILITY CLASSIFICATION USING DEEPSPEECH POSTERIORS 本文探讨了利用DeepSpeech后验概率改进说话人无关的构音障碍可理解性分类方法。作者提出了一个基于DeepSpeech(一种端到端的语音转文本引擎)输出的新特征集…...

Image Stitching using OpenCV

文章目录 简介图像拼接管道特征检测和提取特征检测特征提取 特征匹配强力匹配FLANN&#xff08;近似最近邻快速库&#xff09;匹配 单应性估计扭曲和混合结论 使用opencv进行图像拼接 原为url: https://medium.com/paulsonpremsingh7/image-stitching-using-opencv-a-step-by-s…...

如何将CSDN的文章保存为PDF?

目录 1、打开CSDN文章2、按F12或者鼠标右键选择检查并进入控制台3、在控制台输入以下代码4、然后回车&#xff08;Enter&#xff09;如果纵向显示不全就横向 1、打开CSDN文章 2、按F12或者鼠标右键选择检查并进入控制台 3、在控制台输入以下代码 (function(){ $("#side&q…...

设计模式之工厂模式:从汽车工厂到代码工厂

~犬&#x1f4f0;余~ “我欲贱而贵&#xff0c;愚而智&#xff0c;贫而富&#xff0c;可乎&#xff1f; 曰&#xff1a;其唯学乎” 工厂模式概述 想象一下你走进一家4S店准备买车。作为顾客&#xff0c;你不需要知道汽车是如何被制造出来的&#xff0c;你只需要告诉销售顾问&a…...

在 Ubuntu 20.04 上离线安装和配置 Redis

下面是从零开始配置 Redis 的完整步骤&#xff0c;包括从安装 Redis 到离线安装 Redis 包的步骤。本文将覆盖如何从下载 Redis 安装包、手动安装 Redis、创建所需目录、配置 Redis、以及如何配置 Redis 为系统服务&#xff0c;确保服务可以在启动时自动运行。 步骤 1: 准备环境…...

Redis的哨兵机制

目录 1. 文章前言2. 基本概念2.1 主从复制的问题2.2 人工恢复主节点故障2.3 哨兵机制自动恢复主节点故障 3. 安装部署哨兵&#xff08;基于docker&#xff09;3.1 安装docker3.2 编排redis主从节点3.3 编排redis-sentinel节点 4. 重新选举5. 选举原理6. 总结 1. 文章前言 &…...

服务器数据恢复—热备盘上线过程中硬盘离线导致raid5阵列崩溃的数据恢复案例

服务器数据恢复环境&#xff1a; 两组分别由4块SAS接口硬盘组建的raid5阵列&#xff0c;两组raid5阵列划分LUN并由LVM管理&#xff0c;格式化为EXT3文件系统。 服务器故障&#xff1a; RAID5阵列中一块硬盘未知原因离线&#xff0c;热备盘自动激活上线替换离线硬盘。在热备盘上…...

Android12 设置无home属性的apk为launcher

目标apk的包类名 QSSI.12/device/qcom/qssi/system.prop// add start target_app_package_name=com.mangechargespot.app target_app_class_name=com.mangechargespot.app.ui.activity.SplashActivity // add end...

STM32F407+LAN8720A +LWIP +FreeRTOS ping通

使用STM32CUBEIDE自带的 LWIP和FreeROTS 版本说明STM32CUBEIDE 操作如下1. 配置RCC/SYS2. 配置ETH/USART3. 配置EHT_RESET/LED4. 配置FreeRTOS5. 配置LWIP6. 配置时钟7. 生成单独的源文件和头文件,并生成代码8. printf重定义9. ethernetif.c添加lan8720a复位10. MY_LWIP_Init …...

构建虚幻引擎中的HUD与UI

在游戏开发中,用户界面(UI)和头部显示(HUD)是玩家体验的重要组成部分。它们提供了关键信息,并增强了游戏的互动性。虚幻引擎(Unreal Engine, UE)以其强大的可视化脚本系统Blueprint和C++ API支持,为开发者提供了一个灵活且功能丰富的环境来创建复杂的UI和HUD元素。 本…...

EXCEL数据清洗的几个功能总结备忘

目录 0 参考教材 1 用EXCEL进行数据清洗的几个功能 2 删除重复值&#xff1a; 3 找到缺失值等 4 大小写转换 5 类型转化 6 识别空格 0 参考教材 精通EXCEL数据统计与分析&#xff0c;中国&#xff0c;李宗璋用EXCEL学统计学&#xff0c;日EXCEL统计分析与决策&#x…...

【CSS in Depth 2 精译_076】12.4 @font-face 的工作原理

当前内容所在位置&#xff08;可进入专栏查看其他译好的章节内容&#xff09; 第四部分 视觉增强技术 ✔️【第 12 章 CSS 排版与间距】 ✔️ 12.1 间距设置 12.1.1 使用 em 还是 px12.1.2 对行高的深入思考12.1.3 行内元素的间距设置 12.2 Web 字体12.3 谷歌字体12.4 font-fac…...

深入源码解析:Spring Boot 如何加载 Servlet 、Filter 与 Listener

我们知道&#xff0c;Spring Boot 是在 Spring MVC 的基础上进行了封装&#xff0c;以简化开发者的工作量。尽管如此&#xff0c;Spring Boot 的底层架构依然离不开 Spring MVC 的核心组件&#xff0c;如 Servlet、Filter、Listener&#xff0c;以及RequestMappingHandlerMappi…...

Vue 让视图区域滑到指定位置、回到顶部

滑倒指定位置&#xff1a;获取指定的dom&#xff0c;然后用scrollIntoView使dom出现在视图区域 回到顶部&#xff1a;操作父级dom的scrollTop 0&#xff0c;让该父级下的列表回到顶部 代码如下 <template><div class"testDemo"><div><el-bu…...

C# 属性(Property)

C# 属性(Property) C# 中的属性(Property)是一种用于访问和设置类或结构成员的值的特殊类型的方法。属性允许开发者以字段的形式访问方法,提供了更为安全和灵活的数据封装。在本文中,我们将深入探讨 C# 属性的概念、用途、以及如何有效地使用它们。 属性的定义 属性由…...

Ubuntu20.04调整swap分区大小笔记

Ubuntu20.04调整swap分区大小笔记 参考&#xff1a;https://blog.csdn.net/sunyuhua_keyboard/article/details/142485764 第一步 禁用swap $ cat /etc/fstab # /etc/fstab: static file system information. # # Use blkid to print the universally unique identifier fo…...

【 JAVA中常见的集合操作】

JAVA中常见的集合操作 Java 提供了丰富的集合框架&#xff08;Java Collections Framework&#xff09;&#xff0c;包括多种集合接口和实现类。集合操作可以分为基本操作和高级操作。下面介绍一些常见的集合操作&#xff0c;并给出相应的代码示例。 基本集合操作 创建集合 …...

Kudu 源码编译-aarch架构 1.17.1版本

跟着官方文档编译 第一个问题&#xff1a;在make阶段时会报的问题&#xff1a; kudu/src/kudu/util/block_bloom_filter.cc:210:3: error: ‘vst1q_u32_x2’ was not declared in this scope kudu/src/kudu/util/block_bloom_filter.cc:436:5: error: ‘vst1q_u8_x2’ was no…...

JavaEE多线程案例之阻塞队列

上文我们了解了多线程案例中的单例模式&#xff0c;此文我们来探讨多线程案例之阻塞队列吧 1. 阻塞队列是什么&#xff1f; 阻塞队列是⼀种特殊的队列.也遵守"先进先出"的原则. 阻塞队列是⼀种线程安全的数据结构,并且具有以下特性: 当队列满的时候,继续⼊队列就会…...

Java Web 开发学习中:过滤器与 Ajax 异步请求

一、过滤器 Filter&#xff1a; 过滤器的概念与用途 在一个庞大的 Web 应用中&#xff0c;有许多资源需要受到保护或进行特定的预处理。过滤器就像是一位智能的守卫&#xff0c;站在资源的入口处&#xff0c;根据预先设定的规则&#xff0c;决定哪些请求可以顺利访问资源&…...