はじめに
BLEデバイスからデータを受け取るアプリを開発していたところ、Android 13以降の端末でだけデータが保存できないという不具合 に遭遇しました。
その時の流れと解決法を記事に残します。
ログには PACKET_LOSS が連続し、パケット自体は届いているのに処理がおかしい。原因を調べたところ、BluetoothGattCallbackの「deprecated API」に関する見落としやすい罠がありました。
BLEで通知を受け取る仕組み
ここでは、BLE通知を受け取る仕組みをおさらいします。
BLEでデバイスからデータを受け取るには、BluetoothGattCallback の onCharacteristicChanged をオーバーライドします。
Android 13(API 33)でこのメソッドの内容が変わりました。
// 【旧API】Android 12以下用 → API 33でdeprecatedに
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic
// characteristic.value でデータを取得
) { ... }
// 【新API】Android 13以上用
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
value: ByteArray
// データが直接渡ってくる(characteristic.value の非推奨な参照が不要に)
) { ... }
新APIは value を直接引数で受け取れるようになり、characteristic.value への非推奨な参照が不要になりました。よくある移行パターンとして、両方をオーバーライドして古い端末にも対応しようとします。
// 「古い端末にも対応しよう」と両方実装したコード
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic
) {
val payload = characteristic.value?.clone() ?:
ByteArray(0)
processPacket(payload) // ← ここが2回呼ばれる
}
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
value: ByteArray
) {
processPacket(value) // ← ここも呼ばれる
}
問題
上記のおさらいでのような、古い端末にも対応できるような両方をオーバーライドする実装にすると Android 13では両方が呼ばれる(BLE通知が1回届くたびに、上記の2つが両方呼ばれる)という問題が発生しました。
調査した結果以下のことがわかりました。
「deprecatedになったなら旧APIは呼ばれなくなるんじゃないの?」と思いがちですが、そうではありません。Android 13以降でも後方互換のために旧オーバーロードは引き続き発火します。
結果として processPacket が1回の通知につき2回実行されます。
修正方法
旧オーバーロードの先頭に早期リターンを1行追加するだけです。
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic
) {
// Android
13以降は新オーバーロードが処理するので、ここでは何もしない
if (Build.VERSION.SDK_INT >=
Build.VERSION_CODES.TIRAMISU) return
val payload = characteristic.value?.clone() ?:
ByteArray(0)
processPacket(payload)
}
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
value: ByteArray
) {
processPacket(value)
}