Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 7fd74cef authored by Febin Thattil's avatar Febin Thattil
Browse files

Create new APIs for accessory streams

Change-Id: I677ea32fdf7a4fd368dce5f88f38b1c6009b93cb
Bug: 369356693
Flag: android.hardware.usb.flags.enable_accessory_stream_api
Test: Manually tested using CTS Verifier
Test: Unit tests in UsbManagerApiTest
API-Coverage-Bug: 378510666
parent e5834251
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -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);
+268 −1
Original line number Diff line number Diff line
@@ -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.
     */
@@ -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
@@ -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>
+8 −0
Original line number Diff line number Diff line
@@ -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"
}
+105 −3
Original line number Diff line number Diff line
@@ -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
        }
    }

}
+29 −3
Original line number Diff line number Diff line
@@ -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();
    }

}