蓝牙

来源:互联网 发布:js 动态创建二维数组 编辑:程序博客网 时间:2024/06/10 11:40



翻译自Android Developer,方便感兴趣的同学阅读。原址:http://developer.android.com/guide/topics/connectivity/bluetooth.html


Android平台支持一系列的蓝牙网络,蓝牙是一种允许多个蓝牙设备间交换数据的技术。Android的应用框架提供了一些蓝牙API访问蓝牙的所有功能。这些API允许应用程序无线连接到其他蓝牙设备,支持点到点或多点连接。


使用蓝牙API,一个应用程序可以拥有如下功能:

  • 扫描其他蓝牙设备;
  • 查询周围的蓝牙设备并进行配对;
  • 创建RFCOMM信道;(RFCOMM自己百度吧)
  • 通过发现服务,连接到其他设备;
  • 发送数据到其他拥有蓝牙的设备,或者从其他设备接收数据;
  • 管理多个蓝牙连接。

本文档描述怎样使用典型的蓝牙(当前主要使用的蓝牙,还有一种耗电量低的蓝牙类型)。经典的蓝牙设备适合于电量敏感设备之间数据流的传输,例如不同的android设备之间。为了满足蓝牙设备的低功耗要求,Android 4.3(API Level 18)提供了支持蓝牙低功耗技术的API。想知道更多,查看蓝牙低功耗(Bluetooth Low Energy

基本类The Basics)


本文档详细描述如何使用Android提供的蓝牙设备API完成蓝牙设备间通讯主要的四个常用操作:设置蓝牙、发现已经配对的蓝牙或者可用的附近的蓝牙设备、已经连接的设备、设备间传输数据。

所有的蓝牙相关的API都在包android.bluetooth里面。下面是你在创建蓝牙连接时会用到的一些类和接口的简介:

BluetoothAdapter
表示本地的蓝牙适配器(无线蓝牙)。BluetoothAdapter是所有蓝牙交互的入口。通过适配器,你可以发现其他蓝牙设备、查询有效范围内或者已经适配的蓝牙设备列表、使用MAC地址初始化一个BluetoothDevice、创建监听来自其他蓝牙设备的BluetoothServiceSocket对象。
BluetoothDevice
表示一个远程蓝牙设备。通过该类,通过BluetoothSocket请求一个远程设备连接,或者查询设备的信息,例如,设备名、地址、类、状态等。
BluetoothSocket
表示蓝牙Socket(类似于TCP Socket)的接口。应用程序通过该类来与其他蓝牙设备交换数据,使用输入流和输出流的方式。
BluetoothServerSocket
表示一个开放的服务接口,用来监听进入的请求(类似TCP的ServerSocket)。为了连接两个Android设备,一个设备必须使用该类打开一个服务端口。当一个远程蓝牙设备发送一个请求到这个设备的时候,而且同意该请求时,BluetoothServerSocket将会返回一个已连接的BluetoothSocket
BluetoothClass
描述蓝牙设备的常用特色和功能。这是一个只读的设备集合,该集合定义了设备主要和次要的设备类和他提供的服务。然而,它并不是可靠的描述了设备支持的所有的蓝牙参数和服务,但是作为某种设备类型的提示很有用(原句:However, this does not reliably describe all Bluetooth profiles and services supported by the device, but is useful as a hint to the device type.翻译的不太对,求指点)
BluetoothProfile
(Bluetooth规范中定义了Profile。Profile定义了设备如何实现一种连接或者应用,你可以把Profile理解为连接层或者应用层协议。)
表示蓝牙协议的接口。蓝牙协议定义了负责设备间基于蓝牙通讯的具体无线接口。比如蓝牙免提设备等。想要知道跟多的蓝牙设备类型,查看Working with Profiles。
BluetoothHeadset
提供对在移动电话中使用的蓝牙头戴式设备的支持(比如蓝牙耳机)。该类包含了蓝牙头戴式设备和免提设备(v1.5)。
BluetoothA2dp
Defines how high quality audio can be streamed from one device to another over a Bluetooth connection. "A2DP" stands for Advanced Audio Distribution Profile.
定义了通过蓝牙连接,两个设备间可以传输多高质量的音频。“A2DP”表示高级音频传输规范。(Advanced Audio Distribution Profile
BluetoothHealth
表示健康设备规范代理,可以控制蓝牙设备。(类似于健康蓝牙称、蓝牙手表等穿戴式设备,检测心跳之类的设备)
BluetoothHealthCallback
实现BluetoothHealth回调的抽象类。为了接受到应用程序的登记状态和蓝牙信道的状态,你必须继承并实现回调函数来对这些状态进行处理。
BluetoothHealthAppConfiguration
三方的蓝牙应用程序需要进行此设置来与远程的蓝牙健康设备进行通信。
BluetoothProfile.ServiceListener
通知BluetoothProfile进程间通信的接口,当他们与服务器连接上或者中断连接(也就是内部服务已经在某个设备上运行了,翻译不对啊)

蓝牙授权 Bluetooth Permissions


为了在你的应用程序中使用蓝牙,你必须声明蓝牙权限BLUETOOTH。 你需要这个权限来执行任何蓝牙通讯,例如请求一个连接、接收一个连接、传输数据。

如果你需要你的应用程序能够发现周围的设备或者修改蓝牙设置,你还必须声明BLUETOOTH_ADMIN权限。

大部分应用程序需要此唯一的权限来实现搜索本地蓝牙设备的功能。该权限还可以授权应用程序在用户要求下修改蓝牙设置。注意:如果你在应用中引入了BLUETOOTH_ADMIN权限,那么你必须同样要声明引用BLUETOOTH权限。

在应用中声明这些权限如下所示:

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

 <uses-permission>中查看更多如何在应用中声明权限的信息。

Setting Up Bluetooth



图1 开启蓝牙对话框.

设置蓝牙

在你的应用可以通过蓝牙通讯之前,你必须确认蓝牙在设备上可用,如果可用,还需要保证蓝牙已经打开了。

如果该设备不支持蓝牙,那么你必须在应用中关掉所有蓝牙相关的特色功能。如果支持,但是没有打开,你可以子啊应用中请求用户打开蓝牙。该步骤可以使用BluetoothAdapter在两步之内完成。

  1. 获取蓝牙适配器

    所有的蓝牙活动都需要一个BluetoothAdapter。 使用静态的getDefaultAdapter()方法获取蓝牙适配器(BluetoothAdapter)。该方法将返回一个蓝牙适配器(BluetoothAdapter),该对象代表设备自己的蓝牙适配器(蓝牙无线连接)。有一个针对整个系统的蓝牙适配器,你可以使用这个对象与其交互。如果getDefaultAdapter()返回null,那么该设备不支持蓝牙,到此结束了。代码如下:

 BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();

if (mBluetoothAdapter == null) {
    
// Device does not support Bluetooth设备不支持蓝牙,就是说没有蓝牙
}

  1.  开启蓝牙
    下一步,你需要开启蓝牙。调用函数isEnabled()检查当前蓝牙是否开启。 如果返回false,那么说明蓝牙是关闭的。如果请求开启蓝牙,使用带动作ACTION_REQUEST_ENABLE的意图,调用startActivityForResult()函数。这个操作将通过系统设置(而不用中断自己的应用)发出开启蓝牙的请求。代码如下:
    if (!mBluetoothAdapter.isEnabled()) {
        
    Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
        startActivityForResult
    (enableBtIntent, REQUEST_ENABLE_BT);
    }

    一个要求用户授权的对话框将显示出来,如图1所示。如果用户点击“Yes”,系统将会开启蓝牙,并且界面将会回到你的应用程序中。

    传递给函数 startActivityForResult()的常量通常定义为一个大于零的整数, 系统将返回该值,在函数onActivityResult()中,包含在变量requestCode中。

    如果开启蓝牙成功,你的活动将在回调函数onActivityResult()中,收到表示成功的返回码RESULT_OK。如果由于某个错误(比如用户选择“No”,拒绝了授权),导致蓝牙并没有开启,那么将会返回错误码RESULT_CANCELED

还有另外的途径,你的应用可以监听带有动作ACTION_STATE_CHANGED(蓝牙状态改变)的广播意图,这个意图在蓝牙状态改变的时候被系统广播。该广播包含两个属性EXTRA_STATEEXTRA_PREVIOUS_STATE,相对应的包含了蓝牙的新状态和旧状态。这两个属性的可能值为STATE_TURNING_ONSTATE_ONSTATE_TURNING_OFF或者STATE_OFF。在你的应用程序运行的时候,接收该广播,可以很方便的获取到蓝牙状态的改变。

提示:开启可发现功能,将自动启动蓝牙。如果你决定一直开启设备的可发现功能在执行蓝牙活动之前,你可以跳过第二步。阅读下面与enabling discoverability相关的内容,获取跟多信息。

搜寻设备(Finding Devices)


使用蓝牙适配器(BluetoothAdapter),你可以通过寻找设备或者从已配对的设备列表中,找到远端的蓝牙设备。

搜寻设备是一个扫描流程,它会寻找附近的打开了蓝牙的设备,并且从这些设备中获取一些信息(我们也称之为发现、询问、扫描)。然而,只有当某蓝牙设备被设置成可发现时,才能够被其他蓝牙设备搜寻到。当一台蓝牙设备被设置为可发现时,它才会对附近的蓝牙扫描请求回应,回应信息包含设备名、类、唯一的MAC地址。通过这些信息,两台设备之间才能够初始化设备间的连接。

一旦与远程设备的连接第一次建立,一个配对请求就会自动显示在用户面前。当一台设备配对后,设备的基本信息(设备名、类、MAC地址等)机会被保存下来,而且可以使用蓝牙API了。使用已知的远程设备的MAC地址(在配对的时候就远程地址已经保存了),可以在任何时候初始化一个连接而不用再次搜寻了(译者注:我们假设设备还在有效范围内,另外一种情况是设备超出了有效范围,但是同样会在蓝牙设备的列表中)。

记住,被配对和被连接是不同的。被配对意味着两台设备都知晓另外一台设备的存在,有用来互相授权的相同的分享链接码(可能就是配对码了?),而且还可以建立连接两者的加密连接。被连接表示设备间分享同一个RFCOMM信道,并且可以互相传送数据(译者注:比如传个mp3啥的)。目前的Android平台的蓝牙API要求在建立RFCOMM信道之前先配对(配对动作会在你使用蓝牙API初始化一个加密连接的时候自动调用)。

下面的小结将描述如何发现已配对的设备,或者使用搜索设备功能发现新设备。

注意:Android蓝牙设备默认是不可发现的。用户可以在系统设置里面设定限定时间的设备可发现属性,或者应用程序可以要求用户授权在该应用内蓝牙都是可发现的。下面将会讨论如何使设备可发现(enable discoverability)。

询问已配对设备(Querying paired devices)

在执行搜索设备之前,最好先查询已配对设备列表,看看我们要连接的设备是否已经在列表中。可以通过函数getBondedDevices()来达到这个目的。 这个函数将返回一系列包含已配对蓝牙设备(BluetoothDevice)的列表。例如,你可以查询所有已配对过的设备,并使用ArrayAdapter来显示出来,如下代码所示:

Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices();
// 如果有已配对过的设备
if (pairedDevices.size() > 0) {
    
// 循环该列表
    
for (BluetoothDevice device : pairedDevices) {
        //将名称和地址加入一个数组适配器,并显示在一个列表视图中
        mArrayAdapter.add(device.getName() + "\n" + device.getAddress());
    
}
}

为了初始化一个连接,我们唯一需要的是从BluetoothDevice 中获取的MAC地址。在这个例子中,地址被作为数组适配器的一部分被显示给用户了。MAC地址后面可以用来精确的初始化一个连接。在Connecting Devices节中,你可以学到更多关于如何创建连接的知识。

搜索设备(Discovering devices)

简单的调用函数startDiscovery()就可以开始搜索附近的蓝牙设备了。该过程是一个异步过程,并且当搜索过程成功启动之后,就会立即返回一个布尔值的变量。该搜索过程通常包含12秒钟的查询扫描inquiry scan,不懂翻啊,随后包含一个返回所有蓝牙设备名称的呼叫扫描(page scan,不懂翻啊)。

当发现一个设备之后,系统就会发送广播,所以你的应用程序必须包含一个含有ACTION_FOUND意图的广播接收器来接收信息。对每个已发现设备来说,系统会广播ACTION_FOUND意图。该意图还携带有额外的值域: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地址。在这个例子中,MAC地址作为显示给用户的部分数据,被保存在数组适配器中了。该MAC地址随后被用来初始化连接。Connecting Devices节中,你可以学到更多关于如何创建连接的知识。

注意:执行搜索操作对于蓝牙适配器来说是一个非常耗时的操作,而且会消耗很多资源。一旦你找到了需要连接的设备,在视图连接之前,最好立刻停止搜索,停止搜索可以使用cancelDiscovery()函数。同样地,如果你同一个设备已经连接上,如果再开启搜索,将会减少已有连接的可用带宽,所以当连接的时候,最好不要执行扫描操作。

使设备可发现(Enabling discoverability)

使设备可发现(也就是说允许设备被扫描到)

如果你想允许设备被扫描到,调用startActivityForResult(Intent, int)函数,设置其中的意图的动作为ACTION_REQUEST_DISCOVERABLE。该函数将通过系统设置将设备变为可被发现(该操作不需要退出你的应用界面),并保持12秒钟的该状态。可以通过该意图中额外的值域EXTRA_DISCOVERABLE_DURATION 定义不同的可被发现时间。应用程序可以设置的最大时间为3600秒(1小时), 值0表示设备一直可被发现。任何大于3600秒或者小于0的值,都会被默认为120秒。比如下面的代码将会设置时间为300秒:

Intent discoverableIntent = new
Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverableIntent
.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
startActivity
(discoverableIntent);
Figure 2: 开启授权允许发现对话框。

当设置这个时间的时候,如图2的对话框会弹出提示用户。如果用户选择“Yes”,设备将变为可发现状态并保持300秒。你的应用程序中的当前活动将会回调函数onActivityResult(),返回的值就是设备设置的可发现时间。如果用户选择“No”,或者有一个错误发生时,返回值就是RESULT_CANCELED

注意:如果蓝牙为开启,那么设置设备为可发现的时候就会自动开启蓝牙。(一般是在应用程序中才会有这个流程,系统设置中都是先开启蓝牙,再设置可被发现的)

在指定时间内,设备都会保持可被发现状态。如果你想在该状态发生改变的时候收到通知,只需要使用包ACTION_SCAN_MODE_CHANGED含动作的意图注册一个广播接收器即可。该意图包含额外的值域 EXTRA_SCAN_MODEEXTRA_PREVIOUS_SCAN_MODE,这两个分别告诉新的和旧的扫描模式。扫描模式的可能值有SCAN_MODE_CONNECTABLE_DISCOVERABLE(两台设备都是可发现状态)SCAN_MODE_CONNECTABLE(不在可发现状态,但是依然可以接受连接)或者SCAN_MODE_NONE(不在可发现状态,同样也不能接受连接)

如果你初始化一个连接到远程的设备,那么就不需要开启设备的可发现属性。只有在你需要自己的应用程序维持一个可以接受客户端连接请求的服务端时,才需要开启设备的可发现性(译者注:也就是说某台设备想连接到你的设备,那么你的设备就需要设置为连接属性)。因为远程服务器必须发现你的设备才能够初始化连接。

连接设备(Connecting Devices)


为了通过你的应用在两台设备间创建连接,你必须同时实现服务器端和客户端机制,因为一台设备必须作为服务器端口,另外一台必须初始化连接(使用服务器设备的MAC地址类初始化连接,这个过程跟TCP的连接开启类似)。如果服务器端和客户端都通过同一个RFCOMM信道,使用BluetoothSocket连接到一起,那么我们就认为它们成功建立了一个连接。这样,每台设备都可以输入输出数据,并且可以开始传输数据。细节将在管理连接(Managing a Connection)这一节中详细描述。该小节描述了如何初始化两台设备之间的连接。

服务器端和客户端获取BluetoothSocket的方式不同。服务器端在有连接请求的时候获得,而客户端在它打开一个通向服务器端的RFCOMM信道时获得。

Figure 3: 蓝牙配对对话框。

有一个已经实现的技术就是自动将每个设备都作为服务器,所以每个设备都打开一个服务器端口,并监听连接请求。这样每个设备都可以跟另外一台设备初始化一个连接,并变成客户端。另外一个选择是,一台设备可以明确声明为服务器端,并且打开一个服务器端口,这样其他设备就可以很简单的初始化连接了。

注意:如果两台设备之前并没有配对过,那么Android系统会自动显示一个请求配对的对话框给用户,如图3所示。所以当你的应用请求连接的时候,两台设备并不要求已连接或者已配对。在两台设备配对成功之前,你的RFCOMM信道将会一直堵塞。如果用户拒绝配对或者配对超时的话,会返回配对失败信息。

作为服务器端(Connecting as a server)

如果你想连接两台设备,那么其中一台必须作为服务器端,并且打开一个蓝牙服务器端口(BluetoothServerSocket)。服务器端口的目的是监听连接请求,当接受该请求的时候,提供一个已连接的BluetoothSocket。当从BluetoothServerSocket获取到BluetoothSocket时,就可以被丢弃BluetoothServerSocket了,除非你想接收更多连接。

下面是设置服务器端口和接受连接时的基本流程:

  1. 调用函数listenUsingRfcommWithServiceRecord(String, UUID)获取BluetoothServerSocket

    参数里面的字串是你的设备一个可辨识的名称,系统会自动将他写入一个新的服务发现协议(Service Discovery Protocol SDP)数据库条目(名称任意,可以使用你自己的应用名称)。SDP条目也包含UUID,这个id将是与客户端连接的基础。也即是,当客户端视图连接本设备时,请求中必须包含唯一的UUID标识符,用来标识目的服务器。如果想连接成功,那么UUID必须匹配(下一步骤中将用到)。

  2. 调用accept()监听连接请求

    这是一个阻塞调用。当连接请求被接受或者有异常发生时将返回。当远程设备发送的连接包含的UUID匹配到该服务器端口注册时候使用的UUID时,该连接就建立成功了。当连接成功的时候,accept()将返回一个已连接的蓝牙端口(BluetoothSocket)。

  3. 如果你不需要接受额外的连接,那么调用close()

    这个函数将释放服务器端口和它占用的资源,但是不会关闭从accept()返回的已连接蓝牙端口(BluetoothSocket)。不像TCP/IP中,RFCOMM的信道一次仅允许一个客户端连接,所以在绝大多数情况中,在接受了连接请求之后,就会立即在BluetoothServerSocket中调用close(),关闭端口并释放资源。

因为accept()函数会阻塞进程,并且会影响应用中其他的内部交互,所以最好不要在主线程(UI线程)中调用。最好应用程序另开一个线程,并且在该线程中通过BluetoothServerSocketBluetoothSocket来调用该函数。可以通过在另外线程中通过BluetoothServerSocket(或者BluetoothSocket)调用close(),可以中断类似accept()阻塞函数,并且立即返回。注意,BluetoothServerSocketBluetoothSocket中所有的函数都是线程安全的。

例子:

下面是一个简化的线程,服务器端可以使用它来接受连接请求:

 
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()了。(不用像你从客户端调用connect()那样)

在应用程序中,manageConnectedSocket()方法将初始化一个线程来传输数据,该方法将在Managing a Connection小节中详细讨论。

在完成了监听连接请求之后,一般你就需要关闭你的BluetoothServerSocket。在本例中,在获取到BluetoothSocket之后,就马上调用close()函数了。也许你依然想要在你的线程中提供一个公共方法来关闭私有的 BluetoothSocket,你也需要在服务器端停止监听。

作为客户端进行连接(Connecting as a client)

为了与远程设备(该设备作为服务器端,提供一个服务器端口)初始化一个连接(),你必须获取一个逻辑上代表远程设备的BluetoothDevice。(在上一节的Finding Devices中已经讲述了如何获取一个BluetoothDevice了。)

下面是基本流程:

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

    它将初始化一个连接到蓝牙设备(BluetoothDevice)的BluetoothSocket。这里的UUID必须跟服务器端打开的服务器端口(BluetoothServerSocket)之后获取的UUID相同。通过简单的将UUID硬编码到你的应用程序中,并且在服务器端和客户端都引用它。

  2. 通过调用connect()初始化连接。

    调用完这个函数之后,系统将在远程设备执行一个服务发现协议(SDP)为了获取UUID。

    因为connect()是一个阻塞调用,所以这个连接过程必须在非UI线程中运行。

    注意:必须确保你在调用connect()时,你的设备没有在执行搜索其他设备的操作(译者注:在android中的表现就是,如果你在搜索设备的同时连接已经在列表中的设备,并且对方设备同样也在搜索,那么操作会卡在那里。在程序员调试两台设备的时候会出现这样的情况)。如果搜索正在进行中,那么连接操作就会被延后,看起来更像是连接失败。

例子

下面的例子将简单描述,在一个线程中如何初始化一个蓝牙连接:

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()了。在每次你执行连接之前,必须调用该函数,并不需要检测搜索操作是否正在运行也可以安全的调用该函数(当然你如果想要检测是否搜索正在运行,调用函数isDiscovering())。

在管理一个连接(Managing a Connection)小节中讨论过的manageConnectedSocket(),在应用中该函数将会初始化传输数据的线程。

当你用完BluetoothSocket时,一定要调用close()关闭连接。

管理连接(Managing a Connection


如果成功连接了两台(或者更多)设备,每台设备就都会拥有一个已连接的BluetoothSocket,这样你就可以通过它来共享数据了。使用BluetoothSocket来传输任意数据的流程如下:

  1. 使用getInputStream()和getOutputStream(),获取InputStream和OutputStream来处理socket间的数据传输。
  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) { }
    
}
}

构造器需要一个必要的流,一旦执行了之后,线程将等待从输入流传入数据。当read(byte[])从输入流获取到数据的时候,父类的一个处理函数会将数据传到主活动中去。然后返回等待从流中获取更多数据。

发送数据最简单的方法莫过于在主活动内调用线程内的write()方法。该方法将会直接调用write(byte[])将数据发送到远程设备。

线程的cancel()方法非常重要,可以在任何时候用来关闭BluetoothSocket。当你使用完蓝牙连接之后,请记得一定要调用此方法。

如果想要看蓝牙API相关的实例,查看蓝牙聊天示例程序(Bluetooth Chat sample app)。

使用不同的蓝牙模式(Working with Profiles)


使用不同的蓝牙模式(译者注:比如免提模式、无线耳机模式、健康设备模式等,请意会。。)

从Android 3.0平台开始,蓝牙API开始支持不同的蓝牙模式。 一个蓝牙模式是指一个不同设备间,支持基于蓝牙通讯的特定无线蓝牙接口。比如说免提模式。如果一个移动手机需要连接到一个无线耳机,那么双方必须都支持免提模式。

你可以通过实现BluetoothProfile接口来支持你自己的特定蓝牙模式。

  • 无线耳机:该模式支持在移动电话上使用蓝牙耳机。Android提供BluetoothHeadset类,该类作为一个代理,通过进程间通信 (IPC)使用蓝牙无线服务。他包含蓝牙耳机和免提 (v1.5)模式。该类还支持AT命令。想了解更多关于AT命令的内容,参考Vendor-specific AT commands
  • A2DP:高级音频分配模式(A2DP)定义了通过蓝牙连接可以将多高质量的音频流从一台设备传到另一台。同样Android提供了BluetoothA2dp类,通过该代理类,使用进程间通信的方式可以使用蓝牙的A2DP服务。
  • 健康设备:Android 4.0(API Level 14)支持蓝牙健康设备模式(HDP)。该模式将运行你创建一个应用程序,可以使用蓝牙与支持蓝牙功能的健康设备进行通信,比如说心率监控器、血压计、温度计、刻度尺等等。可以在www.bluetooth.org网站了解更多相关的支持的健康设备以及对应的蓝牙分配码。这些值同样在ISO/IEEE 11073-20601 [7]的系统编码附录中也包含,并且命名形式如MDC_DEV_SPEC_PROFILE_*。如果想讨论更多关于HDP的信息,查看Health Device Profile

下面是如果使用模式的基本步骤:

  1.  获取默认适配器,就像设置蓝牙(Setting Up Bluetooth)小结所述。

2. 使用getProfileProxy()获取与特定模式代理的连接。下面的实例中,该模式代理对象是BluetoothHeadset的实例。

3. 设置一个蓝牙服务监听器( BluetoothProfile.ServiceListener)。当客户端与服务器连接或者断开连接的时候,将会通知客户端(BluetoothProfile)。

4. 在onServiceConnected()中,获取一个处理模式代理对象的句柄。

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命令(Vendor-specific AT commands)

从Android 3.0开始,应用程序可以注册广播接收器,接受系统发送的预定义厂商AT命令,这些命令由厂商提供的耳机设备发出,例如缤特力(Plantronics)公司的XEVENT命令。比如,应用程序可以接受关于设备电力的广播,并且通知用户或者采取其他操作。创建针对ACTION_VENDOR_SPECIFIC_HEADSET_EVENT的广播接收器,来获取厂商特定无线耳机的AT命令。

健康设备模式(Health Device Profile)

Android 4.0(API Level 14)介绍并支持蓝牙健康设备模式(HDP)。该模式将运行你创建一个应用程序,可以使用蓝牙与支持蓝牙功能的健康设备进行通信,比如说心率监控器、血压计、温度计、刻度尺等等。蓝牙健康设备API包括如下在前面的基本章节中介绍过的类:BluetoothHealthBluetoothHealthCallbackBluetoothHealthAppConfiguration。

在使用蓝牙健康API中,了解下面的HDP相关的键将会非常有帮助。

概念描述HDP中定义的角色。源就是一个健康设备,它将医疗数据(比如重量、血糖、体温)传输到一个智能设备,比如android手机或者平板。目的HDP中定义的一个角色。在HDP中,目的是接收医疗数据的智能设备。在一个Android的HDP应用中,目的用对象BluetoothHealthAppConfiguration来表示。注册针对特定的健康设备的注册目标的引用。连接一个已打开信道的引用,这个信道是连接健康设备和智能设备,比如一个Android手机或者平板。

创建一个HDP应用

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

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

    类似于无线耳机和A2DP模式设备,必须在BluetoothProfile.ServiceListener中调用BluetoothProfile.ServiceListener,并使用HEALTH模式类型来获取一个与模式代理对象的连接。

  2. 创建一个BluetoothHealthCallback并且注册一个应用配置 (BluetoothHealthAppConfiguration) 来作为一个健康目的。(译者注:翻译不太对啊,原文如,Create a BluetoothHealthCallback and register an application configuration (BluetoothHealthAppConfiguration) that acts as a health sink.)
  3. 建立一个到健康设备的连接。一些设备会初始化改连接。通常在这一步中并不是必须要实现的。
  4. 当建立到健康设备的连接成功之后,使用文件描述符的形式向健康设备读取和写入数据。

    使用实现了IEEE 11073-xxxxx要求的健康设备管理器来解释接收到的数据。

  5. 当完成这些之后,关闭建立的信道并且注销该应用。当有延伸的非活动时,信道也会关闭(译者注:貌似翻译不太对啊,原文 ,The channel also closes when there is extended inactivity.)。

如果要看实现了这些不走的演示程序,查看Bluetooth HDP (健康设备模式)












翻译自Android Developer,方便感兴趣的同学阅读。原址:http://developer.android.com/guide/topics/connectivity/bluetooth.html

quote:先占坑,后面会逐步翻译。


Android平台支持一系列的蓝牙网络,蓝牙是一种允许多个蓝牙设备间交换数据的技术。Android的应用框架提供了一些蓝牙API访问蓝牙的所有功能。这些API允许应用程序无线连接到其他蓝牙设备,支持点到点或多点连接。


使用蓝牙API,一个应用程序可以拥有如下功能:

  • 扫描其他蓝牙设备;
  • 查询周围的蓝牙设备并进行配对;
  • 创建RFCOMM信道;(RFCOMM自己百度吧)
  • 通过发现服务,连接到其他设备;
  • 发送数据到其他拥有蓝牙的设备,或者从其他设备接收数据;
  • 管理多个蓝牙连接。

本文档描述怎样使用典型的蓝牙(当前主要使用的蓝牙,还有一种耗电量低的蓝牙类型)。经典的蓝牙设备适合于电量敏感设备之间数据流的传输,例如不同的android设备之间。为了满足蓝牙设备的低功耗要求,Android 4.3(API Level 18)提供了支持蓝牙低功耗技术的API。想知道更多,查看蓝牙低功耗(Bluetooth Low Energy

基本类The Basics)


本文档详细描述如何使用Android提供的蓝牙设备API完成蓝牙设备间通讯主要的四个常用操作:设置蓝牙、发现已经配对的蓝牙或者可用的附近的蓝牙设备、已经连接的设备、设备间传输数据。

所有的蓝牙相关的API都在包android.bluetooth里面。下面是你在创建蓝牙连接时会用到的一些类和接口的简介:

BluetoothAdapter
表示本地的蓝牙适配器(无线蓝牙)。BluetoothAdapter是所有蓝牙交互的入口。通过适配器,你可以发现其他蓝牙设备、查询有效范围内或者已经适配的蓝牙设备列表、使用MAC地址初始化一个BluetoothDevice、创建监听来自其他蓝牙设备的BluetoothServiceSocket对象。
BluetoothDevice
表示一个远程蓝牙设备。通过该类,通过BluetoothSocket请求一个远程设备连接,或者查询设备的信息,例如,设备名、地址、类、状态等。
BluetoothSocket
表示蓝牙Socket(类似于TCP Socket)的接口。应用程序通过该类来与其他蓝牙设备交换数据,使用输入流和输出流的方式。
BluetoothServerSocket
表示一个开放的服务接口,用来监听进入的请求(类似TCP的ServerSocket)。为了连接两个Android设备,一个设备必须使用该类打开一个服务端口。当一个远程蓝牙设备发送一个请求到这个设备的时候,而且同意该请求时,BluetoothServerSocket将会返回一个已连接的BluetoothSocket
BluetoothClass
描述蓝牙设备的常用特色和功能。这是一个只读的设备集合,该集合定义了设备主要和次要的设备类和他提供的服务。然而,它并不是可靠的描述了设备支持的所有的蓝牙参数和服务,但是作为某种设备类型的提示很有用(原句:However, this does not reliably describe all Bluetooth profiles and services supported by the device, but is useful as a hint to the device type.翻译的不太对,求指点)
BluetoothProfile
(Bluetooth规范中定义了Profile。Profile定义了设备如何实现一种连接或者应用,你可以把Profile理解为连接层或者应用层协议。)
表示蓝牙协议的接口。蓝牙协议定义了负责设备间基于蓝牙通讯的具体无线接口。比如蓝牙免提设备等。想要知道跟多的蓝牙设备类型,查看Working with Profiles。
BluetoothHeadset
提供对在移动电话中使用的蓝牙头戴式设备的支持(比如蓝牙耳机)。该类包含了蓝牙头戴式设备和免提设备(v1.5)。
BluetoothA2dp
Defines how high quality audio can be streamed from one device to another over a Bluetooth connection. "A2DP" stands for Advanced Audio Distribution Profile.
定义了通过蓝牙连接,两个设备间可以传输多高质量的音频。“A2DP”表示高级音频传输规范。(Advanced Audio Distribution Profile
BluetoothHealth
表示健康设备规范代理,可以控制蓝牙设备。(类似于健康蓝牙称、蓝牙手表等穿戴式设备,检测心跳之类的设备)
BluetoothHealthCallback
实现BluetoothHealth回调的抽象类。为了接受到应用程序的登记状态和蓝牙信道的状态,你必须继承并实现回调函数来对这些状态进行处理。
BluetoothHealthAppConfiguration
三方的蓝牙应用程序需要进行此设置来与远程的蓝牙健康设备进行通信。
BluetoothProfile.ServiceListener
通知BluetoothProfile进程间通信的接口,当他们与服务器连接上或者中断连接(也就是内部服务已经在某个设备上运行了,翻译不对啊)

蓝牙授权 Bluetooth Permissions


为了在你的应用程序中使用蓝牙,你必须声明蓝牙权限BLUETOOTH。 你需要这个权限来执行任何蓝牙通讯,例如请求一个连接、接收一个连接、传输数据。

如果你需要你的应用程序能够发现周围的设备或者修改蓝牙设置,你还必须声明BLUETOOTH_ADMIN权限。

大部分应用程序需要此唯一的权限来实现搜索本地蓝牙设备的功能。该权限还可以授权应用程序在用户要求下修改蓝牙设置。注意:如果你在应用中引入了BLUETOOTH_ADMIN权限,那么你必须同样要声明引用BLUETOOTH权限。

在应用中声明这些权限如下所示:

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

 <uses-permission>中查看更多如何在应用中声明权限的信息。

Setting Up Bluetooth


图1 开启蓝牙对话框.

设置蓝牙

在你的应用可以通过蓝牙通讯之前,你必须确认蓝牙在设备上可用,如果可用,还需要保证蓝牙已经打开了。

如果该设备不支持蓝牙,那么你必须在应用中关掉所有蓝牙相关的特色功能。如果支持,但是没有打开,你可以子啊应用中请求用户打开蓝牙。该步骤可以使用BluetoothAdapter在两步之内完成。

  1. 获取蓝牙适配器

    所有的蓝牙活动都需要一个BluetoothAdapter。 使用静态的getDefaultAdapter()方法获取蓝牙适配器(BluetoothAdapter)。该方法将返回一个蓝牙适配器(BluetoothAdapter),该对象代表设备自己的蓝牙适配器(蓝牙无线连接)。有一个针对整个系统的蓝牙适配器,你可以使用这个对象与其交互。如果getDefaultAdapter()返回null,那么该设备不支持蓝牙,到此结束了。代码如下:

 BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();

if (mBluetoothAdapter == null) {
    
// Device does not support Bluetooth设备不支持蓝牙,就是说没有蓝牙
}

  1.  开启蓝牙
    下一步,你需要开启蓝牙。调用函数isEnabled()检查当前蓝牙是否开启。 如果返回false,那么说明蓝牙是关闭的。如果请求开启蓝牙,使用带动作ACTION_REQUEST_ENABLE的意图,调用startActivityForResult()函数。这个操作将通过系统设置(而不用中断自己的应用)发出开启蓝牙的请求。代码如下:
    if (!mBluetoothAdapter.isEnabled()) {
        
    Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
        startActivityForResult
    (enableBtIntent, REQUEST_ENABLE_BT);
    }

    一个要求用户授权的对话框将显示出来,如图1所示。如果用户点击“Yes”,系统将会开启蓝牙,并且界面将会回到你的应用程序中。

    传递给函数 startActivityForResult()的常量通常定义为一个大于零的整数, 系统将返回该值,在函数onActivityResult()中,包含在变量requestCode中。

    如果开启蓝牙成功,你的活动将在回调函数onActivityResult()中,收到表示成功的返回码RESULT_OK。如果由于某个错误(比如用户选择“No”,拒绝了授权),导致蓝牙并没有开启,那么将会返回错误码RESULT_CANCELED

还有另外的途径,你的应用可以监听带有动作ACTION_STATE_CHANGED(蓝牙状态改变)的广播意图,这个意图在蓝牙状态改变的时候被系统广播。该广播包含两个属性EXTRA_STATEEXTRA_PREVIOUS_STATE,相对应的包含了蓝牙的新状态和旧状态。这两个属性的可能值为STATE_TURNING_ONSTATE_ONSTATE_TURNING_OFF或者STATE_OFF。在你的应用程序运行的时候,接收该广播,可以很方便的获取到蓝牙状态的改变。

提示:开启可发现功能,将自动启动蓝牙。如果你决定一直开启设备的可发现功能在执行蓝牙活动之前,你可以跳过第二步。阅读下面与enabling discoverability相关的内容,获取跟多信息。

搜寻设备(Finding Devices)


使用蓝牙适配器(BluetoothAdapter),你可以通过寻找设备或者从已配对的设备列表中,找到远端的蓝牙设备。

搜寻设备是一个扫描流程,它会寻找附近的打开了蓝牙的设备,并且从这些设备中获取一些信息(我们也称之为发现、询问、扫描)。然而,只有当某蓝牙设备被设置成可发现时,才能够被其他蓝牙设备搜寻到。当一台蓝牙设备被设置为可发现时,它才会对附近的蓝牙扫描请求回应,回应信息包含设备名、类、唯一的MAC地址。通过这些信息,两台设备之间才能够初始化设备间的连接。

一旦与远程设备的连接第一次建立,一个配对请求就会自动显示在用户面前。当一台设备配对后,设备的基本信息(设备名、类、MAC地址等)机会被保存下来,而且可以使用蓝牙API了。使用已知的远程设备的MAC地址(在配对的时候就远程地址已经保存了),可以在任何时候初始化一个连接而不用再次搜寻了(译者注:我们假设设备还在有效范围内,另外一种情况是设备超出了有效范围,但是同样会在蓝牙设备的列表中)。

记住,被配对和被连接是不同的。被配对意味着两台设备都知晓另外一台设备的存在,有用来互相授权的相同的分享链接码(可能就是配对码了?),而且还可以建立连接两者的加密连接。被连接表示设备间分享同一个RFCOMM信道,并且可以互相传送数据(译者注:比如传个mp3啥的)。目前的Android平台的蓝牙API要求在建立RFCOMM信道之前先配对(配对动作会在你使用蓝牙API初始化一个加密连接的时候自动调用)。

下面的小结将描述如何发现已配对的设备,或者使用搜索设备功能发现新设备。

注意:Android蓝牙设备默认是不可发现的。用户可以在系统设置里面设定限定时间的设备可发现属性,或者应用程序可以要求用户授权在该应用内蓝牙都是可发现的。下面将会讨论如何使设备可发现(enable discoverability)。

询问已配对设备(Querying paired devices)

在执行搜索设备之前,最好先查询已配对设备列表,看看我们要连接的设备是否已经在列表中。可以通过函数getBondedDevices()来达到这个目的。 这个函数将返回一系列包含已配对蓝牙设备(BluetoothDevice)的列表。例如,你可以查询所有已配对过的设备,并使用ArrayAdapter来显示出来,如下代码所示:

Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices();
// 如果有已配对过的设备
if (pairedDevices.size() > 0) {
    
// 循环该列表
    
for (BluetoothDevice device : pairedDevices) {
        //将名称和地址加入一个数组适配器,并显示在一个列表视图中
        mArrayAdapter.add(device.getName() + "\n" + device.getAddress());
    
}
}

为了初始化一个连接,我们唯一需要的是从BluetoothDevice 中获取的MAC地址。在这个例子中,地址被作为数组适配器的一部分被显示给用户了。MAC地址后面可以用来精确的初始化一个连接。在Connecting Devices节中,你可以学到更多关于如何创建连接的知识。

搜索设备(Discovering devices)

简单的调用函数startDiscovery()就可以开始搜索附近的蓝牙设备了。该过程是一个异步过程,并且当搜索过程成功启动之后,就会立即返回一个布尔值的变量。该搜索过程通常包含12秒钟的查询扫描inquiry scan,不懂翻啊,随后包含一个返回所有蓝牙设备名称的呼叫扫描(page scan,不懂翻啊)。

当发现一个设备之后,系统就会发送广播,所以你的应用程序必须包含一个含有ACTION_FOUND意图的广播接收器来接收信息。对每个已发现设备来说,系统会广播ACTION_FOUND意图。该意图还携带有额外的值域: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地址。在这个例子中,MAC地址作为显示给用户的部分数据,被保存在数组适配器中了。该MAC地址随后被用来初始化连接。Connecting Devices节中,你可以学到更多关于如何创建连接的知识。

注意:执行搜索操作对于蓝牙适配器来说是一个非常耗时的操作,而且会消耗很多资源。一旦你找到了需要连接的设备,在视图连接之前,最好立刻停止搜索,停止搜索可以使用cancelDiscovery()函数。同样地,如果你同一个设备已经连接上,如果再开启搜索,将会减少已有连接的可用带宽,所以当连接的时候,最好不要执行扫描操作。

使设备可发现(Enabling discoverability)

使设备可发现(也就是说允许设备被扫描到)

如果你想允许设备被扫描到,调用startActivityForResult(Intent, int)函数,设置其中的意图的动作为ACTION_REQUEST_DISCOVERABLE。该函数将通过系统设置将设备变为可被发现(该操作不需要退出你的应用界面),并保持12秒钟的该状态。可以通过该意图中额外的值域EXTRA_DISCOVERABLE_DURATION 定义不同的可被发现时间。应用程序可以设置的最大时间为3600秒(1小时), 值0表示设备一直可被发现。任何大于3600秒或者小于0的值,都会被默认为120秒。比如下面的代码将会设置时间为300秒:

Intent discoverableIntent = new
Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverableIntent
.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
startActivity
(discoverableIntent);
Figure 2: 开启授权允许发现对话框。

当设置这个时间的时候,如图2的对话框会弹出提示用户。如果用户选择“Yes”,设备将变为可发现状态并保持300秒。你的应用程序中的当前活动将会回调函数onActivityResult(),返回的值就是设备设置的可发现时间。如果用户选择“No”,或者有一个错误发生时,返回值就是RESULT_CANCELED

注意:如果蓝牙为开启,那么设置设备为可发现的时候就会自动开启蓝牙。(一般是在应用程序中才会有这个流程,系统设置中都是先开启蓝牙,再设置可被发现的)

在指定时间内,设备都会保持可被发现状态。如果你想在该状态发生改变的时候收到通知,只需要使用包ACTION_SCAN_MODE_CHANGED含动作的意图注册一个广播接收器即可。该意图包含额外的值域 EXTRA_SCAN_MODEEXTRA_PREVIOUS_SCAN_MODE,这两个分别告诉新的和旧的扫描模式。扫描模式的可能值有SCAN_MODE_CONNECTABLE_DISCOVERABLE(两台设备都是可发现状态)SCAN_MODE_CONNECTABLE(不在可发现状态,但是依然可以接受连接)或者SCAN_MODE_NONE(不在可发现状态,同样也不能接受连接)

如果你初始化一个连接到远程的设备,那么就不需要开启设备的可发现属性。只有在你需要自己的应用程序维持一个可以接受客户端连接请求的服务端时,才需要开启设备的可发现性(译者注:也就是说某台设备想连接到你的设备,那么你的设备就需要设置为连接属性)。因为远程服务器必须发现你的设备才能够初始化连接。

连接设备(Connecting Devices)


为了通过你的应用在两台设备间创建连接,你必须同时实现服务器端和客户端机制,因为一台设备必须作为服务器端口,另外一台必须初始化连接(使用服务器设备的MAC地址类初始化连接,这个过程跟TCP的连接开启类似)。如果服务器端和客户端都通过同一个RFCOMM信道,使用BluetoothSocket连接到一起,那么我们就认为它们成功建立了一个连接。这样,每台设备都可以输入输出数据,并且可以开始传输数据。细节将在管理连接(Managing a Connection)这一节中详细描述。该小节描述了如何初始化两台设备之间的连接。

服务器端和客户端获取BluetoothSocket的方式不同。服务器端在有连接请求的时候获得,而客户端在它打开一个通向服务器端的RFCOMM信道时获得。

Figure 3: 蓝牙配对对话框。

有一个已经实现的技术就是自动将每个设备都作为服务器,所以每个设备都打开一个服务器端口,并监听连接请求。这样每个设备都可以跟另外一台设备初始化一个连接,并变成客户端。另外一个选择是,一台设备可以明确声明为服务器端,并且打开一个服务器端口,这样其他设备就可以很简单的初始化连接了。

注意:如果两台设备之前并没有配对过,那么Android系统会自动显示一个请求配对的对话框给用户,如图3所示。所以当你的应用请求连接的时候,两台设备并不要求已连接或者已配对。在两台设备配对成功之前,你的RFCOMM信道将会一直堵塞。如果用户拒绝配对或者配对超时的话,会返回配对失败信息。

作为服务器端(Connecting as a server)

如果你想连接两台设备,那么其中一台必须作为服务器端,并且打开一个蓝牙服务器端口(BluetoothServerSocket)。服务器端口的目的是监听连接请求,当接受该请求的时候,提供一个已连接的BluetoothSocket。当从BluetoothServerSocket获取到BluetoothSocket时,就可以被丢弃BluetoothServerSocket了,除非你想接收更多连接。

下面是设置服务器端口和接受连接时的基本流程:

  1. 调用函数listenUsingRfcommWithServiceRecord(String, UUID)获取BluetoothServerSocket

    参数里面的字串是你的设备一个可辨识的名称,系统会自动将他写入一个新的服务发现协议(Service Discovery Protocol SDP)数据库条目(名称任意,可以使用你自己的应用名称)。SDP条目也包含UUID,这个id将是与客户端连接的基础。也即是,当客户端视图连接本设备时,请求中必须包含唯一的UUID标识符,用来标识目的服务器。如果想连接成功,那么UUID必须匹配(下一步骤中将用到)。

  2. 调用accept()监听连接请求

    这是一个阻塞调用。当连接请求被接受或者有异常发生时将返回。当远程设备发送的连接包含的UUID匹配到该服务器端口注册时候使用的UUID时,该连接就建立成功了。当连接成功的时候,accept()将返回一个已连接的蓝牙端口(BluetoothSocket)。

  3. 如果你不需要接受额外的连接,那么调用close()

    这个函数将释放服务器端口和它占用的资源,但是不会关闭从accept()返回的已连接蓝牙端口(BluetoothSocket)。不像TCP/IP中,RFCOMM的信道一次仅允许一个客户端连接,所以在绝大多数情况中,在接受了连接请求之后,就会立即在BluetoothServerSocket中调用close(),关闭端口并释放资源。

因为accept()函数会阻塞进程,并且会影响应用中其他的内部交互,所以最好不要在主线程(UI线程)中调用。最好应用程序另开一个线程,并且在该线程中通过BluetoothServerSocketBluetoothSocket来调用该函数。可以通过在另外线程中通过BluetoothServerSocket(或者BluetoothSocket)调用close(),可以中断类似accept()阻塞函数,并且立即返回。注意,BluetoothServerSocketBluetoothSocket中所有的函数都是线程安全的。

例子:

下面是一个简化的线程,服务器端可以使用它来接受连接请求:

 
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()了。(不用像你从客户端调用connect()那样)

在应用程序中,manageConnectedSocket()方法将初始化一个线程来传输数据,该方法将在Managing a Connection小节中详细讨论。

在完成了监听连接请求之后,一般你就需要关闭你的BluetoothServerSocket。在本例中,在获取到BluetoothSocket之后,就马上调用close()函数了。也许你依然想要在你的线程中提供一个公共方法来关闭私有的 BluetoothSocket,你也需要在服务器端停止监听。

作为客户端进行连接(Connecting as a client)

为了与远程设备(该设备作为服务器端,提供一个服务器端口)初始化一个连接(),你必须获取一个逻辑上代表远程设备的BluetoothDevice。(在上一节的Finding Devices中已经讲述了如何获取一个BluetoothDevice了。)

下面是基本流程:

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

    它将初始化一个连接到蓝牙设备(BluetoothDevice)的BluetoothSocket。这里的UUID必须跟服务器端打开的服务器端口(BluetoothServerSocket)之后获取的UUID相同。通过简单的将UUID硬编码到你的应用程序中,并且在服务器端和客户端都引用它。

  2. 通过调用connect()初始化连接。

    调用完这个函数之后,系统将在远程设备执行一个服务发现协议(SDP)为了获取UUID。

    因为connect()是一个阻塞调用,所以这个连接过程必须在非UI线程中运行。

    注意:必须确保你在调用connect()时,你的设备没有在执行搜索其他设备的操作(译者注:在android中的表现就是,如果你在搜索设备的同时连接已经在列表中的设备,并且对方设备同样也在搜索,那么操作会卡在那里。在程序员调试两台设备的时候会出现这样的情况)。如果搜索正在进行中,那么连接操作就会被延后,看起来更像是连接失败。

例子

下面的例子将简单描述,在一个线程中如何初始化一个蓝牙连接:

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()了。在每次你执行连接之前,必须调用该函数,并不需要检测搜索操作是否正在运行也可以安全的调用该函数(当然你如果想要检测是否搜索正在运行,调用函数isDiscovering())。

在管理一个连接(Managing a Connection)小节中讨论过的manageConnectedSocket(),在应用中该函数将会初始化传输数据的线程。

当你用完BluetoothSocket时,一定要调用close()关闭连接。

管理连接(Managing a Connection


如果成功连接了两台(或者更多)设备,每台设备就都会拥有一个已连接的BluetoothSocket,这样你就可以通过它来共享数据了。使用BluetoothSocket来传输任意数据的流程如下:

  1. 使用getInputStream()和getOutputStream(),获取InputStream和OutputStream来处理socket间的数据传输。
  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) { }
    
}
}

构造器需要一个必要的流,一旦执行了之后,线程将等待从输入流传入数据。当read(byte[])从输入流获取到数据的时候,父类的一个处理函数会将数据传到主活动中去。然后返回等待从流中获取更多数据。

发送数据最简单的方法莫过于在主活动内调用线程内的write()方法。该方法将会直接调用write(byte[])将数据发送到远程设备。

线程的cancel()方法非常重要,可以在任何时候用来关闭BluetoothSocket。当你使用完蓝牙连接之后,请记得一定要调用此方法。

如果想要看蓝牙API相关的实例,查看蓝牙聊天示例程序(Bluetooth Chat sample app)。

使用不同的蓝牙模式(Working with Profiles)


使用不同的蓝牙模式(译者注:比如免提模式、无线耳机模式、健康设备模式等,请意会。。)

从Android 3.0平台开始,蓝牙API开始支持不同的蓝牙模式。 一个蓝牙模式是指一个不同设备间,支持基于蓝牙通讯的特定无线蓝牙接口。比如说免提模式。如果一个移动手机需要连接到一个无线耳机,那么双方必须都支持免提模式。

你可以通过实现BluetoothProfile接口来支持你自己的特定蓝牙模式。

  • 无线耳机:该模式支持在移动电话上使用蓝牙耳机。Android提供BluetoothHeadset类,该类作为一个代理,通过进程间通信 (IPC)使用蓝牙无线服务。他包含蓝牙耳机和免提 (v1.5)模式。该类还支持AT命令。想了解更多关于AT命令的内容,参考Vendor-specific AT commands
  • A2DP:高级音频分配模式(A2DP)定义了通过蓝牙连接可以将多高质量的音频流从一台设备传到另一台。同样Android提供了BluetoothA2dp类,通过该代理类,使用进程间通信的方式可以使用蓝牙的A2DP服务。
  • 健康设备:Android 4.0(API Level 14)支持蓝牙健康设备模式(HDP)。该模式将运行你创建一个应用程序,可以使用蓝牙与支持蓝牙功能的健康设备进行通信,比如说心率监控器、血压计、温度计、刻度尺等等。可以在www.bluetooth.org网站了解更多相关的支持的健康设备以及对应的蓝牙分配码。这些值同样在ISO/IEEE 11073-20601 [7]的系统编码附录中也包含,并且命名形式如MDC_DEV_SPEC_PROFILE_*。如果想讨论更多关于HDP的信息,查看Health Device Profile

下面是如果使用模式的基本步骤:

  1.  获取默认适配器,就像设置蓝牙(Setting Up Bluetooth)小结所述。

2. 使用getProfileProxy()获取与特定模式代理的连接。下面的实例中,该模式代理对象是BluetoothHeadset的实例。

3. 设置一个蓝牙服务监听器( BluetoothProfile.ServiceListener)。当客户端与服务器连接或者断开连接的时候,将会通知客户端(BluetoothProfile)。

4. 在onServiceConnected()中,获取一个处理模式代理对象的句柄。

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命令(Vendor-specific AT commands)

从Android 3.0开始,应用程序可以注册广播接收器,接受系统发送的预定义厂商AT命令,这些命令由厂商提供的耳机设备发出,例如缤特力(Plantronics)公司的XEVENT命令。比如,应用程序可以接受关于设备电力的广播,并且通知用户或者采取其他操作。创建针对ACTION_VENDOR_SPECIFIC_HEADSET_EVENT的广播接收器,来获取厂商特定无线耳机的AT命令。

健康设备模式(Health Device Profile)

Android 4.0(API Level 14)介绍并支持蓝牙健康设备模式(HDP)。该模式将运行你创建一个应用程序,可以使用蓝牙与支持蓝牙功能的健康设备进行通信,比如说心率监控器、血压计、温度计、刻度尺等等。蓝牙健康设备API包括如下在前面的基本章节中介绍过的类:BluetoothHealthBluetoothHealthCallbackBluetoothHealthAppConfiguration。

在使用蓝牙健康API中,了解下面的HDP相关的键将会非常有帮助。

概念描述HDP中定义的角色。源就是一个健康设备,它将医疗数据(比如重量、血糖、体温)传输到一个智能设备,比如android手机或者平板。目的HDP中定义的一个角色。在HDP中,目的是接收医疗数据的智能设备。在一个Android的HDP应用中,目的用对象BluetoothHealthAppConfiguration来表示。注册针对特定的健康设备的注册目标的引用。连接一个已打开信道的引用,这个信道是连接健康设备和智能设备,比如一个Android手机或者平板。

创建一个HDP应用

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

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

    类似于无线耳机和A2DP模式设备,必须在BluetoothProfile.ServiceListener中调用BluetoothProfile.ServiceListener,并使用HEALTH模式类型来获取一个与模式代理对象的连接。

  2. 创建一个BluetoothHealthCallback并且注册一个应用配置 (BluetoothHealthAppConfiguration) 来作为一个健康目的。(译者注:翻译不太对啊,原文如,Create a BluetoothHealthCallback and register an application configuration (BluetoothHealthAppConfiguration) that acts as a health sink.)
  3. 建立一个到健康设备的连接。一些设备会初始化改连接。通常在这一步中并不是必须要实现的。
  4. 当建立到健康设备的连接成功之后,使用文件描述符的形式向健康设备读取和写入数据。

    使用实现了IEEE 11073-xxxxx要求的健康设备管理器来解释接收到的数据。

  5. 当完成这些之后,关闭建立的信道并且注销该应用。当有延伸的非活动时,信道也会关闭(译者注:貌似翻译不太对啊,原文 ,The channel also closes when there is extended inactivity.)。

如果要看实现了这些不走的演示程序,查看Bluetooth HDP (健康设备模式)











0 1
原创粉丝点击