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