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