| /* |
| * Copyright (C) 2016 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.cts.verifier.usb.device; |
| |
| import static com.android.cts.verifier.usb.Util.runAndAssertException; |
| |
| import static org.junit.Assert.assertArrayEquals; |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.assertNotNull; |
| import static org.junit.Assert.assertNull; |
| import static org.junit.Assert.assertSame; |
| import static org.junit.Assert.assertTrue; |
| import static org.junit.Assume.assumeNotNull; |
| import static org.junit.Assume.assumeTrue; |
| |
| import android.app.PendingIntent; |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.hardware.usb.UsbConfiguration; |
| 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.Bundle; |
| import android.support.annotation.NonNull; |
| import android.support.annotation.Nullable; |
| import android.util.ArraySet; |
| import android.util.Log; |
| import android.util.Pair; |
| import android.view.View; |
| import android.widget.ProgressBar; |
| import android.widget.TextView; |
| import android.widget.Toast; |
| |
| import com.android.cts.verifier.PassFailButtons; |
| import com.android.cts.verifier.R; |
| |
| import org.junit.AssumptionViolatedException; |
| |
| import java.nio.ByteBuffer; |
| import java.nio.CharBuffer; |
| import java.nio.charset.Charset; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.LinkedList; |
| import java.util.Map; |
| import java.util.NoSuchElementException; |
| import java.util.Random; |
| import java.util.Set; |
| import java.util.concurrent.TimeoutException; |
| import java.util.concurrent.atomic.AtomicInteger; |
| |
| public class UsbDeviceTestActivity extends PassFailButtons.Activity { |
| private static final String ACTION_USB_PERMISSION = |
| "com.android.cts.verifier.usb.device.USB_PERMISSION"; |
| private static final String LOG_TAG = UsbDeviceTestActivity.class.getSimpleName(); |
| private static final int TIMEOUT_MILLIS = 5000; |
| private static final int MAX_BUFFER_SIZE = 16384; |
| private static final int OVERSIZED_BUFFER_SIZE = MAX_BUFFER_SIZE + 100; |
| |
| private UsbManager mUsbManager; |
| private BroadcastReceiver mUsbDeviceConnectionReceiver; |
| private Thread mTestThread; |
| private TextView mStatus; |
| private ProgressBar mProgress; |
| |
| /** |
| * Some N and older accessories do not send a zero sized package after a request that is a |
| * multiple of the maximum package size. |
| */ |
| private boolean mDoesCompanionZeroTerminate; |
| |
| private static long now() { |
| return System.nanoTime() / 1000000; |
| } |
| |
| /** |
| * Check if we should expect a zero sized transfer after a certain sized transfer |
| * |
| * @param transferSize The size of the previous transfer |
| * |
| * @return {@code true} if a zero sized transfer is expected |
| */ |
| private boolean isZeroTransferExpected(int transferSize) { |
| if (mDoesCompanionZeroTerminate) { |
| if (transferSize % 1024 == 0) { |
| if (transferSize % 8 != 0) { |
| throw new IllegalArgumentException("As the transfer speed is unknown the code " |
| + "has to work for all speeds"); |
| } |
| |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| @Override |
| protected void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| |
| setContentView(R.layout.usb_main); |
| setInfoResources(R.string.usb_device_test, R.string.usb_device_test_info, -1); |
| |
| mStatus = (TextView) findViewById(R.id.status); |
| mProgress = (ProgressBar) findViewById(R.id.progress_bar); |
| |
| mUsbManager = getSystemService(UsbManager.class); |
| |
| getPassButton().setEnabled(false); |
| |
| IntentFilter filter = new IntentFilter(); |
| filter.addAction(ACTION_USB_PERMISSION); |
| filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED); |
| |
| mStatus.setText(R.string.usb_device_test_step1); |
| |
| mUsbDeviceConnectionReceiver = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| synchronized (UsbDeviceTestActivity.this) { |
| UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); |
| |
| switch (intent.getAction()) { |
| case UsbManager.ACTION_USB_DEVICE_ATTACHED: |
| if (!AoapInterface.isDeviceInAoapMode(device)) { |
| mStatus.setText(R.string.usb_device_test_step2); |
| } |
| |
| mUsbManager.requestPermission(device, |
| PendingIntent.getBroadcast(UsbDeviceTestActivity.this, 0, |
| new Intent(ACTION_USB_PERMISSION), 0)); |
| break; |
| case ACTION_USB_PERMISSION: |
| boolean granted = intent.getBooleanExtra( |
| UsbManager.EXTRA_PERMISSION_GRANTED, false); |
| |
| if (granted) { |
| if (!AoapInterface.isDeviceInAoapMode(device)) { |
| mStatus.setText(R.string.usb_device_test_step3); |
| |
| UsbDeviceConnection connection = mUsbManager.openDevice(device); |
| try { |
| makeThisDeviceAnAccessory(connection); |
| } finally { |
| connection.close(); |
| } |
| } else { |
| mStatus.setText(R.string.usb_device_test_step4); |
| mProgress.setIndeterminate(true); |
| mProgress.setVisibility(View.VISIBLE); |
| |
| unregisterReceiver(mUsbDeviceConnectionReceiver); |
| mUsbDeviceConnectionReceiver = null; |
| |
| // Do not run test on main thread |
| mTestThread = new Thread() { |
| @Override |
| public void run() { |
| runTests(device); |
| } |
| }; |
| |
| mTestThread.start(); |
| } |
| } else { |
| fail("Permission to connect to " + device.getProductName() |
| + " not granted", null); |
| } |
| break; |
| } |
| } |
| } |
| }; |
| |
| registerReceiver(mUsbDeviceConnectionReceiver, filter); |
| } |
| |
| /** |
| * Indicate that the test failed. |
| */ |
| private void fail(@Nullable String s, @Nullable Throwable e) { |
| Log.e(LOG_TAG, s, e); |
| setTestResultAndFinish(false); |
| } |
| |
| /** |
| * Converts the device under test into an Android accessory. Accessories are USB hosts that are |
| * detected on the device side via {@link UsbManager#getAccessoryList()}. |
| * |
| * @param connection The connection to the USB device |
| */ |
| private void makeThisDeviceAnAccessory(@NonNull UsbDeviceConnection connection) { |
| AoapInterface.sendString(connection, AoapInterface.ACCESSORY_STRING_MANUFACTURER, |
| "Android"); |
| AoapInterface.sendString(connection, AoapInterface.ACCESSORY_STRING_MODEL, |
| "Android device"); |
| AoapInterface.sendString(connection, AoapInterface.ACCESSORY_STRING_DESCRIPTION, |
| "Android device running CTS verifier"); |
| AoapInterface.sendString(connection, AoapInterface.ACCESSORY_STRING_VERSION, "1"); |
| AoapInterface.sendString(connection, AoapInterface.ACCESSORY_STRING_URI, |
| "https://source.android.com/compatibility/cts/verifier.html"); |
| AoapInterface.sendString(connection, AoapInterface.ACCESSORY_STRING_SERIAL, "0"); |
| AoapInterface.sendAoapStart(connection); |
| } |
| |
| /** |
| * Switch to next test. |
| * |
| * @param connection Connection to the USB device |
| * @param in The in endpoint |
| * @param out The out endpoint |
| * @param nextTestName The name of the new test |
| */ |
| private void nextTest(@NonNull UsbDeviceConnection connection, @NonNull UsbEndpoint in, |
| @NonNull UsbEndpoint out, @NonNull CharSequence nextTestName) { |
| Log.v(LOG_TAG, "Finishing previous test"); |
| |
| // Make sure name length is not a multiple of 8 to avoid zero-termination issues |
| StringBuilder safeNextTestName = new StringBuilder(nextTestName); |
| if (nextTestName.length() % 8 == 0) { |
| safeNextTestName.append(' '); |
| } |
| |
| // Send name of next test |
| assertTrue(safeNextTestName.length() <= Byte.MAX_VALUE); |
| ByteBuffer nextTestNameBuffer = Charset.forName("UTF-8") |
| .encode(CharBuffer.wrap(safeNextTestName)); |
| byte[] sizeBuffer = { (byte) nextTestNameBuffer.limit() }; |
| int numSent = connection.bulkTransfer(out, sizeBuffer, 1, 0); |
| assertEquals(1, numSent); |
| |
| numSent = connection.bulkTransfer(out, nextTestNameBuffer.array(), |
| nextTestNameBuffer.limit(), 0); |
| assertEquals(nextTestNameBuffer.limit(), numSent); |
| |
| // Receive result of last test |
| byte[] lastTestResultBytes = new byte[1]; |
| int numReceived = connection.bulkTransfer(in, lastTestResultBytes, |
| lastTestResultBytes.length, TIMEOUT_MILLIS); |
| assertEquals(1, numReceived); |
| assertEquals(1, lastTestResultBytes[0]); |
| |
| // Send ready signal |
| sizeBuffer[0] = 42; |
| numSent = connection.bulkTransfer(out, sizeBuffer, 1, 0); |
| assertEquals(1, numSent); |
| |
| Log.i(LOG_TAG, "Running test \"" + safeNextTestName + "\""); |
| } |
| |
| /** |
| * Receive a transfer that has size zero using bulk-transfer. |
| * |
| * @param connection Connection to the USB device |
| * @param in The in endpoint |
| */ |
| private void receiveZeroSizedTransfer(@NonNull UsbDeviceConnection connection, |
| @NonNull UsbEndpoint in) { |
| byte[] buffer = new byte[1]; |
| int numReceived = connection.bulkTransfer(in, buffer, 1, TIMEOUT_MILLIS); |
| assertEquals(0, numReceived); |
| } |
| |
| /** |
| * Send some data and expect it to be echoed back. |
| * |
| * @param connection Connection to the USB device |
| * @param in The in endpoint |
| * @param out The out endpoint |
| * @param size The number of bytes to send |
| */ |
| private void echoBulkTransfer(@NonNull UsbDeviceConnection connection, |
| @NonNull UsbEndpoint in, @NonNull UsbEndpoint out, int size) { |
| byte[] sentBuffer = new byte[size]; |
| Random r = new Random(); |
| r.nextBytes(sentBuffer); |
| |
| int numSent = connection.bulkTransfer(out, sentBuffer, sentBuffer.length, 0); |
| assertEquals(size, numSent); |
| |
| byte[] receivedBuffer = new byte[size]; |
| int numReceived = connection.bulkTransfer(in, receivedBuffer, receivedBuffer.length, |
| TIMEOUT_MILLIS); |
| assertEquals(size, numReceived); |
| |
| assertArrayEquals(sentBuffer, receivedBuffer); |
| |
| if (isZeroTransferExpected(size)) { |
| receiveZeroSizedTransfer(connection, in); |
| } |
| } |
| |
| /** |
| * Send some data and expect it to be echoed back (but have an offset in the send buffer). |
| * |
| * @param connection Connection to the USB device |
| * @param in The in endpoint |
| * @param out The out endpoint |
| * @param size The number of bytes to send |
| */ |
| private void echoBulkTransferOffset(@NonNull UsbDeviceConnection connection, |
| @NonNull UsbEndpoint in, @NonNull UsbEndpoint out, int offset, int size) { |
| byte[] sentBuffer = new byte[offset + size]; |
| Random r = new Random(); |
| r.nextBytes(sentBuffer); |
| |
| int numSent = connection.bulkTransfer(out, sentBuffer, offset, size, 0); |
| assertEquals(size, numSent); |
| |
| byte[] receivedBuffer = new byte[offset + size]; |
| int numReceived = connection.bulkTransfer(in, receivedBuffer, offset, size, TIMEOUT_MILLIS); |
| assertEquals(size, numReceived); |
| |
| for (int i = 0; i < offset + size; i++) { |
| if (i < offset) { |
| assertEquals(0, receivedBuffer[i]); |
| } else { |
| assertEquals(sentBuffer[i], receivedBuffer[i]); |
| } |
| } |
| |
| if (isZeroTransferExpected(size)) { |
| receiveZeroSizedTransfer(connection, in); |
| } |
| } |
| |
| /** |
| * Send a transfer that is larger than MAX_BUFFER_SIZE. |
| * |
| * @param connection Connection to the USB device |
| * @param in The in endpoint |
| * @param out The out endpoint |
| */ |
| private void echoOversizedBulkTransfer(@NonNull UsbDeviceConnection connection, |
| @NonNull UsbEndpoint in, @NonNull UsbEndpoint out) { |
| int totalSize = OVERSIZED_BUFFER_SIZE; |
| byte[] sentBuffer = new byte[totalSize]; |
| Random r = new Random(); |
| r.nextBytes(sentBuffer); |
| |
| int numSent = connection.bulkTransfer(out, sentBuffer, sentBuffer.length, 0); |
| |
| // Buffer will only be partially transferred |
| assertEquals(MAX_BUFFER_SIZE, numSent); |
| |
| byte[] receivedBuffer = new byte[totalSize]; |
| int numReceived = connection.bulkTransfer(in, receivedBuffer, receivedBuffer.length, |
| TIMEOUT_MILLIS); |
| |
| // All beyond MAX_BUFFER_SIZE was not send, hence it will not be echoed back |
| assertEquals(MAX_BUFFER_SIZE, numReceived); |
| |
| for (int i = 0; i < totalSize; i++) { |
| if (i < MAX_BUFFER_SIZE) { |
| assertEquals(sentBuffer[i], receivedBuffer[i]); |
| } else { |
| assertEquals(0, receivedBuffer[i]); |
| } |
| } |
| |
| if (mDoesCompanionZeroTerminate) { |
| receiveZeroSizedTransfer(connection, in); |
| } |
| } |
| |
| /** |
| * Receive a transfer that is larger than MAX_BUFFER_SIZE |
| * |
| * @param connection Connection to the USB device |
| * @param in The in endpoint |
| */ |
| private void receiveOversizedBulkTransfer(@NonNull UsbDeviceConnection connection, |
| @NonNull UsbEndpoint in) { |
| // Buffer will be received as two transfers |
| byte[] receivedBuffer1 = new byte[OVERSIZED_BUFFER_SIZE]; |
| int numReceived = connection.bulkTransfer(in, receivedBuffer1, receivedBuffer1.length, |
| TIMEOUT_MILLIS); |
| assertEquals(MAX_BUFFER_SIZE, numReceived); |
| |
| byte[] receivedBuffer2 = new byte[OVERSIZED_BUFFER_SIZE - MAX_BUFFER_SIZE]; |
| numReceived = connection.bulkTransfer(in, receivedBuffer2, receivedBuffer2.length, |
| TIMEOUT_MILLIS); |
| assertEquals(OVERSIZED_BUFFER_SIZE - MAX_BUFFER_SIZE, numReceived); |
| |
| assertEquals(1, receivedBuffer1[0]); |
| assertEquals(2, receivedBuffer1[MAX_BUFFER_SIZE - 1]); |
| assertEquals(3, receivedBuffer2[0]); |
| assertEquals(4, receivedBuffer2[OVERSIZED_BUFFER_SIZE - MAX_BUFFER_SIZE - 1]); |
| } |
| |
| /** |
| * Receive data but supply an empty buffer. This causes the thread to block until any data is |
| * sent. The zero-sized receive-transfer just returns without data and the next transfer can |
| * actually read the data. |
| * |
| * @param connection Connection to the USB device |
| * @param in The in endpoint |
| * @param buffer The buffer to use |
| * @param offset The offset into the buffer |
| * @param length The lenght of data to receive |
| */ |
| private void receiveWithEmptyBuffer(@NonNull UsbDeviceConnection connection, |
| @NonNull UsbEndpoint in, @Nullable byte[] buffer, int offset, int length) { |
| long startTime = now(); |
| int numReceived; |
| if (offset == 0) { |
| numReceived = connection.bulkTransfer(in, buffer, length, 0); |
| } else { |
| numReceived = connection.bulkTransfer(in, buffer, offset, length, 0); |
| } |
| long endTime = now(); |
| assertEquals(-1, numReceived); |
| |
| // The transfer should block |
| assertTrue(endTime - startTime > 100); |
| |
| numReceived = connection.bulkTransfer(in, new byte[1], 1, 0); |
| assertEquals(1, numReceived); |
| } |
| |
| /** |
| * Tests {@link UsbDeviceConnection#controlTransfer}. |
| * |
| * <p>Note: We cannot send ctrl data to the device as it thinks it talks to an accessory, hence |
| * the testing is currently limited.</p> |
| * |
| * @param connection The connection to use for testing |
| * |
| * @throws Throwable |
| */ |
| private void ctrlTransferTests(@NonNull UsbDeviceConnection connection) throws Throwable { |
| runAndAssertException(() -> connection.controlTransfer(0, 0, 0, 0, null, 1, 0), |
| IllegalArgumentException.class); |
| |
| runAndAssertException(() -> connection.controlTransfer(0, 0, 0, 0, new byte[1], -1, 0), |
| IllegalArgumentException.class); |
| |
| runAndAssertException(() -> connection.controlTransfer(0, 0, 0, 0, new byte[1], 2, 0), |
| IllegalArgumentException.class); |
| |
| runAndAssertException(() -> connection.controlTransfer(0, 0, 0, 0, null, 0, 1, 0), |
| IllegalArgumentException.class); |
| |
| runAndAssertException(() -> connection.controlTransfer(0, 0, 0, 0, new byte[1], 0, -1, 0), |
| IllegalArgumentException.class); |
| |
| runAndAssertException(() -> connection.controlTransfer(0, 0, 0, 0, new byte[1], 1, 1, 0), |
| IllegalArgumentException.class); |
| } |
| |
| /** |
| * Search an {@link UsbInterface} for an {@link UsbEndpoint endpoint} of a certain direction. |
| * |
| * @param iface The interface to search |
| * @param direction The direction the endpoint is for. |
| * |
| * @return The first endpoint found or {@link null}. |
| */ |
| private @NonNull UsbEndpoint getEndpoint(@NonNull UsbInterface iface, int direction) { |
| for (int i = 0; i < iface.getEndpointCount(); i++) { |
| UsbEndpoint ep = iface.getEndpoint(i); |
| if (ep.getDirection() == direction) { |
| return ep; |
| } |
| } |
| |
| throw new IllegalStateException("Could not find " + direction + " endpoint in " |
| + iface.getName()); |
| } |
| |
| /** |
| * Receive a transfer that has size zero using deprecated usb-request methods. |
| * |
| * @param connection Connection to the USB device |
| * @param in The in endpoint |
| */ |
| private void receiveZeroSizeRequestLegacy(@NonNull UsbDeviceConnection connection, |
| @NonNull UsbEndpoint in) { |
| UsbRequest receiveZero = new UsbRequest(); |
| boolean isInited = receiveZero.initialize(connection, in); |
| assertTrue(isInited); |
| ByteBuffer zeroBuffer = ByteBuffer.allocate(1); |
| receiveZero.queue(zeroBuffer, 1); |
| |
| UsbRequest finished = connection.requestWait(); |
| assertEquals(receiveZero, finished); |
| assertEquals(0, zeroBuffer.position()); |
| } |
| |
| /** |
| * Send a USB request using the {@link UsbRequest#queue legacy path} and receive it back. |
| * |
| * @param connection The connection to use |
| * @param in The endpoint to receive requests from |
| * @param out The endpoint to send requests to |
| * @param size The size of the request to send |
| * @param originalSize The size of the original buffer |
| * @param sliceStart The start of the final buffer in the original buffer |
| * @param sliceEnd The end of the final buffer in the original buffer |
| * @param positionInSlice The position parameter in the final buffer |
| * @param limitInSlice The limited parameter in the final buffer |
| * @param useDirectBuffer If the buffer to be used should be a direct buffer |
| */ |
| private void echoUsbRequestLegacy(@NonNull UsbDeviceConnection connection, |
| @NonNull UsbEndpoint in, @NonNull UsbEndpoint out, int size, int originalSize, |
| int sliceStart, int sliceEnd, int positionInSlice, int limitInSlice, |
| boolean useDirectBuffer) { |
| Random random = new Random(); |
| |
| UsbRequest sent = new UsbRequest(); |
| boolean isInited = sent.initialize(connection, out); |
| assertTrue(isInited); |
| Object sentClientData = new Object(); |
| sent.setClientData(sentClientData); |
| |
| UsbRequest receive = new UsbRequest(); |
| isInited = receive.initialize(connection, in); |
| assertTrue(isInited); |
| Object receiveClientData = new Object(); |
| receive.setClientData(receiveClientData); |
| |
| ByteBuffer bufferSent; |
| if (useDirectBuffer) { |
| bufferSent = ByteBuffer.allocateDirect(originalSize); |
| } else { |
| bufferSent = ByteBuffer.allocate(originalSize); |
| } |
| for (int i = 0; i < originalSize; i++) { |
| bufferSent.put((byte) random.nextInt()); |
| } |
| bufferSent.position(sliceStart); |
| bufferSent.limit(sliceEnd); |
| ByteBuffer bufferSentSliced = bufferSent.slice(); |
| bufferSentSliced.position(positionInSlice); |
| bufferSentSliced.limit(limitInSlice); |
| |
| bufferSent.position(0); |
| bufferSent.limit(originalSize); |
| |
| ByteBuffer bufferReceived; |
| if (useDirectBuffer) { |
| bufferReceived = ByteBuffer.allocateDirect(originalSize); |
| } else { |
| bufferReceived = ByteBuffer.allocate(originalSize); |
| } |
| bufferReceived.position(sliceStart); |
| bufferReceived.limit(sliceEnd); |
| ByteBuffer bufferReceivedSliced = bufferReceived.slice(); |
| bufferReceivedSliced.position(positionInSlice); |
| bufferReceivedSliced.limit(limitInSlice); |
| |
| bufferReceived.position(0); |
| bufferReceived.limit(originalSize); |
| |
| boolean wasQueued = receive.queue(bufferReceivedSliced, size); |
| assertTrue(wasQueued); |
| wasQueued = sent.queue(bufferSentSliced, size); |
| assertTrue(wasQueued); |
| |
| for (int reqRun = 0; reqRun < 2; reqRun++) { |
| UsbRequest finished; |
| |
| try { |
| finished = connection.requestWait(); |
| } catch (IllegalArgumentException e) { |
| if (size > bufferSentSliced.limit() || size > bufferReceivedSliced.limit()) { |
| Log.e(LOG_TAG, "Expected failure", e); |
| continue; |
| } else { |
| throw e; |
| } |
| } |
| |
| // Should we have gotten a failure? |
| if (finished == receive) { |
| // We should have gotten an exception if size > limit |
| assumeTrue(bufferReceivedSliced.limit() >= size); |
| |
| assertEquals(size, bufferReceivedSliced.position()); |
| |
| for (int i = 0; i < size; i++) { |
| if (i < size) { |
| assertEquals(bufferSent.get(i), bufferReceived.get(i)); |
| } else { |
| assertEquals(0, bufferReceived.get(i)); |
| } |
| } |
| |
| assertSame(receiveClientData, finished.getClientData()); |
| assertSame(in, finished.getEndpoint()); |
| } else { |
| assertEquals(size, bufferSentSliced.position()); |
| |
| // We should have gotten an exception if size > limit |
| assumeTrue(bufferSentSliced.limit() >= size); |
| assertSame(sent, finished); |
| assertSame(sentClientData, finished.getClientData()); |
| assertSame(out, finished.getEndpoint()); |
| } |
| finished.close(); |
| } |
| |
| if (isZeroTransferExpected(size)) { |
| receiveZeroSizeRequestLegacy(connection, in); |
| } |
| } |
| |
| /** |
| * Receive a transfer that has size zero using current usb-request methods. |
| * |
| * @param connection Connection to the USB device |
| * @param in The in endpoint |
| */ |
| private void receiveZeroSizeRequest(@NonNull UsbDeviceConnection connection, |
| @NonNull UsbEndpoint in) { |
| UsbRequest receiveZero = new UsbRequest(); |
| boolean isInited = receiveZero.initialize(connection, in); |
| assertTrue(isInited); |
| ByteBuffer zeroBuffer = ByteBuffer.allocate(1); |
| receiveZero.queue(zeroBuffer); |
| |
| UsbRequest finished = connection.requestWait(); |
| assertEquals(receiveZero, finished); |
| assertEquals(0, zeroBuffer.position()); |
| } |
| |
| /** |
| * Send a USB request and receive it back. |
| * |
| * @param connection The connection to use |
| * @param in The endpoint to receive requests from |
| * @param out The endpoint to send requests to |
| * @param originalSize The size of the original buffer |
| * @param sliceStart The start of the final buffer in the original buffer |
| * @param sliceEnd The end of the final buffer in the original buffer |
| * @param positionInSlice The position parameter in the final buffer |
| * @param limitInSlice The limited parameter in the final buffer |
| * @param useDirectBuffer If the buffer to be used should be a direct buffer |
| */ |
| private void echoUsbRequest(@NonNull UsbDeviceConnection connection, @NonNull UsbEndpoint in, |
| @NonNull UsbEndpoint out, int originalSize, int sliceStart, int sliceEnd, |
| int positionInSlice, int limitInSlice, boolean useDirectBuffer, |
| boolean makeSendBufferReadOnly) { |
| Random random = new Random(); |
| |
| UsbRequest sent = new UsbRequest(); |
| boolean isInited = sent.initialize(connection, out); |
| assertTrue(isInited); |
| Object sentClientData = new Object(); |
| sent.setClientData(sentClientData); |
| |
| UsbRequest receive = new UsbRequest(); |
| isInited = receive.initialize(connection, in); |
| assertTrue(isInited); |
| Object receiveClientData = new Object(); |
| receive.setClientData(receiveClientData); |
| |
| ByteBuffer bufferSent; |
| if (useDirectBuffer) { |
| bufferSent = ByteBuffer.allocateDirect(originalSize); |
| } else { |
| bufferSent = ByteBuffer.allocate(originalSize); |
| } |
| for (int i = 0; i < originalSize; i++) { |
| bufferSent.put((byte) random.nextInt()); |
| } |
| if (makeSendBufferReadOnly) { |
| bufferSent = bufferSent.asReadOnlyBuffer(); |
| } |
| bufferSent.position(sliceStart); |
| bufferSent.limit(sliceEnd); |
| ByteBuffer bufferSentSliced = bufferSent.slice(); |
| bufferSentSliced.position(positionInSlice); |
| bufferSentSliced.limit(limitInSlice); |
| |
| bufferSent.position(0); |
| bufferSent.limit(originalSize); |
| |
| ByteBuffer bufferReceived; |
| if (useDirectBuffer) { |
| bufferReceived = ByteBuffer.allocateDirect(originalSize); |
| } else { |
| bufferReceived = ByteBuffer.allocate(originalSize); |
| } |
| bufferReceived.position(sliceStart); |
| bufferReceived.limit(sliceEnd); |
| ByteBuffer bufferReceivedSliced = bufferReceived.slice(); |
| bufferReceivedSliced.position(positionInSlice); |
| bufferReceivedSliced.limit(limitInSlice); |
| |
| bufferReceived.position(0); |
| bufferReceived.limit(originalSize); |
| |
| boolean wasQueued = receive.queue(bufferReceivedSliced); |
| assertTrue(wasQueued); |
| wasQueued = sent.queue(bufferSentSliced); |
| assertTrue(wasQueued); |
| |
| for (int reqRun = 0; reqRun < 2; reqRun++) { |
| UsbRequest finished = connection.requestWait(); |
| |
| if (finished == receive) { |
| assertEquals(limitInSlice, bufferReceivedSliced.limit()); |
| assertEquals(limitInSlice, bufferReceivedSliced.position()); |
| |
| for (int i = 0; i < originalSize; i++) { |
| if (i >= sliceStart + positionInSlice && i < sliceStart + limitInSlice) { |
| assertEquals(bufferSent.get(i), bufferReceived.get(i)); |
| } else { |
| assertEquals(0, bufferReceived.get(i)); |
| } |
| } |
| |
| assertSame(receiveClientData, finished.getClientData()); |
| assertSame(in, finished.getEndpoint()); |
| } else { |
| assertEquals(limitInSlice, bufferSentSliced.limit()); |
| assertEquals(limitInSlice, bufferSentSliced.position()); |
| |
| assertSame(sent, finished); |
| assertSame(sentClientData, finished.getClientData()); |
| assertSame(out, finished.getEndpoint()); |
| } |
| finished.close(); |
| } |
| |
| if (isZeroTransferExpected(sliceStart + limitInSlice - (sliceStart + positionInSlice))) { |
| receiveZeroSizeRequest(connection, in); |
| } |
| } |
| |
| /** |
| * Send a USB request using the {@link UsbRequest#queue legacy path} and receive it back. |
| * |
| * @param connection The connection to use |
| * @param in The endpoint to receive requests from |
| * @param out The endpoint to send requests to |
| * @param size The size of the request to send |
| * @param useDirectBuffer If the buffer to be used should be a direct buffer |
| */ |
| private void echoUsbRequestLegacy(@NonNull UsbDeviceConnection connection, |
| @NonNull UsbEndpoint in, @NonNull UsbEndpoint out, int size, boolean useDirectBuffer) { |
| echoUsbRequestLegacy(connection, in, out, size, size, 0, size, 0, size, useDirectBuffer); |
| } |
| |
| /** |
| * Send a USB request and receive it back. |
| * |
| * @param connection The connection to use |
| * @param in The endpoint to receive requests from |
| * @param out The endpoint to send requests to |
| * @param size The size of the request to send |
| * @param useDirectBuffer If the buffer to be used should be a direct buffer |
| */ |
| private void echoUsbRequest(@NonNull UsbDeviceConnection connection, @NonNull UsbEndpoint in, |
| @NonNull UsbEndpoint out, int size, boolean useDirectBuffer) { |
| echoUsbRequest(connection, in, out, size, 0, size, 0, size, useDirectBuffer, false); |
| } |
| |
| /** |
| * Send a USB request which more than the allowed size and receive it back. |
| * |
| * @param connection The connection to use |
| * @param in The endpoint to receive requests from |
| * @param out The endpoint to send requests to |
| */ |
| private void echoOversizedUsbRequestLegacy(@NonNull UsbDeviceConnection connection, |
| @NonNull UsbEndpoint in, @NonNull UsbEndpoint out) { |
| Random random = new Random(); |
| int totalSize = OVERSIZED_BUFFER_SIZE; |
| |
| UsbRequest sent = new UsbRequest(); |
| boolean isInited = sent.initialize(connection, out); |
| assertTrue(isInited); |
| |
| UsbRequest receive = new UsbRequest(); |
| isInited = receive.initialize(connection, in); |
| assertTrue(isInited); |
| |
| byte[] sentBytes = new byte[totalSize]; |
| random.nextBytes(sentBytes); |
| ByteBuffer bufferSent = ByteBuffer.wrap(sentBytes); |
| |
| byte[] receivedBytes = new byte[totalSize]; |
| ByteBuffer bufferReceived = ByteBuffer.wrap(receivedBytes); |
| |
| boolean wasQueued = receive.queue(bufferReceived, totalSize); |
| assertTrue(wasQueued); |
| wasQueued = sent.queue(bufferSent, totalSize); |
| assertTrue(wasQueued); |
| |
| for (int requestNum = 0; requestNum < 2; requestNum++) { |
| UsbRequest finished = connection.requestWait(); |
| if (finished == receive) { |
| // size beyond MAX_BUFFER_SIZE is ignored |
| for (int i = 0; i < totalSize; i++) { |
| if (i < MAX_BUFFER_SIZE) { |
| assertEquals(sentBytes[i], receivedBytes[i]); |
| } else { |
| assertEquals(0, receivedBytes[i]); |
| } |
| } |
| } else { |
| assertSame(sent, finished); |
| } |
| finished.close(); |
| } |
| |
| if (mDoesCompanionZeroTerminate) { |
| receiveZeroSizeRequestLegacy(connection, in); |
| } |
| } |
| |
| /** |
| * Time out while waiting for USB requests. |
| * |
| * @param connection The connection to use |
| */ |
| private void timeoutWhileWaitingForUsbRequest(@NonNull UsbDeviceConnection connection) |
| throws Throwable { |
| runAndAssertException(() -> connection.requestWait(-1), IllegalArgumentException.class); |
| |
| long startTime = now(); |
| runAndAssertException(() -> connection.requestWait(100), TimeoutException.class); |
| assertTrue(now() - startTime >= 100); |
| assertTrue(now() - startTime < 400); |
| |
| startTime = now(); |
| runAndAssertException(() -> connection.requestWait(0), TimeoutException.class); |
| assertTrue(now() - startTime < 400); |
| } |
| |
| /** |
| * Receive a USB request before a timeout triggers |
| * |
| * @param connection The connection to use |
| * @param in The endpoint to receive requests from |
| */ |
| private void receiveAfterTimeout(@NonNull UsbDeviceConnection connection, |
| @NonNull UsbEndpoint in, long timeout) throws InterruptedException, TimeoutException { |
| UsbRequest reqQueued = new UsbRequest(); |
| ByteBuffer buffer = ByteBuffer.allocate(1); |
| |
| reqQueued.initialize(connection, in); |
| reqQueued.queue(buffer); |
| |
| // Let the kernel receive and process the request |
| Thread.sleep(50); |
| |
| long startTime = now(); |
| UsbRequest reqFinished = connection.requestWait(timeout); |
| assertTrue(now() - startTime < timeout + 50); |
| assertSame(reqQueued, reqFinished); |
| reqFinished.close(); |
| } |
| |
| /** |
| * Send a USB request with size 0 using the {@link UsbRequest#queue legacy path}. |
| * |
| * @param connection The connection to use |
| * @param out The endpoint to send requests to |
| * @param useDirectBuffer Send data from a direct buffer |
| */ |
| private void sendZeroLengthRequestLegacy(@NonNull UsbDeviceConnection connection, |
| @NonNull UsbEndpoint out, boolean useDirectBuffer) { |
| UsbRequest sent = new UsbRequest(); |
| boolean isInited = sent.initialize(connection, out); |
| assertTrue(isInited); |
| |
| ByteBuffer buffer; |
| if (useDirectBuffer) { |
| buffer = ByteBuffer.allocateDirect(0); |
| } else { |
| buffer = ByteBuffer.allocate(0); |
| } |
| |
| boolean isQueued = sent.queue(buffer, 0); |
| assumeTrue(isQueued); |
| UsbRequest finished = connection.requestWait(); |
| assertSame(finished, sent); |
| finished.close(); |
| } |
| |
| /** |
| * Send a USB request with size 0. |
| * |
| * @param connection The connection to use |
| * @param out The endpoint to send requests to |
| * @param useDirectBuffer Send data from a direct buffer |
| */ |
| private void sendZeroLengthRequest(@NonNull UsbDeviceConnection connection, |
| @NonNull UsbEndpoint out, boolean useDirectBuffer) { |
| UsbRequest sent = new UsbRequest(); |
| boolean isInited = sent.initialize(connection, out); |
| assertTrue(isInited); |
| |
| ByteBuffer buffer; |
| if (useDirectBuffer) { |
| buffer = ByteBuffer.allocateDirect(0); |
| } else { |
| buffer = ByteBuffer.allocate(0); |
| } |
| |
| boolean isQueued = sent.queue(buffer); |
| assumeTrue(isQueued); |
| UsbRequest finished = connection.requestWait(); |
| assertSame(finished, sent); |
| finished.close(); |
| } |
| |
| /** |
| * Send a USB request with a null buffer. |
| * |
| * @param connection The connection to use |
| * @param out The endpoint to send requests to |
| */ |
| private void sendNullRequest(@NonNull UsbDeviceConnection connection, |
| @NonNull UsbEndpoint out) { |
| UsbRequest sent = new UsbRequest(); |
| boolean isInited = sent.initialize(connection, out); |
| assertTrue(isInited); |
| |
| boolean isQueued = sent.queue(null); |
| assumeTrue(isQueued); |
| UsbRequest finished = connection.requestWait(); |
| assertSame(finished, sent); |
| finished.close(); |
| } |
| |
| /** |
| * Receive a USB request with size 0. |
| * |
| * @param connection The connection to use |
| * @param in The endpoint to recevie requests from |
| */ |
| private void receiveZeroLengthRequestLegacy(@NonNull UsbDeviceConnection connection, |
| @NonNull UsbEndpoint in, boolean useDirectBuffer) { |
| UsbRequest zeroReceived = new UsbRequest(); |
| boolean isInited = zeroReceived.initialize(connection, in); |
| assertTrue(isInited); |
| |
| UsbRequest oneReceived = new UsbRequest(); |
| isInited = oneReceived.initialize(connection, in); |
| assertTrue(isInited); |
| |
| ByteBuffer buffer; |
| if (useDirectBuffer) { |
| buffer = ByteBuffer.allocateDirect(0); |
| } else { |
| buffer = ByteBuffer.allocate(0); |
| } |
| |
| ByteBuffer buffer1; |
| if (useDirectBuffer) { |
| buffer1 = ByteBuffer.allocateDirect(1); |
| } else { |
| buffer1 = ByteBuffer.allocate(1); |
| } |
| |
| boolean isQueued = zeroReceived.queue(buffer); |
| assumeTrue(isQueued); |
| isQueued = oneReceived.queue(buffer1); |
| assumeTrue(isQueued); |
| |
| // We expect both to be returned after some time |
| ArrayList<UsbRequest> finished = new ArrayList<>(2); |
| |
| // We expect both request to come back after the delay, but then quickly |
| long startTime = now(); |
| finished.add(connection.requestWait()); |
| long firstReturned = now(); |
| finished.add(connection.requestWait()); |
| long secondReturned = now(); |
| |
| assumeTrue(firstReturned - startTime > 100); |
| assumeTrue(secondReturned - firstReturned < 100); |
| |
| assertTrue(finished.contains(zeroReceived)); |
| assertTrue(finished.contains(oneReceived)); |
| } |
| |
| /** |
| * Tests the {@link UsbRequest#queue legacy implementaion} of {@link UsbRequest} and |
| * {@link UsbDeviceConnection#requestWait()}. |
| * |
| * @param connection The connection to use for testing |
| * @param iface The interface of the android accessory interface of the device |
| * @throws Throwable |
| */ |
| private void usbRequestLegacyTests(@NonNull UsbDeviceConnection connection, |
| @NonNull UsbInterface iface) throws Throwable { |
| // Find bulk in and out endpoints |
| assumeTrue(iface.getEndpointCount() == 2); |
| final UsbEndpoint in = getEndpoint(iface, UsbConstants.USB_DIR_IN); |
| final UsbEndpoint out = getEndpoint(iface, UsbConstants.USB_DIR_OUT); |
| assertNotNull(in); |
| assertNotNull(out); |
| |
| // Single threaded send and receive |
| nextTest(connection, in, out, "Echo 1 byte"); |
| echoUsbRequestLegacy(connection, in, out, 1, true); |
| |
| nextTest(connection, in, out, "Echo 1 byte"); |
| echoUsbRequestLegacy(connection, in, out, 1, false); |
| |
| nextTest(connection, in, out, "Echo max bytes"); |
| echoUsbRequestLegacy(connection, in, out, MAX_BUFFER_SIZE, true); |
| |
| nextTest(connection, in, out, "Echo max bytes"); |
| echoUsbRequestLegacy(connection, in, out, MAX_BUFFER_SIZE, false); |
| |
| nextTest(connection, in, out, "Echo oversized buffer"); |
| echoOversizedUsbRequestLegacy(connection, in, out); |
| |
| // Send empty requests |
| sendZeroLengthRequestLegacy(connection, out, true); |
| sendZeroLengthRequestLegacy(connection, out, false); |
| |
| // waitRequest with timeout |
| timeoutWhileWaitingForUsbRequest(connection); |
| |
| nextTest(connection, in, out, "Receive byte after some time"); |
| receiveAfterTimeout(connection, in, 400); |
| |
| nextTest(connection, in, out, "Receive byte immediately"); |
| // Make sure the data is received before we queue the request for it |
| Thread.sleep(50); |
| receiveAfterTimeout(connection, in, 0); |
| |
| /* TODO: Unreliable |
| |
| // Zero length means waiting for the next data and then return |
| nextTest(connection, in, out, "Receive byte after some time"); |
| receiveZeroLengthRequestLegacy(connection, in, true); |
| |
| nextTest(connection, in, out, "Receive byte after some time"); |
| receiveZeroLengthRequestLegacy(connection, in, true); |
| |
| */ |
| |
| // UsbRequest.queue ignores position, limit, arrayOffset, and capacity |
| nextTest(connection, in, out, "Echo 42 bytes"); |
| echoUsbRequestLegacy(connection, in, out, 42, 42, 0, 42, 5, 42, false); |
| |
| nextTest(connection, in, out, "Echo 42 bytes"); |
| echoUsbRequestLegacy(connection, in, out, 42, 42, 0, 42, 0, 36, false); |
| |
| nextTest(connection, in, out, "Echo 42 bytes"); |
| echoUsbRequestLegacy(connection, in, out, 42, 42, 5, 42, 0, 36, false); |
| |
| nextTest(connection, in, out, "Echo 42 bytes"); |
| echoUsbRequestLegacy(connection, in, out, 42, 42, 0, 36, 0, 31, false); |
| |
| nextTest(connection, in, out, "Echo 42 bytes"); |
| echoUsbRequestLegacy(connection, in, out, 42, 47, 0, 47, 0, 47, false); |
| |
| nextTest(connection, in, out, "Echo 42 bytes"); |
| echoUsbRequestLegacy(connection, in, out, 42, 47, 5, 47, 0, 42, false); |
| |
| nextTest(connection, in, out, "Echo 42 bytes"); |
| echoUsbRequestLegacy(connection, in, out, 42, 47, 0, 42, 0, 42, false); |
| |
| nextTest(connection, in, out, "Echo 42 bytes"); |
| echoUsbRequestLegacy(connection, in, out, 42, 47, 0, 47, 5, 47, false); |
| |
| nextTest(connection, in, out, "Echo 42 bytes"); |
| echoUsbRequestLegacy(connection, in, out, 42, 47, 5, 47, 5, 36, false); |
| |
| // Illegal arguments |
| final UsbRequest req1 = new UsbRequest(); |
| runAndAssertException(() -> req1.initialize(null, in), NullPointerException.class); |
| runAndAssertException(() -> req1.initialize(connection, null), NullPointerException.class); |
| boolean isInited = req1.initialize(connection, in); |
| assertTrue(isInited); |
| runAndAssertException(() -> req1.queue(null, 0), NullPointerException.class); |
| runAndAssertException(() -> req1.queue(ByteBuffer.allocate(1).asReadOnlyBuffer(), 1), |
| IllegalArgumentException.class); |
| req1.close(); |
| |
| // Cannot queue closed request |
| runAndAssertException(() -> req1.queue(ByteBuffer.allocate(1), 1), |
| NullPointerException.class); |
| runAndAssertException(() -> req1.queue(ByteBuffer.allocateDirect(1), 1), |
| NullPointerException.class); |
| } |
| |
| /** |
| * Repeat c n times |
| * |
| * @param c The character to repeat |
| * @param n The number of times to repeat |
| * |
| * @return c repeated n times |
| */ |
| public static String repeat(char c, int n) { |
| final StringBuilder result = new StringBuilder(); |
| for (int i = 0; i < n; i++) { |
| if (c != ' ' && i % 10 == 0) { |
| result.append(i / 10); |
| } else { |
| result.append(c); |
| } |
| } |
| return result.toString(); |
| } |
| |
| /** |
| * Tests {@link UsbRequest} and {@link UsbDeviceConnection#requestWait()}. |
| * |
| * @param connection The connection to use for testing |
| * @param iface The interface of the android accessory interface of the device |
| * @throws Throwable |
| */ |
| private void usbRequestTests(@NonNull UsbDeviceConnection connection, |
| @NonNull UsbInterface iface) throws Throwable { |
| // Find bulk in and out endpoints |
| assumeTrue(iface.getEndpointCount() == 2); |
| final UsbEndpoint in = getEndpoint(iface, UsbConstants.USB_DIR_IN); |
| final UsbEndpoint out = getEndpoint(iface, UsbConstants.USB_DIR_OUT); |
| assertNotNull(in); |
| assertNotNull(out); |
| |
| // Single threaded send and receive |
| nextTest(connection, in, out, "Echo 1 byte"); |
| echoUsbRequest(connection, in, out, 1, true); |
| |
| nextTest(connection, in, out, "Echo 1 byte"); |
| echoUsbRequest(connection, in, out, 1, false); |
| |
| nextTest(connection, in, out, "Echo max bytes"); |
| echoUsbRequest(connection, in, out, MAX_BUFFER_SIZE, true); |
| |
| nextTest(connection, in, out, "Echo max bytes"); |
| echoUsbRequest(connection, in, out, MAX_BUFFER_SIZE, false); |
| |
| // Send empty requests |
| sendZeroLengthRequest(connection, out, true); |
| sendZeroLengthRequest(connection, out, false); |
| sendNullRequest(connection, out); |
| |
| /* TODO: Unreliable |
| |
| // Zero length means waiting for the next data and then return |
| nextTest(connection, in, out, "Receive byte after some time"); |
| receiveZeroLengthRequest(connection, in, true); |
| |
| nextTest(connection, in, out, "Receive byte after some time"); |
| receiveZeroLengthRequest(connection, in, true); |
| |
| */ |
| |
| for (int startOfSlice : new int[]{0, 1}) { |
| for (int endOffsetOfSlice : new int[]{0, 2}) { |
| for (int positionInSlice : new int[]{0, 5}) { |
| for (int limitOffsetInSlice : new int[]{0, 11}) { |
| for (boolean useDirectBuffer : new boolean[]{true, false}) { |
| for (boolean makeSendBufferReadOnly : new boolean[]{true, false}) { |
| int sliceSize = 42 + positionInSlice + limitOffsetInSlice; |
| int originalSize = sliceSize + startOfSlice + endOffsetOfSlice; |
| |
| nextTest(connection, in, out, "Echo 42 bytes"); |
| |
| // Log buffer, slice, and data offsets |
| Log.i(LOG_TAG, |
| "buffer" + (makeSendBufferReadOnly ? "(ro): [" : ": [") |
| + repeat('.', originalSize) + "]"); |
| Log.i(LOG_TAG, |
| "slice: " + repeat(' ', startOfSlice) + " [" + repeat( |
| '.', sliceSize) + "]"); |
| Log.i(LOG_TAG, |
| "data: " + repeat(' ', startOfSlice + positionInSlice) |
| + " [" + repeat('.', 42) + "]"); |
| |
| echoUsbRequest(connection, in, out, originalSize, startOfSlice, |
| originalSize - endOffsetOfSlice, positionInSlice, |
| sliceSize - limitOffsetInSlice, useDirectBuffer, |
| makeSendBufferReadOnly); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| // Illegal arguments |
| final UsbRequest req1 = new UsbRequest(); |
| runAndAssertException(() -> req1.initialize(null, in), NullPointerException.class); |
| runAndAssertException(() -> req1.initialize(connection, null), NullPointerException.class); |
| boolean isInited = req1.initialize(connection, in); |
| assertTrue(isInited); |
| runAndAssertException(() -> req1.queue(ByteBuffer.allocate(16384 + 1).asReadOnlyBuffer()), |
| IllegalArgumentException.class); |
| runAndAssertException(() -> req1.queue(ByteBuffer.allocate(1).asReadOnlyBuffer()), |
| IllegalArgumentException.class); |
| req1.close(); |
| |
| // Cannot queue closed request |
| runAndAssertException(() -> req1.queue(ByteBuffer.allocate(1)), |
| IllegalStateException.class); |
| runAndAssertException(() -> req1.queue(ByteBuffer.allocateDirect(1)), |
| IllegalStateException.class); |
| |
| // Initialize |
| UsbRequest req2 = new UsbRequest(); |
| isInited = req2.initialize(connection, in); |
| assertTrue(isInited); |
| isInited = req2.initialize(connection, out); |
| assertTrue(isInited); |
| req2.close(); |
| |
| // Close |
| req2 = new UsbRequest(); |
| req2.close(); |
| |
| req2.initialize(connection, in); |
| req2.close(); |
| req2.close(); |
| } |
| |
| /** State of a {@link UsbRequest} in flight */ |
| private static class RequestState { |
| final ByteBuffer buffer; |
| final Object clientData; |
| |
| private RequestState(ByteBuffer buffer, Object clientData) { |
| this.buffer = buffer; |
| this.clientData = clientData; |
| } |
| } |
| |
| /** Recycles elements that might be expensive to create */ |
| private abstract class Recycler<T> { |
| private final Random mRandom; |
| private final LinkedList<T> mData; |
| |
| protected Recycler() { |
| mData = new LinkedList<>(); |
| mRandom = new Random(); |
| } |
| |
| /** |
| * Add a new element to be recycled. |
| * |
| * @param newElement The element that is not used anymore and can be used by someone else. |
| */ |
| private void recycle(@NonNull T newElement) { |
| synchronized (mData) { |
| if (mRandom.nextBoolean()) { |
| mData.addLast(newElement); |
| } else { |
| mData.addFirst(newElement); |
| } |
| } |
| } |
| |
| /** |
| * Get a recycled element or create a new one if needed. |
| * |
| * @return An element that can be used (maybe recycled) |
| */ |
| private @NonNull T get() { |
| T recycledElement; |
| |
| try { |
| synchronized (mData) { |
| recycledElement = mData.pop(); |
| } |
| } catch (NoSuchElementException ignored) { |
| recycledElement = create(); |
| } |
| |
| reset(recycledElement); |
| |
| return recycledElement; |
| } |
| |
| /** Reset internal state of {@code recycledElement} */ |
| protected abstract void reset(@NonNull T recycledElement); |
| |
| /** Create a new element */ |
| protected abstract @NonNull T create(); |
| |
| /** Get all elements that are currently recycled and waiting to be used again */ |
| public @NonNull LinkedList<T> getAll() { |
| return mData; |
| } |
| } |
| |
| /** |
| * Common code between {@link QueuerThread} and {@link ReceiverThread}. |
| */ |
| private class TestThread extends Thread { |
| /** State copied from the main thread (see runTest()) */ |
| protected final UsbDeviceConnection mConnection; |
| protected final Recycler<UsbRequest> mInRequestRecycler; |
| protected final Recycler<UsbRequest> mOutRequestRecycler; |
| protected final Recycler<ByteBuffer> mBufferRecycler; |
| protected final HashMap<UsbRequest, RequestState> mRequestsInFlight; |
| protected final HashMap<Integer, Integer> mData; |
| protected final ArrayList<Throwable> mErrors; |
| |
| protected volatile boolean mShouldStop; |
| |
| TestThread(@NonNull UsbDeviceConnection connection, |
| @NonNull Recycler<UsbRequest> inRequestRecycler, |
| @NonNull Recycler<UsbRequest> outRequestRecycler, |
| @NonNull Recycler<ByteBuffer> bufferRecycler, |
| @NonNull HashMap<UsbRequest, RequestState> requestsInFlight, |
| @NonNull HashMap<Integer, Integer> data, |
| @NonNull ArrayList<Throwable> errors) { |
| super(); |
| |
| mShouldStop = false; |
| mConnection = connection; |
| mBufferRecycler = bufferRecycler; |
| mInRequestRecycler = inRequestRecycler; |
| mOutRequestRecycler = outRequestRecycler; |
| mRequestsInFlight = requestsInFlight; |
| mData = data; |
| mErrors = errors; |
| } |
| |
| /** |
| * Stop thread |
| */ |
| void abort() { |
| mShouldStop = true; |
| interrupt(); |
| } |
| } |
| |
| /** |
| * A thread that queues matching write and read {@link UsbRequest requests}. We expect the |
| * writes to be echoed back and return in unchanged in the read requests. |
| * <p> This thread just issues the requests and does not care about them anymore after the |
| * system took them. The {@link ReceiverThread} handles the result of both write and read |
| * requests.</p> |
| */ |
| private class QueuerThread extends TestThread { |
| private static final int MAX_IN_FLIGHT = 64; |
| private static final long RUN_TIME = 10 * 1000; |
| |
| private final AtomicInteger mCounter; |
| |
| /** |
| * Create a new thread that queues matching write and read UsbRequests. |
| * |
| * @param connection Connection to communicate with |
| * @param inRequestRecycler Pool of in-requests that can be reused |
| * @param outRequestRecycler Pool of out-requests that can be reused |
| * @param bufferRecycler Pool of byte buffers that can be reused |
| * @param requestsInFlight State of the requests currently in flight |
| * @param data Mapping counter -> data |
| * @param counter An atomic counter |
| * @param errors Pool of throwables created by threads like this |
| */ |
| QueuerThread(@NonNull UsbDeviceConnection connection, |
| @NonNull Recycler<UsbRequest> inRequestRecycler, |
| @NonNull Recycler<UsbRequest> outRequestRecycler, |
| @NonNull Recycler<ByteBuffer> bufferRecycler, |
| @NonNull HashMap<UsbRequest, RequestState> requestsInFlight, |
| @NonNull HashMap<Integer, Integer> data, |
| @NonNull AtomicInteger counter, |
| @NonNull ArrayList<Throwable> errors) { |
| super(connection, inRequestRecycler, outRequestRecycler, bufferRecycler, |
| requestsInFlight, data, errors); |
| |
| mCounter = counter; |
| } |
| |
| @Override |
| public void run() { |
| Random random = new Random(); |
| |
| long endTime = now() + RUN_TIME; |
| |
| while (now() < endTime && !mShouldStop) { |
| try { |
| int counter = mCounter.getAndIncrement(); |
| |
| if (counter % 1024 == 0) { |
| Log.i(LOG_TAG, "Counter is " + counter); |
| } |
| |
| // Write [1:counter:data] |
| UsbRequest writeRequest = mOutRequestRecycler.get(); |
| ByteBuffer writeBuffer = mBufferRecycler.get(); |
| int data = random.nextInt(); |
| writeBuffer.put((byte)1).putInt(counter).putInt(data); |
| writeBuffer.flip(); |
| |
| // Send read that will receive the data back from the write as the other side |
| // will echo all requests. |
| UsbRequest readRequest = mInRequestRecycler.get(); |
| ByteBuffer readBuffer = mBufferRecycler.get(); |
| |
| // Register requests |
| synchronized (mRequestsInFlight) { |
| // Wait until previous requests were processed |
| while (mRequestsInFlight.size() > MAX_IN_FLIGHT) { |
| try { |
| mRequestsInFlight.wait(); |
| } catch (InterruptedException e) { |
| break; |
| } |
| } |
| |
| if (mShouldStop) { |
| break; |
| } else { |
| mRequestsInFlight.put(writeRequest, new RequestState(writeBuffer, |
| writeRequest.getClientData())); |
| mRequestsInFlight.put(readRequest, new RequestState(readBuffer, |
| readRequest.getClientData())); |
| mRequestsInFlight.notifyAll(); |
| } |
| } |
| |
| // Store which data was written for the counter |
| synchronized (mData) { |
| mData.put(counter, data); |
| } |
| |
| // Send both requests to the system. Once they finish the ReceiverThread will |
| // be notified |
| boolean isQueued = writeRequest.queue(writeBuffer); |
| assertTrue(isQueued); |
| |
| isQueued = readRequest.queue(readBuffer, 9); |
| assertTrue(isQueued); |
| } catch (Throwable t) { |
| synchronized (mErrors) { |
| mErrors.add(t); |
| mErrors.notify(); |
| } |
| break; |
| } |
| } |
| } |
| } |
| |
| /** |
| * A thread that receives processed UsbRequests and compares the expected result. The requests |
| * can be both read and write requests. The requests were created and given to the system by |
| * the {@link QueuerThread}. |
| */ |
| private class ReceiverThread extends TestThread { |
| private final UsbEndpoint mOut; |
| |
| /** |
| * Create a thread that receives processed UsbRequests and compares the expected result. |
| * |
| * @param connection Connection to communicate with |
| * @param out Endpoint to queue write requests on |
| * @param inRequestRecycler Pool of in-requests that can be reused |
| * @param outRequestRecycler Pool of out-requests that can be reused |
| * @param bufferRecycler Pool of byte buffers that can be reused |
| * @param requestsInFlight State of the requests currently in flight |
| * @param data Mapping counter -> data |
| * @param errors Pool of throwables created by threads like this |
| */ |
| ReceiverThread(@NonNull UsbDeviceConnection connection, @NonNull UsbEndpoint out, |
| @NonNull Recycler<UsbRequest> inRequestRecycler, |
| @NonNull Recycler<UsbRequest> outRequestRecycler, |
| @NonNull Recycler<ByteBuffer> bufferRecycler, |
| @NonNull HashMap<UsbRequest, RequestState> requestsInFlight, |
| @NonNull HashMap<Integer, Integer> data, @NonNull ArrayList<Throwable> errors) { |
| super(connection, inRequestRecycler, outRequestRecycler, bufferRecycler, |
| requestsInFlight, data, errors); |
| |
| mOut = out; |
| } |
| |
| @Override |
| public void run() { |
| while (!mShouldStop) { |
| try { |
| // Wait until a request is queued as mConnection.requestWait() cannot be |
| // interrupted. |
| synchronized (mRequestsInFlight) { |
| while (mRequestsInFlight.isEmpty()) { |
| try { |
| mRequestsInFlight.wait(); |
| } catch (InterruptedException e) { |
| break; |
| } |
| } |
| |
| if (mShouldStop) { |
| break; |
| } |
| } |
| |
| // Receive request |
| UsbRequest request = mConnection.requestWait(); |
| assertNotNull(request); |
| |
| // Find the state the request should have |
| RequestState state; |
| synchronized (mRequestsInFlight) { |
| state = mRequestsInFlight.remove(request); |
| mRequestsInFlight.notifyAll(); |
| } |
| |
| // Compare client data |
| assertSame(state.clientData, request.getClientData()); |
| |
| // There is nothing more to check about write requests, but for read requests |
| // (the ones going to an out endpoint) we know that it just an echoed back write |
| // request. |
| if (!request.getEndpoint().equals(mOut)) { |
| state.buffer.flip(); |
| |
| // Read request buffer, check that data is correct |
| byte alive = state.buffer.get(); |
| int counter = state.buffer.getInt(); |
| int receivedData = state.buffer.getInt(); |
| |
| // We stored which data-combinations were written |
| int expectedData; |
| synchronized(mData) { |
| expectedData = mData.remove(counter); |
| } |
| |
| // Make sure read request matches a write request we sent before |
| assertEquals(1, alive); |
| assertEquals(expectedData, receivedData); |
| } |
| |
| // Recycle buffers and requests so they can be reused later. |
| mBufferRecycler.recycle(state.buffer); |
| |
| if (request.getEndpoint().equals(mOut)) { |
| mOutRequestRecycler.recycle(request); |
| } else { |
| mInRequestRecycler.recycle(request); |
| } |
| } catch (Throwable t) { |
| synchronized (mErrors) { |
| mErrors.add(t); |
| mErrors.notify(); |
| } |
| break; |
| } |
| } |
| } |
| } |
| |
| /** |
| * Tests parallel issuance and receiving of {@link UsbRequest usb requests}. |
| * |
| * @param connection The connection to use for testing |
| * @param iface The interface of the android accessory interface of the device |
| */ |
| private void parallelUsbRequestsTests(@NonNull UsbDeviceConnection connection, |
| @NonNull UsbInterface iface) { |
| // Find bulk in and out endpoints |
| assumeTrue(iface.getEndpointCount() == 2); |
| final UsbEndpoint in = getEndpoint(iface, UsbConstants.USB_DIR_IN); |
| final UsbEndpoint out = getEndpoint(iface, UsbConstants.USB_DIR_OUT); |
| assertNotNull(in); |
| assertNotNull(out); |
| |
| // Recycler for requests for the in-endpoint |
| Recycler<UsbRequest> inRequestRecycler = new Recycler<UsbRequest>() { |
| @Override |
| protected void reset(@NonNull UsbRequest recycledElement) { |
| recycledElement.setClientData(new Object()); |
| } |
| |
| @Override |
| protected @NonNull UsbRequest create() { |
| UsbRequest request = new UsbRequest(); |
| request.initialize(connection, in); |
| |
| return request; |
| } |
| }; |
| |
| // Recycler for requests for the in-endpoint |
| Recycler<UsbRequest> outRequestRecycler = new Recycler<UsbRequest>() { |
| @Override |
| protected void reset(@NonNull UsbRequest recycledElement) { |
| recycledElement.setClientData(new Object()); |
| } |
| |
| @Override |
| protected @NonNull UsbRequest create() { |
| UsbRequest request = new UsbRequest(); |
| request.initialize(connection, out); |
| |
| return request; |
| } |
| }; |
| |
| // Recycler for requests for read and write buffers |
| Recycler<ByteBuffer> bufferRecycler = new Recycler<ByteBuffer>() { |
| @Override |
| protected void reset(@NonNull ByteBuffer recycledElement) { |
| recycledElement.rewind(); |
| } |
| |
| @Override |
| protected @NonNull ByteBuffer create() { |
| return ByteBuffer.allocateDirect(9); |
| } |
| }; |
| |
| HashMap<UsbRequest, RequestState> requestsInFlight = new HashMap<>(); |
| |
| // Data in the requests |
| HashMap<Integer, Integer> data = new HashMap<>(); |
| AtomicInteger counter = new AtomicInteger(0); |
| |
| // Errors created in the threads |
| ArrayList<Throwable> errors = new ArrayList<>(); |
| |
| // Create two threads that queue read and write requests |
| QueuerThread queuer1 = new QueuerThread(connection, inRequestRecycler, |
| outRequestRecycler, bufferRecycler, requestsInFlight, data, counter, errors); |
| QueuerThread queuer2 = new QueuerThread(connection, inRequestRecycler, |
| outRequestRecycler, bufferRecycler, requestsInFlight, data, counter, errors); |
| |
| // Create a thread that receives the requests after they are processed. |
| ReceiverThread receiver = new ReceiverThread(connection, out, inRequestRecycler, |
| outRequestRecycler, bufferRecycler, requestsInFlight, data, errors); |
| |
| nextTest(connection, in, out, "Echo until stop signal"); |
| |
| queuer1.start(); |
| queuer2.start(); |
| receiver.start(); |
| |
| Log.i(LOG_TAG, "Waiting for queuers to stop"); |
| |
| try { |
| queuer1.join(); |
| queuer2.join(); |
| } catch (InterruptedException e) { |
| synchronized(errors) { |
| errors.add(e); |
| } |
| } |
| |
| if (errors.isEmpty()) { |
| Log.i(LOG_TAG, "Wait for all requests to finish"); |
| synchronized (requestsInFlight) { |
| while (!requestsInFlight.isEmpty()) { |
| try { |
| requestsInFlight.wait(); |
| } catch (InterruptedException e) { |
| synchronized(errors) { |
| errors.add(e); |
| } |
| break; |
| } |
| } |
| } |
| |
| receiver.abort(); |
| |
| try { |
| receiver.join(); |
| } catch (InterruptedException e) { |
| synchronized(errors) { |
| errors.add(e); |
| } |
| } |
| |
| // Close all requests that are currently recycled |
| inRequestRecycler.getAll().forEach(UsbRequest::close); |
| outRequestRecycler.getAll().forEach(UsbRequest::close); |
| } else { |
| receiver.abort(); |
| } |
| |
| for (Throwable t : errors) { |
| Log.e(LOG_TAG, "Error during test", t); |
| } |
| |
| byte[] stopBytes = new byte[9]; |
| connection.bulkTransfer(out, stopBytes, 9, 0); |
| |
| // If we had any error make the test fail |
| assertEquals(0, errors.size()); |
| } |
| |
| /** |
| * Tests {@link UsbDeviceConnection#bulkTransfer}. |
| * |
| * @param connection The connection to use for testing |
| * @param iface The interface of the android accessory interface of the device |
| * @throws Throwable |
| */ |
| private void bulkTransferTests(@NonNull UsbDeviceConnection connection, |
| @NonNull UsbInterface iface) throws Throwable { |
| // Find bulk in and out endpoints |
| assumeTrue(iface.getEndpointCount() == 2); |
| final UsbEndpoint in = getEndpoint(iface, UsbConstants.USB_DIR_IN); |
| final UsbEndpoint out = getEndpoint(iface, UsbConstants.USB_DIR_OUT); |
| assertNotNull(in); |
| assertNotNull(out); |
| |
| // Transmission tests |
| nextTest(connection, in, out, "Echo 1 byte"); |
| echoBulkTransfer(connection, in, out, 1); |
| |
| nextTest(connection, in, out, "Echo 42 bytes"); |
| echoBulkTransferOffset(connection, in, out, 23, 42); |
| |
| nextTest(connection, in, out, "Echo max bytes"); |
| echoBulkTransfer(connection, in, out, MAX_BUFFER_SIZE); |
| |
| nextTest(connection, in, out, "Echo oversized buffer"); |
| echoOversizedBulkTransfer(connection, in, out); |
| |
| nextTest(connection, in, out, "Receive oversized buffer"); |
| receiveOversizedBulkTransfer(connection, in); |
| |
| // Illegal arguments |
| runAndAssertException(() -> connection.bulkTransfer(out, new byte[1], 2, 0), |
| IllegalArgumentException.class); |
| runAndAssertException(() -> connection.bulkTransfer(in, new byte[1], 2, 0), |
| IllegalArgumentException.class); |
| runAndAssertException(() -> connection.bulkTransfer(out, new byte[2], 1, 2, 0), |
| IllegalArgumentException.class); |
| runAndAssertException(() -> connection.bulkTransfer(in, new byte[2], 1, 2, 0), |
| IllegalArgumentException.class); |
| runAndAssertException(() -> connection.bulkTransfer(out, new byte[1], -1, 0), |
| IllegalArgumentException.class); |
| runAndAssertException(() -> connection.bulkTransfer(in, new byte[1], -1, 0), |
| IllegalArgumentException.class); |
| runAndAssertException(() -> connection.bulkTransfer(out, new byte[1], 1, -1, 0), |
| IllegalArgumentException.class); |
| runAndAssertException(() -> connection.bulkTransfer(in, new byte[1], 1, -1, 0), |
| IllegalArgumentException.class); |
| runAndAssertException(() -> connection.bulkTransfer(out, new byte[1], -1, -1, 0), |
| IllegalArgumentException.class); |
| runAndAssertException(() -> connection.bulkTransfer(in, new byte[1], -1, -1, 0), |
| IllegalArgumentException.class); |
| runAndAssertException(() -> connection.bulkTransfer(null, new byte[1], 1, 0), |
| NullPointerException.class); |
| |
| // Transmissions that do nothing |
| int numSent = connection.bulkTransfer(out, null, 0, 0); |
| assertEquals(0, numSent); |
| |
| numSent = connection.bulkTransfer(out, null, 0, 0, 0); |
| assertEquals(0, numSent); |
| |
| numSent = connection.bulkTransfer(out, new byte[0], 0, 0); |
| assertEquals(0, numSent); |
| |
| numSent = connection.bulkTransfer(out, new byte[0], 0, 0, 0); |
| assertEquals(0, numSent); |
| |
| numSent = connection.bulkTransfer(out, new byte[2], 2, 0, 0); |
| assertEquals(0, numSent); |
| |
| /* TODO: These tests are flaky as they appear to be affected by previous tests |
| |
| // Transmissions that do not transfer data: |
| // - first transfer blocks until data is received, but does not return the data. |
| // - The data is read in the second transfer |
| nextTest(connection, in, out, "Receive byte after some time"); |
| receiveWithEmptyBuffer(connection, in, null, 0, 0); |
| |
| nextTest(connection, in, out, "Receive byte after some time"); |
| receiveWithEmptyBuffer(connection, in, new byte[0], 0, 0); |
| |
| nextTest(connection, in, out, "Receive byte after some time"); |
| receiveWithEmptyBuffer(connection, in, new byte[2], 2, 0); |
| |
| */ |
| |
| // Timeouts |
| int numReceived = connection.bulkTransfer(in, new byte[1], 1, 100); |
| assertEquals(-1, numReceived); |
| |
| nextTest(connection, in, out, "Receive byte after some time"); |
| numReceived = connection.bulkTransfer(in, new byte[1], 1, 10000); |
| assertEquals(1, numReceived); |
| |
| nextTest(connection, in, out, "Receive byte after some time"); |
| numReceived = connection.bulkTransfer(in, new byte[1], 1, 0); |
| assertEquals(1, numReceived); |
| |
| nextTest(connection, in, out, "Receive byte after some time"); |
| numReceived = connection.bulkTransfer(in, new byte[1], 1, -1); |
| assertEquals(1, numReceived); |
| |
| numReceived = connection.bulkTransfer(in, new byte[2], 1, 1, 100); |
| assertEquals(-1, numReceived); |
| |
| nextTest(connection, in, out, "Receive byte after some time"); |
| numReceived = connection.bulkTransfer(in, new byte[2], 1, 1, 0); |
| assertEquals(1, numReceived); |
| |
| nextTest(connection, in, out, "Receive byte after some time"); |
| numReceived = connection.bulkTransfer(in, new byte[2], 1, 1, -1); |
| assertEquals(1, numReceived); |
| } |
| |
| /** |
| * Test if the companion device zero-terminates their requests that are multiples of the |
| * maximum package size. Then sets {@link #mDoesCompanionZeroTerminate} if the companion |
| * zero terminates |
| * |
| * @param connection Connection to the USB device |
| * @param iface The interface to use |
| */ |
| private void testIfCompanionZeroTerminates(@NonNull UsbDeviceConnection connection, |
| @NonNull UsbInterface iface) { |
| assumeTrue(iface.getEndpointCount() == 2); |
| final UsbEndpoint in = getEndpoint(iface, UsbConstants.USB_DIR_IN); |
| final UsbEndpoint out = getEndpoint(iface, UsbConstants.USB_DIR_OUT); |
| assertNotNull(in); |
| assertNotNull(out); |
| |
| nextTest(connection, in, out, "does companion zero terminate"); |
| |
| // The other size sends: |
| // - 1024 bytes |
| // - maybe a zero sized package |
| // - 1 byte |
| |
| byte[] buffer = new byte[1024]; |
| int numTransferred = connection.bulkTransfer(in, buffer, 1024, 0); |
| assertEquals(1024, numTransferred); |
| |
| numTransferred = connection.bulkTransfer(in, buffer, 1, 0); |
| if (numTransferred == 0) { |
| assertEquals(0, numTransferred); |
| |
| numTransferred = connection.bulkTransfer(in, buffer, 1, 0); |
| assertEquals(1, numTransferred); |
| |
| mDoesCompanionZeroTerminate = true; |
| Log.i(LOG_TAG, "Companion zero terminates"); |
| } else { |
| assertEquals(1, numTransferred); |
| Log.i(LOG_TAG, "Companion does not zero terminate - an older device"); |
| } |
| } |
| |
| /** |
| * Send signal to the remove device that testing is finished. |
| * |
| * @param connection The connection to use for testing |
| * @param iface The interface of the android accessory interface of the device |
| */ |
| private void endTesting(@NonNull UsbDeviceConnection connection, @NonNull UsbInterface iface) { |
| // "done" signals that testing is over |
| nextTest(connection, getEndpoint(iface, UsbConstants.USB_DIR_IN), |
| getEndpoint(iface, UsbConstants.USB_DIR_OUT), "done"); |
| } |
| |
| /** |
| * Test the behavior of {@link UsbDeviceConnection#claimInterface} and |
| * {@link UsbDeviceConnection#releaseInterface}. |
| * |
| * <p>Note: The interface under test is <u>not</u> claimed by a kernel driver, hence there is |
| * no difference in behavior between force and non-force versions of |
| * {@link UsbDeviceConnection#claimInterface}</p> |
| * |
| * @param connection The connection to use |
| * @param iface The interface to claim and release |
| * |
| * @throws Throwable |
| */ |
| private void claimInterfaceTests(@NonNull UsbDeviceConnection connection, |
| @NonNull UsbInterface iface) throws Throwable { |
| // The interface is not claimed by the kernel driver, so not forcing it should work |
| boolean claimed = connection.claimInterface(iface, false); |
| assertTrue(claimed); |
| boolean released = connection.releaseInterface(iface); |
| assertTrue(released); |
| |
| // Forcing if it is not necessary does no harm |
| claimed = connection.claimInterface(iface, true); |
| assertTrue(claimed); |
| |
| // Re-claiming does nothing |
| claimed = connection.claimInterface(iface, true); |
| assertTrue(claimed); |
| |
| released = connection.releaseInterface(iface); |
| assertTrue(released); |
| |
| // Re-releasing is not allowed |
| released = connection.releaseInterface(iface); |
| assertFalse(released); |
| |
| // Using an unclaimed interface claims it automatically |
| int numSent = connection.bulkTransfer(getEndpoint(iface, UsbConstants.USB_DIR_OUT), null, 0, |
| 0); |
| assertEquals(0, numSent); |
| |
| released = connection.releaseInterface(iface); |
| assertTrue(released); |
| |
| runAndAssertException(() -> connection.claimInterface(null, true), |
| NullPointerException.class); |
| runAndAssertException(() -> connection.claimInterface(null, false), |
| NullPointerException.class); |
| runAndAssertException(() -> connection.releaseInterface(null), NullPointerException.class); |
| } |
| |
| /** |
| * Test all input parameters to {@link UsbDeviceConnection#setConfiguration} . |
| * |
| * <p>Note: |
| * <ul> |
| * <li>The device under test only supports one configuration, hence changing configuration |
| * is not tested.</li> |
| * <li>This test sets the current configuration again. This resets the device.</li> |
| * </ul></p> |
| * |
| * @param device the device under test |
| * @param connection The connection to use |
| * @param iface An interface of the device |
| * |
| * @throws Throwable |
| */ |
| private void setConfigurationTests(@NonNull UsbDevice device, |
| @NonNull UsbDeviceConnection connection, @NonNull UsbInterface iface) throws Throwable { |
| assumeTrue(device.getConfigurationCount() == 1); |
| boolean wasSet = connection.setConfiguration(device.getConfiguration(0)); |
| assertTrue(wasSet); |
| |
| // Cannot set configuration for a device with a claimed interface |
| boolean claimed = connection.claimInterface(iface, false); |
| assertTrue(claimed); |
| wasSet = connection.setConfiguration(device.getConfiguration(0)); |
| assertFalse(wasSet); |
| boolean released = connection.releaseInterface(iface); |
| assertTrue(released); |
| |
| runAndAssertException(() -> connection.setConfiguration(null), NullPointerException.class); |
| } |
| |
| /** |
| * Test all input parameters to {@link UsbDeviceConnection#setConfiguration} . |
| * |
| * <p>Note: The interface under test only supports one settings, hence changing the setting can |
| * not be tested.</p> |
| * |
| * @param connection The connection to use |
| * @param iface The interface to test |
| * |
| * @throws Throwable |
| */ |
| private void setInterfaceTests(@NonNull UsbDeviceConnection connection, |
| @NonNull UsbInterface iface) throws Throwable { |
| boolean claimed = connection.claimInterface(iface, false); |
| assertTrue(claimed); |
| boolean wasSet = connection.setInterface(iface); |
| assertTrue(wasSet); |
| boolean released = connection.releaseInterface(iface); |
| assertTrue(released); |
| |
| // Setting the interface for an unclaimed interface automatically claims it |
| wasSet = connection.setInterface(iface); |
| assertTrue(wasSet); |
| released = connection.releaseInterface(iface); |
| assertTrue(released); |
| |
| runAndAssertException(() -> connection.setInterface(null), NullPointerException.class); |
| } |
| |
| /** |
| * Enumerate all known devices and check basic relationship between the properties. |
| */ |
| private void enumerateDevices() throws Exception { |
| Set<Integer> knownDeviceIds = new ArraySet<>(); |
| |
| for (Map.Entry<String, UsbDevice> entry : mUsbManager.getDeviceList().entrySet()) { |
| UsbDevice device = entry.getValue(); |
| |
| assertEquals(entry.getKey(), device.getDeviceName()); |
| assertNotNull(device.getDeviceName()); |
| |
| // Device ID should be unique |
| assertFalse(knownDeviceIds.contains(device.getDeviceId())); |
| knownDeviceIds.add(device.getDeviceId()); |
| |
| assertEquals(device.getDeviceName(), UsbDevice.getDeviceName(device.getDeviceId())); |
| |
| // Properties without constraints |
| device.getManufacturerName(); |
| device.getProductName(); |
| device.getVersion(); |
| device.getSerialNumber(); |
| device.getVendorId(); |
| device.getProductId(); |
| device.getDeviceClass(); |
| device.getDeviceSubclass(); |
| device.getDeviceProtocol(); |
| |
| Set<UsbInterface> interfacesFromAllConfigs = new ArraySet<>(); |
| Set<Pair<Integer, Integer>> knownInterfaceIds = new ArraySet<>(); |
| Set<Integer> knownConfigurationIds = new ArraySet<>(); |
| int numConfigurations = device.getConfigurationCount(); |
| for (int configNum = 0; configNum < numConfigurations; configNum++) { |
| UsbConfiguration config = device.getConfiguration(configNum); |
| |
| // Configuration ID should be unique |
| assertFalse(knownConfigurationIds.contains(config.getId())); |
| knownConfigurationIds.add(config.getId()); |
| |
| assertTrue(config.getMaxPower() >= 0); |
| |
| // Properties without constraints |
| config.getName(); |
| config.isSelfPowered(); |
| config.isRemoteWakeup(); |
| |
| int numInterfaces = config.getInterfaceCount(); |
| for (int interfaceNum = 0; interfaceNum < numInterfaces; interfaceNum++) { |
| UsbInterface iface = config.getInterface(interfaceNum); |
| interfacesFromAllConfigs.add(iface); |
| |
| Pair<Integer, Integer> ifaceId = new Pair<>(iface.getId(), |
| iface.getAlternateSetting()); |
| assertFalse(knownInterfaceIds.contains(ifaceId)); |
| knownInterfaceIds.add(ifaceId); |
| |
| // Properties without constraints |
| iface.getName(); |
| iface.getInterfaceClass(); |
| iface.getInterfaceSubclass(); |
| iface.getInterfaceProtocol(); |
| |
| int numEndpoints = iface.getEndpointCount(); |
| for (int endpointNum = 0; endpointNum < numEndpoints; endpointNum++) { |
| UsbEndpoint endpoint = iface.getEndpoint(endpointNum); |
| |
| assertEquals(endpoint.getAddress(), |
| endpoint.getEndpointNumber() | endpoint.getDirection()); |
| |
| assertTrue(endpoint.getDirection() == UsbConstants.USB_DIR_OUT || |
| endpoint.getDirection() == UsbConstants.USB_DIR_IN); |
| |
| assertTrue(endpoint.getType() == UsbConstants.USB_ENDPOINT_XFER_CONTROL || |
| endpoint.getType() == UsbConstants.USB_ENDPOINT_XFER_ISOC || |
| endpoint.getType() == UsbConstants.USB_ENDPOINT_XFER_BULK || |
| endpoint.getType() == UsbConstants.USB_ENDPOINT_XFER_INT); |
| |
| assertTrue(endpoint.getMaxPacketSize() >= 0); |
| assertTrue(endpoint.getInterval() >= 0); |
| |
| // Properties without constraints |
| endpoint.getAttributes(); |
| } |
| } |
| } |
| |
| int numInterfaces = device.getInterfaceCount(); |
| for (int interfaceNum = 0; interfaceNum < numInterfaces; interfaceNum++) { |
| assertTrue(interfacesFromAllConfigs.contains(device.getInterface(interfaceNum))); |
| } |
| } |
| } |
| |
| /** |
| * Run tests. |
| * |
| * @param device The device to run the test against. This device is running |
| * com.android.cts.verifierusbcompanion.DeviceTestCompanion |
| */ |
| private void runTests(@NonNull UsbDevice device) { |
| try { |
| // Find the AOAP interface |
| UsbInterface iface = null; |
| for (int i = 0; i < device.getConfigurationCount(); i++) { |
| if (device.getInterface(i).getName().equals("Android Accessory Interface")) { |
| iface = device.getInterface(i); |
| break; |
| } |
| } |
| assumeNotNull(iface); |
| |
| enumerateDevices(); |
| |
| UsbDeviceConnection connection = mUsbManager.openDevice(device); |
| assertNotNull(connection); |
| |
| claimInterfaceTests(connection, iface); |
| |
| boolean claimed = connection.claimInterface(iface, false); |
| assertTrue(claimed); |
| |
| testIfCompanionZeroTerminates(connection, iface); |
| |
| usbRequestLegacyTests(connection, iface); |
| usbRequestTests(connection, iface); |
| parallelUsbRequestsTests(connection, iface); |
| ctrlTransferTests(connection); |
| bulkTransferTests(connection, iface); |
| |
| // Signal to the DeviceTestCompanion that there are no more transfer test |
| endTesting(connection, iface); |
| boolean released = connection.releaseInterface(iface); |
| assertTrue(released); |
| |
| setInterfaceTests(connection, iface); |
| setConfigurationTests(device, connection, iface); |
| |
| assertFalse(connection.getFileDescriptor() == -1); |
| assertNotNull(connection.getRawDescriptors()); |
| assertFalse(connection.getRawDescriptors().length == 0); |
| assertEquals(device.getSerialNumber(), connection.getSerial()); |
| |
| connection.close(); |
| |
| // We should not be able to communicate with the device anymore |
| assertFalse(connection.claimInterface(iface, true)); |
| assertFalse(connection.releaseInterface(iface)); |
| assertFalse(connection.setConfiguration(device.getConfiguration(0))); |
| assertFalse(connection.setInterface(iface)); |
| assertTrue(connection.getFileDescriptor() == -1); |
| assertNull(connection.getRawDescriptors()); |
| assertNull(connection.getSerial()); |
| assertEquals(-1, connection.bulkTransfer(getEndpoint(iface, UsbConstants.USB_DIR_OUT), |
| new byte[1], 1, 0)); |
| assertEquals(-1, connection.bulkTransfer(getEndpoint(iface, UsbConstants.USB_DIR_OUT), |
| null, 0, 0)); |
| assertEquals(-1, connection.bulkTransfer(getEndpoint(iface, UsbConstants.USB_DIR_IN), |
| null, 0, 0)); |
| assertFalse((new UsbRequest()).initialize(connection, getEndpoint(iface, |
| UsbConstants.USB_DIR_IN))); |
| |
| // Double close should do no harm |
| connection.close(); |
| |
| setTestResultAndFinish(true); |
| } catch (AssumptionViolatedException e) { |
| // Assumptions failing means that somehow the device/connection is set up incorrectly |
| Toast.makeText(this, getString(R.string.usb_device_unexpected, e.getLocalizedMessage()), |
| Toast.LENGTH_LONG).show(); |
| } catch (Throwable e) { |
| fail(null, e); |
| } |
| } |
| |
| @Override |
| protected void onDestroy() { |
| if (mUsbDeviceConnectionReceiver != null) { |
| unregisterReceiver(mUsbDeviceConnectionReceiver); |
| } |
| |
| super.onDestroy(); |
| } |
| } |