译:Android蓝牙开发

蓝牙

注:本文翻译自https://developer.android.com/guide/topics/connectivity/bluetooth.html

Android平台提供了对蓝牙网络栈的支持,蓝牙网络栈可以让一台设备与另一台蓝牙设备之间实现无线数据交换。Android平台通过Android蓝牙API提供对蓝牙的访问能力。这些API能让这些API与其他蓝牙设备无线连接,具有通过点对点和多点无线特性。
通过蓝牙API,Android应用可是做下面的操作:

  • 扫描其他的蓝牙设备
  • 为已经匹配的蓝牙设配查询本地蓝牙适配器。
  • 发布RFCOMM通道
  • 通过服务查询链接到其他设备
  • 传输数据给其他设备
  • 管理多个链接

本文描述如何使用Classic Bluetooth,Classic Bluetooth是针对较多对较多耗电敏感的操作例如流媒体和通信正确的选择。对只需低耗电的设备,Android 4.3 (API 18版)引入用于支持蓝牙低功耗的API。参见 Bluetooth Low Energy了解更多内容。

基础

本文档描述了如何使用Android蓝牙APIs来完成使用蓝牙进行通信所需要的四个主要任务:设置蓝牙,检索周围匹配的或者可用的设备,连接设备以及设备间传输数据。所有蓝牙APIs在android.bluetooth 包中。

下面是对创建蓝牙连接所要用到的类和接口的一个总结。

  • BluetoothAdapter
    表示本地蓝牙适配器(蓝牙无线电广播)。BluetoothAdapter是所有蓝牙交互的入口点。你能够通过它发现其它蓝牙设备,查询一系列已经匹配的设备,使用已知的MAC地址实例化一个 BluetoothDevice,创建 BluetoothServerSocket 监听来自其他设备的通信。

  • BluetoothDevice
    表示远程蓝牙设备。用这个类通过 BluetoothSocket能够请求同远程设备的链接,或者查询设备的名字、地址、类和绑定状态。

  • BluetoothSocket
    表示蓝牙套接字通信(类似于TCP Socket)的接口。这是允许应用通过InputStream和OutputStream与其他蓝牙设备进行数据交换的连接点。

  • BluetoothServerSocket
    表示用于监听即将到来的请求的对外公开套接字(类似于TCP ServerSocket)。为了连接两个安卓设备,一个设备必须用这个类开放一个服务端的套接字。当远程的蓝牙设备向该设备发起连接请求时,在连接建立的时候BluetoothServerSocket会返回一个BluetoothSocket

  • BluetoothClass
    描述蓝牙设备的普遍的特征和功能。这是一个只读的属性集合,它定义了设备的主要和次要的设备类及服务。然而,它并没有可靠地描述所有的蓝牙配置及设备所支持的服务,但是作为设备类型的提示是很有用的。

  • BluetoothProfile
    表示蓝牙配置文件的接口。一个蓝牙配置文件是一个无线接口规格说明书,用于基于蓝牙设备间的通信。一个示例就是免提装置配置文件,想了解更多有关配置文件的内容,参见Working with Profiles

  • BluetoothHeadset
    它为同移动手机一起使用的蓝牙耳机提供支持。这包括蓝牙耳机和免提装置配置文件。

  • BluetoothA2dp
    定义了通过蓝牙连接能够从一个设备传输多么高质量的音频到另外一个设备上。"A2DP"表示高级音频发布配置文件(Advanced Audio Distribution Profile)。

  • BluetoothHealth
    表示能够控制蓝牙服务的医疗设备配置文件代理。

  • BluetoothHealthCallback
    用于实现BluetoothHealth 回调的抽象类。你必须继承这个类并且实现其中的回调方法来接收与应用注册状态和蓝牙通道状态有关的的变化信息。

  • BluetoothHealthAppConfiguration
    表示蓝牙医疗第三方应用所注册的用来与远程蓝牙医疗设备通信的配置信息。

  • BluetoothProfile.ServiceListener
    用于通知 BluetoothProfile跨进程通信的客户端已经连接或者从服务端断开链接的接口(也就是说,内部服务运行了一个配置文件)。

蓝牙权限

为了在你的应用中使用蓝牙特性,你必须声明蓝牙权限 BLUETOOTH。你需要这个权限来执行任何蓝牙通信,例如请求连接,接受连接以及传输数据。

如果你希望你的应用初始化设备感应或者操作蓝牙设置,你也必须声明 BLUETOOTH_ADMIN 权限。大多数应用为了具有感应周围蓝牙设备的能力,需要单独申明这个权限。除非这个应用是一个能够根据用户请求修改蓝牙设置的超级管理员,该权限所赋予的其他功能不应被当使用。注意:如果使用 BLUETOOTH_ADMIN 权限,你仍须申明 BLUETOOTH权限。

在你的manifest文件中声明蓝牙权限。例如:

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

参见 <uses-permission>了解更多有关申明配置权限的信息。

设置蓝牙

在应用能够通过蓝牙通信之前,你需要校验设备是否支持蓝牙,如果支持,请确保蓝牙是出于开启状态。如果不支持蓝牙,你应当优雅地禁止掉任何蓝牙功能。如果支持蓝牙但是没有打开,你可以在不离开应用的情况下请求应用开启蓝牙,这个设置通过使用BluetoothAdapter在两步内完成。

  1. 获取 BluetoothAdapter
    所有的蓝牙Activity都需要BluetoothAdapter 。为了得到BluetoothAdapter,调用静态方法getDefaultAdapter() 。这个方法会返回一个BluetoothAdapter,他表示设备自身的蓝牙适配器(蓝牙无线电广播)。整个系统只存在一个蓝牙适配器,你的应用通过它来交互。如果 getDefaultAdapter()返回为null则表示设备不支持蓝牙,只能到此为止。例如:
BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (mBluetoothAdapter == null) {
   // Device does not support Bluetooth
}
  1. 打开蓝牙

**图1:** 开启蓝牙对话框
下一步,你需要确保蓝牙已经启用。调用 isEnabled() 去检查蓝牙是否已经启用。如果方法返回为false,则蓝牙未被启用。要请求启用蓝牙,调用 [startActivityForResult()](https://developer.android.com/reference/android/app/Activity.html#startActivityForResult(android.content.Intent, int))并且携带action intent参数 ACTION_REQUEST_ENABLE 。这会发起一个请求,通过系统设置来开启蓝牙(同时不停止你的应用)。例如:

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

会显示一个对话框请求用户权限来启用蓝牙,如图1所示。如果用户点击“yes”,则系统会开始启用蓝牙,并且一旦你的应用处理完毕(或者失败),界面焦点将会回到你的应用。

传输给 [startActivityForResult()](https://developer.android.com/reference/android/app/Activity.html#startActivityForResult(android.content.Intent, int))的常量REQUEST_ENABLE_BT是一个本地定义的整型(必须大于零),在[onActivityResult()](https://developer.android.com/reference/android/app/Activity.html#onActivityResult(int, int, android.content.Intent))实现中,系统将这个整型作为requestCode参数返回给你。

如果蓝牙启动成功,在 [onActivityResult()](https://developer.android.com/reference/android/app/Activity.html#onActivityResult(int, int, android.content.Intent)) 回调中,你的Activity接收到RESULT_OK结果码。如果蓝牙由于某一个错误没有启动,则返回码为 RESULT_CANCELED

此外,你的应用同样能也够监听Intent广播 ACTION_STATE_CHANGED,每当蓝牙状态发生改变,系统就会发送此广播。这个广播包含额外的参数 EXTRA_STATEEXTRA_PREVIOUS_STATE, 分别包含新的和旧的蓝牙状态。这些额外的参数可能的值有 STATE_TURNING_ONSTATE_ONSTATE_TURNING_OFF,以及 STATE_OFF。在你的应用运行的时候监测蓝牙状态改变,这时侦听广播变得重要。

小贴士:开启蓝牙可见性会自动开启蓝牙。如果打算在执行蓝牙Activity之前开启设备的可见性,你可以跳过上面的两部,参考下方内容开启可见性。

查找设备

使用 BluetoothAdapter,你能够找到远程蓝牙设备,不论是通过设备查找还是通过查询已匹配设备列表。

设备感应是一个扫描的过程,会查询周围已经开启蓝牙的设备,然后请求每一个设备的相关信息(有时候会将这个过程称之为感应、检索或者扫描),如果一台设备可以被感应到,它会通过共享一些诸如设备名称,类型以及它的唯一MAC地址来响应感应请求。利用这些信息,执行查找的设备能够选择去初始化连接到一个已经感应到的设备。

一旦与远程的设备第一次建立起了连接,匹配请求会自动地呈现给用户,当一台设备匹配成功,设备相关的基本信息会被保存起来并且可以利用蓝牙API来读取。使用已有的远程设备MAC地址可以在仍和时候建立连接而无需执行感应(假设设备实在感应范围内)

记住正在配对和正在连接之间是存在差别的。进行匹配意味着两个设备都知道各自的存在,拥有一个能够用来授权的共享连接密钥,并且能够互相建立一个加密的连接。进行连接意味着当前设备间共享一个RFCOMM信道,并且能够互相传递数据。
目前Android蓝牙APIs在一个RFCOMM连接建立之前要求设备已经配对。(当你使用蓝牙APIs初始化一个加密连接的时候,匹配是自动被执行的。)

下面的章节描述如何找到已经匹配的设备,或者使用设备感应感应新的设备。

Note:Android平台的设备默认是不能够被感应到的。用户可以通过系统设置让设备在一个有限的时间内能够被感应到。或者在不离开当前应用的情况下,能够请求用户使得设备能够可以被感应,下面的内容讲如何让设备被感应到。

查询已匹配的设备

在执行设备感应之前,查询已匹配设备集合,来确认期望设备可见是有价值的。要做到这一点,调用getBondedDevices()。这会返回代表已匹配设备BluetoothDevice
的一个集合。例如,你能够查询所有已匹配设备,然后使用ArrayAdapter,将每个设备的名称显示给用户。

为了初始化一个连接,只需要 BluetoothDevice 对象中的MAC地址参数。在这个例子中,它被保存为一个ArrayAdapter的一部分用于显示给用户。MAC地址可以在稍后为了初始化连接的时候再提取出来。你可以在 Connecting Devices章节中了解更多有关创建一个连接的内容。

感应设备

只需简单地调用startDiscovery()即可开始感应设备。这个处理过程是异步的,并且方法会立即返回一个boolean值,用于指明感应操作是否已经成功启动。感应过程通常包含一个大约十二秒的查询扫描操作,紧接着的页面扫描每个已找到的设备取出它的蓝牙名称。

为了接收关于每个感应到的设备的信息,你的应用必须为 ACTION_FOUND intent注册一个BroadcastReceiver。对每个设备,系统将会广播 ACTION_FOUND intent。这个intent携带额外的参数 EXTRA_DEVICEEXTRA_CLASS,分别包含一个相应的BluetoothDevice 和一个 BluetoothClass,例如,下面演示了当设备被发现的时候,你可以如何注册对广播的处理。

// Create a BroadcastReceiver for ACTION_FOUND
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        // When discovery finds a device
        if (BluetoothDevice.ACTION_FOUND.equals(action)) {
            // Get the BluetoothDevice object from the Intent
            BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
            // Add the name and address to an array adapter to show in a ListView
            mArrayAdapter.add(device.getName() + "\n" + device.getAddress());
        }
    }
};
// Register the BroadcastReceiver
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
registerReceiver(mReceiver, filter); // Don't forget to unregister during onDestroy

为了初始化一个连接,只需要来自 BluetoothDevice 的是MAC地址。在上面的例子中,将其保存为ArrayAdapter 的一部分展示给用户。MAC地址可以在稍后为了初始化连接的时候再提取出来。你可以在Connecting Devices章节中了解更多有关创建一个连接的内容

注意: 对于蓝牙适配器,执行设备查询是一个重度操作,将会消耗很多它的资源。一旦你发现了一个要去连接的设备,在你试图开始连接之前,确认你总是使用cancelDiscovery()来停止查找。同样,如果你已经和一台设备保持着一个连接,则执行查找会很大程度上削减这个连接的可用带宽,因此当你已经连接时,你不应当执行查找操作。

开启可见性

如果你希望本地设备对其他设备是可见的,调用 [startActivityForResult(Intent, int)](https://developer.android.com/reference/android/app/Activity.html#startActivityForResult(android.content.Intent, int)),并且使用ACTION_REQUEST_DISCOVERABLE action Intent。这会通过系统设置发出一个开启可见性模式的请求(同时不会停止你的应用)。默认的,这个设备会在120秒内成为可被查找的。通过添加Intent EXTRA_DISCOVERABLE_DURATION extra数据,你能够定义一个不同的持续时间。一个app能够设定的最大持续时间为3600秒,并且0意味着这个设备总是可以被查找到的。任何小于0并且大于3600的值都将自动设置为120秒。例如,如下程序片段设置持续时间为300:

Intent discoverableIntent = new
Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
startActivity(discoverableIntent);

**图 2:** 打开蓝牙可见性对话框

会显示一个对话框,请求开启设备可见性的用户权限,如图2所示。如果用户响应“Yes”,则设备在给定的时间内将会成为可被查找的。然后你的activity将会收到对 [onActivityResult())](https://developer.android.com/reference/android/app/Activity.html#onActivityResult(int, int, android.content.Intent))回调方法的调用。回调方法返回的result code和设备可见性的持续时间是相等的。如果用户响应“No.”或者如果发生了一个错误,则这个result code将会是 RESULT_CANCELED

注意:如果在此设备上没有打开蓝牙,则启用设备可见性将会自动启用蓝牙。

设备在分配的时间后台里将会保持可见模式。当可被查找模式发生改变的时候,如果你想得到提醒,你可以为 ACTION_SCAN_MODE_CHANGED intent注册一个BroadcastReceiver。这会包含额外的参数 EXTRA_SCAN_MODE以及 EXTRA_PREVIOUS_SCAN_MODE,它们分别告诉你新的和旧的扫描模式。它们各自可能的值有 SCAN_MODE_CONNECTABLE_DISCOVERABLE, SCAN_MODE_CONNECTABLE,或者 SCAN_MODE_NONE,这表明这个设备在可见模式,或者不在可见模式但是依旧能够收到连接,或者不在可见模式并且不能够收到连接。如果你将初始化到一个远程设备的连接,你不需要启用设备的可见性。只有当你希望你的应用保持一个server socket的时候,你才需要启用可见性,这个server socket用于接收新来到的连接,因为远程设备在能够初始化连接之前,它必须能够感应到这个设备。

连接设备

为了能够在两台设备的应用之间创建一个连接,你必须实现客户端以及服务端的机制,因为一台设备必须打开一个server socket,同时另外一个必须初始化连接(使用服务端设备的MAC地址来初始化一个连接)。当两台设备在相同的RFCOMM信道中各有一个已连接的 BluetoothSocket 的时候,服务端和客户端被认为是互相连接的。此时每台设备能够获得输入和输出流并且开始数据传输,本节将介绍如何在两台设备之间初始化连接。

服务端和客户端各自以不同的方式获得所需的BluetoothSocket 。当一个来到的连接被接受的时候,服务端将会收到BluetoothSocket。当客户端打开一个连接服务端的RFCOMM信道的时候,它会收到它的BluetoothSocket。

一个技术实现就是自动将每个设备准备好作为一个服务端,因此每个设备都将打开一个server socket并且侦听连接。然后每个设备都能够初始化一个到另外一个设备的连接并且成为客户端。另外一方面,一个设备能够显示地保持住这个连接并且根据需要打开一个server socket,另外一个设备能够简单地初始化这个连接。

**图 3:** 蓝牙配对对话框

注意:如果两台设备之前没有配对过,则在连接过程中,Android框架层会自动给用户显示一个配对请求提示或者对话框,如图3所示。因此当试图连接设备的时候,你的应用不需要考虑,你的设备是否已经配对过。在用户成功配对之前,你的RFCOMM连接请求会一直阻塞,如果用户拒绝配对的话则RFCOMM连接请求会失败,这也有可能是配对失败或者请求超时。

作为服务端来连接

当你希望连接两台设备的时候,其中一台必须通过保持一个打开的BluetoothServerSocket来作为服务器。Server socket的目的是在接受用于侦听到来的连接请求,同时当一台接受请求的时候,提供一个已连接的 BluetoothServerSocket。当从 BluetoothServerSocket获得 BluetoothSocket 的时候,BluetoothServerSocket应当撤销,除非你希望接受更多的连接。

关于UUID
通用唯一标识符(UUID)是为字符串ID而生成的标准化128bit格式,用于唯一标示信息。UUID的关键点是它足够大,以至于你任意随即选择都不会产生冲突。在此情况中,它被用于标识你应用的蓝牙服务。为了获得一个你的应用可以使用的UUID,你可以从众多的UUID生成器中任意选择一个,然后通过fromString(String)实例化一个UUID

下面是设置一个server socket以及获得一个连接的基本处理流程:

  1. 调用 [listenUsingRfcommWithServiceRecord(String, UUID)](https://developer.android.com/reference/android/bluetooth/BluetoothAdapter.html#listenUsingRfcommWithServiceRecord(java.lang.String, java.util.UUID))得到一个 BluetoothServerSocket
    生成的字符串是你设备的一个标识,系统会自动将这个字符串写入到位于设备上的新的服务查找协议(SDP)数据库条目中(名字是任意的,可能就是你的应用名字)。UUID也包含在SDP条目中,并且是允许连接到客户端设备的基础。即是,当客户端试图去连接这个设备的时候,它会携带一个UUID,这个UUID能够唯一识别它希望连接的服务。为了连接能够被接受,这些UUID必须匹配(见下一步)。

  2. 通过调用accept()开始侦听连接请求
    这是一个阻塞调用。当有一个连接被接受或者发生一个异常的时候,调用才会返回。只有当远程设备发送一个连接请求,并且带有注册在监听服务socket中相匹配的UUID的时候,连接才会被接受。当连接成功的时候, accept()会返回一个已连接的 BluetoothSocket

  3. 调用 close(),除非你希望接收其他的连接。
    这个操作会释放掉server socket以及所有它的资源,但是并不关闭 accept()返回的已连接的BluetoothSocket。不像TCP/IP,RFCOMM在同一时刻在每个信道中,仅仅允许一个已连接的客户端,因此在大多数情况下,在接收一个已连接socket之后,立刻调用BluetoothServerSocketclose()是有意义的。

调用 accept() 不应当在主Activity UI线程中被执行,因为它是一个阻塞调用,会阻止任何其他同应用的交互。通常在你的应用中,所有 BluetoothServerSocket或者 BluetoothSocket的操作都应当在一个新的线程中完成。要想终止一个像 accept()这样的阻塞调用,调用来自另外一个线程中 BluetoothServerSocket(或者 BluetoothSocket)中的 close() 方法,此时阻塞调用会立即返回结果。注意所有在 BluetoothServerSocket或者 BluetoothSocket中的方法都是线程安全的。

示例

下面是用于接收来到连接请求的服务端组件做了简化的线程。

  
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 = mBluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
        } catch (IOException 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) {
                break;
            }
            // If a connection was accepted
            if (socket != null) {
                // Do work to manage the connection (in a separate thread)
                manageConnectedSocket(socket);
                mmServerSocket.close();
                break;
            }
        }
    }
 
    /** Will cancel the listening socket, and cause the thread to finish */
    public void cancel() {
        try {
            mmServerSocket.close();
        } catch (IOException e) { }
    }
}

在本例中,仅希望有一个到来的连接,因此只要接收到连接,并且获得 BluetoothSocket ,应用程序就会发送获得的 BluetoothSocket 到一个独立的线程中,关闭 BluetoothServerSocket 并且终止循环。

注意到当accept()返回 BluetoothSocket的时候,这个socket已经连接上了,因此你应当调用 connect()
manageConnectedSocket()是应用中虚构的一个方法,会初始化用于传输数据的线程,这会在管理一个连接章节中讨论。
只要你一完成监听到来连接操作,你就应当关闭 BluetoothServerSocket。在本例中,一旦获得了 BluetoothSocket就调用 close()方法。你也可以在你的线程中提供一个公共方法,它能够在你需要停止监听server socket的时候关闭私有属性 BluetoothSocket

作为客户端来连接

为了初始化一个到远程设备(保持者一个打开的server socket的设备)的连接,你必须首先获得一个代表远程设备的 BluetoothDevice 对象(获取 BluetoothDevice已经在上面的 查询设备章节中提到)。然后你必须使用BluetoothDevice去获取一个 BluetoothSocket 并且初始化这个连接。
这里是基本的操作流程:

  1. 使用 BluetoothDevice,通过调用 createRfcommSocketToServiceRecord(UUID)到一个 BluetoothSocket.
    这将初始化一个连接到 BluetoothDeviceBluetoothSocket。当服务端打开它的 BluetoothServerSocket(使用 [listenUsingRfcommWithServiceRecord(String, UUID)](https://developer.android.com/reference/android/bluetooth/BluetoothAdapter.html#listenUsingRfcommWithServiceRecord(java.lang.String, java.util.UUID)),传递到这里的UUID必须和服务端设备所使用的UUID相匹配。使用同样的UUID就是简单地将UUID字符串硬编码进你的应用,并且在服务端和客户端代码中引用它。
  2. 调用 connect()初始化连接
    一旦调用这个接口,为了匹配UUID,系统会在远程设备上执行一个SDP查询操作。如果查询成功,并且远程设备接收这个连接,则在连接期间它会共享使用RFCOMM信道,并且connect()调用也会返回。这个方法是一个阻塞调用。如果因为任何原因,连接失败或者connect() 方法超时(大约超过12秒),则它会抛出一个异常。
    因为connect() 是一个阻塞调用,这个连接处理应当总是在主activity线程之外的一个独立线程中执行。

Note:你应当总是确保,当你调用connect()的时候,你的设备没有在执行设备查找操作。如果正在处理查找,则连接请求将会非常慢并且很有可能失败。

示例

下面是一个线程中初始化一个蓝牙连接的基本示例

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;
 
        // Get a BluetoothSocket to connect with the given BluetoothDevice
        try {
            // MY_UUID is the app's UUID string, also used by the server code
            tmp = device.createRfcommSocketToServiceRecord(MY_UUID);
        } catch (IOException e) { }
        mmSocket = tmp;
    }
 
    public void run() {
        // Cancel discovery because it will slow down the connection
        mBluetoothAdapter.cancelDiscovery();
 
        try {
            // Connect the device through the socket. This will block
            // until it succeeds or throws an exception
            mmSocket.connect();
        } catch (IOException connectException) {
            // Unable to connect; close the socket and get out
            try {
                mmSocket.close();
            } catch (IOException closeException) { }
            return;
        }
 
        // Do work to manage the connection (in a separate thread)
        manageConnectedSocket(mmSocket);
    }
 
    /** Will cancel an in-progress connection, and close the socket */
    public void cancel() {
        try {
            mmSocket.close();
        } catch (IOException e) { }
    }
}

注意cancelDiscovery()是在建立连接之前调用的。你应当在连接之前总是执行这个操作,并且不用检查是否在运行,调用cancelDiscovery()总是安全的(如果你确实想检查,调用 isDiscovering())。
manageConnectedSocket()在应用程序中是一个虚构的方法,用于初始化传输数据的线程,这将在管理连接中讨论。
当你用完 BluetoothSocket,一定要调用 close()来完成清理工作。这么做会立即关闭掉已连接的socket并且清理所有中间资源。

管理连接

当你成功连接两个(或者更多)设备的时候,每个都拥有一个已连接的 BluetoothSocket。这便开始变得比较有趣,因为你能够在设备之间共享数据。使用 BluetoothSocket,任意传输数据的通常处理是简单的:

  1. 通过socket,分别使用 getInputStream()getOutputStream()得到InputStreamOutputStream来处理传输。

  2. 使用 read(byte[])write(byte[])分别读数据以及写数据到流中。
    就酱!

当然有一些实现细节需要考虑。首先,你应当使用一个专门的线程来处理所有流的读写。这是非常重要的,因为所有 read(byte[])以及write(byte[]) 方法都是阻塞调用。 read(byte[])会一直阻塞,直到存在某些东西从流中取读,write(byte[])通常不会阻塞,但是如果远程设备没有足够快地调用 read(byte[]),就会阻塞流程控制,并且中间缓冲会溢出。因此,你线程中的主循环应当专门被用于从InputStream中读数据。在线程中的一个独立的公共方法,能够被用于实例化到OutputStream中的写操作。

示例

下面是一个连接管理的可能的范例:

private class ConnectedThread extends Thread {
    private final BluetoothSocket mmSocket;
    private final InputStream mmInStream;
    private final OutputStream mmOutStream;
 
    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();
            tmpOut = socket.getOutputStream();
        } catch (IOException e) { }
 
        mmInStream = tmpIn;
        mmOutStream = tmpOut;
    }
 
    public void run() {
        byte[] buffer = new byte[1024];  // buffer store for the stream
        int bytes; // bytes returned from read()
 
        // Keep listening to the InputStream until an exception occurs
        while (true) {
            try {
                // Read from the InputStream
                bytes = mmInStream.read(buffer);
                // Send the obtained bytes to the UI activity
                mHandler.obtainMessage(MESSAGE_READ, bytes, -1, buffer)
                        .sendToTarget();
            } catch (IOException e) {
                break;
            }
        }
    }
 
    /* Call this from the main activity to send data to the remote device */
    public void write(byte[] bytes) {
        try {
            mmOutStream.write(bytes);
        } catch (IOException e) { }
    }
 
    /* Call this from the main activity to shutdown the connection */
    public void cancel() {
        try {
            mmSocket.close();
        } catch (IOException e) { }
    }
}

构造函数获得所需的流并且一旦执行,线程就会通过InputStream等待数据的到来。当 read(byte[])从流中返回字节的时候,使用来自父类中的成员Handler,数据被传送到主Activity中。然后它会返回并且等待来自stream的更多的字节。

发送向外的数据就像调用主Activity的线程中write()方法一样简单。然后这个方法简单地调用 write(byte[])发送数据到远程设备。

线程中的cancel()方法非常重要,因为通过关闭BluetoothSocket,连接能够在任何时候被终止。当你使用完蓝牙连接之后,你应当总是调用这个方法。

一个作为使用Bluetooth APIs范例, 参见 Bluetooth Chat sample app

使用Profiles

从Android3.0开始,蓝牙API支持使用蓝牙Profiles。一个蓝牙profile是一个无线接口规格说明,用于支持设备间基于蓝牙的通信。一个例子就是Hands-Free profile。对于一个移动电话连接到一个无线耳机,两个设备都必须支持Hands-Free profile。

通过实现接口 BluetoothProfile ,你能够写你自己的类来支持一个特别的蓝牙Profile。Android蓝牙API提供如下蓝牙profiles的实现:

  • 耳机 耳机配置文件对于移动手机使用蓝牙耳机提供支持。Android提供 BluetoothHeadset类,它是通过跨进程通信(IPC)来控制蓝牙耳机服务的代理。其中包括蓝牙耳机以及Hands-Free(V1.5)profiles。 BluetoothHeadset类包含对AT命令的支持,想了解更多有关这部分的内容,参见 制造商定制AT命令

  • A2DP 高级音频发布配置文件(Advanced Audio Distribution Profile profile 简称A2DP)定义了通过一个蓝牙连接将多高质量的音频能够从一台设备传输到另外一台。Android提供 BluetoothA2dp 代理类,通过跨进程通信(IPC)控制蓝牙A2DP。

  • 医疗设备 ** Android4.0(API 14)引入对蓝牙医疗设备(Health Device Profile 简称HDP)的支持。这允许你创建应用,同支持蓝牙的医疗设备进行通信,例如心率监测,血压,温度计,定标器等等。对于支持的设备列表以及它们对应设备数据特殊码(device data specialization codes),参见www.bluetooth.org.中的Bluetooth Assigned Numbers*,注意这些值也在 ISO/IEEE 11073-20601 [7] 规格书中作为在命名规范附件(Nomenclature Codes Annex )中的MDC_DEV_SPEC_PROFILE_被引用。想了解更多内容,参见 医疗设备配置文件

这里是使用配置文件的基本步骤:

  1. 得到默认的adapter,如设置蓝牙章节所述。

  2. 使用 [getProfileProxy()](https://developer.android.com/reference/android/bluetooth/BluetoothAdapter.html#getProfileProxy(android.content.Context, android.bluetooth.BluetoothProfile.ServiceListener, int)) 建立相关配置文件的配置文件代理对象的一个连接。在如下的示例中,配置文件代理对象是 BluetoothHeadset的一个实例。

  3. 建立一个 BluetoothProfile.ServiceListener。当BluetoothProfile IPC客户端已经连接到服务或者和服务断开连接的时候,会得到listener的通知。

  4. 在 [onServiceConnected()](https://developer.android.com/reference/android/bluetooth/BluetoothProfile.ServiceListener.html#onServiceConnected(int, android.bluetooth.BluetoothProfile))中,获得一个配置代理对象的句柄。

  5. 一旦你获得配置代理对象,你能够使用它来监测连接的状态,并且执行和那个配置文件相关的其他操作。
    例如,如下代码片段显示如何连接到一个BluetoothHeadset代理对象,通过它你能够控制耳机配置文件:

BluetoothHeadset mBluetoothHeadset;
 
// Get the default adapter
BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
 
// Establish connection to the proxy.
mBluetoothAdapter.getProfileProxy(context, mProfileListener, BluetoothProfile.HEADSET);
 
private BluetoothProfile.ServiceListener mProfileListener = new BluetoothProfile.ServiceListener() {
    public void onServiceConnected(int profile, BluetoothProfile proxy) {
        if (profile == BluetoothProfile.HEADSET) {
            mBluetoothHeadset = (BluetoothHeadset) proxy;
        }
    }
    public void onServiceDisconnected(int profile) {
        if (profile == BluetoothProfile.HEADSET) {
            mBluetoothHeadset = null;
        }
    }
};
 
// ... call functions on mBluetoothHeadset
 
// Close proxy connection after use.
mBluetoothAdapter.closeProfileProxy(mBluetoothHeadset);

制造商定制AT命令

从Android3.0开始,应用能够注册,接收耳机发送的,预先定义的,制造商预先定制的AT命令的系统广播(例如Plantronics + XEVENT命令)。例如,一个应用能够接收支持已连接设备电池电量的广播,并且提醒用户采取其他需要的行动。为ACTION_VENDER_SPECIFIC_
HEADSET_EVENT intent 创建一个广播接收器,为耳机操作制造商定制的AT命令。

医疗设备配置文件(HDP)

Android 4.0(API 14版)引入对蓝牙医疗设备配置文件(HDP)的支持。它能让你创建这样的应用程序,它能够使用蓝牙和支持蓝牙的医疗设备进行通信,例如心率监测,血压,温度计,定标器等等。蓝牙医疗API包括类 BluetoothHealthBluetoothHealthCallbackBluetoothHealthAppConfiguration,这些已经在基础部分描述过。

要使用医疗设备API,了解下面这些医疗设备配置文件的中的关键概念是很有帮助的。

| 概念 | 说明 |
|:----- :| ------ |
|Source | 定义在HDP中的角色。一个source代表一个医疗设备(体重计,葡萄糖计量仪,温度计等等。),此设备将医学数据传输到例如Android手机或者平板的一个智能设备中。 |
| Sink | 定义在HDP中的角色。一个sink就是接收医学数据的那个智能设备。在一个Android HDP应用中,Sink通过一个BluetoothHealthAppConfiguration对象来体 |
| Registration | 指为一个特定的医疗设备注册一个sink |
|Connection | 指在一个智能设备(例如一个Android手机或者平板)和一个医疗设备之间打开一个信道(channel ) |

创建一个HDP应用

下面是创建一个Android HDP应用所需的基本步骤:

  1. 获得一个 BluetoothHealth 代理对象的引用。
    类似于常规的耳机和A2DP配置文件设备,你必须用BluetoothProfile.ServiceListenerHEALTH配置文件类型来调用 [getProfileProxy()](https://developer.android.com/reference/android/bluetooth/BluetoothAdapter.html#getProfileProxy(android.content.Context, android.bluetooth.BluetoothProfile.ServiceListener, int)),以便和这个配置代理对象建立一个连接。

  2. 创建一个 BluetoothHealthCallback,并注册一个应用配置(BluetoothHealthAppConfiguration)作为一个医疗sink。

  3. 建立同医疗设备的连接。某些设备会初始化这个连接。对于那些设备不需要执行这个步骤。

  4. 当同一个医疗设备连接成功的时候,使用文件描述符( file descriptor)对医疗设备进行读写。 接收到的数据需要使用一个实现了IEEE 11073-xxxxx规范升的医疗管理器进行解析。

  5. 当完成操作,关闭医疗信道并且注销这个应用。当处于长时间不运行状态的时候,信道也会关闭。

完整的实现步骤相关代码,可以参考Android Bluetooth HDP (Health Device Profile).。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容