blob: ece700dc8b6d292224832fd4085601aa1b11419d [file] [log] [blame]
Mike Lockwoodf0a41d12015-03-24 08:27:11 -07001/*
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
17package com.android.bluetoothmidiservice;
18
19import android.bluetooth.BluetoothDevice;
20import android.bluetooth.BluetoothGatt;
21import android.bluetooth.BluetoothGattCallback;
22import android.bluetooth.BluetoothGattCharacteristic;
23import android.bluetooth.BluetoothGattDescriptor;
24import android.bluetooth.BluetoothGattService;
25import android.bluetooth.BluetoothProfile;
26import android.content.Context;
Mike Lockwoodf0a41d12015-03-24 08:27:11 -070027import android.media.midi.MidiDeviceInfo;
Mike Lockwoode0a6ca62015-06-04 13:43:56 -070028import android.media.midi.MidiDeviceServer;
29import android.media.midi.MidiDeviceStatus;
30import android.media.midi.MidiManager;
31import android.media.midi.MidiReceiver;
Mike Lockwoodf0a41d12015-03-24 08:27:11 -070032import android.os.Bundle;
33import android.os.IBinder;
34import android.util.Log;
35
36import com.android.internal.midi.MidiEventScheduler;
37import com.android.internal.midi.MidiEventScheduler.MidiEvent;
38
39import libcore.io.IoUtils;
40
41import java.io.IOException;
42import java.util.List;
43import java.util.UUID;
44
45/**
46 * Class used to implement a Bluetooth MIDI device.
47 */
48public final class BluetoothMidiDevice {
49
50 private static final String TAG = "BluetoothMidiDevice";
Mike Lockwood8c26d842015-05-01 14:36:44 -070051 private static final boolean DEBUG = false;
Mike Lockwoodf0a41d12015-03-24 08:27:11 -070052
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 Lockwoode0a6ca62015-06-04 13:43:56 -070085 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 Lockwoodf0a41d12015-03-24 08:27:11 -070097 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 Burk1f99a322017-04-14 12:15:56 -0700103 Log.d(TAG, "Connected to GATT server.");
104 Log.d(TAG, "Attempting to start service discovery:" +
Mike Lockwoodf0a41d12015-03-24 08:27:11 -0700105 mBluetoothGatt.discoverServices());
106 } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
107 Log.i(TAG, "Disconnected from GATT server.");
Mike Lockwoodf0a41d12015-03-24 08:27:11 -0700108 close();
109 }
110 }
111
112 @Override
113 public void onServicesDiscovered(BluetoothGatt gatt, int status) {
114 if (status == BluetoothGatt.GATT_SUCCESS) {
Phil Burk1f99a322017-04-14 12:15:56 -0700115 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 Lockwoodf0a41d12015-03-24 08:27:11 -0700123
Phil Burk1f99a322017-04-14 12:15:56 -0700124 // 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 Lockwoodf0a41d12015-03-24 08:27:11 -0700133 }
134 }
135 } else {
Mike Lockwood9490eae2015-05-07 13:39:01 -0700136 Log.e(TAG, "onServicesDiscovered received: " + status);
137 close();
Mike Lockwoodf0a41d12015-03-24 08:27:11 -0700138 }
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 Burk95129f52015-09-22 12:07:31 -0700150 // 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 Lockwoodf0a41d12015-03-24 08:27:11 -0700155 BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
156 CLIENT_CHARACTERISTIC_CONFIG);
Mike Lockwood9490eae2015-05-07 13:39:01 -0700157 if (descriptor != null) {
158 descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
Phil Burk95129f52015-09-22 12:07:31 -0700159 boolean result = mBluetoothGatt.writeDescriptor(descriptor);
160 Log.d(TAG, "writeDescriptor returned " + result);
Mike Lockwood9490eae2015-05-07 13:39:01 -0700161 } else {
162 Log.e(TAG, "No CLIENT_CHARACTERISTIC_CONFIG for device " + mBluetoothDevice);
163 }
Phil Burk95129f52015-09-22 12:07:31 -0700164
165 characteristic.setWriteType(originalWriteType);
Mike Lockwoodf0a41d12015-03-24 08:27:11 -0700166 }
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 Lockwood8c26d842015-05-01 14:36:44 -0700179 if (DEBUG) {
180 logByteArray("Received ", characteristic.getValue(), 0,
181 characteristic.getValue().length);
182 }
Mike Lockwoodf0a41d12015-03-24 08:27:11 -0700183 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 Lockwood8c26d842015-05-01 14:36:44 -0700211 if (DEBUG) {
212 logByteArray("Sent ", mCharacteristic.getValue(), 0,
213 mCharacteristic.getValue().length);
214 }
Mike Lockwoodf0a41d12015-03-24 08:27:11 -0700215 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 Lockwoode0a6ca62015-06-04 13:43:56 -0700237 null, null, properties, MidiDeviceInfo.TYPE_BLUETOOTH, mDeviceServerCallback);
Mike Lockwoodf0a41d12015-03-24 08:27:11 -0700238
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 Lockwood7eb441c2015-05-12 13:32:16 -0700258 mPacketEncoder.send(event.data, 0, event.count,
Mike Lockwoodf0a41d12015-03-24 08:27:11 -0700259 event.getTimestamp());
260 } catch (IOException e) {
Mike Lockwood7eb441c2015-05-12 13:32:16 -0700261 Log.e(TAG, "mPacketAccumulator.send failed", e);
Mike Lockwoodf0a41d12015-03-24 08:27:11 -0700262 }
263 mEventScheduler.addEventToPool(event);
264 }
265 Log.d(TAG, "BluetoothMidiDevice thread exit");
266 }
267 }.start();
268 }
269
Mike Lockwood9490eae2015-05-07 13:39:01 -0700270 private void close() {
271 synchronized (mBluetoothDevice) {
Mike Lockwoode0a6ca62015-06-04 13:43:56 -0700272 mEventScheduler.close();
273 mService.deviceClosed(mBluetoothDevice);
274
Mike Lockwood9490eae2015-05-07 13:39:01 -0700275 if (mDeviceServer != null) {
276 IoUtils.closeQuietly(mDeviceServer);
277 mDeviceServer = null;
Mike Lockwood9490eae2015-05-07 13:39:01 -0700278 }
279 if (mBluetoothGatt != null) {
280 mBluetoothGatt.close();
281 mBluetoothGatt = null;
282 }
Mike Lockwoodf0a41d12015-03-24 08:27:11 -0700283 }
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 Lockwood8c26d842015-05-01 14:36:44 -0700293 builder.append(String.format("0x%02X", value[i]));
Mike Lockwoodf0a41d12015-03-24 08:27:11 -0700294 if (i != value.length - 1) {
295 builder.append(", ");
296 }
297 }
298 Log.d(TAG, builder.toString());
299 }
300}