blob: 8b4122055698ec19aadb760f973ad4be2173ca10 [file] [log] [blame]
// 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.midi;
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.annotations.CalledByNative;
import org.chromium.base.annotations.JNINamespace;
import java.nio.ByteBuffer;
import java.util.Arrays;
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::midi")
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;
/**
* The request type to request a USB descriptor.
*/
static final int REQUEST_GET_DESCRIPTOR = 0x06;
/**
* The STRING descriptor type.
*/
static final int STRING_DESCRIPTOR_TYPE = 0x03;
/**
* 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();
}
/**
* Returns the string descriptor bytes for the given index
* @param index index of the descriptor
* @return the string descriptor bytes for the given index.
*/
@CalledByNative
byte[] getStringDescriptor(int index) {
if (mConnection == null) {
return new byte[0];
}
byte[] buffer = new byte[255];
int type = UsbConstants.USB_DIR_IN | UsbConstants.USB_TYPE_STANDARD;
int request = REQUEST_GET_DESCRIPTOR;
int value = (STRING_DESCRIPTOR_TYPE << 8) | index;
int read = mConnection.controlTransfer(type, request, value, 0, buffer, buffer.length, 0);
if (read < 0) {
return new byte[0];
}
return Arrays.copyOf(buffer, read);
}
/**
* 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);
}