AndroidのBluetooth LE接続で何かと起こる問題とアプリ側でできる対処

仕事でも個人的にもAndroidでBluetooth LEの製品を扱うことが多いです。ですがAndroidはBLE関連機器の扱いがかなりテキトーで、それなりに問題が多いので、情報共有します。

直し方知ってたら教えて下さい。

## 試しているBLE機器

仕事で扱ってるBeacon類と、[Andriders Central Engine(ACE)](https://play.google.com/store/apps/details?id=com.eaglesakura.andriders)で対応しているBLEフィットネス機器を対象に書いています。

* 各種Beacon
* Wahoo 心拍計
* Wahoo ケイデンス・スピードセンサー

## Wi-Fiと併用すると通信不可能になり復帰できない

規格的に周波数帯が同一だからかもしれませんが、Wi-FiをONにした状態（アクセスポイントに接続する・しないは問わない）でBLE機器との通信が行えなくなります。

この状態になった場合、GATTの切断メッセージ(BluetoothGattCallback.onConnectionStateChange)が到達しないため、アプリ側で切断を検知することができなくなります。

Beaconはその特性上、スキャンを何度も行うことになりますが、スキャンのみを繰り返しても行えなくなります。

### 対処方法

心拍のように、「一定時間データが来なかったら切断されているとみなせる」場合は、切断扱いにして再接続を促すのが良いです。ただし、スピード＆ケイデンスセンサーのように「ロードバイクが走っていない場合はデータが到達しない」パターンの機器は検知のしようがないため、諦めています。

また、プロセスKILLによって再度通信できるようになりますが、一定時間が経過すると同様の事態になるため、根治はしません。

ACEでは、アプリのオプションに「Wi-Fiを強制OFFにする」という項目を設置し、BLE機器の不意な切断を抑制するよう努めています。

### 対処方法の問題点

Wi-Fi Only端末（Nexus7等）ではWi-Fiを切ってしまうと通信が行えなくなってしまうので、問題の難易度が増します。思い切ってBluetoothテザリングのみで運用するという手に出るのもありかもしれません（やったことないので、できるかもわかりませんが）

## 通信切断後に再接続できない

心拍計を付けたユーザーとアプリをインストールした端末が物理的に離れるとOS側でBLE切断のハンドリングが行われて、BluetoothGattCallback.onConnectionStateChangeが呼び出されます。

ロードバイクにおいては「コンビニ休憩で自転車から離れる」という動作でよく発生しますが、当然ながらユーザーはロードバイクに再度近づきますし、期待する動作としてはBLE機器を再接続する事になります。

アプリは多くの場合、次のような挙動でBLE機器をチェックし、接続します。ですが一度でも切断されてしまった機器は「スキャン」処理で反応しなくなります。

1. 近距離にあるBLE機器をスキャン
2. 対応している機器であれば、GATT接続を行う
3. データを受信し、アプリ側で処理を行う

### 対処方法

事前にAddressを保存しておき、Addressを指定してBLE機器にGATT接続すれば、切断後も再接続が行えます。ただしこの場合、機器が近くにある・ないに関わらずすぐにBluetoothDeviceオブジェクトが返却されているため、「本当に機器が近くに（接続可能な状態で）存在するのか？」をチェックする仕組みを新設する必要があります。

また、この方法は後述のバッテリードレイン問題を発生させます。

```java:
BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(targetFitnessDeviceAddress);
if (!adapter.isEnabled() || device == null) {
    throw new IllegalStateException();
} else {
    return device;
}
```

## バッテリードレイン

上記のgetRemoteDevice(address)で直接BLE機器に接続するように処理を変えた場合、端末によってはBluetoothのハードウェアがバッテリードレイン化します。これはプロセスをKILLしてもBluetoothが異常にバッテリーを喰い続けるため、一度発生してしまうとアプリ側でハンドリングすることが出来ません。

今のところ発生した端末はXperia ZRのみです（サイコンとして長時間運用しているのがコレだけなので、他の端末は不明）。

### 対処方法

恐らくgetRemoteDeviceで取得したBluetoothDeviceへ接続しに行くと「近くに存在しない端末を求めて電波を出す」からバッテリーを食い過ぎるんじゃね？程度に予測していますが、ハードウェア屋じゃないので審議は不明です。単純に「ケータイが圏外になるとバッテリー食うよね」という論理に近いです。

この状態になった場合、フル充電 → 電池切れシャットダウンまでにBluetoothが全体の60%のバッテリーを喰いました。

ACEでは「接続施行→切断」までの時間を短くし、逆に「切断→再接続施行」までの時間を少しずつ長くとる（ネットワーク処理のバックオフ的な）処理を追加することで対応し、現在運用しています。

## プロセス再起動後もBLE機器のスキャン・接続が行えない

Nexus5、Nexus7、Xperia ZRでたまに発生します。多くの場合プロセスKILLすれば問題なく動きますが、稀にプロセスKILL後もBLE機器へのスキャンや接続が行えなくなります。

その場合、Bluetoothの機能自体をOFF→ONすることで復旧することがあります。

ACEでは自動的にBluetoothをOFF→ONする機能は今のところ入れていませんが、ユーザーが能動的にBluetoothをOFFにした場合、自動的にBluetoothをONに戻し、再度接続を試行するように組み込まれています。

Bluetooth OFF/ONを繰り返しても復旧しない場合、いよいよ端末自体の再起動を行う必要があります。多くの場合それで復旧します。

## Google Fit APIで開発者サービスごとフリーズ

APIに含まれている[フィットネス機器のスキャン(Fitness.BleApi.startBleScan)](https://developer.android.com/reference/com/google/android/gms/fitness/BleApi.html#startBleScan(com.google.android.gms.common.api.GoogleApiClient, com.google.android.gms.fitness.request.StartBleScanRequest))は期待通り動作し、ACEもこの機能を利用してスキャンを実装しています。

ですが、実際のGATT接続はPlay Service 7.0現在の時点であまりコードの品質が良くないようです。どういう原因かはわかりませんが、Google Fitのフィットネス機器接続Serviceは二度目以降の接続でフリーズするため、ACEでは採用されていません。

「二度目」というのは、開発者サービス自体が独立して稼働している都合上、端末全体で回数を共有します。つまりアプリのプロセスをKILLしても意味が無いため、カジュアルに行える復帰手段は端末再起動に限られます。

この頃は直ったのかな？（最近試してない）

## これらに対応するアプリ側の処理

ACEではそういった事情があり、AndroidManifestでプロセスを次のように制御しています。プロセスがアプリ本体を含めて４つも稼働するのでメモリも結構喰っているかと思いますが、基本的にサイクリング中はさほど多くのアプリを同時に動かす必要がないため、運用上は問題無さそうです。

* アプリ本体
* BLE処理を含むサイクルコンピューターService(name=":ace")
 * このServiceはonDestroy時に強制的に自分のプロセスをKILLして自分自身の後始末をする
* チーム接続用通信Service(name=":team")
* 上記２つのServiceの共通処理を行うServide(name=":system")

よりユーザー側の利益を考えるのであれば、BLE処理自体を別プロセスに移して、サイクルコンピューターServiceから自動でプロセスKILLと再起動を（ユーザーが気づかないように）行うほうが良いでしょう。

## GSRカップでの上記BLE処理の運用結果

[2015年4月に行われたGSRカップ](http://gear.goodsmileracing.com/gsrcup02/index.html)という自転車のレースでは参加者が約800名、ANT(2.4GHz)機器とBLE機器が（ほぼ全員が心拍とケイデンスセンサーを装備していたと考えると）会場内に約1500個あったかと思います。場合によっては干渉的な何かが発生するかと思っていましたが、そういった問題は特に発生しませんでした。

Xperia ZRは朝に起動し、その後一度も再起動を行うこと無く、プロセスKILLのみで運用が行えました。また、終日バッテリードレイン化することなく（モバイルブースターによる補給はありましたが）無事に運用を終えています。