Loading core/api/current.txt +2 −0 Original line number Diff line number Diff line Loading @@ -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); core/java/android/hardware/usb/UsbManager.java +268 −1 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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(); Loading @@ -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 */ Loading Loading @@ -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. */ Loading @@ -934,6 +1158,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 Loading Loading @@ -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> Loading core/java/android/hardware/usb/flags/usb_framework_flags.aconfig +8 −0 Original line number Diff line number Diff line Loading @@ -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" } tests/UsbManagerTests/lib/src/com/android/server/usblib/UsbManagerTestLib.java +105 −3 Original line number Diff line number Diff line Loading @@ -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; /** Loading @@ -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; Loading @@ -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()); Loading @@ -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"); } Loading @@ -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() { Loading @@ -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 } } } tests/UsbManagerTests/src/com/android/server/usbtest/UsbManagerApiTest.java +29 −3 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading Loading @@ -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(); } } Loading
core/api/current.txt +2 −0 Original line number Diff line number Diff line Loading @@ -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);
core/java/android/hardware/usb/UsbManager.java +268 −1 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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(); Loading @@ -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 */ Loading Loading @@ -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. */ Loading @@ -934,6 +1158,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 Loading Loading @@ -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> Loading
core/java/android/hardware/usb/flags/usb_framework_flags.aconfig +8 −0 Original line number Diff line number Diff line Loading @@ -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" }
tests/UsbManagerTests/lib/src/com/android/server/usblib/UsbManagerTestLib.java +105 −3 Original line number Diff line number Diff line Loading @@ -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; /** Loading @@ -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; Loading @@ -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()); Loading @@ -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"); } Loading @@ -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() { Loading @@ -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 } } }
tests/UsbManagerTests/src/com/android/server/usbtest/UsbManagerApiTest.java +29 −3 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading Loading @@ -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(); } }