Mike Lockwood | f0a41d1 | 2015-03-24 08:27:11 -0700 | [diff] [blame] | 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; |
Mike Lockwood | f0a41d1 | 2015-03-24 08:27:11 -0700 | [diff] [blame] | 27 | import android.media.midi.MidiDeviceInfo; |
Mike Lockwood | e0a6ca6 | 2015-06-04 13:43:56 -0700 | [diff] [blame] | 28 | import android.media.midi.MidiDeviceServer; |
| 29 | import android.media.midi.MidiDeviceStatus; |
| 30 | import android.media.midi.MidiManager; |
| 31 | import android.media.midi.MidiReceiver; |
Mike Lockwood | f0a41d1 | 2015-03-24 08:27:11 -0700 | [diff] [blame] | 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"; |
Mike Lockwood | 8c26d84 | 2015-05-01 14:36:44 -0700 | [diff] [blame] | 51 | private static final boolean DEBUG = false; |
Mike Lockwood | f0a41d1 | 2015-03-24 08:27:11 -0700 | [diff] [blame] | 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 | |
Mike Lockwood | e0a6ca6 | 2015-06-04 13:43:56 -0700 | [diff] [blame] | 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 | |
Mike Lockwood | f0a41d1 | 2015-03-24 08:27:11 -0700 | [diff] [blame] | 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) { |
Phil Burk | 1f99a32 | 2017-04-14 12:15:56 -0700 | [diff] [blame] | 103 | Log.d(TAG, "Connected to GATT server."); |
| 104 | Log.d(TAG, "Attempting to start service discovery:" + |
Mike Lockwood | f0a41d1 | 2015-03-24 08:27:11 -0700 | [diff] [blame] | 105 | mBluetoothGatt.discoverServices()); |
| 106 | } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { |
| 107 | Log.i(TAG, "Disconnected from GATT server."); |
Mike Lockwood | f0a41d1 | 2015-03-24 08:27:11 -0700 | [diff] [blame] | 108 | close(); |
| 109 | } |
| 110 | } |
| 111 | |
| 112 | @Override |
| 113 | public void onServicesDiscovered(BluetoothGatt gatt, int status) { |
| 114 | if (status == BluetoothGatt.GATT_SUCCESS) { |
Phil Burk | 1f99a32 | 2017-04-14 12:15:56 -0700 | [diff] [blame] | 115 | BluetoothGattService service = gatt.getService(MIDI_SERVICE); |
| 116 | if (service != null) { |
| 117 | Log.d(TAG, "found MIDI_SERVICE"); |
| 118 | BluetoothGattCharacteristic characteristic |
| 119 | = service.getCharacteristic(MIDI_CHARACTERISTIC); |
| 120 | if (characteristic != null) { |
| 121 | Log.d(TAG, "found MIDI_CHARACTERISTIC"); |
| 122 | mCharacteristic = characteristic; |
Mike Lockwood | f0a41d1 | 2015-03-24 08:27:11 -0700 | [diff] [blame] | 123 | |
Phil Burk | 1f99a32 | 2017-04-14 12:15:56 -0700 | [diff] [blame] | 124 | // Request a lower Connection Interval for better latency. |
| 125 | boolean result = gatt.requestConnectionPriority( |
| 126 | BluetoothGatt.CONNECTION_PRIORITY_HIGH); |
| 127 | Log.d(TAG, "requestConnectionPriority(CONNECTION_PRIORITY_HIGH):" |
| 128 | + result); |
| 129 | |
| 130 | // Specification says to read the characteristic first and then |
| 131 | // switch to receiving notifications |
| 132 | mBluetoothGatt.readCharacteristic(characteristic); |
Mike Lockwood | f0a41d1 | 2015-03-24 08:27:11 -0700 | [diff] [blame] | 133 | } |
| 134 | } |
| 135 | } else { |
Mike Lockwood | 9490eae | 2015-05-07 13:39:01 -0700 | [diff] [blame] | 136 | Log.e(TAG, "onServicesDiscovered received: " + status); |
| 137 | close(); |
Mike Lockwood | f0a41d1 | 2015-03-24 08:27:11 -0700 | [diff] [blame] | 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 | |
Phil Burk | 95129f5 | 2015-09-22 12:07:31 -0700 | [diff] [blame] | 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 | |
Mike Lockwood | f0a41d1 | 2015-03-24 08:27:11 -0700 | [diff] [blame] | 155 | BluetoothGattDescriptor descriptor = characteristic.getDescriptor( |
| 156 | CLIENT_CHARACTERISTIC_CONFIG); |
Mike Lockwood | 9490eae | 2015-05-07 13:39:01 -0700 | [diff] [blame] | 157 | if (descriptor != null) { |
| 158 | descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); |
Phil Burk | 95129f5 | 2015-09-22 12:07:31 -0700 | [diff] [blame] | 159 | boolean result = mBluetoothGatt.writeDescriptor(descriptor); |
| 160 | Log.d(TAG, "writeDescriptor returned " + result); |
Mike Lockwood | 9490eae | 2015-05-07 13:39:01 -0700 | [diff] [blame] | 161 | } else { |
| 162 | Log.e(TAG, "No CLIENT_CHARACTERISTIC_CONFIG for device " + mBluetoothDevice); |
| 163 | } |
Phil Burk | 95129f5 | 2015-09-22 12:07:31 -0700 | [diff] [blame] | 164 | |
| 165 | characteristic.setWriteType(originalWriteType); |
Mike Lockwood | f0a41d1 | 2015-03-24 08:27:11 -0700 | [diff] [blame] | 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) { |
Mike Lockwood | 8c26d84 | 2015-05-01 14:36:44 -0700 | [diff] [blame] | 179 | if (DEBUG) { |
| 180 | logByteArray("Received ", characteristic.getValue(), 0, |
| 181 | characteristic.getValue().length); |
| 182 | } |
Mike Lockwood | f0a41d1 | 2015-03-24 08:27:11 -0700 | [diff] [blame] | 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 | |
| 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 |
| 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); |
Mike Lockwood | 8c26d84 | 2015-05-01 14:36:44 -0700 | [diff] [blame] | 211 | if (DEBUG) { |
| 212 | logByteArray("Sent ", mCharacteristic.getValue(), 0, |
| 213 | mCharacteristic.getValue().length); |
| 214 | } |
Mike Lockwood | f0a41d1 | 2015-03-24 08:27:11 -0700 | [diff] [blame] | 215 | mBluetoothGatt.writeCharacteristic(mCharacteristic); |
| 216 | } |
| 217 | } |
| 218 | |
| 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, |
Mike Lockwood | e0a6ca6 | 2015-06-04 13:43:56 -0700 | [diff] [blame] | 237 | null, null, properties, MidiDeviceInfo.TYPE_BLUETOOTH, mDeviceServerCallback); |
Mike Lockwood | f0a41d1 | 2015-03-24 08:27:11 -0700 | [diff] [blame] | 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 { |
Mike Lockwood | 7eb441c | 2015-05-12 13:32:16 -0700 | [diff] [blame] | 258 | mPacketEncoder.send(event.data, 0, event.count, |
Mike Lockwood | f0a41d1 | 2015-03-24 08:27:11 -0700 | [diff] [blame] | 259 | event.getTimestamp()); |
| 260 | } catch (IOException e) { |
Mike Lockwood | 7eb441c | 2015-05-12 13:32:16 -0700 | [diff] [blame] | 261 | Log.e(TAG, "mPacketAccumulator.send failed", e); |
Mike Lockwood | f0a41d1 | 2015-03-24 08:27:11 -0700 | [diff] [blame] | 262 | } |
| 263 | mEventScheduler.addEventToPool(event); |
| 264 | } |
| 265 | Log.d(TAG, "BluetoothMidiDevice thread exit"); |
| 266 | } |
| 267 | }.start(); |
| 268 | } |
| 269 | |
Mike Lockwood | 9490eae | 2015-05-07 13:39:01 -0700 | [diff] [blame] | 270 | private void close() { |
| 271 | synchronized (mBluetoothDevice) { |
Mike Lockwood | e0a6ca6 | 2015-06-04 13:43:56 -0700 | [diff] [blame] | 272 | mEventScheduler.close(); |
| 273 | mService.deviceClosed(mBluetoothDevice); |
| 274 | |
Mike Lockwood | 9490eae | 2015-05-07 13:39:01 -0700 | [diff] [blame] | 275 | if (mDeviceServer != null) { |
| 276 | IoUtils.closeQuietly(mDeviceServer); |
| 277 | mDeviceServer = null; |
Mike Lockwood | 9490eae | 2015-05-07 13:39:01 -0700 | [diff] [blame] | 278 | } |
| 279 | if (mBluetoothGatt != null) { |
| 280 | mBluetoothGatt.close(); |
| 281 | mBluetoothGatt = null; |
| 282 | } |
Mike Lockwood | f0a41d1 | 2015-03-24 08:27:11 -0700 | [diff] [blame] | 283 | } |
| 284 | } |
| 285 | |
| 286 | public IBinder getBinder() { |
| 287 | return mDeviceServer.asBinder(); |
| 288 | } |
| 289 | |
| 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++) { |
Mike Lockwood | 8c26d84 | 2015-05-01 14:36:44 -0700 | [diff] [blame] | 293 | builder.append(String.format("0x%02X", value[i])); |
Mike Lockwood | f0a41d1 | 2015-03-24 08:27:11 -0700 | [diff] [blame] | 294 | if (i != value.length - 1) { |
| 295 | builder.append(", "); |
| 296 | } |
| 297 | } |
| 298 | Log.d(TAG, builder.toString()); |
| 299 | } |
| 300 | } |