Android 經典藍牙用法

概述:

Android平臺包括了對藍牙網絡協議棧的支持, 它讓設備可以跟其它藍牙設備實現無線數據交換. 應用框架通過Android Bluetooth API提供了訪問藍牙的功能. 這些API可以讓APP無線連接到其它的藍牙設備, 可以使用點對點和多點無線功能. 通過使用藍牙API, 一個Android APP可以實現這些功能:

l  掃描其它的藍牙設備

l  爲配對的藍牙設備查詢本地藍牙適配器

l  建立RFCOMM通道

l  通過”服務發現”連接到其它設備

l  與其它設備交換數據

l  管理多連接

本文描述瞭如何使用經典藍牙. 經典藍牙是電池密集型操作的正確選擇, 比如流和Android設備之間通信. 爲了實現藍牙設備低耗電的需求, Android4.3引入了支持Bluetooth Low Energy的API. 該功能可以參考這裏. 將在下一篇blog中介紹.

基礎:

該文檔描述瞭如何使用Android Bluetooth API來完成四種使用藍牙通信的主要任務: 設置藍牙, 在本地區域搜索配對或者可用的設備, 連接設備, 和設備間交換數據. 所有Bluetooth API都在android.bluetooth包中. 這裏列舉了包中我們需要用到的類和接口的總覽:

1.      BluetoothAdapter: 代表本地藍牙適配器(藍牙無線電(radio)).BluetoothAdapter是藍牙交互的入口. 通過它我們可以發現其它的藍牙設備, 查詢配對設備的列表, 使用一個已知的MAC地址實例化一個BluetoothDevice, 還可以創建一個BluetoothServerSocket來監聽其它設備發來的通信.

2.      BluetoothDevice: 表示一個遠程的藍牙設備. 使用它可以請求一個與遠程設備通過BluetoothSocket建立的鏈接, 或者用來查詢設備的相關信息, 比如名字, 地址, 類, 和配對狀態.

3.      BluetoothSocket: 表示爲藍牙socket的接口(類似於TCPSocket). 它是一個連接點, 允許一個APP跟其它的藍牙設備通過InputStream和OutputStream來交換數據.

4.      BluetoothServerSocket: 代表一個開放的服務器socket,它持續的監聽連接進來的需求(類似於一個TCP ServerSocket). 爲了連接兩個Android設備, 一個設備必須用該類開啓一個服務器socket. 當一個遠程藍牙設備請求連接該設備的時候, BluetoothServerSocket將會在連接建立後返回一個連接了的BluetoothSocket.

5.      BluetoothClass: 描述藍牙設備的一般特徵和能力. 這是一組只讀的屬性, 定義了設備的主要和次要設備類和它的服務. 然而它對所有藍牙配置文件和服務的描述並不是很牢靠, 但作爲設備類型的參考還是有用的.

6.      BluetoothProfile: 一個代表藍牙配置文件(Bluetoothprofile)的接口. 藍牙配置文件是一個設備間基於藍牙通信的無線接口規範. 有一個栗子是Hands-Free(免提)配置文件. 更多信息可以參考”通過配置文件工作”小節.

7.      BluetoothHeadset: 爲移動電話的耳機提供支持.包括藍牙耳機和Hands-Free(v1.5)配置文件.

8.      BluetoothA2dp: 定義高質量音頻如何從一個設備流傳輸到另一個設備. “A2DP”代表Advanced Audio Distribution Profile.

9.      BluetoothHealth: 代表一個控制藍牙服務的健康設備配置文件代理.

10.  BluetoothHealthCallback: 一個抽象類, 我們可以用它來實現BluetoothHealth的回調. 要使用它則必須要繼承該類並實現回調方法來接收關於APP註冊狀態和藍牙通道狀態的更新.

11.  BluetoothHealthAppConfiguration:代表一個藍牙健康第三方應用註冊到了一個遠程藍牙健康設備的應用配置.

12.  BluetoothProfile.ServiceListener:一個接口, 用於BluetoothProfile IPC客戶端與服務連接或者斷開連接的時候發出提醒.

藍牙權限:

爲了在APP中使用藍牙功能, 我們必須聲明藍牙權限:BLUETOOTH. 執行任何的藍牙通信, 比如請求連接, 接收連接, 數據交換, 都需要該權限. 如果想要我們的APP要啓動設備發現或者操作藍牙設置, 就必須聲明BLUETOOTH_ADMIN權限. 大多數APP需要這個權限僅僅是爲了發現本地藍牙設備. 其它被該權限授權的能力不該被使用, 除非APP是”電源管理器”類的, 會根據用戶的需求修改藍牙設置. 注意: 如果使用了BLUETOOTH_ADMIN權限, 那麼比需也聲明BLUETOOTH權限. 栗子:

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

設置藍牙:

在使用藍牙連接和通信之前, 我們必須確認設備可以支持藍牙, 如果支持的話, 還要確認是否目前是啓用的. 如果不支持藍牙, 那麼應該合理的關閉任何藍牙相關的功能. 如果支持藍牙, 但是目前藍牙處於關閉狀態, 那麼我麼你可以在不離開APP的情況下請求用戶啓動藍牙. 這個操作需要個步驟, 並且使用BluetoothAdapter類:

1.      得到BluetoothAdapter: 所有的藍牙activity都需要BluetoothAdapter. 想要得到它需要調用getDefaultAdapter()方法. 該方法返回一個BluetoothAdapter, 代表設備本身的藍牙適配器(藍牙無線電). 整個系統有一個藍牙適配器, 並且APP可以使用該對象跟它通信. 如果getDefaultAdapter()方法返回了null, 那麼代表設備不支持藍牙, 接下來也就沒啥事可做了. 栗子:

BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (mBluetoothAdapter == null) {
    // Device does not support Bluetooth
}

2.      啓動藍牙: 下一步我們就該啓動藍牙了. 調用isEnabled()方法來查看藍牙當前是否在啓動着. 如果返回false, 那麼表示藍牙關閉. 這時候如果想要啓動藍牙, 調用startActivityForResult()方法, 並傳給它ACTION_REQUEST_ENABLEaction intent. 這將會通過系統設置發出一個請求啓動藍牙的操作. 栗子:

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

這時候將會彈出一個對話框讓用戶選擇是否啓動藍牙. 如果用戶選擇了”是”, 系統將會開啓藍牙, 成功(或者失敗)後會將焦點返回給APP. 栗子中的REQUEST_ENABLE_BT常量是一個大於0的數字, 它將在onActivityResult()中作爲requestCode返回給我們作爲參數. 如果成功啓動了藍牙, activity將會在onActivityResult()中收到一個RESULT_OK的結果. 如果失敗了, 將會收到RESULT_CANCELED.

還有一個可選項我們可以做的, APP可以監聽ACTION_STATE_CHANGED廣播intent, 當藍牙狀態改變的時候, 系統將會廣播它. 該廣播包含額外的域EXTRA_STATE和EXTRA_PREVIOUS_STATE, 分別包含新老藍牙狀態. 這些額外的域可能包含以下值: STATE_TURNING_ON, STATE_ON, STATE_TURNING_OFF, 和STATE_OFF. 如果關注藍牙狀態的話, 監聽該廣播會很有用.

啓動”可以被發現”功能會自動開啓藍牙. 如果我們打算在執行藍牙activity前持續的啓動設備發現功能, 可以跳過上面的兩步. 後文詳述.

查找設備:

通過BluetoothAdapter, 我們可以通過設備發現和查詢配對列表找到遠程藍牙設備.

設備發現是一個掃描程序, 可以搜索附近區域啓動了藍牙的設備並獲取各個設備的信息. 但是隻有當該遠程設備啓動了”被發現”功能的時候纔有可能被發現. 如果一個設備是可發現的, 那麼它將會通過共享信息來回應發現需求, 比如設備的名字, 類, 和MAC地址. 使用這些信息, 就可以選擇啓動與該設備的連接了.

一旦首次建立了跟遠程設備的連接, 一個配對需求會自動展現給用戶. 當設備配對了之後, 關於該基礎信息就會被保存並可以使用藍牙API讀取到. 使用其中的MAC地址就可以啓動一個與它的連接而不用再次處理髮現了(假如設備是在範圍內的).

配對和連接之間有一個不同. 配對意味着兩個設備都知道對方的存在, 有一個共享的link-key在認證的時候可以用, 並可以建立彼此的加密連接. 而”連接上”則意味着設備當前共享一個RFCOMM通道並可以交換數據. 當前Android Bluetooth API要求設備進行RFCOMM連接建立之前需要先進行配對(當通過Bluetooth API啓用一個加密連接的時候, 配對將會被自動執行).

下面的小節將會介紹如何找到已經配對的設備以及如何用設備發現功能發現新的設備.

注意: Android設備默認情況下是不可發現的. 用戶可以通過系統設置讓設備可以被發現一段時間, 或者APP可以請求用戶開啓發現功能.

查詢配對設備:

在執行設備發現之前, 查詢一下要連接的設備是否在已經配對的設備列表中是有必要的. 想要實現這個操作, 調用getBonderDevices()方法. 該方法會返回一組BluetoothDevice來描述配對設備. 慄如, 我們可以查詢所有的配對設備, 然後用一個列表展示給用戶, 用一個ArrayAdapter:

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());
    }
}

爲了啓動一個連接而從BluetoothDevice對象中獲得的所有東西就是一樣: MAC地址. 在該栗子中, 它被保存在一個ArrayAdapter中以便用來展示給用戶看. MAC地址可以稍後被提取出來用於啓動連接.

發現設備:

想要啓動發現設備功能, 只要調用startDiscovery()就可以了. 該過程是同步的, 它會直接返回一個boolean值來表示發現功能是否成功啓用. 發現過程通常需要12秒鐘來查詢掃描, 然後每個發現的設備會用發現的設備名列出來. 我們的APP必須爲ACTION_FOUND intent註冊一個BroadcastReceiver, 這樣可以接收關於每個設備發現的數據. 對於每個設備, 系統都會廣播一個ACTION_FOUND的Intent. 這個intent會攜帶額外的數據域EXTRA_DEVICE和EXTRA_CLASS, 分別包括一個BluetoothDevice和一個BluetoothClass. 慄如, 這是一段關於如何註冊處理設備發現的broadcast的代碼:

// 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

跟之前的栗子一樣, 我們得到了MAC地址並將其保存起來. 注意, 處理設備發現是一個繁重的任務將會消耗很多資源. 一旦發現了要連接的設備, 請確保嘗試連接之前用cancelDiscovery()方法停止發現. 另外, 如果已經與一個設備連接上了, 這時候執行發現動作將會顯著降低連接的可用帶寬, 所以我們應該在連接的時候不執行發現操作.

啓用發現:

如果想要開啓本地設備的被發現功能, 調用startActivityForResult(Intent, int)方法並傳入ACTION_REQUEST_DISCOVERABLEaction intent. 這將會通過系統設置發出一個需求來啓動可發現模式(而不必停止我們自己的APP). 默認情況下, 設備將會開啓可發現功能120s. 我們可以通過EXTRA_DISCOVERABLE_DURATION intent extra來定義一個延時時間長度. 最大的開啓被發現功能的時間長度是3600秒, 如果設置爲0, 則表示永遠開啓. 任何不在這個範圍內的值會被設置爲120秒. 慄如, 這是設置爲300秒的栗子:

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

這時將會顯示一個對話框請求用戶權限來使設備可發現. 如果用戶點擊了”是”, 那麼設備將會被發現指定時長. 然後我們的activity將會在onActivityResult()中收到結果, 返回的result code等於延時時長. 如果用戶點擊了”否”或者這中間發生了錯誤, result code將會是RESULT_CANCELED. 如果藍牙沒有開啓的話, 啓動藍牙發現功能會自動開啓藍牙.

設備將會默默的在指定時間內保持在可發現模式. 如果我們想要在發現模式發生變化的時候收到提醒, 則需要註冊一個廣播用來接收ACTION_SCAN_MODE_CHANGED intent. 這將會包含額外數據域EXTRA_SCAN_MODE和EXTRA_PREVIOUS_SCAN_MODE,它們將分別告知我們新老掃描模式. 可能的取值是SCAN_MODE_CONNECTABLE_DISCOVERABLE, SCAN_MODE_CONNECTABLE, 或者SCAN_MODE_NONE,它們分別表示設備時處於可發現模式, 不在可發現模式但是依然可以收到連接, 或者不可發現模式並不能收到連接.

如果我們想要對一個遠程設備發起連接, 我們並不需要啓用設備發現. 只有在APP要打開一個server socket並接受連接的時候, 我們纔有必要啓動設備發現功能. 因爲只有這樣才能被遠程設備發現並連接到我們.

連接設備:

爲了創建APP在兩臺設備之間的連接, 我們必須要實現服務器端和客戶端機制, 因爲一個設備必須打開server socket, 另一個設備必須啓動連接(使用server的MAC地址來發起連接). 當客戶端和服務端擁有一個通過相同RFCOMM通道連接的BluetoothSocket的時候就可以連接了!(大概是這麼個意思. . .). 在此刻, 每個設備可以獲取輸入和輸出流, 可以開始交換數據, 這些將在後面的小節講到, 本節介紹如何連接兩個設備.

服務器設備和客戶端設備都會用不同的方法取得BluetoothSocket. 服務器將會在接受請求連接的時候收到BluetoothSocket. 客戶端則會在向服務器打開一個RFCOMM通道的時候收到.

一種實現的方法是自動準備每個設備爲服務器端, 這樣每個設備都有一個服務器socket開啓並監聽連接. 然後每個設備都可以發起連接成爲客戶端. 另外還有就是一臺設備明確的做主機並在需要的時候打開一個server socket, 然後另一臺設備只需要連接就可以了.

注意: 如果兩臺設備之前不是配對設備, 那麼Android framework將會自動的在連接過程中展示一個配對需求提示或者對話框給用戶. 所以當嘗試連接到設備的時候, 我們的APP不需要確認是否已經配對. RFCOMM連接嘗試將會在用戶選擇成功配對之前阻塞, 或者用戶選擇不配對的時候失敗.

作爲服務器連接:

當我們想要連接兩個設備, 其中一個必須扮演服務器角色, 並持有一個打開的BluetoothServerSocket. 它的用途是監聽接入的連接需求, 當有一個被接受的時候, 提供一個連接的BluetoothSocket.當從BluetoothServerSocket中得到BluetoothSocket後, BluetoothServerSocket可以(應該)被釋放, 除非我們想要接受更多的連接.下面是設置server socket和接受連接的基本步驟:

1.      通過listenUsingRfcommWithServiceRecord(String, UUID)得到一個BluetoothServerSocket.方法中的string是一個服務的識別名字, 系統將會自動寫入一個Service Discovery Protocol(SDP)數據庫(這個名字是隨意的, 可以簡單的使用APP的名字). UUID也是包含在SDP中的, 並且是連接客戶端設備協議的基礎. 就是說, 當客戶端嘗試連接該設備的時候, 它將會攜帶一個UUID, 這個UUID用來識別唯一的它想要連接的服務. UUID必須匹配才能建立連接.

2.      通過accept()方法啓動監聽. 這是一個阻塞方法. 它不管連接已經被接受或者是有異常, 都會直接返回. 只有遠程設備發起連接請求並且攜帶一個匹配的UUID的時候連接纔會被接受. 當成功之後, accept()將會返回一個連接的BluetoothSocket.

3.      除非我們還需要接受更多的連接, 否則調用close(). 該方法將會釋放sever socket和它的所有資源, 但是不會關閉accept()返回的已經連接的BluetoothSocket. 不像TCP/IP, RFCOMM每個通道一次只允許一個客戶端連接, 所以大多數情況下都應該在連接建立之後對BluetoothServerSocket直接調用close()方法.

accept()方法不應該在主activity UI線程中調用, 因爲它是一個阻塞方法, 會阻止任何其它的app交互. 它通常應該在一個新的線程中跟一個BluetoothServerSocket或者BluetoothSocket合作工作. 想要終止這個阻塞方法的話, 需要從另一個線程調用BluetoothServerSocket(或者BluetoothSocket)的close()方法, 這樣所有的阻塞方法就會直接返回了. 注意所有的BluetoothServerSocket和BluetoothSocket方法都是線程安全的.

這裏是一個簡單的server組件接收連接的栗子:

privateclass AcceptThreadextends Thread{
    private finalBluetoothServerSocket 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, alsoused 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 (ina separate thread)
                manageConnectedSocket(socket);
                mmServerSocket.close();
                break;
            }
        }
    }
 
    /** Will cancelthe listening socket, and cause the thread to finish */
    public void cancel(){
        try {
            mmServerSocket.close();
        } catch(IOException e){ }
    }
}

在該栗子中, 只期望一個接入連接, 所以一旦有一個連接接入了並得到了BluetoothSocket, APP就會發送這個BluetoothSocket到一個獨立的線程, 關閉BluetoothServerSocket並停止循環. 注意當accept()返回BluetoothSocket, socket已經連接, 所以我們不應該調用connect()方法. ManageConnectedSocket()是一個虛構的方法, 它將會實例化一個線程用於交換數據. 將在下面的小節討論它.

我們應該保持在結束監聽接入連接後立刻關閉BluetoothServerSocket. 在該栗子中, close()在BluetoothSocket獲取到之後立刻就被執行了.我們可能需要在線程中提供一個公共方法來關閉私有的BluetoothSocket.

作爲客戶端連接:

爲了啓動一個跟遠程設備的連接(一個開啓服務端監聽的設備), 我們必須先取得一個BluetoothDevice對象來表示遠程設備. (獲取BluetoothDevice的內容在上面的”查找設備”小節). 然後必須使用BluetoothDevice來獲得一個BluetoothSocket並啓動連接. 下面是一個基本的步驟:

1.      使用BluetoothDevice, 通過createRfcommSocketToServiceRecord(UUID)得到一個BluetoothSocket.這將初始化一個BluetoothSocket, 它會連接到一個BluetoothDevice. 這裏使用的UUID必須匹配服務端設備使用的UUID. 使用相同的UUID很簡單只需要在APP中生成硬編碼UUID, 然後關聯到客戶端和服務端代碼中.

2.      用connect()方法初始化連接. 一旦調用了該方法, 系統將會在遠程設備上執行一個SDP查詢來匹配UUID. 如果查詢成功並且遠程設備接受了連接, 它將會在連接期間共享RFCOMM通道, connect()方法將會返回. 該方法是一個阻塞方法. 如果, 不管啥原因連接失敗了或者connect()方法超時(大概12秒), 那麼它將會拋出一個異常. 所以它應該在一個獨立的線程中運行.

注意, 我們應該總是保證在調用connect()的時候設備沒有執行”設備發現”功能. 如果設備發現正在執行, 那麼連接嘗試將會顯著減慢速度並看起來好像失敗了.

栗子: 下面的代碼展示了一個初始化連接的線程:

privateclass ConnectThreadextends Thread{
    private finalBluetoothSocket mmSocket;
    private finalBluetoothDevice 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, alsoused 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(IOExceptionconnectException){
            // Unable to connect; close the socket andget out
            try {
                mmSocket.close();
            } catch(IOExceptioncloseException){ }
            return;
        }
 
        // Do work to manage the connection (in a separate thread)
        manageConnectedSocket(mmSocket);
    }
 
    /** Will cancelan in-progress connection, and close the socket */
    public void cancel(){
        try {
            mmSocket.close();
        } catch(IOException e){ }
    }
}

注意栗子中的cancelDiscovery()方法在連接建立之前被調用了. 我們應該總是在連接之前這樣做, 不管它是不是在運行, 調用該方法都是安全的(但是如果想要檢查一下的話, 應該調用isDiscovering()方法). ManageConnectedSocket()是一個虛構的方法, 它將會實例化一個線程用於交換數據. 將在下面的小節討論它.

當我們處理完了BluetoothSocket, 應該記得調用close()方法來清理. 這樣做將會直接關閉連接的socket並清除所有的內部資源.

管理一個連接:

當我們成功的連接了兩個(或者更多的)設備, 每個設備都會擁有一個連接着的BluetoothSocket. 這裏開始就比較有意思了, 因爲我們可以在設備間分享數據了. 使用BluetoothSocket交換數據的一般步驟是這樣的:

1.      從socket獲取傳輸的InputStream和OutputStream, 分別使用getInputStream()和getOutputStream()方法.

2.      用read(byte[])和write(byte[])方法讀寫數據.

完事兒.

當然還有一些實現的細節需要考慮. 首先也是最重要的, 我們應該使用一個專有線程來執行所有的數據讀寫操作. 這很有必要, 因爲read(byte[])和write(byte[])方法都是阻塞方法. Write(byte[])不經常阻塞, 但是可以如果遠程設備調用read(byte[])不夠快的話會導致中間緩衝區堆滿, 並使流控制阻塞. 所以我們的線程主循環應該專門用來從InputStream中讀取數據. 線程中可以有一個獨立的公共方法來執行OutputStream的寫操作.

栗子, 這裏是它們實現的樣子:

privateclass ConnectedThreadextends Thread{
    private finalBluetoothSocket mmSocket;
    private finalInputStream mmInStream;
    private finalOutputStream 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 UIactivity
                mHandler.obtainMessage(MESSAGE_READ, bytes,-1, buffer)
                       .sendToTarget();
            } catch(IOException e){
                break;
            }
        }
    }
 
    /* Call thisfrom the main activity to send data to the remote device */
    public void write(byte[] bytes){
        try {
            mmOutStream.write(bytes);
        } catch(IOException e){ }
    }
 
    /* Call thisfrom the main activity to shutdown the connection */
    public void cancel(){
        try {
            mmSocket.close();
        } catch(IOException e){ }
    }
}

構造方法取得需要的流, 並只執行一次, 線程將會通過InputStream等待數據到來. 當read(byte[])從流中返回數據的時候, 數據將會被通過成員handler發送給主activity. 然後返回再次等待更多的數據.

發送要輸出的數據跟在主activity中調用write()方法無異, 只需要傳給它要發送的數據就行了. 該方法然後調用write(byte[])來發送數據到遠程設備.

線程的cancel()方法很重要, 這樣就可以在任何時間都能關閉BluetoothSocket了. 完成藍牙通信之後應該記得調用該方法.

使用配置文件(BluetoothProfile):

從Android3.0開始, Bluetooth API開始提供對使用配置文件的支持. 藍牙配置文件是一個基於藍牙通信的設備間的無線接口規範. 其中一個栗子是免提(Hands-Free)配置文件. 如果一個手機想要連接到無線耳機, 那麼兩個設備必須都支持免提配置文件.

我們可以通過實現BluetoothProfile接口來實現自己的類, 這樣就可以支持一個特有的藍牙配置文件了. Android Bluetooth API提供了這些藍牙配置文件的實現:

1.      耳機(Headset): 耳機配置文件提供了手機使用藍牙耳機的支持. Android爲我們提供了BluetoothHeadset類, 它是一個通過IPC(inter process communication)控制藍牙耳機服務的代理. BluetoothHeadset類包括了對AT命令的支持(文後介紹).

2.      A2DP: A2DP是AdvancedAudio Distribution Profile. 它定義了高質量音頻如何通過藍牙實現流傳輸. Android提供了BluetoothA2dp類, 它是一個通過IPC來控制藍牙A2DP服務的代理.

3.      健康設備(Health Device): Android4.0引入了對藍牙健康設備配置文件(BluetoothHealth Device Profile(HDP))的支持. 這讓我們可以創建使用藍牙與支持藍牙的健康設備通信的APP. 比如心率監測器, 血壓計, 溫度計等. 想要了解更多相應設備的專業實現代碼, 可以到www.bluetooth.org 中的BluetoothAssigned numbers中查找. 還有 ISO/IEEE 11073-20601 [7]中也有.

這裏是使用配置文件的基本步驟:

1.      獲取默認的adapter, 在前文”設置藍牙”中有描述.

2.      使用getProfileProxy()來建立一個配置文件到配置文件代理對象的連接. 在下面的栗子中, 配置文件代理對象是一個BluetoothHeadset實例.

3.      設置一個BluetoothProfile.ServiceListener. 當它們從服務器連接或者斷開連接的時候該監聽器提醒BluetoothProfile IPC客戶端.

4.      在onServiceConnected()方法中取得一個配置文件代理對象的句柄.

5.      一旦得到了這個配置文件句柄, 我們就可以用它來監測連接狀態和處理其他相應的操作了.

下面的代碼片段展示瞭如何連接到一個BluetoothHeadset代理對象, 以便控制headset配置文件:

BluetoothHeadset mBluetoothHeadset;
 
// Get thedefault adapter
BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
 
// Establishconnection to the proxy.
mBluetoothAdapter.getProfileProxy(context, mProfileListener,BluetoothProfile.HEADSET);
 
private BluetoothProfile.ServiceListenermProfileListener= 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;
        }
    }
};
 
// ... callfunctions on mBluetoothHeadset
 
// Close proxyconnection after use.
mBluetoothAdapter.closeProfileProxy(mBluetoothHeadset);

廠商指定的AT命令:

從Android3.0開始, APP可以註冊接收預定義的廠商指定的AT由耳機發送的系統廣播命令(比如一個Plantronics +XEVENT命令). 慄如, 一個APP可以接收一個連接設備電量等級的廣播, 然後提醒用戶或者採取需要的措施. 爲ACTION_VENDOR_SPECIFIC_HEADSET_EVENT intent創建一個broadcastreceiver就可以處理耳機廠商指定的AT命令了.

健康設備配置文件:

從Android4.0開始, 引入了對藍牙健康設備配置文件(HDP)的支持. 這讓我們可以創建使用藍牙跟健康設備(需要支持藍牙)通信的APP, 比如心率監測器, 血壓計, 體溫計等. 藍牙健康API包括BluetoothHealth, BluetoothHealthCallback和BluetoothHealthAppConfiguration類. 要使用藍牙健康API, 懂得這些關鍵的HDP概念很重要:

概念

描述

Source

HDP中定義的一個規則. Source是一個健康設備, 該設備必須可以傳輸醫療數據(體重, 心率等)給一個只能設備, 比如Android手機.

Sink

HDP中定義的一個規則. 在HDP中, 一個sink是一個智能設備, 該設備必須可以接受健康數據. 在Android HDP APP中, sink通過BluetoothHealthAppConfiguration對象表達.

Registration

指爲一個指定的健康設備註冊一個sink.

Connection

是指在一個健康設備和一個智能設備(比如手機)之間打開一個通道.

創建一個HDP APP:

這是創建一個Android HDP APP 的基本步驟:

1.      獲取一個相關的BluetoothHealth代理對象. 就想headset和A2DP配置文件設備一樣, 我們必須在BluetoothProfile.ServiceListener中調用getProfileProxy()方法並指定HEALTH配置文件類型來建立一個跟配置文件代理對象的連接.

2.      創建一個BluetoothHealthCallback並註冊一個APP配置(BluetoothHealthAppConfiguration)作爲健康”sink”.

3.      創建一個連接到健康設備. 一些設備將會啓動連接, 這個步驟對於那些設備是不必要的.

4.      當成功連接到一個健康設備, 使用文件描述符讀寫健康設備. 收到的數據需要使用在IEEE 11073-xxxxx中定義的健康管理規範來解讀.

5.      結束之後, 關閉健康通道, 註銷APP.

 

總結:

根據類來分析: 一共12個類, 分兩組:

BluetoothAdapter

BluetoothDevice

BluetoothSocket

BluetoothServerSocket

BluetoothClass

上面這組是藍牙的基本類, 會在使用藍牙發現, 查詢和連接中用到.

BluetoothProfile

BluetoothProfile.ServiceListener

BluetoothHeadset

BluetoothA2dp

BluetoothHealth

BluetoothHealthCallback

BluetoothHealthAppConfiguration

這些則相當於擴展類, 用於實現特定功能. 相當於爲更豐富的藍牙功能提供了接口, 而且還對連接等操作做了封裝, 使其用法更加簡單.

 

參考: https://developer.android.com/guide/topics/connectivity/bluetooth.html

 

發佈了81 篇原創文章 · 獲贊 4 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章