summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/api/current.txt2
-rw-r--r--core/java/android/hardware/usb/UsbManager.java269
-rw-r--r--core/java/android/hardware/usb/flags/usb_framework_flags.aconfig8
-rw-r--r--tests/UsbManagerTests/lib/src/com/android/server/usblib/UsbManagerTestLib.java108
-rw-r--r--tests/UsbManagerTests/src/com/android/server/usbtest/UsbManagerApiTest.java32
5 files changed, 412 insertions, 7 deletions
diff --git a/core/api/current.txt b/core/api/current.txt
index fda66fa65e7e..395c00dea079 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -20914,6 +20914,8 @@ package android.hardware.usb {
method public boolean hasPermission(android.hardware.usb.UsbDevice);
method public boolean hasPermission(android.hardware.usb.UsbAccessory);
method public android.os.ParcelFileDescriptor openAccessory(android.hardware.usb.UsbAccessory);
+ method @FlaggedApi("android.hardware.usb.flags.enable_accessory_stream_api") @NonNull public java.io.InputStream openAccessoryInputStream(@NonNull android.hardware.usb.UsbAccessory);
+ method @FlaggedApi("android.hardware.usb.flags.enable_accessory_stream_api") @NonNull public java.io.OutputStream openAccessoryOutputStream(@NonNull android.hardware.usb.UsbAccessory);
method public android.hardware.usb.UsbDeviceConnection openDevice(android.hardware.usb.UsbDevice);
method public void requestPermission(android.hardware.usb.UsbDevice, android.app.PendingIntent);
method public void requestPermission(android.hardware.usb.UsbAccessory, android.app.PendingIntent);
diff --git a/core/java/android/hardware/usb/UsbManager.java b/core/java/android/hardware/usb/UsbManager.java
index 92608d048135..d2e232a94622 100644
--- a/core/java/android/hardware/usb/UsbManager.java
+++ b/core/java/android/hardware/usb/UsbManager.java
@@ -54,6 +54,11 @@ import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
@@ -823,6 +828,216 @@ public class UsbManager {
}
}
+ /**
+ * Opens the handle for accessory, marks it as input or output, and adds it to the map
+ * if it is the first time the accessory has had an I/O stream associated with it.
+ */
+ private AccessoryHandle openHandleForAccessory(UsbAccessory accessory,
+ boolean openingInputStream)
+ throws RemoteException {
+ synchronized (mAccessoryHandleMapLock) {
+ if (mAccessoryHandleMap == null) {
+ mAccessoryHandleMap = new ArrayMap<>();
+ }
+
+ // If accessory isn't available in map
+ if (!mAccessoryHandleMap.containsKey(accessory)) {
+ // open accessory and store associated AccessoryHandle in map
+ ParcelFileDescriptor pfd = mService.openAccessory(accessory);
+ AccessoryHandle newHandle = new AccessoryHandle(pfd, openingInputStream,
+ !openingInputStream);
+ mAccessoryHandleMap.put(accessory, newHandle);
+
+ return newHandle;
+ }
+
+ // if accessory is already in map, get modified handle
+ AccessoryHandle currentHandle = mAccessoryHandleMap.get(accessory);
+ if (currentHandle == null) {
+ throw new IllegalStateException("Accessory doesn't have an associated handle yet!");
+ }
+
+ AccessoryHandle modifiedHandle = getModifiedHandleForOpeningStream(
+ openingInputStream, currentHandle);
+
+ mAccessoryHandleMap.put(accessory, modifiedHandle);
+
+ return modifiedHandle;
+ }
+ }
+
+ private AccessoryHandle getModifiedHandleForOpeningStream(boolean openingInputStream,
+ @NonNull AccessoryHandle currentHandle) {
+ if (currentHandle.isInputStreamOpened() && openingInputStream) {
+ throw new IllegalStateException("Input stream already open for this accessory! "
+ + "Please close the existing input stream before opening a new one.");
+ }
+
+ if (currentHandle.isOutputStreamOpened() && !openingInputStream) {
+ throw new IllegalStateException("Output stream already open for this accessory! "
+ + "Please close the existing output stream before opening a new one.");
+ }
+
+ boolean isInputStreamOpened = openingInputStream || currentHandle.isInputStreamOpened();
+ boolean isOutputStreamOpened = !openingInputStream || currentHandle.isOutputStreamOpened();
+
+ return new AccessoryHandle(
+ currentHandle.getPfd(), isInputStreamOpened, isOutputStreamOpened);
+ }
+
+ /**
+ * Marks the handle for the given accessory closed for input or output, and closes the handle
+ * and removes it from the map if there are no more I/O streams associated with the handle.
+ */
+ private void closeHandleForAccessory(UsbAccessory accessory, boolean closingInputStream)
+ throws IOException {
+ synchronized (mAccessoryHandleMapLock) {
+ AccessoryHandle currentHandle = mAccessoryHandleMap.get(accessory);
+
+ if (currentHandle == null) {
+ throw new IllegalStateException(
+ "No handle has been initialised for this accessory!");
+ }
+
+ AccessoryHandle modifiedHandle = getModifiedHandleForClosingStream(
+ closingInputStream, currentHandle);
+ if (!modifiedHandle.isOpen()) {
+ //close handle and remove accessory handle pair from map
+ modifiedHandle.getPfd().close();
+ mAccessoryHandleMap.remove(accessory);
+ } else {
+ mAccessoryHandleMap.put(accessory, modifiedHandle);
+ }
+ }
+ }
+
+ private AccessoryHandle getModifiedHandleForClosingStream(boolean closingInputStream,
+ @NonNull AccessoryHandle currentHandle) {
+ if (!currentHandle.isInputStreamOpened() && closingInputStream) {
+ throw new IllegalStateException(
+ "Attempting to close an input stream that has not been opened "
+ + "for this accessory!");
+ }
+
+ if (!currentHandle.isOutputStreamOpened() && !closingInputStream) {
+ throw new IllegalStateException(
+ "Attempting to close an output stream that has not been opened "
+ + "for this accessory!");
+ }
+
+ boolean isInputStreamOpened = !closingInputStream && currentHandle.isInputStreamOpened();
+ boolean isOutputStreamOpened = closingInputStream && currentHandle.isOutputStreamOpened();
+
+ return new AccessoryHandle(
+ currentHandle.getPfd(), isInputStreamOpened, isOutputStreamOpened);
+ }
+
+ /**
+ * An InputStream you can create on a UsbAccessory, which will
+ * take care of calling {@link ParcelFileDescriptor#close
+ * ParcelFileDescriptor.close()} for you when the stream is closed.
+ */
+ private class AccessoryAutoCloseInputStream extends FileInputStream {
+
+ private final ParcelFileDescriptor mPfd;
+ private final UsbAccessory mAccessory;
+
+ AccessoryAutoCloseInputStream(UsbAccessory accessory, ParcelFileDescriptor pfd) {
+ super(pfd.getFileDescriptor());
+ this.mAccessory = accessory;
+ this.mPfd = pfd;
+ }
+
+ @Override
+ public void close() throws IOException {
+ /* TODO(b/377850642) : Ensure the stream is closed even if client does not
+ explicitly close the stream to avoid corrupt FDs*/
+ super.close();
+ closeHandleForAccessory(mAccessory, true);
+ }
+
+
+ @Override
+ public int read() throws IOException {
+ final int result = super.read();
+ checkError(result);
+ return result;
+ }
+
+ @Override
+ public int read(byte[] b) throws IOException {
+ final int result = super.read(b);
+ checkError(result);
+ return result;
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException {
+ final int result = super.read(b, off, len);
+ checkError(result);
+ return result;
+ }
+
+ private void checkError(int result) throws IOException {
+ if (result == -1 && mPfd.canDetectErrors()) {
+ mPfd.checkError();
+ }
+ }
+ }
+
+ /**
+ * An OutputStream you can create on a UsbAccessory, which will
+ * take care of calling {@link ParcelFileDescriptor#close
+ * ParcelFileDescriptor.close()} for you when the stream is closed.
+ */
+ private class AccessoryAutoCloseOutputStream extends FileOutputStream {
+ private final UsbAccessory mAccessory;
+
+ AccessoryAutoCloseOutputStream(UsbAccessory accessory, ParcelFileDescriptor pfd) {
+ super(pfd.getFileDescriptor());
+ mAccessory = accessory;
+ }
+
+ @Override
+ public void close() throws IOException {
+ /* TODO(b/377850642) : Ensure the stream is closed even if client does not
+ explicitly close the stream to avoid corrupt FDs*/
+ super.close();
+ closeHandleForAccessory(mAccessory, false);
+ }
+ }
+
+ /**
+ * Holds file descriptor and marks whether input and output streams have been opened for it.
+ */
+ private static class AccessoryHandle {
+ private final ParcelFileDescriptor mPfd;
+ private final boolean mInputStreamOpened;
+ private final boolean mOutputStreamOpened;
+ AccessoryHandle(ParcelFileDescriptor parcelFileDescriptor,
+ boolean inputStreamOpened, boolean outputStreamOpened) {
+ mPfd = parcelFileDescriptor;
+ mInputStreamOpened = inputStreamOpened;
+ mOutputStreamOpened = outputStreamOpened;
+ }
+
+ public ParcelFileDescriptor getPfd() {
+ return mPfd;
+ }
+
+ public boolean isInputStreamOpened() {
+ return mInputStreamOpened;
+ }
+
+ public boolean isOutputStreamOpened() {
+ return mOutputStreamOpened;
+ }
+
+ public boolean isOpen() {
+ return (mInputStreamOpened || mOutputStreamOpened);
+ }
+ }
+
private final Context mContext;
private final IUsbManager mService;
private final Object mDisplayPortListenersLock = new Object();
@@ -831,6 +1046,11 @@ public class UsbManager {
@GuardedBy("mDisplayPortListenersLock")
private DisplayPortAltModeInfoDispatchingListener mDisplayPortServiceListener;
+ private final Object mAccessoryHandleMapLock = new Object();
+ @GuardedBy("mAccessoryHandleMapLock")
+ private ArrayMap<UsbAccessory, AccessoryHandle> mAccessoryHandleMap;
+
+
/**
* @hide
*/
@@ -922,6 +1142,10 @@ public class UsbManager {
* data of a USB transfer should be read at once. If only a partial request is read the rest of
* the transfer is dropped.
*
+ * <p>It is strongly recommended to use newer methods instead of this method,
+ * since this method may provide sub-optimal performance on some devices.
+ * This method could potentially face interim performance degradation as well.
+ *
* @param accessory the USB accessory to open
* @return file descriptor, or null if the accessory could not be opened.
*/
@@ -935,6 +1159,49 @@ public class UsbManager {
}
/**
+ * Opens an input stream for reading from the USB accessory.
+ * If accessory is not open at this point, accessory will first be opened.
+ * <p>If data is read from the created {@link java.io.InputStream} all
+ * data of a USB transfer should be read at once. If only a partial request is read, the rest of
+ * the transfer is dropped.
+ * <p>The caller is responsible for ensuring that the returned stream is closed.
+ *
+ * @param accessory the USB accessory to open an input stream for
+ * @return input stream to read from given USB accessory
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_ACCESSORY_STREAM_API)
+ @RequiresFeature(PackageManager.FEATURE_USB_ACCESSORY)
+ public @NonNull InputStream openAccessoryInputStream(@NonNull UsbAccessory accessory) {
+ try {
+ return new AccessoryAutoCloseInputStream(accessory,
+ openHandleForAccessory(accessory, true).getPfd());
+
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Opens an output stream for writing to the USB accessory.
+ * If accessory is not open at this point, accessory will first be opened.
+ * <p>The caller is responsible for ensuring that the returned stream is closed.
+ *
+ * @param accessory the USB accessory to open an output stream for
+ * @return output stream to write to given USB accessory
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_ACCESSORY_STREAM_API)
+ @RequiresFeature(PackageManager.FEATURE_USB_ACCESSORY)
+ public @NonNull OutputStream openAccessoryOutputStream(@NonNull UsbAccessory accessory) {
+ try {
+ return new AccessoryAutoCloseOutputStream(accessory,
+ openHandleForAccessory(accessory, false).getPfd());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+
+ }
+
+ /**
* Gets the functionfs control file descriptor for the given function, with
* the usb descriptors and strings already written. The file descriptor is used
* by the function implementation to handle events and control requests.
@@ -1293,7 +1560,7 @@ public class UsbManager {
* <p>
* This function returns the current USB bandwidth through USB Gadget HAL.
* It should be used when Android device is in USB peripheral mode and
- * connects to a USB host. If USB state is not configued, API will return
+ * connects to a USB host. If USB state is not configured, API will return
* {@value #USB_DATA_TRANSFER_RATE_UNKNOWN}. In addition, the unit of the
* return value is Mbps.
* </p>
diff --git a/core/java/android/hardware/usb/flags/usb_framework_flags.aconfig b/core/java/android/hardware/usb/flags/usb_framework_flags.aconfig
index 3b7a9e95c521..b719a7c6daac 100644
--- a/core/java/android/hardware/usb/flags/usb_framework_flags.aconfig
+++ b/core/java/android/hardware/usb/flags/usb_framework_flags.aconfig
@@ -31,3 +31,11 @@ flag {
description: "Feature flag to enable exposing usb speed system api"
bug: "373653182"
}
+
+flag {
+ name: "enable_accessory_stream_api"
+ is_exported: true
+ namespace: "usb"
+ description: "Feature flag to enable stream APIs for Accessory mode"
+ bug: "369356693"
+}
diff --git a/tests/UsbManagerTests/lib/src/com/android/server/usblib/UsbManagerTestLib.java b/tests/UsbManagerTests/lib/src/com/android/server/usblib/UsbManagerTestLib.java
index e2099e652c49..635e5de935c7 100644
--- a/tests/UsbManagerTests/lib/src/com/android/server/usblib/UsbManagerTestLib.java
+++ b/tests/UsbManagerTests/lib/src/com/android/server/usblib/UsbManagerTestLib.java
@@ -18,19 +18,27 @@ package com.android.server.usblib;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
+import android.hardware.usb.UsbAccessory;
import android.hardware.usb.UsbManager;
+import android.hardware.usb.flags.Flags;
import android.os.Binder;
+import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.util.Log;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
import java.util.concurrent.atomic.AtomicInteger;
/**
@@ -43,13 +51,36 @@ public class UsbManagerTestLib {
private UsbManager mUsbManagerSys;
private UsbManager mUsbManagerMock;
- @Mock private android.hardware.usb.IUsbManager mMockUsbService;
+ @Mock
+ private android.hardware.usb.IUsbManager mMockUsbService;
+ private TestParcelFileDescriptor mTestParcelFileDescriptor = new TestParcelFileDescriptor(
+ new ParcelFileDescriptor(new FileDescriptor()));
+ @Mock
+ private UsbAccessory mMockUsbAccessory;
/**
* Counter for tracking UsbOperation operations.
*/
private static final AtomicInteger sUsbOperationCount = new AtomicInteger();
+ private class TestParcelFileDescriptor extends ParcelFileDescriptor {
+
+ private final AtomicInteger mCloseCount = new AtomicInteger();
+
+ TestParcelFileDescriptor(ParcelFileDescriptor wrapped) {
+ super(wrapped);
+ }
+
+ @Override
+ public void close() {
+ int unused = mCloseCount.incrementAndGet();
+ }
+
+ public void clearCloseCount() {
+ mCloseCount.set(0);
+ }
+ }
+
public UsbManagerTestLib(Context context) {
MockitoAnnotations.initMocks(this);
mContext = context;
@@ -74,6 +105,34 @@ public class UsbManagerTestLib {
mUsbManagerSys.setCurrentFunctions(functions);
}
+ private InputStream openAccessoryInputStream(UsbAccessory accessory) {
+ try {
+ when(mMockUsbService.openAccessory(accessory)).thenReturn(mTestParcelFileDescriptor);
+ } catch (RemoteException remEx) {
+ Log.w(TAG, "RemoteException");
+ }
+
+ if (Flags.enableAccessoryStreamApi()) {
+ return mUsbManagerMock.openAccessoryInputStream(accessory);
+ }
+
+ throw new UnsupportedOperationException("Stream APIs not available");
+ }
+
+ private OutputStream openAccessoryOutputStream(UsbAccessory accessory) {
+ try {
+ when(mMockUsbService.openAccessory(accessory)).thenReturn(mTestParcelFileDescriptor);
+ } catch (RemoteException remEx) {
+ Log.w(TAG, "RemoteException");
+ }
+
+ if (Flags.enableAccessoryStreamApi()) {
+ return mUsbManagerMock.openAccessoryOutputStream(accessory);
+ }
+
+ throw new UnsupportedOperationException("Stream APIs not available");
+ }
+
private void testSetGetCurrentFunctions_Matched(long functions) {
setCurrentFunctions(functions);
assertEquals("CurrentFunctions mismatched: ", functions, getCurrentFunctions());
@@ -94,7 +153,7 @@ public class UsbManagerTestLib {
try {
setCurrentFunctions(functions);
- verify(mMockUsbService).setCurrentFunctions(eq(functions), operationId);
+ verify(mMockUsbService).setCurrentFunctions(eq(functions), eq(operationId));
} catch (RemoteException remEx) {
Log.w(TAG, "RemoteException");
}
@@ -118,7 +177,7 @@ public class UsbManagerTestLib {
int operationId = sUsbOperationCount.incrementAndGet() + Binder.getCallingUid();
setCurrentFunctions(functions);
- verify(mMockUsbService).setCurrentFunctions(eq(functions), operationId);
+ verify(mMockUsbService).setCurrentFunctions(eq(functions), eq(operationId));
}
public void testGetCurrentFunctions_shouldMatched() {
@@ -138,4 +197,47 @@ public class UsbManagerTestLib {
testSetCurrentFunctionsMock_Matched(UsbManager.FUNCTION_RNDIS);
testSetCurrentFunctionsMock_Matched(UsbManager.FUNCTION_NCM);
}
+
+ public void testParcelFileDescriptorClosedWhenAllOpenStreamsAreClosed() {
+ mTestParcelFileDescriptor.clearCloseCount();
+ try {
+ try (InputStream ignored = openAccessoryInputStream(mMockUsbAccessory)) {
+ //noinspection EmptyTryBlock
+ try (OutputStream ignored2 = openAccessoryOutputStream(mMockUsbAccessory)) {
+ // do nothing
+ }
+ }
+
+ // ParcelFileDescriptor is closed only once.
+ assertEquals(mTestParcelFileDescriptor.mCloseCount.get(), 1);
+ mTestParcelFileDescriptor.clearCloseCount();
+ } catch (IOException e) {
+ // do nothing
+ }
+ }
+
+ public void testOnlyOneOpenInputStreamAllowed() {
+ try {
+ //noinspection EmptyTryBlock
+ try (InputStream ignored = openAccessoryInputStream(mMockUsbAccessory)) {
+ assertThrows(IllegalStateException.class,
+ () -> openAccessoryInputStream(mMockUsbAccessory));
+ }
+ } catch (IOException e) {
+ // do nothing
+ }
+ }
+
+ public void testOnlyOneOpenOutputStreamAllowed() {
+ try {
+ //noinspection EmptyTryBlock
+ try (OutputStream ignored = openAccessoryOutputStream(mMockUsbAccessory)) {
+ assertThrows(IllegalStateException.class,
+ () -> openAccessoryOutputStream(mMockUsbAccessory));
+ }
+ } catch (IOException e) {
+ // do nothing
+ }
+ }
+
}
diff --git a/tests/UsbManagerTests/src/com/android/server/usbtest/UsbManagerApiTest.java b/tests/UsbManagerTests/src/com/android/server/usbtest/UsbManagerApiTest.java
index 8b21763b4a24..40fd0b431451 100644
--- a/tests/UsbManagerTests/src/com/android/server/usbtest/UsbManagerApiTest.java
+++ b/tests/UsbManagerTests/src/com/android/server/usbtest/UsbManagerApiTest.java
@@ -18,17 +18,21 @@ package com.android.server.usbtest;
import android.content.Context;
import android.hardware.usb.UsbManager;
+import android.hardware.usb.flags.Flags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
-import org.junit.Ignore;
+import com.android.server.usblib.UsbManagerTestLib;
+
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
-import com.android.server.usblib.UsbManagerTestLib;
-
/**
* Unit tests for {@link android.hardware.usb.UsbManager}.
* Note: MUST claimed MANAGE_USB permission in Manifest
@@ -41,6 +45,9 @@ public class UsbManagerApiTest {
private final UsbManagerTestLib mUsbManagerTestLib =
new UsbManagerTestLib(mContext = InstrumentationRegistry.getContext());
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule =
+ DeviceFlagsValueProvider.createCheckFlagsRule();
/**
* Verify NO SecurityException
* Go through System Server
@@ -92,4 +99,23 @@ public class UsbManagerApiTest {
public void testUsbApi_SetCurrentFunctions_shouldMatched() {
mUsbManagerTestLib.testSetCurrentFunctions_shouldMatched();
}
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_ACCESSORY_STREAM_API)
+ public void testUsbApi_closesParcelFileDescriptorAfterAllStreamsClosed() {
+ mUsbManagerTestLib.testParcelFileDescriptorClosedWhenAllOpenStreamsAreClosed();
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_ACCESSORY_STREAM_API)
+ public void testUsbApi_callingOpenAccessoryInputStreamTwiceThrowsException() {
+ mUsbManagerTestLib.testOnlyOneOpenInputStreamAllowed();
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_ACCESSORY_STREAM_API)
+ public void testUsbApi_callingOpenAccessoryOutputStreamTwiceThrowsException() {
+ mUsbManagerTestLib.testOnlyOneOpenOutputStreamAllowed();
+ }
+
}