| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.bluetoothmidiservice; |
| |
| import android.bluetooth.BluetoothDevice; |
| import android.bluetooth.BluetoothGatt; |
| import android.bluetooth.BluetoothGattCallback; |
| import android.bluetooth.BluetoothGattCharacteristic; |
| import android.bluetooth.BluetoothGattDescriptor; |
| import android.bluetooth.BluetoothGattService; |
| import android.bluetooth.BluetoothProfile; |
| import android.content.Context; |
| import android.media.midi.MidiDeviceInfo; |
| import android.media.midi.MidiDeviceServer; |
| import android.media.midi.MidiDeviceStatus; |
| import android.media.midi.MidiManager; |
| import android.media.midi.MidiReceiver; |
| import android.os.Bundle; |
| import android.os.IBinder; |
| import android.util.Log; |
| |
| import com.android.internal.midi.MidiEventScheduler; |
| import com.android.internal.midi.MidiEventScheduler.MidiEvent; |
| |
| import libcore.io.IoUtils; |
| |
| import java.io.IOException; |
| import java.util.List; |
| import java.util.UUID; |
| |
| /** |
| * Class used to implement a Bluetooth MIDI device. |
| */ |
| public final class BluetoothMidiDevice { |
| |
| private static final String TAG = "BluetoothMidiDevice"; |
| private static final boolean DEBUG = false; |
| |
| private static final int MAX_PACKET_SIZE = 20; |
| |
| // Bluetooth MIDI Gatt service UUID |
| private static final UUID MIDI_SERVICE = UUID.fromString( |
| "03B80E5A-EDE8-4B33-A751-6CE34EC4C700"); |
| // Bluetooth MIDI Gatt characteristic UUID |
| private static final UUID MIDI_CHARACTERISTIC = UUID.fromString( |
| "7772E5DB-3868-4112-A1A9-F2669D106BF3"); |
| // Descriptor UUID for enabling characteristic changed notifications |
| private static final UUID CLIENT_CHARACTERISTIC_CONFIG = UUID.fromString( |
| "00002902-0000-1000-8000-00805f9b34fb"); |
| |
| private final BluetoothDevice mBluetoothDevice; |
| private final BluetoothMidiService mService; |
| private final MidiManager mMidiManager; |
| private MidiReceiver mOutputReceiver; |
| private final MidiEventScheduler mEventScheduler = new MidiEventScheduler(); |
| |
| private MidiDeviceServer mDeviceServer; |
| private BluetoothGatt mBluetoothGatt; |
| |
| private BluetoothGattCharacteristic mCharacteristic; |
| |
| // PacketReceiver for receiving formatted packets from our BluetoothPacketEncoder |
| private final PacketReceiver mPacketReceiver = new PacketReceiver(); |
| |
| private final BluetoothPacketEncoder mPacketEncoder |
| = new BluetoothPacketEncoder(mPacketReceiver, MAX_PACKET_SIZE); |
| |
| private final BluetoothPacketDecoder mPacketDecoder |
| = new BluetoothPacketDecoder(MAX_PACKET_SIZE); |
| |
| private final MidiDeviceServer.Callback mDeviceServerCallback |
| = new MidiDeviceServer.Callback() { |
| @Override |
| public void onDeviceStatusChanged(MidiDeviceServer server, MidiDeviceStatus status) { |
| } |
| |
| @Override |
| public void onClose() { |
| close(); |
| } |
| }; |
| |
| private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() { |
| @Override |
| public void onConnectionStateChange(BluetoothGatt gatt, int status, |
| int newState) { |
| String intentAction; |
| if (newState == BluetoothProfile.STATE_CONNECTED) { |
| Log.d(TAG, "Connected to GATT server."); |
| Log.d(TAG, "Attempting to start service discovery:" + |
| mBluetoothGatt.discoverServices()); |
| } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { |
| Log.i(TAG, "Disconnected from GATT server."); |
| close(); |
| } |
| } |
| |
| @Override |
| public void onServicesDiscovered(BluetoothGatt gatt, int status) { |
| if (status == BluetoothGatt.GATT_SUCCESS) { |
| BluetoothGattService service = gatt.getService(MIDI_SERVICE); |
| if (service != null) { |
| Log.d(TAG, "found MIDI_SERVICE"); |
| BluetoothGattCharacteristic characteristic |
| = service.getCharacteristic(MIDI_CHARACTERISTIC); |
| if (characteristic != null) { |
| Log.d(TAG, "found MIDI_CHARACTERISTIC"); |
| mCharacteristic = characteristic; |
| |
| // Request a lower Connection Interval for better latency. |
| boolean result = gatt.requestConnectionPriority( |
| BluetoothGatt.CONNECTION_PRIORITY_HIGH); |
| Log.d(TAG, "requestConnectionPriority(CONNECTION_PRIORITY_HIGH):" |
| + result); |
| |
| // Specification says to read the characteristic first and then |
| // switch to receiving notifications |
| mBluetoothGatt.readCharacteristic(characteristic); |
| } |
| } |
| } else { |
| Log.e(TAG, "onServicesDiscovered received: " + status); |
| close(); |
| } |
| } |
| |
| @Override |
| public void onCharacteristicRead(BluetoothGatt gatt, |
| BluetoothGattCharacteristic characteristic, |
| int status) { |
| Log.d(TAG, "onCharacteristicRead " + status); |
| |
| // switch to receiving notifications after initial characteristic read |
| mBluetoothGatt.setCharacteristicNotification(characteristic, true); |
| |
| // Use writeType that requests acknowledgement. |
| // This improves compatibility with various BLE-MIDI devices. |
| int originalWriteType = characteristic.getWriteType(); |
| characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT); |
| |
| BluetoothGattDescriptor descriptor = characteristic.getDescriptor( |
| CLIENT_CHARACTERISTIC_CONFIG); |
| if (descriptor != null) { |
| descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); |
| boolean result = mBluetoothGatt.writeDescriptor(descriptor); |
| Log.d(TAG, "writeDescriptor returned " + result); |
| } else { |
| Log.e(TAG, "No CLIENT_CHARACTERISTIC_CONFIG for device " + mBluetoothDevice); |
| } |
| |
| characteristic.setWriteType(originalWriteType); |
| } |
| |
| @Override |
| public void onCharacteristicWrite(BluetoothGatt gatt, |
| BluetoothGattCharacteristic characteristic, |
| int status) { |
| Log.d(TAG, "onCharacteristicWrite " + status); |
| mPacketEncoder.writeComplete(); |
| } |
| |
| @Override |
| public void onCharacteristicChanged(BluetoothGatt gatt, |
| BluetoothGattCharacteristic characteristic) { |
| if (DEBUG) { |
| logByteArray("Received ", characteristic.getValue(), 0, |
| characteristic.getValue().length); |
| } |
| mPacketDecoder.decodePacket(characteristic.getValue(), mOutputReceiver); |
| } |
| }; |
| |
| // This receives MIDI data that has already been passed through our MidiEventScheduler |
| // and has been normalized by our MidiFramer. |
| |
| private class PacketReceiver implements PacketEncoder.PacketReceiver { |
| // buffers of every possible packet size |
| private final byte[][] mWriteBuffers; |
| |
| public PacketReceiver() { |
| // Create buffers of every possible packet size |
| mWriteBuffers = new byte[MAX_PACKET_SIZE + 1][]; |
| for (int i = 0; i <= MAX_PACKET_SIZE; i++) { |
| mWriteBuffers[i] = new byte[i]; |
| } |
| } |
| |
| @Override |
| public void writePacket(byte[] buffer, int count) { |
| if (mCharacteristic == null) { |
| Log.w(TAG, "not ready to send packet yet"); |
| return; |
| } |
| byte[] writeBuffer = mWriteBuffers[count]; |
| System.arraycopy(buffer, 0, writeBuffer, 0, count); |
| mCharacteristic.setValue(writeBuffer); |
| if (DEBUG) { |
| logByteArray("Sent ", mCharacteristic.getValue(), 0, |
| mCharacteristic.getValue().length); |
| } |
| mBluetoothGatt.writeCharacteristic(mCharacteristic); |
| } |
| } |
| |
| public BluetoothMidiDevice(Context context, BluetoothDevice device, |
| BluetoothMidiService service) { |
| mBluetoothDevice = device; |
| mService = service; |
| |
| mBluetoothGatt = mBluetoothDevice.connectGatt(context, false, mGattCallback); |
| |
| mMidiManager = (MidiManager)context.getSystemService(Context.MIDI_SERVICE); |
| |
| Bundle properties = new Bundle(); |
| properties.putString(MidiDeviceInfo.PROPERTY_NAME, mBluetoothGatt.getDevice().getName()); |
| properties.putParcelable(MidiDeviceInfo.PROPERTY_BLUETOOTH_DEVICE, |
| mBluetoothGatt.getDevice()); |
| |
| MidiReceiver[] inputPortReceivers = new MidiReceiver[1]; |
| inputPortReceivers[0] = mEventScheduler.getReceiver(); |
| |
| mDeviceServer = mMidiManager.createDeviceServer(inputPortReceivers, 1, |
| null, null, properties, MidiDeviceInfo.TYPE_BLUETOOTH, mDeviceServerCallback); |
| |
| mOutputReceiver = mDeviceServer.getOutputPortReceivers()[0]; |
| |
| // This thread waits for outgoing messages from our MidiEventScheduler |
| // And forwards them to our MidiFramer to be prepared to send via Bluetooth. |
| new Thread("BluetoothMidiDevice " + mBluetoothDevice) { |
| @Override |
| public void run() { |
| while (true) { |
| MidiEvent event; |
| try { |
| event = (MidiEvent)mEventScheduler.waitNextEvent(); |
| } catch (InterruptedException e) { |
| // try again |
| continue; |
| } |
| if (event == null) { |
| break; |
| } |
| try { |
| mPacketEncoder.send(event.data, 0, event.count, |
| event.getTimestamp()); |
| } catch (IOException e) { |
| Log.e(TAG, "mPacketAccumulator.send failed", e); |
| } |
| mEventScheduler.addEventToPool(event); |
| } |
| Log.d(TAG, "BluetoothMidiDevice thread exit"); |
| } |
| }.start(); |
| } |
| |
| private void close() { |
| synchronized (mBluetoothDevice) { |
| mEventScheduler.close(); |
| mService.deviceClosed(mBluetoothDevice); |
| |
| if (mDeviceServer != null) { |
| IoUtils.closeQuietly(mDeviceServer); |
| mDeviceServer = null; |
| } |
| if (mBluetoothGatt != null) { |
| mBluetoothGatt.close(); |
| mBluetoothGatt = null; |
| } |
| } |
| } |
| |
| public IBinder getBinder() { |
| return mDeviceServer.asBinder(); |
| } |
| |
| private static void logByteArray(String prefix, byte[] value, int offset, int count) { |
| StringBuilder builder = new StringBuilder(prefix); |
| for (int i = offset; i < count; i++) { |
| builder.append(String.format("0x%02X", value[i])); |
| if (i != value.length - 1) { |
| builder.append(", "); |
| } |
| } |
| Log.d(TAG, builder.toString()); |
| } |
| } |