藍牙總覽

Android 平台支援藍牙網路堆疊,讓裝置與其他藍牙裝置以無線方式交換資料。應用程式架構可讓您透過 Android Bluetooth API 存取藍牙功能。這些 API 可讓應用程式以無線方式連線至其他藍牙裝置,提供點對點和多點無線功能。

使用 Bluetooth API,Android 應用程式可執行下列操作:

  • 掃描其他藍牙裝置
  • 查詢配對藍牙裝置的本機藍牙轉接器
  • 建立 RFCOMM 管道
  • 透過服務探索功能連線至其他裝置
  • 將資料與其他裝置轉移,或從其他裝置轉移資料
  • 管理多個連結

本頁面著重於傳統藍牙。經典藍牙是較耗電作業 (包括在 Android 裝置之間串流和通訊) 的理想選擇。針對低功耗藍牙裝置,Android 4.3 (API 級別 18) 推出了對藍牙低功耗的 API 支援。詳情請參閱藍牙低功耗一文。

本文說明不同的藍牙設定檔,包括健康裝置設定檔。接著,我們會說明如何使用 Android Bluetooth API 來完成透過藍牙進行通訊所需的四大工作:設定藍牙、尋找本機區域中已配對或可以使用的裝置、連結裝置,以及在裝置間傳輸資料。

基本概念

為了讓支援藍牙的裝置彼此傳輸資料,必須先使用「配對」程序建立通訊管道。一部裝置 (即「可偵測的裝置」) 會自行用於傳入連線要求。其他裝置使用服務探索程序尋找可探索的裝置。可供搜尋的裝置接受配對要求後,兩部裝置就會完成「綁定」程序,並交換安全金鑰。裝置會快取這些金鑰以供日後使用。配對和綁定程序完成後,兩部裝置就會交換資訊。工作階段結束後,啟動配對要求的裝置會釋出先前連結至可搜尋裝置的頻道。但是,這兩部裝置仍會保持綁定,因此只要兩者位於彼此的範圍內,且兩者都未移除繫結,就能在日後的工作階段期間自動重新連線。

藍牙權限

如要在應用程式中使用藍牙功能,您必須宣告兩項權限。第一個函式是 BLUETOOTH。您必須具備這項權限才能執行任何藍牙通訊,例如要求連線、接受連線及傳輸資料。

其他必須宣告的權限為 ACCESS_FINE_LOCATION。您的應用程式需要這項權限,因為藍牙掃描功能可用來收集使用者位置的相關資訊。這些資訊可能來自使用者擁有的裝置,以及商店和大眾運輸設施等位置使用的藍牙信標。

如果服務是在 Android 10 以上版本中執行,除非具備 ACCESS_BACKGROUND_LOCATION 權限,否則無法找出藍牙裝置。如要進一步瞭解這項規定,請參閱「在背景存取位置資訊」。

下列程式碼片段說明如何檢查權限。

Kotlin

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    if (ContextCompat.checkSelfPermission(baseContext,
        Manifest.permission.ACCESS_BACKGROUND_LOCATION)
        != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(
                this,
                arrayOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION),
                    PERMISSION_CODE)
        }
}

Java

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
  if (ContextCompat.checkSelfPermission(baseContext,
      Manifest.permission.ACCESS_BACKGROUND_LOCATION)
      != PackageManager.PERMISSION_GRANTED) {
          ActivityCompat.requestPermissions(
              MyActivity.this,
              new String[]{Manifest.permission.ACCESS_BACKGROUND_LOCATION},
                  PERMISSION_CODE)
      }
}

不過,如果應用程式安裝在搭載 Android 11 以上版本的裝置上,且使用隨附裝置配對來與裝置建立關聯,則不符合這項權限要求。在這種情況下,一旦裝置與裝置建立關聯,應用程式就能掃描相關聯的藍牙裝置,而不必取得位置存取權。

在搭載 Android 8.0 (API 級別 26) 以上版本的裝置上,您可以使用 CompanionDeviceManager 來代表應用程式執行鄰近裝置掃描,不必取得位置存取權。如要進一步瞭解這個選項,請參閱「隨附裝置配對」。

注意: 如果應用程式指定的是 Android 9 (API 級別 28) 以下版本,您可以宣告 ACCESS_COARSE_LOCATION 權限,而非 ACCESS_FINE_LOCATION 權限。

如要讓應用程式啟動裝置探索或操控藍牙設定,除了 BLUETOOTH 權限之外,您還必須宣告 BLUETOOTH_ADMIN 權限。大多數應用程式需要這項權限才能搜尋本機藍牙裝置。請勿使用此權限授予的其他功能,除非應用程式是可在使用者要求修改藍牙設定的「電源管理工具」。

在應用程式資訊清單檔案中宣告藍牙權限。例如:

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

  <!-- If your app targets Android 9 or lower, you can declare
       ACCESS_COARSE_LOCATION instead. -->
  <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
  ...
</manifest>

如要進一步瞭解如何宣告應用程式權限,請參閱 <uses-permission> 參考資料。

使用個人資料

從 Android 3.0 開始,Bluetooth API 提供使用藍牙設定檔的支援。藍牙設定檔是一種無線介面規格,用於在裝置間進行藍牙通訊。「手持式」設定檔即為一例。手機必須支援免持聽筒設定檔,手機才能連線至無線耳機。

Android Bluetooth API 提供下列藍牙設定檔的實作方式:

  • 耳機。耳機設定檔支援搭配手機使用的藍牙耳機。Android 提供 BluetoothHeadset 類別,這是用於控制藍牙頭戴式服務的 Proxy。這包括藍牙耳機和免持功能 (v1.5) 設定檔。BluetoothHeadset 類別支援 AT 指令。如要進一步瞭解這個主題,請參閱供應商專屬的 AT 指令
  • A2DP。進階音訊發布設定檔 (A2DP) 設定檔會定義如何透過藍牙連線將某部裝置的高品質音訊串流至其他裝置。Android 提供 BluetoothA2dp 類別,這是用於控制藍牙 A2DP 服務的 Proxy。
  • 健康裝置。Android 4.0 (API 級別 14) 推出了藍牙健康裝置設定檔 (HDP) 支援功能。這可讓您建立使用藍牙與支援藍牙的健康裝置 (例如心率監測器、血計、溫度計、體重計等) 的應用程式進行通訊的應用程式。如需支援的裝置及對應的裝置資料專業代碼清單,請參閱藍牙的 HDP 裝置資料專業認證。這些值也會在 ISO/IEEE 11073-20601 [7] 規格中參照為 Nomenclature Codes 附錄的 MDC_DEV_SPEC_PROFILE_*。如要進一步瞭解 HDP,請參閱健康裝置設定檔

使用付款資料的基本步驟如下:

  1. 取得預設轉接器,如設定藍牙所述。
  2. 設定 BluetoothProfile.ServiceListener。當 BluetoothProfile 用戶端連線至服務或中斷連線時,這個事件監聽器會通知他們。
  3. 請使用 getProfileProxy() 連線至與設定檔相關聯的設定檔 Proxy 物件。在以下範例中,設定檔 Proxy 物件是 BluetoothHeadset 的例項。
  4. onServiceConnected() 中,取得設定檔 Proxy 物件的控制代碼。
  5. 取得設定檔 Proxy 物件後,您就能使用該物件監控連線狀態,並執行與該設定檔相關的其他作業。

例如,以下程式碼片段說明如何連線至 BluetoothHeadset Proxy 物件,以便控制耳機設定檔:

Kotlin

var bluetoothHeadset: BluetoothHeadset? = null

// Get the default adapter
val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter()

private val profileListener = object : BluetoothProfile.ServiceListener {

    override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
        if (profile == BluetoothProfile.HEADSET) {
            bluetoothHeadset = proxy as BluetoothHeadset
        }
    }

    override fun onServiceDisconnected(profile: Int) {
        if (profile == BluetoothProfile.HEADSET) {
            bluetoothHeadset = null
        }
    }
}

// Establish connection to the proxy.
bluetoothAdapter?.getProfileProxy(context, profileListener, BluetoothProfile.HEADSET)

// ... call functions on bluetoothHeadset

// Close proxy connection after use.
bluetoothAdapter?.closeProfileProxy(BluetoothProfile.HEADSET, bluetoothHeadset)

Java

BluetoothHeadset bluetoothHeadset;

// Get the default adapter
BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();

private BluetoothProfile.ServiceListener profileListener = new BluetoothProfile.ServiceListener() {
    public void onServiceConnected(int profile, BluetoothProfile proxy) {
        if (profile == BluetoothProfile.HEADSET) {
            bluetoothHeadset = (BluetoothHeadset) proxy;
        }
    }
    public void onServiceDisconnected(int profile) {
        if (profile == BluetoothProfile.HEADSET) {
            bluetoothHeadset = null;
        }
    }
};

// Establish connection to the proxy.
bluetoothAdapter.getProfileProxy(context, profileListener, BluetoothProfile.HEADSET);

// ... call functions on bluetoothHeadset

// Close proxy connection after use.
bluetoothAdapter.closeProfileProxy(bluetoothHeadset);

供應商專屬的 AT 指令

從 Android 3.0 (API 級別 11) 開始,應用程式可以註冊,以便接收系統廣播訊息 (例如 Plantronics +XEVENT 指令),接收預先定義的廠商專用 AT 指令廣播訊息。舉例來說,應用程式可以接收廣播訊息,指出已連結裝置的電池電量,並在必要時通知使用者或採取其他行動。為 ACTION_VENDOR_SPECIFIC_HEADSET_EVENT 意圖建立廣播接收器,為耳機處理廠商專屬的 AT 指令。

健康裝置設定檔

Android 4.0 (API 級別 14) 推出了對藍牙健康裝置設定檔 (HDP) 的支援。這可讓您建立使用藍牙與支援藍牙的健康裝置通訊的應用程式,例如心率監測器、血表、溫度計和體重計。Bluetooth Health API 包含 BluetoothHealthBluetoothHealthCallbackBluetoothHealthAppConfiguration 類別,相關說明請見「重要類別和介面」。

使用 Bluetooth Health API 時,瞭解下列主要 HDP 概念會相當實用:

Source
將醫療資料傳送至智慧型裝置 (例如 Android 手機或平板電腦) 的健康裝置,例如體重體重計、葡萄糖測量儀或溫度計。
水槽
接收醫療資料的智慧型裝置。在 Android HDP 應用程式中,接收器以 BluetoothHealthAppConfiguration 物件表示。
報名
用於註冊接收器的程序,用於與特定健康裝置通訊。
連線
在健康裝置 (來源) 和智慧型裝置 (接收器) 之間開啟頻道的程序。

建立 HDP 應用程式

以下為建立 Android HDP 應用程式的基本步驟:

  1. 取得 BluetoothHealth Proxy 物件的參照。

    與一般耳機和 A2DP 設定檔裝置類似,您必須使用 BluetoothProfile.ServiceListenerHEALTH 設定檔類型呼叫 getProfileProxy(),才能與設定檔 Proxy 物件建立連線。

  2. 建立 BluetoothHealthCallback,並註冊做為健康狀態接收器的應用程式設定 (BluetoothHealthAppConfiguration)。
  3. 與健康裝置建立連線。

    注意: 部分裝置會自動啟動連線。您無需為這些裝置執行這個步驟。

  4. 成功連線至健康裝置後,請使用檔案描述元讀取/寫入健康裝置。收到的資料必須使用實作 IEEE 11073 規格的健康管理工具來解讀。
  5. 完成後,關閉健康管道並取消註冊應用程式。管道也會在閒置時關閉。

設定藍牙

您必須先驗證裝置是否支援藍牙,以及確認裝置支援藍牙後,才能透過藍牙進行通訊。

如果不支援藍牙,您應妥善停用任何藍牙功能。如果藍牙支援但已停用,您就可以要求使用者在應用程式中啟用藍牙,而不必離開應用程式。系統會使用 BluetoothAdapter,在兩個步驟中完成這項設定:

  1. 取得 BluetoothAdapter

    所有和所有藍牙活動都必須使用 BluetoothAdapter。如要取得 BluetoothAdapter,請呼叫靜態 getDefaultAdapter() 方法。這會傳回 BluetoothAdapter,代表裝置擁有的藍牙轉接器 (藍牙無線電)。整個系統都有一個藍牙轉接器,而您的應用程式可以透過此物件與該系統互動。如果 getDefaultAdapter() 傳回 null,代表裝置不支援藍牙。例如:

    Kotlin

    val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter()
    if (bluetoothAdapter == null) {
        // Device doesn't support Bluetooth
    }
    

    Java

    BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
    if (bluetoothAdapter == null) {
        // Device doesn't support Bluetooth
    }
    
  2. 啟用藍牙。

    接下來,您必須確定已啟用藍牙。呼叫 isEnabled() 來檢查目前是否已啟用藍牙。如果這個方法傳回 false,則會停用藍牙。如要要求啟用藍牙,請呼叫 startActivityForResult(),然後傳入 ACTION_REQUEST_ENABLE 意圖動作。此呼叫會發出透過系統設定啟用藍牙的要求 (不會停止應用程式)。例如:

    Kotlin

    if (bluetoothAdapter?.isEnabled == false) {
        val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
        startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
    }
    

    Java

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

    畫面上會顯示對話方塊,要求使用者授予藍牙啟用權限,如圖 1 所示。如果使用者回應「Yes」,系統會開始啟用藍牙,並在程序完成 (或失敗) 後返回應用程式。

    圖 1:啟用藍牙對話方塊。

    傳送至 startActivityForResult()REQUEST_ENABLE_BT 常數是本機定義的整數,必須大於 0。系統會將此常數做為 requestCode 參數,傳回給您的 onActivityResult() 實作中。

    如果藍牙啟用成功,您的活動會在 onActivityResult() 回呼中收到 RESULT_OK 結果碼。如果因發生錯誤 (或使用者回應「否」) 而未啟用藍牙,則結果代碼為 RESULT_CANCELED

或者,應用程式也可以監聽 ACTION_STATE_CHANGED 廣播意圖,每當藍牙狀態變更時,系統就會廣播這個意圖。這個廣播訊息包含 EXTRA_STATEEXTRA_PREVIOUS_STATE 額外欄位,分別包含新舊藍牙狀態。這些額外欄位可能的值包括 STATE_TURNING_ONSTATE_ONSTATE_TURNING_OFFSTATE_OFF。如果應用程式需要偵測藍牙狀態的執行階段變更,監聽這個廣播是很實用的做法。

提示:啟用可偵測性會自動啟用藍牙。如要在執行藍牙活動前持續啟用裝置偵測性,請略過上述步驟 2。詳情請參閱本頁的啟用可偵測性一節。

尋找裝置

使用 BluetoothAdapter 時,您可以透過裝置探索或查詢配對裝置清單來尋找遠端藍牙裝置。

裝置探索是一項掃描程序,可針對支援藍牙的裝置搜尋本機區域,並要求各裝置的相關資訊。這項程序有時也稱為「探索」、「取得」或「掃描」。但是,附近的藍牙裝置必須「可供搜尋」,並目前接受資訊要求,才會回應探索要求。如果該裝置可供偵測,就會分享部分資訊 (例如裝置名稱、類別和專屬 MAC 位址),藉此回應探索要求。然後,執行探索程序的裝置即可選擇啟動與發現裝置的連線。

由於可供探索的裝置可能會揭露使用者的位置資訊,因此裝置探索程序需要位置存取權。如要在搭載 Android 8.0 (API 級別 26) 以上版本的裝置上使用應用程式,請使用 Companion Device Manager API。這個 API 會代表應用程式執行裝置探索作業,因此應用程式不需要要求位置存取權

首次與遠端裝置建立連線後,系統會自動向使用者顯示配對要求。裝置配對完成後,系統就會儲存該裝置的基本資訊,例如裝置名稱、類別和 MAC 位址,且可透過 Bluetooth API 讀取。如果使用遠端裝置的已知 MAC 位址,使用者隨時都可以透過該裝置發起連線,不必執行探索作業 (假設裝置仍在有效範圍內)。

請注意,配對與連線之間有所不同:

  • 「配對」表示兩部裝置知道彼此是否存在,且具備可用於驗證的共用連結金鑰,並且能夠彼此建立加密連線。
  • 連線代表裝置目前共用 RFCOMM 管道,並且能彼此傳輸資料。目前的 Android Bluetooth API 規定裝置必須配對,才能建立 RFCOMM 連線。當您使用 Bluetooth API 啟動加密連線時,系統會自動進行配對。

以下各節將說明如何使用裝置探索功能尋找配對的裝置,或探索新裝置。

注意:根據預設,無法搜尋 Android 裝置。使用者可以透過系統設定,在限定時間內讓裝置可供偵測,或讓應用程式直接要求使用者啟用可偵測性,而不必離開應用程式。詳情請參閱本頁的啟用可偵測性一節。

查詢配對的裝置

在執行裝置探索作業前,建議您查詢一組配對的裝置,確認是否已知道想要的裝置。方法是呼叫 getBondedDevices()。這樣做會傳回一組代表配對裝置的 BluetoothDevice 物件。舉例來說,您可以查詢所有配對的裝置,並取得每部裝置的名稱和 MAC 位址,如以下程式碼片段示範:

Kotlin

val pairedDevices: Set<BluetoothDevice>? = bluetoothAdapter?.bondedDevices
pairedDevices?.forEach { device ->
    val deviceName = device.name
    val deviceHardwareAddress = device.address // MAC address
}

Java

Set<BluetoothDevice> pairedDevices = bluetoothAdapter.getBondedDevices();

if (pairedDevices.size() > 0) {
    // There are paired devices. Get the name and address of each paired device.
    for (BluetoothDevice device : pairedDevices) {
        String deviceName = device.getName();
        String deviceHardwareAddress = device.getAddress(); // MAC address
    }
}

如要與藍牙裝置建立連線,只需從相關聯的 BluetoothDevice 物件取得 MAC 位址即可,呼叫 getAddress() 即可擷取這項資訊。如要進一步瞭解如何建立連線,請參閱「連結裝置」一節。

注意:執行裝置探索作業會耗用許多藍牙轉接器的資源。找到要連線的裝置後,請務必先停止使用 cancelDiscovery() 尋找裝置,再嘗試連線。此外,連接裝置時不應執行探索作業,因為探索程序會大幅降低任何現有連線的可用頻寬。

探索裝置

如要開始探索裝置,只要呼叫 startDiscovery() 即可。這項程序為非同步性質,並會傳回布林值,表示是否已成功啟動探索作業。探索程序通常需要大約 12 秒的調查掃描,接著是頁面掃描找到的每部裝置,以擷取其藍牙名稱。

為了接收找到的每部裝置相關資訊,您的應用程式必須為 ACTION_FOUND 意圖註冊 BroadcastReceiver。系統會向每部裝置播送這個意圖。意圖包含 EXTRA_DEVICEEXTRA_CLASS 額外的欄位,而這些欄位又分別包含 BluetoothDeviceBluetoothClass。下列程式碼片段說明如何註冊,以便在找到裝置時處理廣播訊息:

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    ...

    // Register for broadcasts when a device is discovered.
    val filter = IntentFilter(BluetoothDevice.ACTION_FOUND)
    registerReceiver(receiver, filter)
}

// Create a BroadcastReceiver for ACTION_FOUND.
private val receiver = object : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) {
        val action: String = intent.action
        when(action) {
            BluetoothDevice.ACTION_FOUND -> {
                // Discovery has found a device. Get the BluetoothDevice
                // object and its info from the Intent.
                val device: BluetoothDevice =
                        intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
                val deviceName = device.name
                val deviceHardwareAddress = device.address // MAC address
            }
        }
    }
}

override fun onDestroy() {
    super.onDestroy()
    ...

    // Don't forget to unregister the ACTION_FOUND receiver.
    unregisterReceiver(receiver)
}

Java

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...

    // Register for broadcasts when a device is discovered.
    IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
    registerReceiver(receiver, filter);
}

// Create a BroadcastReceiver for ACTION_FOUND.
private final BroadcastReceiver receiver = new BroadcastReceiver() {
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if (BluetoothDevice.ACTION_FOUND.equals(action)) {
            // Discovery has found a device. Get the BluetoothDevice
            // object and its info from the Intent.
            BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
            String deviceName = device.getName();
            String deviceHardwareAddress = device.getAddress(); // MAC address
        }
    }
};

@Override
protected void onDestroy() {
    super.onDestroy();
    ...

    // Don't forget to unregister the ACTION_FOUND receiver.
    unregisterReceiver(receiver);
}

如要與藍牙裝置建立連線,只需從相關聯的 BluetoothDevice 物件取得 MAC 位址即可,呼叫 getAddress() 即可擷取這項資訊。如要進一步瞭解如何建立連線,請參閱「連接裝置」一節。

注意:執行裝置探索作業會耗用許多藍牙轉接器的資源。找到要連線的裝置後,請務必先停止使用 cancelDiscovery() 尋找裝置,再嘗試連線。此外,連接裝置時不應執行探索作業,因為探索程序會大幅降低任何現有連線的可用頻寬。

啟用可偵測性

如果您希望讓其他裝置可以偵測到本機裝置,請使用 ACTION_REQUEST_DISCOVERABLE 意圖呼叫 startActivityForResult(Intent, int)。這會發出要求啟用系統可偵測模式的要求,無需前往「設定」應用程式,這樣應用程式就會停止。根據預設,裝置可供搜尋 120 秒或 2 分鐘。只要新增 EXTRA_DISCOVERABLE_DURATION 額外項目,即可定義不同的時間長度 (最多 3600 秒 (1 小時)。

注意: 如果將 EXTRA_DISCOVERABLE_DURATION 額外的值設為 0,則使用者隨時可以找到裝置。這項設定並不安全,因此強烈建議不要採用。

下列程式碼片段會將裝置設為 5 分鐘 (300 秒) 的偵測範圍:

Kotlin

val discoverableIntent: Intent = Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE).apply {
    putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300)
}
startActivity(discoverableIntent)

Java

Intent discoverableIntent =
        new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
startActivity(discoverableIntent);
圖 2:啟用可偵測性對話方塊。

系統會顯示對話方塊,要求使用者授予裝置可供偵測的裝置,如圖 2 所示。如果使用者回應「是」,就會在指定時間內偵測到裝置。然後活動會收到對 onActivityResult() 回呼的呼叫,結果代碼等於裝置可供探索的時間長度。如果使用者回應「否」或發生錯誤,則結果代碼為 RESULT_CANCELED

注意:如果裝置尚未啟用藍牙,然後將裝置設為可偵測,系統就會自動啟用藍牙。

裝置會在指定時間保持為可偵測模式。 如果您希望在可探索模式發生變更時收到通知,您可以註冊 ACTION_SCAN_MODE_CHANGED 意圖的 BroadcastReceiver。這個意圖包含額外欄位 EXTRA_SCAN_MODEEXTRA_PREVIOUS_SCAN_MODE,這兩個欄位分別提供新舊掃描模式。每個額外項目可能的值如下:

SCAN_MODE_CONNECTABLE_DISCOVERABLE
裝置處於可供搜尋模式。
SCAN_MODE_CONNECTABLE
裝置未處於可偵測模式,但仍可接收連線。
SCAN_MODE_NONE
裝置未處於可偵測模式,無法接收連線。

如要啟動與遠端裝置的連線,不需要啟用裝置可搜尋性,只有在您希望應用程式代管接受傳入連線的伺服器通訊端時,才需要啟用可偵測性,因為遠端裝置必須先能找到其他裝置,才能開始連線至其他裝置。

連接裝置

如要在兩部裝置之間建立連線,您必須同時實作伺服器端和用戶端機制,因為其中一部裝置必須開啟伺服器通訊端,而另一個裝置必須使用伺服器裝置的 MAC 位址來啟動連線。伺服器裝置和用戶端裝置會透過不同方式取得所需的 BluetoothSocket。伺服器在接受傳入連線時收到通訊端資訊。用戶端會在向伺服器開啟 RFCOMM 管道時提供通訊端資訊。

當伺服器和用戶端都在同一個 RFCOMM 管道上連結 BluetoothSocket 時,系統就會將伺服器和用戶端視為彼此連線。此時,每部裝置都可以取得輸入和輸出串流,並開始資料移轉,詳情請參閱管理連線一節。本節說明如何啟動兩部裝置之間的連線。

連線技巧

一種實作技巧是將每部裝置自動備妥伺服器,讓每部裝置都有一個伺服器通訊端,並監聽這個連線。在這種情況下,任一裝置都可以啟動另一個裝置的連線,並成為用戶端。或者,某部裝置可以明確代管連線,並視需求開啟伺服器通訊端,接著另一部裝置就會啟動連線。

圖 3:藍牙配對對話方塊。

注意:如果兩部裝置先前未曾配對,Android 架構會在連線程序中自動向使用者顯示配對要求通知或對話方塊,如圖 3 所示。因此,當應用程式嘗試連接裝置時,不需要考量裝置是否已配對。除非使用者成功配對兩部裝置,否則您的 RFCOMM 連線嘗試會遭到封鎖。如果使用者拒絕配對,或是配對程序失敗或逾時,嘗試就會失敗。

以伺服器形式連線

如要連接兩部裝置,一部裝置必須保持開啟的 BluetoothServerSocket 權限,以便做為伺服器使用。伺服器通訊端的用途是監聽傳入的連線要求,並在使用者接受要求後提供已連結的 BluetoothSocket。從 BluetoothServerSocket 取得 BluetoothSocket 時,除非您想讓裝置接受更多連線,否則 BluetoothServerSocket 可以 (也應該) 刪除。

如要設定伺服器通訊端並接受連線,請完成下列步驟:

  1. 呼叫 listenUsingRfcommWithServiceRecord() 取得 BluetoothServerSocket

    字串是服務的可識別名稱,系統會自動寫入裝置上的新服務探索通訊協定 (SDP) 資料庫項目。名稱可以任意使用,而且可以是應用程式名稱。通用唯一識別碼 (UUID) 也會包含在 SDP 項目中,並奠定用戶端裝置連線協議的基礎。也就是說,用戶端嘗試與這部裝置連線時,會傳輸一個 UUID,能夠正確識別要與其連線的服務。這些 UUID 必須相符,系統才能接受連線。

    UUID 是字串 ID 的標準化 128 位元格式,專門用於識別身分資訊。UUID 的特點是夠大,您可以任意選取任何隨機 ID,不會與任何其他 ID 衝突。在這個情況下,這項資訊會用於明確識別應用程式的藍牙服務。如要取得用於應用程式的 UUID,您可以在網路上使用眾多隨機 UUID 產生器,然後用 fromString(String) 初始化 UUID

  2. 呼叫 accept() 即可開始監聽連線要求。

    此為撥通電話,系統會在接受連線或發生例外狀況時傳回相關資訊。只有在遠端裝置傳送的連線要求內含的 UUID,且該要求與此監聽伺服器通訊端註冊的 UUID 相符時,才會接受連線。如果成功,accept() 會傳回已連結的 BluetoothSocket

  3. 除非您要接受其他連線,否則請呼叫 close()

    這個方法呼叫會釋放伺服器通訊端及其所有資源,但不會關閉 accept() 傳回的已連線 BluetoothSocket。與 TCP/IP 不同,RFCOMM 僅允許每個管道一個已連線的用戶端,因此在大多數情況下,在接受已連線的通訊端後,立即在 BluetoothServerSocket 上呼叫 close() 是合理的。

由於 accept() 呼叫屬於封鎖呼叫,因此不應在主要活動 UI 執行緒中執行,讓應用程式仍然可以回應其他使用者互動。通常在應用程式管理的新執行緒中,執行所有涉及 BluetoothServerSocketBluetoothSocket 的工作十分合理。如要取消已封鎖的通話 (例如 accept()),請在 BluetoothServerSocket 上呼叫 close(),或從其他執行緒呼叫 BluetoothSocket。請注意,BluetoothServerSocketBluetoothSocket 上的所有方法都屬於執行緒安全。

範例

以下是接受傳入連線的伺服器元件的簡化執行緒:

Kotlin

private inner class AcceptThread : Thread() {
    
    private val mmServerSocket: BluetoothServerSocket? by lazy(LazyThreadSafetyMode.NONE) {
        bluetoothAdapter?.listenUsingInsecureRfcommWithServiceRecord(NAME, MY_UUID)
    }

    override fun run() {
        // Keep listening until exception occurs or a socket is returned.
        var shouldLoop = true
        while (shouldLoop) {
            val socket: BluetoothSocket? = try {
                mmServerSocket?.accept()
            } catch (e: IOException) {
                Log.e(TAG, "Socket's accept() method failed", e)
                shouldLoop = false
                null
            }
            socket?.also {
                manageMyConnectedSocket(it)
                mmServerSocket?.close()
                shouldLoop = false
            }
        }
    }

    // Closes the connect socket and causes the thread to finish.
    fun cancel() {
        try {
            mmServerSocket?.close()
        } catch (e: IOException) {
            Log.e(TAG, "Could not close the connect socket", e)
        }
    }
}

Java

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 = bluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
        } catch (IOException e) {
            Log.e(TAG, "Socket's listen() method failed", 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) {
                Log.e(TAG, "Socket's accept() method failed", e);
                break;
            }

            if (socket != null) {
                // A connection was accepted. Perform work associated with
                // the connection in a separate thread.
                manageMyConnectedSocket(socket);
                mmServerSocket.close();
                break;
            }
        }
    }

    // Closes the connect socket and causes the thread to finish.
    public void cancel() {
        try {
            mmServerSocket.close();
        } catch (IOException e) {
            Log.e(TAG, "Could not close the connect socket", e);
        }
    }
}

在這個範例中,只需要一個連入連線,因此在接受連線且取得 BluetoothSocket 後,應用程式會將取得的 BluetoothSocket 傳遞至另一個執行緒,並關閉 BluetoothServerSocket,並中斷迴圈。

請注意,當 accept() 傳回 BluetoothSocket 時,表示通訊端已連線。因此,請勿呼叫 connect(),就像從用戶端呼叫一樣。

應用程式專屬的 manageMyConnectedSocket() 方法旨在啟動轉移資料的執行緒,詳情請參閱「管理連線」一節。

一般而言,當您監聽完傳入的連線後,應立即關閉 BluetoothServerSocket。在此範例中,系統會在取得 BluetoothSocket 後立即呼叫 close()。您可能也可以在執行緒中提供公開方法,以便在您需要停止監聽該伺服器通訊端時關閉私人 BluetoothSocket

以用戶端身分連線

如要與接受開放伺服器通訊端連線的遠端裝置建立連線,您必須先取得代表遠端裝置的 BluetoothDevice 物件。如要瞭解如何建立 BluetoothDevice,請參閱「尋找裝置」。接著,您必須使用 BluetoothDevice 取得 BluetoothSocket 並啟動連線。

基本程序如下:

  1. 使用 BluetoothDevice,呼叫 createRfcommSocketToServiceRecord(UUID) 以取得 BluetoothSocket

    這個方法會初始化 BluetoothSocket 物件,讓用戶端連線至 BluetoothDevice。此處傳遞的 UUID 必須與伺服器裝置呼叫 listenUsingRfcommWithServiceRecord(String, UUID) 以開啟 BluetoothServerSocket 時使用的 UUID 相符。如要使用相符的 UUID,請將 UUID 字串硬式編碼到應用程式中,然後從伺服器和用戶端程式碼參照該字串。

  2. 呼叫 connect() 來啟動連線。請注意,這個方法屬於封鎖呼叫。

    用戶端呼叫此方法後,系統會執行 SDP 查詢,尋找具有相符 UUID 的遠端裝置。如果查詢成功且遠端裝置接受連線,就會共用在連線期間使用的 RFCOMM 管道,且 connect() 方法會傳回。如果連線失敗,或是 connect() 方法逾時 (約 12 秒後),這個方法會擲回 IOException

    由於 connect() 為封鎖呼叫,因此請一律在與主要活動 (UI) 執行緒以外的執行緒中執行此連線程序。

    注意: 在呼叫 connect() 之前,建議您一律呼叫 cancelDiscovery(),確保裝置並未執行裝置探索。如果探索作業正在進行中,則連線嘗試速度會大幅降低,而且較有可能失敗。

範例

以下是啟動藍牙連線的用戶端執行緒基本範例:

Kotlin

private inner class ConnectThread(device: BluetoothDevice) : Thread() {

    private val mmSocket: BluetoothSocket? by lazy(LazyThreadSafetyMode.NONE) {
        device.createRfcommSocketToServiceRecord(MY_UUID)
    }

    public override fun run() {
        // Cancel discovery because it otherwise slows down the connection.
        bluetoothAdapter?.cancelDiscovery()

        mmSocket?.use { socket ->
            // Connect to the remote device through the socket. This call blocks
            // until it succeeds or throws an exception.
            socket.connect()

            // The connection attempt succeeded. Perform work associated with
            // the connection in a separate thread.
            manageMyConnectedSocket(socket)
        }
    }

    // Closes the client socket and causes the thread to finish.
    fun cancel() {
        try {
            mmSocket?.close()
        } catch (e: IOException) {
            Log.e(TAG, "Could not close the client socket", e)
        }
    }
}

Java

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;

        try {
            // Get a BluetoothSocket to connect with the given BluetoothDevice.
            // MY_UUID is the app's UUID string, also used in the server code.
            tmp = device.createRfcommSocketToServiceRecord(MY_UUID);
        } catch (IOException e) {
            Log.e(TAG, "Socket's create() method failed", e);
        }
        mmSocket = tmp;
    }

    public void run() {
        // Cancel discovery because it otherwise slows down the connection.
        bluetoothAdapter.cancelDiscovery();

        try {
            // Connect to the remote device through the socket. This call blocks
            // until it succeeds or throws an exception.
            mmSocket.connect();
        } catch (IOException connectException) {
            // Unable to connect; close the socket and return.
            try {
                mmSocket.close();
            } catch (IOException closeException) {
                Log.e(TAG, "Could not close the client socket", closeException);
            }
            return;
        }

        // The connection attempt succeeded. Perform work associated with
        // the connection in a separate thread.
        manageMyConnectedSocket(mmSocket);
    }

    // Closes the client socket and causes the thread to finish.
    public void cancel() {
        try {
            mmSocket.close();
        } catch (IOException e) {
            Log.e(TAG, "Could not close the client socket", e);
        }
    }
}

請注意,在嘗試連線之前,會呼叫這個程式碼片段中的 cancelDiscovery()。您應一律在 connect() 之前呼叫 cancelDiscovery(),尤其是無論目前是否進行裝置探索,cancelDiscovery() 都會成功。不過,如果應用程式需要判斷裝置探索作業是否仍在進行中,您可以使用 isDiscovering() 進行檢查。

應用程式專屬的 manageMyConnectedSocket() 方法旨在啟動轉移資料的執行緒,詳情請參閱「管理連線」一節。

完成 BluetoothSocket 後,請一律呼叫 close()。這麼做會立即關閉已連線的通訊端,並釋出所有相關的內部資源。

管理連結

成功連結多部裝置後,每部裝置都會有連線的 BluetoothSocket。而這才是樂趣的起點,因為您可以在裝置之間分享資訊。使用 BluetoothSocket 移轉資料的一般程序如下:

  1. 分別使用 getInputStream()getOutputStream(),取得透過通訊端處理傳輸的 InputStreamOutputStream
  2. 使用 read(byte[])write(byte[]) 將資料讀取及寫入串流。

當然需要考量實作詳情。請特別注意,您應該使用專屬執行緒從串流讀取及寫入串流。這點非常重要,因為 read(byte[])write(byte[]) 方法都會封鎖呼叫。read(byte[]) 方法會封鎖,直到串流讀取內容為止。write(byte[]) 方法通常不會封鎖,但如果遠端裝置呼叫 read(byte[]) 的速度不夠快,且中繼緩衝區因此已滿,則該方法可能會封鎖流量控制。因此,執行緒中的主要迴圈應專門用於讀取 InputStream。執行緒中的獨立公開方法可用於啟動寫入 OutputStream 的作業。

範例

以下範例說明如何透過藍牙連線的兩部裝置之間轉移資料:

Kotlin

private const val TAG = "MY_APP_DEBUG_TAG"

// Defines several constants used when transmitting messages between the
// service and the UI.
const val MESSAGE_READ: Int = 0
const val MESSAGE_WRITE: Int = 1
const val MESSAGE_TOAST: Int = 2
// ... (Add other message types here as needed.)

class MyBluetoothService(
        // handler that gets info from Bluetooth service
        private val handler: Handler) {

    private inner class ConnectedThread(private val mmSocket: BluetoothSocket) : Thread() {

        private val mmInStream: InputStream = mmSocket.inputStream
        private val mmOutStream: OutputStream = mmSocket.outputStream
        private val mmBuffer: ByteArray = ByteArray(1024) // mmBuffer store for the stream

        override fun run() {
            var numBytes: Int // bytes returned from read()

            // Keep listening to the InputStream until an exception occurs.
            while (true) {
                // Read from the InputStream.
                numBytes = try {
                    mmInStream.read(mmBuffer)
                } catch (e: IOException) {
                    Log.d(TAG, "Input stream was disconnected", e)
                    break
                }

                // Send the obtained bytes to the UI activity.
                val readMsg = handler.obtainMessage(
                        MESSAGE_READ, numBytes, -1,
                        mmBuffer)
                readMsg.sendToTarget()
            }
        }

        // Call this from the main activity to send data to the remote device.
        fun write(bytes: ByteArray) {
            try {
                mmOutStream.write(bytes)
            } catch (e: IOException) {
                Log.e(TAG, "Error occurred when sending data", e)

                // Send a failure message back to the activity.
                val writeErrorMsg = handler.obtainMessage(MESSAGE_TOAST)
                val bundle = Bundle().apply {
                    putString("toast", "Couldn't send data to the other device")
                }
                writeErrorMsg.data = bundle
                handler.sendMessage(writeErrorMsg)
                return
            }

            // Share the sent message with the UI activity.
            val writtenMsg = handler.obtainMessage(
                    MESSAGE_WRITE, -1, -1, bytes)
            writtenMsg.sendToTarget()
        }

        // Call this method from the main activity to shut down the connection.
        fun cancel() {
            try {
                mmSocket.close()
            } catch (e: IOException) {
                Log.e(TAG, "Could not close the connect socket", e)
            }
        }
    }
}

Java

public class MyBluetoothService {
    private static final String TAG = "MY_APP_DEBUG_TAG";
    private Handler handler; // handler that gets info from Bluetooth service

    // Defines several constants used when transmitting messages between the
    // service and the UI.
    private interface MessageConstants {
        public static final int MESSAGE_READ = 0;
        public static final int MESSAGE_WRITE = 1;
        public static final int MESSAGE_TOAST = 2;

        // ... (Add other message types here as needed.)
    }

    private class ConnectedThread extends Thread {
        private final BluetoothSocket mmSocket;
        private final InputStream mmInStream;
        private final OutputStream mmOutStream;
        private byte[] mmBuffer; // mmBuffer store for the stream

        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();
            } catch (IOException e) {
                Log.e(TAG, "Error occurred when creating input stream", e);
            }
            try {
                tmpOut = socket.getOutputStream();
            } catch (IOException e) {
                Log.e(TAG, "Error occurred when creating output stream", e);
            }

            mmInStream = tmpIn;
            mmOutStream = tmpOut;
        }

        public void run() {
            mmBuffer = new byte[1024];
            int numBytes; // bytes returned from read()

            // Keep listening to the InputStream until an exception occurs.
            while (true) {
                try {
                    // Read from the InputStream.
                    numBytes = mmInStream.read(mmBuffer);
                    // Send the obtained bytes to the UI activity.
                    Message readMsg = handler.obtainMessage(
                            MessageConstants.MESSAGE_READ, numBytes, -1,
                            mmBuffer);
                    readMsg.sendToTarget();
                } catch (IOException e) {
                    Log.d(TAG, "Input stream was disconnected", e);
                    break;
                }
            }
        }

        // Call this from the main activity to send data to the remote device.
        public void write(byte[] bytes) {
            try {
                mmOutStream.write(bytes);

                // Share the sent message with the UI activity.
                Message writtenMsg = handler.obtainMessage(
                        MessageConstants.MESSAGE_WRITE, -1, -1, bytes);
                writtenMsg.sendToTarget();
            } catch (IOException e) {
                Log.e(TAG, "Error occurred when sending data", e);

                // Send a failure message back to the activity.
                Message writeErrorMsg =
                        handler.obtainMessage(MessageConstants.MESSAGE_TOAST);
                Bundle bundle = new Bundle();
                bundle.putString("toast",
                        "Couldn't send data to the other device");
                writeErrorMsg.setData(bundle);
                handler.sendMessage(writeErrorMsg);
            }
        }

        // Call this method from the main activity to shut down the connection.
        public void cancel() {
            try {
                mmSocket.close();
            } catch (IOException e) {
                Log.e(TAG, "Could not close the connect socket", e);
            }
        }
    }
}

在建構函式取得必要的串流後,執行緒會等待資料透過 InputStream 傳送。當 read(byte[]) 傳回來自串流的資料時,系統會使用父項類別的成員 Handler,將資料傳送至主要活動。接著,執行緒會等待更多位元組從 InputStream 讀取。

傳送傳出資料的做法,就是從主要活動呼叫執行緒的 write() 方法,然後傳入要傳送的位元組。這個方法會呼叫 write(byte[]),將資料傳送至遠端裝置。如果呼叫 write(byte[]) 時擲回 IOException,執行緒會將浮動式訊息傳送至主要活動,向使用者說明裝置無法將指定的位元組傳送到其他 (已連線) 裝置。

執行緒的 cancel() 方法可讓系統隨時透過關閉 BluetoothSocket 來終止連線。使用藍牙連線後,應一律呼叫此方法。

如需 Bluetooth API 的使用示範,請參閱 Bluetooth Chat 範例應用程式

主要類別和介面

android.bluetooth 套件中提供了所有藍牙 API。以下摘要說明建立藍牙連線時所需的類別和介面:

BluetoothAdapter
代表本機藍牙轉接器 (藍牙無線電)。BluetoothAdapter 是所有藍牙互動的進入點。您可以利用這項功能搜尋其他藍牙裝置、查詢綁定 (配對) 的裝置清單、使用已知 MAC 位址將 BluetoothDevice 執行個體化,以及建立 BluetoothServerSocket 監聽其他裝置的通訊。
BluetoothDevice
代表遠端藍牙裝置。您可以使用此 API 透過 BluetoothSocket 要求與遠端裝置建立連線,或是查詢裝置名稱、位址、類別和連接狀態等裝置資訊。
BluetoothSocket
代表藍牙通訊端的介面 (類似 TCP Socket)。這個連接點可讓應用程式使用 InputStreamOutputStream 與其他藍牙裝置交換資料。
BluetoothServerSocket
代表可監聽傳入要求的開放伺服器通訊端 (類似於 TCP ServerSocket)。若要連接兩個 Android 裝置,一部裝置必須透過此類別開啟伺服器通訊端。當遠端藍牙裝置向這部裝置發出連線要求時,裝置會接受連線,然後傳回已連線的 BluetoothSocket
BluetoothClass
說明藍牙裝置的一般特性和功能。這是一組定義裝置類別和服務的唯讀屬性。雖然這些資訊可以提供有關裝置類型的實用提示,但此類別的屬性並不一定能說明裝置支援的所有藍牙設定檔和服務。
BluetoothProfile
代表藍牙設定檔的介面。藍牙設定檔是一種無線介面規格,用於在裝置之間進行藍牙通訊。例如「手持式」設定檔。如要進一步瞭解個人資料,請參閱「使用設定檔」。
BluetoothHeadset
提供搭配手機使用的藍牙耳機支援。這包括藍牙耳機設定檔和免持裝置 (v1.5) 設定檔。
BluetoothA2dp
定義如何使用進階音訊發布設定檔 (A2DP),透過藍牙連線從某裝置串流高品質的音訊。
BluetoothHealth
代表控制藍牙服務的健康裝置設定檔 Proxy。
BluetoothHealthCallback
用於實作 BluetoothHealth 回呼的抽象類別。您必須擴充此類別並實作回呼方法,以接收應用程式註冊狀態和藍牙頻道狀態變更的最新消息。
BluetoothHealthAppConfiguration
代表 Bluetooth Health 第三方應用程式註冊的應用程式設定,以便與遠端藍牙健康裝置通訊。
BluetoothProfile.ServiceListener
此介面在 BluetoothProfile 處理序間通訊 (IPC) 用戶端連線或中斷與執行特定設定檔的內部服務連線時,通知該用戶端。