1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Android】BLE通知が2回処理されてデータが壊れた話

1
Posted at

はじめに

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)
  }
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?