Index: media/base/android/java/src/org/chromium/media/UsbMidiDeviceAndroid.java |
diff --git a/media/base/android/java/src/org/chromium/media/UsbMidiDeviceAndroid.java b/media/base/android/java/src/org/chromium/media/UsbMidiDeviceAndroid.java |
new file mode 100644 |
index 0000000000000000000000000000000000000000..2f80b961da1b3daa56c7165243a1e55792b060e5 |
--- /dev/null |
+++ b/media/base/android/java/src/org/chromium/media/UsbMidiDeviceAndroid.java |
@@ -0,0 +1,295 @@ |
+// Copyright 2014 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+package org.chromium.media; |
+ |
+import android.annotation.TargetApi; |
+import android.hardware.usb.UsbConstants; |
+import android.hardware.usb.UsbDevice; |
+import android.hardware.usb.UsbDeviceConnection; |
+import android.hardware.usb.UsbEndpoint; |
+import android.hardware.usb.UsbInterface; |
+import android.hardware.usb.UsbManager; |
+import android.hardware.usb.UsbRequest; |
+import android.os.Build; |
+import android.os.Handler; |
+import android.util.SparseArray; |
+ |
+import org.chromium.base.CalledByNative; |
+import org.chromium.base.JNINamespace; |
+ |
+import java.nio.ByteBuffer; |
+import java.util.HashMap; |
+import java.util.Map; |
+ |
+/** |
+ * Owned by its native counterpart declared in usb_midi_device_android.h. |
+ * Refer to that class for general comments. |
+ */ |
+@JNINamespace("media") |
+class UsbMidiDeviceAndroid { |
+ /** |
+ * A connection handle for this device. |
+ */ |
+ private final UsbDeviceConnection mConnection; |
+ |
+ /** |
+ * A map from endpoint number to UsbEndpoint. |
+ */ |
+ private final SparseArray<UsbEndpoint> mEndpointMap; |
+ |
+ /** |
+ * A map from UsbEndpoint to UsbRequest associated to it. |
+ */ |
+ private final Map<UsbEndpoint, UsbRequest> mRequestMap; |
+ |
+ /** |
+ * The handler used for posting events on the main thread. |
+ */ |
+ private final Handler mHandler; |
+ |
+ /** |
+ * True if this device is closed. |
+ */ |
+ private boolean mIsClosed; |
+ |
+ /** |
+ * True if there is a thread processing input data. |
+ */ |
+ private boolean mHasInputThread; |
+ |
+ /** |
+ * The identifier of this device. |
+ */ |
+ private long mNativePointer; |
+ |
+ /** |
+ * The underlying USB device. |
+ */ |
+ private UsbDevice mUsbDevice; |
+ |
+ /** |
+ * Audio interface subclass code for MIDI. |
+ */ |
+ static final int MIDI_SUBCLASS = 3; |
+ |
+ /** |
+ * Constructs a UsbMidiDeviceAndroid. |
+ * @param manager |
+ * @param device The USB device which this object is assocated with. |
+ */ |
+ UsbMidiDeviceAndroid(UsbManager manager, UsbDevice device) { |
+ mConnection = manager.openDevice(device); |
+ mEndpointMap = new SparseArray<UsbEndpoint>(); |
+ mRequestMap = new HashMap<UsbEndpoint, UsbRequest>(); |
+ mHandler = new Handler(); |
+ mUsbDevice = device; |
+ mIsClosed = false; |
+ mHasInputThread = false; |
+ mNativePointer = 0; |
+ |
+ for (int i = 0; i < device.getInterfaceCount(); ++i) { |
+ UsbInterface iface = device.getInterface(i); |
+ if (iface.getInterfaceClass() != UsbConstants.USB_CLASS_AUDIO |
+ || iface.getInterfaceSubclass() != MIDI_SUBCLASS) { |
+ continue; |
+ } |
+ mConnection.claimInterface(iface, true); |
+ for (int j = 0; j < iface.getEndpointCount(); ++j) { |
+ UsbEndpoint endpoint = iface.getEndpoint(j); |
+ if (endpoint.getDirection() == UsbConstants.USB_DIR_OUT) { |
+ mEndpointMap.put(endpoint.getEndpointNumber(), endpoint); |
+ } |
+ } |
+ } |
+ // Start listening for input endpoints. |
+ // This function will create and run a thread if there is USB-MIDI endpoints in the |
+ // device. Note that because UsbMidiDevice is shared among all tabs and the thread |
+ // will be terminated when the device is disconnected, at most one thread can be created |
+ // for each connected USB-MIDI device. |
+ startListen(device); |
+ } |
+ |
+ /** |
+ * Starts listening for input endpoints. |
+ */ |
+ private void startListen(final UsbDevice device) { |
+ final Map<UsbEndpoint, ByteBuffer> bufferForEndpoints = |
+ new HashMap<UsbEndpoint, ByteBuffer>(); |
+ |
+ for (int i = 0; i < device.getInterfaceCount(); ++i) { |
+ UsbInterface iface = device.getInterface(i); |
+ if (iface.getInterfaceClass() != UsbConstants.USB_CLASS_AUDIO |
+ || iface.getInterfaceSubclass() != MIDI_SUBCLASS) { |
+ continue; |
+ } |
+ for (int j = 0; j < iface.getEndpointCount(); ++j) { |
+ UsbEndpoint endpoint = iface.getEndpoint(j); |
+ if (endpoint.getDirection() == UsbConstants.USB_DIR_IN) { |
+ ByteBuffer buffer = ByteBuffer.allocate(endpoint.getMaxPacketSize()); |
+ UsbRequest request = new UsbRequest(); |
+ request.initialize(mConnection, endpoint); |
+ request.queue(buffer, buffer.remaining()); |
+ bufferForEndpoints.put(endpoint, buffer); |
+ } |
+ } |
+ } |
+ if (bufferForEndpoints.isEmpty()) { |
+ return; |
+ } |
+ mHasInputThread = true; |
+ // bufferForEndpoints must not be accessed hereafter on this thread. |
+ new Thread() { |
+ @Override |
+ public void run() { |
+ while (true) { |
+ UsbRequest request = mConnection.requestWait(); |
+ if (request == null) { |
+ // When the device is closed requestWait will fail. |
+ break; |
+ } |
+ UsbEndpoint endpoint = request.getEndpoint(); |
+ if (endpoint.getDirection() != UsbConstants.USB_DIR_IN) { |
+ continue; |
+ } |
+ ByteBuffer buffer = bufferForEndpoints.get(endpoint); |
+ int length = getInputDataLength(buffer); |
+ if (length > 0) { |
+ buffer.rewind(); |
+ final byte[] bs = new byte[length]; |
+ buffer.get(bs, 0, length); |
+ postOnDataEvent(endpoint.getEndpointNumber(), bs); |
+ } |
+ buffer.rewind(); |
+ request.queue(buffer, buffer.capacity()); |
+ } |
+ } |
+ }.start(); |
+ } |
+ |
+ /** |
+ * Posts a data input event to the main thread. |
+ */ |
+ private void postOnDataEvent(final int endpointNumber, final byte[] bs) { |
+ mHandler.post(new Runnable() { |
+ @Override |
+ public void run() { |
+ if (mIsClosed) { |
+ return; |
+ } |
+ nativeOnData(mNativePointer, endpointNumber, bs); |
+ } |
+ }); |
+ } |
+ |
+ UsbDevice getUsbDevice() { |
+ return mUsbDevice; |
+ } |
+ |
+ boolean isClosed() { |
+ return mIsClosed; |
+ } |
+ |
+ /** |
+ * Register the own native pointer. |
+ */ |
+ @CalledByNative |
+ void registerSelf(long nativePointer) { |
+ mNativePointer = nativePointer; |
+ } |
+ |
+ /** |
+ * Sends a USB-MIDI data to the device. |
+ * @param endpointNumber The endpoint number of the destination endpoint. |
+ * @param bs The data to be sent. |
+ */ |
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) |
+ @CalledByNative |
+ void send(int endpointNumber, byte[] bs) { |
+ if (mIsClosed) { |
+ return; |
+ } |
+ UsbEndpoint endpoint = mEndpointMap.get(endpointNumber); |
+ if (endpoint == null) { |
+ return; |
+ } |
+ if (shouldUseBulkTransfer()) { |
+ // We use bulkTransfer instead of UsbRequest.queue because queueing |
+ // a UsbRequest is currently not thread safe. |
+ // Note that this is not exactly correct because we don't care |
+ // about the transfer attribute (bmAttribute) of the endpoint. |
+ // See also: |
+ // http://stackoverflow.com/questions/9644415/ |
+ // https://code.google.com/p/android/issues/detail?id=59467 |
+ // |
+ // TODO(yhirano): Delete this block once the problem is fixed. |
+ final int timeout = 100; |
+ mConnection.bulkTransfer(endpoint, bs, bs.length, timeout); |
+ } else { |
+ UsbRequest request = mRequestMap.get(endpoint); |
+ if (request == null) { |
+ request = new UsbRequest(); |
+ request.initialize(mConnection, endpoint); |
+ mRequestMap.put(endpoint, request); |
+ } |
+ request.queue(ByteBuffer.wrap(bs), bs.length); |
+ } |
+ } |
+ |
+ /** |
+ * Returns true if |bulkTransfer| should be used in |send|. |
+ * See comments in |send|. |
+ */ |
+ private boolean shouldUseBulkTransfer() { |
+ return mHasInputThread; |
+ } |
+ |
+ /** |
+ * Returns the descriptors bytes of this device. |
+ * @return The descriptors bytes of this device. |
+ */ |
+ @CalledByNative |
+ byte[] getDescriptors() { |
+ if (mConnection == null) { |
+ return new byte[0]; |
+ } |
+ return mConnection.getRawDescriptors(); |
+ } |
+ |
+ /** |
+ * Closes the device connection. |
+ */ |
+ @CalledByNative |
+ void close() { |
+ mEndpointMap.clear(); |
+ for (UsbRequest request : mRequestMap.values()) { |
+ request.close(); |
+ } |
+ mRequestMap.clear(); |
+ mConnection.close(); |
+ mNativePointer = 0; |
+ mIsClosed = true; |
+ } |
+ |
+ /** |
+ * Returns the length of a USB-MIDI input. |
+ * Since the Android API doesn't provide us the length, |
+ * we calculate it manually. |
+ */ |
+ private static int getInputDataLength(ByteBuffer buffer) { |
+ int position = buffer.position(); |
+ // We assume that the data length is always divisable by 4. |
+ for (int i = 0; i < position; i += 4) { |
+ // Since Code Index Number 0 is reserved, it is not a valid USB-MIDI data. |
+ if (buffer.get(i) == 0) { |
+ return i; |
+ } |
+ } |
+ return position; |
+ } |
+ |
+ private static native void nativeOnData(long nativeUsbMidiDeviceAndroid, |
+ int endpointNumber, |
+ byte[] data); |
+} |