diff options
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(); + } + } |