CompanionDeviceManagerでBLE再接続を拾う、ACL_CONNECTEDとGATTコールバックの使い分け

  • Android
  • BLE
  • Bluetooth

この記事は、構成と文章の整理にAIを使っています。
あくまで読みやすくするための補助で、内容そのものをAIにすべて任せて書いているわけではありませんが、ところどころ相談しながら整えています。

CompanionDeviceManager(CDM)でペアリングしたBLEデバイスが、電源断で切れたあと復帰したときにどう検知するか整理した。アプリがフォアグラウンドにもバックグラウンドにもいない状態からの再接続検知が前提。

CompanionDeviceManagerとBLEフィルタ

CDMはAPI 26以降で使える機能で、BluetoothDeviceFilter / BluetoothLeDeviceFilter / WifiDeviceFilter でペアリング候補を絞り込み、システム側の選択ダイアログをユーザーに出す。BLEデバイスを対象にする場合は BluetoothLeDeviceFilter を使う。

val deviceFilter = BluetoothLeDeviceFilter.Builder()
.setNamePattern(Pattern.compile("MyDevice"))
.build()
val pairingRequest = AssociationRequest.Builder()
.addDeviceFilter(deviceFilter)
.build()
deviceManager.associate(pairingRequest, callback, null)

ここまではドキュメント通りで特に癖はない。

再接続の検知方法、2つの選択肢

ペアリング済みデバイスの再接続を拾う方法として ACTION_ACL_CONNECTED ブロードキャストと BluetoothGattCallback#onConnectionStateChange() の2つがある。

ACTION_ACL_CONNECTED は「Classic Bluetooth専用でBLEには効かない」という説明を見かけるけど、これは正確じゃない。現行のAPIリファレンスでは EXTRA_DEVICE に加えて EXTRA_TRANSPORT が必ず含まれるようになっていて、TRANSPORT_LE かどうかで判定できる。つまりBLEのACL接続でもこのブロードキャストは飛んでくる。

ただし実務ではあまり使わない。理由は、connectGatt() で自分から張ったGATT接続の状態はACLレベルのブロードキャストより BluetoothGattCallback のほうが正確に追えるから。ACLはOSのBluetoothスタックが管理する低レベルの接続で、GATT接続確立のタイミングとは必ずしも一致しない。なので「ACL_CONNECTEDはBLEで使えない」ではなく、「アプリ管理のBLE接続監視にはGATTコールバックのほうが向いている」が正しい理解。

private val gattCallback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(
gatt: BluetoothGatt?,
status: Int,
newState: Int
) {
when (newState) {
BluetoothProfile.STATE_CONNECTED -> {
// 再接続時の処理
}
BluetoothProfile.STATE_DISCONNECTED -> {
// 切断時の処理
}
}
}
}

フォアグラウンドサービスで監視する

アプリがバックグラウンドにもフォアグラウンドにもいない状態で監視を続けるにはフォアグラウンドサービスが必要になる。foregroundServiceType="connectedDevice" を使うが、Manifest側の権限が抜けやすい。

Android 14(API 34)以降をターゲットにする場合、connectedDevice タイプでサービスを起動するには FOREGROUND_SERVICE_CONNECTED_DEVICE 権限に加えて、BLUETOOTH_CONNECT / BLUETOOTH_SCAN / BLUETOOTH_ADVERTISE などのいずれか1つが必要。これがないとサービス起動時に SecurityException で落ちる。

<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<service
android:name=".BleMonitorService"
android:foregroundServiceType="connectedDevice"
android:exported="false" />

サービス側の startForeground() 呼び出しでも型を明示的に渡す。

class BleMonitorService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val notification = createNotification()
ServiceCompat.startForeground(
this,
NOTIFICATION_ID,
notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE
)
val device: BluetoothDevice? = intent?.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
device?.connectGatt(this, false, gattCallback)
return START_STICKY
}
override fun onDestroy() {
bluetoothGatt?.close()
bluetoothGatt = null
super.onDestroy()
}
}

再接続検知の流れを図にするとこう。

stateDiagram-v2
    [*] --> Disconnected
    Disconnected --> Connecting: BLEデバイス電源復帰\n(アドバタイズ再開)
    Connecting --> Connected: connectGatt成功\nonConnectionStateChange(STATE_CONNECTED)
    Connected --> Disconnected: 電源断/圏外\nonConnectionStateChange(STATE_DISCONNECTED)
    Connecting --> Disconnected: 接続失敗

負荷については、フォアグラウンドサービス自体のCPU負荷は低く、BLE接続もイベント駆動なので待機中はほぼコストがかからない。接続・切断が頻発するケースだとバッテリー消費は増えるが、常時ポーリングするわけではないので全体としては軽い部類。

2024年6月18日以降の更新

  • Android 15(2024年10月リリース):バックグラウンドでBLEスキャンを行うアプリに対して foregroundServiceType="connectedDevice" と対応する権限の宣言を厳格に要求するようになった。以前は汎用のフォアグラウンドサービスで動いてしまっていたケースも塞がれている。
  • Android 16(2025年6月GA):フォアグラウンドサービスから開始するバックグラウンドジョブが、JobScheduler経由かWorkManager/DownloadManager経由かに関わらず通常のジョブクォータに従うようになった。BLE監視サービスの中で追加のバックグラウンド処理を動かしている場合は影響を受ける可能性がある。

いずれもCDM自体のAPIが変わったわけではなく、フォアグラウンドサービスまわりの制約が段階的に厳しくなっている流れ。継続的にBLEデバイスを監視するアプリを作るなら、都度この制約を確認したほうがいい。