| /* |
| * 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.IAccessibilityServiceConnection; |
| import android.accessibilityservice.MagnificationConfig; |
| import android.annotation.ColorInt; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.annotation.SystemApi; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.pm.ResolveInfo; |
| import android.graphics.Region; |
| import android.os.IBinder; |
| import android.os.RemoteException; |
| import android.util.Log; |
| 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.Collections; |
| 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 by accessibility, such as private displays, 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. |
| * |
| * <p> |
| * Avoid using the app's main thread. Proxy methods such as {@link #getWindows} and node methods |
| * like {@link AccessibilityNodeInfo#getChild(int)} will happen frequently. Node methods may also |
| * wait on the displayed app's UI thread to obtain accurate screen data. |
| * |
| * <p> |
| * To get a list of {@link AccessibilityServiceInfo}s that have populated {@link ComponentName}s and |
| * {@link ResolveInfo}s, retrieve the list using {@link #getInstalledAndEnabledServices()} after |
| * {@link #onProxyConnected()} has been called. |
| * |
| * @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 accessibility 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; |
| } |
| |
| /** |
| * Handles {@link android.view.accessibility.AccessibilityEvent}s. |
| * <p> |
| * AccessibilityEvents represent changes to the UI, or what parts of the node tree have changed. |
| * AccessibilityDisplayProxy should use these to query new UI and send appropriate feedback |
| * to their users. |
| * <p> |
| * For example, a {@link AccessibilityEvent#TYPE_WINDOWS_CHANGED} indicates a change in windows, |
| * so a proxy may query {@link #getWindows} to obtain updated UI and potentially inform of a new |
| * window title. Or a proxy may emit an earcon on a |
| * {@link AccessibilityEvent#TYPE_VIEW_SCROLLED} event. |
| */ |
| public void onAccessibilityEvent(@NonNull AccessibilityEvent event) { |
| // Default no-op |
| } |
| |
| /** |
| * Handles a successful system connection after |
| * {@link AccessibilityManager#registerDisplayProxy(AccessibilityDisplayProxy)} is called. |
| * |
| * <p> |
| * At this point, querying for UI is available and {@link AccessibilityEvent}s will begin being |
| * sent. An AccessibilityDisplayProxy may instantiate core infrastructure components here. |
| */ |
| public void onProxyConnected() { |
| // Default no-op |
| } |
| |
| /** |
| * Handles a request to interrupt the accessibility feedback. |
| * <p> |
| * AccessibilityDisplayProxy should interrupt the accessibility activity occurring on its |
| * display. For example, a screen reader may interrupt speech. |
| * |
| * @see AccessibilityManager#interrupt() |
| * @see AccessibilityService#onInterrupt() |
| */ |
| public void interrupt() { |
| // Default no-op |
| } |
| |
| /** |
| * Gets the node with focus, in this display. |
| * |
| * <p>For {@link AccessibilityNodeInfo#FOCUS_INPUT}, this returns the input-focused node in the |
| * proxy display if this display can receive unspecified input events (input that does not |
| * specify a target display.) |
| * |
| * <p>For {@link AccessibilityNodeInfo#FOCUS_ACCESSIBILITY}, this returns the |
| * accessibility-focused node in the proxy display if the display has accessibility focus. |
| * @param focusType The focus to find. One of {@link AccessibilityNodeInfo#FOCUS_INPUT} or |
| * {@link AccessibilityNodeInfo#FOCUS_ACCESSIBILITY}. |
| * @return The node info of the focused view or null. |
| |
| */ |
| @Nullable |
| public AccessibilityNodeInfo findFocus(int focusType) { |
| // TODO(264423198): Support querying the focused node of the proxy's display even if it is |
| // not the top-focused display and can't receive untargeted input events. |
| // TODO(254545943): Separate accessibility focus between proxy and phone state. |
| return AccessibilityInteractionClient.getInstance().findFocus(mConnectionId, |
| AccessibilityWindowInfo.ANY_WINDOW_ID, AccessibilityNodeInfo.ROOT_NODE_ID, |
| focusType); |
| } |
| |
| /** |
| * Gets the windows of the tracked display. |
| * |
| * @see AccessibilityService#getWindows() |
| */ |
| @NonNull |
| public List<AccessibilityWindowInfo> getWindows() { |
| return AccessibilityInteractionClient.getInstance().getWindowsOnDisplay(mConnectionId, |
| mDisplayId); |
| } |
| |
| /** |
| * Sets the list of {@link AccessibilityServiceInfo}s describing the services interested in the |
| * {@link AccessibilityDisplayProxy}'s display. |
| * |
| * <p>These represent accessibility features and services that are installed and running. These |
| * should not include {@link AccessibilityService}s installed on the phone. |
| * |
| * @param installedAndEnabledServices the list of installed and running accessibility services. |
| */ |
| public void setInstalledAndEnabledServices( |
| @NonNull List<AccessibilityServiceInfo> installedAndEnabledServices) { |
| mInstalledAndEnabledServices = installedAndEnabledServices; |
| sendServiceInfos(); |
| } |
| |
| /** |
| * Sets the {@link AccessibilityServiceInfo} for this service if the latter is |
| * properly set and there is an {@link IAccessibilityServiceConnection} to the |
| * AccessibilityManagerService. |
| */ |
| private void sendServiceInfos() { |
| IAccessibilityServiceConnection connection = |
| AccessibilityInteractionClient.getInstance().getConnection(mConnectionId); |
| if (mInstalledAndEnabledServices != null && mInstalledAndEnabledServices.size() > 0 |
| && connection != null) { |
| try { |
| connection.setInstalledAndEnabledServices(mInstalledAndEnabledServices); |
| AccessibilityInteractionClient.getInstance().clearCache(mConnectionId); |
| } catch (RemoteException re) { |
| Log.w(LOG_TAG, "Error while setting AccessibilityServiceInfos", re); |
| re.rethrowFromSystemServer(); |
| } |
| } |
| mInstalledAndEnabledServices = null; |
| } |
| |
| /** |
| * Gets the list of {@link AccessibilityServiceInfo}s describing the services interested in the |
| * {@link AccessibilityDisplayProxy}'s display. |
| * |
| * @return The {@link AccessibilityServiceInfo}s of interested services. |
| * @see AccessibilityServiceInfo |
| */ |
| @NonNull |
| public final List<AccessibilityServiceInfo> getInstalledAndEnabledServices() { |
| IAccessibilityServiceConnection connection = |
| AccessibilityInteractionClient.getInstance().getConnection(mConnectionId); |
| if (connection != null) { |
| try { |
| return connection.getInstalledAndEnabledServices(); |
| } catch (RemoteException re) { |
| Log.w(LOG_TAG, "Error while getting AccessibilityServiceInfo", re); |
| re.rethrowFromSystemServer(); |
| } |
| } |
| return Collections.emptyList(); |
| } |
| |
| /** |
| * Sets the strokeWidth and color of the accessibility focus rectangle. |
| * |
| * @param strokeWidth The stroke width of the rectangle in pixels. |
| * Setting this value to zero results in no focus rectangle being drawn. |
| * @param color The color of the rectangle. |
| */ |
| public void setAccessibilityFocusAppearance(int strokeWidth, @ColorInt int color) { |
| IAccessibilityServiceConnection connection = |
| AccessibilityInteractionClient.getInstance().getConnection(mConnectionId); |
| if (connection != null) { |
| try { |
| connection.setFocusAppearance(strokeWidth, color); |
| } catch (RemoteException re) { |
| Log.w(LOG_TAG, "Error while setting the strokeWidth and color of the " |
| + "accessibility focus rectangle", re); |
| re.rethrowFromSystemServer(); |
| } |
| } |
| } |
| |
| /** |
| * An IAccessibilityServiceClient that handles interrupts, accessibility events, and system |
| * connection. |
| */ |
| private class IAccessibilityServiceClientImpl extends |
| AccessibilityService.IAccessibilityServiceClientWrapper { |
| |
| IAccessibilityServiceClientImpl(Context context, Executor executor) { |
| super(context, executor, new AccessibilityService.Callbacks() { |
| @Override |
| public void onAccessibilityEvent(AccessibilityEvent event) { |
| AccessibilityDisplayProxy.this.onAccessibilityEvent(event); |
| } |
| |
| @Override |
| public void onInterrupt() { |
| AccessibilityDisplayProxy.this.interrupt(); |
| } |
| |
| @Override |
| public void onServiceConnected() { |
| AccessibilityDisplayProxy.this.sendServiceInfos(); |
| AccessibilityDisplayProxy.this.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) { |
| } |
| }); |
| } |
| } |
| } |