diff options
27 files changed, 1395 insertions, 90 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index fd429f037895..14c8bb4a18f5 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -25339,6 +25339,7 @@ package android.media.midi { public final class MidiDeviceInfo implements android.os.Parcelable { method public int describeContents(); + method public int getDefaultProtocol(); method public int getId(); method public int getInputPortCount(); method public int getOutputPortCount(); @@ -25355,6 +25356,14 @@ package android.media.midi { field public static final String PROPERTY_SERIAL_NUMBER = "serial_number"; field public static final String PROPERTY_USB_DEVICE = "usb_device"; field public static final String PROPERTY_VERSION = "version"; + field public static final int PROTOCOL_UMP_MIDI_1_0_UP_TO_128_BITS = 3; // 0x3 + field public static final int PROTOCOL_UMP_MIDI_1_0_UP_TO_128_BITS_AND_JRTS = 4; // 0x4 + field public static final int PROTOCOL_UMP_MIDI_1_0_UP_TO_64_BITS = 1; // 0x1 + field public static final int PROTOCOL_UMP_MIDI_1_0_UP_TO_64_BITS_AND_JRTS = 2; // 0x2 + field public static final int PROTOCOL_UMP_MIDI_2_0 = 17; // 0x11 + field public static final int PROTOCOL_UMP_MIDI_2_0_AND_JRTS = 18; // 0x12 + field public static final int PROTOCOL_UMP_USE_MIDI_CI = 0; // 0x0 + field public static final int PROTOCOL_UNKNOWN = -1; // 0xffffffff field public static final int TYPE_BLUETOOTH = 3; // 0x3 field public static final int TYPE_USB = 1; // 0x1 field public static final int TYPE_VIRTUAL = 2; // 0x2 @@ -25396,10 +25405,14 @@ package android.media.midi { public final class MidiManager { method public android.media.midi.MidiDeviceInfo[] getDevices(); + method @NonNull public java.util.Collection<android.media.midi.MidiDeviceInfo> getDevicesForTransport(int); method public void openBluetoothDevice(android.bluetooth.BluetoothDevice, android.media.midi.MidiManager.OnDeviceOpenedListener, android.os.Handler); method public void openDevice(android.media.midi.MidiDeviceInfo, android.media.midi.MidiManager.OnDeviceOpenedListener, android.os.Handler); method public void registerDeviceCallback(android.media.midi.MidiManager.DeviceCallback, android.os.Handler); + method public void registerDeviceCallbackForTransport(@NonNull android.media.midi.MidiManager.DeviceCallback, @Nullable android.os.Handler, int); method public void unregisterDeviceCallback(android.media.midi.MidiManager.DeviceCallback); + field public static final int TRANSPORT_MIDI_BYTE_STREAM = 1; // 0x1 + field public static final int TRANSPORT_UNIVERSAL_MIDI_PACKETS = 2; // 0x2 } public static class MidiManager.DeviceCallback { diff --git a/media/java/android/media/midi/IMidiManager.aidl b/media/java/android/media/midi/IMidiManager.aidl index d3c8e0adb3ce..b03f78504635 100644 --- a/media/java/android/media/midi/IMidiManager.aidl +++ b/media/java/android/media/midi/IMidiManager.aidl @@ -31,6 +31,8 @@ interface IMidiManager { MidiDeviceInfo[] getDevices(); + MidiDeviceInfo[] getDevicesForTransport(int transport); + // for device creation & removal notifications void registerListener(IBinder clientToken, in IMidiDeviceListener listener); void unregisterListener(IBinder clientToken, in IMidiDeviceListener listener); @@ -43,7 +45,7 @@ interface IMidiManager // for registering built-in MIDI devices MidiDeviceInfo registerDeviceServer(in IMidiDeviceServer server, int numInputPorts, int numOutputPorts, in String[] inputPortNames, in String[] outputPortNames, - in Bundle properties, int type); + in Bundle properties, int type, int defaultProtocol); // for unregistering built-in MIDI devices void unregisterDeviceServer(in IMidiDeviceServer server); diff --git a/media/java/android/media/midi/MidiDeviceInfo.java b/media/java/android/media/midi/MidiDeviceInfo.java index dd3b6dbd6a39..b888fb00df9b 100644 --- a/media/java/android/media/midi/MidiDeviceInfo.java +++ b/media/java/android/media/midi/MidiDeviceInfo.java @@ -16,11 +16,15 @@ package android.media.midi; +import android.annotation.IntDef; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.util.Log; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + /** * This class contains information to describe a MIDI device. * For now we only have information that can be retrieved easily for USB devices, @@ -54,6 +58,110 @@ public final class MidiDeviceInfo implements Parcelable { public static final int TYPE_BLUETOOTH = 3; /** + * Constant representing a default protocol with Universal MIDI Packets (UMP). + * UMP is defined in "Universal MIDI Packet (UMP) Format and MIDI 2.0 Protocol" spec. + * All UMP data should be a multiple of 4 bytes. + * Use UMP to negotiate with the device with MIDI-CI. + * MIDI-CI is defined in "MIDI Capability Inquiry (MIDI-CI)" spec. + * Call {@link MidiManager#getDevicesForTransport} with parameter + * {@link MidiManager#TRANSPORT_UNIVERSAL_MIDI_PACKETS} to get devices with this transport. + * @see MidiDeviceInfo#getDefaultProtocol + */ + public static final int PROTOCOL_UMP_USE_MIDI_CI = 0; + + /** + * Constant representing a default protocol with Universal MIDI Packets (UMP). + * UMP is defined in "Universal MIDI Packet (UMP) Format and MIDI 2.0 Protocol" spec. + * All UMP data should be a multiple of 4 bytes. + * Use MIDI 1.0 through UMP with packet sizes up to 64 bits. + * Call {@link MidiManager#getDevicesForTransport} with parameter + * {@link MidiManager#TRANSPORT_UNIVERSAL_MIDI_PACKETS} to get devices with this transport. + * @see MidiDeviceInfo#getDefaultProtocol + */ + public static final int PROTOCOL_UMP_MIDI_1_0_UP_TO_64_BITS = 1; + + /** + * Constant representing a default protocol with Universal MIDI Packets (UMP). + * UMP is defined in "Universal MIDI Packet (UMP) Format and MIDI 2.0 Protocol" spec. + * All UMP data should be a multiple of 4 bytes. + * Use MIDI 1.0 through UMP with packet sizes up to 64 bits and jitter reduction timestamps. + * Call {@link MidiManager#getDevicesForTransport} with parameter + * {@link MidiManager#TRANSPORT_UNIVERSAL_MIDI_PACKETS} to get devices with this transport. + * @see MidiDeviceInfo#getDefaultProtocol + */ + public static final int PROTOCOL_UMP_MIDI_1_0_UP_TO_64_BITS_AND_JRTS = 2; + + /** + * Constant representing a default protocol with Universal MIDI Packets (UMP). + * UMP is defined in "Universal MIDI Packet (UMP) Format and MIDI 2.0 Protocol" spec. + * All UMP data should be a multiple of 4 bytes. + * Use MIDI 1.0 through UMP with packet sizes up to 128 bits. + * Call {@link MidiManager#getDevicesForTransport} with parameter + * {@link MidiManager#TRANSPORT_UNIVERSAL_MIDI_PACKETS} to get devices with this transport. + * @see MidiDeviceInfo#getDefaultProtocol + */ + public static final int PROTOCOL_UMP_MIDI_1_0_UP_TO_128_BITS = 3; + + /** + * Constant representing a default protocol with Universal MIDI Packets (UMP). + * UMP is defined in "Universal MIDI Packet (UMP) Format and MIDI 2.0 Protocol" spec. + * All UMP data should be a multiple of 4 bytes. + * Use MIDI 1.0 through UMP with packet sizes up to 128 bits and jitter reduction timestamps. + * Call {@link MidiManager#getDevicesForTransport} with parameter + * {@link MidiManager#TRANSPORT_UNIVERSAL_MIDI_PACKETS} to get devices with this transport. + * @see MidiDeviceInfo#getDefaultProtocol + */ + public static final int PROTOCOL_UMP_MIDI_1_0_UP_TO_128_BITS_AND_JRTS = 4; + + /** + * Constant representing a default protocol with Universal MIDI Packets (UMP). + * UMP is defined in "Universal MIDI Packet (UMP) Format and MIDI 2.0 Protocol" spec. + * All UMP data should be a multiple of 4 bytes. + * Use MIDI 2.0 through UMP. + * Call {@link MidiManager#getDevicesForTransport} with parameter + * {@link MidiManager#TRANSPORT_UNIVERSAL_MIDI_PACKETS} to get devices with this transport. + * @see MidiDeviceInfo#getDefaultProtocol + */ + public static final int PROTOCOL_UMP_MIDI_2_0 = 17; + + /** + * Constant representing a default protocol with Universal MIDI Packets (UMP). + * UMP is defined in "Universal MIDI Packet (UMP) Format and MIDI 2.0 Protocol" spec. + * All UMP data should be a multiple of 4 bytes. + * Use MIDI 2.0 through UMP and jitter reduction timestamps. + * Call {@link MidiManager#getDevicesForTransport} with parameter + * {@link MidiManager#TRANSPORT_UNIVERSAL_MIDI_PACKETS} to get devices with this transport. + * @see MidiDeviceInfo#getDefaultProtocol + */ + public static final int PROTOCOL_UMP_MIDI_2_0_AND_JRTS = 18; + + /** + * Constant representing a device with an unknown default protocol. + * If Universal MIDI Packets (UMP) are needed, use MIDI-CI through MIDI 1.0. + * UMP is defined in "Universal MIDI Packet (UMP) Format and MIDI 2.0 Protocol" spec. + * MIDI-CI is defined in "MIDI Capability Inquiry (MIDI-CI)" spec. + * @see MidiDeviceInfo#getDefaultProtocol + */ + public static final int PROTOCOL_UNKNOWN = -1; + + /** + * @see MidiDeviceInfo#getDefaultProtocol + * @hide + */ + @IntDef(prefix = { "PROTOCOL_" }, value = { + PROTOCOL_UMP_USE_MIDI_CI, + PROTOCOL_UMP_MIDI_1_0_UP_TO_64_BITS, + PROTOCOL_UMP_MIDI_1_0_UP_TO_64_BITS_AND_JRTS, + PROTOCOL_UMP_MIDI_1_0_UP_TO_128_BITS, + PROTOCOL_UMP_MIDI_1_0_UP_TO_128_BITS_AND_JRTS, + PROTOCOL_UMP_MIDI_2_0, + PROTOCOL_UMP_MIDI_2_0_AND_JRTS, + PROTOCOL_UNKNOWN + }) + @Retention(RetentionPolicy.SOURCE) + public @interface DefaultProtocol {} + + /** * Bundle key for the device's user visible name property. * The value for this property is of type {@link java.lang.String}. * Used with the {@link android.os.Bundle} returned by {@link #getProperties}. @@ -196,6 +304,7 @@ public final class MidiDeviceInfo implements Parcelable { private final String[] mOutputPortNames; private final Bundle mProperties; private final boolean mIsPrivate; + private final int mDefaultProtocol; /** * MidiDeviceInfo should only be instantiated by MidiService implementation @@ -203,7 +312,7 @@ public final class MidiDeviceInfo implements Parcelable { */ public MidiDeviceInfo(int type, int id, int numInputPorts, int numOutputPorts, String[] inputPortNames, String[] outputPortNames, Bundle properties, - boolean isPrivate) { + boolean isPrivate, int defaultProtocol) { // Check num ports for out-of-range values. Typical values will be // between zero and three. More than 16 would be very unlikely // because the port index field in the USB packet is only 4 bits. @@ -234,6 +343,7 @@ public final class MidiDeviceInfo implements Parcelable { } mProperties = properties; mIsPrivate = isPrivate; + mDefaultProtocol = defaultProtocol; } /** @@ -312,6 +422,18 @@ public final class MidiDeviceInfo implements Parcelable { return mIsPrivate; } + /** + * Returns the default protocol. For most devices, this will be {@link #PROTOCOL_UNKNOWN}. + * Returning {@link #PROTOCOL_UNKNOWN} is not an error; the device just doesn't support + * Universal MIDI Packets by default. + * + * @return the device's default protocol. + */ + @DefaultProtocol + public int getDefaultProtocol() { + return mDefaultProtocol; + } + @Override public boolean equals(Object o) { if (o instanceof MidiDeviceInfo) { @@ -331,11 +453,12 @@ public final class MidiDeviceInfo implements Parcelable { // This is a hack to force the mProperties Bundle to unparcel so we can // print all the names and values. mProperties.getString(PROPERTY_NAME); - return ("MidiDeviceInfo[mType=" + mType + - ",mInputPortCount=" + mInputPortCount + - ",mOutputPortCount=" + mOutputPortCount + - ",mProperties=" + mProperties + - ",mIsPrivate=" + mIsPrivate); + return ("MidiDeviceInfo[mType=" + mType + + ",mInputPortCount=" + mInputPortCount + + ",mOutputPortCount=" + mOutputPortCount + + ",mProperties=" + mProperties + + ",mIsPrivate=" + mIsPrivate + + ",mDefaultProtocol=" + mDefaultProtocol); } public static final @android.annotation.NonNull Parcelable.Creator<MidiDeviceInfo> CREATOR = @@ -349,10 +472,12 @@ public final class MidiDeviceInfo implements Parcelable { String[] inputPortNames = in.createStringArray(); String[] outputPortNames = in.createStringArray(); boolean isPrivate = (in.readInt() == 1); + int defaultProtocol = in.readInt(); Bundle basicPropertiesIgnored = in.readBundle(); Bundle properties = in.readBundle(); return new MidiDeviceInfo(type, id, inputPortCount, outputPortCount, - inputPortNames, outputPortNames, properties, isPrivate); + inputPortNames, outputPortNames, properties, isPrivate, + defaultProtocol); } public MidiDeviceInfo[] newArray(int size) { @@ -390,6 +515,7 @@ public final class MidiDeviceInfo implements Parcelable { parcel.writeStringArray(mInputPortNames); parcel.writeStringArray(mOutputPortNames); parcel.writeInt(mIsPrivate ? 1 : 0); + parcel.writeInt(mDefaultProtocol); // "Basic" properties only contain properties of primitive types // and thus can be read back by native code. "Extra" properties is // a superset that contains all properties. diff --git a/media/java/android/media/midi/MidiManager.java b/media/java/android/media/midi/MidiManager.java index dee94c681e87..a049a8891f48 100644 --- a/media/java/android/media/midi/MidiManager.java +++ b/media/java/android/media/midi/MidiManager.java @@ -16,18 +16,25 @@ package android.media.midi; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; 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.IBinder; import android.os.Bundle; import android.os.Handler; +import android.os.IBinder; import android.os.RemoteException; import android.util.Log; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Collection; import java.util.concurrent.ConcurrentHashMap; /** @@ -39,6 +46,39 @@ 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 */ @@ -68,37 +108,43 @@ public final class MidiManager { private class DeviceListener extends IMidiDeviceListener.Stub { private final DeviceCallback mCallback; private final Handler mHandler; + private final int mTransport; - public DeviceListener(DeviceCallback callback, Handler handler) { + DeviceListener(DeviceCallback callback, Handler handler, int transport) { mCallback = callback; mHandler = handler; + mTransport = transport; } @Override public void onDeviceAdded(MidiDeviceInfo device) { - if (mHandler != null) { - final MidiDeviceInfo deviceF = device; - mHandler.post(new Runnable() { - @Override public void run() { - mCallback.onDeviceAdded(deviceF); - } - }); - } else { - mCallback.onDeviceAdded(device); + if (shouldInvokeCallback(device)) { + if (mHandler != null) { + final MidiDeviceInfo deviceF = device; + mHandler.post(new Runnable() { + @Override public void run() { + mCallback.onDeviceAdded(deviceF); + } + }); + } else { + mCallback.onDeviceAdded(device); + } } } @Override public void onDeviceRemoved(MidiDeviceInfo device) { - if (mHandler != null) { - final MidiDeviceInfo deviceF = device; - mHandler.post(new Runnable() { - @Override public void run() { - mCallback.onDeviceRemoved(deviceF); - } - }); - } else { - mCallback.onDeviceRemoved(device); + if (shouldInvokeCallback(device)) { + if (mHandler != null) { + final MidiDeviceInfo deviceF = device; + mHandler.post(new Runnable() { + @Override public void run() { + mCallback.onDeviceRemoved(deviceF); + } + }); + } else { + mCallback.onDeviceRemoved(device); + } } } @@ -115,6 +161,25 @@ public final class MidiManager { 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; + } + } } /** @@ -167,8 +232,10 @@ public final class MidiManager { } /** - * Registers a callback to receive notifications when MIDI devices are added and removed. - * + * 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 #registerDeviceCallbackForTransport} 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. @@ -182,7 +249,30 @@ public final class MidiManager { * callback is unspecified. */ public void registerDeviceCallback(DeviceCallback callback, Handler handler) { - DeviceListener deviceListener = new DeviceListener(callback, handler); + registerDeviceCallbackForTransport(callback, handler, TRANSPORT_MIDI_BYTE_STREAM); + } + + /** + * 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 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. + * @param transport The transport to be used. This is either TRANSPORT_MIDI_BYTE_STREAM or + * TRANSPORT_UNIVERSAL_MIDI_PACKETS. + */ + public void registerDeviceCallbackForTransport(@NonNull DeviceCallback callback, + @Nullable Handler handler, @Transport int transport) { + DeviceListener deviceListener = new DeviceListener(callback, handler, transport); try { mService.registerListener(mToken, deviceListener); } catch (RemoteException e) { @@ -208,9 +298,11 @@ public final class MidiManager { } /** - * Gets the list of all connected MIDI devices. + * 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 all MIDI devices + * @return an array of MIDI devices */ public MidiDeviceInfo[] getDevices() { try { @@ -220,6 +312,29 @@ public final class MidiManager { } } + /** + * 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 Collection<MidiDeviceInfo> getDevicesForTransport(@Transport int transport) { + try { + MidiDeviceInfo[] devices = mService.getDevicesForTransport(transport); + Collection<MidiDeviceInfo> out = new ArrayList<MidiDeviceInfo>(devices.length); + for (int i = 0; i < devices.length; i++) { + out.add(devices[i]); + } + return out; + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + private void sendOpenDeviceResponse(final MidiDevice device, final OnDeviceOpenedListener listener, Handler handler) { if (handler != null) { @@ -311,13 +426,14 @@ public final class MidiManager { /** @hide */ public MidiDeviceServer createDeviceServer(MidiReceiver[] inputPortReceivers, int numOutputPorts, String[] inputPortNames, String[] outputPortNames, - Bundle properties, int type, MidiDeviceServer.Callback callback) { + 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); + properties, type, defaultProtocol); if (deviceInfo == null) { Log.e(TAG, "registerVirtualDevice failed"); return null; diff --git a/media/java/android/media/midi/package.html b/media/java/android/media/midi/package.html index 33c54900cd07..67df1b2fa315 100644 --- a/media/java/android/media/midi/package.html +++ b/media/java/android/media/midi/package.html @@ -405,5 +405,46 @@ apps using the <a href="#get_list_of_already_plugged_in_entities">MIDI device discovery calls described above</a>. </p> +<h1 id=using_midi_2_0_over_usb>Using MIDI 2.0 over USB</h1> + +<p>An app can use MIDI 2.0 over USB starting in Android T. MIDI 2.0 packets are embedded in +Universal MIDI Packets, or UMP for short. A MIDI 2.0 USB device should create two interfaces, +one endpoint that accepts only MIDI 1.0 packets and one that accepts only UMP packets. +For more info about MIDI 2.0 and UMP, please read the MIDI 2.0 USB spec.</p> + +<p>MidiManager.getDevices() would simply return the 1.0 interface. This interface should work +exactly the same as before. In order to use the new UMP interface, retrieve the device with the +following code snippet.</p> + +<pre class=prettyprint> +Collection<MidiDeviceInfo> universalDeviceInfos = midiManager.getDevicesForTransport( + MidiManager.TRANSPORT_UNIVERSAL_MIDI_PACKETS); +</pre> + +<p>UMP Packets are always in multiple of 4 bytes. For each set of 4 bytes, they are sent in network +order. Compare the following NoteOn code snippet with the NoteOn code snippet above. </p> + +<pre class=prettyprint> +byte[] buffer = new byte[32]; +int numBytes = 0; +int channel = 3; // MIDI channels 1-16 are encoded as 0-15. +int group = 0; +buffer[numBytes++] = (byte)(0x20 + group); // MIDI 1.0 voice message +buffer[numBytes++] = (byte)(0x90 + (channel - 1)); // note on +buffer[numBytes++] = (byte)60; // pitch is middle C +buffer[numBytes++] = (byte)127; // max velocity +int offset = 0; +// post is non-blocking +inputPort.send(buffer, offset, numBytes); +</pre> + +<p>MIDI 2.0 messages can be sent through UMP after negotiating with the device. This is called +MIDI-CI and is documented in the MIDI 2.0 spec. Some USB devices support pre-negotiated MIDI 2.0. +For a MidiDeviceInfo, you can query the defaultProtocol.</p> + +<pre class=prettyprint> +int defaultProtocol = info.getDefaultProtocol(); +</pre> + </body> </html> diff --git a/media/native/midi/MidiDeviceInfo.cpp b/media/native/midi/MidiDeviceInfo.cpp index 8a573fba322b..14524883470f 100644 --- a/media/native/midi/MidiDeviceInfo.cpp +++ b/media/native/midi/MidiDeviceInfo.cpp @@ -64,6 +64,7 @@ status_t MidiDeviceInfo::writeToParcel(Parcel* parcel) const { RETURN_IF_FAILED(writeStringVector(parcel, mInputPortNames)); RETURN_IF_FAILED(writeStringVector(parcel, mOutputPortNames)); RETURN_IF_FAILED(parcel->writeInt32(mIsPrivate ? 1 : 0)); + RETURN_IF_FAILED(parcel->writeInt32(mDefaultProtocol)); RETURN_IF_FAILED(mProperties.writeToParcel(parcel)); // This corresponds to "extra" properties written by Java code RETURN_IF_FAILED(mProperties.writeToParcel(parcel)); @@ -83,6 +84,7 @@ status_t MidiDeviceInfo::readFromParcel(const Parcel* parcel) { int32_t isPrivate; RETURN_IF_FAILED(parcel->readInt32(&isPrivate)); mIsPrivate = isPrivate == 1; + RETURN_IF_FAILED(parcel->readInt32(&mDefaultProtocol)); RETURN_IF_FAILED(mProperties.readFromParcel(parcel)); // Ignore "extra" properties as they may contain Java Parcelables return OK; @@ -130,7 +132,8 @@ bool operator==(const MidiDeviceInfo& lhs, const MidiDeviceInfo& rhs) { areVectorsEqual(lhs.mInputPortNames, rhs.mInputPortNames) && areVectorsEqual(lhs.mOutputPortNames, rhs.mOutputPortNames) && lhs.mProperties == rhs.mProperties && - lhs.mIsPrivate == rhs.mIsPrivate); + lhs.mIsPrivate == rhs.mIsPrivate && + lhs.mDefaultProtocol == rhs.mDefaultProtocol); } } // namespace midi diff --git a/media/native/midi/MidiDeviceInfo.h b/media/native/midi/MidiDeviceInfo.h index 5b4a241323d7..23e1cb474168 100644 --- a/media/native/midi/MidiDeviceInfo.h +++ b/media/native/midi/MidiDeviceInfo.h @@ -38,6 +38,7 @@ public: int getType() const { return mType; } int getUid() const { return mId; } bool isPrivate() const { return mIsPrivate; } + int getDefaultProtocol() const { return mDefaultProtocol; } const Vector<String16>& getInputPortNames() const { return mInputPortNames; } const Vector<String16>& getOutputPortNames() const { return mOutputPortNames; } String16 getProperty(const char* propertyName); @@ -48,6 +49,18 @@ public: TYPE_VIRTUAL = 2, TYPE_BLUETOOTH = 3, }; + + enum { + PROTOCOL_UMP_USE_MIDI_CI = 0, + PROTOCOL_UMP_MIDI_1_0_UP_TO_64_BITS = 1, + PROTOCOL_UMP_MIDI_1_0_UP_TO_64_BITS_AND_JRTS = 2, + PROTOCOL_UMP_MIDI_1_0_UP_TO_128_BITS = 3, + PROTOCOL_UMP_MIDI_1_0_UP_TO_128_BITS_AND_JRTS = 4, + PROTOCOL_UMP_MIDI_2_0 = 17, + PROTOCOL_UMP_MIDI_2_0_AND_JRTS = 18, + PROTOCOL_UNKNOWN = -1, + }; + static const char* const PROPERTY_NAME; static const char* const PROPERTY_MANUFACTURER; static const char* const PROPERTY_PRODUCT; @@ -72,6 +85,7 @@ private: Vector<String16> mOutputPortNames; os::PersistableBundle mProperties; bool mIsPrivate; + int32_t mDefaultProtocol; }; } // namespace midi diff --git a/media/native/midi/amidi.cpp b/media/native/midi/amidi.cpp index f90796e415c0..aa076e85e30d 100644 --- a/media/native/midi/amidi.cpp +++ b/media/native/midi/amidi.cpp @@ -138,6 +138,7 @@ static media_status_t AMIDI_getDeviceInfo(const AMidiDevice *device, outDeviceInfoPtr->type = deviceInfo.getType(); outDeviceInfoPtr->inputPortCount = deviceInfo.getInputPortNames().size(); outDeviceInfoPtr->outputPortCount = deviceInfo.getOutputPortNames().size(); + outDeviceInfoPtr->defaultProtocol = deviceInfo.getDefaultProtocol(); return AMEDIA_OK; } @@ -238,6 +239,13 @@ ssize_t AMIDI_API AMidiDevice_getNumOutputPorts(const AMidiDevice *device) { return device->deviceInfo.outputPortCount; } +AMidiDevice_Protocol AMIDI_API AMidiDevice_getDefaultProtocol(const AMidiDevice *device) { + if (device == nullptr) { + return AMIDI_DEVICE_PROTOCOL_UNKNOWN; + } + return static_cast<AMidiDevice_Protocol>(device->deviceInfo.defaultProtocol); +} + /* * Port Helpers */ diff --git a/media/native/midi/amidi_internal.h b/media/native/midi/amidi_internal.h index fce85963d217..023a6f5ec900 100644 --- a/media/native/midi/amidi_internal.h +++ b/media/native/midi/amidi_internal.h @@ -25,6 +25,7 @@ typedef struct { int32_t type; /* one of AMIDI_DEVICE_TYPE_* constants */ int32_t inputPortCount; /* number of input (send) ports associated with the device */ int32_t outputPortCount; /* number of output (received) ports associated with the device */ + int32_t defaultProtocol; /* one of the AMIDI_DEVICE_PROTOCOL_* constants */ } AMidiDeviceInfo; struct AMidiDevice { diff --git a/media/native/midi/include/amidi/AMidi.h b/media/native/midi/include/amidi/AMidi.h index 742db34b74a7..fbb7fb329659 100644 --- a/media/native/midi/include/amidi/AMidi.h +++ b/media/native/midi/include/amidi/AMidi.h @@ -62,6 +62,78 @@ enum { }; /* + * Protocol IDs for various MIDI devices. + * + * Introduced in API 33. + */ +enum AMidiDevice_Protocol : int32_t { + /** + * Constant representing a default protocol with Universal MIDI Packets (UMP). + * UMP is defined in "Universal MIDI Packet (UMP) Format and MIDI 2.0 Protocol" spec. + * All UMP data should be a multiple of 4 bytes. + * Use UMP to negotiate with the device with MIDI-CI. + * MIDI-CI is defined in "MIDI Capability Inquiry (MIDI-CI)" spec. + */ + AMIDI_DEVICE_PROTOCOL_UMP_USE_MIDI_CI = 0, + + /** + * Constant representing a default protocol with Universal MIDI Packets (UMP). + * UMP is defined in "Universal MIDI Packet (UMP) Format and MIDI 2.0 Protocol" spec. + * All UMP data should be a multiple of 4 bytes. + * Use MIDI 1.0 through UMP with packet sizes up to 64 bits. + */ + AMIDI_DEVICE_PROTOCOL_UMP_MIDI_1_0_UP_TO_64_BITS = 1, + + /** + * Constant representing a default protocol with Universal MIDI Packets (UMP). + * UMP is defined in "Universal MIDI Packet (UMP) Format and MIDI 2.0 Protocol" spec. + * All UMP data should be a multiple of 4 bytes. + * Use MIDI 1.0 through UMP with packet sizes up to 64 bits and jitter reduction timestamps. + */ + AMIDI_DEVICE_PROTOCOL_UMP_MIDI_1_0_UP_TO_64_BITS_AND_JRTS = 2, + + /** + * Constant representing a default protocol with Universal MIDI Packets (UMP). + * UMP is defined in "Universal MIDI Packet (UMP) Format and MIDI 2.0 Protocol" spec. + * All UMP data should be a multiple of 4 bytes. + * Use MIDI 1.0 through UMP with packet sizes up to 128 bits. + */ + AMIDI_DEVICE_PROTOCOL_UMP_MIDI_1_0_UP_TO_128_BITS = 3, + + /** + * Constant representing a default protocol with Universal MIDI Packets (UMP). + * UMP is defined in "Universal MIDI Packet (UMP) Format and MIDI 2.0 Protocol" spec. + * All UMP data should be a multiple of 4 bytes. + * Use MIDI 1.0 through UMP with packet sizes up to 128 bits and jitter reduction timestamps. + */ + AMIDI_DEVICE_PROTOCOL_UMP_MIDI_1_0_UP_TO_128_BITS_AND_JRTS = 4, + + /** + * Constant representing a default protocol with Universal MIDI Packets (UMP). + * UMP is defined in "Universal MIDI Packet (UMP) Format and MIDI 2.0 Protocol" spec. + * All UMP data should be a multiple of 4 bytes. + * Use MIDI 2.0 through UMP. + */ + AMIDI_DEVICE_PROTOCOL_UMP_MIDI_2_0 = 17, + + /** + * Constant representing a default protocol with Universal MIDI Packets (UMP). + * UMP is defined in "Universal MIDI Packet (UMP) Format and MIDI 2.0 Protocol" spec. + * All UMP data should be a multiple of 4 bytes. + * Use MIDI 2.0 through UMP and jitter reduction timestamps. + */ + AMIDI_DEVICE_PROTOCOL_UMP_MIDI_2_0_AND_JRTS = 18, + + /** + * Constant representing a device with an unknown default protocol. + * If Universal MIDI Packets (UMP) are needed, use MIDI-CI through MIDI 1.0. + * UMP is defined in "Universal MIDI Packet (UMP) Format and MIDI 2.0 Protocol" spec. + * MIDI-CI is defined in "MIDI Capability Inquiry (MIDI-CI)" spec. + */ + AMIDI_DEVICE_PROTOCOL_UNKNOWN = -1 +}; + +/* * Device API */ /** @@ -134,6 +206,30 @@ ssize_t AMIDI_API AMidiDevice_getNumInputPorts(const AMidiDevice *device) __INTR */ ssize_t AMIDI_API AMidiDevice_getNumOutputPorts(const AMidiDevice *device) __INTRODUCED_IN(29); +/** + * Gets the MIDI device default protocol. + * + * @param device Specifies the MIDI device. + * + * @return The identifier of the MIDI device default protocol: + * AMIDI_DEVICE_PROTOCOL_UMP_USE_MIDI_CI + * AMIDI_DEVICE_PROTOCOL_UMP_MIDI_1_0_UP_TO_64_BITS + * AMIDI_DEVICE_PROTOCOL_UMP_MIDI_1_0_UP_TO_64_BITS_AND_JRTS + * AMIDI_DEVICE_PROTOCOL_UMP_MIDI_1_0_UP_TO_128_BITS + * AMIDI_DEVICE_PROTOCOL_UMP_MIDI_1_0_UP_TO_128_BITS_AND_JRTS + * AMIDI_DEVICE_PROTOCOL_UMP_MIDI_2_0 + * AMIDI_DEVICE_PROTOCOL_UMP_MIDI_2_0_AND_JRTS + * AMIDI_DEVICE_PROTOCOL_UNKNOWN + * + * Most devices should return PROTOCOL_UNKNOWN (-1). This is intentional as devices + * with default UMP support are not backwards compatible. When the device is null, + * return AMIDI_DEVICE_PROTOCOL_UNKNOWN. + * + * Available since API 33. + */ +AMidiDevice_Protocol AMIDI_API AMidiDevice_getDefaultProtocol(const AMidiDevice *device) + __INTRODUCED_IN(33); + /* * API for receiving data from the Output port of a device. */ diff --git a/media/native/midi/libamidi.map.txt b/media/native/midi/libamidi.map.txt index 62627f8c5ef7..f25f97770b50 100644 --- a/media/native/midi/libamidi.map.txt +++ b/media/native/midi/libamidi.map.txt @@ -2,6 +2,7 @@ LIBAMIDI { global: AMidiDevice_fromJava; AMidiDevice_release; + AMidiDevice_getDefaultProtocol; # introduced=Tiramisu AMidiDevice_getType; AMidiDevice_getNumInputPorts; AMidiDevice_getNumOutputPorts; diff --git a/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiDevice.java b/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiDevice.java index 62c313ace306..2dd9525867b6 100644 --- a/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiDevice.java +++ b/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiDevice.java @@ -260,7 +260,8 @@ public final class BluetoothMidiDevice { inputPortReceivers[0] = mEventScheduler.getReceiver(); mDeviceServer = mMidiManager.createDeviceServer(inputPortReceivers, 1, - null, null, properties, MidiDeviceInfo.TYPE_BLUETOOTH, mDeviceServerCallback); + null, null, properties, MidiDeviceInfo.TYPE_BLUETOOTH, + MidiDeviceInfo.PROTOCOL_UNKNOWN, mDeviceServerCallback); mOutputReceiver = mDeviceServer.getOutputPortReceivers()[0]; diff --git a/services/midi/java/com/android/server/midi/MidiService.java b/services/midi/java/com/android/server/midi/MidiService.java index d0205ae24f85..e4e0ff0a81db 100644 --- a/services/midi/java/com/android/server/midi/MidiService.java +++ b/services/midi/java/com/android/server/midi/MidiService.java @@ -638,13 +638,31 @@ public class MidiService extends IMidiManager.Stub { private static final MidiDeviceInfo[] EMPTY_DEVICE_INFO_ARRAY = new MidiDeviceInfo[0]; public MidiDeviceInfo[] getDevices() { + return getDevicesForTransport(MidiManager.TRANSPORT_MIDI_BYTE_STREAM); + } + + /** + * @hide + */ + public MidiDeviceInfo[] getDevicesForTransport(int transport) { ArrayList<MidiDeviceInfo> deviceInfos = new ArrayList<MidiDeviceInfo>(); int uid = Binder.getCallingUid(); synchronized (mDevicesByInfo) { for (Device device : mDevicesByInfo.values()) { if (device.isUidAllowed(uid)) { - deviceInfos.add(device.getDeviceInfo()); + // UMP devices have protocols that are not PROTOCOL_UNKNOWN + if (transport == MidiManager.TRANSPORT_UNIVERSAL_MIDI_PACKETS) { + if (device.getDeviceInfo().getDefaultProtocol() + != MidiDeviceInfo.PROTOCOL_UNKNOWN) { + deviceInfos.add(device.getDeviceInfo()); + } + } else if (transport == MidiManager.TRANSPORT_MIDI_BYTE_STREAM) { + if (device.getDeviceInfo().getDefaultProtocol() + == MidiDeviceInfo.PROTOCOL_UNKNOWN) { + deviceInfos.add(device.getDeviceInfo()); + } + } } } } @@ -713,7 +731,7 @@ public class MidiService extends IMidiManager.Stub { @Override public MidiDeviceInfo registerDeviceServer(IMidiDeviceServer server, int numInputPorts, int numOutputPorts, String[] inputPortNames, String[] outputPortNames, - Bundle properties, int type) { + Bundle properties, int type, int defaultProtocol) { int uid = Binder.getCallingUid(); if (type == MidiDeviceInfo.TYPE_USB && uid != Process.SYSTEM_UID) { throw new SecurityException("only system can create USB devices"); @@ -723,7 +741,8 @@ public class MidiService extends IMidiManager.Stub { synchronized (mDevicesByInfo) { return addDeviceLocked(type, numInputPorts, numOutputPorts, inputPortNames, - outputPortNames, properties, server, null, false, uid); + outputPortNames, properties, server, null, false, uid, + defaultProtocol); } } @@ -792,11 +811,12 @@ public class MidiService extends IMidiManager.Stub { private MidiDeviceInfo addDeviceLocked(int type, int numInputPorts, int numOutputPorts, String[] inputPortNames, String[] outputPortNames, Bundle properties, IMidiDeviceServer server, ServiceInfo serviceInfo, - boolean isPrivate, int uid) { + boolean isPrivate, int uid, int defaultProtocol) { int id = mNextDeviceId++; MidiDeviceInfo deviceInfo = new MidiDeviceInfo(type, id, numInputPorts, numOutputPorts, - inputPortNames, outputPortNames, properties, isPrivate); + inputPortNames, outputPortNames, properties, isPrivate, + defaultProtocol); if (server != null) { try { @@ -983,10 +1003,11 @@ public class MidiService extends IMidiManager.Stub { synchronized (mDevicesByInfo) { addDeviceLocked(MidiDeviceInfo.TYPE_VIRTUAL, - numInputPorts, numOutputPorts, - inputPortNames.toArray(EMPTY_STRING_ARRAY), - outputPortNames.toArray(EMPTY_STRING_ARRAY), - properties, null, serviceInfo, isPrivate, uid); + numInputPorts, numOutputPorts, + inputPortNames.toArray(EMPTY_STRING_ARRAY), + outputPortNames.toArray(EMPTY_STRING_ARRAY), + properties, null, serviceInfo, isPrivate, uid, + MidiDeviceInfo.PROTOCOL_UNKNOWN); } // setting properties to null signals that we are no longer // processing a <device> diff --git a/services/usb/java/com/android/server/usb/UsbAlsaManager.java b/services/usb/java/com/android/server/usb/UsbAlsaManager.java index 0aa62c53e269..1c72eb8db708 100644 --- a/services/usb/java/com/android/server/usb/UsbAlsaManager.java +++ b/services/usb/java/com/android/server/usb/UsbAlsaManager.java @@ -257,10 +257,11 @@ public final class UsbAlsaManager { // look for MIDI devices boolean hasMidi = parser.hasMIDIInterface(); - int midiNumInputs = parser.calculateNumMidiInputs(); - int midiNumOutputs = parser.calculateNumMidiOutputs(); + int midiNumInputs = parser.calculateNumLegacyMidiInputs(); + int midiNumOutputs = parser.calculateNumLegacyMidiOutputs(); if (DEBUG) { Slog.d(TAG, "hasMidi: " + hasMidi + " mHasMidiFeature:" + mHasMidiFeature); + Slog.d(TAG, "midiNumInputs: " + midiNumInputs + " midiNumOutputs:" + midiNumOutputs); } if (hasMidi && mHasMidiFeature) { int device = 0; diff --git a/services/usb/java/com/android/server/usb/UsbHostManager.java b/services/usb/java/com/android/server/usb/UsbHostManager.java index f33001c9241e..9ac270f17fc4 100644 --- a/services/usb/java/com/android/server/usb/UsbHostManager.java +++ b/services/usb/java/com/android/server/usb/UsbHostManager.java @@ -23,6 +23,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.content.ComponentName; import android.content.Context; +import android.content.pm.PackageManager; import android.hardware.usb.UsbConstants; import android.hardware.usb.UsbDevice; import android.os.Bundle; @@ -46,6 +47,8 @@ import com.android.server.usb.descriptors.UsbInterfaceDescriptor; import com.android.server.usb.descriptors.report.TextReportCanvas; import com.android.server.usb.descriptors.tree.UsbDescriptorsTree; +import libcore.io.IoUtils; + import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; @@ -90,6 +93,13 @@ public class UsbHostManager { private ConnectionRecord mLastConnect; private final ArrayMap<String, ConnectionRecord> mConnected = new ArrayMap<>(); + /** + * List of connected MIDI devices + */ + private final HashMap<String, UsbUniversalMidiDevice> + mMidiDevices = new HashMap<String, UsbUniversalMidiDevice>(); + private final boolean mHasMidiFeature; + /* * ConnectionRecord * Stores connection/disconnection data. @@ -245,6 +255,7 @@ public class UsbHostManager { setUsbDeviceConnectionHandler(ComponentName.unflattenFromString( deviceConnectionHandler)); } + mHasMidiFeature = context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_MIDI); } public void setCurrentUserSettings(UsbProfileGroupSettingsManager settings) { @@ -413,6 +424,18 @@ public class UsbHostManager { mUsbAlsaManager.usbDeviceAdded(deviceAddress, newDevice, parser); + if (mHasMidiFeature) { + if (parser.containsUniversalMidiDeviceEndpoint()) { + UsbUniversalMidiDevice midiDevice = UsbUniversalMidiDevice.create(mContext, + newDevice, parser); + if (midiDevice != null) { + mMidiDevices.put(deviceAddress, midiDevice); + } else { + Slog.e(TAG, "Universal Midi Device is null."); + } + } + } + // Tracking addConnectionRecord(deviceAddress, ConnectionRecord.CONNECT, parser.getRawDescriptors()); @@ -446,6 +469,14 @@ public class UsbHostManager { Slog.d(TAG, "Removed device at " + deviceAddress + ": " + device.getProductName()); mUsbAlsaManager.usbDeviceRemoved(deviceAddress); mPermissionManager.usbDeviceRemoved(device); + + // MIDI + UsbUniversalMidiDevice midiDevice = mMidiDevices.remove(deviceAddress); + if (midiDevice != null) { + Slog.i(TAG, "USB Universal MIDI Device Removed: " + deviceAddress); + IoUtils.closeQuietly(midiDevice); + } + getCurrentUserSettings().usbDeviceRemoved(device); ConnectionRecord current = mConnected.get(deviceAddress); // Tracking diff --git a/services/usb/java/com/android/server/usb/UsbMidiDevice.java b/services/usb/java/com/android/server/usb/UsbMidiDevice.java index b61e93b36e58..275f21755141 100644 --- a/services/usb/java/com/android/server/usb/UsbMidiDevice.java +++ b/services/usb/java/com/android/server/usb/UsbMidiDevice.java @@ -303,7 +303,8 @@ public final class UsbMidiDevice implements Closeable { } mServer = midiManager.createDeviceServer(mMidiInputPortReceivers, mNumInputs, - null, null, properties, MidiDeviceInfo.TYPE_USB, mCallback); + null, null, properties, MidiDeviceInfo.TYPE_USB, + MidiDeviceInfo.PROTOCOL_UNKNOWN, mCallback); if (mServer == null) { return false; } diff --git a/services/usb/java/com/android/server/usb/UsbUniversalMidiDevice.java b/services/usb/java/com/android/server/usb/UsbUniversalMidiDevice.java new file mode 100644 index 000000000000..db0c80f189d3 --- /dev/null +++ b/services/usb/java/com/android/server/usb/UsbUniversalMidiDevice.java @@ -0,0 +1,469 @@ +/* + * Copyright (C) 2021 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.server.usb; + +import android.content.Context; +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbDeviceConnection; +import android.hardware.usb.UsbEndpoint; +import android.hardware.usb.UsbManager; +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.util.Log; + +import com.android.internal.midi.MidiEventScheduler; +import com.android.internal.midi.MidiEventScheduler.MidiEvent; +import com.android.server.usb.descriptors.UsbDescriptorParser; +import com.android.server.usb.descriptors.UsbEndpointDescriptor; +import com.android.server.usb.descriptors.UsbInterfaceDescriptor; +import com.android.server.usb.descriptors.UsbMidiBlockParser; + +import libcore.io.IoUtils; + +import java.io.Closeable; +import java.io.IOException; +import java.util.ArrayList; + +/** + * A MIDI device that opens device connections to MIDI 2.0 endpoints. + */ +public final class UsbUniversalMidiDevice implements Closeable { + private static final String TAG = "UsbUniversalMidiDevice"; + private static final boolean DEBUG = false; + + private Context mContext; + private UsbDevice mUsbDevice; + private UsbDescriptorParser mParser; + private ArrayList<UsbInterfaceDescriptor> mUsbInterfaces; + + // USB outputs are MIDI inputs + private final InputReceiverProxy[] mMidiInputPortReceivers; + private final int mNumInputs; + private final int mNumOutputs; + + private MidiDeviceServer mServer; + + // event schedulers for each input port of the physical device + private MidiEventScheduler[] mEventSchedulers; + + private ArrayList<UsbDeviceConnection> mUsbDeviceConnections; + private ArrayList<ArrayList<UsbEndpoint>> mInputUsbEndpoints; + private ArrayList<ArrayList<UsbEndpoint>> mOutputUsbEndpoints; + + private UsbMidiBlockParser mMidiBlockParser = new UsbMidiBlockParser(); + private int mDefaultMidiProtocol = MidiDeviceInfo.PROTOCOL_UMP_MIDI_1_0_UP_TO_64_BITS; + + private final Object mLock = new Object(); + private boolean mIsOpen; + + private final MidiDeviceServer.Callback mCallback = new MidiDeviceServer.Callback() { + + @Override + public void onDeviceStatusChanged(MidiDeviceServer server, MidiDeviceStatus status) { + MidiDeviceInfo deviceInfo = status.getDeviceInfo(); + int numInputPorts = deviceInfo.getInputPortCount(); + int numOutputPorts = deviceInfo.getOutputPortCount(); + boolean hasOpenPorts = false; + + for (int i = 0; i < numInputPorts; i++) { + if (status.isInputPortOpen(i)) { + hasOpenPorts = true; + break; + } + } + + if (!hasOpenPorts) { + for (int i = 0; i < numOutputPorts; i++) { + if (status.getOutputPortOpenCount(i) > 0) { + hasOpenPorts = true; + break; + } + } + } + + synchronized (mLock) { + if (hasOpenPorts && !mIsOpen) { + openLocked(); + } else if (!hasOpenPorts && mIsOpen) { + closeLocked(); + } + } + } + + @Override + public void onClose() { + } + }; + + // This class acts as a proxy for our MidiEventScheduler receivers, which do not exist + // until the device has active clients + private static final class InputReceiverProxy extends MidiReceiver { + private MidiReceiver mReceiver; + + @Override + public void onSend(byte[] msg, int offset, int count, long timestamp) throws IOException { + MidiReceiver receiver = mReceiver; + if (receiver != null) { + receiver.send(msg, offset, count, timestamp); + } + } + + public void setReceiver(MidiReceiver receiver) { + mReceiver = receiver; + } + + @Override + public void onFlush() throws IOException { + MidiReceiver receiver = mReceiver; + if (receiver != null) { + receiver.flush(); + } + } + } + + /** + * Creates an UsbUniversalMidiDevice based on the input parameters. Read/Write streams + * will be created individually as some devices don't have the same number of + * inputs and outputs. + */ + public static UsbUniversalMidiDevice create(Context context, UsbDevice usbDevice, + UsbDescriptorParser parser) { + UsbUniversalMidiDevice midiDevice = new UsbUniversalMidiDevice(usbDevice, parser); + if (!midiDevice.register(context)) { + IoUtils.closeQuietly(midiDevice); + Log.e(TAG, "createDeviceServer failed"); + return null; + } + return midiDevice; + } + + private UsbUniversalMidiDevice(UsbDevice usbDevice, UsbDescriptorParser parser) { + mUsbDevice = usbDevice; + mParser = parser; + + mUsbInterfaces = parser.findUniversalMidiInterfaceDescriptors(); + + int numInputs = 0; + int numOutputs = 0; + + for (int interfaceIndex = 0; interfaceIndex < mUsbInterfaces.size(); interfaceIndex++) { + UsbInterfaceDescriptor interfaceDescriptor = mUsbInterfaces.get(interfaceIndex); + for (int endpointIndex = 0; endpointIndex < interfaceDescriptor.getNumEndpoints(); + endpointIndex++) { + UsbEndpointDescriptor endpoint = + interfaceDescriptor.getEndpointDescriptor(endpointIndex); + // 0 is output, 1 << 7 is input. + if (endpoint.getDirection() == 0) { + numOutputs++; + } else { + numInputs++; + } + } + } + + mNumInputs = numInputs; + mNumOutputs = numOutputs; + + Log.d(TAG, "Created UsbUniversalMidiDevice with " + numInputs + " inputs and " + + numOutputs + " outputs"); + + // Create MIDI port receivers based on the number of output ports. The + // output of USB is the input of MIDI. + mMidiInputPortReceivers = new InputReceiverProxy[numOutputs]; + for (int port = 0; port < numOutputs; port++) { + mMidiInputPortReceivers[port] = new InputReceiverProxy(); + } + } + + private int calculateDefaultMidiProtocol() { + UsbManager manager = mContext.getSystemService(UsbManager.class); + + for (int interfaceIndex = 0; interfaceIndex < mUsbInterfaces.size(); interfaceIndex++) { + UsbInterfaceDescriptor interfaceDescriptor = mUsbInterfaces.get(interfaceIndex); + boolean doesInterfaceContainInput = false; + boolean doesInterfaceContainOutput = false; + for (int endpointIndex = 0; (endpointIndex < interfaceDescriptor.getNumEndpoints()) + && !(doesInterfaceContainInput && doesInterfaceContainOutput); + endpointIndex++) { + UsbEndpointDescriptor endpoint = + interfaceDescriptor.getEndpointDescriptor(endpointIndex); + // 0 is output, 1 << 7 is input. + if (endpoint.getDirection() == 0) { + doesInterfaceContainOutput = true; + } else { + doesInterfaceContainInput = true; + } + } + + // Intentionally open the device connection to query the default MIDI type for + // a connection with both the input and output set. + if (doesInterfaceContainInput + && doesInterfaceContainOutput) { + UsbDeviceConnection connection = manager.openDevice(mUsbDevice); + if (!connection.claimInterface(interfaceDescriptor.toAndroid(mParser), true)) { + Log.d(TAG, "Can't claim control interface"); + continue; + } + int defaultMidiProtocol = mMidiBlockParser.calculateMidiType(connection, + interfaceDescriptor.getInterfaceNumber(), + interfaceDescriptor.getAlternateSetting()); + + connection.close(); + return defaultMidiProtocol; + } + } + + Log.d(TAG, "Cannot find interface with both input and output endpoints"); + return MidiDeviceInfo.PROTOCOL_UMP_MIDI_1_0_UP_TO_64_BITS; + } + + private boolean openLocked() { + UsbManager manager = mContext.getSystemService(UsbManager.class); + + mUsbDeviceConnections = new ArrayList<UsbDeviceConnection>(mUsbInterfaces.size()); + mInputUsbEndpoints = new ArrayList<ArrayList<UsbEndpoint>>(mUsbInterfaces.size()); + mOutputUsbEndpoints = new ArrayList<ArrayList<UsbEndpoint>>(mUsbInterfaces.size()); + + for (int interfaceIndex = 0; interfaceIndex < mUsbInterfaces.size(); interfaceIndex++) { + ArrayList<UsbEndpoint> inputEndpoints = new ArrayList<UsbEndpoint>(); + ArrayList<UsbEndpoint> outputEndpoints = new ArrayList<UsbEndpoint>(); + UsbInterfaceDescriptor interfaceDescriptor = mUsbInterfaces.get(interfaceIndex); + for (int endpointIndex = 0; endpointIndex < interfaceDescriptor.getNumEndpoints(); + endpointIndex++) { + UsbEndpointDescriptor endpoint = + interfaceDescriptor.getEndpointDescriptor(endpointIndex); + // 0 is output, 1 << 7 is input. + if (endpoint.getDirection() == 0) { + outputEndpoints.add(endpoint.toAndroid(mParser)); + } else { + inputEndpoints.add(endpoint.toAndroid(mParser)); + } + } + if (!outputEndpoints.isEmpty() || !inputEndpoints.isEmpty()) { + UsbDeviceConnection connection = manager.openDevice(mUsbDevice); + connection.setInterface(interfaceDescriptor.toAndroid(mParser)); + connection.claimInterface(interfaceDescriptor.toAndroid(mParser), true); + mUsbDeviceConnections.add(connection); + mInputUsbEndpoints.add(inputEndpoints); + mOutputUsbEndpoints.add(outputEndpoints); + } + } + + mEventSchedulers = new MidiEventScheduler[mNumOutputs]; + + for (int i = 0; i < mNumOutputs; i++) { + MidiEventScheduler scheduler = new MidiEventScheduler(); + mEventSchedulers[i] = scheduler; + mMidiInputPortReceivers[i].setReceiver(scheduler.getReceiver()); + } + + final MidiReceiver[] outputReceivers = mServer.getOutputPortReceivers(); + + // Create input thread for each input port of the physical device + int portNumber = 0; + for (int connectionIndex = 0; connectionIndex < mInputUsbEndpoints.size(); + connectionIndex++) { + for (int endpointIndex = 0; + endpointIndex < mInputUsbEndpoints.get(connectionIndex).size(); + endpointIndex++) { + final UsbDeviceConnection connectionF = mUsbDeviceConnections.get(connectionIndex); + final UsbEndpoint epF = mInputUsbEndpoints.get(connectionIndex).get(endpointIndex); + final int portF = portNumber; + + new Thread("UsbUniversalMidiDevice input thread " + portF) { + @Override + public void run() { + byte[] inputBuffer = new byte[epF.getMaxPacketSize()]; + try { + while (true) { + // Record time of event immediately after waking. + long timestamp = System.nanoTime(); + synchronized (mLock) { + if (!mIsOpen) break; + + int nRead = connectionF.bulkTransfer(epF, inputBuffer, + inputBuffer.length, 0); + + // For USB, each 32 bit word of a UMP is + // sent with the least significant byte first. + swapEndiannessPerWord(inputBuffer, inputBuffer.length); + + if (nRead > 0) { + if (DEBUG) { + logByteArray("Input ", inputBuffer, 0, + nRead); + } + outputReceivers[portF].send(inputBuffer, 0, nRead, + timestamp); + } + } + } + } catch (IOException e) { + Log.d(TAG, "reader thread exiting"); + } + Log.d(TAG, "input thread exit"); + } + }.start(); + + portNumber++; + } + } + + // Create output thread for each output port of the physical device + portNumber = 0; + for (int connectionIndex = 0; connectionIndex < mOutputUsbEndpoints.size(); + connectionIndex++) { + for (int endpointIndex = 0; + endpointIndex < mOutputUsbEndpoints.get(connectionIndex).size(); + endpointIndex++) { + final UsbDeviceConnection connectionF = mUsbDeviceConnections.get(connectionIndex); + final UsbEndpoint epF = + mOutputUsbEndpoints.get(connectionIndex).get(endpointIndex); + final int portF = portNumber; + final MidiEventScheduler eventSchedulerF = mEventSchedulers[portF]; + + new Thread("UsbUniversalMidiDevice output thread " + portF) { + @Override + public void run() { + while (true) { + MidiEvent event; + try { + event = (MidiEvent) eventSchedulerF.waitNextEvent(); + } catch (InterruptedException e) { + // try again + continue; + } + if (event == null) { + break; + } + + // For USB, each 32 bit word of a UMP is + // sent with the least significant byte first. + swapEndiannessPerWord(event.data, event.count); + + if (DEBUG) { + logByteArray("Output ", event.data, 0, + event.count); + } + connectionF.bulkTransfer(epF, event.data, event.count, 0); + eventSchedulerF.addEventToPool(event); + } + Log.d(TAG, "output thread exit"); + } + }.start(); + + portNumber++; + } + } + + mIsOpen = true; + return true; + } + + private boolean register(Context context) { + mContext = context; + MidiManager midiManager = context.getSystemService(MidiManager.class); + if (midiManager == null) { + Log.e(TAG, "No MidiManager in UsbUniversalMidiDevice.create()"); + return false; + } + + mDefaultMidiProtocol = calculateDefaultMidiProtocol(); + + Bundle properties = new Bundle(); + String manufacturer = mUsbDevice.getManufacturerName(); + String product = mUsbDevice.getProductName(); + String version = mUsbDevice.getVersion(); + String name; + if (manufacturer == null || manufacturer.isEmpty()) { + name = product; + } else if (product == null || product.isEmpty()) { + name = manufacturer; + } else { + name = manufacturer + " " + product + " MIDI 2.0"; + } + properties.putString(MidiDeviceInfo.PROPERTY_NAME, name); + properties.putString(MidiDeviceInfo.PROPERTY_MANUFACTURER, manufacturer); + properties.putString(MidiDeviceInfo.PROPERTY_PRODUCT, product); + properties.putString(MidiDeviceInfo.PROPERTY_VERSION, version); + properties.putString(MidiDeviceInfo.PROPERTY_SERIAL_NUMBER, + mUsbDevice.getSerialNumber()); + properties.putParcelable(MidiDeviceInfo.PROPERTY_USB_DEVICE, mUsbDevice); + + mServer = midiManager.createDeviceServer(mMidiInputPortReceivers, mNumInputs, + null, null, properties, MidiDeviceInfo.TYPE_USB, mDefaultMidiProtocol, mCallback); + if (mServer == null) { + return false; + } + + return true; + } + + @Override + public void close() throws IOException { + synchronized (mLock) { + if (mIsOpen) { + closeLocked(); + } + } + + if (mServer != null) { + IoUtils.closeQuietly(mServer); + } + } + + private void closeLocked() { + for (int i = 0; i < mEventSchedulers.length; i++) { + mMidiInputPortReceivers[i].setReceiver(null); + mEventSchedulers[i].close(); + } + for (UsbDeviceConnection connection : mUsbDeviceConnections) { + connection.close(); + } + mUsbDeviceConnections = null; + mInputUsbEndpoints = null; + mOutputUsbEndpoints = null; + + mIsOpen = false; + } + + private void swapEndiannessPerWord(byte[] array, int size) { + for (int i = 0; i + 3 < size; i += 4) { + byte tmp = array[i]; + array[i] = array[i + 3]; + array[i + 3] = tmp; + tmp = array[i + 1]; + array[i + 1] = array[i + 2]; + array[i + 2] = tmp; + } + } + + private static void logByteArray(String prefix, byte[] value, int offset, int count) { + StringBuilder builder = new StringBuilder(prefix); + for (int i = offset; i < offset + count; i++) { + builder.append(String.format("0x%02X", value[i])); + if (i != value.length - 1) { + builder.append(", "); + } + } + Log.d(TAG, builder.toString()); + } +} diff --git a/services/usb/java/com/android/server/usb/descriptors/UsbACAudioControlEndpoint.java b/services/usb/java/com/android/server/usb/descriptors/UsbACAudioControlEndpoint.java index 409e605c3c2f..bfcf62147f75 100644 --- a/services/usb/java/com/android/server/usb/descriptors/UsbACAudioControlEndpoint.java +++ b/services/usb/java/com/android/server/usb/descriptors/UsbACAudioControlEndpoint.java @@ -38,8 +38,8 @@ public class UsbACAudioControlEndpoint extends UsbACEndpoint { static final byte ATTRIBSMASK_SYNC = 0x0C; static final byte ATTRIBMASK_TRANS = 0x03; - public UsbACAudioControlEndpoint(int length, byte type, int subclass) { - super(length, type, subclass); + public UsbACAudioControlEndpoint(int length, byte type, int subclass, byte subtype) { + super(length, type, subclass, subtype); } public byte getAddress() { diff --git a/services/usb/java/com/android/server/usb/descriptors/UsbACAudioStreamEndpoint.java b/services/usb/java/com/android/server/usb/descriptors/UsbACAudioStreamEndpoint.java index e63bb74abdf7..ae9ca0d827f5 100644 --- a/services/usb/java/com/android/server/usb/descriptors/UsbACAudioStreamEndpoint.java +++ b/services/usb/java/com/android/server/usb/descriptors/UsbACAudioStreamEndpoint.java @@ -24,8 +24,8 @@ public class UsbACAudioStreamEndpoint extends UsbACEndpoint { private static final String TAG = "UsbACAudioStreamEndpoint"; //TODO data fields... - public UsbACAudioStreamEndpoint(int length, byte type, int subclass) { - super(length, type, subclass); + public UsbACAudioStreamEndpoint(int length, byte type, int subclass, byte subtype) { + super(length, type, subclass, subtype); } @Override diff --git a/services/usb/java/com/android/server/usb/descriptors/UsbACEndpoint.java b/services/usb/java/com/android/server/usb/descriptors/UsbACEndpoint.java index ff7f3934086c..b7f9ac334954 100644 --- a/services/usb/java/com/android/server/usb/descriptors/UsbACEndpoint.java +++ b/services/usb/java/com/android/server/usb/descriptors/UsbACEndpoint.java @@ -25,13 +25,17 @@ import android.util.Log; abstract class UsbACEndpoint extends UsbDescriptor { private static final String TAG = "UsbACEndpoint"; + public static final byte MS_GENERAL = 1; + public static final byte MS_GENERAL_2_0 = 2; + protected final int mSubclass; // from the mSubclass member of the "enclosing" // Interface Descriptor, not the stream. - protected byte mSubtype; // 2:1 HEADER descriptor subtype + protected final byte mSubtype; // 2:1 HEADER descriptor subtype - UsbACEndpoint(int length, byte type, int subclass) { + UsbACEndpoint(int length, byte type, int subclass, byte subtype) { super(length, type); mSubclass = subclass; + mSubtype = subtype; } public int getSubclass() { @@ -44,33 +48,39 @@ abstract class UsbACEndpoint extends UsbDescriptor { @Override public int parseRawDescriptors(ByteStream stream) { - mSubtype = stream.getByte(); return mLength; } public static UsbDescriptor allocDescriptor(UsbDescriptorParser parser, - int length, byte type) { + int length, byte type, byte subType) { UsbInterfaceDescriptor interfaceDesc = parser.getCurInterface(); int subClass = interfaceDesc.getUsbSubclass(); - // TODO shouldn't this switch on subtype? switch (subClass) { case AUDIO_AUDIOCONTROL: if (UsbDescriptorParser.DEBUG) { Log.d(TAG, "---> AUDIO_AUDIOCONTROL"); } - return new UsbACAudioControlEndpoint(length, type, subClass); + return new UsbACAudioControlEndpoint(length, type, subClass, subType); case AUDIO_AUDIOSTREAMING: if (UsbDescriptorParser.DEBUG) { Log.d(TAG, "---> AUDIO_AUDIOSTREAMING"); } - return new UsbACAudioStreamEndpoint(length, type, subClass); + return new UsbACAudioStreamEndpoint(length, type, subClass, subType); case AUDIO_MIDISTREAMING: if (UsbDescriptorParser.DEBUG) { Log.d(TAG, "---> AUDIO_MIDISTREAMING"); } - return new UsbACMidiEndpoint(length, type, subClass); + switch (subType) { + case MS_GENERAL: + return new UsbACMidi10Endpoint(length, type, subClass, subType); + case MS_GENERAL_2_0: + return new UsbACMidi20Endpoint(length, type, subClass, subType); + default: + Log.w(TAG, "Unknown Midi Endpoint id:0x" + Integer.toHexString(subType)); + return null; + } default: Log.w(TAG, "Unknown Audio Class Endpoint id:0x" + Integer.toHexString(subClass)); diff --git a/services/usb/java/com/android/server/usb/descriptors/UsbACMidiEndpoint.java b/services/usb/java/com/android/server/usb/descriptors/UsbACMidi10Endpoint.java index 42ee88922edd..49b9d7b30d29 100644 --- a/services/usb/java/com/android/server/usb/descriptors/UsbACMidiEndpoint.java +++ b/services/usb/java/com/android/server/usb/descriptors/UsbACMidi10Endpoint.java @@ -22,14 +22,14 @@ import com.android.server.usb.descriptors.report.ReportCanvas; * An audio class-specific Midi Endpoint. * see midi10.pdf section 6.2.2 */ -public final class UsbACMidiEndpoint extends UsbACEndpoint { - private static final String TAG = "UsbACMidiEndpoint"; +public final class UsbACMidi10Endpoint extends UsbACEndpoint { + private static final String TAG = "UsbACMidi10Endpoint"; private byte mNumJacks; - private byte[] mJackIds; + private byte[] mJackIds = new byte[0]; - public UsbACMidiEndpoint(int length, byte type, int subclass) { - super(length, type, subclass); + public UsbACMidi10Endpoint(int length, byte type, int subclass, byte subtype) { + super(length, type, subclass, subtype); } public byte getNumJacks() { @@ -45,9 +45,11 @@ public final class UsbACMidiEndpoint extends UsbACEndpoint { super.parseRawDescriptors(stream); mNumJacks = stream.getByte(); - mJackIds = new byte[mNumJacks]; - for (int jack = 0; jack < mNumJacks; jack++) { - mJackIds[jack] = stream.getByte(); + if (mNumJacks > 0) { + mJackIds = new byte[mNumJacks]; + for (int jack = 0; jack < mNumJacks; jack++) { + mJackIds[jack] = stream.getByte(); + } } return mLength; } @@ -56,10 +58,10 @@ public final class UsbACMidiEndpoint extends UsbACEndpoint { public void report(ReportCanvas canvas) { super.report(canvas); - canvas.writeHeader(3, "AC Midi Endpoint: " + ReportCanvas.getHexString(getType()) + canvas.writeHeader(3, "ACMidi10Endpoint: " + ReportCanvas.getHexString(getType()) + " Length: " + getLength()); canvas.openList(); canvas.writeListItem("" + getNumJacks() + " Jacks."); canvas.closeList(); } -}
\ No newline at end of file +} diff --git a/services/usb/java/com/android/server/usb/descriptors/UsbACMidi20Endpoint.java b/services/usb/java/com/android/server/usb/descriptors/UsbACMidi20Endpoint.java new file mode 100644 index 000000000000..1024a5bf55a6 --- /dev/null +++ b/services/usb/java/com/android/server/usb/descriptors/UsbACMidi20Endpoint.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2017 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.server.usb.descriptors; + +import com.android.server.usb.descriptors.report.ReportCanvas; + +/** + * @hide + * An audio class-specific Midi Endpoint. + * see midi10.pdf section 6.2.2 + */ +public final class UsbACMidi20Endpoint extends UsbACEndpoint { + private static final String TAG = "UsbACMidi20Endpoint"; + + private byte mNumGroupTerminals; + private byte[] mBlockIds = new byte[0]; + + public UsbACMidi20Endpoint(int length, byte type, int subclass, byte subtype) { + super(length, type, subclass, subtype); + } + + public byte getNumGroupTerminals() { + return mNumGroupTerminals; + } + + public byte[] getBlockIds() { + return mBlockIds; + } + + @Override + public int parseRawDescriptors(ByteStream stream) { + super.parseRawDescriptors(stream); + + mNumGroupTerminals = stream.getByte(); + if (mNumGroupTerminals > 0) { + mBlockIds = new byte[mNumGroupTerminals]; + for (int block = 0; block < mNumGroupTerminals; block++) { + mBlockIds[block] = stream.getByte(); + } + } + return mLength; + } + + @Override + public void report(ReportCanvas canvas) { + super.report(canvas); + + canvas.writeHeader(3, "AC Midi20 Endpoint: " + ReportCanvas.getHexString(getType()) + + " Length: " + getLength()); + canvas.openList(); + canvas.writeListItem("" + getNumGroupTerminals() + " Group Terminals."); + canvas.closeList(); + } +} diff --git a/services/usb/java/com/android/server/usb/descriptors/UsbDescriptorParser.java b/services/usb/java/com/android/server/usb/descriptors/UsbDescriptorParser.java index 7250a071835d..3412a6f80cc7 100644 --- a/services/usb/java/com/android/server/usb/descriptors/UsbDescriptorParser.java +++ b/services/usb/java/com/android/server/usb/descriptors/UsbDescriptorParser.java @@ -30,6 +30,9 @@ public final class UsbDescriptorParser { private final String mDeviceAddr; + private static final int MS_MIDI_1_0 = 0x0100; + private static final int MS_MIDI_2_0 = 0x0200; + // Descriptor Objects private static final int DESCRIPTORS_ALLOC_SIZE = 128; private final ArrayList<UsbDescriptor> mDescriptors; @@ -215,6 +218,7 @@ public final class UsbDescriptorParser { Log.w(TAG, " Unparsed Class-specific"); break; } + mCurInterfaceDescriptor.setClassSpecificInterfaceDescriptor(descriptor); } break; @@ -222,17 +226,25 @@ public final class UsbDescriptorParser { if (mCurInterfaceDescriptor != null) { int subClass = mCurInterfaceDescriptor.getUsbClass(); switch (subClass) { - case UsbDescriptor.CLASSID_AUDIO: - descriptor = UsbACEndpoint.allocDescriptor(this, length, type); + case UsbDescriptor.CLASSID_AUDIO: { + Byte subType = stream.getByte(); + if (DEBUG) { + Log.d(TAG, "UsbDescriptor.CLASSID_AUDIO type:0x" + + Integer.toHexString(type)); + } + descriptor = UsbACEndpoint.allocDescriptor(this, length, type, + subType); + } break; case UsbDescriptor.CLASSID_VIDEO: { - Byte subtype = stream.getByte(); + Byte subType = stream.getByte(); if (DEBUG) { Log.d(TAG, "UsbDescriptor.CLASSID_VIDEO type:0x" + Integer.toHexString(type)); } - descriptor = UsbVCEndpoint.allocDescriptor(this, length, type, subtype); + descriptor = UsbVCEndpoint.allocDescriptor(this, length, type, + subType); } break; @@ -644,8 +656,8 @@ public final class UsbDescriptorParser { for (UsbDescriptor descriptor : descriptors) { // enusure that this isn't an unrecognized interface descriptor if (descriptor instanceof UsbInterfaceDescriptor) { - UsbInterfaceDescriptor interfaceDescr = (UsbInterfaceDescriptor) descriptor; - if (interfaceDescr.getUsbSubclass() == UsbDescriptor.AUDIO_MIDISTREAMING) { + UsbInterfaceDescriptor interfaceDescriptor = (UsbInterfaceDescriptor) descriptor; + if (interfaceDescriptor.getUsbSubclass() == UsbDescriptor.AUDIO_MIDISTREAMING) { return true; } } else { @@ -656,17 +668,90 @@ public final class UsbDescriptorParser { return false; } - private int calculateNumMidiPorts(boolean isOutput) { + /** + * @hide + */ + public boolean containsUniversalMidiDeviceEndpoint() { + ArrayList<UsbInterfaceDescriptor> interfaceDescriptors = + findUniversalMidiInterfaceDescriptors(); + int outputCount = 0; + int inputCount = 0; + for (int interfaceIndex = 0; interfaceIndex < interfaceDescriptors.size(); + interfaceIndex++) { + UsbInterfaceDescriptor interfaceDescriptor = interfaceDescriptors.get(interfaceIndex); + for (int endpointIndex = 0; endpointIndex < interfaceDescriptor.getNumEndpoints(); + endpointIndex++) { + UsbEndpointDescriptor endpoint = + interfaceDescriptor.getEndpointDescriptor(endpointIndex); + // 0 is output, 1 << 7 is input. + if (endpoint.getDirection() == 0) { + outputCount++; + } else { + inputCount++; + } + } + } + return (outputCount > 0) || (inputCount > 0); + } + + /** + * @hide + */ + public ArrayList<UsbInterfaceDescriptor> findUniversalMidiInterfaceDescriptors() { int count = 0; ArrayList<UsbDescriptor> descriptors = getInterfaceDescriptorsForClass(UsbDescriptor.CLASSID_AUDIO); + ArrayList<UsbInterfaceDescriptor> universalMidiInterfaces = + new ArrayList<UsbInterfaceDescriptor>(); + for (UsbDescriptor descriptor : descriptors) { // ensure that this isn't an unrecognized interface descriptor if (descriptor instanceof UsbInterfaceDescriptor) { - UsbInterfaceDescriptor interfaceDescr = (UsbInterfaceDescriptor) descriptor; - if (interfaceDescr.getUsbSubclass() == UsbDescriptor.AUDIO_MIDISTREAMING) { - for (int i = 0; i < interfaceDescr.getNumEndpoints(); i++) { - UsbEndpointDescriptor endpoint = interfaceDescr.getEndpointDescriptor(i); + UsbInterfaceDescriptor interfaceDescriptor = (UsbInterfaceDescriptor) descriptor; + if (interfaceDescriptor.getUsbSubclass() == UsbDescriptor.AUDIO_MIDISTREAMING) { + UsbDescriptor classSpecificDescriptor = + interfaceDescriptor.getClassSpecificInterfaceDescriptor(); + if (classSpecificDescriptor != null) { + if (classSpecificDescriptor instanceof UsbMSMidiHeader) { + UsbMSMidiHeader midiHeader = + (UsbMSMidiHeader) classSpecificDescriptor; + if (midiHeader.getMidiStreamingClass() == MS_MIDI_2_0) { + universalMidiInterfaces.add(interfaceDescriptor); + } + } + } + } + } else { + Log.w(TAG, "Undefined Audio Class Interface l: " + descriptor.getLength() + + " t:0x" + Integer.toHexString(descriptor.getType())); + } + } + return universalMidiInterfaces; + } + + private int calculateNumLegacyMidiPorts(boolean isOutput) { + int count = 0; + ArrayList<UsbDescriptor> descriptors = + getInterfaceDescriptorsForClass(UsbDescriptor.CLASSID_AUDIO); + for (UsbDescriptor descriptor : descriptors) { + // ensure that this isn't an unrecognized interface descriptor + if (descriptor instanceof UsbInterfaceDescriptor) { + UsbInterfaceDescriptor interfaceDescriptor = (UsbInterfaceDescriptor) descriptor; + if (interfaceDescriptor.getUsbSubclass() == UsbDescriptor.AUDIO_MIDISTREAMING) { + UsbDescriptor classSpecificDescriptor = + interfaceDescriptor.getClassSpecificInterfaceDescriptor(); + if (classSpecificDescriptor != null) { + if (classSpecificDescriptor instanceof UsbMSMidiHeader) { + UsbMSMidiHeader midiHeader = + (UsbMSMidiHeader) classSpecificDescriptor; + if (midiHeader.getMidiStreamingClass() != MS_MIDI_1_0) { + continue; + } + } + } + for (int i = 0; i < interfaceDescriptor.getNumEndpoints(); i++) { + UsbEndpointDescriptor endpoint = + interfaceDescriptor.getEndpointDescriptor(i); // 0 is output, 1 << 7 is input. if ((endpoint.getDirection() == 0) == isOutput) { count++; @@ -684,15 +769,15 @@ public final class UsbDescriptorParser { /** * @hide */ - public int calculateNumMidiInputs() { - return calculateNumMidiPorts(false /*isOutput*/); + public int calculateNumLegacyMidiInputs() { + return calculateNumLegacyMidiPorts(false /*isOutput*/); } /** * @hide */ - public int calculateNumMidiOutputs() { - return calculateNumMidiPorts(true /*isOutput*/); + public int calculateNumLegacyMidiOutputs() { + return calculateNumLegacyMidiPorts(true /*isOutput*/); } /** diff --git a/services/usb/java/com/android/server/usb/descriptors/UsbEndpointDescriptor.java b/services/usb/java/com/android/server/usb/descriptors/UsbEndpointDescriptor.java index 4d0cfea98630..ab07ce7fdb7a 100644 --- a/services/usb/java/com/android/server/usb/descriptors/UsbEndpointDescriptor.java +++ b/services/usb/java/com/android/server/usb/descriptors/UsbEndpointDescriptor.java @@ -112,7 +112,10 @@ public class UsbEndpointDescriptor extends UsbDescriptor { return mEndpointAddress & UsbEndpointDescriptor.MASK_ENDPOINT_DIRECTION; } - /* package */ UsbEndpoint toAndroid(UsbDescriptorParser parser) { + /** + * Returns a UsbEndpoint that this UsbEndpointDescriptor is describing. + */ + public UsbEndpoint toAndroid(UsbDescriptorParser parser) { if (UsbDescriptorParser.DEBUG) { Log.d(TAG, "toAndroid() type:" + Integer.toHexString(mAttributes & MASK_ATTRIBS_TRANSTYPE) diff --git a/services/usb/java/com/android/server/usb/descriptors/UsbInterfaceDescriptor.java b/services/usb/java/com/android/server/usb/descriptors/UsbInterfaceDescriptor.java index 64dbd971cc67..ca4613b17873 100644 --- a/services/usb/java/com/android/server/usb/descriptors/UsbInterfaceDescriptor.java +++ b/services/usb/java/com/android/server/usb/descriptors/UsbInterfaceDescriptor.java @@ -42,6 +42,8 @@ public class UsbInterfaceDescriptor extends UsbDescriptor { private ArrayList<UsbEndpointDescriptor> mEndpointDescriptors = new ArrayList<UsbEndpointDescriptor>(); + private UsbDescriptor mClassSpecificInterfaceDescriptor; + UsbInterfaceDescriptor(int length, byte type) { super(length, type); mHierarchyLevel = 3; @@ -105,7 +107,18 @@ public class UsbInterfaceDescriptor extends UsbDescriptor { mEndpointDescriptors.add(endpoint); } - UsbInterface toAndroid(UsbDescriptorParser parser) { + public void setClassSpecificInterfaceDescriptor(UsbDescriptor descriptor) { + mClassSpecificInterfaceDescriptor = descriptor; + } + + public UsbDescriptor getClassSpecificInterfaceDescriptor() { + return mClassSpecificInterfaceDescriptor; + } + + /** + * Returns a UsbInterface that this UsbInterfaceDescriptor is describing. + */ + public UsbInterface toAndroid(UsbDescriptorParser parser) { if (UsbDescriptorParser.DEBUG) { Log.d(TAG, "toAndroid() class:" + Integer.toHexString(mUsbClass) + " subclass:" + Integer.toHexString(mUsbSubclass) diff --git a/services/usb/java/com/android/server/usb/descriptors/UsbMSMidiHeader.java b/services/usb/java/com/android/server/usb/descriptors/UsbMSMidiHeader.java index d0ca6db87d38..76535612f54d 100644 --- a/services/usb/java/com/android/server/usb/descriptors/UsbMSMidiHeader.java +++ b/services/usb/java/com/android/server/usb/descriptors/UsbMSMidiHeader.java @@ -25,13 +25,19 @@ import com.android.server.usb.descriptors.report.ReportCanvas; public final class UsbMSMidiHeader extends UsbACInterface { private static final String TAG = "UsbMSMidiHeader"; + private int mMidiStreamingClass; // MSC Specification Release (BCD). + public UsbMSMidiHeader(int length, byte type, byte subtype, int subclass) { super(length, type, subtype, subclass); } + public int getMidiStreamingClass() { + return mMidiStreamingClass; + } + @Override public int parseRawDescriptors(ByteStream stream) { - // TODO - read data memebers + mMidiStreamingClass = stream.unpackUsbShort(); stream.advance(mLength - stream.getReadCount()); return mLength; } @@ -42,6 +48,7 @@ public final class UsbMSMidiHeader extends UsbACInterface { canvas.writeHeader(3, "MS Midi Header: " + ReportCanvas.getHexString(getType()) + " SubType: " + ReportCanvas.getHexString(getSubclass()) - + " Length: " + getLength()); + + " Length: " + getLength() + + " MidiStreamingClass :" + getMidiStreamingClass()); } } diff --git a/services/usb/java/com/android/server/usb/descriptors/UsbMidiBlockParser.java b/services/usb/java/com/android/server/usb/descriptors/UsbMidiBlockParser.java new file mode 100644 index 000000000000..37bd0f8f6f7b --- /dev/null +++ b/services/usb/java/com/android/server/usb/descriptors/UsbMidiBlockParser.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2017 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.server.usb.descriptors; + +import android.hardware.usb.UsbConstants; +import android.hardware.usb.UsbDeviceConnection; +import android.util.Log; + +import java.util.ArrayList; + +/** + * @hide + * A class to parse Block Descriptors + * see midi20.pdf section 5.4 + */ +public class UsbMidiBlockParser { + private static final String TAG = "UsbMidiBlockParser"; + + // Block header size + public static final int MIDI_BLOCK_HEADER_SIZE = 5; + public static final int MIDI_BLOCK_SIZE = 13; + public static final int REQ_GET_DESCRIPTOR = 0x06; + public static final int CS_GR_TRM_BLOCK = 0x26; // Class-specific GR_TRM_BLK + public static final int GR_TRM_BLOCK_HEADER = 0x01; // Group block header + public static final int REQ_TIMEOUT_MS = 2000; // 2 second timeout + public static final int DEFAULT_MIDI_TYPE = 1; // Default MIDI type + + protected int mHeaderLength; // 0:1 Size of header descriptor + protected int mHeaderDescriptorType; // 1:1 Descriptor Type + protected int mHeaderDescriptorSubtype; // 2:1 Descriptor Subtype + protected int mTotalLength; // 3:2 Total Length of header and blocks + + static class GroupTerminalBlock { + protected int mLength; // 0:1 Size of descriptor + protected int mDescriptorType; // 1:1 Descriptor Type + protected int mDescriptorSubtype; // 2:1 Descriptor Subtype + protected int mGroupBlockId; // 3:1 Id of Group Terminal Block + protected int mGroupTerminalBlockType; // 4:1 bi-directional, IN, or OUT + protected int mGroupTerminal; // 5:1 Group Terminal Number + protected int mNumGroupTerminals; // 6:1 Number of Group Terminals + protected int mBlockItem; // 7:1 ID of STRING descriptor of Block item + protected int mMidiProtocol; // 8:1 MIDI protocol + protected int mMaxInputBandwidth; // 9:2 Max Input Bandwidth + protected int mMaxOutputBandwidth; // 11:2 Max Output Bandwidth + + public int parseRawDescriptors(ByteStream stream) { + mLength = stream.getUnsignedByte(); + mDescriptorType = stream.getUnsignedByte(); + mDescriptorSubtype = stream.getUnsignedByte(); + mGroupBlockId = stream.getUnsignedByte(); + mGroupTerminalBlockType = stream.getUnsignedByte(); + mGroupTerminal = stream.getUnsignedByte(); + mNumGroupTerminals = stream.getUnsignedByte(); + mBlockItem = stream.getUnsignedByte(); + mMidiProtocol = stream.getUnsignedByte(); + mMaxInputBandwidth = stream.unpackUsbShort(); + mMaxOutputBandwidth = stream.unpackUsbShort(); + return mLength; + } + } + + private ArrayList<GroupTerminalBlock> mGroupTerminalBlocks = + new ArrayList<GroupTerminalBlock>(); + + public UsbMidiBlockParser() { + } + + /** + * Parses a raw ByteStream into a block terminal descriptor. + * The header is parsed before each block is parsed. + * @param stream ByteStream to parse + * @return The total length that has been parsed. + */ + public int parseRawDescriptors(ByteStream stream) { + mHeaderLength = stream.getUnsignedByte(); + mHeaderDescriptorType = stream.getUnsignedByte(); + mHeaderDescriptorSubtype = stream.getUnsignedByte(); + mTotalLength = stream.unpackUsbShort(); + + while (stream.available() >= MIDI_BLOCK_SIZE) { + GroupTerminalBlock block = new GroupTerminalBlock(); + block.parseRawDescriptors(stream); + mGroupTerminalBlocks.add(block); + } + + return mTotalLength; + } + + /** + * Calculates the MIDI type through querying the device twice, once for the size + * of the block descriptor and once for the block descriptor. This descriptor is + * then parsed to return the MIDI type. + * See the MIDI 2.0 USB doc for more info. + * @param connection UsbDeviceConnection to send the request + * @param interfaceNumber The interface number to query + * @param alternateInterfaceNumber The alternate interface of the interface + * @return The MIDI type as an int. + */ + public int calculateMidiType(UsbDeviceConnection connection, int interfaceNumber, + int alternateInterfaceNumber) { + byte[] byteArray = new byte[MIDI_BLOCK_HEADER_SIZE]; + try { + // This first request is simply to get the full size of the descriptor. + // This info is stored in the last two bytes of the header. + int rdo = connection.controlTransfer( + UsbConstants.USB_DIR_IN | UsbConstants.USB_TYPE_STANDARD + | UsbConstants.USB_CLASS_AUDIO, + REQ_GET_DESCRIPTOR, + (CS_GR_TRM_BLOCK << 8) + alternateInterfaceNumber, + interfaceNumber, + byteArray, + MIDI_BLOCK_HEADER_SIZE, + REQ_TIMEOUT_MS); + if (rdo > 0) { + if (byteArray[1] != CS_GR_TRM_BLOCK) { + Log.e(TAG, "Incorrect descriptor type: " + byteArray[1]); + return DEFAULT_MIDI_TYPE; + } + if (byteArray[2] != GR_TRM_BLOCK_HEADER) { + Log.e(TAG, "Incorrect descriptor subtype: " + byteArray[2]); + return DEFAULT_MIDI_TYPE; + } + int newSize = (((int) byteArray[3]) & (0xff)) + + ((((int) byteArray[4]) & (0xff)) << 8); + if (newSize <= 0) { + Log.e(TAG, "Parsed a non-positive block terminal size: " + newSize); + return DEFAULT_MIDI_TYPE; + } + byteArray = new byte[newSize]; + rdo = connection.controlTransfer( + UsbConstants.USB_DIR_IN | UsbConstants.USB_TYPE_STANDARD + | UsbConstants.USB_CLASS_AUDIO, + REQ_GET_DESCRIPTOR, + (CS_GR_TRM_BLOCK << 8) + alternateInterfaceNumber, + interfaceNumber, + byteArray, + newSize, + REQ_TIMEOUT_MS); + if (rdo > 0) { + ByteStream stream = new ByteStream(byteArray); + parseRawDescriptors(stream); + if (mGroupTerminalBlocks.isEmpty()) { + Log.e(TAG, "Group Terminal Blocks failed parsing: " + DEFAULT_MIDI_TYPE); + return DEFAULT_MIDI_TYPE; + } else { + Log.d(TAG, "MIDI protocol: " + mGroupTerminalBlocks.get(0).mMidiProtocol); + return mGroupTerminalBlocks.get(0).mMidiProtocol; + } + } else { + Log.e(TAG, "second transfer failed: " + rdo); + } + } else { + Log.e(TAG, "first transfer failed: " + rdo); + } + } catch (Exception e) { + Log.e(TAG, "Can not communicate with USB device", e); + } + return DEFAULT_MIDI_TYPE; + } +} |