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