diff options
Diffstat (limited to 'android/view/accessibility')
4 files changed, 992 insertions, 65 deletions
diff --git a/android/view/accessibility/AccessibilityEvent.java b/android/view/accessibility/AccessibilityEvent.java index e0f74a7d..7946e9e2 100644 --- a/android/view/accessibility/AccessibilityEvent.java +++ b/android/view/accessibility/AccessibilityEvent.java @@ -201,6 +201,7 @@ import java.util.List; * <em>Properties:</em></br> * <ul> * <li>{@link #getEventType()} - The type of the event.</li> + * <li>{@link #getContentChangeTypes()} - The type of state changes.</li> * <li>{@link #getSource()} - The source info (for registered clients).</li> * <li>{@link #getClassName()} - The class name of the source.</li> * <li>{@link #getPackageName()} - The package name of the source.</li> @@ -388,6 +389,8 @@ import java.util.List; */ public final class AccessibilityEvent extends AccessibilityRecord implements Parcelable { private static final boolean DEBUG = false; + /** @hide */ + public static final boolean DEBUG_ORIGIN = false; /** * Invalid selection/focus position. @@ -748,7 +751,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par private static final int MAX_POOL_SIZE = 10; private static final SynchronizedPool<AccessibilityEvent> sPool = - new SynchronizedPool<AccessibilityEvent>(MAX_POOL_SIZE); + new SynchronizedPool<>(MAX_POOL_SIZE); private @EventType int mEventType; private CharSequence mPackageName; @@ -758,6 +761,17 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par int mContentChangeTypes; int mWindowChangeTypes; + /** + * The stack trace describing where this event originated from on the app side. + * Only populated if {@link #DEBUG_ORIGIN} is enabled + * Can be inspected(e.g. printed) from an + * {@link android.accessibilityservice.AccessibilityService} to trace where particular events + * are being dispatched from. + * + * @hide + */ + public StackTraceElement[] originStackTrace = null; + private ArrayList<AccessibilityRecord> mRecords; /* @@ -780,6 +794,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par mWindowChangeTypes = event.mWindowChangeTypes; mEventTime = event.mEventTime; mPackageName = event.mPackageName; + if (DEBUG_ORIGIN) originStackTrace = event.originStackTrace; } /** @@ -849,16 +864,17 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par } /** - * Gets the bit mask of change types signaled by an - * {@link #TYPE_WINDOW_CONTENT_CHANGED} event. A single event may represent - * multiple change types. + * Gets the bit mask of change types signaled by a + * {@link #TYPE_WINDOW_CONTENT_CHANGED} event or {@link #TYPE_WINDOW_STATE_CHANGED}. A single + * event may represent multiple change types. * * @return The bit mask of change types. One or more of: * <ul> - * <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION} - * <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_SUBTREE} - * <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_TEXT} - * <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_UNDEFINED} + * <li>{@link #CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION} + * <li>{@link #CONTENT_CHANGE_TYPE_SUBTREE} + * <li>{@link #CONTENT_CHANGE_TYPE_TEXT} + * <li>{@link #CONTENT_CHANGE_TYPE_PANE_TITLE} + * <li>{@link #CONTENT_CHANGE_TYPE_UNDEFINED} * </ul> */ @ContentChangeTypes @@ -877,6 +893,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par } case CONTENT_CHANGE_TYPE_SUBTREE: return "CONTENT_CHANGE_TYPE_SUBTREE"; case CONTENT_CHANGE_TYPE_TEXT: return "CONTENT_CHANGE_TYPE_TEXT"; + case CONTENT_CHANGE_TYPE_PANE_TITLE: return "CONTENT_CHANGE_TYPE_PANE_TITLE"; case CONTENT_CHANGE_TYPE_UNDEFINED: return "CONTENT_CHANGE_TYPE_UNDEFINED"; default: return Integer.toHexString(type); } @@ -1104,7 +1121,9 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par */ public static AccessibilityEvent obtain() { AccessibilityEvent event = sPool.acquire(); - return (event != null) ? event : new AccessibilityEvent(); + if (event == null) event = new AccessibilityEvent(); + if (DEBUG_ORIGIN) event.originStackTrace = Thread.currentThread().getStackTrace(); + return event; } /** @@ -1142,6 +1161,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par record.recycle(); } } + if (DEBUG_ORIGIN) originStackTrace = null; } /** @@ -1164,7 +1184,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par // Read the records. final int recordCount = parcel.readInt(); if (recordCount > 0) { - mRecords = new ArrayList<AccessibilityRecord>(recordCount); + mRecords = new ArrayList<>(recordCount); for (int i = 0; i < recordCount; i++) { AccessibilityRecord record = AccessibilityRecord.obtain(); readAccessibilityRecordFromParcel(record, parcel); @@ -1172,6 +1192,17 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par mRecords.add(record); } } + + if (DEBUG_ORIGIN) { + originStackTrace = new StackTraceElement[parcel.readInt()]; + for (int i = 0; i < originStackTrace.length; i++) { + originStackTrace[i] = new StackTraceElement( + parcel.readString(), + parcel.readString(), + parcel.readString(), + parcel.readInt()); + } + } } /** @@ -1227,6 +1258,17 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par AccessibilityRecord record = mRecords.get(i); writeAccessibilityRecordToParcel(record, parcel, flags); } + + if (DEBUG_ORIGIN) { + if (originStackTrace == null) originStackTrace = Thread.currentThread().getStackTrace(); + parcel.writeInt(originStackTrace.length); + for (StackTraceElement element : originStackTrace) { + parcel.writeString(element.getClassName()); + parcel.writeString(element.getMethodName()); + parcel.writeString(element.getFileName()); + parcel.writeInt(element.getLineNumber()); + } + } } /** @@ -1285,7 +1327,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par } if (!DEBUG_CONCISE_TOSTRING || mWindowChangeTypes != 0) { builder.append("; WindowChangeTypes: ").append( - contentChangeTypesToString(mWindowChangeTypes)); + windowChangeTypesToString(mWindowChangeTypes)); } super.appendTo(builder); if (DEBUG || DEBUG_CONCISE_TOSTRING) { diff --git a/android/view/accessibility/AccessibilityInteractionClient.java b/android/view/accessibility/AccessibilityInteractionClient.java index 72af203e..d60c4819 100644 --- a/android/view/accessibility/AccessibilityInteractionClient.java +++ b/android/view/accessibility/AccessibilityInteractionClient.java @@ -326,12 +326,14 @@ public final class AccessibilityInteractionClient accessibilityWindowId, accessibilityNodeId); if (cachedInfo != null) { if (DEBUG) { - Log.i(LOG_TAG, "Node cache hit"); + Log.i(LOG_TAG, "Node cache hit for " + + idToString(accessibilityWindowId, accessibilityNodeId)); } return cachedInfo; } if (DEBUG) { - Log.i(LOG_TAG, "Node cache miss"); + Log.i(LOG_TAG, "Node cache miss for " + + idToString(accessibilityWindowId, accessibilityNodeId)); } } final int interactionId = mInteractionIdCounter.getAndIncrement(); @@ -368,6 +370,11 @@ public final class AccessibilityInteractionClient return null; } + private static String idToString(int accessibilityWindowId, long accessibilityNodeId) { + return accessibilityWindowId + "/" + + AccessibilityNodeInfo.idToString(accessibilityNodeId); + } + /** * Finds an {@link AccessibilityNodeInfo} by View id. The search is performed in * the window whose id is specified and starts from the node whose accessibility diff --git a/android/view/accessibility/AccessibilityManager.java b/android/view/accessibility/AccessibilityManager.java index 84b40641..cbb23f1a 100644 --- a/android/view/accessibility/AccessibilityManager.java +++ b/android/view/accessibility/AccessibilityManager.java @@ -16,48 +16,156 @@ package android.view.accessibility; +import static android.accessibilityservice.AccessibilityServiceInfo.FLAG_ENABLE_ACCESSIBILITY_VOLUME; + +import android.Manifest; import android.accessibilityservice.AccessibilityServiceInfo; import android.accessibilityservice.AccessibilityServiceInfo.FeedbackType; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SdkConstant; +import android.annotation.SystemService; +import android.content.ComponentName; import android.content.Context; +import android.content.pm.PackageManager; import android.content.pm.ServiceInfo; +import android.content.res.Resources; +import android.os.Binder; import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.Process; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemClock; +import android.os.UserHandle; +import android.util.ArrayMap; +import android.util.Log; +import android.util.SparseArray; import android.view.IWindow; import android.view.View; import android.view.accessibility.AccessibilityEvent.EventType; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.IntPair; + +import java.util.ArrayList; import java.util.Collections; import java.util.List; /** - * System level service that serves as an event dispatch for {@link AccessibilityEvent}s. - * Such events are generated when something notable happens in the user interface, + * System level service that serves as an event dispatch for {@link AccessibilityEvent}s, + * and provides facilities for querying the accessibility state of the system. + * Accessibility events are generated when something notable happens in the user interface, * for example an {@link android.app.Activity} starts, the focus or selection of a * {@link android.view.View} changes etc. Parties interested in handling accessibility * events implement and register an accessibility service which extends - * {@code android.accessibilityservice.AccessibilityService}. + * {@link android.accessibilityservice.AccessibilityService}. * * @see AccessibilityEvent - * @see android.content.Context#getSystemService + * @see AccessibilityNodeInfo + * @see android.accessibilityservice.AccessibilityService + * @see Context#getSystemService + * @see Context#ACCESSIBILITY_SERVICE */ -@SuppressWarnings("UnusedDeclaration") +@SystemService(Context.ACCESSIBILITY_SERVICE) public final class AccessibilityManager { + private static final boolean DEBUG = false; + + private static final String LOG_TAG = "AccessibilityManager"; + + /** @hide */ + public static final int STATE_FLAG_ACCESSIBILITY_ENABLED = 0x00000001; + + /** @hide */ + public static final int STATE_FLAG_TOUCH_EXPLORATION_ENABLED = 0x00000002; + + /** @hide */ + public static final int STATE_FLAG_HIGH_TEXT_CONTRAST_ENABLED = 0x00000004; + + /** @hide */ + public static final int DALTONIZER_DISABLED = -1; + + /** @hide */ + public static final int DALTONIZER_SIMULATE_MONOCHROMACY = 0; + + /** @hide */ + public static final int DALTONIZER_CORRECT_DEUTERANOMALY = 12; + + /** @hide */ + public static final int AUTOCLICK_DELAY_DEFAULT = 600; + + /** + * Activity action: Launch UI to manage which accessibility service or feature is assigned + * to the navigation bar Accessibility button. + * <p> + * Input: Nothing. + * </p> + * <p> + * Output: Nothing. + * </p> + * + * @hide + */ + @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_CHOOSE_ACCESSIBILITY_BUTTON = + "com.android.internal.intent.action.CHOOSE_ACCESSIBILITY_BUTTON"; + + static final Object sInstanceSync = new Object(); + + private static AccessibilityManager sInstance; + + private final Object mLock = new Object(); + + private IAccessibilityManager mService; + + final int mUserId; + + final Handler mHandler; + + final Handler.Callback mCallback; + + boolean mIsEnabled; + + int mRelevantEventTypes = AccessibilityEvent.TYPES_ALL_MASK; + + boolean mIsTouchExplorationEnabled; - private static AccessibilityManager sInstance = new AccessibilityManager(null, null, 0); + boolean mIsHighTextContrastEnabled; + AccessibilityPolicy mAccessibilityPolicy; + + private final ArrayMap<AccessibilityStateChangeListener, Handler> + mAccessibilityStateChangeListeners = new ArrayMap<>(); + + private final ArrayMap<TouchExplorationStateChangeListener, Handler> + mTouchExplorationStateChangeListeners = new ArrayMap<>(); + + private final ArrayMap<HighTextContrastChangeListener, Handler> + mHighTextContrastStateChangeListeners = new ArrayMap<>(); + + private final ArrayMap<AccessibilityServicesStateChangeListener, Handler> + mServicesStateChangeListeners = new ArrayMap<>(); /** - * Listener for the accessibility state. + * Map from a view's accessibility id to the list of request preparers set for that view + */ + private SparseArray<List<AccessibilityRequestPreparer>> mRequestPreparerLists; + + /** + * Listener for the system accessibility state. To listen for changes to the + * accessibility state on the device, implement this interface and register + * it with the system by calling {@link #addAccessibilityStateChangeListener}. */ public interface AccessibilityStateChangeListener { /** - * Called back on change in the accessibility state. + * Called when the accessibility enabled state changes. * * @param enabled Whether accessibility is enabled. */ - public void onAccessibilityStateChanged(boolean enabled); + void onAccessibilityStateChanged(boolean enabled); } /** @@ -73,7 +181,24 @@ public final class AccessibilityManager { * * @param enabled Whether touch exploration is enabled. */ - public void onTouchExplorationStateChanged(boolean enabled); + void onTouchExplorationStateChanged(boolean enabled); + } + + /** + * Listener for changes to the state of accessibility services. Changes include services being + * enabled or disabled, or changes to the {@link AccessibilityServiceInfo} of a running service. + * {@see #addAccessibilityServicesStateChangeListener}. + * + * @hide + */ + public interface AccessibilityServicesStateChangeListener { + + /** + * Called when the state of accessibility services changes. + * + * @param manager The manager that is calling back + */ + void onAccessibilityServicesStateChanged(AccessibilityManager manager); } /** @@ -81,6 +206,8 @@ public final class AccessibilityManager { * the high text contrast state on the device, implement this interface and * register it with the system by calling * {@link #addHighTextContrastStateChangeListener}. + * + * @hide */ public interface HighTextContrastChangeListener { @@ -89,7 +216,7 @@ public final class AccessibilityManager { * * @param enabled Whether high text contrast is enabled. */ - public void onHighTextContrastStateChanged(boolean enabled); + void onHighTextContrastStateChanged(boolean enabled); } /** @@ -148,21 +275,67 @@ public final class AccessibilityManager { private final IAccessibilityManagerClient.Stub mClient = new IAccessibilityManagerClient.Stub() { - public void setState(int state) { - } + @Override + public void setState(int state) { + // We do not want to change this immediately as the application may + // have already checked that accessibility is on and fired an event, + // that is now propagating up the view tree, Hence, if accessibility + // is now off an exception will be thrown. We want to have the exception + // enforcement to guard against apps that fire unnecessary accessibility + // events when accessibility is off. + mHandler.obtainMessage(MyCallback.MSG_SET_STATE, state, 0).sendToTarget(); + } - public void notifyServicesStateChanged() { + @Override + public void notifyServicesStateChanged() { + final ArrayMap<AccessibilityServicesStateChangeListener, Handler> listeners; + synchronized (mLock) { + if (mServicesStateChangeListeners.isEmpty()) { + return; } + listeners = new ArrayMap<>(mServicesStateChangeListeners); + } - public void setRelevantEventTypes(int eventTypes) { - } - }; + int numListeners = listeners.size(); + for (int i = 0; i < numListeners; i++) { + final AccessibilityServicesStateChangeListener listener = + mServicesStateChangeListeners.keyAt(i); + mServicesStateChangeListeners.valueAt(i).post(() -> listener + .onAccessibilityServicesStateChanged(AccessibilityManager.this)); + } + } + + @Override + public void setRelevantEventTypes(int eventTypes) { + mRelevantEventTypes = eventTypes; + } + }; /** * Get an AccessibilityManager instance (create one if necessary). * + * @param context Context in which this manager operates. + * + * @hide */ public static AccessibilityManager getInstance(Context context) { + synchronized (sInstanceSync) { + if (sInstance == null) { + final int userId; + if (Binder.getCallingUid() == Process.SYSTEM_UID + || context.checkCallingOrSelfPermission( + Manifest.permission.INTERACT_ACROSS_USERS) + == PackageManager.PERMISSION_GRANTED + || context.checkCallingOrSelfPermission( + Manifest.permission.INTERACT_ACROSS_USERS_FULL) + == PackageManager.PERMISSION_GRANTED) { + userId = UserHandle.USER_CURRENT; + } else { + userId = context.getUserId(); + } + sInstance = new AccessibilityManager(context, null, userId); + } + } return sInstance; } @@ -170,21 +343,65 @@ public final class AccessibilityManager { * Create an instance. * * @param context A {@link Context}. + * @param service An interface to the backing service. + * @param userId User id under which to run. + * + * @hide */ public AccessibilityManager(Context context, IAccessibilityManager service, int userId) { + // Constructor can't be chained because we can't create an instance of an inner class + // before calling another constructor. + mCallback = new MyCallback(); + mHandler = new Handler(context.getMainLooper(), mCallback); + mUserId = userId; + synchronized (mLock) { + tryConnectToServiceLocked(service); + } + } + + /** + * Create an instance. + * + * @param handler The handler to use + * @param service An interface to the backing service. + * @param userId User id under which to run. + * + * @hide + */ + public AccessibilityManager(Handler handler, IAccessibilityManager service, int userId) { + mCallback = new MyCallback(); + mHandler = handler; + mUserId = userId; + synchronized (mLock) { + tryConnectToServiceLocked(service); + } } + /** + * @hide + */ public IAccessibilityManagerClient getClient() { return mClient; } /** - * Returns if the {@link AccessibilityManager} is enabled. + * @hide + */ + @VisibleForTesting + public Handler.Callback getCallback() { + return mCallback; + } + + /** + * Returns if the accessibility in the system is enabled. * - * @return True if this {@link AccessibilityManager} is enabled, false otherwise. + * @return True if accessibility is enabled, false otherwise. */ public boolean isEnabled() { - return false; + synchronized (mLock) { + return mIsEnabled || (mAccessibilityPolicy != null + && mAccessibilityPolicy.isEnabled(mIsEnabled)); + } } /** @@ -193,7 +410,13 @@ public final class AccessibilityManager { * @return True if touch exploration is enabled, false otherwise. */ public boolean isTouchExplorationEnabled() { - return true; + synchronized (mLock) { + IAccessibilityManager service = getServiceLocked(); + if (service == null) { + return false; + } + return mIsTouchExplorationEnabled; + } } /** @@ -203,47 +426,188 @@ public final class AccessibilityManager { * doing its own rendering and does not rely on the platform rendering pipeline. * </p> * + * @return True if high text contrast is enabled, false otherwise. + * + * @hide */ public boolean isHighTextContrastEnabled() { - return false; + synchronized (mLock) { + IAccessibilityManager service = getServiceLocked(); + if (service == null) { + return false; + } + return mIsHighTextContrastEnabled; + } } /** * Sends an {@link AccessibilityEvent}. - */ - public void sendAccessibilityEvent(AccessibilityEvent event) { - } - - /** - * Returns whether there are observers registered for this event type. If - * this method returns false you shuold not generate events of this type - * to conserve resources. * - * @param type The event type. - * @return Whether the event is being observed. + * @param event The event to send. + * + * @throws IllegalStateException if accessibility is not enabled. + * + * <strong>Note:</strong> The preferred mechanism for sending custom accessibility + * events is through calling + * {@link android.view.ViewParent#requestSendAccessibilityEvent(View, AccessibilityEvent)} + * instead of this method to allow predecessors to augment/filter events sent by + * their descendants. */ - public boolean isObservedEventType(@AccessibilityEvent.EventType int type) { - return false; + public void sendAccessibilityEvent(AccessibilityEvent event) { + final IAccessibilityManager service; + final int userId; + final AccessibilityEvent dispatchedEvent; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return; + } + event.setEventTime(SystemClock.uptimeMillis()); + if (mAccessibilityPolicy != null) { + dispatchedEvent = mAccessibilityPolicy.onAccessibilityEvent(event, + mIsEnabled, mRelevantEventTypes); + if (dispatchedEvent == null) { + return; + } + } else { + dispatchedEvent = event; + } + if (!isEnabled()) { + Looper myLooper = Looper.myLooper(); + if (myLooper == Looper.getMainLooper()) { + throw new IllegalStateException( + "Accessibility off. Did you forget to check that?"); + } else { + // If we're not running on the thread with the main looper, it's possible for + // the state of accessibility to change between checking isEnabled and + // calling this method. So just log the error rather than throwing the + // exception. + Log.e(LOG_TAG, "AccessibilityEvent sent with accessibility disabled"); + return; + } + } + if ((dispatchedEvent.getEventType() & mRelevantEventTypes) == 0) { + if (DEBUG) { + Log.i(LOG_TAG, "Not dispatching irrelevant event: " + dispatchedEvent + + " that is not among " + + AccessibilityEvent.eventTypeToString(mRelevantEventTypes)); + } + return; + } + userId = mUserId; + } + try { + // it is possible that this manager is in the same process as the service but + // client using it is called through Binder from another process. Example: MMS + // app adds a SMS notification and the NotificationManagerService calls this method + long identityToken = Binder.clearCallingIdentity(); + try { + service.sendAccessibilityEvent(dispatchedEvent, userId); + } finally { + Binder.restoreCallingIdentity(identityToken); + } + if (DEBUG) { + Log.i(LOG_TAG, dispatchedEvent + " sent"); + } + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error during sending " + dispatchedEvent + " ", re); + } finally { + if (event != dispatchedEvent) { + event.recycle(); + } + dispatchedEvent.recycle(); + } } /** - * Requests interruption of the accessibility feedback from all accessibility services. + * Requests feedback interruption from all accessibility services. */ public void interrupt() { + final IAccessibilityManager service; + final int userId; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return; + } + if (!isEnabled()) { + Looper myLooper = Looper.myLooper(); + if (myLooper == Looper.getMainLooper()) { + throw new IllegalStateException( + "Accessibility off. Did you forget to check that?"); + } else { + // If we're not running on the thread with the main looper, it's possible for + // the state of accessibility to change between checking isEnabled and + // calling this method. So just log the error rather than throwing the + // exception. + Log.e(LOG_TAG, "Interrupt called with accessibility disabled"); + return; + } + } + userId = mUserId; + } + try { + service.interrupt(userId); + if (DEBUG) { + Log.i(LOG_TAG, "Requested interrupt from all services"); + } + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error while requesting interrupt from all services. ", re); + } } /** * Returns the {@link ServiceInfo}s of the installed accessibility services. * * @return An unmodifiable list with {@link ServiceInfo}s. + * + * @deprecated Use {@link #getInstalledAccessibilityServiceList()} */ @Deprecated public List<ServiceInfo> getAccessibilityServiceList() { - return Collections.emptyList(); + List<AccessibilityServiceInfo> infos = getInstalledAccessibilityServiceList(); + List<ServiceInfo> services = new ArrayList<>(); + final int infoCount = infos.size(); + for (int i = 0; i < infoCount; i++) { + AccessibilityServiceInfo info = infos.get(i); + services.add(info.getResolveInfo().serviceInfo); + } + return Collections.unmodifiableList(services); } + /** + * Returns the {@link AccessibilityServiceInfo}s of the installed accessibility services. + * + * @return An unmodifiable list with {@link AccessibilityServiceInfo}s. + */ public List<AccessibilityServiceInfo> getInstalledAccessibilityServiceList() { - return Collections.emptyList(); + final IAccessibilityManager service; + final int userId; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return Collections.emptyList(); + } + userId = mUserId; + } + + List<AccessibilityServiceInfo> services = null; + try { + services = service.getInstalledAccessibilityServiceList(userId); + if (DEBUG) { + Log.i(LOG_TAG, "Installed AccessibilityServices " + services); + } + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error while obtaining the installed AccessibilityServices. ", re); + } + if (mAccessibilityPolicy != null) { + services = mAccessibilityPolicy.getInstalledAccessibilityServiceList(services); + } + if (services != null) { + return Collections.unmodifiableList(services); + } else { + return Collections.emptyList(); + } } /** @@ -258,21 +622,52 @@ public final class AccessibilityManager { * @see AccessibilityServiceInfo#FEEDBACK_HAPTIC * @see AccessibilityServiceInfo#FEEDBACK_SPOKEN * @see AccessibilityServiceInfo#FEEDBACK_VISUAL + * @see AccessibilityServiceInfo#FEEDBACK_BRAILLE */ public List<AccessibilityServiceInfo> getEnabledAccessibilityServiceList( int feedbackTypeFlags) { - return Collections.emptyList(); + final IAccessibilityManager service; + final int userId; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return Collections.emptyList(); + } + userId = mUserId; + } + + List<AccessibilityServiceInfo> services = null; + try { + services = service.getEnabledAccessibilityServiceList(feedbackTypeFlags, userId); + if (DEBUG) { + Log.i(LOG_TAG, "Installed AccessibilityServices " + services); + } + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error while obtaining the installed AccessibilityServices. ", re); + } + if (mAccessibilityPolicy != null) { + services = mAccessibilityPolicy.getEnabledAccessibilityServiceList( + feedbackTypeFlags, services); + } + if (services != null) { + return Collections.unmodifiableList(services); + } else { + return Collections.emptyList(); + } } /** * Registers an {@link AccessibilityStateChangeListener} for changes in - * the global accessibility state of the system. + * the global accessibility state of the system. Equivalent to calling + * {@link #addAccessibilityStateChangeListener(AccessibilityStateChangeListener, Handler)} + * with a null handler. * * @param listener The listener. - * @return True if successfully registered. + * @return Always returns {@code true}. */ public boolean addAccessibilityStateChangeListener( - AccessibilityStateChangeListener listener) { + @NonNull AccessibilityStateChangeListener listener) { + addAccessibilityStateChangeListener(listener, null); return true; } @@ -286,22 +681,40 @@ public final class AccessibilityManager { * for a callback on the process's main handler. */ public void addAccessibilityStateChangeListener( - @NonNull AccessibilityStateChangeListener listener, @Nullable Handler handler) {} + @NonNull AccessibilityStateChangeListener listener, @Nullable Handler handler) { + synchronized (mLock) { + mAccessibilityStateChangeListeners + .put(listener, (handler == null) ? mHandler : handler); + } + } + /** + * Unregisters an {@link AccessibilityStateChangeListener}. + * + * @param listener The listener. + * @return True if the listener was previously registered. + */ public boolean removeAccessibilityStateChangeListener( - AccessibilityStateChangeListener listener) { - return true; + @NonNull AccessibilityStateChangeListener listener) { + synchronized (mLock) { + int index = mAccessibilityStateChangeListeners.indexOfKey(listener); + mAccessibilityStateChangeListeners.remove(listener); + return (index >= 0); + } } /** * Registers a {@link TouchExplorationStateChangeListener} for changes in - * the global touch exploration state of the system. + * the global touch exploration state of the system. Equivalent to calling + * {@link #addTouchExplorationStateChangeListener(TouchExplorationStateChangeListener, Handler)} + * with a null handler. * * @param listener The listener. - * @return True if successfully registered. + * @return Always returns {@code true}. */ public boolean addTouchExplorationStateChangeListener( @NonNull TouchExplorationStateChangeListener listener) { + addTouchExplorationStateChangeListener(listener, null); return true; } @@ -315,17 +728,104 @@ public final class AccessibilityManager { * for a callback on the process's main handler. */ public void addTouchExplorationStateChangeListener( - @NonNull TouchExplorationStateChangeListener listener, @Nullable Handler handler) {} + @NonNull TouchExplorationStateChangeListener listener, @Nullable Handler handler) { + synchronized (mLock) { + mTouchExplorationStateChangeListeners + .put(listener, (handler == null) ? mHandler : handler); + } + } /** * Unregisters a {@link TouchExplorationStateChangeListener}. * * @param listener The listener. - * @return True if successfully unregistered. + * @return True if listener was previously registered. */ public boolean removeTouchExplorationStateChangeListener( @NonNull TouchExplorationStateChangeListener listener) { - return true; + synchronized (mLock) { + int index = mTouchExplorationStateChangeListeners.indexOfKey(listener); + mTouchExplorationStateChangeListeners.remove(listener); + return (index >= 0); + } + } + + /** + * Registers a {@link AccessibilityServicesStateChangeListener}. + * + * @param listener The listener. + * @param handler The handler on which the listener should be called back, or {@code null} + * for a callback on the process's main handler. + * @hide + */ + public void addAccessibilityServicesStateChangeListener( + @NonNull AccessibilityServicesStateChangeListener listener, @Nullable Handler handler) { + synchronized (mLock) { + mServicesStateChangeListeners + .put(listener, (handler == null) ? mHandler : handler); + } + } + + /** + * Unregisters a {@link AccessibilityServicesStateChangeListener}. + * + * @param listener The listener. + * + * @hide + */ + public void removeAccessibilityServicesStateChangeListener( + @NonNull AccessibilityServicesStateChangeListener listener) { + synchronized (mLock) { + mServicesStateChangeListeners.remove(listener); + } + } + + /** + * Registers a {@link AccessibilityRequestPreparer}. + */ + public void addAccessibilityRequestPreparer(AccessibilityRequestPreparer preparer) { + if (mRequestPreparerLists == null) { + mRequestPreparerLists = new SparseArray<>(1); + } + int id = preparer.getView().getAccessibilityViewId(); + List<AccessibilityRequestPreparer> requestPreparerList = mRequestPreparerLists.get(id); + if (requestPreparerList == null) { + requestPreparerList = new ArrayList<>(1); + mRequestPreparerLists.put(id, requestPreparerList); + } + requestPreparerList.add(preparer); + } + + /** + * Unregisters a {@link AccessibilityRequestPreparer}. + */ + public void removeAccessibilityRequestPreparer(AccessibilityRequestPreparer preparer) { + if (mRequestPreparerLists == null) { + return; + } + int viewId = preparer.getView().getAccessibilityViewId(); + List<AccessibilityRequestPreparer> requestPreparerList = mRequestPreparerLists.get(viewId); + if (requestPreparerList != null) { + requestPreparerList.remove(preparer); + if (requestPreparerList.isEmpty()) { + mRequestPreparerLists.remove(viewId); + } + } + } + + /** + * Get the preparers that are registered for an accessibility ID + * + * @param id The ID of interest + * @return The list of preparers, or {@code null} if there are none. + * + * @hide + */ + public List<AccessibilityRequestPreparer> getRequestPreparersForAccessibilityId(int id) { + if (mRequestPreparerLists == null) { + return null; + } + return mRequestPreparerLists.get(id); } /** @@ -337,7 +837,12 @@ public final class AccessibilityManager { * @hide */ public void addHighTextContrastStateChangeListener( - @NonNull HighTextContrastChangeListener listener, @Nullable Handler handler) {} + @NonNull HighTextContrastChangeListener listener, @Nullable Handler handler) { + synchronized (mLock) { + mHighTextContrastStateChangeListeners + .put(listener, (handler == null) ? mHandler : handler); + } + } /** * Unregisters a {@link HighTextContrastChangeListener}. @@ -347,7 +852,64 @@ public final class AccessibilityManager { * @hide */ public void removeHighTextContrastStateChangeListener( - @NonNull HighTextContrastChangeListener listener) {} + @NonNull HighTextContrastChangeListener listener) { + synchronized (mLock) { + mHighTextContrastStateChangeListeners.remove(listener); + } + } + + /** + * Sets the {@link AccessibilityPolicy} controlling this manager. + * + * @param policy The policy. + * + * @hide + */ + public void setAccessibilityPolicy(@Nullable AccessibilityPolicy policy) { + synchronized (mLock) { + mAccessibilityPolicy = policy; + } + } + + /** + * Check if the accessibility volume stream is active. + * + * @return True if accessibility volume is active (i.e. some service has requested it). False + * otherwise. + * @hide + */ + public boolean isAccessibilityVolumeStreamActive() { + List<AccessibilityServiceInfo> serviceInfos = + getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_ALL_MASK); + for (int i = 0; i < serviceInfos.size(); i++) { + if ((serviceInfos.get(i).flags & FLAG_ENABLE_ACCESSIBILITY_VOLUME) != 0) { + return true; + } + } + return false; + } + + /** + * Report a fingerprint gesture to accessibility. Only available for the system process. + * + * @param keyCode The key code of the gesture + * @return {@code true} if accessibility consumes the event. {@code false} if not. + * @hide + */ + public boolean sendFingerprintGesture(int keyCode) { + final IAccessibilityManager service; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return false; + } + } + try { + return service.sendFingerprintGesture(keyCode); + } catch (RemoteException e) { + return false; + } + } /** * Sets the current state and notifies listeners, if necessary. @@ -355,14 +917,312 @@ public final class AccessibilityManager { * @param stateFlags The state flags. */ private void setStateLocked(int stateFlags) { + final boolean enabled = (stateFlags & STATE_FLAG_ACCESSIBILITY_ENABLED) != 0; + final boolean touchExplorationEnabled = + (stateFlags & STATE_FLAG_TOUCH_EXPLORATION_ENABLED) != 0; + final boolean highTextContrastEnabled = + (stateFlags & STATE_FLAG_HIGH_TEXT_CONTRAST_ENABLED) != 0; + + final boolean wasEnabled = isEnabled(); + final boolean wasTouchExplorationEnabled = mIsTouchExplorationEnabled; + final boolean wasHighTextContrastEnabled = mIsHighTextContrastEnabled; + + // Ensure listeners get current state from isZzzEnabled() calls. + mIsEnabled = enabled; + mIsTouchExplorationEnabled = touchExplorationEnabled; + mIsHighTextContrastEnabled = highTextContrastEnabled; + + if (wasEnabled != isEnabled()) { + notifyAccessibilityStateChanged(); + } + + if (wasTouchExplorationEnabled != touchExplorationEnabled) { + notifyTouchExplorationStateChanged(); + } + + if (wasHighTextContrastEnabled != highTextContrastEnabled) { + notifyHighTextContrastStateChanged(); + } + } + + /** + * Find an installed service with the specified {@link ComponentName}. + * + * @param componentName The name to match to the service. + * + * @return The info corresponding to the installed service, or {@code null} if no such service + * is installed. + * @hide + */ + public AccessibilityServiceInfo getInstalledServiceInfoWithComponentName( + ComponentName componentName) { + final List<AccessibilityServiceInfo> installedServiceInfos = + getInstalledAccessibilityServiceList(); + if ((installedServiceInfos == null) || (componentName == null)) { + return null; + } + for (int i = 0; i < installedServiceInfos.size(); i++) { + if (componentName.equals(installedServiceInfos.get(i).getComponentName())) { + return installedServiceInfos.get(i); + } + } + return null; } + /** + * Adds an accessibility interaction connection interface for a given window. + * @param windowToken The window token to which a connection is added. + * @param connection The connection. + * + * @hide + */ public int addAccessibilityInteractionConnection(IWindow windowToken, - IAccessibilityInteractionConnection connection) { + String packageName, IAccessibilityInteractionConnection connection) { + final IAccessibilityManager service; + final int userId; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return View.NO_ID; + } + userId = mUserId; + } + try { + return service.addAccessibilityInteractionConnection(windowToken, connection, + packageName, userId); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error while adding an accessibility interaction connection. ", re); + } return View.NO_ID; } + /** + * Removed an accessibility interaction connection interface for a given window. + * @param windowToken The window token to which a connection is removed. + * + * @hide + */ public void removeAccessibilityInteractionConnection(IWindow windowToken) { + final IAccessibilityManager service; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return; + } + } + try { + service.removeAccessibilityInteractionConnection(windowToken); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error while removing an accessibility interaction connection. ", re); + } } + /** + * Perform the accessibility shortcut if the caller has permission. + * + * @hide + */ + public void performAccessibilityShortcut() { + final IAccessibilityManager service; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return; + } + } + try { + service.performAccessibilityShortcut(); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error performing accessibility shortcut. ", re); + } + } + + /** + * Notifies that the accessibility button in the system's navigation area has been clicked + * + * @hide + */ + public void notifyAccessibilityButtonClicked() { + final IAccessibilityManager service; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return; + } + } + try { + service.notifyAccessibilityButtonClicked(); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error while dispatching accessibility button click", re); + } + } + + /** + * Notifies that the visibility of the accessibility button in the system's navigation area + * has changed. + * + * @param shown {@code true} if the accessibility button is visible within the system + * navigation area, {@code false} otherwise + * @hide + */ + public void notifyAccessibilityButtonVisibilityChanged(boolean shown) { + final IAccessibilityManager service; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return; + } + } + try { + service.notifyAccessibilityButtonVisibilityChanged(shown); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error while dispatching accessibility button visibility change", re); + } + } + + /** + * Set an IAccessibilityInteractionConnection to replace the actions of a picture-in-picture + * window. Intended for use by the System UI only. + * + * @param connection The connection to handle the actions. Set to {@code null} to avoid + * affecting the actions. + * + * @hide + */ + public void setPictureInPictureActionReplacingConnection( + @Nullable IAccessibilityInteractionConnection connection) { + final IAccessibilityManager service; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return; + } + } + try { + service.setPictureInPictureActionReplacingConnection(connection); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error setting picture in picture action replacement", re); + } + } + + private IAccessibilityManager getServiceLocked() { + if (mService == null) { + tryConnectToServiceLocked(null); + } + return mService; + } + + private void tryConnectToServiceLocked(IAccessibilityManager service) { + if (service == null) { + IBinder iBinder = ServiceManager.getService(Context.ACCESSIBILITY_SERVICE); + if (iBinder == null) { + return; + } + service = IAccessibilityManager.Stub.asInterface(iBinder); + } + + try { + final long userStateAndRelevantEvents = service.addClient(mClient, mUserId); + setStateLocked(IntPair.first(userStateAndRelevantEvents)); + mRelevantEventTypes = IntPair.second(userStateAndRelevantEvents); + mService = service; + } catch (RemoteException re) { + Log.e(LOG_TAG, "AccessibilityManagerService is dead", re); + } + } + + /** + * Notifies the registered {@link AccessibilityStateChangeListener}s. + */ + private void notifyAccessibilityStateChanged() { + final boolean isEnabled; + final ArrayMap<AccessibilityStateChangeListener, Handler> listeners; + synchronized (mLock) { + if (mAccessibilityStateChangeListeners.isEmpty()) { + return; + } + isEnabled = isEnabled(); + listeners = new ArrayMap<>(mAccessibilityStateChangeListeners); + } + + final int numListeners = listeners.size(); + for (int i = 0; i < numListeners; i++) { + final AccessibilityStateChangeListener listener = listeners.keyAt(i); + listeners.valueAt(i).post(() -> + listener.onAccessibilityStateChanged(isEnabled)); + } + } + + /** + * Notifies the registered {@link TouchExplorationStateChangeListener}s. + */ + private void notifyTouchExplorationStateChanged() { + final boolean isTouchExplorationEnabled; + final ArrayMap<TouchExplorationStateChangeListener, Handler> listeners; + synchronized (mLock) { + if (mTouchExplorationStateChangeListeners.isEmpty()) { + return; + } + isTouchExplorationEnabled = mIsTouchExplorationEnabled; + listeners = new ArrayMap<>(mTouchExplorationStateChangeListeners); + } + + final int numListeners = listeners.size(); + for (int i = 0; i < numListeners; i++) { + final TouchExplorationStateChangeListener listener = listeners.keyAt(i); + listeners.valueAt(i).post(() -> + listener.onTouchExplorationStateChanged(isTouchExplorationEnabled)); + } + } + + /** + * Notifies the registered {@link HighTextContrastChangeListener}s. + */ + private void notifyHighTextContrastStateChanged() { + final boolean isHighTextContrastEnabled; + final ArrayMap<HighTextContrastChangeListener, Handler> listeners; + synchronized (mLock) { + if (mHighTextContrastStateChangeListeners.isEmpty()) { + return; + } + isHighTextContrastEnabled = mIsHighTextContrastEnabled; + listeners = new ArrayMap<>(mHighTextContrastStateChangeListeners); + } + + final int numListeners = listeners.size(); + for (int i = 0; i < numListeners; i++) { + final HighTextContrastChangeListener listener = listeners.keyAt(i); + listeners.valueAt(i).post(() -> + listener.onHighTextContrastStateChanged(isHighTextContrastEnabled)); + } + } + + /** + * Determines if the accessibility button within the system navigation area is supported. + * + * @return {@code true} if the accessibility button is supported on this device, + * {@code false} otherwise + */ + public static boolean isAccessibilityButtonSupported() { + final Resources res = Resources.getSystem(); + return res.getBoolean(com.android.internal.R.bool.config_showNavigationBar); + } + + private final class MyCallback implements Handler.Callback { + public static final int MSG_SET_STATE = 1; + + @Override + public boolean handleMessage(Message message) { + switch (message.what) { + case MSG_SET_STATE: { + // See comment at mClient + final int state = message.arg1; + synchronized (mLock) { + setStateLocked(state); + } + } break; + } + return true; + } + } } diff --git a/android/view/accessibility/AccessibilityNodeInfo.java b/android/view/accessibility/AccessibilityNodeInfo.java index 4c437dd4..03f1c124 100644 --- a/android/view/accessibility/AccessibilityNodeInfo.java +++ b/android/view/accessibility/AccessibilityNodeInfo.java @@ -3874,6 +3874,24 @@ public class AccessibilityNodeInfo implements Parcelable { | FLAG_PREFETCH_DESCENDANTS | FLAG_PREFETCH_SIBLINGS, null); } + /** @hide */ + public static String idToString(long accessibilityId) { + int accessibilityViewId = getAccessibilityViewId(accessibilityId); + int virtualDescendantId = getVirtualDescendantId(accessibilityId); + return virtualDescendantId == AccessibilityNodeProvider.HOST_VIEW_ID + ? idItemToString(accessibilityViewId) + : idItemToString(accessibilityViewId) + ":" + idItemToString(virtualDescendantId); + } + + private static String idItemToString(int item) { + switch (item) { + case ROOT_ITEM_ID: return "ROOT"; + case UNDEFINED_ITEM_ID: return "UNDEFINED"; + case AccessibilityNodeProvider.HOST_VIEW_ID: return "HOST"; + default: return "" + item; + } + } + /** * A class defining an action that can be performed on an {@link AccessibilityNodeInfo}. * Each action has a unique id that is mandatory and optional data. |