summaryrefslogtreecommitdiff
path: root/android/view/accessibility
diff options
context:
space:
mode:
Diffstat (limited to 'android/view/accessibility')
-rw-r--r--android/view/accessibility/AccessibilityEvent.java64
-rw-r--r--android/view/accessibility/AccessibilityInteractionClient.java11
-rw-r--r--android/view/accessibility/AccessibilityManager.java964
-rw-r--r--android/view/accessibility/AccessibilityNodeInfo.java18
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.