/* * Copyright (C) 2014 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 android.media.midi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.RequiresFeature; import android.annotation.SystemService; import android.bluetooth.BluetoothDevice; import android.content.Context; import android.content.pm.PackageManager; import android.os.Binder; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; import android.util.ArraySet; import android.util.Log; import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Collections; import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; // BLE-MIDI /** * This class is the public application interface to the MIDI service. */ @SystemService(Context.MIDI_SERVICE) @RequiresFeature(PackageManager.FEATURE_MIDI) public final class MidiManager { private static final String TAG = "MidiManager"; /** * Constant representing MIDI devices. * These devices do NOT support Universal MIDI Packets by default. * These support the original MIDI 1.0 byte stream. * When communicating to a USB device, a raw byte stream will be padded for USB. * Likewise, for a Bluetooth device, the raw bytes will be converted for Bluetooth. * For virtual devices, the byte stream will be passed directly. * If Universal MIDI Packets are needed, please use MIDI-CI. * @see MidiManager#getDevicesForTransport */ public static final int TRANSPORT_MIDI_BYTE_STREAM = 1; /** * Constant representing Universal MIDI devices. * These devices do support Universal MIDI Packets (UMP) by default. * When sending data to these devices, please send UMP. * Packets should always be a multiple of 4 bytes. * UMP is defined in the USB MIDI 2.0 spec. Please read the standard for more info. * @see MidiManager#getDevicesForTransport */ public static final int TRANSPORT_UNIVERSAL_MIDI_PACKETS = 2; /** * @see MidiManager#getDevicesForTransport * @hide */ @IntDef(prefix = { "TRANSPORT_" }, value = { TRANSPORT_MIDI_BYTE_STREAM, TRANSPORT_UNIVERSAL_MIDI_PACKETS }) @Retention(RetentionPolicy.SOURCE) public @interface Transport {} /** * Intent for starting BluetoothMidiService * @hide */ public static final String BLUETOOTH_MIDI_SERVICE_INTENT = "android.media.midi.BluetoothMidiService"; /** * BluetoothMidiService package name * @hide */ public static final String BLUETOOTH_MIDI_SERVICE_PACKAGE = "com.android.bluetoothmidiservice"; /** * BluetoothMidiService class name * @hide */ public static final String BLUETOOTH_MIDI_SERVICE_CLASS = "com.android.bluetoothmidiservice.BluetoothMidiService"; private final IMidiManager mService; private final IBinder mToken = new Binder(); private ConcurrentHashMap mDeviceListeners = new ConcurrentHashMap(); // Binder stub for receiving device notifications from MidiService private class DeviceListener extends IMidiDeviceListener.Stub { private final DeviceCallback mCallback; private final Executor mExecutor; private final int mTransport; DeviceListener(DeviceCallback callback, Executor executor, int transport) { mCallback = callback; mExecutor = executor; mTransport = transport; } @Override public void onDeviceAdded(MidiDeviceInfo device) { if (shouldInvokeCallback(device)) { if (mExecutor != null) { mExecutor.execute(() -> mCallback.onDeviceAdded(device)); } else { mCallback.onDeviceAdded(device); } } } @Override public void onDeviceRemoved(MidiDeviceInfo device) { if (shouldInvokeCallback(device)) { if (mExecutor != null) { mExecutor.execute(() -> mCallback.onDeviceRemoved(device)); } else { mCallback.onDeviceRemoved(device); } } } @Override public void onDeviceStatusChanged(MidiDeviceStatus status) { if (mExecutor != null) { mExecutor.execute(() -> mCallback.onDeviceStatusChanged(status)); } else { mCallback.onDeviceStatusChanged(status); } } /** * Used to figure out whether callbacks should be invoked. Only invoke callbacks of * the correct type. * * @param MidiDeviceInfo the device to check * @return whether to invoke a callback */ private boolean shouldInvokeCallback(MidiDeviceInfo device) { // UMP devices have protocols that are not PROTOCOL_UNKNOWN if (mTransport == TRANSPORT_UNIVERSAL_MIDI_PACKETS) { return (device.getDefaultProtocol() != MidiDeviceInfo.PROTOCOL_UNKNOWN); } else if (mTransport == TRANSPORT_MIDI_BYTE_STREAM) { return (device.getDefaultProtocol() == MidiDeviceInfo.PROTOCOL_UNKNOWN); } else { Log.e(TAG, "Invalid transport type: " + mTransport); return false; } } } /** * Callback class used for clients to receive MIDI device added and removed notifications */ public static class DeviceCallback { /** * Called to notify when a new MIDI device has been added * * @param device a {@link MidiDeviceInfo} for the newly added device */ public void onDeviceAdded(MidiDeviceInfo device) { } /** * Called to notify when a MIDI device has been removed * * @param device a {@link MidiDeviceInfo} for the removed device */ public void onDeviceRemoved(MidiDeviceInfo device) { } /** * Called to notify when the status of a MIDI device has changed * * @param status a {@link MidiDeviceStatus} for the changed device */ public void onDeviceStatusChanged(MidiDeviceStatus status) { } } /** * Listener class used for receiving the results of {@link #openDevice} and * {@link #openBluetoothDevice} */ public interface OnDeviceOpenedListener { /** * Called to respond to a {@link #openDevice} request * * @param device a {@link MidiDevice} for opened device, or null if opening failed */ abstract public void onDeviceOpened(MidiDevice device); } /** * @hide */ public MidiManager(IMidiManager service) { mService = service; } /** * Registers a callback to receive notifications when MIDI 1.0 devices are added and removed. * These are devices that do not default to Universal MIDI Packets. To register for a callback * for those, call {@link #registerDeviceCallback} instead. * * The {@link DeviceCallback#onDeviceStatusChanged} method will be called immediately * for any devices that have open ports. This allows applications to know which input * ports are already in use and, therefore, unavailable. * * Applications should call {@link #getDevices} before registering the callback * to get a list of devices already added. * * @param callback a {@link DeviceCallback} for MIDI device notifications * @param handler The {@link android.os.Handler Handler} that will be used for delivering the * device notifications. If handler is null, then the thread used for the * callback is unspecified. * @deprecated Use {@link #registerDeviceCallback(int, Executor, DeviceCallback)} instead. */ @Deprecated public void registerDeviceCallback(DeviceCallback callback, Handler handler) { Executor executor = null; if (handler != null) { executor = handler::post; } DeviceListener deviceListener = new DeviceListener(callback, executor, TRANSPORT_MIDI_BYTE_STREAM); try { mService.registerListener(mToken, deviceListener); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } mDeviceListeners.put(callback, deviceListener); } /** * Registers a callback to receive notifications when MIDI devices are added and removed * for a specific transport type. * * The {@link DeviceCallback#onDeviceStatusChanged} method will be called immediately * for any devices that have open ports. This allows applications to know which input * ports are already in use and, therefore, unavailable. * * Applications should call {@link #getDevicesForTransport} before registering the callback * to get a list of devices already added. * * @param transport The transport to be used. This is either TRANSPORT_MIDI_BYTE_STREAM or * TRANSPORT_UNIVERSAL_MIDI_PACKETS. * @param executor The {@link Executor} that will be used for delivering the * device notifications. * @param callback a {@link DeviceCallback} for MIDI device notifications */ public void registerDeviceCallback(@Transport int transport, @NonNull Executor executor, @NonNull DeviceCallback callback) { Objects.requireNonNull(executor); DeviceListener deviceListener = new DeviceListener(callback, executor, transport); try { mService.registerListener(mToken, deviceListener); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } mDeviceListeners.put(callback, deviceListener); } /** * Unregisters a {@link DeviceCallback}. * * @param callback a {@link DeviceCallback} to unregister */ public void unregisterDeviceCallback(DeviceCallback callback) { DeviceListener deviceListener = mDeviceListeners.remove(callback); if (deviceListener != null) { try { mService.unregisterListener(mToken, deviceListener); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } } /** * Gets a list of connected MIDI devices. This returns all devices that do * not default to Universal MIDI Packets. To get those instead, please call * {@link #getDevicesForTransport} instead. * * @return an array of MIDI devices * @deprecated Use {@link #getDevicesForTransport} instead. */ @Deprecated public MidiDeviceInfo[] getDevices() { try { return mService.getDevices(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Gets a list of connected MIDI devices by transport. TRANSPORT_MIDI_BYTE_STREAM * is used for MIDI 1.0 and is the most common. * For devices with built in Universal MIDI Packet support, use * TRANSPORT_UNIVERSAL_MIDI_PACKETS instead. * * @param transport The transport to be used. This is either TRANSPORT_MIDI_BYTE_STREAM or * TRANSPORT_UNIVERSAL_MIDI_PACKETS. * @return a collection of MIDI devices */ public @NonNull Set getDevicesForTransport(@Transport int transport) { try { MidiDeviceInfo[] devices = mService.getDevicesForTransport(transport); if (devices == null) { return Collections.emptySet(); } return new ArraySet<>(devices); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } private void sendOpenDeviceResponse(final MidiDevice device, final OnDeviceOpenedListener listener, Handler handler) { if (handler != null) { handler.post(new Runnable() { @Override public void run() { listener.onDeviceOpened(device); } }); } else { listener.onDeviceOpened(device); } } /** * Opens a MIDI device for reading and writing. * * @param deviceInfo a {@link android.media.midi.MidiDeviceInfo} to open * @param listener a {@link MidiManager.OnDeviceOpenedListener} to be called * to receive the result * @param handler the {@link android.os.Handler Handler} that will be used for delivering * the result. If handler is null, then the thread used for the * listener is unspecified. */ public void openDevice(MidiDeviceInfo deviceInfo, OnDeviceOpenedListener listener, Handler handler) { final MidiDeviceInfo deviceInfoF = deviceInfo; final OnDeviceOpenedListener listenerF = listener; final Handler handlerF = handler; IMidiDeviceOpenCallback callback = new IMidiDeviceOpenCallback.Stub() { @Override public void onDeviceOpened(IMidiDeviceServer server, IBinder deviceToken) { MidiDevice device; if (server != null) { device = new MidiDevice(deviceInfoF, server, mService, mToken, deviceToken); } else { device = null; } sendOpenDeviceResponse(device, listenerF, handlerF); } }; try { mService.openDevice(mToken, deviceInfo, callback); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Opens a Bluetooth MIDI device for reading and writing. * * @param bluetoothDevice a {@link android.bluetooth.BluetoothDevice} to open as a MIDI device * @param listener a {@link MidiManager.OnDeviceOpenedListener} to be called to receive the * result * @param handler the {@link android.os.Handler Handler} that will be used for delivering * the result. If handler is null, then the thread used for the * listener is unspecified. */ public void openBluetoothDevice(BluetoothDevice bluetoothDevice, OnDeviceOpenedListener listener, Handler handler) { final OnDeviceOpenedListener listenerF = listener; final Handler handlerF = handler; Log.d(TAG, "openBluetoothDevice() " + bluetoothDevice); IMidiDeviceOpenCallback callback = new IMidiDeviceOpenCallback.Stub() { @Override public void onDeviceOpened(IMidiDeviceServer server, IBinder deviceToken) { Log.d(TAG, "onDeviceOpened() server:" + server); MidiDevice device = null; if (server != null) { try { // fetch MidiDeviceInfo from the server MidiDeviceInfo deviceInfo = server.getDeviceInfo(); device = new MidiDevice(deviceInfo, server, mService, mToken, deviceToken); } catch (RemoteException e) { Log.e(TAG, "remote exception in getDeviceInfo()"); } } sendOpenDeviceResponse(device, listenerF, handlerF); } }; try { mService.openBluetoothDevice(mToken, bluetoothDevice, callback); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** @hide */ // for now public void closeBluetoothDevice(@NonNull MidiDevice midiDevice) { try { midiDevice.close(); } catch (IOException ex) { Log.e(TAG, "Exception closing BLE-MIDI device" + ex); } } /** @hide */ public MidiDeviceServer createDeviceServer(MidiReceiver[] inputPortReceivers, int numOutputPorts, String[] inputPortNames, String[] outputPortNames, Bundle properties, int type, int defaultProtocol, MidiDeviceServer.Callback callback) { try { MidiDeviceServer server = new MidiDeviceServer(mService, inputPortReceivers, numOutputPorts, callback); MidiDeviceInfo deviceInfo = mService.registerDeviceServer(server.getBinderInterface(), inputPortReceivers.length, numOutputPorts, inputPortNames, outputPortNames, properties, type, defaultProtocol); if (deviceInfo == null) { Log.e(TAG, "registerVirtualDevice failed"); return null; } return server; } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } }