• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.MidiDevice;
28 import android.media.midi.MidiDeviceInfo;
29 import android.media.midi.MidiDeviceServer;
30 import android.media.midi.MidiDeviceStatus;
31 import android.media.midi.MidiManager;
32 import android.media.midi.MidiReceiver;
33 import android.os.Bundle;
34 import android.os.IBinder;
35 import android.util.Log;
36 
37 import com.android.internal.midi.MidiEventScheduler;
38 import com.android.internal.midi.MidiEventScheduler.MidiEvent;
39 
40 import libcore.io.IoUtils;
41 
42 import java.io.IOException;
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 DEFAULT_PACKET_SIZE = 20;
54     private static final int MAX_PACKET_SIZE = 512;
55 
56     //  Bluetooth MIDI Gatt service UUID
57     private static final UUID MIDI_SERVICE = UUID.fromString(
58             "03B80E5A-EDE8-4B33-A751-6CE34EC4C700");
59     // Bluetooth MIDI Gatt characteristic UUID
60     private static final UUID MIDI_CHARACTERISTIC = UUID.fromString(
61             "7772E5DB-3868-4112-A1A9-F2669D106BF3");
62     // Descriptor UUID for enabling characteristic changed notifications
63     private static final UUID CLIENT_CHARACTERISTIC_CONFIG = UUID.fromString(
64             "00002902-0000-1000-8000-00805f9b34fb");
65 
66     private final BluetoothDevice mBluetoothDevice;
67     private final Context mContext;
68     private final BluetoothMidiService mService;
69     private final MidiManager mMidiManager;
70     private MidiReceiver mOutputReceiver;
71     private final MidiEventScheduler mEventScheduler = new MidiEventScheduler();
72 
73     private MidiDeviceServer mDeviceServer;
74     private BluetoothGatt mBluetoothGatt;
75 
76     private BluetoothGattCharacteristic mCharacteristic;
77 
78     // PacketReceiver for receiving formatted packets from our BluetoothPacketEncoder
79     private final PacketReceiver mPacketReceiver = new PacketReceiver();
80 
81     private final BluetoothPacketEncoder mPacketEncoder
82             = new BluetoothPacketEncoder(mPacketReceiver, MAX_PACKET_SIZE);
83 
84     private final BluetoothPacketDecoder mPacketDecoder
85             = new BluetoothPacketDecoder(MAX_PACKET_SIZE);
86 
87     private final MidiDeviceServer.Callback mDeviceServerCallback
88             = new MidiDeviceServer.Callback() {
89         @Override
90         public void onDeviceStatusChanged(MidiDeviceServer server, MidiDeviceStatus status) {
91         }
92 
93         @Override
94         public void onClose() {
95             close();
96         }
97     };
98 
99     private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
100         @Override
101         public void onConnectionStateChange(BluetoothGatt gatt, int status,
102                 int newState) {
103             Log.d(TAG, "onConnectionStateChange() status: " + status + ", newState: " + newState);
104             String intentAction;
105             if (newState == BluetoothProfile.STATE_CONNECTED) {
106                 Log.d(TAG, "Connected to GATT server.");
107                 Log.d(TAG, "Attempting to start service discovery:" +
108                         mBluetoothGatt.discoverServices());
109             } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
110                 Log.i(TAG, "Disconnected from GATT server.");
111                 close();
112             }
113         }
114 
115         @Override
116         public void onServicesDiscovered(BluetoothGatt gatt, int status) {
117             Log.d(TAG, "onServicesDiscovered() status: " +  status);
118             if (status == BluetoothGatt.GATT_SUCCESS) {
119                 BluetoothGattService service = gatt.getService(MIDI_SERVICE);
120                 if (service != null) {
121                     Log.d(TAG, "found MIDI_SERVICE");
122                     BluetoothGattCharacteristic characteristic
123                             = service.getCharacteristic(MIDI_CHARACTERISTIC);
124                     if (characteristic != null) {
125                         Log.d(TAG, "found MIDI_CHARACTERISTIC");
126                         mCharacteristic = characteristic;
127 
128                         // Request a lower Connection Interval for better latency.
129                         boolean result = gatt.requestConnectionPriority(
130                                 BluetoothGatt.CONNECTION_PRIORITY_HIGH);
131                         Log.d(TAG, "requestConnectionPriority(CONNECTION_PRIORITY_HIGH):"
132                             + result);
133 
134                         // Specification says to read the characteristic first and then
135                         // switch to receiving notifications
136                         mBluetoothGatt.readCharacteristic(characteristic);
137 
138                         // Request higher MTU size
139                         if (!gatt.requestMtu(MAX_PACKET_SIZE)) {
140                             Log.e(TAG, "request mtu failed");
141                             mPacketEncoder.setMaxPacketSize(DEFAULT_PACKET_SIZE);
142                             mPacketDecoder.setMaxPacketSize(DEFAULT_PACKET_SIZE);
143                         }
144                     }
145                 }
146             } else {
147                 Log.e(TAG, "onServicesDiscovered received: " + status);
148                 close();
149             }
150         }
151 
152         @Override
153         public void onCharacteristicRead(BluetoothGatt gatt,
154                 BluetoothGattCharacteristic characteristic,
155                 byte[] value,
156                 int status) {
157             Log.d(TAG, "onCharacteristicRead status:" + status);
158 
159             StackTraceElement[] elements = Thread.currentThread().getStackTrace();
160             for (StackTraceElement element : elements) {
161                 Log.i(TAG, "  " + element);
162             }
163             // switch to receiving notifications after initial characteristic read
164             mBluetoothGatt.setCharacteristicNotification(characteristic, true);
165 
166             // Use writeType that requests acknowledgement.
167             // This improves compatibility with various BLE-MIDI devices.
168             int originalWriteType = characteristic.getWriteType();
169             characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
170 
171             BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
172                     CLIENT_CHARACTERISTIC_CONFIG);
173             if (descriptor != null) {
174                 int result = mBluetoothGatt.writeDescriptor(descriptor,
175                         BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
176                 Log.d(TAG, "writeDescriptor returned " + result);
177             } else {
178                 Log.e(TAG, "No CLIENT_CHARACTERISTIC_CONFIG for device " + mBluetoothDevice);
179             }
180 
181             characteristic.setWriteType(originalWriteType);
182         }
183 
184         @Override
185         public void onCharacteristicWrite(BluetoothGatt gatt,
186                 BluetoothGattCharacteristic characteristic,
187                 int status) {
188             Log.d(TAG, "onCharacteristicWrite " + status);
189             mPacketEncoder.writeComplete();
190         }
191 
192         @Override
193         public void onCharacteristicChanged(BluetoothGatt gatt,
194                                             BluetoothGattCharacteristic characteristic,
195                                             byte[] value) {
196             if (DEBUG) {
197                 logByteArray("Received BLE packet", value, 0,
198                         value.length);
199             }
200             mPacketDecoder.decodePacket(value, mOutputReceiver);
201         }
202 
203         @Override
204         public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
205             Log.d(TAG, "onMtuChanged callback received. mtu: " + mtu + ", status: " + status);
206             if (status == BluetoothGatt.GATT_SUCCESS) {
207                 mPacketEncoder.setMaxPacketSize(Math.min(mtu, MAX_PACKET_SIZE));
208                 mPacketDecoder.setMaxPacketSize(Math.min(mtu, MAX_PACKET_SIZE));
209             } else {
210                 mPacketEncoder.setMaxPacketSize(DEFAULT_PACKET_SIZE);
211                 mPacketDecoder.setMaxPacketSize(DEFAULT_PACKET_SIZE);
212             }
213         }
214     };
215 
216     // This receives MIDI data that has already been passed through our MidiEventScheduler
217     // and has been normalized by our MidiFramer.
218 
219     private class PacketReceiver implements PacketEncoder.PacketReceiver {
220         private byte[] mCachedBuffer;
221 
PacketReceiver()222         public PacketReceiver() {
223         }
224 
225         @Override
writePacket(byte[] buffer, int count)226         public boolean writePacket(byte[] buffer, int count) {
227             if (mCharacteristic == null) {
228                 Log.w(TAG, "not ready to send packet yet");
229                 return false;
230             }
231 
232             // Cache the previous buffer for writePacket so buffers aren't
233             // consistently created if the buffer sizes are consistent.
234             if ((mCachedBuffer == null) || (mCachedBuffer.length != count)) {
235                 mCachedBuffer = new byte[count];
236             }
237             System.arraycopy(buffer, 0, mCachedBuffer, 0, count);
238 
239             if (DEBUG) {
240                 logByteArray("Sent ", mCachedBuffer, 0, mCachedBuffer.length);
241             }
242 
243             int result = mBluetoothGatt.writeCharacteristic(mCharacteristic, mCachedBuffer,
244                     mCharacteristic.getWriteType());
245             if (result != BluetoothGatt.GATT_SUCCESS) {
246                 Log.w(TAG, "could not write characteristic to Bluetooth GATT. result: " + result);
247                 return false;
248             }
249 
250             return true;
251         }
252     }
253 
BluetoothMidiDevice(Context context, BluetoothDevice device, BluetoothMidiService service)254     public BluetoothMidiDevice(Context context, BluetoothDevice device,
255             BluetoothMidiService service) {
256         mBluetoothDevice = device;
257         mService = service;
258 
259         // Set a small default packet size in case there is an issue with configuring MTUs.
260         mPacketEncoder.setMaxPacketSize(DEFAULT_PACKET_SIZE);
261         mPacketDecoder.setMaxPacketSize(DEFAULT_PACKET_SIZE);
262 
263         mBluetoothGatt = mBluetoothDevice.connectGatt(context, false, mGattCallback);
264 
265         mContext = context;
266         mMidiManager = (MidiManager)context.getSystemService(Context.MIDI_SERVICE);
267 
268         Bundle properties = new Bundle();
269         properties.putString(MidiDeviceInfo.PROPERTY_NAME, mBluetoothGatt.getDevice().getName());
270         properties.putParcelable(MidiDeviceInfo.PROPERTY_BLUETOOTH_DEVICE,
271                 mBluetoothGatt.getDevice());
272 
273         MidiReceiver[] inputPortReceivers = new MidiReceiver[1];
274         inputPortReceivers[0] = mEventScheduler.getReceiver();
275 
276         mDeviceServer = mMidiManager.createDeviceServer(inputPortReceivers, 1,
277                 null, null, properties, MidiDeviceInfo.TYPE_BLUETOOTH,
278                 MidiDeviceInfo.PROTOCOL_UNKNOWN, mDeviceServerCallback);
279 
280         mOutputReceiver = mDeviceServer.getOutputPortReceivers()[0];
281 
282         // This thread waits for outgoing messages from our MidiEventScheduler
283         // And forwards them to our MidiFramer to be prepared to send via Bluetooth.
284         new Thread("BluetoothMidiDevice " + mBluetoothDevice) {
285             @Override
286             public void run() {
287                 while (true) {
288                     MidiEvent event;
289                     try {
290                         event = (MidiEvent)mEventScheduler.waitNextEvent();
291                     } catch (InterruptedException e) {
292                         // try again
293                         continue;
294                     }
295                     if (event == null) {
296                         break;
297                     }
298                     try {
299                         mPacketEncoder.send(event.data, 0, event.count,
300                                 event.getTimestamp());
301                     } catch (IOException e) {
302                         Log.e(TAG, "mPacketAccumulator.send failed", e);
303                     }
304                     mEventScheduler.addEventToPool(event);
305                 }
306                 Log.d(TAG, "BluetoothMidiDevice thread exit");
307             }
308         }.start();
309     }
310 
close()311     private void close() {
312         synchronized (mBluetoothDevice) {
313             mEventScheduler.close();
314             mService.deviceClosed(mBluetoothDevice);
315 
316             if (mDeviceServer != null) {
317                 IoUtils.closeQuietly(mDeviceServer);
318                 mDeviceServer = null;
319             }
320             if (mBluetoothGatt != null) {
321                 mBluetoothGatt.close();
322                 mBluetoothGatt = null;
323             }
324         }
325     }
326 
openBluetoothDevice(BluetoothDevice btDevice)327     void openBluetoothDevice(BluetoothDevice btDevice) {
328         Log.d(TAG, "openBluetoothDevice() device: " + btDevice);
329 
330         MidiManager midiManager = mContext.getSystemService(MidiManager.class);
331         midiManager.openBluetoothDevice(btDevice,
332                 new MidiManager.OnDeviceOpenedListener() {
333                     @Override
334                     public void onDeviceOpened(MidiDevice device) {
335                     }
336                 }, null);
337     }
338 
getBinder()339     public IBinder getBinder() {
340         return mDeviceServer.asBinder();
341     }
342 
logByteArray(String prefix, byte[] value, int offset, int count)343     private static void logByteArray(String prefix, byte[] value, int offset, int count) {
344         StringBuilder builder = new StringBuilder(prefix);
345         for (int i = offset; i < count; i++) {
346             builder.append(String.format("0x%02X", value[i]));
347             if (i != value.length - 1) {
348                 builder.append(", ");
349             }
350         }
351         Log.d(TAG, builder.toString());
352     }
353 }
354