BluetoothMidiDevice.java revision f0a41d1c591193fbe02c9ddbaf24c79af4da9972
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
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;
27import android.media.midi.MidiReceiver;
28import android.media.midi.MidiManager;
29import android.media.midi.MidiDeviceServer;
30import android.media.midi.MidiDeviceInfo;
31import android.os.Bundle;
32import android.os.IBinder;
33import android.util.Log;
34
35import com.android.internal.midi.MidiEventScheduler;
36import com.android.internal.midi.MidiEventScheduler.MidiEvent;
37
38import libcore.io.IoUtils;
39
40import java.io.IOException;
41import java.util.List;
42import java.util.UUID;
43
44/**
45 * Class used to implement a Bluetooth MIDI device.
46 */
47public final class BluetoothMidiDevice {
48
49    private static final String TAG = "BluetoothMidiDevice";
50
51    private static final int MAX_PACKET_SIZE = 20;
52
53    //  Bluetooth MIDI Gatt service UUID
54    private static final UUID MIDI_SERVICE = UUID.fromString(
55            "03B80E5A-EDE8-4B33-A751-6CE34EC4C700");
56    // Bluetooth MIDI Gatt characteristic UUID
57    private static final UUID MIDI_CHARACTERISTIC = UUID.fromString(
58            "7772E5DB-3868-4112-A1A9-F2669D106BF3");
59    // Descriptor UUID for enabling characteristic changed notifications
60    private static final UUID CLIENT_CHARACTERISTIC_CONFIG = UUID.fromString(
61            "00002902-0000-1000-8000-00805f9b34fb");
62
63    private final BluetoothDevice mBluetoothDevice;
64    private final BluetoothMidiService mService;
65    private final MidiManager mMidiManager;
66    private MidiReceiver mOutputReceiver;
67    private final MidiEventScheduler mEventScheduler = new MidiEventScheduler();
68
69    private MidiDeviceServer mDeviceServer;
70    private BluetoothGatt mBluetoothGatt;
71
72    private BluetoothGattCharacteristic mCharacteristic;
73
74    // PacketReceiver for receiving formatted packets from our BluetoothPacketEncoder
75    private final PacketReceiver mPacketReceiver = new PacketReceiver();
76
77    private final BluetoothPacketEncoder mPacketEncoder
78            = new BluetoothPacketEncoder(mPacketReceiver, MAX_PACKET_SIZE);
79
80    private final BluetoothPacketDecoder mPacketDecoder
81            = new BluetoothPacketDecoder(MAX_PACKET_SIZE);
82
83    private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
84        @Override
85        public void onConnectionStateChange(BluetoothGatt gatt, int status,
86                int newState) {
87            String intentAction;
88            if (newState == BluetoothProfile.STATE_CONNECTED) {
89                Log.i(TAG, "Connected to GATT server.");
90                Log.i(TAG, "Attempting to start service discovery:" +
91                        mBluetoothGatt.discoverServices());
92            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
93                Log.i(TAG, "Disconnected from GATT server.");
94                // FIXME synchronize?
95                close();
96            }
97        }
98
99        @Override
100        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
101            if (status == BluetoothGatt.GATT_SUCCESS) {
102                List<BluetoothGattService> services = mBluetoothGatt.getServices();
103                for (BluetoothGattService service : services) {
104                    if (MIDI_SERVICE.equals(service.getUuid())) {
105                        Log.d(TAG, "found MIDI_SERVICE");
106                        List<BluetoothGattCharacteristic> characteristics
107                            = service.getCharacteristics();
108                        for (BluetoothGattCharacteristic characteristic : characteristics) {
109                            if (MIDI_CHARACTERISTIC.equals(characteristic.getUuid())) {
110                                Log.d(TAG, "found MIDI_CHARACTERISTIC");
111                                mCharacteristic = characteristic;
112
113                                // Specification says to read the characteristic first and then
114                                // switch to receiving notifications
115                                mBluetoothGatt.readCharacteristic(characteristic);
116                                break;
117                            }
118                        }
119                        break;
120                    }
121                }
122            } else {
123                Log.w(TAG, "onServicesDiscovered received: " + status);
124                // FIXME - report error back to client?
125            }
126        }
127
128        @Override
129        public void onCharacteristicRead(BluetoothGatt gatt,
130                BluetoothGattCharacteristic characteristic,
131                int status) {
132            Log.d(TAG, "onCharacteristicRead " + status);
133
134            // switch to receiving notifications after initial characteristic read
135            mBluetoothGatt.setCharacteristicNotification(characteristic, true);
136
137            BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
138                    CLIENT_CHARACTERISTIC_CONFIG);
139            // FIXME null check
140            descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
141            mBluetoothGatt.writeDescriptor(descriptor);
142        }
143
144        @Override
145        public void onCharacteristicWrite(BluetoothGatt gatt,
146                BluetoothGattCharacteristic characteristic,
147                int status) {
148            Log.d(TAG, "onCharacteristicWrite " + status);
149            mPacketEncoder.writeComplete();
150        }
151
152        @Override
153        public void onCharacteristicChanged(BluetoothGatt gatt,
154                                            BluetoothGattCharacteristic characteristic) {
155//            logByteArray("Received ", characteristic.getValue(), 0,
156//                    characteristic.getValue().length);
157            mPacketDecoder.decodePacket(characteristic.getValue(), mOutputReceiver);
158        }
159    };
160
161    // This receives MIDI data that has already been passed through our MidiEventScheduler
162    // and has been normalized by our MidiFramer.
163
164    private class PacketReceiver implements PacketEncoder.PacketReceiver {
165        // buffers of every possible packet size
166        private final byte[][] mWriteBuffers;
167
168        public PacketReceiver() {
169            // Create buffers of every possible packet size
170            mWriteBuffers = new byte[MAX_PACKET_SIZE + 1][];
171            for (int i = 0; i <= MAX_PACKET_SIZE; i++) {
172                mWriteBuffers[i] = new byte[i];
173            }
174        }
175
176        @Override
177        public void writePacket(byte[] buffer, int count) {
178            if (mCharacteristic == null) {
179                Log.w(TAG, "not ready to send packet yet");
180                return;
181            }
182            byte[] writeBuffer = mWriteBuffers[count];
183            System.arraycopy(buffer, 0, writeBuffer, 0, count);
184            mCharacteristic.setValue(writeBuffer);
185//            logByteArray("Sent ", mCharacteristic.getValue(), 0,
186//                    mCharacteristic.getValue().length);
187            mBluetoothGatt.writeCharacteristic(mCharacteristic);
188        }
189    }
190
191    public BluetoothMidiDevice(Context context, BluetoothDevice device,
192            BluetoothMidiService service) {
193        mBluetoothDevice = device;
194        mService = service;
195
196        mBluetoothGatt = mBluetoothDevice.connectGatt(context, false, mGattCallback);
197
198        mMidiManager = (MidiManager)context.getSystemService(Context.MIDI_SERVICE);
199
200        Bundle properties = new Bundle();
201        properties.putString(MidiDeviceInfo.PROPERTY_NAME, mBluetoothGatt.getDevice().getName());
202        properties.putParcelable(MidiDeviceInfo.PROPERTY_BLUETOOTH_DEVICE,
203                mBluetoothGatt.getDevice());
204
205        MidiReceiver[] inputPortReceivers = new MidiReceiver[1];
206        inputPortReceivers[0] = mEventScheduler.getReceiver();
207
208        mDeviceServer = mMidiManager.createDeviceServer(inputPortReceivers, 1,
209                null, null, properties, MidiDeviceInfo.TYPE_BLUETOOTH, null);
210
211        mOutputReceiver = mDeviceServer.getOutputPortReceivers()[0];
212
213        // This thread waits for outgoing messages from our MidiEventScheduler
214        // And forwards them to our MidiFramer to be prepared to send via Bluetooth.
215        new Thread("BluetoothMidiDevice " + mBluetoothDevice) {
216            @Override
217            public void run() {
218                while (true) {
219                    MidiEvent event;
220                    try {
221                        event = (MidiEvent)mEventScheduler.waitNextEvent();
222                    } catch (InterruptedException e) {
223                        // try again
224                        continue;
225                    }
226                    if (event == null) {
227                        break;
228                    }
229                    try {
230                        mPacketEncoder.sendWithTimestamp(event.data, 0, event.count,
231                                event.getTimestamp());
232                    } catch (IOException e) {
233                        Log.e(TAG, "mPacketAccumulator.sendWithTimestamp failed", e);
234                    }
235                    mEventScheduler.addEventToPool(event);
236                }
237                Log.d(TAG, "BluetoothMidiDevice thread exit");
238            }
239        }.start();
240    }
241
242    void close() {
243        mEventScheduler.close();
244        if (mDeviceServer != null) {
245            IoUtils.closeQuietly(mDeviceServer);
246            mDeviceServer = null;
247            mService.deviceClosed(mBluetoothDevice);
248        }
249        if (mBluetoothGatt != null) {
250            mBluetoothGatt.close();
251            mBluetoothGatt = null;
252        }
253    }
254
255    public IBinder getBinder() {
256        return mDeviceServer.asBinder();
257    }
258
259    private static void logByteArray(String prefix, byte[] value, int offset, int count) {
260        StringBuilder builder = new StringBuilder(prefix);
261        for (int i = offset; i < count; i++) {
262            String hex = Integer.toHexString(value[i]);
263            int length = hex.length();
264            if (length == 1) {
265                hex = "0x" + hex;
266            } else {
267                hex = hex.substring(length - 2, length);
268            }
269            builder.append(hex);
270            if (i != value.length - 1) {
271                builder.append(", ");
272            }
273        }
274        Log.d(TAG, builder.toString());
275    }
276}
277