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 
PacketReceiver()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
writePacket(byte[] buffer, int count)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 
BluetoothMidiDevice(Context context, BluetoothDevice device, BluetoothMidiService service)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 
close()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 
getBinder()286     public IBinder getBinder() {
287         return mDeviceServer.asBinder();
288     }
289 
logByteArray(String prefix, byte[] value, int offset, int count)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