package jp.kshoji.blemidi.central;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothProfile;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import jp.kshoji.blemidi.device.MidiInputDevice;
import jp.kshoji.blemidi.device.MidiOutputDevice;
import jp.kshoji.blemidi.listener.OnMidiDeviceAttachedListener;
import jp.kshoji.blemidi.listener.OnMidiDeviceDetachedListener;
import jp.kshoji.blemidi.listener.OnMidiInputEventListener;
import jp.kshoji.blemidi.util.BleMidiDeviceUtils;
import jp.kshoji.blemidi.util.BleMidiParser;
import jp.kshoji.blemidi.util.BleUuidUtils;
import jp.kshoji.blemidi.util.Constants;
/**
* BluetoothGattCallback implementation for BLE MIDI devices.
*
* @author K.Shoji
*/
public final class BleMidiCallback extends BluetoothGattCallback {
private final Map<String, Set<MidiInputDevice>> midiInputDevicesMap = new HashMap<>();
private final Map<String, Set<MidiOutputDevice>> midiOutputDevicesMap = new HashMap<>();
private final Map<String, BluetoothGatt> deviceAddressGattMap = new HashMap<>();
private final Context context;
private OnMidiDeviceAttachedListener midiDeviceAttachedListener;
private OnMidiDeviceDetachedListener midiDeviceDetachedListener;
private boolean needsBonding = false;
/**
* Constructor
*
* @param context the context
*/
public BleMidiCallback(@NonNull final Context context) {
super();
this.context = context;
}
/**
* Checks if the specified device is already connected
*
* @param device the device
* @return true if already connected
*/
boolean isConnected(@NonNull BluetoothDevice device) {
synchronized (deviceAddressGattMap) {
return deviceAddressGattMap.containsKey(device.getAddress());
}
}
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
super.onConnectionStateChange(gatt, status, newState);
// In this method, the `status` parameter shall be ignored.
// so, look `newState` parameter only.
if (newState == BluetoothProfile.STATE_CONNECTED) {
if (!deviceAddressGattMap.containsKey(gatt.getDevice().getAddress())) {
if (gatt.discoverServices()) {
// successfully started discovering
} else {
// already disconnected
disconnectByDeviceAddress(gatt.getDevice().getAddress());
}
}
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
disconnectByDeviceAddress(gatt.getDevice().getAddress());
}
}
@SuppressLint("NewApi")
@Override
public void onServicesDiscovered(final BluetoothGatt gatt, int status) {
super.onServicesDiscovered(gatt, status);
if (status != BluetoothGatt.GATT_SUCCESS) {
return;
}
final String gattDeviceAddress = gatt.getDevice().getAddress();
// find MIDI Input device
if (midiInputDevicesMap.containsKey(gattDeviceAddress)) {
synchronized (midiInputDevicesMap) {
Set<MidiInputDevice> midiInputDevices = midiInputDevicesMap.get(gattDeviceAddress);
for (MidiInputDevice midiInputDevice : midiInputDevices) {
((InternalMidiInputDevice) midiInputDevice).stop();
midiInputDevice.setOnMidiInputEventListener(null);
}
midiInputDevicesMap.remove(gattDeviceAddress);
}
}
MidiInputDevice midiInputDevice = null;
try {
midiInputDevice = new InternalMidiInputDevice(context, gatt);
} catch (IllegalArgumentException iae) {
Log.d(Constants.TAG, iae.getMessage());
}
if (midiInputDevice != null) {
synchronized (midiInputDevicesMap) {
Set<MidiInputDevice> midiInputDevices = midiInputDevicesMap.get(gattDeviceAddress);
if (midiInputDevices == null) {
midiInputDevices = new HashSet<>();
midiInputDevicesMap.put(gattDeviceAddress, midiInputDevices);
}
midiInputDevices.add(midiInputDevice);
}
// don't notify if the same device already connected
if (!deviceAddressGattMap.containsKey(gattDeviceAddress)) {
if (midiDeviceAttachedListener != null) {
midiDeviceAttachedListener.onMidiInputDeviceAttached(midiInputDevice);
}
}
}
// find MIDI Output device
if (midiOutputDevicesMap.containsKey(gattDeviceAddress)) {
synchronized (midiOutputDevicesMap) {
midiOutputDevicesMap.remove(gattDeviceAddress);
}
}
MidiOutputDevice midiOutputDevice = null;
try {
midiOutputDevice = new InternalMidiOutputDevice(context, gatt);
} catch (IllegalArgumentException iae) {
Log.d(Constants.TAG, iae.getMessage());
}
if (midiOutputDevice != null) {
synchronized (midiOutputDevicesMap) {
Set<MidiOutputDevice> midiOutputDevices = midiOutputDevicesMap.get(gattDeviceAddress);
if (midiOutputDevices == null) {
midiOutputDevices = new HashSet<>();
midiOutputDevicesMap.put(gattDeviceAddress, midiOutputDevices);
}
midiOutputDevices.add(midiOutputDevice);
}
// don't notify if the same device already connected
if (!deviceAddressGattMap.containsKey(gattDeviceAddress)) {
if (midiDeviceAttachedListener != null) {
midiDeviceAttachedListener.onMidiOutputDeviceAttached(midiOutputDevice);
}
}
}
if (midiInputDevice != null || midiOutputDevice != null) {
synchronized (deviceAddressGattMap) {
deviceAddressGattMap.put(gattDeviceAddress, gatt);
}
if (needsBonding && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
// Create bond and configure Gatt, if this is BLE MIDI device
BluetoothDevice bluetoothDevice = gatt.getDevice();
if (bluetoothDevice.getBondState() != BluetoothDevice.BOND_BONDED) {
bluetoothDevice.createBond();
bluetoothDevice.setPairingConfirmation(true);
if (bondingBroadcastReceiver != null) {
context.unregisterReceiver(bondingBroadcastReceiver);
}
bondingBroadcastReceiver = new BondingBroadcastReceiver(midiInputDevice, midiOutputDevice);
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
context.registerReceiver(bondingBroadcastReceiver, filter);
}
} else {
if (midiInputDevice != null) {
((InternalMidiInputDevice)midiInputDevice).configureAsCentralDevice();
}
if (midiOutputDevice != null) {
((InternalMidiOutputDevice)midiOutputDevice).configureAsCentralDevice();
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// default is 23: maximum data length is 20 bytes
// max is 512: maximum data length is 509 bytes
gatt.requestMtu(23); // default value
// Set the connection priority to high(for low latency)
gatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH);
}
}
}
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
super.onCharacteristicChanged(gatt, characteristic);
Set<MidiInputDevice> midiInputDevices = midiInputDevicesMap.get(gatt.getDevice().getAddress());
for (MidiInputDevice midiInputDevice : midiInputDevices) {
((InternalMidiInputDevice)midiInputDevice).incomingData(characteristic.getValue());
}
}
@Override
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
super.onDescriptorWrite(gatt, descriptor, status);
if (descriptor != null) {
if (Arrays.equals(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE, descriptor.getValue())) {
gatt.setCharacteristicNotification(descriptor.getCharacteristic(), true);
}
}
}
/**
* Disconnect the specified device
*
* @param midiInputDevice the device
*/
void disconnectDevice(@NonNull MidiInputDevice midiInputDevice) {
if (!(midiInputDevice instanceof InternalMidiInputDevice)) {
return;
}
disconnectByDeviceAddress(midiInputDevice.getDeviceAddress());
}
/**
* Disconnect the specified device
*
* @param midiOutputDevice the device
*/
void disconnectDevice(@NonNull MidiOutputDevice midiOutputDevice) {
if (!(midiOutputDevice instanceof InternalMidiOutputDevice)) {
return;
}
disconnectByDeviceAddress(midiOutputDevice.getDeviceAddress());
}
/**
* Disconnects the device by its address
*
* @param deviceAddress the device address from {@link android.bluetooth.BluetoothGatt}
*/
private void disconnectByDeviceAddress(@NonNull String deviceAddress) {
synchronized (deviceAddressGattMap) {
BluetoothGatt bluetoothGatt = deviceAddressGattMap.get(deviceAddress);
if (bluetoothGatt != null) {
bluetoothGatt.disconnect();
bluetoothGatt.close();
deviceAddressGattMap.remove(deviceAddress);
}
}
synchronized (midiInputDevicesMap) {
Set<MidiInputDevice> midiInputDevices = midiInputDevicesMap.get(deviceAddress);
if (midiInputDevices != null) {
midiInputDevicesMap.remove(deviceAddress);
for (MidiInputDevice midiInputDevice : midiInputDevices) {
((InternalMidiInputDevice) midiInputDevice).stop();
midiInputDevice.setOnMidiInputEventListener(null);
if (midiDeviceDetachedListener != null) {
midiDeviceDetachedListener.onMidiInputDeviceDetached(midiInputDevice);
}
}
midiInputDevices.clear();
}
}
synchronized (midiOutputDevicesMap) {
Set<MidiOutputDevice> midiOutputDevices = midiOutputDevicesMap.get(deviceAddress);
if (midiOutputDevices != null) {
midiOutputDevicesMap.remove(deviceAddress);
for (MidiOutputDevice midiOutputDevice : midiOutputDevices) {
if (midiDeviceDetachedListener != null) {
midiDeviceDetachedListener.onMidiOutputDeviceDetached(midiOutputDevice);
}
}
midiOutputDevices.clear();
}
}
}
/**
* Terminates callback
*/
public void terminate() {
synchronized (deviceAddressGattMap) {
for (BluetoothGatt bluetoothGatt : deviceAddressGattMap.values()) {
bluetoothGatt.disconnect();
bluetoothGatt.close();
}
deviceAddressGattMap.clear();
}
synchronized (midiInputDevicesMap) {
for (Set<MidiInputDevice> midiInputDevices : midiInputDevicesMap.values()) {
for (MidiInputDevice midiInputDevice : midiInputDevices) {
((InternalMidiInputDevice) midiInputDevice).stop();
midiInputDevice.setOnMidiInputEventListener(null);
}
midiInputDevices.clear();
}
midiInputDevicesMap.clear();
}
synchronized (midiOutputDevicesMap) {
midiOutputDevicesMap.clear();
}
if (bondingBroadcastReceiver != null) {
context.unregisterReceiver(bondingBroadcastReceiver);
bondingBroadcastReceiver = null;
}
}
private BondingBroadcastReceiver bondingBroadcastReceiver;
/**
* Set if the Bluetooth LE device need `Pairing`
*
* @param needsBonding if true, request paring with the connecting device
*/
@TargetApi(Build.VERSION_CODES.KITKAT)
public void setNeedsBonding(boolean needsBonding) {
this.needsBonding = needsBonding;
}
/**
* {@link android.content.BroadcastReceiver} for BLE Bonding
*
* @author K.Shoji
*/
private class BondingBroadcastReceiver extends BroadcastReceiver {
final MidiInputDevice midiInputDevice;
final MidiOutputDevice midiOutputDevice;
/**
* Constructor
*
* @param midiInputDevice input device
* @param midiOutputDevice output device
*/
BondingBroadcastReceiver(@Nullable MidiInputDevice midiInputDevice, @Nullable MidiOutputDevice midiOutputDevice) {
this.midiInputDevice = midiInputDevice;
this.midiOutputDevice = midiOutputDevice;
}
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action)) {
final int state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.ERROR);
if (state == BluetoothDevice.BOND_BONDED) {
// successfully bonded
context.unregisterReceiver(this);
bondingBroadcastReceiver = null;
if (midiInputDevice != null) {
((InternalMidiInputDevice) midiInputDevice).configureAsCentralDevice();
}
if (midiOutputDevice != null) {
((InternalMidiOutputDevice) midiOutputDevice).configureAsCentralDevice();
}
}
}
}
}
/**
* Obtains connected input devices
*
* @return Set of {@link jp.kshoji.blemidi.device.MidiInputDevice}
*/
@NonNull
public Set<MidiInputDevice> getMidiInputDevices() {
Collection<Set<MidiInputDevice>> values = midiInputDevicesMap.values();
Set<MidiInputDevice> result = new HashSet<>();
for (Set<MidiInputDevice> value: values) {
result.addAll(value);
}
return Collections.unmodifiableSet(result);
}
/**
* Obtains connected output devices
*
* @return Set of {@link jp.kshoji.blemidi.device.MidiOutputDevice}
*/
@NonNull
public Set<MidiOutputDevice> getMidiOutputDevices() {
Collection<Set<MidiOutputDevice>> values = midiOutputDevicesMap.values();
Set<MidiOutputDevice> result = new HashSet<>();
for (Set<MidiOutputDevice> value: values) {
result.addAll(value);
}
return Collections.unmodifiableSet(result);
}
/**
* Set the listener for attaching devices
*
* @param midiDeviceAttachedListener the listener
*/
public void setOnMidiDeviceAttachedListener(@Nullable OnMidiDeviceAttachedListener midiDeviceAttachedListener) {
this.midiDeviceAttachedListener = midiDeviceAttachedListener;
}
/**
* Set the listener for detaching devices
*
* @param midiDeviceDetachedListener the listener
*/
public void setOnMidiDeviceDetachedListener(@Nullable OnMidiDeviceDetachedListener midiDeviceDetachedListener) {
this.midiDeviceDetachedListener = midiDeviceDetachedListener;
}
/**
* {@link MidiInputDevice} for Central
*
* @author K.Shoji
*/
private static final class InternalMidiInputDevice extends MidiInputDevice {
private final BluetoothGatt bluetoothGatt;
private final BluetoothGattCharacteristic midiInputCharacteristic;
private final BleMidiParser midiParser = new BleMidiParser(this);
/**
* Constructor for Central
*
* @param context the context
* @param bluetoothGatt the gatt of device
* @throws IllegalArgumentException if specified gatt doesn't contain BLE MIDI service
*/
public InternalMidiInputDevice(@NonNull final Context context, @NonNull final BluetoothGatt bluetoothGatt) throws IllegalArgumentException {
super();
this.bluetoothGatt = bluetoothGatt;
BluetoothGattService midiService = BleMidiDeviceUtils.getMidiService(context, bluetoothGatt);
if (midiService == null) {
List<UUID> uuidList = new ArrayList<>();
for (BluetoothGattService service : bluetoothGatt.getServices()) {
uuidList.add(service.getUuid());
}
throw new IllegalArgumentException("MIDI GattService not found from '" + bluetoothGatt.getDevice().getName() + "'. Service UUIDs:" + Arrays.toString(uuidList.toArray()));
}
midiInputCharacteristic = BleMidiDeviceUtils.getMidiInputCharacteristic(context, midiService);
if (midiInputCharacteristic == null) {
throw new IllegalArgumentException("MIDI Input GattCharacteristic not found. Service UUID:" + midiService.getUuid());
}
}
/**
* Stops parser's thread
*/
void stop() {
midiParser.stop();
}
/**
* Configure the device as BLE Central
*/
public void configureAsCentralDevice() {
bluetoothGatt.setCharacteristicNotification(midiInputCharacteristic, true);
List<BluetoothGattDescriptor> descriptors = midiInputCharacteristic.getDescriptors();
for (BluetoothGattDescriptor descriptor : descriptors) {
if (BleUuidUtils.matches(BleUuidUtils.fromShortValue(0x2902), descriptor.getUuid())) {
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
bluetoothGatt.writeDescriptor(descriptor);
}
}
bluetoothGatt.readCharacteristic(midiInputCharacteristic);
}
@Override
public void setOnMidiInputEventListener(OnMidiInputEventListener midiInputEventListener) {
midiParser.setMidiInputEventListener(midiInputEventListener);
}
@NonNull
@Override
public String getDeviceName() {
return bluetoothGatt.getDevice().getName();
}
/**
* Obtains device address
*
* @return device address
*/
@NonNull
public String getDeviceAddress() {
return bluetoothGatt.getDevice().getAddress();
}
/**
* Parse the MIDI data
*
* @param data the MIDI data
*/
private void incomingData(@NonNull byte[] data) {
midiParser.parse(data);
}
}
/**
* {@link jp.kshoji.blemidi.device.MidiOutputDevice} for Central
*
* @author K.Shoji
*/
private static final class InternalMidiOutputDevice extends MidiOutputDevice {
private final BluetoothGatt bluetoothGatt;
private final BluetoothGattCharacteristic midiOutputCharacteristic;
/**
* Constructor for Central
*
* @param context the context
* @param bluetoothGatt the gatt of device
* @throws IllegalArgumentException if specified gatt doesn't contain BLE MIDI service
*/
public InternalMidiOutputDevice(@NonNull final Context context, @NonNull final BluetoothGatt bluetoothGatt) throws IllegalArgumentException {
super();
this.bluetoothGatt = bluetoothGatt;
BluetoothGattService midiService = BleMidiDeviceUtils.getMidiService(context, bluetoothGatt);
if (midiService == null) {
List<UUID> uuidList = new ArrayList<>();
for (BluetoothGattService service : bluetoothGatt.getServices()) {
uuidList.add(service.getUuid());
}
throw new IllegalArgumentException("MIDI GattService not found from '" + bluetoothGatt.getDevice().getName() + "'. Service UUIDs:" + Arrays.toString(uuidList.toArray()));
}
midiOutputCharacteristic = BleMidiDeviceUtils.getMidiOutputCharacteristic(context, midiService);
if (midiOutputCharacteristic == null) {
throw new IllegalArgumentException("MIDI Output GattCharacteristic not found. Service UUID:" + midiService.getUuid());
}
}
/**
* Configure the device as BLE Central
*/
public void configureAsCentralDevice() {
midiOutputCharacteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);
}
@Override
public void transferData(@NonNull byte[] writeBuffer) {
midiOutputCharacteristic.setValue(writeBuffer);
try {
bluetoothGatt.writeCharacteristic(midiOutputCharacteristic);
} catch (Throwable ignored) {
// android.os.DeadObjectException will be thrown
// ignore it
}
}
@NonNull
@Override
public String getDeviceName() {
return bluetoothGatt.getDevice().getName();
}
/**
* Obtains device address
*
* @return device address
*/
@NonNull
public String getDeviceAddress() {
return bluetoothGatt.getDevice().getAddress();
}
}
}