蓝牙概览

Android 平台支持蓝牙网络堆栈,从而使设备可以通过无线方式与其他蓝牙设备交换数据。应用框架提供通过 Android Bluetooth API 访问蓝牙功能的权限。这些 API 允许应用以无线方式连接到其他蓝牙设备,从而实现点对点和多点无线功能。

使用 Bluetooth API,Android 应用可以执行以下操作:

  • 扫描其他蓝牙设备
  • 查询本地蓝牙适配器的配对蓝牙设备
  • 建立 RFCOMM 通道
  • 通过服务发现连接到其他设备
  • 与其他设备进行双向数据传输
  • 管理多个连接

本页将重点介绍传统蓝牙。传统蓝牙适用于耗电量较大的操作,包括 Android 设备之间的流式传输和通信。对于具有低功耗要求的蓝牙设备,Android 4.3(API 级别 18)引入了对蓝牙低功耗的 API 支持。如需了解详情,请参阅低功耗蓝牙

本文档介绍了不同的蓝牙配置文件,包括健康设备配置文件。然后介绍了如何使用 Android Bluetooth API 完成使用蓝牙进行通信的四大任务:设置蓝牙、查找局部区域内配对或可用的设备、连接设备,以及在设备之间传输数据。

基础知识

为了使支持蓝牙的设备能够在彼此之间传输数据,它们必须先通过配对过程形成通信通道。其中一台设备(可检测到的设备)使其自身可用于传入的连接请求。其他设备会使用服务发现进程找到可检测到的设备。在可检测到的设备接受配对请求后,两台设备会完成绑定过程,并在其中交换安全密钥。设备会缓存这些键以供日后使用。配对和绑定过程完成后,两台设备会交换信息。会话完成后,发起配对请求的设备会释放已将其与可发现设备关联的通道。不过,这两台设备仍保持绑定状态,因此只要它们在彼此的范围内且均未移除绑定,它们就可以在未来会话期间自动重新连接。

蓝牙权限

若要在您的应用中使用蓝牙功能,您必须声明两项权限。第一个是 BLUETOOTH。您需要该权限才能执行任何蓝牙通信,例如请求连接、接受连接和传输数据。

您必须声明的另一个权限是 ACCESS_FINE_LOCATION。您的应用需要此权限,因为蓝牙扫描可用于收集有关用户位置信息的信息。这些信息可能来自用户自己的设备,以及在商店和公交设施等地点使用的蓝牙信标。

在 Android 10 及更高版本上运行的服务无法发现蓝牙设备,除非它们具有 ACCESS_BACKGROUND_LOCATION 权限。如需详细了解此要求,请参阅在后台访问位置信息

以下代码段展示了如何检查权限。

Kotlin

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    if (ContextCompat.checkSelfPermission(baseContext,
        Manifest.permission.ACCESS_BACKGROUND_LOCATION)
        != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(
                this,
                arrayOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION),
                    PERMISSION_CODE)
        }
}

Java

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
  if (ContextCompat.checkSelfPermission(baseContext,
      Manifest.permission.ACCESS_BACKGROUND_LOCATION)
      != PackageManager.PERMISSION_GRANTED) {
          ActivityCompat.requestPermissions(
              MyActivity.this,
              new String[]{Manifest.permission.ACCESS_BACKGROUND_LOCATION},
                  PERMISSION_CODE)
      }
}

此权限要求的例外情况是,您的应用安装在搭载 Android 11 或更高版本的设备上,并且已使用配套设备配对来关联设备。在这种情况下,一旦设备关联,应用可以扫描关联的蓝牙设备,而无需位置信息权限。

在搭载 Android 8.0(API 级别 26)及更高版本的设备上,您可以使用 CompanionDeviceManager 代表您的应用扫描附近的配套设备,而无需请求位置信息权限。如需详细了解此选项,请参阅配套设备配对

注意 :如果您的应用以 Android 9(API 级别 28)或更低版本为目标平台,您可以声明 ACCESS_COARSE_LOCATION 权限,而不是 ACCESS_FINE_LOCATION 权限。

如果您希望应用启动设备发现或操纵蓝牙设置,那么除了 BLUETOOTH 权限之外,您还必须声明 BLUETOOTH_ADMIN 权限。大多数应用需要此权限只是为了能够发现本地蓝牙设备。除非该应用是应用户请求修改蓝牙设置的“电源管理器”,否则不应使用此权限授予的其他功能。

在应用清单文件中声明蓝牙权限。例如:

<manifest ... >
  <uses-permission android:name="android.permission.BLUETOOTH" />
  <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />

  <!-- If your app targets Android 9 or lower, you can declare
       ACCESS_COARSE_LOCATION instead. -->
  <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
  ...
</manifest>

如需详细了解如何声明应用权限,请参阅 <uses-permission> 参考。

使用配置文件

从 Android 3.0 开始,Bluetooth API 便支持使用蓝牙配置文件。蓝牙配置文件是适用于设备间蓝牙通信的无线接口规范。例如,免触摸配置文件。如需让手机连接到无线耳机,两台设备都必须支持免触摸配置文件。

Android Bluetooth API 为以下蓝牙配置文件提供了实现:

  • 耳机。耳机配置文件支持蓝牙耳机,以便与手机配合使用。Android 提供了 BluetoothHeadset 类,它是用于控制蓝牙耳机服务的代理。这包括蓝牙耳机和免提 (v1.5) 配置文件。BluetoothHeadset 类包含对 AT 命令的支持。如需详细了解此主题,请参阅特定于供应商的 AT 命令
  • A2DP。高级音频分发配置文件 (A2DP) 配置文件定义了如何通过蓝牙连接将高质量音频从一个设备流式传输到另一个设备。Android 提供了 BluetoothA2dp 类,它是用于控制蓝牙 A2DP 服务的代理。
  • 健康设备。Android 4.0(API 级别 14)引入了对蓝牙健康设备配置文件 (HDP) 的支持。这样,您就可以创建应用,使用蓝牙与支持蓝牙功能的健康设备(例如心率监测仪、血糖仪、温度计、体重秤等)进行通信。如需查看受支持设备及其对应的设备数据专业化代码的列表,请参阅蓝牙的 HDP 设备数据专精领域认证。这些值在 ISO/IEEE 11073-20601 [7] 规范的命名法附录中也以 MDC_DEV_SPEC_PROFILE_* 的形式引用。如需详细了解 HDP,请参阅健康设备配置文件

以下是使用配置文件的基本步骤:

  1. 获取默认适配器,如设置蓝牙中所述。
  2. 设置 BluetoothProfile.ServiceListener。此监听器会在 BluetoothProfile 客户端连接到服务或断开服务连接时向其发送通知。
  3. 使用 getProfileProxy() 与个人资料关联的个人资料代理对象建立连接。 在以下示例中,个人资料代理对象是 BluetoothHeadset 的实例。
  4. onServiceConnected() 中,获取配置文件代理对象的句柄。
  5. 有了配置文件代理对象后,您就可以用它来监控连接状态并执行与该配置文件相关的其他操作。

例如,以下代码段展示了如何连接到 BluetoothHeadset 代理对象,以便您可以控制耳机配置文件:

Kotlin

var bluetoothHeadset: BluetoothHeadset? = null

// Get the default adapter
val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter()

private val profileListener = object : BluetoothProfile.ServiceListener {

    override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
        if (profile == BluetoothProfile.HEADSET) {
            bluetoothHeadset = proxy as BluetoothHeadset
        }
    }

    override fun onServiceDisconnected(profile: Int) {
        if (profile == BluetoothProfile.HEADSET) {
            bluetoothHeadset = null
        }
    }
}

// Establish connection to the proxy.
bluetoothAdapter?.getProfileProxy(context, profileListener, BluetoothProfile.HEADSET)

// ... call functions on bluetoothHeadset

// Close proxy connection after use.
bluetoothAdapter?.closeProfileProxy(BluetoothProfile.HEADSET, bluetoothHeadset)

Java

BluetoothHeadset bluetoothHeadset;

// Get the default adapter
BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();

private BluetoothProfile.ServiceListener profileListener = new BluetoothProfile.ServiceListener() {
    public void onServiceConnected(int profile, BluetoothProfile proxy) {
        if (profile == BluetoothProfile.HEADSET) {
            bluetoothHeadset = (BluetoothHeadset) proxy;
        }
    }
    public void onServiceDisconnected(int profile) {
        if (profile == BluetoothProfile.HEADSET) {
            bluetoothHeadset = null;
        }
    }
};

// Establish connection to the proxy.
bluetoothAdapter.getProfileProxy(context, profileListener, BluetoothProfile.HEADSET);

// ... call functions on bluetoothHeadset

// Close proxy connection after use.
bluetoothAdapter.closeProfileProxy(bluetoothHeadset);

供应商特定的 AT 命令

从 Android 3.0(API 级别 11)开始,应用可以注册接收耳机发送的预定义供应商专用 AT 命令(例如 Plantronics +XEVENT 命令)的系统广播。例如,应用可以接收指示所连接设备电池电量的广播,并根据需要通知用户或执行其他操作。为 ACTION_VENDOR_SPECIFIC_HEADSET_EVENT intent 创建广播接收器,以处理耳机的供应商特定 AT 命令。

健康设备配置文件

Android 4.0(API 级别 14)引入了对蓝牙健康设备配置文件 (HDP) 的支持。这样,您就可以创建使用蓝牙与支持蓝牙功能的健康设备(例如心率监测仪、血糖仪、温度计和台秤)进行通信的应用。Bluetooth Health API 包括 BluetoothHealthBluetoothHealthCallbackBluetoothHealthAppConfiguration 类,关键类和接口中介绍了这些类。

在使用 Bluetooth Health API 时,了解以下关键 HDP 概念会很有帮助:

来源
将医疗数据传输到 Android 手机或平板电脑等智能设备的健康设备,如体重秤、血糖仪或温度计。
接收器
接收医疗数据的智能设备。在 Android HDP 应用中,接收器由 BluetoothHealthAppConfiguration 对象表示。
注册
用于注册接收器以与特定健康设备进行通信的过程。
连接
用于打开健康设备(源)和智能设备(接收器)之间的通道的过程。

创建 HDP 应用

以下是创建 Android HDP 应用所涉及的基本步骤:

  1. 获取对 BluetoothHealth 代理对象的引用。

    与常规耳机和采用 A2DP 配置文件的设备类似,您必须使用 BluetoothProfile.ServiceListenerHEALTH 配置文件类型调用 getProfileProxy(),才能与配置文件代理对象建立连接。

  2. 创建 BluetoothHealthCallback 并注册充当健康接收器的应用配置 (BluetoothHealthAppConfiguration)。
  3. 建立与健康设备的连接。

    注意 :某些设备会自动发起连接。对于这类设备,无需执行该步骤。

  4. 成功连接到健康设备后,使用文件描述符对健康设备执行读/写操作。接收的数据需要使用实现 IEEE 11073 规范的运行状况管理器进行解读。
  5. 完成后,关闭健康通道并取消注册应用。当长期闲置时,该频道也会关闭。

设置蓝牙

您需要先验证设备是否支持蓝牙,并且确保已启用蓝牙,您的应用才能通过蓝牙进行通信。

如果不支持蓝牙,您应妥善停用所有蓝牙功能。如果支持蓝牙但已停用,您可以请求用户在不离开应用的同时启用蓝牙。使用 BluetoothAdapter 分两步完成此设置:

  1. 获取 BluetoothAdapter

    所有蓝牙 activity 都需要具有 BluetoothAdapter。如需获取 BluetoothAdapter,请调用静态 getDefaultAdapter() 方法。这将返回一个 BluetoothAdapter,表示设备自身的蓝牙适配器(蓝牙无线装置)。整个系统有一个蓝牙适配器,您的应用可以使用此对象与之交互。如果 getDefaultAdapter() 返回 null,则表示设备不支持蓝牙。例如:

    Kotlin

    val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter()
    if (bluetoothAdapter == null) {
        // Device doesn't support Bluetooth
    }
    

    Java

    BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
    if (bluetoothAdapter == null) {
        // Device doesn't support Bluetooth
    }
    
  2. 启用蓝牙。

    接下来,您需要确保蓝牙已启用。调用 isEnabled() 以检查当前是否已启用蓝牙。如果此方法返回 false,则表示蓝牙处于停用状态。如需请求启用蓝牙,请调用 startActivityForResult(),并传入 ACTION_REQUEST_ENABLE intent 操作。此调用会发出通过系统设置启用蓝牙的请求(而不停止您的应用)。例如:

    Kotlin

    if (bluetoothAdapter?.isEnabled == false) {
        val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
        startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
    }
    

    Java

    if (!bluetoothAdapter.isEnabled()) {
        Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
        startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
    }
    

    系统会显示一个对话框,请求用户授予启用蓝牙的权限,如图 1 所示。如果用户响应“Yes”,系统将开始启用蓝牙,并且会在该过程完成(或失败)后返回您的应用。

    图 1:启用蓝牙对话框。

    传递给 startActivityForResult()REQUEST_ENABLE_BT 常量是在本地定义的整数,并且必须大于 0。系统会在 onActivityResult() 实现中将此常量作为 requestCode 参数传回给您。

    如果成功启用蓝牙,您的 activity 会在 onActivityResult() 回调中收到 RESULT_OK 结果代码。如果由于某个错误(或用户回复“No”)而未启用蓝牙,则结果代码为 RESULT_CANCELED

(可选)您的应用还可以监听 ACTION_STATE_CHANGED 广播 intent,每当蓝牙状态发生变化时,系统都会广播此 intent。此广播包含 extra 字段 EXTRA_STATEEXTRA_PREVIOUS_STATE,分别包含新旧蓝牙状态。这些额外字段的可能值包括 STATE_TURNING_ONSTATE_ONSTATE_TURNING_OFFSTATE_OFF。如果您的应用需要检测对蓝牙状态做出的运行时更改,则监听此广播可能会很有用。

提示:启用可检测性会自动启用蓝牙。如果您计划在执行蓝牙 activity 之前一直启用设备的可检测性,则可以跳过上面的第 2 步。如需了解详情,请参阅本页面中的启用可检测性部分。

查找设备

使用 BluetoothAdapter,您可以通过设备发现或查询配对设备列表来查找远程蓝牙设备。

设备发现是一种扫描过程,会在局部区域内搜索支持蓝牙的设备,并请求每种设备的一些信息。此过程有时也称为“发现”“查询”或“扫描”。不过,仅当附近的蓝牙设备目前因处于可检测到状态而接受信息请求时,才会响应发现请求。如果设备可被检测到,它会通过分享一些信息(例如设备的名称、类及其唯一 MAC 地址)来响应发现请求。利用这些信息,执行发现过程的设备可以选择发起与被发现设备的连接。

由于可检测到的设备可能会泄露用户位置信息,因此设备发现过程需要位置信息访问权限。如果您的应用在搭载 Android 8.0(API 级别 26)或更高版本的设备上使用,请使用 Companion Device Manager API。此 API 会代表您的应用执行设备发现,因此该应用无需请求位置信息权限

在首次与远程设备建立连接后,系统会自动向用户显示配对请求。设备配对后,系统会保存有关该设备的基本信息(例如设备的名称、类和 MAC 地址),并且可使用 Bluetooth API 读取这些信息。借助远程设备的已知 MAC 地址,您可以随时向其发起与远程设备的连接,而无需执行发现操作(假定该设备仍在有效范围内)。

请注意,被配对与被连接之间存在区别:

  • “要配对”意味着两台设备知晓彼此的存在,具有可用于身份验证的共享链路密钥,并且能够与彼此建立加密连接。
  • 被连接是指设备当前共享一个 RFCOMM 通道,并且能够相互传输数据。当前的 Android Bluetooth API 要求对设备进行配对,然后才能建立 RFCOMM 连接。使用 Bluetooth API 发起加密连接时,系统会自动执行配对。

以下部分介绍了如何查找已配对的设备,或使用设备发现来发现新设备。

注意:默认情况下,Android 设备处于不可检测到状态。用户可以通过系统设置将设备设为在有限的时间内处于可检测到状态,或者应用可以请求用户启用可检测性,而无需离开应用。如需了解详情,请参阅本页面中的启用可检测性部分。

查询已配对设备

在执行设备发现之前,有必要查询已配对的设备集,以了解所需的设备是否处于已知状态。为此,请调用 getBondedDevices()。此方法会返回一组表示已配对设备的 BluetoothDevice 对象。例如,您可以查询所有已配对的设备,并获取每台设备的名称和 MAC 地址,如以下代码段所示:

Kotlin

val pairedDevices: Set<BluetoothDevice>? = bluetoothAdapter?.bondedDevices
pairedDevices?.forEach { device ->
    val deviceName = device.name
    val deviceHardwareAddress = device.address // MAC address
}

Java

Set<BluetoothDevice> pairedDevices = bluetoothAdapter.getBondedDevices();

if (pairedDevices.size() > 0) {
    // There are paired devices. Get the name and address of each paired device.
    for (BluetoothDevice device : pairedDevices) {
        String deviceName = device.getName();
        String deviceHardwareAddress = device.getAddress(); // MAC address
    }
}

如需发起与蓝牙设备的连接,您只需从关联的 BluetoothDevice 对象中获取 MAC 地址,即可通过调用 getAddress() 检索该地址。如需详细了解如何创建连接,请参阅有关连接设备的部分。

注意:执行设备发现会消耗蓝牙适配器的大量资源。找到要连接的设备后,请确保先使用 cancelDiscovery() 停止发现,然后再尝试连接。此外,您不应在连接到设备时执行发现操作,因为发现过程会显著减少可供任何现有连接使用的带宽。

发现设备

如需开始发现设备,只需调用 startDiscovery() 即可。该过程是异步的,并且会返回一个布尔值,指示发现是否已成功启动。发现过程通常包含大约 12 秒的查询扫描,随后会对发现的每个设备进行页面扫描,以检索其蓝牙名称。

为了接收有关每台发现的设备的信息,您的应用必须为 ACTION_FOUND intent 注册 BroadcastReceiver。系统会为每个设备广播此 intent。该 intent 包含 extra 字段 EXTRA_DEVICEEXTRA_CLASS,而这两个字段分别包含 BluetoothDeviceBluetoothClass。以下代码段展示了如何在发现设备时通过注册来处理广播:

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    ...

    // Register for broadcasts when a device is discovered.
    val filter = IntentFilter(BluetoothDevice.ACTION_FOUND)
    registerReceiver(receiver, filter)
}

// Create a BroadcastReceiver for ACTION_FOUND.
private val receiver = object : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) {
        val action: String = intent.action
        when(action) {
            BluetoothDevice.ACTION_FOUND -> {
                // Discovery has found a device. Get the BluetoothDevice
                // object and its info from the Intent.
                val device: BluetoothDevice =
                        intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
                val deviceName = device.name
                val deviceHardwareAddress = device.address // MAC address
            }
        }
    }
}

override fun onDestroy() {
    super.onDestroy()
    ...

    // Don't forget to unregister the ACTION_FOUND receiver.
    unregisterReceiver(receiver)
}

Java

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...

    // Register for broadcasts when a device is discovered.
    IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
    registerReceiver(receiver, filter);
}

// Create a BroadcastReceiver for ACTION_FOUND.
private final BroadcastReceiver receiver = new BroadcastReceiver() {
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if (BluetoothDevice.ACTION_FOUND.equals(action)) {
            // Discovery has found a device. Get the BluetoothDevice
            // object and its info from the Intent.
            BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
            String deviceName = device.getName();
            String deviceHardwareAddress = device.getAddress(); // MAC address
        }
    }
};

@Override
protected void onDestroy() {
    super.onDestroy();
    ...

    // Don't forget to unregister the ACTION_FOUND receiver.
    unregisterReceiver(receiver);
}

如需发起与蓝牙设备的连接,只需从关联的 BluetoothDevice 对象获取 MAC 地址,您可以通过调用 getAddress() 检索该地址。如需详细了解如何创建连接,请参阅有关连接设备的部分。

注意:执行设备发现会消耗蓝牙适配器的大量资源。找到要连接的设备后,请确保先使用 cancelDiscovery() 停止发现,然后再尝试连接。此外,您不应在连接到设备时执行发现操作,因为发现过程会显著减少可供任何现有连接使用的带宽。

启用可检测性

如果您希望本地设备可被其他设备检测到,请使用 ACTION_REQUEST_DISCOVERABLE intent 调用 startActivityForResult(Intent, int)。此操作会发出启用系统的可检测到模式的请求,而无需前往“设置”应用,这会导致您自己的应用停止运行。默认情况下,设备在 120 秒(即 2 分钟)内可检测到。您可以通过添加 EXTRA_DISCOVERABLE_DURATION extra 来定义不同的时长,最高可达 3600 秒(1 小时)。

注意 :如果您将 EXTRA_DISCOVERABLE_DURATION extra 的值设置为 0,则设备始终可检测到。此配置不安全,因此强烈建议不要使用。

以下代码段将设备处于可检测到模式的时间设置为 5 分钟(300 秒):

Kotlin

val discoverableIntent: Intent = Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE).apply {
    putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300)
}
startActivity(discoverableIntent)

Java

Intent discoverableIntent =
        new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
startActivity(discoverableIntent);
图 2:启用可检测性对话框。

系统会显示一个对话框,请求用户授予权限以使设备可被检测到,如图 2 所示。如果用户响应“Yes”,则设备在指定的时间段内变为可检测到。然后,您的 activity 会收到对 onActivityResult() 回调的调用,其结果代码等于设备可检测到的时长。如果用户响应“No”或出现错误,结果代码为 RESULT_CANCELED

注意:如果设备尚未启用蓝牙,则将设备设为可检测性会自动启用蓝牙。

设备会在规定的时间内以静默方式保持可检测模式。 如果您希望在可检测到模式发生变化时收到通知,可以为 ACTION_SCAN_MODE_CHANGED intent 注册一个 BroadcastReceiver。此 intent 包含 extra 字段 EXTRA_SCAN_MODEEXTRA_PREVIOUS_SCAN_MODE,分别提供新旧扫描模式。每个 Extra 属性可能拥有以下值:

SCAN_MODE_CONNECTABLE_DISCOVERABLE
设备处于可检测到模式。
SCAN_MODE_CONNECTABLE
设备未处于可检测到模式,但仍然可以接收连接。
SCAN_MODE_NONE
设备未处于可检测到模式,因此无法接收连接。

如果您是要发起与远程设备的连接,则无需启用设备可检测性。只有当您希望应用托管用于接受传入连接的服务器套接字时,才有必要启用可检测性,因为远程设备必须能够发现其他设备,然后才能发起与这些设备的连接。

连接设备

为了在两台设备之间创建连接,您必须同时实现服务器端和客户端机制,因为其中一台设备必须开放服务器套接字,另一台设备必须使用服务器设备的 MAC 地址发起连接。服务器设备和客户端设备分别以不同的方式获取所需的 BluetoothSocket。接受传入连接时,服务器会收到套接字信息。客户端会在打开到服务器的 RFCOMM 通道时提供套接字信息。

当服务器和客户端在同一 RFCOMM 通道上分别拥有已连接的 BluetoothSocket 时,可将二者视为彼此连接。此时,每台设备都可以获取输入流和输出流,并且可以开始传输数据,相关内容将在有关管理连接的部分讨论。本部分介绍如何在两台设备之间发起连接。

连接技术

一种实现技术是自动将每个设备准备为服务器,以便每个设备都打开服务器套接字并监听连接。在这种情况下,任一设备都可以发起与另一台设备的连接,并成为客户端。或者,其中一台设备可以显式托管连接并按需打开服务器套接字,而另一台设备则发起连接。

图 3:蓝牙配对对话框。

注意:如果这两台设备之前未配对,则 Android 框架会在连接过程中自动向用户显示配对请求通知或对话框,如图 3 所示。 因此,当您的应用尝试连接设备时,无需担心设备是否已配对。在用户成功配对两台设备之前,您的 RFCOMM 连接尝试会被阻止;如果用户拒绝配对,或者配对过程失败或超时,则尝试将失败。

作为服务器连接

当您想要连接两台设备时,其中一台设备必须保持开放的 BluetoothServerSocket,从而充当服务器。服务器套接字的用途是监听传入的连接请求,并在接受请求后提供已连接的 BluetoothSocket。从 BluetoothServerSocket 获取 BluetoothSocket 后,除非您希望设备接受更多连接,否则可以(并且应该)舍弃 BluetoothServerSocket

如需设置服务器套接字并接受连接,请完成以下步骤:

  1. 通过调用 listenUsingRfcommWithServiceRecord() 获取 BluetoothServerSocket

    该字符串是您的服务的可识别名称,系统会自动将其写入设备上的新服务发现协议 (SDP) 数据库条目。该名称可以任意设置,也可以直接使用您的应用名称。通用唯一标识符 (UUID) 也包含在 SDP 条目中,并且构成了与客户端设备连接协议的基础。也就是说,当客户端尝试与此设备连接时,它会携带 UUID,该 UUID 可用于唯一标识要连接的服务。这两个 UUID 必须匹配,这样连接才会被接受。

    UUID 是一种标准化的 128 位格式,用于对信息进行唯一标识的字符串 ID。UUID 的特点是它足够大,因此您可以选择任何随机 ID,并且不会与任何其他 ID 发生冲突。在这种情况下,它用于唯一标识应用的蓝牙服务。如需获取要在应用中使用的 UUID,您可以使用网上提供的众多随机 UUID 生成器之一,然后使用 fromString(String) 初始化 UUID

  2. 通过调用 accept() 开始监听连接请求。

    这是一个阻塞调用。当连接被接受或发生异常时,它会返回。仅当远程设备发送的连接请求中包含的 UUID 与向此监听服务器套接字注册的 UUID 相匹配时,连接才会被接受。成功后,accept() 会返回已连接的 BluetoothSocket

  3. 除非您想要接受其他连接,否则请调用 close()

    此方法调用会释放服务器套接字及其所有资源,但不会关闭 accept() 返回的已连接 BluetoothSocket。与 TCP/IP 不同,RFCOMM 一次只允许每个通道有一个已连接的客户端,因此在大多数情况下,一种合理的做法是,在接受已连接的套接字后立即在 BluetoothServerSocket 上调用 close()

由于 accept() 调用属于阻塞调用,因此不应在主 activity 界面线程中执行该调用,否则您的应用仍然可以响应其他用户互动。通常而言,应该在应用管理的新线程中执行涉及 BluetoothServerSocketBluetoothSocket 的所有工作。如需取消 accept() 等被阻塞的调用,请从另一个线程对 BluetoothServerSocketBluetoothSocket 调用 close()。请注意,BluetoothServerSocketBluetoothSocket 上的所有方法都是线程安全的。

示例

以下是用于接受传入连接的服务器组件的简化线程:

Kotlin

private inner class AcceptThread : Thread() {
    
    private val mmServerSocket: BluetoothServerSocket? by lazy(LazyThreadSafetyMode.NONE) {
        bluetoothAdapter?.listenUsingInsecureRfcommWithServiceRecord(NAME, MY_UUID)
    }

    override fun run() {
        // Keep listening until exception occurs or a socket is returned.
        var shouldLoop = true
        while (shouldLoop) {
            val socket: BluetoothSocket? = try {
                mmServerSocket?.accept()
            } catch (e: IOException) {
                Log.e(TAG, "Socket's accept() method failed", e)
                shouldLoop = false
                null
            }
            socket?.also {
                manageMyConnectedSocket(it)
                mmServerSocket?.close()
                shouldLoop = false
            }
        }
    }

    // Closes the connect socket and causes the thread to finish.
    fun cancel() {
        try {
            mmServerSocket?.close()
        } catch (e: IOException) {
            Log.e(TAG, "Could not close the connect socket", e)
        }
    }
}

Java

private class AcceptThread extends Thread {
    private final BluetoothServerSocket mmServerSocket;

    public AcceptThread() {
        // Use a temporary object that is later assigned to mmServerSocket
        // because mmServerSocket is final.
        BluetoothServerSocket tmp = null;
        try {
            // MY_UUID is the app's UUID string, also used by the client code.
            tmp = bluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
        } catch (IOException e) {
            Log.e(TAG, "Socket's listen() method failed", e);
        }
        mmServerSocket = tmp;
    }

    public void run() {
        BluetoothSocket socket = null;
        // Keep listening until exception occurs or a socket is returned.
        while (true) {
            try {
                socket = mmServerSocket.accept();
            } catch (IOException e) {
                Log.e(TAG, "Socket's accept() method failed", e);
                break;
            }

            if (socket != null) {
                // A connection was accepted. Perform work associated with
                // the connection in a separate thread.
                manageMyConnectedSocket(socket);
                mmServerSocket.close();
                break;
            }
        }
    }

    // Closes the connect socket and causes the thread to finish.
    public void cancel() {
        try {
            mmServerSocket.close();
        } catch (IOException e) {
            Log.e(TAG, "Could not close the connect socket", e);
        }
    }
}

在此示例中,只需要一个传入连接,因此在接受连接并获取 BluetoothSocket 后,应用会立即将获取的 BluetoothSocket 传递给单独的线程,关闭 BluetoothServerSocket 并终止循环。

请注意,当 accept() 返回 BluetoothSocket 时,表示套接字已连接。因此,您不应像在客户端那样调用 connect()

应用专属的 manageMyConnectedSocket() 方法旨在启动用于传输数据的线程,详见管理连接部分。

通常,在完成传入连接的监听后,应立即关闭 BluetoothServerSocket。在此示例中,获取 BluetoothSocket 后会立即调用 close()。您可能还需要在线程中提供一个公共方法,以便在您需要停止监听该服务器套接字时关闭专用 BluetoothSocket

作为客户端连接

如需发起与在开放服务器套接字上接受连接的远程设备建立连接,您必须先获取一个代表该远程设备的 BluetoothDevice 对象。如需了解如何创建 BluetoothDevice,请参阅查找设备。然后,您必须使用 BluetoothDevice 获取 BluetoothSocket 并启动连接。

基本步骤如下所示:

  1. 使用 BluetoothDevice,通过调用 createRfcommSocketToServiceRecord(UUID) 获取 BluetoothSocket

    此方法会初始化一个允许客户端连接到 BluetoothDeviceBluetoothSocket 对象。此处传递的 UUID 必须与服务器设备在调用 listenUsingRfcommWithServiceRecord(String, UUID) 以打开其 BluetoothServerSocket 时使用的 UUID 一致。如需使用匹配的 UUID,请将 UUID 字符串硬编码到您的应用中,然后通过服务器代码和客户端代码引用该字符串。

  2. 通过调用 connect() 发起连接。请注意,此方法属于阻塞调用。

    在客户端调用此方法后,系统会执行 SDP 查找,以查找具有匹配 UUID 的远程设备。如果查找成功并且远程设备接受连接,则远程设备会共享要在连接期间使用的 RFCOMM 通道,并且 connect() 方法会返回结果。如果连接失败,或者 connect() 方法超时(大约 12 秒后),该方法会抛出 IOException

    由于 connect() 是阻塞调用,因此您应始终在独立于主 activity(界面)线程的线程中执行此连接过程。

    注意 :在调用 connect() 之前,您应始终调用 cancelDiscovery() 以确保设备未执行设备发现。如果正在进行发现,则会显著减慢连接尝试的速度,并且更有可能失败。

示例

以下是发起蓝牙连接的客户端线程的基本示例:

Kotlin

private inner class ConnectThread(device: BluetoothDevice) : Thread() {

    private val mmSocket: BluetoothSocket? by lazy(LazyThreadSafetyMode.NONE) {
        device.createRfcommSocketToServiceRecord(MY_UUID)
    }

    public override fun run() {
        // Cancel discovery because it otherwise slows down the connection.
        bluetoothAdapter?.cancelDiscovery()

        mmSocket?.use { socket ->
            // Connect to the remote device through the socket. This call blocks
            // until it succeeds or throws an exception.
            socket.connect()

            // The connection attempt succeeded. Perform work associated with
            // the connection in a separate thread.
            manageMyConnectedSocket(socket)
        }
    }

    // Closes the client socket and causes the thread to finish.
    fun cancel() {
        try {
            mmSocket?.close()
        } catch (e: IOException) {
            Log.e(TAG, "Could not close the client socket", e)
        }
    }
}

Java

private class ConnectThread extends Thread {
    private final BluetoothSocket mmSocket;
    private final BluetoothDevice mmDevice;

    public ConnectThread(BluetoothDevice device) {
        // Use a temporary object that is later assigned to mmSocket
        // because mmSocket is final.
        BluetoothSocket tmp = null;
        mmDevice = device;

        try {
            // Get a BluetoothSocket to connect with the given BluetoothDevice.
            // MY_UUID is the app's UUID string, also used in the server code.
            tmp = device.createRfcommSocketToServiceRecord(MY_UUID);
        } catch (IOException e) {
            Log.e(TAG, "Socket's create() method failed", e);
        }
        mmSocket = tmp;
    }

    public void run() {
        // Cancel discovery because it otherwise slows down the connection.
        bluetoothAdapter.cancelDiscovery();

        try {
            // Connect to the remote device through the socket. This call blocks
            // until it succeeds or throws an exception.
            mmSocket.connect();
        } catch (IOException connectException) {
            // Unable to connect; close the socket and return.
            try {
                mmSocket.close();
            } catch (IOException closeException) {
                Log.e(TAG, "Could not close the client socket", closeException);
            }
            return;
        }

        // The connection attempt succeeded. Perform work associated with
        // the connection in a separate thread.
        manageMyConnectedSocket(mmSocket);
    }

    // Closes the client socket and causes the thread to finish.
    public void cancel() {
        try {
            mmSocket.close();
        } catch (IOException e) {
            Log.e(TAG, "Could not close the client socket", e);
        }
    }
}

请注意,在此代码段中,系统会在尝试连接之前调用 cancelDiscovery()。您始终应在 connect() 之前调用 cancelDiscovery(),尤其是因为无论设备发现当前是否正在进行中,cancelDiscovery() 都会成功。不过,如果您的应用需要确定设备是否正在执行中,您可以使用 isDiscovering() 进行检查。

应用专属的 manageMyConnectedSocket() 方法旨在启动用于传输数据的线程(详见管理连接部分)。

使用完 BluetoothSocket 后,请始终调用 close()。这样做会立即关闭已连接的套接字并释放所有相关的内部资源。

管理连接

成功连接多台设备后,每台设备都有一个已连接的 BluetoothSocket。这一点非常有趣,因为您可以在设备之间共享信息。使用 BluetoothSocket 传输数据的一般过程如下:

  1. 获取 InputStreamOutputStream,分别使用 getInputStream()getOutputStream() 通过套接字处理传输。
  2. 使用 read(byte[])write(byte[]) 读取和写入数据流。

当然,您还需要考虑实现细节。具体而言,您应使用专用线程从数据流读取数据以及向数据流写入数据。这一点非常重要,因为 read(byte[])write(byte[]) 方法都会阻止调用。read(byte[]) 方法会阻塞,直到从数据流中读取数据为止。write(byte[]) 方法通常不会阻塞,但如果远程设备调用 read(byte[]) 的速度不够快,导致中间缓冲区已满,则该方法可能会阻塞以执行流控制。因此,线程中的主循环应专门用于从 InputStream 读取数据。可以使用线程中单独的公共方法发起对 OutputStream 的写入操作。

示例

以下示例展示了如何在通过蓝牙连接的两台设备之间传输数据:

Kotlin

private const val TAG = "MY_APP_DEBUG_TAG"

// Defines several constants used when transmitting messages between the
// service and the UI.
const val MESSAGE_READ: Int = 0
const val MESSAGE_WRITE: Int = 1
const val MESSAGE_TOAST: Int = 2
// ... (Add other message types here as needed.)

class MyBluetoothService(
        // handler that gets info from Bluetooth service
        private val handler: Handler) {

    private inner class ConnectedThread(private val mmSocket: BluetoothSocket) : Thread() {

        private val mmInStream: InputStream = mmSocket.inputStream
        private val mmOutStream: OutputStream = mmSocket.outputStream
        private val mmBuffer: ByteArray = ByteArray(1024) // mmBuffer store for the stream

        override fun run() {
            var numBytes: Int // bytes returned from read()

            // Keep listening to the InputStream until an exception occurs.
            while (true) {
                // Read from the InputStream.
                numBytes = try {
                    mmInStream.read(mmBuffer)
                } catch (e: IOException) {
                    Log.d(TAG, "Input stream was disconnected", e)
                    break
                }

                // Send the obtained bytes to the UI activity.
                val readMsg = handler.obtainMessage(
                        MESSAGE_READ, numBytes, -1,
                        mmBuffer)
                readMsg.sendToTarget()
            }
        }

        // Call this from the main activity to send data to the remote device.
        fun write(bytes: ByteArray) {
            try {
                mmOutStream.write(bytes)
            } catch (e: IOException) {
                Log.e(TAG, "Error occurred when sending data", e)

                // Send a failure message back to the activity.
                val writeErrorMsg = handler.obtainMessage(MESSAGE_TOAST)
                val bundle = Bundle().apply {
                    putString("toast", "Couldn't send data to the other device")
                }
                writeErrorMsg.data = bundle
                handler.sendMessage(writeErrorMsg)
                return
            }

            // Share the sent message with the UI activity.
            val writtenMsg = handler.obtainMessage(
                    MESSAGE_WRITE, -1, -1, bytes)
            writtenMsg.sendToTarget()
        }

        // Call this method from the main activity to shut down the connection.
        fun cancel() {
            try {
                mmSocket.close()
            } catch (e: IOException) {
                Log.e(TAG, "Could not close the connect socket", e)
            }
        }
    }
}

Java

public class MyBluetoothService {
    private static final String TAG = "MY_APP_DEBUG_TAG";
    private Handler handler; // handler that gets info from Bluetooth service

    // Defines several constants used when transmitting messages between the
    // service and the UI.
    private interface MessageConstants {
        public static final int MESSAGE_READ = 0;
        public static final int MESSAGE_WRITE = 1;
        public static final int MESSAGE_TOAST = 2;

        // ... (Add other message types here as needed.)
    }

    private class ConnectedThread extends Thread {
        private final BluetoothSocket mmSocket;
        private final InputStream mmInStream;
        private final OutputStream mmOutStream;
        private byte[] mmBuffer; // mmBuffer store for the stream

        public ConnectedThread(BluetoothSocket socket) {
            mmSocket = socket;
            InputStream tmpIn = null;
            OutputStream tmpOut = null;

            // Get the input and output streams; using temp objects because
            // member streams are final.
            try {
                tmpIn = socket.getInputStream();
            } catch (IOException e) {
                Log.e(TAG, "Error occurred when creating input stream", e);
            }
            try {
                tmpOut = socket.getOutputStream();
            } catch (IOException e) {
                Log.e(TAG, "Error occurred when creating output stream", e);
            }

            mmInStream = tmpIn;
            mmOutStream = tmpOut;
        }

        public void run() {
            mmBuffer = new byte[1024];
            int numBytes; // bytes returned from read()

            // Keep listening to the InputStream until an exception occurs.
            while (true) {
                try {
                    // Read from the InputStream.
                    numBytes = mmInStream.read(mmBuffer);
                    // Send the obtained bytes to the UI activity.
                    Message readMsg = handler.obtainMessage(
                            MessageConstants.MESSAGE_READ, numBytes, -1,
                            mmBuffer);
                    readMsg.sendToTarget();
                } catch (IOException e) {
                    Log.d(TAG, "Input stream was disconnected", e);
                    break;
                }
            }
        }

        // Call this from the main activity to send data to the remote device.
        public void write(byte[] bytes) {
            try {
                mmOutStream.write(bytes);

                // Share the sent message with the UI activity.
                Message writtenMsg = handler.obtainMessage(
                        MessageConstants.MESSAGE_WRITE, -1, -1, bytes);
                writtenMsg.sendToTarget();
            } catch (IOException e) {
                Log.e(TAG, "Error occurred when sending data", e);

                // Send a failure message back to the activity.
                Message writeErrorMsg =
                        handler.obtainMessage(MessageConstants.MESSAGE_TOAST);
                Bundle bundle = new Bundle();
                bundle.putString("toast",
                        "Couldn't send data to the other device");
                writeErrorMsg.setData(bundle);
                handler.sendMessage(writeErrorMsg);
            }
        }

        // Call this method from the main activity to shut down the connection.
        public void cancel() {
            try {
                mmSocket.close();
            } catch (IOException e) {
                Log.e(TAG, "Could not close the connect socket", e);
            }
        }
    }
}

在构造函数获取必要的数据流之后,线程会等待通过 InputStream 传入数据。当 read(byte[]) 随数据流中的数据返回时,系统会使用父类中的成员 Handler 将数据发送到主 activity。然后,线程会等待从 InputStream 中读取更多字节。

发送传出数据非常简单,只需从主 activity 调用线程的 write() 方法并传入要发送的字节即可。此方法会调用 write(byte[]) 将数据发送到远程设备。如果在调用 write(byte[]) 时抛出 IOException,线程会向主 activity 发送消息框,向用户说明设备无法将给定的字节发送到另一台(已连接的)设备。

线程的 cancel() 方法允许通过关闭 BluetoothSocket 随时终止连接。使用完蓝牙连接后,应始终调用此方法。

如需查看使用 Bluetooth API 的演示,请参阅蓝牙聊天示例应用

关键类和接口

android.bluetooth 软件包中提供了所有蓝牙 API。下面总结了创建蓝牙连接所需的类和接口:

BluetoothAdapter
表示本地蓝牙适配器(蓝牙无线装置)。BluetoothAdapter 是所有蓝牙交互的入口点。利用这种方法,您可以发现其他蓝牙设备,查询绑定(配对)设备的列表,使用已知的 MAC 地址实例化 BluetoothDevice,以及创建 BluetoothServerSocket 以监听来自其他设备的通信。
BluetoothDevice
表示远程蓝牙设备。使用此方法可以通过 BluetoothSocket 请求与远程设备建立连接,或查询有关设备的信息,例如设备的名称、地址、类和绑定状态。
BluetoothSocket
表示蓝牙套接字的接口(类似于 TCP Socket)。这是允许应用使用 InputStreamOutputStream 与其他蓝牙设备交换数据的连接点。
BluetoothServerSocket
表示用于监听传入请求的开放服务器套接字(类似于 TCP ServerSocket)。为了连接两台 Android 设备,一台设备必须使用此类打开服务器套接字。当远程蓝牙设备向此设备发出连接请求时,设备会接受连接,然后返回已连接的 BluetoothSocket
BluetoothClass
描述蓝牙设备的一般特性和功能。这是一组只读属性,用于定义设备的类和服务。尽管这些信息提供了有关设备类型的有用提示,但该类的属性不一定要描述设备支持的所有蓝牙配置文件和服务。
BluetoothProfile
表示蓝牙配置文件的接口。蓝牙配置文件是适用于设备间蓝牙通信的无线接口规范。免触摸配置文件就是一个示例。如需详细了解配置文件,请参阅使用配置文件
BluetoothHeadset
支持蓝牙耳机,以便与手机配合使用。这包括蓝牙耳机配置文件和免提 (v1.5) 配置文件。
BluetoothA2dp
定义如何使用高级音频分发配置文件 (A2DP) 通过蓝牙连接将高品质音频从一台设备流式传输到另一台设备。
BluetoothHealth
表示用于控制蓝牙服务的健康设备配置文件代理。
BluetoothHealthCallback
用于实现 BluetoothHealth 回调的抽象类。您必须扩展此类并实现回调方法,以接收关于应用注册状态和蓝牙通道状态变化的更新。
BluetoothHealthAppConfiguration
表示蓝牙运行状况第三方应用注册的应用配置,该配置用于与远程蓝牙健康设备通信。
BluetoothProfile.ServiceListener
一个接口,用于在 BluetoothProfile 进程间通信 (IPC) 客户端连接到运行特定配置文件的内部服务或断开其与服务的连接时通知这些客户端。