天天看點

Android - 傳統藍牙(藍牙2.0)Android Bluetooth

Android Bluetooth

源碼基于 Android L

[TOC]

Reference

BluetoothAdapter

首先調用靜态方法getDefaultAdapter()擷取藍牙擴充卡bluetoothadapter,

如果傳回為空,則表明此裝置不支援藍牙。

代表本地藍牙擴充卡。BluetoothAdapter 讓你進行基礎的藍牙操作,比如初始化搜尋裝置,對已配對裝置進行檢索,根據一直MAC位址

執行個體化一個 BluetoothDevice,建立一個監聽其他裝置連接配接請求的 BluetoothServerSocket,啟動對藍牙LE裝置的搜尋。

這是使用藍牙的起點。獲得了本地擴充卡後,可以調用

getBondedDevices()

獲得代表配對裝置的 BluetoothDevice 對象

startDiscovery()

啟動搜尋藍牙裝置。或者建立一個BluetoothServerSocket ,

調用

listenUsingRfcommWithServiceRecord(String, UUID)

來監聽接入請求

startLeScan(LeScanCallback)

搜尋Bluetooth LE 裝置

相應的源碼

BluetoothAdapter.java (frameworks/base/core/java/android/bluetooth)

/**
     * Get a handle to the default local Bluetooth adapter.
     * <p>Currently Android only supports one Bluetooth adapter, but the API
     * could be extended to support more. This will always return the default
     * adapter.
     * @return the default local adapter, or null if Bluetooth is not supported
     *         on this hardware platform
     */
    public static synchronized BluetoothAdapter getDefaultAdapter() {
        if (sAdapter == null) {
            IBinder b = ServiceManager.getService(BLUETOOTH_MANAGER_SERVICE);
            if (b != null) {
                IBluetoothManager managerService = IBluetoothManager.Stub.asInterface(b);
                sAdapter = new BluetoothAdapter(managerService);
            } else {
                Log.e(TAG, "Bluetooth binder is null");
            }
        }
        return sAdapter;
    }           

BluetoothSocket

一個連接配接上或連接配接中的socket

使用BluetoothServerSocket來建立一個監聽的服務socket。

對于服務端,當BluetoothServerSocket接收了一個連接配接,會傳回一個新的BluetoothSocket來管理這個連接配接。

對于用戶端,使用一個單獨的BluetoothSocket來初始化一個發送連接配接并管理這個連接配接。

藍牙socket最普通的模式是RFCOMM,這是Android API支援的模式。

RFCOMM面向連接配接,使用流傳輸。也稱為Serial Port Profile (SPP)

建立一個到已知藍牙裝置的BluetoothSocket,使用

BluetoothDevice.createRfcommSocketToServiceRecord()

然後調用

connect()

來連接配接這個遠端裝置。調用這個方法會阻塞程式直到建立連接配接或者連接配接失敗。

一旦socket連接配接上,不論初始化為用戶端或者服務端,調用

getInputStream()

getOutputStream()

打開IO流來分别接收

輸入流和輸出流對象。流對象自動連接配接到socket

BluetoothSocket是線程安全的。特别的是,

close()

會立刻關閉進行中的操作并關閉socket。

需要 BLUETOOTH 相關權限

BluetoothServerSocket

一個監聽的藍牙socket

在服務端,使用BluetoothServerSocket來建立一個監聽的服務socket。

使用

BluetoothAdapter.listenUsingRfcommWithServiceRecord()

來建立一個監聽接入連接配接的BluetoothServerSocket

accept()

監聽連接配接請求。這個調用會阻塞,直到建立連接配接,并傳回一個管理連接配接的BluetoothSocket

獲得了 BluetoothSocket,并且不再需要連接配接,可以調用

close()

來關閉掉 BluetoothServerSocket

關閉 BluetoothServerSocket 并不會關閉傳回的 BluetoothSocket

BluetoothServerSocket 是線程安全的,特别的是,

close()

會立刻關閉進行中的操作并關閉服務 socket。

BluetoothDevice

代表一個遠端藍牙裝置。BluetoothDevice 能讓你與其他裝置建立連接配接,或者查詢裝置資訊,比如名稱,位址,類别和連接配接狀态等。

這是藍牙硬體位址的簡單包裝類。找個類的對象都說不可變的。這個類的操作會在遠端藍牙硬體位址上展現。

通過一個已知的MAC位址(可用BluetoothDevice來發現),或是通過

BluetoothAdapter.getBondedDevices()

傳回的已連接配接裝置

BluetoothAdapter.getRemoteDevice(String)

來獲得一個 BluetoothDevice。

然後就可以打開 BluetoothSocket 來與遠端裝置建立連接配接,調用

createRfcommSocketToServiceRecord(UUID)

API Guides

  • 搜尋其他藍牙裝置
  • 檢索比對到的藍牙裝置
  • 建立RFCOMM頻道
  • 通過發現服務來連接配接其他裝置
  • 管理多個連接配接

Setting Up Bluetooth 設定藍牙

使用藍牙通信前,确定裝置支援藍牙,并将藍牙打開

1.擷取 BluetoothAdapter

使用靜态方法

BluetoothAdapter.getDefaultAdapter()

擷取機器的藍牙擴充卡;若傳回null,則表示機器不支援藍牙

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

2.激活藍牙

檢查藍牙是否已經打開;若沒打開,可以使用下面的方法打開藍牙

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

會彈出一個對話框,請求使用者打開藍牙。如果成功打開,activity 的 onActivityResult() 會收到 RESULT_OK , 如果未打開,

則收到 RESULT_CANCELED

可以讓應用監聽 ACTION_STATE_CHANGED ,當藍牙狀态改變時會發出這個廣播。這個廣播包括藍牙原先狀态和現在狀态,分别裝在

EXTRA_PREVIOUS_STATE 和 EXTRA_STATE 裡。

狀态可能是 STATE_TURNING_ON, STATE_ON, STATE_TURNING_OFF, 和 STATE_OFF

Finding Devices 尋找裝置

使用 BluetoothAdapter,可以尋找遠端藍牙裝置或檢索比對裝置

搜尋裝置是搜尋本地區域已開啟的藍牙裝置程式。藍牙裝置有可見模式和不可見模式。如果一個裝置是可發現的,它會相應搜尋請求并

傳回一些資訊,比如裝置名,類别,獨立的MAC位址。利用這些資訊,裝置可以初始化一個到被發現裝置的連接配接。

第一次與其他裝置連接配接建立,會自動彈出一個配對請求給使用者。成功配對後,裝置的基本資訊會被儲存下來,并且可以被藍牙API調用。

使用已知的遠端裝置的MAC位址,可以在不搜尋裝置的情況下建立連接配接。

配對和連接配接是不同的。配對表示兩個裝置知道對方的存在,有互相認證的key,能夠與對方建立加密的連接配接。

連接配接意味着裝置目前共享一個RFCOMM頻道,并能互相發送資料。目前android藍牙API要求裝置先配對,再進行連接配接。

注意:Android裝置并不是預設藍牙可見的。使用者可以在系統設定中讓裝置藍牙可被搜尋到。

Querying paired devices 檢索已配對的裝置

搜尋裝置前,可以調用

getBondedDevices()

檢索一下已配對的裝置。這會傳回配對裝置的BluetoothDevices集合。

例如,你可以檢索配對裝置并把每個裝置資訊存入 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());
    }
}           

隻需要MAC位址,就能夠用BluetoothDevice對象建立連接配接。

Discovering devices 搜尋裝置

startDiscovery()

搜尋裝置。這個異步程序會立刻傳回是否啟動成功的boolean值。搜尋程序通常是inquiry scan進行12秒,

接下來是每個發現裝置的 page scan 。

你的應用必須注冊一個廣播接收器來監聽 ACTION_FOUND ,接收發現的每個裝置的資訊。每發現一個裝置,系統會發送 ACTION_FOUND

這個 Intent 帶有 EXTRA_DEVICE 和 EXTRA_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           

警告:搜尋裝置對于藍牙擴充卡來說是一個耗費資源的程序。發現目标裝置後,一定要在連接配接前調用

cancelDiscovery()

停止搜尋。

不要在與裝置連接配接時啟動搜尋。搜尋裝置會減小連接配接的帶寬。

Enabling discoverability 激活裝置藍牙可見

如果想讓本地裝置對其他裝置藍牙可見,調用

startActivityForResult(Intent, int)

,傳入ACTION_REQUEST_DISCOVERABLE

這會請求激活系統設定。預設激活120秒。可以用EXTRA_DISCOVERABLE_DURATION來請求别的時間。應用可設最長時間是3600秒。

0表示裝置永遠可見。在0~3600外的數字會被設定為120秒。比如,将時間設定為300秒:

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

會顯示一個對話框來請求使用者。如果使用者點選“Yes”,裝置會變得藍牙可見。activity會收到

onActivityResult()

回調,并傳回

設定藍牙可見時間的數值。如果使用者點選“No”或出現錯誤,傳回代碼會是 RESULT_CANCELED 。

注意:如果裝置未開啟藍牙,将裝置藍牙設為可見會自動打開藍牙

裝置會在允許時間内保持沉默。可以注冊廣播接收器來監聽 ACTION_SCAN_MODE_CHANGED 。這個Intent會帶着 EXTRA_SCAN_MODE

和 EXTRA_PREVIOUS_SCAN_MODE,分别是新的和舊的狀态。可能的狀态值會是:

SCAN_MODE_CONNECTABLE_DISCOVERABLE, SCAN_MODE_CONNECTABLE, 或 SCAN_MODE_NONE

如果要與遠端裝置連接配接,可以不激活裝置可見。當應用想要建立服務socket擷取接入時,必須打開裝置可見。因為遠端裝置必須要發現

本地裝置來建立連接配接。

Connecting Devices 連接配接裝置

服務端裝置和用戶端裝置以不同的方式擷取 BluetoothSocket。連接配接建立時,服務端擷取一個 BluetoothSocket。

用戶端打開一個RFCOMM時會得到 BluetoothSocket 。

Connecting as a server 在連接配接中作為服務端

當你要連接配接2個裝置,其中一個必須作為伺服器并持有一個打開的 BluetoothServerSocket 。服務socket的目的是擷取接入請求并

給已連上裝置一個 BluetoothSocket 。當從 BluetoothServerSocket 擷取到 BluetoothSocket,BluetoothServerSocket

可以關閉掉,除非你想接入更多連接配接。

  1. listenUsingRfcommWithServiceRecord(String, UUID)

    來獲得一個

    BluetoothServerSocket

  2. accept()

  3. 如果不需要接入更多連接配接,調用

    close()

accept()

不應該在UI線程中調用,因為它是阻塞式的。通常在應用中啟動新的線程來操作BluetoothServerSocket 或 BluetoothSocket

在另一個線程調用BluetoothServerSocket (或 BluetoothSocket)的

close()

能跳出阻塞,并立刻傳回

例子:

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

可以線上程中寫一個公共方法來關閉私有的 BluetoothSocket 。當需要停止監聽服務socket時可以使用這個方法。

Connecting as a client 在連接配接中作為用戶端

與遠端裝置(服務端)建立連接配接,先擷取一個代表遠端裝置的 BluetoothDevice 對象。必須使用 BluetoothDevice 來擷取

BluetoothSocket 并初始化連接配接。

基本流程:

  1. 調用BluetoothDevice的

    createRfcommSocketToServiceRecord(UUID)

    擷取BluetoothSocket
  2. connect()

    來初始化連接配接

注意:在調用

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

manageConnectedSocket()

是一個虛構的方法,在連接配接管理中讨論它。

使用完

BluetoothSocket

,一定要調用

close()

來結束

Managing a Connection

成功連接配接2個或更多的裝置後,每個裝置有一個 BluetoothSocket 。裝置直接可以共享資料。使用BluetoothSocket傳輸任意資料

擷取處理傳輸的 InputStream 和 OutputStream,分别調用

getInputStream()

getOutputStream()

read(byte[])

write(byte[])

來讀寫資料

線程的主循環中應該用于專門從InputStream中讀資料。線程中要有專門的public方法來寫資料到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。然後等待資料流中更多的位元組

向外發送資料調用

write()

線程的

cancel()

方法很重要。完成了藍牙連接配接的所有操作後,應當cancel掉

繼續閱讀