目的
ペアリング時にPINコードの入力が必要となる場合において、プログラム的に (ユーザによる手入力なしで) Bluetoothのペアリングを行う方法をここにメモしたいと思います。
概要
大まかに書くと以下の手順を行うこととなります。
- NFCやUSBなどを経由して、周辺機器のBDアドレス (MACアドレス) とPINコードを受信する。
- BDアドレスより、その周辺機器の
BluetoothDevice
を生成する。 -
BluetoothDevice.ACTION_PAIRING_REQUEST
を受信するBroadcastReceiverを登録する。 -
BluetoothDevice#createBond()
によってペアリング要求を行う。 -
BluetoothDevice#setPin()
によってPINコードを入力する。
手順
それでは各手順ごとに説明していきます。
1. 周辺機器のBDアドレスとPINコードを受信
まず最初にペアリングを行いたい周辺機器のPINコード (とBDアドレス) を手に入れるところから始めます。
これを行う手段に決まりはないのですが、大抵はNFC経由で行われると思います。
この手順に関してはここでは詳しくは触れません。
(もしくは機器によってはPINコード入力を促されるものの、コードが固定の値 (0000
や 1234
) である場合があるかもしれません。)
2. 周辺機器のBluetoothDeviceを取得
使用しているライブラリによって手順は異なるとは思いますが、先程の手順で取得したBDアドレス (MACアドレス) を元に BluetoothDevice
のインスタンスを取得します。
Androidの標準ライブラリを使っている場合は
BluetoothAdapter#getRemoteDevice(address: String): BluetoothDevice
を使えばいいかと思います。
RxAndroidBleを使っている場合は
RxBleClient#getBleDevice(macAddress: String): RxBleDevice
→ RxBleDevice#getBluetoothDevice(): BluetoothDevice
で取得できます。
また、周辺機器のBDアドレス (MACアドレス) がない場合は、スキャンを掛けるなりして BluetoothDevice
を取得してください。
3. BroadcastReceiverの登録
Bluetoothのペアリング要求が行われた際に BluetoothDevice.ACTION_PAIRING_REQUEST
のメッセージを飛んで来るので、これを購読するBroadcastReceiverを登録します。
どのように実装してもいいのですが、私は以下のようにしました。
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()
を呼んでいます。
// RxAndroidBleを使っている場合
private fun pair(address: String, pin: ByteArray) {
this.pairingBroadcastReceiver!!.pin = pin
val bluetoothDevice = this.bleClient.getBleDevice(address).bluetoothDevice
bluetoothDevice.createBond()
}
// 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コードをプログラム的に入力します。
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のシステムの通知が出てきてしまいます。
また、PINコードの受け渡しに PairingBroadcastReceiver#pin
をActivity側からセットさせてやっていますが、実際にはもっとちゃんとやりましょう。
// ※ 例えばの実装例です。
// こんなインタフェースを切って、
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を使うといいでしょう。
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
にちゃんとパーミッションを書きましょう。
<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" />