【Android】プログラム的にBluetoothのペアリングを行う方法【Bluetooth】

# 目的
ペアリング時にPINコードの入力が必要となる場合において、プログラム的に (ユーザによる手入力なしで) Bluetoothのペアリングを行う方法をここにメモしたいと思います。

# 概要
大まかに書くと以下の手順を行うこととなります。

1. NFCやUSBなどを経由して、周辺機器のBDアドレス (MACアドレス) とPINコードを受信する。
2. BDアドレスより、その周辺機器の `BluetoothDevice` を生成する。
3. `BluetoothDevice.ACTION_PAIRING_REQUEST` を受信するBroadcastReceiverを登録する。
4. `BluetoothDevice#createBond()` によってペアリング要求を行う。
5. `BluetoothDevice#setPin()` によってPINコードを入力する。

# 手順
それでは各手順ごとに説明していきます。

## 1. 周辺機器のBDアドレスとPINコードを受信
まず最初にペアリングを行いたい周辺機器のPINコード (とBDアドレス) を手に入れるところから始めます。
これを行う手段に決まりはないのですが、大抵はNFC経由で行われると思います。
この手順に関してはここでは詳しくは触れません。  
(もしくは機器によってはPINコード入力を促されるものの、コードが固定の値 (`0000` や `1234`) である場合があるかもしれません。)

## 2. 周辺機器のBluetoothDeviceを取得
使用しているライブラリによって手順は異なるとは思いますが、先程の手順で取得したBDアドレス (MACアドレス) を元に `BluetoothDevice` のインスタンスを取得します。

Androidの標準ライブラリを使っている場合は
`BluetoothAdapter#getRemoteDevice(address: String): BluetoothDevice`
を使えばいいかと思います。  
[RxAndroidBle](https://github.com/Polidea/RxAndroidBle)を使っている場合は
`RxBleClient#getBleDevice(macAddress: String): RxBleDevice`
→ `RxBleDevice#getBluetoothDevice(): BluetoothDevice`
で取得できます。

また、周辺機器のBDアドレス (MACアドレス) がない場合は、スキャンを掛けるなりして `BluetoothDevice` を取得してください。

## 3. BroadcastReceiverの登録
Bluetoothのペアリング要求が行われた際に `BluetoothDevice.ACTION_PAIRING_REQUEST` のメッセージを飛んで来るので、これを購読するBroadcastReceiverを登録します。

どのように実装してもいいのですが、私は以下のようにしました。

```Kotlin
package net.aridai.pairingapp

import android.bluetooth.BluetoothDevice
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.polidea.rxandroidble2.RxBleClient
import com.polidea.rxandroidble2.RxBleDevice
import kotlinx.android.synthetic.main.main_activity.*
import org.koin.android.ext.android.inject

//  Activityのコードです。
//  一部省略している部分があります。
class MainActivity : AppCompatActivity() {

    //  私はRxAndroidBleを使っています。
    private val bleClient: RxBleClient by inject()

    //  IntentFilterの設定です。
    //  優先度を最大の999 (SYSTEM_HIGH_PRIORITY - 1) にしています。
    private val pairingRequestIntentFilter: IntentFilter by lazy {
        IntentFilter(BluetoothDevice.ACTION_PAIRING_REQUEST).also {
            it.priority = IntentFilter.SYSTEM_HIGH_PRIORITY - 1
        }
    }

    //  BroadcastReceiverです。
    //  「PairingBroadcastReceiver」は私が定義したクラスです。
    private var pairingBroadcastReceiver: PairingBroadcastReceiver? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        //  省略...
    }

    override fun onResume() {
        super.onResume()

        //  ここでBroadcastReceiverの登録を行っています。
        this.pairingBroadcastReceiver = PairingBroadcastReceiver()
        this.registerReceiver(this.pairingBroadcastReceiver, this.pairingRequestIntentFilter)
    }

    override fun onPause() {
        super.onPause()

        //  ここでBroadcastReceiverの登録解除を行っています。
        this.unregisterReceiver(this.pairingBroadcastReceiver)
        this.pairingBroadcastReceiver = null
    }

    //  ペアリングを行います。
    private fun pair(address: String, pin: ByteArray) {
        //  私はRxAndroidBleを使っているのでこのように書きます。
        val bluetoothDevice = this.bleClient.getBleDevice(address).bluetoothDevice
        bluetoothDevice.createBond()

        //  Android標準ライブラリで行う場合は BluetoothAdapter#getRemoteDevice(address) を使うといいかと思います。
    }
}

```

## 4. ペアリングの要求
上記のコードの以下の部分です。  
`BluetoothDevice` を取得して `createBond()` を呼んでいます。

```Kotlin
//  RxAndroidBleを使っている場合
private fun pair(address: String, pin: ByteArray) {
    this.pairingBroadcastReceiver!!.pin = pin
    val bluetoothDevice = this.bleClient.getBleDevice(address).bluetoothDevice
    bluetoothDevice.createBond()
}
```

```Kotlin
//  Android標準でやる場合
//  (this.adapter: BluetoothAdapter)
private fun pair(address: String, pin: ByteArray) {
    this.pairingBroadcastReceiver!!.pin = pin
    val bluetoothDevice = this.adapter.getRemoteDevice(address)
    bluetoothDevice.createBond()
}
```

## 5. PINコードの入力
正常にペアリング要求がされた場合、BroadcastReceiverがメッセージを受信しますので、そこでPINコードをプログラム的に入力します。

```Kotlin
package net.aridai.pairingapp

import android.bluetooth.BluetoothDevice
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent

class PairingBroadcastReceiver : BroadcastReceiver() {

    //  PINコードの受け渡し口
    //  (サンプルコードなので雑でも気にしないでね♡)
    var pin: ByteArray? = null

    override fun onReceive(context: Context?, intent: Intent?) {
        if (intent == null) return

        if (intent.action == BluetoothDevice.ACTION_PAIRING_REQUEST) {
            val device = intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)!!
            val pin = this.pin!!

            device.setPin(pin)

            //  他のBroadcastReceiverにメッセージが飛んでいかなないようにする。
            //  これを呼ばないとシステムのペアリング要求通知が出てきてしまう。
            this.abortBroadcast()
        }
    }
}

```

コード中で `BroadcastReceiver#abortBroadcast()` を呼んでいますが、これを呼ばないとAndroidのシステムの通知が出てきてしまいます。 

![ペアリング通知.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/341119/6f4bbed3-ef0b-f1e2-169c-0ae17ab9b1f4.png)

また、PINコードの受け渡しに `PairingBroadcastReceiver#pin` をActivity側からセットさせてやっていますが、実際にはもっとちゃんとやりましょう。

```kotlin
//  ※ 例えばの実装例です。
//  こんなインタフェースを切って、
interface PairingPinProvider {
    fun providePin(address: String): ByteArray?
}

//  BroadcastReceiver購読元に実装させて、
class MainActivity : AppCompatActivity(), PairingPinProvider {
    //  いろいろと省略...

    override fun providePin(address: String): ByteArray? {
        //  ちゃんと実装
    }
}

//  特定のActivityじゃなくてインタフェースに依存させる。
override fun onReceive(context: Context?, intent: Intent?) {
    if (context !is PairingPinProvider || intent == null) return
    //  省略...
    val pin = context.providePin(device.address) ?: throw IllegalStateException("ペアリング要求したくせにPINねぇじゃんww")
}
```

# その他

ペアリング (ボンディング) の状態が変更されたことを知るには `BluetoothDevice.ACTION_BOND_STATE_CHANGED` のBroadcastReceiverを使うといいでしょう。

```Kotlin
package net.aridai.pairingapp

import android.bluetooth.BluetoothDevice
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent

class BondStateChangedBroadcastReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context?, intent: Intent?) {
        if (context !is BondStateChangedListener || intent == null) return

        if (intent.action == BluetoothDevice.ACTION_BOND_STATE_CHANGED) {
            val device = intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)!!
            val prevBondState = intent.getIntExtra(BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE, -1)
            val bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1)

            context.onBondStateChanged(device, prevBondState, bondState)
        }
    }

    interface BondStateChangedListener {
        fun onBondStateChanged(device: BluetoothDevice, prevState: Int, currentState: Int)
    }
}

```

また `AndroidManifest.xml` にちゃんとパーミッションを書きましょう。

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

    <uses-feature
        android:name="android.hardware.bluetooth_le"
        android:required="true" />
```

# 参考

* [BluetoothDevice | Android Developers](https://developer.android.com/reference/android/bluetooth/BluetoothDevice)
* [BluetoothAdapter | Android Developers](https://developer.android.com/reference/android/bluetooth/BluetoothAdapter.html#getRemoteDevice(java.lang.String))
* [android - Broadcast receiver highest priority not working - Stack Overflow](https://stackoverflow.com/a/27653789)