| OLD | NEW |
| (Empty) |
| 1 // Copyright 2014 The Chromium Authors. All rights reserved. | |
| 2 // Use of this source code is governed by a BSD-style license that can be | |
| 3 // found in the LICENSE file. | |
| 4 | |
| 5 package org.chromium.media.midi; | |
| 6 | |
| 7 import android.annotation.TargetApi; | |
| 8 import android.hardware.usb.UsbConstants; | |
| 9 import android.hardware.usb.UsbDevice; | |
| 10 import android.hardware.usb.UsbDeviceConnection; | |
| 11 import android.hardware.usb.UsbEndpoint; | |
| 12 import android.hardware.usb.UsbInterface; | |
| 13 import android.hardware.usb.UsbManager; | |
| 14 import android.hardware.usb.UsbRequest; | |
| 15 import android.os.Build; | |
| 16 import android.os.Handler; | |
| 17 import android.util.SparseArray; | |
| 18 | |
| 19 import org.chromium.base.annotations.CalledByNative; | |
| 20 import org.chromium.base.annotations.JNINamespace; | |
| 21 | |
| 22 import java.nio.ByteBuffer; | |
| 23 import java.util.Arrays; | |
| 24 import java.util.HashMap; | |
| 25 import java.util.Map; | |
| 26 | |
| 27 /** | |
| 28 * Owned by its native counterpart declared in usb_midi_device_android.h. | |
| 29 * Refer to that class for general comments. | |
| 30 */ | |
| 31 @JNINamespace("media::midi") | |
| 32 class UsbMidiDeviceAndroid { | |
| 33 /** | |
| 34 * A connection handle for this device. | |
| 35 */ | |
| 36 private final UsbDeviceConnection mConnection; | |
| 37 | |
| 38 /** | |
| 39 * A map from endpoint number to UsbEndpoint. | |
| 40 */ | |
| 41 private final SparseArray<UsbEndpoint> mEndpointMap; | |
| 42 | |
| 43 /** | |
| 44 * A map from UsbEndpoint to UsbRequest associated to it. | |
| 45 */ | |
| 46 private final Map<UsbEndpoint, UsbRequest> mRequestMap; | |
| 47 | |
| 48 /** | |
| 49 * The handler used for posting events on the main thread. | |
| 50 */ | |
| 51 private final Handler mHandler; | |
| 52 | |
| 53 /** | |
| 54 * True if this device is closed. | |
| 55 */ | |
| 56 private boolean mIsClosed; | |
| 57 | |
| 58 /** | |
| 59 * True if there is a thread processing input data. | |
| 60 */ | |
| 61 private boolean mHasInputThread; | |
| 62 | |
| 63 /** | |
| 64 * The identifier of this device. | |
| 65 */ | |
| 66 private long mNativePointer; | |
| 67 | |
| 68 /** | |
| 69 * The underlying USB device. | |
| 70 */ | |
| 71 private UsbDevice mUsbDevice; | |
| 72 | |
| 73 /** | |
| 74 * Audio interface subclass code for MIDI. | |
| 75 */ | |
| 76 static final int MIDI_SUBCLASS = 3; | |
| 77 | |
| 78 /** | |
| 79 * The request type to request a USB descriptor. | |
| 80 */ | |
| 81 static final int REQUEST_GET_DESCRIPTOR = 0x06; | |
| 82 | |
| 83 /** | |
| 84 * The STRING descriptor type. | |
| 85 */ | |
| 86 static final int STRING_DESCRIPTOR_TYPE = 0x03; | |
| 87 | |
| 88 /** | |
| 89 * Constructs a UsbMidiDeviceAndroid. | |
| 90 * @param manager | |
| 91 * @param device The USB device which this object is assocated with. | |
| 92 */ | |
| 93 UsbMidiDeviceAndroid(UsbManager manager, UsbDevice device) { | |
| 94 mConnection = manager.openDevice(device); | |
| 95 mEndpointMap = new SparseArray<UsbEndpoint>(); | |
| 96 mRequestMap = new HashMap<UsbEndpoint, UsbRequest>(); | |
| 97 mHandler = new Handler(); | |
| 98 mUsbDevice = device; | |
| 99 mIsClosed = false; | |
| 100 mHasInputThread = false; | |
| 101 mNativePointer = 0; | |
| 102 | |
| 103 for (int i = 0; i < device.getInterfaceCount(); ++i) { | |
| 104 UsbInterface iface = device.getInterface(i); | |
| 105 if (iface.getInterfaceClass() != UsbConstants.USB_CLASS_AUDIO | |
| 106 || iface.getInterfaceSubclass() != MIDI_SUBCLASS) { | |
| 107 continue; | |
| 108 } | |
| 109 mConnection.claimInterface(iface, true); | |
| 110 for (int j = 0; j < iface.getEndpointCount(); ++j) { | |
| 111 UsbEndpoint endpoint = iface.getEndpoint(j); | |
| 112 if (endpoint.getDirection() == UsbConstants.USB_DIR_OUT) { | |
| 113 mEndpointMap.put(endpoint.getEndpointNumber(), endpoint); | |
| 114 } | |
| 115 } | |
| 116 } | |
| 117 // Start listening for input endpoints. | |
| 118 // This function will create and run a thread if there is USB-MIDI endpo
ints in the | |
| 119 // device. Note that because UsbMidiDevice is shared among all tabs and
the thread | |
| 120 // will be terminated when the device is disconnected, at most one threa
d can be created | |
| 121 // for each connected USB-MIDI device. | |
| 122 startListen(device); | |
| 123 } | |
| 124 | |
| 125 /** | |
| 126 * Starts listening for input endpoints. | |
| 127 */ | |
| 128 private void startListen(final UsbDevice device) { | |
| 129 final Map<UsbEndpoint, ByteBuffer> bufferForEndpoints = | |
| 130 new HashMap<UsbEndpoint, ByteBuffer>(); | |
| 131 | |
| 132 for (int i = 0; i < device.getInterfaceCount(); ++i) { | |
| 133 UsbInterface iface = device.getInterface(i); | |
| 134 if (iface.getInterfaceClass() != UsbConstants.USB_CLASS_AUDIO | |
| 135 || iface.getInterfaceSubclass() != MIDI_SUBCLASS) { | |
| 136 continue; | |
| 137 } | |
| 138 for (int j = 0; j < iface.getEndpointCount(); ++j) { | |
| 139 UsbEndpoint endpoint = iface.getEndpoint(j); | |
| 140 if (endpoint.getDirection() == UsbConstants.USB_DIR_IN) { | |
| 141 ByteBuffer buffer = ByteBuffer.allocate(endpoint.getMaxPacke
tSize()); | |
| 142 UsbRequest request = new UsbRequest(); | |
| 143 request.initialize(mConnection, endpoint); | |
| 144 request.queue(buffer, buffer.remaining()); | |
| 145 bufferForEndpoints.put(endpoint, buffer); | |
| 146 } | |
| 147 } | |
| 148 } | |
| 149 if (bufferForEndpoints.isEmpty()) { | |
| 150 return; | |
| 151 } | |
| 152 mHasInputThread = true; | |
| 153 // bufferForEndpoints must not be accessed hereafter on this thread. | |
| 154 new Thread() { | |
| 155 @Override | |
| 156 public void run() { | |
| 157 while (true) { | |
| 158 UsbRequest request = mConnection.requestWait(); | |
| 159 if (request == null) { | |
| 160 // When the device is closed requestWait will fail. | |
| 161 break; | |
| 162 } | |
| 163 UsbEndpoint endpoint = request.getEndpoint(); | |
| 164 if (endpoint.getDirection() != UsbConstants.USB_DIR_IN) { | |
| 165 continue; | |
| 166 } | |
| 167 ByteBuffer buffer = bufferForEndpoints.get(endpoint); | |
| 168 int length = getInputDataLength(buffer); | |
| 169 if (length > 0) { | |
| 170 buffer.rewind(); | |
| 171 final byte[] bs = new byte[length]; | |
| 172 buffer.get(bs, 0, length); | |
| 173 postOnDataEvent(endpoint.getEndpointNumber(), bs); | |
| 174 } | |
| 175 buffer.rewind(); | |
| 176 request.queue(buffer, buffer.capacity()); | |
| 177 } | |
| 178 } | |
| 179 }.start(); | |
| 180 } | |
| 181 | |
| 182 /** | |
| 183 * Posts a data input event to the main thread. | |
| 184 */ | |
| 185 private void postOnDataEvent(final int endpointNumber, final byte[] bs) { | |
| 186 mHandler.post(new Runnable() { | |
| 187 @Override | |
| 188 public void run() { | |
| 189 if (mIsClosed) { | |
| 190 return; | |
| 191 } | |
| 192 nativeOnData(mNativePointer, endpointNumber, bs); | |
| 193 } | |
| 194 }); | |
| 195 } | |
| 196 | |
| 197 UsbDevice getUsbDevice() { | |
| 198 return mUsbDevice; | |
| 199 } | |
| 200 | |
| 201 boolean isClosed() { | |
| 202 return mIsClosed; | |
| 203 } | |
| 204 | |
| 205 /** | |
| 206 * Register the own native pointer. | |
| 207 */ | |
| 208 @CalledByNative | |
| 209 void registerSelf(long nativePointer) { | |
| 210 mNativePointer = nativePointer; | |
| 211 } | |
| 212 | |
| 213 /** | |
| 214 * Sends a USB-MIDI data to the device. | |
| 215 * @param endpointNumber The endpoint number of the destination endpoint. | |
| 216 * @param bs The data to be sent. | |
| 217 */ | |
| 218 @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) | |
| 219 @CalledByNative | |
| 220 void send(int endpointNumber, byte[] bs) { | |
| 221 if (mIsClosed) { | |
| 222 return; | |
| 223 } | |
| 224 UsbEndpoint endpoint = mEndpointMap.get(endpointNumber); | |
| 225 if (endpoint == null) { | |
| 226 return; | |
| 227 } | |
| 228 if (shouldUseBulkTransfer()) { | |
| 229 // We use bulkTransfer instead of UsbRequest.queue because queueing | |
| 230 // a UsbRequest is currently not thread safe. | |
| 231 // Note that this is not exactly correct because we don't care | |
| 232 // about the transfer attribute (bmAttribute) of the endpoint. | |
| 233 // See also: | |
| 234 // http://stackoverflow.com/questions/9644415/ | |
| 235 // https://code.google.com/p/android/issues/detail?id=59467 | |
| 236 // | |
| 237 // TODO(yhirano): Delete this block once the problem is fixed. | |
| 238 final int timeout = 100; | |
| 239 mConnection.bulkTransfer(endpoint, bs, bs.length, timeout); | |
| 240 } else { | |
| 241 UsbRequest request = mRequestMap.get(endpoint); | |
| 242 if (request == null) { | |
| 243 request = new UsbRequest(); | |
| 244 request.initialize(mConnection, endpoint); | |
| 245 mRequestMap.put(endpoint, request); | |
| 246 } | |
| 247 request.queue(ByteBuffer.wrap(bs), bs.length); | |
| 248 } | |
| 249 } | |
| 250 | |
| 251 /** | |
| 252 * Returns true if |bulkTransfer| should be used in |send|. | |
| 253 * See comments in |send|. | |
| 254 */ | |
| 255 private boolean shouldUseBulkTransfer() { | |
| 256 return mHasInputThread; | |
| 257 } | |
| 258 | |
| 259 /** | |
| 260 * Returns the descriptors bytes of this device. | |
| 261 * @return The descriptors bytes of this device. | |
| 262 */ | |
| 263 @CalledByNative | |
| 264 byte[] getDescriptors() { | |
| 265 if (mConnection == null) { | |
| 266 return new byte[0]; | |
| 267 } | |
| 268 return mConnection.getRawDescriptors(); | |
| 269 } | |
| 270 | |
| 271 /** | |
| 272 * Returns the string descriptor bytes for the given index | |
| 273 * @param index index of the descriptor | |
| 274 * @return the string descriptor bytes for the given index. | |
| 275 */ | |
| 276 @CalledByNative | |
| 277 byte[] getStringDescriptor(int index) { | |
| 278 if (mConnection == null) { | |
| 279 return new byte[0]; | |
| 280 } | |
| 281 byte[] buffer = new byte[255]; | |
| 282 int type = UsbConstants.USB_DIR_IN | UsbConstants.USB_TYPE_STANDARD; | |
| 283 int request = REQUEST_GET_DESCRIPTOR; | |
| 284 int value = (STRING_DESCRIPTOR_TYPE << 8) | index; | |
| 285 int read = mConnection.controlTransfer(type, request, value, 0, buffer,
buffer.length, 0); | |
| 286 if (read < 0) { | |
| 287 return new byte[0]; | |
| 288 } | |
| 289 return Arrays.copyOf(buffer, read); | |
| 290 } | |
| 291 | |
| 292 /** | |
| 293 * Closes the device connection. | |
| 294 */ | |
| 295 @CalledByNative | |
| 296 void close() { | |
| 297 mEndpointMap.clear(); | |
| 298 for (UsbRequest request : mRequestMap.values()) { | |
| 299 request.close(); | |
| 300 } | |
| 301 mRequestMap.clear(); | |
| 302 mConnection.close(); | |
| 303 mNativePointer = 0; | |
| 304 mIsClosed = true; | |
| 305 } | |
| 306 | |
| 307 /** | |
| 308 * Returns the length of a USB-MIDI input. | |
| 309 * Since the Android API doesn't provide us the length, | |
| 310 * we calculate it manually. | |
| 311 */ | |
| 312 private static int getInputDataLength(ByteBuffer buffer) { | |
| 313 int position = buffer.position(); | |
| 314 // We assume that the data length is always divisable by 4. | |
| 315 for (int i = 0; i < position; i += 4) { | |
| 316 // Since Code Index Number 0 is reserved, it is not a valid USB-MIDI
data. | |
| 317 if (buffer.get(i) == 0) { | |
| 318 return i; | |
| 319 } | |
| 320 } | |
| 321 return position; | |
| 322 } | |
| 323 | |
| 324 private static native void nativeOnData( | |
| 325 long nativeUsbMidiDeviceAndroid, int endpointNumber, byte[] data); | |
| 326 } | |
| OLD | NEW |