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.MidiDeviceInfo;
28import android.media.midi.MidiDeviceServer;
29import android.media.midi.MidiDeviceStatus;
30import android.media.midi.MidiManager;
31import android.media.midi.MidiReceiver;
32import 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";
51    private static final boolean DEBUG = false;
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
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
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) {
103                Log.d(TAG, "Connected to GATT server.");
104                Log.d(TAG, "Attempting to start service discovery:" +
105                        mBluetoothGatt.discoverServices());
106            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
107                Log.i(TAG, "Disconnected from GATT server.");
108                close();
109            }
110        }
111
112        @Override
113        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
114            if (status == BluetoothGatt.GATT_SUCCESS) {
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;
123
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);
133                    }
134                }
135            } else {
136                Log.e(TAG, "onServicesDiscovered received: " + status);
137                close();
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
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
155            BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
156                    CLIENT_CHARACTERISTIC_CONFIG);
157            if (descriptor != null) {
158                descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
159                boolean result = mBluetoothGatt.writeDescriptor(descriptor);
160                Log.d(TAG, "writeDescriptor returned " + result);
161            } else {
162                Log.e(TAG, "No CLIENT_CHARACTERISTIC_CONFIG for device " + mBluetoothDevice);
163            }
164
165            characteristic.setWriteType(originalWriteType);
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) {
179            if (DEBUG) {
180                logByteArray("Received ", characteristic.getValue(), 0,
181                        characteristic.getValue().length);
182            }
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);
211            if (DEBUG) {
212                logByteArray("Sent ", mCharacteristic.getValue(), 0,
213                       mCharacteristic.getValue().length);
214            }
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,
237                null, null, properties, MidiDeviceInfo.TYPE_BLUETOOTH, mDeviceServerCallback);
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 {
258                        mPacketEncoder.send(event.data, 0, event.count,
259                                event.getTimestamp());
260                    } catch (IOException e) {
261                        Log.e(TAG, "mPacketAccumulator.send failed", e);
262                    }
263                    mEventScheduler.addEventToPool(event);
264                }
265                Log.d(TAG, "BluetoothMidiDevice thread exit");
266            }
267        }.start();
268    }
269
270    private void close() {
271        synchronized (mBluetoothDevice) {
272            mEventScheduler.close();
273            mService.deviceClosed(mBluetoothDevice);
274
275            if (mDeviceServer != null) {
276                IoUtils.closeQuietly(mDeviceServer);
277                mDeviceServer = null;
278            }
279            if (mBluetoothGatt != null) {
280                mBluetoothGatt.close();
281                mBluetoothGatt = null;
282            }
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++) {
293            builder.append(String.format("0x%02X", value[i]));
294            if (i != value.length - 1) {
295                builder.append(", ");
296            }
297        }
298        Log.d(TAG, builder.toString());
299    }
300}
301