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

Commit fa16d9b0 authored by Sally Yuen's avatar Sally Yuen Committed by Android (Google) Code Review
Browse files

Merge "Add AccessibilityDisplayProxy and register/unregister methods"

parents 21bde0b1 d86f7e72
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -15924,10 +15924,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