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

Commit 2fdae165 authored by Febin Thattil's avatar Febin Thattil Committed by Android (Google) Code Review
Browse files

Merge "Create new APIs for accessory streams" into main

parents 759a21c2 7fd74cef
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();
    }

}