Merge "Add AccessibilityDisplayProxy and register/unregister methods"

This commit is contained in:
Sally Yuen
2022-11-01 19:38:20 +00:00
committed by Android (Google) Code Review
8 changed files with 314 additions and 3 deletions

View File

@@ -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);
}

View File

@@ -0,0 +1,181 @@
/*
* 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) {
}
});
}
}
}

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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);
}
}
}

View File

@@ -3651,6 +3651,10 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
throw new IllegalArgumentException("The display " + displayId + " does not exist or is"
+ " not tracked by accessibility.");
}
if (mProxyManager.isProxyed(displayId)) {
throw new IllegalArgumentException("The display " + displayId + " is already being"
+ "proxy-ed");
}
mProxyManager.registerProxy(client, displayId);
return true;

View File

@@ -32,6 +32,7 @@ import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteCallback;
import android.view.KeyEvent;
import android.view.accessibility.AccessibilityDisplayProxy;
import android.view.accessibility.AccessibilityNodeInfo;
import androidx.annotation.Nullable;
@@ -44,7 +45,7 @@ import java.util.List;
import java.util.Set;
/**
* Represents the system connection to an {@link android.view.accessibility.AccessibilityProxy}.
* Represents the system connection to an {@link AccessibilityDisplayProxy}.
*
* <p>Most methods are no-ops since this connection does not need to capture input or listen to
* hardware-related changes.

View File

@@ -16,6 +16,8 @@
package com.android.server.accessibility;
import android.accessibilityservice.IAccessibilityServiceClient;
import java.util.HashSet;
/**
* Manages proxy connections.
*
@@ -26,6 +28,7 @@ import android.accessibilityservice.IAccessibilityServiceClient;
*/
public class ProxyManager {
private final Object mLock;
private final HashSet<Integer> mDisplayIds = new HashSet<>();
ProxyManager(Object lock) {
mLock = lock;
@@ -35,12 +38,21 @@ public class ProxyManager {
* TODO: Create the proxy service connection.
*/
public void registerProxy(IAccessibilityServiceClient client, int displayId) {
mDisplayIds.add(displayId);
}
/**
* TODO: Unregister the proxy service connection based on display id.
*/
public boolean unregisterProxy(int displayId) {
mDisplayIds.remove(displayId);
return true;
}
/**
* Checks if a display id is being proxy-ed.
*/
public boolean isProxyed(int displayId) {
return mDisplayIds.contains(displayId);
}
}