package jp.kshoji.blemidi.peripheral; import android.annotation.TargetApi; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothGattDescriptor; import android.bluetooth.BluetoothGattServer; import android.bluetooth.BluetoothGattServerCallback; import android.bluetooth.BluetoothGattService; import android.bluetooth.BluetoothManager; import android.bluetooth.BluetoothProfile; import android.bluetooth.le.AdvertiseCallback; import android.bluetooth.le.AdvertiseData; import android.bluetooth.le.AdvertiseSettings; import android.bluetooth.le.BluetoothLeAdvertiser; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Build; import android.os.ParcelUuid; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.Log; import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; 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.BleMidiParser; import jp.kshoji.blemidi.util.BleUuidUtils; import jp.kshoji.blemidi.util.Constants; /** * Represents BLE MIDI Peripheral functions<br /> * Supported with Android Lollipop or newer. * * @author K.Shoji */ @TargetApi(Build.VERSION_CODES.LOLLIPOP) public final class BleMidiPeripheralProvider { /** * Gatt Services */ private static final UUID SERVICE_DEVICE_INFORMATION = BleUuidUtils.fromShortValue(0x180A); private static final UUID SERVICE_BLE_MIDI = UUID.fromString("03b80e5a-ede8-4b33-a751-6ce34ec4c700"); /** * Gatt Characteristics */ private static final short MANUFACTURER_NAME = 0x2A29; private static final short MODEL_NUMBER = 0x2A24; private static final UUID CHARACTERISTIC_MANUFACTURER_NAME = BleUuidUtils.fromShortValue(MANUFACTURER_NAME); private static final UUID CHARACTERISTIC_MODEL_NUMBER = BleUuidUtils.fromShortValue(MODEL_NUMBER); private static final UUID CHARACTERISTIC_BLE_MIDI = UUID.fromString("7772e5db-3868-4112-a1a9-f2669d106bf3"); /** * Gatt Characteristic Descriptor */ private static final UUID DESCRIPTOR_CLIENT_CHARACTERISTIC_CONFIGURATION = BleUuidUtils.fromShortValue(0x2902); private static final int DEVICE_NAME_MAX_LENGTH = 100; private final Context context; private final BluetoothManager bluetoothManager; private final BluetoothLeAdvertiser bluetoothLeAdvertiser; private final BluetoothGattService informationGattService; private final BluetoothGattService midiGattService; private final BluetoothGattCharacteristic midiCharacteristic; private BluetoothGattServer gattServer; private final Map<String, MidiInputDevice> midiInputDevicesMap = new HashMap<>(); private final Map<String, MidiOutputDevice> midiOutputDevicesMap = new HashMap<>(); private final Map<String, BluetoothDevice> bluetoothDevicesMap = new HashMap<>(); private OnMidiDeviceAttachedListener midiDeviceAttachedListener; private OnMidiDeviceDetachedListener midiDeviceDetachedListener; private String manufacturer = "kshoji.jp"; private String deviceName = "BLE MIDI"; /** * Constructor<br /> * Before constructing the instance, check the Bluetooth availability. * * @param context the context */ public BleMidiPeripheralProvider(final Context context) throws UnsupportedOperationException { this.context = context.getApplicationContext(); bluetoothManager = (BluetoothManager) this.context.getSystemService(Context.BLUETOOTH_SERVICE); final BluetoothAdapter bluetoothAdapter = bluetoothManager.getAdapter(); if (bluetoothAdapter == null) { throw new UnsupportedOperationException("Bluetooth is not available."); } if (bluetoothAdapter.isEnabled() == false) { throw new UnsupportedOperationException("Bluetooth is disabled."); } Log.d(Constants.TAG, "isMultipleAdvertisementSupported:" + bluetoothAdapter.isMultipleAdvertisementSupported()); if (bluetoothAdapter.isMultipleAdvertisementSupported() == false) { throw new UnsupportedOperationException("Bluetooth LE Advertising not supported on this device."); } bluetoothLeAdvertiser = bluetoothAdapter.getBluetoothLeAdvertiser(); Log.d(Constants.TAG, "bluetoothLeAdvertiser: " + bluetoothLeAdvertiser); if (bluetoothLeAdvertiser == null) { throw new UnsupportedOperationException("Bluetooth LE Advertising not supported on this device."); } // Device information service informationGattService = new BluetoothGattService(SERVICE_DEVICE_INFORMATION, BluetoothGattService.SERVICE_TYPE_PRIMARY); informationGattService.addCharacteristic(new BluetoothGattCharacteristic(CHARACTERISTIC_MANUFACTURER_NAME, BluetoothGattCharacteristic.PROPERTY_READ, BluetoothGattCharacteristic.PERMISSION_READ)); informationGattService.addCharacteristic(new BluetoothGattCharacteristic(CHARACTERISTIC_MODEL_NUMBER, BluetoothGattCharacteristic.PROPERTY_READ, BluetoothGattCharacteristic.PERMISSION_READ)); // MIDI service midiCharacteristic = new BluetoothGattCharacteristic(CHARACTERISTIC_BLE_MIDI, BluetoothGattCharacteristic.PROPERTY_NOTIFY | BluetoothGattCharacteristic.PROPERTY_READ | BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE, BluetoothGattCharacteristic.PERMISSION_READ | BluetoothGattCharacteristic.PERMISSION_WRITE); BluetoothGattDescriptor descriptor = new BluetoothGattDescriptor(DESCRIPTOR_CLIENT_CHARACTERISTIC_CONFIGURATION, BluetoothGattDescriptor.PERMISSION_READ | BluetoothGattDescriptor.PERMISSION_WRITE); descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); midiCharacteristic.addDescriptor(descriptor); midiCharacteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE); midiGattService = new BluetoothGattService(SERVICE_BLE_MIDI, BluetoothGattService.SERVICE_TYPE_PRIMARY); midiGattService.addCharacteristic(midiCharacteristic); } /** * Starts advertising */ public void startAdvertising() { // register Gatt service to Gatt server if (gattServer == null) { gattServer = bluetoothManager.openGattServer(context, gattServerCallback); } if (gattServer == null) { Log.d(Constants.TAG, "gattServer is null, check Bluetooth is ON."); return; } // these service will be listened. // FIXME these didn't used for service discovery boolean serviceInitialized = false; while (!serviceInitialized) { try { gattServer.addService(informationGattService); gattServer.addService(midiGattService);// NullPointerException, DeadObjectException thrown here serviceInitialized = true; } catch (Exception e) { Log.d(Constants.TAG, "Adding Service failed, retrying.."); try { gattServer.clearServices(); } catch (Throwable ignored) { } try { Thread.sleep(100); } catch (InterruptedException ignored) { } } } // set up advertising setting AdvertiseSettings advertiseSettings = new AdvertiseSettings.Builder() .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH) .setConnectable(true) .setTimeout(0) .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY) .build(); // set up advertising data AdvertiseData advertiseData = new AdvertiseData.Builder() .setIncludeTxPowerLevel(true) .setIncludeDeviceName(true) .build(); // set up scan result AdvertiseData scanResult = new AdvertiseData.Builder() .addServiceUuid(ParcelUuid.fromString(SERVICE_DEVICE_INFORMATION.toString())) .addServiceUuid(ParcelUuid.fromString(SERVICE_BLE_MIDI.toString())) .build(); bluetoothLeAdvertiser.startAdvertising(advertiseSettings, advertiseData, scanResult, advertiseCallback); } /** * Stops advertising */ public void stopAdvertising() { try { bluetoothLeAdvertiser.stopAdvertising(advertiseCallback); } catch (IllegalStateException ignored) { // BT Adapter is not turned ON } if (gattServer != null) { try { gattServer.clearServices(); } catch (Throwable ignored) { // android.os.DeadObjectException gattServer = null; } } } private boolean requireBonding = false; /** * Set if the Bluetooth LE device need `Pairing` * * @param needsPairing if true, request paring with the connecting device */ public void setRequestPairing(boolean needsPairing) { this.requireBonding = needsPairing; } /** * Callback for BLE connection<br /> * nothing to do. */ private final AdvertiseCallback advertiseCallback = new AdvertiseCallback() {}; /** * Disconnects the specified device * * @param midiInputDevice the device */ public void disconnectDevice(@NonNull MidiInputDevice midiInputDevice) { if (!(midiInputDevice instanceof InternalMidiInputDevice)) { return; } disconnectByDeviceAddress(midiInputDevice.getDeviceAddress()); } /** * Disconnects the specified device * * @param midiOutputDevice the device */ public 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 (bluetoothDevicesMap) { BluetoothDevice bluetoothDevice = bluetoothDevicesMap.get(deviceAddress); if (bluetoothDevice != null) { gattServer.cancelConnection(bluetoothDevice); } bluetoothDevicesMap.remove(deviceAddress); } synchronized (midiInputDevicesMap) { MidiInputDevice midiInputDevice = midiInputDevicesMap.get(deviceAddress); if (midiInputDevice != null) { midiInputDevicesMap.remove(deviceAddress); ((InternalMidiInputDevice) midiInputDevice).stop(); midiInputDevice.setOnMidiInputEventListener(null); if (midiDeviceDetachedListener != null) { midiDeviceDetachedListener.onMidiInputDeviceDetached(midiInputDevice); } } } synchronized (midiOutputDevicesMap) { MidiOutputDevice midiOutputDevice = midiOutputDevicesMap.get(deviceAddress); if (midiOutputDevice != null) { midiOutputDevicesMap.remove(deviceAddress); if (midiDeviceDetachedListener != null) { midiDeviceDetachedListener.onMidiOutputDeviceDetached(midiOutputDevice); } } } } /** * Terminates provider */ public void terminate() { stopAdvertising(); synchronized (bluetoothDevicesMap) { for (BluetoothDevice bluetoothDevice : bluetoothDevicesMap.values()) { if (gattServer != null) { gattServer.cancelConnection(bluetoothDevice); } } bluetoothDevicesMap.clear(); } if (gattServer != null) { gattServer.close(); gattServer = null; } synchronized (midiInputDevicesMap) { for (MidiInputDevice midiInputDevice : midiInputDevicesMap.values()) { ((InternalMidiInputDevice) midiInputDevice).stop(); midiInputDevice.setOnMidiInputEventListener(null); } midiInputDevicesMap.clear(); } synchronized (midiOutputDevicesMap) { midiOutputDevicesMap.clear(); } } /** * Callback for BLE data transfer */ final BluetoothGattServerCallback gattServerCallback = new BluetoothGattServerCallback() { @Override public void onConnectionStateChange(BluetoothDevice device, int status, int newState) { super.onConnectionStateChange(device, status, newState); switch (newState) { case BluetoothProfile.STATE_CONNECTED: // check bond status if (requireBonding && device.getBondState() == BluetoothDevice.BOND_NONE) { // create bond device.createBond(); device.setPairingConfirmation(true); IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED); context.registerReceiver(new BroadcastReceiver() { @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) { BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); // successfully bonded context.unregisterReceiver(this); // connecting to the device connectMidiDevice(device); } } } }, filter); } else { // connecting to the device connectMidiDevice(device); } break; case BluetoothProfile.STATE_DISCONNECTED: String deviceAddress = device.getAddress(); synchronized (midiInputDevicesMap) { MidiInputDevice midiInputDevice = midiInputDevicesMap.get(deviceAddress); if (midiInputDevice != null) { midiInputDevicesMap.remove(deviceAddress); ((InternalMidiInputDevice) midiInputDevice).stop(); midiInputDevice.setOnMidiInputEventListener(null); if (midiDeviceDetachedListener != null) { midiDeviceDetachedListener.onMidiInputDeviceDetached(midiInputDevice); } } } synchronized (midiOutputDevicesMap) { MidiOutputDevice midiOutputDevice = midiOutputDevicesMap.get(deviceAddress); if (midiOutputDevice != null) { midiOutputDevicesMap.remove(deviceAddress); if (midiDeviceDetachedListener != null) { midiDeviceDetachedListener.onMidiOutputDeviceDetached(midiOutputDevice); } } } synchronized (bluetoothDevicesMap) { bluetoothDevicesMap.remove(deviceAddress); } break; } } @Override public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattCharacteristic characteristic) { super.onCharacteristicReadRequest(device, requestId, offset, characteristic); UUID characteristicUuid = characteristic.getUuid(); if (BleUuidUtils.matches(CHARACTERISTIC_BLE_MIDI, characteristicUuid)) { // send empty gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, new byte[] {}); } else { switch (BleUuidUtils.toShortValue(characteristicUuid)) { case MODEL_NUMBER: gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, deviceName.getBytes(StandardCharsets.UTF_8)); break; case MANUFACTURER_NAME: gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, manufacturer.getBytes(StandardCharsets.UTF_8)); break; default: // send empty gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, new byte[] {}); break; } } } @Override public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) { super.onCharacteristicWriteRequest(device, requestId, characteristic, preparedWrite, responseNeeded, offset, value); if (BleUuidUtils.matches(characteristic.getUuid(), CHARACTERISTIC_BLE_MIDI)) { MidiInputDevice midiInputDevice = midiInputDevicesMap.get(device.getAddress()); if (midiInputDevice != null) { ((InternalMidiInputDevice)midiInputDevice).incomingData(value); } if (responseNeeded) { // send empty gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, new byte[] {}); } } } @Override public void onDescriptorWriteRequest(BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) { super.onDescriptorWriteRequest(device, requestId, descriptor, preparedWrite, responseNeeded, offset, value); gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, new byte[] {}); } }; /** * Connect as BLE MIDI device with specified {@link android.bluetooth.BluetoothDevice} * * @param device the device */ private void connectMidiDevice(@NonNull BluetoothDevice device) { MidiInputDevice midiInputDevice = new InternalMidiInputDevice(device); MidiOutputDevice midiOutputDevice = new InternalMidiOutputDevice(device, gattServer, midiCharacteristic); String deviceAddress = device.getAddress(); synchronized (midiInputDevicesMap) { midiInputDevicesMap.put(deviceAddress, midiInputDevice); } synchronized (midiOutputDevicesMap) { midiOutputDevicesMap.put(deviceAddress, midiOutputDevice); } synchronized (bluetoothDevicesMap) { bluetoothDevicesMap.put(deviceAddress, device); } if (midiDeviceAttachedListener != null) { midiDeviceAttachedListener.onMidiInputDeviceAttached(midiInputDevice); midiDeviceAttachedListener.onMidiOutputDeviceAttached(midiOutputDevice); } } /** * Obtains the set of {@link jp.kshoji.blemidi.device.MidiInputDevice} that is currently connected * * @return the set contains all connected devices */ @NonNull public Set<MidiInputDevice> getMidiInputDevices() { Set<MidiInputDevice> result = new HashSet<>(); result.addAll(midiInputDevicesMap.values()); return Collections.unmodifiableSet(result); } /** * Obtains the set of {@link jp.kshoji.blemidi.device.MidiOutputDevice} that is currently connected * * @return the set contains all connected devices */ @NonNull public Set<MidiOutputDevice> getMidiOutputDevices() { Set<MidiOutputDevice> result = new HashSet<>(); result.addAll(midiOutputDevicesMap.values()); 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; } /** * Set the manufacturer name * * @param manufacturer the name */ public void setManufacturer(@NonNull String manufacturer) { // length check byte[] manufacturerBytes = manufacturer.getBytes(StandardCharsets.UTF_8); if (manufacturerBytes.length > DEVICE_NAME_MAX_LENGTH) { // shorten byte[] bytes = new byte[DEVICE_NAME_MAX_LENGTH]; System.arraycopy(manufacturerBytes, 0, bytes, 0, DEVICE_NAME_MAX_LENGTH); this.manufacturer = new String(bytes, StandardCharsets.UTF_8); } else { this.manufacturer = manufacturer; } } /** * Set the device name * * @param deviceName the name */ public void setDeviceName(@NonNull String deviceName) { // length check byte[] deviceNameBytes = deviceName.getBytes(StandardCharsets.UTF_8); if (deviceNameBytes.length > DEVICE_NAME_MAX_LENGTH) { // shorten byte[] bytes = new byte[DEVICE_NAME_MAX_LENGTH]; System.arraycopy(deviceNameBytes, 0, bytes, 0, DEVICE_NAME_MAX_LENGTH); this.deviceName = new String(bytes, StandardCharsets.UTF_8); } else { this.deviceName = deviceName; } } /** * {@link jp.kshoji.blemidi.device.MidiInputDevice} for Peripheral * * @author K.Shoji */ private static final class InternalMidiInputDevice extends MidiInputDevice { private final BluetoothDevice bluetoothDevice; private final BleMidiParser midiParser = new BleMidiParser(this); /** * Constructor for Peripheral * * @param bluetoothDevice the device * */ public InternalMidiInputDevice(@NonNull BluetoothDevice bluetoothDevice) { super(); this.bluetoothDevice = bluetoothDevice; } /** * Stops parser's thread */ void stop() { midiParser.stop(); } @Override public void setOnMidiInputEventListener(OnMidiInputEventListener midiInputEventListener) { midiParser.setMidiInputEventListener(midiInputEventListener); } @NonNull @Override public String getDeviceName() { if (TextUtils.isEmpty(bluetoothDevice.getName())) { return bluetoothDevice.getAddress(); } return bluetoothDevice.getName(); } private void incomingData(@NonNull byte[] data) { midiParser.parse(data); } /** * Obtains device address * * @return device address */ @NonNull public String getDeviceAddress() { return bluetoothDevice.getAddress(); } } /** * {@link jp.kshoji.blemidi.device.MidiOutputDevice} for Peripheral * * @author K.Shoji */ private static final class InternalMidiOutputDevice extends MidiOutputDevice { private final BluetoothGattServer bluetoothGattServer; private final BluetoothDevice bluetoothDevice; private final BluetoothGattCharacteristic midiOutputCharacteristic; /** * Constructor for Peripheral * * @param bluetoothDevice the device * @param bluetoothGattServer the gatt server * @param midiCharacteristic the characteristic of device */ public InternalMidiOutputDevice(@NonNull final BluetoothDevice bluetoothDevice, @NonNull final BluetoothGattServer bluetoothGattServer, @NonNull final BluetoothGattCharacteristic midiCharacteristic) { super(); this.bluetoothDevice = bluetoothDevice; this.bluetoothGattServer = bluetoothGattServer; this.midiOutputCharacteristic = midiCharacteristic; } @NonNull @Override public String getDeviceName() { if (TextUtils.isEmpty(bluetoothDevice.getName())) { return bluetoothDevice.getAddress(); } return bluetoothDevice.getName(); } @Override public void transferData(@NonNull byte[] writeBuffer) { midiOutputCharacteristic.setValue(writeBuffer); try { bluetoothGattServer.notifyCharacteristicChanged(bluetoothDevice, midiOutputCharacteristic, false); } catch (Throwable ignored) { // ignore it } } /** * Obtains device address * * @return device address */ public @NonNull String getDeviceAddress() { return bluetoothDevice.getAddress(); } } }