蓝牙具有point-to-point 和 multipoint两种连接功能。使用蓝牙API,可以做到:
.搜索蓝牙设备
.从本地的Bluetooth adapter中查询已经配对的设备。
.建立RFCOMM通道。
.通过service discovery连接到其它设备。
.在设备之间传输数据。
管理多个连接。
1.The Basics
使用蓝牙进行设备通信,主要包含四个部分:蓝牙设置、搜索设备(配对的或可见的)、连接、传输数据。实现这些功能主要需要下面这几个类:
.BluetoothAdapter
代表本地蓝牙适配器(蓝牙发射器),是所有蓝牙交互的入口。通过它可以搜索其它蓝牙设备,查询已经配对的设备列表,通过已知的MAC地址创建BluetoothDevice,创建BluetoothServerSocket监听来自其它设备的通信。
.BluetoothDevice
代表一个远程的蓝牙设备。通过它可以与远程设备通过BluetoothSocket建立连接,查询远程设备属性。
.BluetoothSocket
.BluetoothServerSocket
.BluetoothClass
描述了一个设备的特性(profile)或该设备上的蓝牙大致可以提供哪些服务(service),但不可信。比如,设备是一个电话、计算机或手持设备;设备可以提供audio/telephony服务等。可以用它来进行一些UI上的提示。
要获取可靠的信息,需要进行SDP请求。当使用createRfcommSocketToServiceRecord(UUID) 或 listenUsingRfcommWithServiceRecord(String, UUID)创建一个RFCOMM socket时,SDP请求会自动进行。
2.Bluetooth Permissions
至少要在AndroidManifest.xml中声明两个权限:BLUETOOTH(任何蓝牙相关API都要使用这个权限) 和 BLUETOOTH_ADMIN(设备搜索、蓝牙设置等)。要请求BLUETOOTH_ADMIN的话,必须要先有BLUETOOTH。
3.Setting Up Bluetooth
程序中使用蓝牙之前,应该先确定蓝牙设备是否存在、蓝牙是否已经被打开。蓝牙设备不存在的话,程序要正常退出。蓝牙存在但没有打开的话,要请求用户打开蓝牙。
.确定蓝牙设备是否存在
BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (mBluetoothAdapter == null) {
// Device does not support Bluetooth
}
.打开蓝牙
通过isEnabled()检查蓝牙是否打开,没有打开的话,通过下面的方法弹出对话框提示用户打开。
if (!mBluetoothAdapter.isEnabled()) {
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}
成功的话,会在onActivityResult()这个回调方法中收到RESULT_OK,否则就是RESULT_CANCELED。
除了通过onActivityResult(),还可以通过监听ACTION_STATE_CHANGED这个broadcast Intent来知道蓝牙状态是否改变。这个Intent包含EXTRA_STATE,EXTRA_PREVIOUS_STATE两个字段,分别代表新旧状态。可能的值是STATE_TURNING_ON, STATE_ON, STATE_TURNING_OFF, 还有STATE_OFF。
另外,enable()也会打开蓝牙,但不会通知用户。
还有,让蓝牙设备可见,也会自动打开蓝牙。
4.Finding Devices
Device discovery是指对本地设备进行扫描的过程,只有把自己设置为可见的设备才能被扫描到。如果设备把设备为可见的话,它会共享设备名字、class、MAC地址等信息。使用这些信息,才可以进行连接。
当第一次连接到一个设备S时,时,S会要求配对,配对成功后,才能获取S的设备名字、class、MAC地址等信息。
通过远程设备的MAC地址,可以与之通信,但设备如果正在进行扫描的话,就不行了。
设备配对是指两个设备之间有一个动态密钥用于认证和传输加密数据。设备连接是指两个设备建立一个RFCOMM通信可以用于数据。Android中,设备配对后才能连接。
Android设备默认不可见,用户可以设置可见性和可见时间。
4.1 Querying paired devices
在进行扫描之前,可以通过getBondedDevices()(会返回已经配对过的设备集合)看一下要寻找的设备是否已经配对过了。
Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices();
// If there are paired devices
if (pairedDevices.size() > 0) {
// Loop through paired devices
for (BluetoothDevice device : pairedDevices) {
// Add the name and address to an array adapter to show in a ListView
mArrayAdapter.add(device.getName() + "/n" + device.getAddress());
}
}
连接一个设备只需要一个MAC地址。所以,如果这个集合中已经有了你想要的设备,直接取出MAC地址连接就可以了。
4.2 Discoverying devices
调用startDiscovery()开始进行设备搜索。扫描过程是这样的。先进行大约12秒的扫描,然后对扫描到的每个设备查询名字。
要接收扫描结果需要注册一个BroadcastReceiver接收ACTION_FOUND Intent。这个Intent里面包含扫描结果,扫描到一个设备就会广播一次。Intent的两个字段 EXTRA_DEVICE和EXTRA_CLASS分别包含与远程设备对应的BluetoothDevice和BluetoothClass。代码如下:
设备扫描很费资源,一旦找到你需要的设备,应该马上调用cancelDiscovery()取消扫描。
如果已经连接到远程设备,再扫描时会减少连接带宽。所以,最好不要这么干。
4.3 Enabling discoverability
想要设备可见的话,像下面这样做。然后通过onActivityResult()用户选择接收结果。下面的代码会弹出对话框让用户选择。
默认情况下,设备可见时间是120秒。如果要改变这个时间的话,为Intent添加一个extra数据EXTRA_DISCOVERABLE_DURATION。
Intent discoverableIntent = new
Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
startActivity(discoverableIntent);
想在设备可见性改变时得到通知的话,可以注册一个BroadcastReceiver来接收ACTION_SCAN_MODE_CHANGED这个Intent。这个Intent的extra有两个EXTRA_SCAN_MODE和EXTRA_PREVIOUS_SCAN_MODE,分别代表新旧状态。它们的值是SCAN_MODE_CONNECTABLE_DISCOVERABLE, SCAN_MODE_CONNECTABLE, 或SCAN_MODE_NONE其中的一个。
只有想让其它设备主动连接你的时候,才有必要使设备可见。
5.Connecting Devices
为了在两台设备上创建一个连接,你必须在软件上实现服务器端和客户端的机制,因为一个设备必须必须打开一个server socket,而另一个必须初始化这个连接(使用服务器端设备的MAC地址进行初始化)。
当服务器端和客户端在同一个RFCOMM信道上都有一个BluetoothSocket时,就可以认为它们之间建立了一个连接。在这个时刻,每个设备能获得一个输出流和一个输入流,也能够开始数据传输。本节介绍如何在两个设备之间初始化一个连接。
服务器端和客户端获得BluetoothSocket的方法是不同的,服务器端是当一个进入的连接被接受时才产生一个BluetoothSocket,客户端是在打开一个到服务器端的RFCOMM信道时获得BluetoothSocket的。
一种实现技术是,每一个设备都自动作为一个服务器,所以每个设备都有一个server socket并监听连接。然后每个设备都能作为客户端建立一个到另一台设备的连接。另外一种代替方法是,一个设备按需打开一个server socket,另外一个设备仅初始化一个到这个设备的连接。
Note: 如果两个设备在建立连接之前并没有配对,那么在建立连接的过程中,Android框架将自动显示一个配对请求的notification或者一个对话框,如Figure 3所示。所以,在尝试连接设备时,你的应用程序无需确保设备之间已经进行了配对。你的RFCOMM连接将会在用户确认配对之后继续进行,或者用户拒绝或者超时之后失败。
5.1 Connecting as a server
当你想要连接两个设备时,其中一个必须保持一个打开的BluetoothServerSocket,作为服务器。服务器socket将监听进入的连接请求,一旦连接被接受,将产生一个BluetoothSocket。
About UUID
一个Universally Unique Identifier(UUID)是一个字符串ID的标准化128位格式,将被用于唯一标识信息。你可以使用web上的任何一款UUID产生器为你的程序获取一个UUID,然后使用fromString(String)初始化一个UUID。你可以用UUID来标识你的蓝牙服务。
这儿是展示设置一个server socket并接受一个连接的基础过程:
调用listenUsingRfcommWithServiceRecord(String, UUID)获得一个BluetoothServerSocket。
字符串参数是你的服务的标识名,系统将自动将这个标识名写到设备上一个新的Service Discovery Protocol(SDP)数据库条目中(这个标识名可以简单地作为你的程序的名字,也就是说可以把你的程序的名字作为命名)。UUID也会被包含在这个新的SDP条目中,并作为与客户端设备建立连接的基础。 That is, when the client attempts to connect with this device, it will carry a UUID that uniquely identifies the service with which it wants to connect. These UUIDs must match in order for the connection to be accepted (in the next step).
调用accept()开始监听连接请求
这是一个阻塞调用。当一个连接被接受或者发生异常时将返回。一个连接,仅当一个远程设备发出的请求包含UUID匹配正在监听的server socket所注册的UUID时被接受。成功的时候,accept()将返回一个连接的BluetoothSocket。
除非你要接受另外一个连接,否则调用close,关闭server socket。
这将释放server socket和它占用的所有资源,但不要用来关闭accept返回的已连接的BluetoothSocket。不像TCP/IP,RFCOMM仅允许一个信道在某一时刻有一个连接的客户端。所以,创建了一个连接的socket之后立即调用close()来关闭BluetoothServerSocket。
accept()方法不应该在主Activity UI线程中执行,因为它是一个阻塞调用,如果在主Activity UI线程中条也能够将会阻止与用户的交互。一般使用BluetoothServerSocket或者BluetoothSocket进行相关工作时都是在一个新的线程中。为了避免调用诸如accept()这样的阻塞调用,针对来自其他线程的BluetoothServerSocket或者BluetoothSocket调用close()将会使阻塞调用立即返回。注意,针对BluetoothServerSocket或者BluetoothSocket调用的方法都是线程安全的,也就是说可以在多个线程中使用。
5.2 示例
manageConnectedSocket()用于初始化传输数据的线程。
另外,你接收到一个连接并建立起BluetoothSocket后,通常应该关掉BluetoothServerSocket。
5.3 Connecting as a client
要连接到远程设备,可以按下面几步进行:
.通过BluetoothDevice的createRfcommSocketToServiceRecord(UUID)方法获取到BluetoothSocket。其中,UUID必须与远程设备的UUID匹配。
.调用BluetoothSocket.connect()方法。调用这个方法时,会在远程设备上做SDP查询,看UUID是否匹配。一切OK的话,会建立RFCOMM通道。connect()阻塞这时才会返回。如果失败,或者connect超时(一般是12秒),会抛出异常。
在connect()前,一定确保扫描结束,否则会很慢,而且容易失败。
5.4 Example
这个例子演示了如何连接到远程设备
上面的代码在connect()前调用了cancelDiscovery()。你最好也这样做,而且不管设备有没有正在扫描,这个方法都不会出错。
manageConnectedSocket()会初始化一个线程用来传输数据。
6. Managing a Connection
得到BluetoothSocket后,就可以开始传输数据了:
.通过getInputStream()和getOutputStream(),得到用于传输数据的InputStream和OutputStream。
.通过read(byte[])和write(byte[])读写数据。
就这么简单。
6.1 Example
下面是示例代码
上面代码很简单,值得一提的是cancel()。任何时候都可以通过关闭Bluetooth来关闭连接。这个方法是可以从主线程里面调用的。
Android源码自带了一个Bluetooth Chat程序,可以参考。