Android MIDI APIでBLE MIDI機器を繋いだときに嵌まったこと

#AndroidでBLE MIDIを繋いでみて嵌まったことを共有します。

## 経緯
BLE MIDI機器同士（KeyBoardと音源)を繋いで、演奏する際は、HUBとなるPCが必要になる。
仮に、電車の中で演奏したいってなると、さすがにPCは広げてまでやろうとすると大変だ！（笑）
そこで、BLE対応のAndroid端末をBLE MIDI Pacthbayにすればいいんじゃないか？と思いついた。
Android 6.0で、MIDI APIもあるようなので、これを使ってみたが嵌まったので、共有します。

## 動作確認環境

### Android端末
UQ AQUOS L(Android 6.01)
Nexus 5X(Android 8.0)

### BLE MIDIデバイス 
KORG microKEY Air
自作BLE MIDIデバイス（これに音源と繋げた）

### 開発環境
Macbook12(2017)
Android Studio 2.3.3

## 嵌まりどころ
MIDI APIは、一見、BLEを意識しないで使って良さそうに見えるが、BLE APIの挙動に合わせないといけない。
エラーにはならないけど、送受信できないとかの不可思議な現象に嵌まる。

BLE MIDIデバイスを接続する場合は

```java:例
MidiManager mMIDIManager = (MidiManager) getSystemService(Context.MIDI_SERVICE);
BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice("AA:BB:CC:DD:EE:FF");

private MidiReceiver midiReceiver = new MidiReceiver() {
    @Override
    public void onSend(byte[] msg, int offset, int count, long timestamp) throws IOException {
　　　　　　　　〜 省略　〜
    }
};

mMIDIManager.openBluetoothDevice(device, new MidiManager.OnDeviceOpenedListener() {
　　　　　　@Override
   public void onDeviceOpened(MidiDevice midiDevice) {
       if(midiDevice != null)
       {
            if(midiDevice.getInfo().getOutputPortCount() > 0) {
                 MidiOutputPort port = midiDevice.openOutputPort(0);
                 if(port != null) {
                      port.onConnect(midiReceiver);
                 }
            }

            if(midiDevice.getInfo().getInputPortCount() > 0) {
               MidiInputPort port = midiDevice.openInputPort(0);
            }
       }
   }
,mHandler);
```

上記のコードみたいに、MIDIのInputPortとOutoutPortを開く同時に行うコードを書きがちなのですが、
BLEのAPIでありがちな、一つ命令を送ったら、コールバックを待ってから、次の命令を送るようにしないと、
不安定な動作を起こします。

なので、

MIDIデバイスのコールバック関数を登録して、onDeviceStatusChanged()が呼ばれたら、別のポートをオープンするようなコードを記述します

```java:例

// 事前のコールバックを登録すること
mMIDIManager.registerDeviceCallback(mDeviceCallback , mHandler);

// 先程のBLE MIDIデバイスを接続する部分は、Output Portのみを開くコードを書く
mMIDIManager.openBluetoothDevice(device, new MidiManager.OnDeviceOpenedListener() {
　　　　　　@Override
   public void onDeviceOpened(MidiDevice midiDevice) {
       if(midiDevice != null)
       {
            if(midiDevice.getInfo().getOutputPortCount() > 0) {
                 MidiOutputPort port = midiDevice.openOutputPort(0);
                 if(port != null) {
                      port.onConnect(midiReceiver);
                 }
            }
       }
   }
,mHandler);

private MidiManager.DeviceCallback mDeviceCallback =  new MidiManager.DeviceCallback() {
  　@Override
  　public void onDeviceAdded(MidiDeviceInfo device) {
　　　　　　〜 省略 〜
  　}

   @Override
   public void onDeviceRemoved(MidiDeviceInfo device) {
　　　　　　　〜 省略 〜
  　}

   @Override
   public void onDeviceStatusChanged(MidiDeviceStatus status) {
       super.onDeviceStatusChanged(status);

       // OutputPortのオープンが成功したら、InputPortをオープンする
       if(status.getOutputPortOpenCount(0) == 1) {
             if(midiDevice.getInfo().getInputPortCount() > 0) {
                   MidiInputPort port = midiDevice.openInputPort(0);
             }
       }
　　　　　　}
};
```

## 後始末
openBluetoothDevice（）関数を使って、BLE MIDIデバイスをAndroid端末で、MIDIデバイスとして登録(BLE接続される)されるのですが、何もしないで、このままアプリを終了させてしまうと、BLEが繋がったままになり、BLEをサポートしていない、MIDI APIを使ったアプリから操作できるという気持ち悪い状態になってしまうので、デバイス接続を解除するというコードを書く必要があります。

オープンしたデバイスは、onPause()関数が呼ばれた際に、close()するようなコードを書いておきましょう。


```java:例
private MidiDevice mMidiDevice = null;

mMIDIManager.openBluetoothDevice(device, new MidiManager.OnDeviceOpenedListener() {
　　　　　　@Override
   public void onDeviceOpened(MidiDevice midiDevice) {
       if(midiDevice != null)
       {
            mMidiDevice = midiDevice;
            〜 省略　〜
       }
   }
,mHandler);

@Override
protected void onPause() {
    super.onPause();

    if(mMidiDevice != null) {
        try { 
           mMidiDevice.close();
           mMidiDevice = null;
        } catch(IOException e) {
        }
    }
}
```

## Android 6.0.1での事象！？（バグ）

デバイスのコールバックを登録するということは、アプリ終了時等に、コールバックに登録を解除する必要があります。

そうしないと、１回のコールバックのはずなのに、複数回呼ばれてしまうという状況になってしまいます。

コールバック解除するためには、mMIDIManager.unregisterDeviceCallback()を呼ぶ訳なのですが、

Android 6.0.1(UQ AQUOS L)は、コールバックの解除が行われないバグをもっているようです。

Android 8.0(Nexus 5X)では、問題ありませんでした。

以上、ご参考までに