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