Home | History | Annotate | Download | only in bluetoothmidiservice
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.bluetoothmidiservice;
     18 
     19 import android.bluetooth.BluetoothDevice;
     20 import android.bluetooth.BluetoothGatt;
     21 import android.bluetooth.BluetoothGattCallback;
     22 import android.bluetooth.BluetoothGattCharacteristic;
     23 import android.bluetooth.BluetoothGattDescriptor;
     24 import android.bluetooth.BluetoothGattService;
     25 import android.bluetooth.BluetoothProfile;
     26 import android.content.Context;
     27 import android.media.midi.MidiDeviceInfo;
     28 import android.media.midi.MidiDeviceServer;
     29 import android.media.midi.MidiDeviceStatus;
     30 import android.media.midi.MidiManager;
     31 import android.media.midi.MidiReceiver;
     32 import android.os.Bundle;
     33 import android.os.IBinder;
     34 import android.util.Log;
     35 
     36 import com.android.internal.midi.MidiEventScheduler;
     37 import com.android.internal.midi.MidiEventScheduler.MidiEvent;
     38 
     39 import libcore.io.IoUtils;
     40 
     41 import java.io.IOException;
     42 import java.util.List;
     43 import java.util.UUID;
     44 
     45 /**
     46  * Class used to implement a Bluetooth MIDI device.
     47  */
     48 public final class BluetoothMidiDevice {
     49 
     50     private static final String TAG = "BluetoothMidiDevice";
     51     private static final boolean DEBUG = false;
     52 
     53     private static final int MAX_PACKET_SIZE = 20;
     54 
     55     //  Bluetooth MIDI Gatt service UUID
     56     private static final UUID MIDI_SERVICE = UUID.fromString(
     57             "03B80E5A-EDE8-4B33-A751-6CE34EC4C700");
     58     // Bluetooth MIDI Gatt characteristic UUID
     59     private static final UUID MIDI_CHARACTERISTIC = UUID.fromString(
     60             "7772E5DB-3868-4112-A1A9-F2669D106BF3");
     61     // Descriptor UUID for enabling characteristic changed notifications
     62     private static final UUID CLIENT_CHARACTERISTIC_CONFIG = UUID.fromString(
     63             "00002902-0000-1000-8000-00805f9b34fb");
     64 
     65     private final BluetoothDevice mBluetoothDevice;
     66     private final BluetoothMidiService mService;
     67     private final MidiManager mMidiManager;
     68     private MidiReceiver mOutputReceiver;
     69     private final MidiEventScheduler mEventScheduler = new MidiEventScheduler();
     70 
     71     private MidiDeviceServer mDeviceServer;
     72     private BluetoothGatt mBluetoothGatt;
     73 
     74     private BluetoothGattCharacteristic mCharacteristic;
     75 
     76     // PacketReceiver for receiving formatted packets from our BluetoothPacketEncoder
     77     private final PacketReceiver mPacketReceiver = new PacketReceiver();
     78 
     79     private final BluetoothPacketEncoder mPacketEncoder
     80             = new BluetoothPacketEncoder(mPacketReceiver, MAX_PACKET_SIZE);
     81 
     82     private final BluetoothPacketDecoder mPacketDecoder
     83             = new BluetoothPacketDecoder(MAX_PACKET_SIZE);
     84 
     85     private final MidiDeviceServer.Callback mDeviceServerCallback
     86             = new MidiDeviceServer.Callback() {
     87         @Override
     88         public void onDeviceStatusChanged(MidiDeviceServer server, MidiDeviceStatus status) {
     89         }
     90 
     91         @Override
     92         public void onClose() {
     93             close();
     94         }
     95     };
     96 
     97     private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
     98         @Override
     99         public void onConnectionStateChange(BluetoothGatt gatt, int status,
    100                 int newState) {
    101             String intentAction;
    102             if (newState == BluetoothProfile.STATE_CONNECTED) {
    103                 Log.i(TAG, "Connected to GATT server.");
    104                 Log.i(TAG, "Attempting to start service discovery:" +
    105                         mBluetoothGatt.discoverServices());
    106             } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
    107                 Log.i(TAG, "Disconnected from GATT server.");
    108                 close();
    109             }
    110         }
    111 
    112         @Override
    113         public void onServicesDiscovered(BluetoothGatt gatt, int status) {
    114             if (status == BluetoothGatt.GATT_SUCCESS) {
    115                 List<BluetoothGattService> services = mBluetoothGatt.getServices();
    116                 for (BluetoothGattService service : services) {
    117                     if (MIDI_SERVICE.equals(service.getUuid())) {
    118                         Log.d(TAG, "found MIDI_SERVICE");
    119                         List<BluetoothGattCharacteristic> characteristics
    120                             = service.getCharacteristics();
    121                         for (BluetoothGattCharacteristic characteristic : characteristics) {
    122                             if (MIDI_CHARACTERISTIC.equals(characteristic.getUuid())) {
    123                                 Log.d(TAG, "found MIDI_CHARACTERISTIC");
    124                                 mCharacteristic = characteristic;
    125 
    126                                 // Specification says to read the characteristic first and then
    127                                 // switch to receiving notifications
    128                                 mBluetoothGatt.readCharacteristic(characteristic);
    129                                 break;
    130                             }
    131                         }
    132                         break;
    133                     }
    134                 }
    135             } else {
    136                 Log.e(TAG, "onServicesDiscovered received: " + status);
    137                 close();
    138             }
    139         }
    140 
    141         @Override
    142         public void onCharacteristicRead(BluetoothGatt gatt,
    143                 BluetoothGattCharacteristic characteristic,
    144                 int status) {
    145             Log.d(TAG, "onCharacteristicRead " + status);
    146 
    147             // switch to receiving notifications after initial characteristic read
    148             mBluetoothGatt.setCharacteristicNotification(characteristic, true);
    149 
    150             // Use writeType that requests acknowledgement.
    151             // This improves compatibility with various BLE-MIDI devices.
    152             int originalWriteType = characteristic.getWriteType();
    153             characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
    154 
    155             BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
    156                     CLIENT_CHARACTERISTIC_CONFIG);
    157             if (descriptor != null) {
    158                 descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
    159                 boolean result = mBluetoothGatt.writeDescriptor(descriptor);
    160                 Log.d(TAG, "writeDescriptor returned " + result);
    161             } else {
    162                 Log.e(TAG, "No CLIENT_CHARACTERISTIC_CONFIG for device " + mBluetoothDevice);
    163             }
    164 
    165             characteristic.setWriteType(originalWriteType);
    166         }
    167 
    168         @Override
    169         public void onCharacteristicWrite(BluetoothGatt gatt,
    170                 BluetoothGattCharacteristic characteristic,
    171                 int status) {
    172             Log.d(TAG, "onCharacteristicWrite " + status);
    173             mPacketEncoder.writeComplete();
    174         }
    175 
    176         @Override
    177         public void onCharacteristicChanged(BluetoothGatt gatt,
    178                                             BluetoothGattCharacteristic characteristic) {
    179             if (DEBUG) {
    180                 logByteArray("Received ", characteristic.getValue(), 0,
    181                         characteristic.getValue().length);
    182             }
    183             mPacketDecoder.decodePacket(characteristic.getValue(), mOutputReceiver);
    184         }
    185     };
    186 
    187     // This receives MIDI data that has already been passed through our MidiEventScheduler
    188     // and has been normalized by our MidiFramer.
    189 
    190     private class PacketReceiver implements PacketEncoder.PacketReceiver {
    191         // buffers of every possible packet size
    192         private final byte[][] mWriteBuffers;
    193 
    194         public PacketReceiver() {
    195             // Create buffers of every possible packet size
    196             mWriteBuffers = new byte[MAX_PACKET_SIZE + 1][];
    197             for (int i = 0; i <= MAX_PACKET_SIZE; i++) {
    198                 mWriteBuffers[i] = new byte[i];
    199             }
    200         }
    201 
    202         @Override
    203         public void writePacket(byte[] buffer, int count) {
    204             if (mCharacteristic == null) {
    205                 Log.w(TAG, "not ready to send packet yet");
    206                 return;
    207             }
    208             byte[] writeBuffer = mWriteBuffers[count];
    209             System.arraycopy(buffer, 0, writeBuffer, 0, count);
    210             mCharacteristic.setValue(writeBuffer);
    211             if (DEBUG) {
    212                 logByteArray("Sent ", mCharacteristic.getValue(), 0,
    213                        mCharacteristic.getValue().length);
    214             }
    215             mBluetoothGatt.writeCharacteristic(mCharacteristic);
    216         }
    217     }
    218 
    219     public BluetoothMidiDevice(Context context, BluetoothDevice device,
    220             BluetoothMidiService service) {
    221         mBluetoothDevice = device;
    222         mService = service;
    223 
    224         mBluetoothGatt = mBluetoothDevice.connectGatt(context, false, mGattCallback);
    225 
    226         mMidiManager = (MidiManager)context.getSystemService(Context.MIDI_SERVICE);
    227 
    228         Bundle properties = new Bundle();
    229         properties.putString(MidiDeviceInfo.PROPERTY_NAME, mBluetoothGatt.getDevice().getName());
    230         properties.putParcelable(MidiDeviceInfo.PROPERTY_BLUETOOTH_DEVICE,
    231                 mBluetoothGatt.getDevice());
    232 
    233         MidiReceiver[] inputPortReceivers = new MidiReceiver[1];
    234         inputPortReceivers[0] = mEventScheduler.getReceiver();
    235 
    236         mDeviceServer = mMidiManager.createDeviceServer(inputPortReceivers, 1,
    237                 null, null, properties, MidiDeviceInfo.TYPE_BLUETOOTH, mDeviceServerCallback);
    238 
    239         mOutputReceiver = mDeviceServer.getOutputPortReceivers()[0];
    240 
    241         // This thread waits for outgoing messages from our MidiEventScheduler
    242         // And forwards them to our MidiFramer to be prepared to send via Bluetooth.
    243         new Thread("BluetoothMidiDevice " + mBluetoothDevice) {
    244             @Override
    245             public void run() {
    246                 while (true) {
    247                     MidiEvent event;
    248                     try {
    249                         event = (MidiEvent)mEventScheduler.waitNextEvent();
    250                     } catch (InterruptedException e) {
    251                         // try again
    252                         continue;
    253                     }
    254                     if (event == null) {
    255                         break;
    256                     }
    257                     try {
    258                         mPacketEncoder.send(event.data, 0, event.count,
    259                                 event.getTimestamp());
    260                     } catch (IOException e) {
    261                         Log.e(TAG, "mPacketAccumulator.send failed", e);
    262                     }
    263                     mEventScheduler.addEventToPool(event);
    264                 }
    265                 Log.d(TAG, "BluetoothMidiDevice thread exit");
    266             }
    267         }.start();
    268     }
    269 
    270     private void close() {
    271         synchronized (mBluetoothDevice) {
    272             mEventScheduler.close();
    273             mService.deviceClosed(mBluetoothDevice);
    274 
    275             if (mDeviceServer != null) {
    276                 IoUtils.closeQuietly(mDeviceServer);
    277                 mDeviceServer = null;
    278             }
    279             if (mBluetoothGatt != null) {
    280                 mBluetoothGatt.close();
    281                 mBluetoothGatt = null;
    282             }
    283         }
    284     }
    285 
    286     public IBinder getBinder() {
    287         return mDeviceServer.asBinder();
    288     }
    289 
    290     private static void logByteArray(String prefix, byte[] value, int offset, int count) {
    291         StringBuilder builder = new StringBuilder(prefix);
    292         for (int i = offset; i < count; i++) {
    293             builder.append(String.format("0x%02X", value[i]));
    294             if (i != value.length - 1) {
    295                 builder.append(", ");
    296             }
    297         }
    298         Log.d(TAG, builder.toString());
    299     }
    300 }
    301