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

Commit a82202a5 authored by Daniel Norman's avatar Daniel Norman Committed by Android (Google) Code Review
Browse files

Merge "Adds more test coverage for BrailleDisplay[Controller|Connection]." into main

parents ce17e6a1 3ae93a50
Loading
Loading
Loading
Loading
+4 −1
Original line number Diff line number Diff line
@@ -69,6 +69,7 @@ import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
import android.view.accessibility.AccessibilityWindowInfo;
import android.view.inputmethod.EditorInfo;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.inputmethod.CancellationGroup;
import com.android.internal.inputmethod.IAccessibilityInputMethodSession;
import com.android.internal.inputmethod.IAccessibilityInputMethodSessionCallback;
@@ -1388,7 +1389,9 @@ public abstract class AccessibilityService extends Service {
        getFingerprintGestureController().onGesture(gesture);
    }

    int getConnectionId() {
    /** @hide */
    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
    public int getConnectionId() {
        return mConnectionId;
    }

+107 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.accessibilityservice;

import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;

import android.hardware.usb.UsbDevice;
import android.platform.test.annotations.RequiresFlagsEnabled;
import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityInteractionClient;
import android.view.accessibility.Flags;

import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;

import java.util.concurrent.Executor;

/**
 * Tests for internal details of BrailleDisplayControllerImpl.
 *
 * <p>Prefer adding new tests in CTS where possible.
 */
@SmallTest
@RunWith(AndroidJUnit4.class)
@RequiresFlagsEnabled(Flags.FLAG_BRAILLE_DISPLAY_HID)
public class BrailleDisplayControllerImplTest {
    private static final int TEST_SERVICE_CONNECTION_ID = 123;

    @Rule
    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();

    private BrailleDisplayController mBrailleDisplayController;

    @Mock
    private IAccessibilityServiceConnection mAccessibilityServiceConnection;
    @Mock
    private BrailleDisplayController.BrailleDisplayCallback mBrailleDisplayCallback;

    public static class TestAccessibilityService extends AccessibilityService {
        public void onAccessibilityEvent(AccessibilityEvent event) {
        }

        public void onInterrupt() {
        }
    }

    @Before
    public void test() {
        MockitoAnnotations.initMocks(this);
        AccessibilityService accessibilityService = spy(new TestAccessibilityService());
        doReturn((Executor) Runnable::run).when(accessibilityService).getMainExecutor();
        doReturn(TEST_SERVICE_CONNECTION_ID).when(accessibilityService).getConnectionId();
        AccessibilityInteractionClient.addConnection(TEST_SERVICE_CONNECTION_ID,
                mAccessibilityServiceConnection, /*initializeCache=*/false);
        mBrailleDisplayController = accessibilityService.getBrailleDisplayController();
    }

    // Automated CTS tests only use the BluetoothDevice version of BrailleDisplayController#connect
    // because fake UsbDevice objects cannot be created in CTS. This internal test can mock the
    // UsbDevice object and at least validate that the correct system_server AIDL call is made.
    @Test
    public void connect_withUsbDevice_callsConnectUsbBrailleDisplay() throws Exception {
        UsbDevice usbDevice = Mockito.mock(UsbDevice.class);

        mBrailleDisplayController.connect(usbDevice, mBrailleDisplayCallback);

        verify(mAccessibilityServiceConnection).connectUsbBrailleDisplay(eq(usbDevice), any());
    }

    @Test
    public void connect_serviceNotConnected_throwsIllegalStateException() {
        AccessibilityInteractionClient.removeConnection(TEST_SERVICE_CONNECTION_ID);
        UsbDevice usbDevice = Mockito.mock(UsbDevice.class);

        assertThrows(IllegalStateException.class,
                () -> mBrailleDisplayController.connect(usbDevice, mBrailleDisplayCallback));
    }
}
+49 −29
Original line number Diff line number Diff line
@@ -42,7 +42,6 @@ import com.android.internal.annotations.VisibleForTesting;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -112,7 +111,7 @@ class BrailleDisplayConnection extends IBrailleDisplayConnection.Stub {
    BrailleDisplayConnection(@NonNull Object lock,
            @NonNull AccessibilityServiceConnection serviceConnection) {
        this.mLock = Objects.requireNonNull(lock);
        this.mScanner = getDefaultNativeScanner(getDefaultNativeInterface());
        this.mScanner = getDefaultNativeScanner(new DefaultNativeInterface());
        this.mServiceConnection = Objects.requireNonNull(serviceConnection);
    }

@@ -128,7 +127,7 @@ class BrailleDisplayConnection extends IBrailleDisplayConnection.Stub {
     */
    @VisibleForTesting
    interface BrailleDisplayScanner {
        Collection<Path> getHidrawNodePaths();
        Collection<Path> getHidrawNodePaths(@NonNull Path directory);

        byte[] getDeviceReportDescriptor(@NonNull Path path);

@@ -158,8 +157,9 @@ class BrailleDisplayConnection extends IBrailleDisplayConnection.Stub {
        Objects.requireNonNull(expectedUniqueId);
        this.mController = Objects.requireNonNull(controller);

        final Path devicePath = Path.of("/dev");
        final List<Pair<File, byte[]>> result = new ArrayList<>();
        final Collection<Path> hidrawNodePaths = mScanner.getHidrawNodePaths();
        final Collection<Path> hidrawNodePaths = mScanner.getHidrawNodePaths(devicePath);
        if (hidrawNodePaths == null) {
            Slog.w(LOG_TAG, "Unable to access the HIDRAW node directory");
            sendConnectionErrorLocked(FLAG_ERROR_CANNOT_ACCESS);
@@ -285,6 +285,9 @@ class BrailleDisplayConnection extends IBrailleDisplayConnection.Stub {
        if (buffer.length > IBinder.getSuggestedMaxIpcSizeBytes()) {
            Slog.e(LOG_TAG, "Requested write of size " + buffer.length
                    + " which is larger than maximum " + IBinder.getSuggestedMaxIpcSizeBytes());
            // The caller only got here by bypassing the AccessibilityService-side check with
            // reflection, so disconnect this connection to prevent further attempts.
            disconnect();
            return;
        }
        synchronized (mLock) {
@@ -292,7 +295,7 @@ class BrailleDisplayConnection extends IBrailleDisplayConnection.Stub {
            if (mOutputThread == null) {
                try {
                    mOutputStream = new FileOutputStream(mHidrawNode);
                } catch (FileNotFoundException e) {
                } catch (Exception e) {
                    Slog.e(LOG_TAG, "Unable to create write stream", e);
                    disconnect();
                    return;
@@ -387,14 +390,13 @@ class BrailleDisplayConnection extends IBrailleDisplayConnection.Stub {
    BrailleDisplayScanner getDefaultNativeScanner(@NonNull NativeInterface nativeInterface) {
        Objects.requireNonNull(nativeInterface);
        return new BrailleDisplayScanner() {
            private static final Path DEVICE_DIR = Path.of("/dev");
            private static final String HIDRAW_DEVICE_GLOB = "hidraw*";

            @Override
            public Collection<Path> getHidrawNodePaths() {
            public Collection<Path> getHidrawNodePaths(@NonNull Path directory) {
                final List<Path> result = new ArrayList<>();
                try (DirectoryStream<Path> hidrawNodePaths = Files.newDirectoryStream(
                        DEVICE_DIR, HIDRAW_DEVICE_GLOB)) {
                        directory, HIDRAW_DEVICE_GLOB)) {
                    for (Path path : hidrawNodePaths) {
                        result.add(path);
                    }
@@ -458,8 +460,8 @@ class BrailleDisplayConnection extends IBrailleDisplayConnection.Stub {
        synchronized (mLock) {
            mScanner = new BrailleDisplayScanner() {
                @Override
                public Collection<Path> getHidrawNodePaths() {
                    return brailleDisplayMap.keySet();
                public Collection<Path> getHidrawNodePaths(@NonNull Path directory) {
                    return brailleDisplayMap.isEmpty() ? null : brailleDisplayMap.keySet();
                }

                @Override
@@ -490,18 +492,37 @@ class BrailleDisplayConnection extends IBrailleDisplayConnection.Stub {
     */
    @VisibleForTesting
    interface NativeInterface {
        /**
         * Returns the HIDRAW descriptor size for the file descriptor.
         *
         * @return the result of ioctl(HIDIOCGRDESCSIZE), or -1 if the ioctl fails.
         */
        int getHidrawDescSize(int fd);

        /**
         * Returns the HIDRAW descriptor for the file descriptor.
         *
         * @return the result of ioctl(HIDIOCGRDESC), or null if the ioctl fails.
         */
        byte[] getHidrawDesc(int fd, int descSize);

        /**
         * Returns the HIDRAW unique identifier for the file descriptor.
         *
         * @return the result of ioctl(HIDIOCGRAWUNIQ), or null if the ioctl fails.
         */
        String getHidrawUniq(int fd);

        /**
         * Returns the HIDRAW bus type for the file descriptor.
         *
         * @return the result of ioctl(HIDIOCGRAWINFO).bustype, or -1 if the ioctl fails.
         */
        int getHidrawBusType(int fd);
    }

    /** Native interface that actually calls native HIDRAW ioctls. */
    private NativeInterface getDefaultNativeInterface() {
        return new NativeInterface() {
    private class DefaultNativeInterface implements NativeInterface {
        @Override
        public int getHidrawDescSize(int fd) {
            return nativeGetHidrawDescSize(fd);
@@ -521,7 +542,6 @@ class BrailleDisplayConnection extends IBrailleDisplayConnection.Stub {
        public int getHidrawBusType(int fd) {
            return nativeGetHidrawBusType(fd);
        }
        };
    }

    private native int nativeGetHidrawDescSize(int fd);
+77 −2
Original line number Diff line number Diff line
@@ -18,22 +18,31 @@ package com.android.server.accessibility;

import static com.google.common.truth.Truth.assertThat;

import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.accessibilityservice.BrailleDisplayController;
import android.content.Context;
import android.os.Bundle;
import android.os.IBinder;
import android.testing.DexmakerShareClassLoaderRule;

import androidx.test.platform.app.InstrumentationRegistry;

import com.google.common.truth.Expect;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;

import java.io.File;
import java.nio.file.Path;
import java.util.List;

@@ -54,6 +63,8 @@ public class BrailleDisplayConnectionTest {
    @Rule
    public final Expect expect = Expect.create();

    private Context mContext;

    // To mock package-private class
    @Rule
    public final DexmakerShareClassLoaderRule mDexmakerShareClassLoaderRule =
@@ -62,7 +73,34 @@ public class BrailleDisplayConnectionTest {
    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);
        mBrailleDisplayConnection = new BrailleDisplayConnection(new Object(), mServiceConnection);
        mContext = InstrumentationRegistry.getInstrumentation().getContext();
        when(mServiceConnection.isConnectedLocked()).thenReturn(true);
        mBrailleDisplayConnection =
                spy(new BrailleDisplayConnection(new Object(), mServiceConnection));
    }

    @Test
    public void defaultNativeScanner_getHidrawNodePaths_returnsHidrawPaths() throws Exception {
        File testDir = mContext.getFilesDir();
        Path hidrawNode0 = Path.of(testDir.getPath(), "hidraw0");
        Path hidrawNode1 = Path.of(testDir.getPath(), "hidraw1");
        Path otherDevice = Path.of(testDir.getPath(), "otherDevice");
        Path[] nodePaths = {hidrawNode0, hidrawNode1, otherDevice};
        try {
            for (Path node : nodePaths) {
                assertThat(node.toFile().createNewFile()).isTrue();
            }

            BrailleDisplayConnection.BrailleDisplayScanner scanner =
                    mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);

            assertThat(scanner.getHidrawNodePaths(testDir.toPath()))
                    .containsExactly(hidrawNode0, hidrawNode1);
        } finally {
            for (Path node : nodePaths) {
                node.toFile().delete();
            }
        }
    }

    @Test
@@ -123,9 +161,38 @@ public class BrailleDisplayConnectionTest {
                .isEqualTo(BrailleDisplayConnection.BUS_BLUETOOTH);
    }

    @Test
    public void write_bypassesServiceSideCheckWithLargeBuffer_disconnects() {
        Mockito.doNothing().when(mBrailleDisplayConnection).disconnect();
        mBrailleDisplayConnection.write(
                new byte[IBinder.getSuggestedMaxIpcSizeBytes() * 2]);

        verify(mBrailleDisplayConnection).disconnect();
    }

    @Test
    public void write_notConnected_throwsIllegalStateException() {
        when(mServiceConnection.isConnectedLocked()).thenReturn(false);

        assertThrows(IllegalStateException.class,
                () -> mBrailleDisplayConnection.write(new byte[1]));
    }

    @Test
    public void write_unableToCreateWriteStream_disconnects() {
        Mockito.doNothing().when(mBrailleDisplayConnection).disconnect();
        // mBrailleDisplayConnection#connectLocked was never called so the
        // connection's mHidrawNode is still null. This will throw an exception
        // when attempting to create FileOutputStream on the node.
        mBrailleDisplayConnection.write(new byte[1]);

        verify(mBrailleDisplayConnection).disconnect();
    }

    // BrailleDisplayConnection#setTestData() is used to enable CTS testing with
    // test Braille display data, but its own implementation should also be tested
    // so that issues in this helper don't cause confusing failures in CTS.

    @Test
    public void setTestData_scannerReturnsTestData() {
        Bundle bd1 = new Bundle(), bd2 = new Bundle();
@@ -148,7 +215,7 @@ public class BrailleDisplayConnectionTest {
        BrailleDisplayConnection.BrailleDisplayScanner scanner =
                mBrailleDisplayConnection.setTestData(List.of(bd1, bd2));

        expect.that(scanner.getHidrawNodePaths()).containsExactly(path1, path2);
        expect.that(scanner.getHidrawNodePaths(Path.of("/dev"))).containsExactly(path1, path2);
        expect.that(scanner.getDeviceReportDescriptor(path1)).isEqualTo(desc1);
        expect.that(scanner.getDeviceReportDescriptor(path2)).isEqualTo(desc2);
        expect.that(scanner.getUniqueId(path1)).isEqualTo(uniq1);
@@ -156,4 +223,12 @@ public class BrailleDisplayConnectionTest {
        expect.that(scanner.getDeviceBusType(path1)).isEqualTo(bus1);
        expect.that(scanner.getDeviceBusType(path2)).isEqualTo(bus2);
    }

    @Test
    public void setTestData_emptyTestData_returnsNullNodePaths() {
        BrailleDisplayConnection.BrailleDisplayScanner scanner =
                mBrailleDisplayConnection.setTestData(List.of());

        expect.that(scanner.getHidrawNodePaths(Path.of("/dev"))).isNull();
    }
}