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

Commit d86f7e72 authored by sallyyuen's avatar sallyyuen
Browse files

Add AccessibilityDisplayProxy and register/unregister methods

An A11yDisplayProxy will be used by Exo to listen to UI changes and
perform actions on the UI.

For a Display that is shown on a ChromeBook/client device,
a privileged app like Exo, or an app with SystemAPI privileges and
the MANAGE_ACCESSIBILITY permission, should call
A11yManager.registerA11yProxy and A11yManager.unRegisterA11yProxy to
enable/disable a11y.

The app will only have visibility in the specified proxy display.

Currently only few A11yProxy methods are exposed. When the proxy is
plugged into the system via ProxyAccessibilityServiceConnection
other methods will be exposed. (See link in comments)

Bug: 241429275
Test: atest A11yProxyTest(cts), atest A11yManagerTest(unit)
Change-Id: I019732e50e6beba0af7f5898a07b44d25e03416b
parent c81b5c68
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -15842,10 +15842,17 @@ package android.view {
package android.view.accessibility {
  public abstract class AccessibilityDisplayProxy {
    ctor public AccessibilityDisplayProxy(int, @NonNull java.util.concurrent.Executor, @NonNull java.util.List<android.accessibilityservice.AccessibilityServiceInfo>);
    method public int getDisplayId();
  }
  public final class AccessibilityManager {
    method public int getAccessibilityWindowId(@Nullable android.os.IBinder);
    method @RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY) public void performAccessibilityShortcut();
    method @RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY) public boolean registerDisplayProxy(@NonNull android.view.accessibility.AccessibilityDisplayProxy);
    method @RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY) public void registerSystemAction(@NonNull android.app.RemoteAction, int);
    method @RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY) public boolean unregisterDisplayProxy(@NonNull android.view.accessibility.AccessibilityDisplayProxy);
    method @RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY) public void unregisterSystemAction(int);
  }
+181 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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.view.accessibility;

import android.accessibilityservice.AccessibilityGestureEvent;
import android.accessibilityservice.AccessibilityService;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.accessibilityservice.IAccessibilityServiceClient;
import android.accessibilityservice.MagnificationConfig;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.content.Context;
import android.graphics.Region;
import android.os.IBinder;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.inputmethod.EditorInfo;

import com.android.internal.inputmethod.IAccessibilityInputMethodSessionCallback;
import com.android.internal.inputmethod.RemoteAccessibilityInputConnection;

import java.util.List;
import java.util.concurrent.Executor;

/**
 * Allows a privileged app - an app with MANAGE_ACCESSIBILITY permission and SystemAPI access - to
 * interact with the windows in the display that this proxy represents. Proxying the default display
 * or a display that is not tracked will throw an exception. Only the real user has access to global
 * clients like SystemUI.
 *
 * <p>
 * To register and unregister a proxy, use
 * {@link AccessibilityManager#registerDisplayProxy(AccessibilityDisplayProxy)}
 * and {@link AccessibilityManager#unregisterDisplayProxy(AccessibilityDisplayProxy)}. If the app
 * that has registered the proxy dies, the system will remove the proxy.
 *
 * TODO(241429275): Complete proxy impl and add additional support (if necessary) like cache methods
 * @hide
 */
@SystemApi
public abstract class AccessibilityDisplayProxy {
    private static final String LOG_TAG = "AccessibilityDisplayProxy";
    private static final int INVALID_CONNECTION_ID = -1;

    private List<AccessibilityServiceInfo> mInstalledAndEnabledServices;
    private Executor mExecutor;
    private int mConnectionId = INVALID_CONNECTION_ID;
    private int mDisplayId;
    IAccessibilityServiceClient mServiceClient;

    /**
     * Constructs an AccessibilityDisplayProxy instance.
     * @param displayId the id of the display to proxy.
     * @param executor the executor used to execute proxy callbacks.
     * @param installedAndEnabledServices the list of infos representing the installed and
     *                                    enabled a11y services.
     */
    public AccessibilityDisplayProxy(int displayId, @NonNull Executor executor,
            @NonNull List<AccessibilityServiceInfo> installedAndEnabledServices) {
        mDisplayId = displayId;
        mExecutor = executor;
        // Typically, the context is the Service context of an accessibility service.
        // Context is used for ResolveInfo check, which a proxy won't have, IME input
        // (FLAG_INPUT_METHOD_EDITOR), which the proxy doesn't need, and tracing
        // A11yInteractionClient methods.
        // TODO(254097475): Enable tracing, potentially without exposing Context.
        mServiceClient = new IAccessibilityServiceClientImpl(null, mExecutor);
        mInstalledAndEnabledServices = installedAndEnabledServices;
    }

    /**
     * Returns the id of the display being proxy-ed.
     */
    public int getDisplayId() {
        return mDisplayId;
    }

    /**
     * An IAccessibilityServiceClient that handles interrupts and accessibility events.
     */
    private class IAccessibilityServiceClientImpl extends
            AccessibilityService.IAccessibilityServiceClientWrapper {

        IAccessibilityServiceClientImpl(Context context, Executor executor) {
            super(context, executor, new AccessibilityService.Callbacks() {
                @Override
                public void onAccessibilityEvent(AccessibilityEvent event) {
                    // TODO: call AccessiiblityProxy.onAccessibilityEvent
                }

                @Override
                public void onInterrupt() {
                    // TODO: call AccessiiblityProxy.onInterrupt
                }
                @Override
                public void onServiceConnected() {
                    // TODO: send service infos and call AccessiiblityProxy.onProxyConnected
                }
                @Override
                public void init(int connectionId, IBinder windowToken) {
                    mConnectionId = connectionId;
                }

                @Override
                public boolean onGesture(AccessibilityGestureEvent gestureInfo) {
                    return false;
                }

                @Override
                public boolean onKeyEvent(KeyEvent event) {
                    return false;
                }

                @Override
                public void onMagnificationChanged(int displayId, @NonNull Region region,
                        MagnificationConfig config) {
                }

                @Override
                public void onMotionEvent(MotionEvent event) {
                }

                @Override
                public void onTouchStateChanged(int displayId, int state) {
                }

                @Override
                public void onSoftKeyboardShowModeChanged(int showMode) {
                }

                @Override
                public void onPerformGestureResult(int sequence, boolean completedSuccessfully) {
                }

                @Override
                public void onFingerprintCapturingGesturesChanged(boolean active) {
                }

                @Override
                public void onFingerprintGesture(int gesture) {
                }

                @Override
                public void onAccessibilityButtonClicked(int displayId) {
                }

                @Override
                public void onAccessibilityButtonAvailabilityChanged(boolean available) {
                }

                @Override
                public void onSystemActionsChanged() {
                }

                @Override
                public void createImeSession(IAccessibilityInputMethodSessionCallback callback) {
                }

                @Override
                public void startInput(@Nullable RemoteAccessibilityInputConnection inputConnection,
                        @NonNull EditorInfo editorInfo, boolean restarting) {
                }
            });
        }
    }
}
+61 −0
Original line number Diff line number Diff line
@@ -1921,6 +1921,67 @@ public final class AccessibilityManager {
        }
    }

    /**
     * Registers an {@link AccessibilityDisplayProxy}, so this proxy can access UI content specific
     * to its display.
     *
     * @param proxy the {@link AccessibilityDisplayProxy} to register.
     * @return {@code true} if the proxy is successfully registered.
     *
     * @throws IllegalArgumentException if the proxy's display is not currently tracked by a11y, is
     * {@link android.view.Display#DEFAULT_DISPLAY}, is or lower than
     * {@link android.view.Display#INVALID_DISPLAY}, or is already being proxy-ed.
     *
     * @throws SecurityException if the app does not hold the
     * {@link Manifest.permission#MANAGE_ACCESSIBILITY} permission.
     *
     * @hide
     */
    @SystemApi
    @RequiresPermission(Manifest.permission.MANAGE_ACCESSIBILITY)
    public boolean registerDisplayProxy(@NonNull AccessibilityDisplayProxy proxy) {
        final IAccessibilityManager service;
        synchronized (mLock) {
            service = getServiceLocked();
            if (service == null) {
                return false;
            }
        }

        try {
            return service.registerProxyForDisplay(proxy.mServiceClient, proxy.getDisplayId());
        }  catch (RemoteException re) {
            throw re.rethrowFromSystemServer();
        }
    }

    /**
     * Unregisters an {@link AccessibilityDisplayProxy}.
     *
     * @return {@code true} if the proxy is successfully unregistered.
     *
     * @throws SecurityException if the app does not hold the
     * {@link Manifest.permission#MANAGE_ACCESSIBILITY} permission.
     *
     * @hide
     */
    @SystemApi
    @RequiresPermission(Manifest.permission.MANAGE_ACCESSIBILITY)
    public boolean unregisterDisplayProxy(@NonNull AccessibilityDisplayProxy proxy)  {
        final IAccessibilityManager service;
        synchronized (mLock) {
            service = getServiceLocked();
            if (service == null) {
                return false;
            }
        }
        try {
            return service.unregisterProxyForDisplay(proxy.getDisplayId());
        } catch (RemoteException re) {
            throw re.rethrowFromSystemServer();
        }
    }

    private IAccessibilityManager getServiceLocked() {
        if (mService == null) {
            tryConnectToServiceLocked(null);
+2 −2
Original line number Diff line number Diff line
@@ -109,9 +109,9 @@ interface IAccessibilityManager {

    oneway void setAccessibilityWindowAttributes(int displayId, int windowId, int userId, in AccessibilityWindowAttributes attributes);

    // Requires Manifest.permission.MANAGE_ACCESSIBILITY
    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY)")
    boolean registerProxyForDisplay(IAccessibilityServiceClient proxy, int displayId);

    // Requires Manifest.permission.MANAGE_ACCESSIBILITY
    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY)")
    boolean unregisterProxyForDisplay(int displayId);
}
+45 −0
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.accessibilityservice.AccessibilityServiceInfo;
import android.accessibilityservice.IAccessibilityServiceClient;
import android.app.Instrumentation;
import android.app.PendingIntent;
import android.app.RemoteAction;
@@ -34,6 +35,7 @@ import android.content.Intent;
import android.graphics.drawable.Icon;
import android.os.UserHandle;

import androidx.annotation.NonNull;
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;

@@ -51,6 +53,7 @@ import org.mockito.MockitoAnnotations;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;

/**
 * Tests for the AccessibilityManager by mocking the backing service.
@@ -70,6 +73,7 @@ public class AccessibilityManagerTest {
            LABEL,
            DESCRIPTION,
            TEST_PENDING_INTENT);
    private static final int DISPLAY_ID = 22;

    @Mock private IAccessibilityManager mMockService;
    private MessageCapturingHandler mHandler;
@@ -224,4 +228,45 @@ public class AccessibilityManagerTest {
        assertEquals(mFocusColorDefaultValue,
                manager.getAccessibilityFocusColor());
    }

    @Test
    public void testRegisterAccessibilityProxy() throws Exception {
        // Accessibility does not need to be enabled for a proxy to be registered.
        AccessibilityManager manager =
                new AccessibilityManager(mInstrumentation.getContext(), mHandler, mMockService,
                        UserHandle.USER_CURRENT, true);


        ArrayList<AccessibilityServiceInfo> infos = new ArrayList<>();
        infos.add(new AccessibilityServiceInfo());
        AccessibilityDisplayProxy proxy = new MyAccessibilityProxy(DISPLAY_ID, infos);
        manager.registerDisplayProxy(proxy);
        // Cannot access proxy.mServiceClient directly due to visibility.
        verify(mMockService).registerProxyForDisplay(any(IAccessibilityServiceClient.class),
                any(Integer.class));
    }

    @Test
    public void testUnregisterAccessibilityProxy() throws Exception {
        // Accessibility does not need to be enabled for a proxy to be registered.
        final AccessibilityManager manager =
                new AccessibilityManager(mInstrumentation.getContext(), mHandler, mMockService,
                        UserHandle.USER_CURRENT, true);

        final ArrayList<AccessibilityServiceInfo> infos = new ArrayList<>();
        infos.add(new AccessibilityServiceInfo());

        final AccessibilityDisplayProxy proxy = new MyAccessibilityProxy(DISPLAY_ID, infos);
        manager.registerDisplayProxy(proxy);
        manager.unregisterDisplayProxy(proxy);
        verify(mMockService).unregisterProxyForDisplay(proxy.getDisplayId());
    }

    private class MyAccessibilityProxy extends AccessibilityDisplayProxy {
        // TODO(241429275): Will override A11yProxy methods in the future.
        MyAccessibilityProxy(int displayId,
                @NonNull List<AccessibilityServiceInfo> serviceInfos) {
            super(displayId, Executors.newSingleThreadExecutor(), serviceInfos);
        }
    }
}
Loading