summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/view/AccessibilityInteractionController.java351
-rw-r--r--core/java/android/view/accessibility/AccessibilityInteractionClient.java93
-rw-r--r--core/java/android/view/accessibility/IAccessibilityInteractionConnectionCallback.aidl9
-rw-r--r--core/tests/coretests/src/android/view/accessibility/AccessibilityInteractionClientTest.java9
-rw-r--r--services/accessibility/java/com/android/server/accessibility/ActionReplacingCallback.java221
-rw-r--r--services/tests/servicestests/src/com/android/server/accessibility/AccessibilityInteractionControllerNodeRequestsTest.java581
6 files changed, 1026 insertions, 238 deletions
diff --git a/core/java/android/view/AccessibilityInteractionController.java b/core/java/android/view/AccessibilityInteractionController.java
index 09452828057e..e5a137cd13f3 100644
--- a/core/java/android/view/AccessibilityInteractionController.java
+++ b/core/java/android/view/AccessibilityInteractionController.java
@@ -86,6 +86,12 @@ public final class AccessibilityInteractionController {
// accessibility from hanging
private static final long REQUEST_PREPARER_TIMEOUT_MS = 500;
+ // Callbacks should have the same configuration of the flags below to allow satisfying a pending
+ // node request on prefetch
+ private static final int FLAGS_AFFECTING_REPORTED_DATA =
+ AccessibilityNodeInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS
+ | AccessibilityNodeInfo.FLAG_REPORT_VIEW_IDS;
+
private final ArrayList<AccessibilityNodeInfo> mTempAccessibilityNodeInfoList =
new ArrayList<AccessibilityNodeInfo>();
@@ -114,6 +120,9 @@ public final class AccessibilityInteractionController {
private AddNodeInfosForViewId mAddNodeInfosForViewId;
@GuardedBy("mLock")
+ private ArrayList<Message> mPendingFindNodeByIdMessages;
+
+ @GuardedBy("mLock")
private int mNumActiveRequestPreparers;
@GuardedBy("mLock")
private List<MessageHolder> mMessagesWaitingForRequestPreparer;
@@ -128,6 +137,7 @@ public final class AccessibilityInteractionController {
mViewRootImpl = viewRootImpl;
mPrefetcher = new AccessibilityNodePrefetcher();
mA11yManager = mViewRootImpl.mContext.getSystemService(AccessibilityManager.class);
+ mPendingFindNodeByIdMessages = new ArrayList<>();
}
private void scheduleMessage(Message message, int interrogatingPid, long interrogatingTid,
@@ -177,7 +187,11 @@ public final class AccessibilityInteractionController {
args.arg4 = arguments;
message.obj = args;
- scheduleMessage(message, interrogatingPid, interrogatingTid, CONSIDER_REQUEST_PREPARERS);
+ synchronized (mLock) {
+ mPendingFindNodeByIdMessages.add(message);
+ scheduleMessage(message, interrogatingPid, interrogatingTid,
+ CONSIDER_REQUEST_PREPARERS);
+ }
}
/**
@@ -315,6 +329,9 @@ public final class AccessibilityInteractionController {
}
private void findAccessibilityNodeInfoByAccessibilityIdUiThread(Message message) {
+ synchronized (mLock) {
+ mPendingFindNodeByIdMessages.remove(message);
+ }
final int flags = message.arg1;
SomeArgs args = (SomeArgs) message.obj;
@@ -329,22 +346,58 @@ public final class AccessibilityInteractionController {
args.recycle();
- List<AccessibilityNodeInfo> infos = mTempAccessibilityNodeInfoList;
- infos.clear();
+ View rootView = null;
+ AccessibilityNodeInfo rootNode = null;
try {
if (mViewRootImpl.mView == null || mViewRootImpl.mAttachInfo == null) {
return;
}
mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = flags;
- final View root = findViewByAccessibilityId(accessibilityViewId);
- if (root != null && isShown(root)) {
- mPrefetcher.prefetchAccessibilityNodeInfos(
- root, virtualDescendantId, flags, infos, arguments);
+ rootView = findViewByAccessibilityId(accessibilityViewId);
+ if (rootView != null && isShown(rootView)) {
+ rootNode = populateAccessibilityNodeInfoForView(
+ rootView, arguments, virtualDescendantId);
}
} finally {
- updateInfosForViewportAndReturnFindNodeResult(
- infos, callback, interactionId, spec, interactiveRegion);
+ updateInfoForViewportAndReturnFindNodeResult(
+ rootNode == null ? null : AccessibilityNodeInfo.obtain(rootNode),
+ callback, interactionId, spec, interactiveRegion);
}
+ ArrayList<AccessibilityNodeInfo> infos = mTempAccessibilityNodeInfoList;
+ infos.clear();
+ mPrefetcher.prefetchAccessibilityNodeInfos(
+ rootView, rootNode == null ? null : AccessibilityNodeInfo.obtain(rootNode),
+ virtualDescendantId, flags, infos);
+ mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = 0;
+ updateInfosForViewPort(infos, spec, interactiveRegion);
+ returnPrefetchResult(interactionId, infos, callback);
+ returnPendingFindAccessibilityNodeInfosInPrefetch(rootNode, infos, flags);
+ }
+
+ private AccessibilityNodeInfo populateAccessibilityNodeInfoForView(
+ View view, Bundle arguments, int virtualViewId) {
+ AccessibilityNodeProvider provider = view.getAccessibilityNodeProvider();
+ // Determine if we'll be populating extra data
+ final String extraDataRequested = (arguments == null) ? null
+ : arguments.getString(EXTRA_DATA_REQUESTED_KEY);
+ AccessibilityNodeInfo root = null;
+ if (provider == null) {
+ root = view.createAccessibilityNodeInfo();
+ if (root != null) {
+ if (extraDataRequested != null) {
+ view.addExtraDataToAccessibilityNodeInfo(root, extraDataRequested, arguments);
+ }
+ }
+ } else {
+ root = provider.createAccessibilityNodeInfo(virtualViewId);
+ if (root != null) {
+ if (extraDataRequested != null) {
+ provider.addExtraDataToAccessibilityNodeInfo(
+ virtualViewId, root, extraDataRequested, arguments);
+ }
+ }
+ }
+ return root;
}
public void findAccessibilityNodeInfosByViewIdClientThread(long accessibilityNodeId,
@@ -403,6 +456,7 @@ public final class AccessibilityInteractionController {
mAddNodeInfosForViewId.reset();
}
} finally {
+ mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = 0;
updateInfosForViewportAndReturnFindNodeResult(
infos, callback, interactionId, spec, interactiveRegion);
}
@@ -485,6 +539,7 @@ public final class AccessibilityInteractionController {
}
}
} finally {
+ mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = 0;
updateInfosForViewportAndReturnFindNodeResult(
infos, callback, interactionId, spec, interactiveRegion);
}
@@ -576,6 +631,7 @@ public final class AccessibilityInteractionController {
}
}
} finally {
+ mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = 0;
updateInfoForViewportAndReturnFindNodeResult(
focused, callback, interactionId, spec, interactiveRegion);
}
@@ -630,6 +686,7 @@ public final class AccessibilityInteractionController {
}
}
} finally {
+ mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = 0;
updateInfoForViewportAndReturnFindNodeResult(
next, callback, interactionId, spec, interactiveRegion);
}
@@ -786,33 +843,6 @@ public final class AccessibilityInteractionController {
}
}
- private void applyAppScaleAndMagnificationSpecIfNeeded(List<AccessibilityNodeInfo> infos,
- MagnificationSpec spec) {
- if (infos == null) {
- return;
- }
- final float applicationScale = mViewRootImpl.mAttachInfo.mApplicationScale;
- if (shouldApplyAppScaleAndMagnificationSpec(applicationScale, spec)) {
- final int infoCount = infos.size();
- for (int i = 0; i < infoCount; i++) {
- AccessibilityNodeInfo info = infos.get(i);
- applyAppScaleAndMagnificationSpecIfNeeded(info, spec);
- }
- }
- }
-
- private void adjustIsVisibleToUserIfNeeded(List<AccessibilityNodeInfo> infos,
- Region interactiveRegion) {
- if (interactiveRegion == null || infos == null) {
- return;
- }
- final int infoCount = infos.size();
- for (int i = 0; i < infoCount; i++) {
- AccessibilityNodeInfo info = infos.get(i);
- adjustIsVisibleToUserIfNeeded(info, interactiveRegion);
- }
- }
-
private void adjustIsVisibleToUserIfNeeded(AccessibilityNodeInfo info,
Region interactiveRegion) {
if (interactiveRegion == null || info == null) {
@@ -833,17 +863,6 @@ public final class AccessibilityInteractionController {
return false;
}
- private void adjustBoundsInScreenIfNeeded(List<AccessibilityNodeInfo> infos) {
- if (infos == null || shouldBypassAdjustBoundsInScreen()) {
- return;
- }
- final int infoCount = infos.size();
- for (int i = 0; i < infoCount; i++) {
- final AccessibilityNodeInfo info = infos.get(i);
- adjustBoundsInScreenIfNeeded(info);
- }
- }
-
private void adjustBoundsInScreenIfNeeded(AccessibilityNodeInfo info) {
if (info == null || shouldBypassAdjustBoundsInScreen()) {
return;
@@ -891,17 +910,6 @@ public final class AccessibilityInteractionController {
return screenMatrix == null || screenMatrix.isIdentity();
}
- private void associateLeashedParentIfNeeded(List<AccessibilityNodeInfo> infos) {
- if (infos == null || shouldBypassAssociateLeashedParent()) {
- return;
- }
- final int infoCount = infos.size();
- for (int i = 0; i < infoCount; i++) {
- final AccessibilityNodeInfo info = infos.get(i);
- associateLeashedParentIfNeeded(info);
- }
- }
-
private void associateLeashedParentIfNeeded(AccessibilityNodeInfo info) {
if (info == null || shouldBypassAssociateLeashedParent()) {
return;
@@ -975,18 +983,46 @@ public final class AccessibilityInteractionController {
return (appScale != 1.0f || (spec != null && !spec.isNop()));
}
+ private void updateInfosForViewPort(List<AccessibilityNodeInfo> infos, MagnificationSpec spec,
+ Region interactiveRegion) {
+ for (int i = 0; i < infos.size(); i++) {
+ updateInfoForViewPort(infos.get(i), spec, interactiveRegion);
+ }
+ }
+
+ private void updateInfoForViewPort(AccessibilityNodeInfo info, MagnificationSpec spec,
+ Region interactiveRegion) {
+ associateLeashedParentIfNeeded(info);
+ applyScreenMatrixIfNeeded(info);
+ adjustBoundsInScreenIfNeeded(info);
+ // To avoid applyAppScaleAndMagnificationSpecIfNeeded changing the bounds of node,
+ // then impact the visibility result, we need to adjust visibility before apply scale.
+ adjustIsVisibleToUserIfNeeded(info, interactiveRegion);
+ applyAppScaleAndMagnificationSpecIfNeeded(info, spec);
+ }
+
private void updateInfosForViewportAndReturnFindNodeResult(List<AccessibilityNodeInfo> infos,
IAccessibilityInteractionConnectionCallback callback, int interactionId,
MagnificationSpec spec, Region interactiveRegion) {
+ if (infos != null) {
+ updateInfosForViewPort(infos, spec, interactiveRegion);
+ }
+ returnFindNodesResult(infos, callback, interactionId);
+ }
+
+ private void returnFindNodeResult(AccessibilityNodeInfo info,
+ IAccessibilityInteractionConnectionCallback callback,
+ int interactionId) {
+ try {
+ callback.setFindAccessibilityNodeInfoResult(info, interactionId);
+ } catch (RemoteException re) {
+ /* ignore - the other side will time out */
+ }
+ }
+
+ private void returnFindNodesResult(List<AccessibilityNodeInfo> infos,
+ IAccessibilityInteractionConnectionCallback callback, int interactionId) {
try {
- mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = 0;
- associateLeashedParentIfNeeded(infos);
- applyScreenMatrixIfNeeded(infos);
- adjustBoundsInScreenIfNeeded(infos);
- // To avoid applyAppScaleAndMagnificationSpecIfNeeded changing the bounds of node,
- // then impact the visibility result, we need to adjust visibility before apply scale.
- adjustIsVisibleToUserIfNeeded(infos, interactiveRegion);
- applyAppScaleAndMagnificationSpecIfNeeded(infos, spec);
callback.setFindAccessibilityNodeInfosResult(infos, interactionId);
if (infos != null) {
infos.clear();
@@ -996,22 +1032,80 @@ public final class AccessibilityInteractionController {
}
}
+ private void returnPendingFindAccessibilityNodeInfosInPrefetch(AccessibilityNodeInfo rootNode,
+ List<AccessibilityNodeInfo> infos, int flags) {
+
+ AccessibilityNodeInfo satisfiedPendingRequestPrefetchedNode = null;
+ IAccessibilityInteractionConnectionCallback satisfiedPendingRequestCallback = null;
+ int satisfiedPendingRequestInteractionId = AccessibilityInteractionClient.NO_ID;
+
+ synchronized (mLock) {
+ for (int i = 0; i < mPendingFindNodeByIdMessages.size(); i++) {
+ final Message pendingMessage = mPendingFindNodeByIdMessages.get(i);
+ final int pendingFlags = pendingMessage.arg1;
+ if ((pendingFlags & FLAGS_AFFECTING_REPORTED_DATA)
+ != (flags & FLAGS_AFFECTING_REPORTED_DATA)) {
+ continue;
+ }
+ SomeArgs args = (SomeArgs) pendingMessage.obj;
+ final int accessibilityViewId = args.argi1;
+ final int virtualDescendantId = args.argi2;
+
+ satisfiedPendingRequestPrefetchedNode = nodeWithIdFromList(rootNode,
+ infos, AccessibilityNodeInfo.makeNodeId(
+ accessibilityViewId, virtualDescendantId));
+
+ if (satisfiedPendingRequestPrefetchedNode != null) {
+ satisfiedPendingRequestCallback =
+ (IAccessibilityInteractionConnectionCallback) args.arg1;
+ satisfiedPendingRequestInteractionId = args.argi3;
+ mHandler.removeMessages(
+ PrivateHandler.MSG_FIND_ACCESSIBILITY_NODE_INFO_BY_ACCESSIBILITY_ID,
+ pendingMessage.obj);
+ args.recycle();
+ break;
+ }
+ }
+ mPendingFindNodeByIdMessages.clear();
+ }
+
+ if (satisfiedPendingRequestPrefetchedNode != null) {
+ returnFindNodeResult(
+ AccessibilityNodeInfo.obtain(satisfiedPendingRequestPrefetchedNode),
+ satisfiedPendingRequestCallback, satisfiedPendingRequestInteractionId);
+ }
+ }
+
+ private AccessibilityNodeInfo nodeWithIdFromList(AccessibilityNodeInfo rootNode,
+ List<AccessibilityNodeInfo> infos, long nodeId) {
+ if (rootNode != null && rootNode.getSourceNodeId() == nodeId) {
+ return rootNode;
+ }
+ for (int j = 0; j < infos.size(); j++) {
+ AccessibilityNodeInfo info = infos.get(j);
+ if (info.getSourceNodeId() == nodeId) {
+ return info;
+ }
+ }
+ return null;
+ }
+
+ private void returnPrefetchResult(int interactionId, List<AccessibilityNodeInfo> infos,
+ IAccessibilityInteractionConnectionCallback callback) {
+ if (infos.size() > 0) {
+ try {
+ callback.setPrefetchAccessibilityNodeInfoResult(infos, interactionId);
+ } catch (RemoteException re) {
+ /* ignore - other side isn't too bothered if this doesn't arrive */
+ }
+ }
+ }
+
private void updateInfoForViewportAndReturnFindNodeResult(AccessibilityNodeInfo info,
IAccessibilityInteractionConnectionCallback callback, int interactionId,
MagnificationSpec spec, Region interactiveRegion) {
- try {
- mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = 0;
- associateLeashedParentIfNeeded(info);
- applyScreenMatrixIfNeeded(info);
- adjustBoundsInScreenIfNeeded(info);
- // To avoid applyAppScaleAndMagnificationSpecIfNeeded changing the bounds of node,
- // then impact the visibility result, we need to adjust visibility before apply scale.
- adjustIsVisibleToUserIfNeeded(info, interactiveRegion);
- applyAppScaleAndMagnificationSpecIfNeeded(info, spec);
- callback.setFindAccessibilityNodeInfoResult(info, interactionId);
- } catch (RemoteException re) {
- /* ignore - the other side will time out */
- }
+ updateInfoForViewPort(info, spec, interactiveRegion);
+ returnFindNodeResult(info, callback, interactionId);
}
private boolean handleClickableSpanActionUiThread(
@@ -1054,56 +1148,45 @@ public final class AccessibilityInteractionController {
private final ArrayList<View> mTempViewList = new ArrayList<View>();
- public void prefetchAccessibilityNodeInfos(View view, int virtualViewId, int fetchFlags,
- List<AccessibilityNodeInfo> outInfos, Bundle arguments) {
+ public void prefetchAccessibilityNodeInfos(View view, AccessibilityNodeInfo root,
+ int virtualViewId, int fetchFlags, List<AccessibilityNodeInfo> outInfos) {
+ if (root == null) {
+ return;
+ }
AccessibilityNodeProvider provider = view.getAccessibilityNodeProvider();
- // Determine if we'll be populating extra data
- final String extraDataRequested = (arguments == null) ? null
- : arguments.getString(EXTRA_DATA_REQUESTED_KEY);
if (provider == null) {
- AccessibilityNodeInfo root = view.createAccessibilityNodeInfo();
- if (root != null) {
- if (extraDataRequested != null) {
- view.addExtraDataToAccessibilityNodeInfo(
- root, extraDataRequested, arguments);
- }
- outInfos.add(root);
- if ((fetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_PREDECESSORS) != 0) {
- prefetchPredecessorsOfRealNode(view, outInfos);
- }
- if ((fetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_SIBLINGS) != 0) {
- prefetchSiblingsOfRealNode(view, outInfos);
- }
- if ((fetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS) != 0) {
- prefetchDescendantsOfRealNode(view, outInfos);
- }
+ if ((fetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_PREDECESSORS) != 0) {
+ prefetchPredecessorsOfRealNode(view, outInfos);
+ }
+ if ((fetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_SIBLINGS) != 0) {
+ prefetchSiblingsOfRealNode(view, outInfos);
+ }
+ if ((fetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS) != 0) {
+ prefetchDescendantsOfRealNode(view, outInfos);
}
} else {
- final AccessibilityNodeInfo root =
- provider.createAccessibilityNodeInfo(virtualViewId);
- if (root != null) {
- if (extraDataRequested != null) {
- provider.addExtraDataToAccessibilityNodeInfo(
- virtualViewId, root, extraDataRequested, arguments);
- }
- outInfos.add(root);
- if ((fetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_PREDECESSORS) != 0) {
- prefetchPredecessorsOfVirtualNode(root, view, provider, outInfos);
- }
- if ((fetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_SIBLINGS) != 0) {
- prefetchSiblingsOfVirtualNode(root, view, provider, outInfos);
- }
- if ((fetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS) != 0) {
- prefetchDescendantsOfVirtualNode(root, provider, outInfos);
- }
+ if ((fetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_PREDECESSORS) != 0) {
+ prefetchPredecessorsOfVirtualNode(root, view, provider, outInfos);
+ }
+ if ((fetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_SIBLINGS) != 0) {
+ prefetchSiblingsOfVirtualNode(root, view, provider, outInfos);
+ }
+ if ((fetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS) != 0) {
+ prefetchDescendantsOfVirtualNode(root, provider, outInfos);
}
}
if (ENFORCE_NODE_TREE_CONSISTENT) {
- enforceNodeTreeConsistent(outInfos);
+ enforceNodeTreeConsistent(root, outInfos);
}
}
- private void enforceNodeTreeConsistent(List<AccessibilityNodeInfo> nodes) {
+ private boolean shouldStopPrefetching(List prefetchededInfos) {
+ return mHandler.hasUserInteractiveMessagesWaiting()
+ || prefetchededInfos.size() >= MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE;
+ }
+
+ private void enforceNodeTreeConsistent(
+ AccessibilityNodeInfo root, List<AccessibilityNodeInfo> nodes) {
LongSparseArray<AccessibilityNodeInfo> nodeMap =
new LongSparseArray<AccessibilityNodeInfo>();
final int nodeCount = nodes.size();
@@ -1114,7 +1197,6 @@ public final class AccessibilityInteractionController {
// If the nodes are a tree it does not matter from
// which node we start to search for the root.
- AccessibilityNodeInfo root = nodeMap.valueAt(0);
AccessibilityNodeInfo parent = root;
while (parent != null) {
root = parent;
@@ -1181,9 +1263,11 @@ public final class AccessibilityInteractionController {
private void prefetchPredecessorsOfRealNode(View view,
List<AccessibilityNodeInfo> outInfos) {
+ if (shouldStopPrefetching(outInfos)) {
+ return;
+ }
ViewParent parent = view.getParentForAccessibility();
- while (parent instanceof View
- && outInfos.size() < MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) {
+ while (parent instanceof View && !shouldStopPrefetching(outInfos)) {
View parentView = (View) parent;
AccessibilityNodeInfo info = parentView.createAccessibilityNodeInfo();
if (info != null) {
@@ -1195,6 +1279,9 @@ public final class AccessibilityInteractionController {
private void prefetchSiblingsOfRealNode(View current,
List<AccessibilityNodeInfo> outInfos) {
+ if (shouldStopPrefetching(outInfos)) {
+ return;
+ }
ViewParent parent = current.getParentForAccessibility();
if (parent instanceof ViewGroup) {
ViewGroup parentGroup = (ViewGroup) parent;
@@ -1204,7 +1291,7 @@ public final class AccessibilityInteractionController {
parentGroup.addChildrenForAccessibility(children);
final int childCount = children.size();
for (int i = 0; i < childCount; i++) {
- if (outInfos.size() >= MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) {
+ if (shouldStopPrefetching(outInfos)) {
return;
}
View child = children.get(i);
@@ -1232,7 +1319,7 @@ public final class AccessibilityInteractionController {
private void prefetchDescendantsOfRealNode(View root,
List<AccessibilityNodeInfo> outInfos) {
- if (!(root instanceof ViewGroup)) {
+ if (shouldStopPrefetching(outInfos) || !(root instanceof ViewGroup)) {
return;
}
HashMap<View, AccessibilityNodeInfo> addedChildren =
@@ -1243,7 +1330,7 @@ public final class AccessibilityInteractionController {
root.addChildrenForAccessibility(children);
final int childCount = children.size();
for (int i = 0; i < childCount; i++) {
- if (outInfos.size() >= MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) {
+ if (shouldStopPrefetching(outInfos)) {
return;
}
View child = children.get(i);
@@ -1268,7 +1355,7 @@ public final class AccessibilityInteractionController {
} finally {
children.clear();
}
- if (outInfos.size() < MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) {
+ if (!shouldStopPrefetching(outInfos)) {
for (Map.Entry<View, AccessibilityNodeInfo> entry : addedChildren.entrySet()) {
View addedChild = entry.getKey();
AccessibilityNodeInfo virtualRoot = entry.getValue();
@@ -1290,7 +1377,7 @@ public final class AccessibilityInteractionController {
long parentNodeId = root.getParentNodeId();
int accessibilityViewId = AccessibilityNodeInfo.getAccessibilityViewId(parentNodeId);
while (accessibilityViewId != AccessibilityNodeInfo.UNDEFINED_ITEM_ID) {
- if (outInfos.size() >= MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) {
+ if (shouldStopPrefetching(outInfos)) {
return;
}
final int virtualDescendantId =
@@ -1335,7 +1422,7 @@ public final class AccessibilityInteractionController {
if (parent != null) {
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
- if (outInfos.size() >= MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) {
+ if (shouldStopPrefetching(outInfos)) {
return;
}
final long childNodeId = parent.getChildId(i);
@@ -1360,7 +1447,7 @@ public final class AccessibilityInteractionController {
final int initialOutInfosSize = outInfos.size();
final int childCount = root.getChildCount();
for (int i = 0; i < childCount; i++) {
- if (outInfos.size() >= MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) {
+ if (shouldStopPrefetching(outInfos)) {
return;
}
final long childNodeId = root.getChildId(i);
@@ -1370,7 +1457,7 @@ public final class AccessibilityInteractionController {
outInfos.add(child);
}
}
- if (outInfos.size() < MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) {
+ if (!shouldStopPrefetching(outInfos)) {
final int addedChildCount = outInfos.size() - initialOutInfosSize;
for (int i = 0; i < addedChildCount; i++) {
AccessibilityNodeInfo child = outInfos.get(initialOutInfosSize + i);
@@ -1479,6 +1566,10 @@ public final class AccessibilityInteractionController {
boolean hasAccessibilityCallback(Message message) {
return message.what < FIRST_NO_ACCESSIBILITY_CALLBACK_MSG ? true : false;
}
+
+ boolean hasUserInteractiveMessagesWaiting() {
+ return hasMessagesOrCallbacks();
+ }
}
private final class AddNodeInfosForViewId implements Predicate<View> {
diff --git a/core/java/android/view/accessibility/AccessibilityInteractionClient.java b/core/java/android/view/accessibility/AccessibilityInteractionClient.java
index f63749be6df2..8d1271d7311c 100644
--- a/core/java/android/view/accessibility/AccessibilityInteractionClient.java
+++ b/core/java/android/view/accessibility/AccessibilityInteractionClient.java
@@ -23,7 +23,9 @@ import android.compat.annotation.UnsupportedAppUsage;
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
+import android.os.Handler;
import android.os.IBinder;
+import android.os.Looper;
import android.os.Message;
import android.os.Process;
import android.os.RemoteException;
@@ -113,6 +115,8 @@ public final class AccessibilityInteractionClient
private final Object mInstanceLock = new Object();
+ private Handler mMainHandler;
+
private volatile int mInteractionId = -1;
private AccessibilityNodeInfo mFindAccessibilityNodeInfoResult;
@@ -123,6 +127,11 @@ public final class AccessibilityInteractionClient
private Message mSameThreadMessage;
+ private int mInteractionIdWaitingForPrefetchResult;
+ private int mConnectionIdWaitingForPrefetchResult;
+ private String[] mPackageNamesForNextPrefetchResult;
+ private Runnable mPrefetchResultRunnable;
+
/**
* @return The client for the current thread.
*/
@@ -197,6 +206,10 @@ public final class AccessibilityInteractionClient
private AccessibilityInteractionClient() {
/* reducing constructor visibility */
+ Looper mainLooper = Looper.getMainLooper();
+ if (mainLooper != null) {
+ mMainHandler = new Handler(mainLooper);
+ }
}
/**
@@ -451,16 +464,16 @@ public final class AccessibilityInteractionClient
Binder.restoreCallingIdentity(identityToken);
}
if (packageNames != null) {
- List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear(
- interactionId);
- finalizeAndCacheAccessibilityNodeInfos(infos, connectionId,
- bypassCache, packageNames);
- if (infos != null && !infos.isEmpty()) {
- for (int i = 1; i < infos.size(); i++) {
- infos.get(i).recycle();
- }
- return infos.get(0);
+ AccessibilityNodeInfo info =
+ getFindAccessibilityNodeInfoResultAndClear(interactionId);
+ if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_MASK) != 0
+ && info != null) {
+ setInteractionWaitingForPrefetchResult(interactionId, connectionId,
+ packageNames);
}
+ finalizeAndCacheAccessibilityNodeInfo(info, connectionId,
+ bypassCache, packageNames);
+ return info;
}
} else {
if (DEBUG) {
@@ -474,6 +487,15 @@ public final class AccessibilityInteractionClient
return null;
}
+ private void setInteractionWaitingForPrefetchResult(int interactionId, int connectionId,
+ String[] packageNames) {
+ synchronized (mInstanceLock) {
+ mInteractionIdWaitingForPrefetchResult = interactionId;
+ mConnectionIdWaitingForPrefetchResult = connectionId;
+ mPackageNamesForNextPrefetchResult = packageNames;
+ }
+ }
+
private static String idToString(int accessibilityWindowId, long accessibilityNodeId) {
return accessibilityWindowId + "/"
+ AccessibilityNodeInfo.idToString(accessibilityNodeId);
@@ -829,6 +851,59 @@ public final class AccessibilityInteractionClient
}
/**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setPrefetchAccessibilityNodeInfoResult(@NonNull List<AccessibilityNodeInfo> infos,
+ int interactionId) {
+ List<AccessibilityNodeInfo> infosCopy = null;
+ int mConnectionIdWaitingForPrefetchResultCopy = -1;
+ String[] mPackageNamesForNextPrefetchResultCopy = null;
+
+ synchronized (mInstanceLock) {
+ if (!infos.isEmpty() && mInteractionIdWaitingForPrefetchResult == interactionId) {
+ if (mMainHandler != null) {
+ if (mPrefetchResultRunnable != null) {
+ mMainHandler.removeCallbacks(mPrefetchResultRunnable);
+ mPrefetchResultRunnable = null;
+ }
+ /**
+ * TODO(b/180957109): AccessibilityCache is prone to deadlocks
+ * We post caching the prefetched nodes in the main thread. Using the binder
+ * thread results in "Long monitor contention with owner main" logs where
+ * service response times may exceed 5 seconds. This is due to the cache calling
+ * out to the system when refreshing nodes with the lock held.
+ */
+ mPrefetchResultRunnable = () -> finalizeAndCacheAccessibilityNodeInfos(
+ infos, mConnectionIdWaitingForPrefetchResult, false,
+ mPackageNamesForNextPrefetchResult);
+ mMainHandler.post(mPrefetchResultRunnable);
+
+ } else {
+ for (AccessibilityNodeInfo info : infos) {
+ infosCopy.add(new AccessibilityNodeInfo(info));
+ }
+ mConnectionIdWaitingForPrefetchResultCopy =
+ mConnectionIdWaitingForPrefetchResult;
+ mPackageNamesForNextPrefetchResultCopy =
+ new String[mPackageNamesForNextPrefetchResult.length];
+ for (int i = 0; i < mPackageNamesForNextPrefetchResult.length; i++) {
+ mPackageNamesForNextPrefetchResultCopy[i] =
+ mPackageNamesForNextPrefetchResult[i];
+ }
+ }
+ }
+
+ }
+
+ if (infosCopy != null) {
+ finalizeAndCacheAccessibilityNodeInfos(
+ infosCopy, mConnectionIdWaitingForPrefetchResultCopy, false,
+ mPackageNamesForNextPrefetchResultCopy);
+ }
+ }
+
+ /**
* Gets the result of a request to perform an accessibility action.
*
* @param interactionId The interaction id to match the result with the request.
diff --git a/core/java/android/view/accessibility/IAccessibilityInteractionConnectionCallback.aidl b/core/java/android/view/accessibility/IAccessibilityInteractionConnectionCallback.aidl
index 049bb31adbb1..231e75a19a06 100644
--- a/core/java/android/view/accessibility/IAccessibilityInteractionConnectionCallback.aidl
+++ b/core/java/android/view/accessibility/IAccessibilityInteractionConnectionCallback.aidl
@@ -47,6 +47,15 @@ oneway interface IAccessibilityInteractionConnectionCallback {
int interactionId);
/**
+ * Sets the result of a prefetch request that returns {@link AccessibilityNodeInfo}s.
+ *
+ * @param root The {@link AccessibilityNodeInfo} for which the prefetching is based off of.
+ * @param infos The result {@link AccessibilityNodeInfo}s.
+ */
+ void setPrefetchAccessibilityNodeInfoResult(
+ in List<AccessibilityNodeInfo> infos, int interactionId);
+
+ /**
* Sets the result of a request to perform an accessibility action.
*
* @param Whether the action was performed.
diff --git a/core/tests/coretests/src/android/view/accessibility/AccessibilityInteractionClientTest.java b/core/tests/coretests/src/android/view/accessibility/AccessibilityInteractionClientTest.java
index ab24f89015c7..7e1e7f4bdd7f 100644
--- a/core/tests/coretests/src/android/view/accessibility/AccessibilityInteractionClientTest.java
+++ b/core/tests/coretests/src/android/view/accessibility/AccessibilityInteractionClientTest.java
@@ -33,9 +33,6 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
-import java.util.Arrays;
-import java.util.List;
-
/**
* Tests for AccessibilityInteractionClient
*/
@@ -65,7 +62,7 @@ public class AccessibilityInteractionClientTest {
final long accessibilityNodeId = 0x4321L;
AccessibilityNodeInfo nodeFromConnection = AccessibilityNodeInfo.obtain();
nodeFromConnection.setSourceNodeId(accessibilityNodeId, windowId);
- mMockConnection.mInfosToReturn = Arrays.asList(nodeFromConnection);
+ mMockConnection.mInfoToReturn = nodeFromConnection;
AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance();
AccessibilityNodeInfo node = client.findAccessibilityNodeInfoByAccessibilityId(
MOCK_CONNECTION_ID, windowId, accessibilityNodeId, true, 0, null);
@@ -75,7 +72,7 @@ public class AccessibilityInteractionClientTest {
}
private static class MockConnection extends AccessibilityServiceConnectionImpl {
- List<AccessibilityNodeInfo> mInfosToReturn;
+ AccessibilityNodeInfo mInfoToReturn;
@Override
public String[] findAccessibilityNodeInfoByAccessibilityId(int accessibilityWindowId,
@@ -83,7 +80,7 @@ public class AccessibilityInteractionClientTest {
IAccessibilityInteractionConnectionCallback callback, int flags, long threadId,
Bundle arguments) {
try {
- callback.setFindAccessibilityNodeInfosResult(mInfosToReturn, interactionId);
+ callback.setFindAccessibilityNodeInfoResult(mInfoToReturn, interactionId);
} catch (RemoteException e) {
throw new RuntimeException(e);
}
diff --git a/services/accessibility/java/com/android/server/accessibility/ActionReplacingCallback.java b/services/accessibility/java/com/android/server/accessibility/ActionReplacingCallback.java
index bafb641dcc9e..6828dd916701 100644
--- a/services/accessibility/java/com/android/server/accessibility/ActionReplacingCallback.java
+++ b/services/accessibility/java/com/android/server/accessibility/ActionReplacingCallback.java
@@ -40,29 +40,34 @@ public class ActionReplacingCallback extends IAccessibilityInteractionConnection
private final IAccessibilityInteractionConnectionCallback mServiceCallback;
private final IAccessibilityInteractionConnection mConnectionWithReplacementActions;
private final int mInteractionId;
+ private final int mNodeWithReplacementActionsInteractionId;
private final Object mLock = new Object();
@GuardedBy("mLock")
- List<AccessibilityNodeInfo> mNodesWithReplacementActions;
+ private boolean mReplacementNodeIsReadyOrFailed;
+
+ @GuardedBy("mLock")
+ AccessibilityNodeInfo mNodeWithReplacementActions;
@GuardedBy("mLock")
List<AccessibilityNodeInfo> mNodesFromOriginalWindow;
@GuardedBy("mLock")
+ boolean mSetFindNodeFromOriginalWindowCalled = false;
+
+ @GuardedBy("mLock")
AccessibilityNodeInfo mNodeFromOriginalWindow;
- // Keep track of whether or not we've been called back for a single node
@GuardedBy("mLock")
- boolean mSingleNodeCallbackHappened;
+ boolean mSetFindNodesFromOriginalWindowCalled = false;
+
- // Keep track of whether or not we've been called back for multiple node
@GuardedBy("mLock")
- boolean mMultiNodeCallbackHappened;
+ List<AccessibilityNodeInfo> mPrefetchedNodesFromOriginalWindow;
- // We shouldn't get any more callbacks after we've called back the original service, but
- // keep track to make sure we catch such strange things
@GuardedBy("mLock")
- boolean mDone;
+ boolean mSetPrefetchFromOriginalWindowCalled = false;
+
public ActionReplacingCallback(IAccessibilityInteractionConnectionCallback serviceCallback,
IAccessibilityInteractionConnection connectionWithReplacementActions,
@@ -70,19 +75,20 @@ public class ActionReplacingCallback extends IAccessibilityInteractionConnection
mServiceCallback = serviceCallback;
mConnectionWithReplacementActions = connectionWithReplacementActions;
mInteractionId = interactionId;
+ mNodeWithReplacementActionsInteractionId = interactionId + 1;
// Request the root node of the replacing window
final long identityToken = Binder.clearCallingIdentity();
try {
mConnectionWithReplacementActions.findAccessibilityNodeInfoByAccessibilityId(
- AccessibilityNodeInfo.ROOT_NODE_ID, null, interactionId + 1, this, 0,
+ AccessibilityNodeInfo.ROOT_NODE_ID, null,
+ mNodeWithReplacementActionsInteractionId, this, 0,
interrogatingPid, interrogatingTid, null, null);
} catch (RemoteException re) {
if (DEBUG) {
Slog.e(LOG_TAG, "Error calling findAccessibilityNodeInfoByAccessibilityId()");
}
- // Pretend we already got a (null) list of replacement nodes
- mMultiNodeCallbackHappened = true;
+ mReplacementNodeIsReadyOrFailed = true;
} finally {
Binder.restoreCallingIdentity(identityToken);
}
@@ -90,46 +96,67 @@ public class ActionReplacingCallback extends IAccessibilityInteractionConnection
@Override
public void setFindAccessibilityNodeInfoResult(AccessibilityNodeInfo info, int interactionId) {
- boolean readyForCallback;
- synchronized(mLock) {
+ synchronized (mLock) {
if (interactionId == mInteractionId) {
mNodeFromOriginalWindow = info;
+ mSetFindNodeFromOriginalWindowCalled = true;
+ } else if (interactionId == mNodeWithReplacementActionsInteractionId) {
+ mNodeWithReplacementActions = info;
+ mReplacementNodeIsReadyOrFailed = true;
} else {
Slog.e(LOG_TAG, "Callback with unexpected interactionId");
return;
}
-
- mSingleNodeCallbackHappened = true;
- readyForCallback = mMultiNodeCallbackHappened;
- }
- if (readyForCallback) {
- replaceInfoActionsAndCallService();
}
+ replaceInfoActionsAndCallServiceIfReady();
}
@Override
public void setFindAccessibilityNodeInfosResult(List<AccessibilityNodeInfo> infos,
int interactionId) {
- boolean callbackForSingleNode;
- boolean callbackForMultipleNodes;
- synchronized(mLock) {
+ synchronized (mLock) {
if (interactionId == mInteractionId) {
mNodesFromOriginalWindow = infos;
- } else if (interactionId == mInteractionId + 1) {
- mNodesWithReplacementActions = infos;
+ mSetFindNodesFromOriginalWindowCalled = true;
+ } else if (interactionId == mNodeWithReplacementActionsInteractionId) {
+ setNodeWithReplacementActionsFromList(infos);
+ mReplacementNodeIsReadyOrFailed = true;
} else {
Slog.e(LOG_TAG, "Callback with unexpected interactionId");
return;
}
- callbackForSingleNode = mSingleNodeCallbackHappened;
- callbackForMultipleNodes = mMultiNodeCallbackHappened;
- mMultiNodeCallbackHappened = true;
}
- if (callbackForSingleNode) {
- replaceInfoActionsAndCallService();
+ replaceInfoActionsAndCallServiceIfReady();
+ }
+
+ @Override
+ public void setPrefetchAccessibilityNodeInfoResult(List<AccessibilityNodeInfo> infos,
+ int interactionId)
+ throws RemoteException {
+ synchronized (mLock) {
+ if (interactionId == mInteractionId) {
+ mPrefetchedNodesFromOriginalWindow = infos;
+ mSetPrefetchFromOriginalWindowCalled = true;
+ } else {
+ Slog.e(LOG_TAG, "Callback with unexpected interactionId");
+ return;
+ }
}
- if (callbackForMultipleNodes) {
- replaceInfosActionsAndCallService();
+ replaceInfoActionsAndCallServiceIfReady();
+ }
+
+ private void replaceInfoActionsAndCallServiceIfReady() {
+ replaceInfoActionsAndCallService();
+ replaceInfosActionsAndCallService();
+ replacePrefetchInfosActionsAndCallService();
+ }
+
+ private void setNodeWithReplacementActionsFromList(List<AccessibilityNodeInfo> infos) {
+ for (int i = 0; i < infos.size(); i++) {
+ AccessibilityNodeInfo info = infos.get(i);
+ if (info.getSourceNodeId() == AccessibilityNodeInfo.ROOT_NODE_ID) {
+ mNodeWithReplacementActions = info;
+ }
}
}
@@ -142,55 +169,81 @@ public class ActionReplacingCallback extends IAccessibilityInteractionConnection
private void replaceInfoActionsAndCallService() {
final AccessibilityNodeInfo nodeToReturn;
+ boolean doCallback = false;
synchronized (mLock) {
- if (mDone) {
- if (DEBUG) {
- Slog.e(LOG_TAG, "Extra callback");
- }
- return;
- }
- if (mNodeFromOriginalWindow != null) {
+ doCallback = mReplacementNodeIsReadyOrFailed
+ && mSetFindNodeFromOriginalWindowCalled;
+ if (doCallback && mNodeFromOriginalWindow != null) {
replaceActionsOnInfoLocked(mNodeFromOriginalWindow);
+ mSetFindNodeFromOriginalWindowCalled = false;
}
- recycleReplaceActionNodesLocked();
nodeToReturn = mNodeFromOriginalWindow;
- mDone = true;
}
- try {
- mServiceCallback.setFindAccessibilityNodeInfoResult(nodeToReturn, mInteractionId);
- } catch (RemoteException re) {
- if (DEBUG) {
- Slog.e(LOG_TAG, "Failed to setFindAccessibilityNodeInfoResult");
+ if (doCallback) {
+ try {
+ mServiceCallback.setFindAccessibilityNodeInfoResult(nodeToReturn, mInteractionId);
+ } catch (RemoteException re) {
+ if (DEBUG) {
+ Slog.e(LOG_TAG, "Failed to setFindAccessibilityNodeInfoResult");
+ }
}
}
}
private void replaceInfosActionsAndCallService() {
- final List<AccessibilityNodeInfo> nodesToReturn;
+ List<AccessibilityNodeInfo> nodesToReturn = null;
+ boolean doCallback = false;
synchronized (mLock) {
- if (mDone) {
+ doCallback = mReplacementNodeIsReadyOrFailed
+ && mSetFindNodesFromOriginalWindowCalled;
+ if (doCallback) {
+ nodesToReturn = replaceActionsLocked(mNodesFromOriginalWindow);
+ mSetFindNodesFromOriginalWindowCalled = false;
+ }
+ }
+ if (doCallback) {
+ try {
+ mServiceCallback.setFindAccessibilityNodeInfosResult(nodesToReturn, mInteractionId);
+ } catch (RemoteException re) {
if (DEBUG) {
- Slog.e(LOG_TAG, "Extra callback");
+ Slog.e(LOG_TAG, "Failed to setFindAccessibilityNodeInfosResult");
}
- return;
}
- if (mNodesFromOriginalWindow != null) {
- for (int i = 0; i < mNodesFromOriginalWindow.size(); i++) {
- replaceActionsOnInfoLocked(mNodesFromOriginalWindow.get(i));
+ }
+ }
+
+ private void replacePrefetchInfosActionsAndCallService() {
+ List<AccessibilityNodeInfo> nodesToReturn = null;
+ boolean doCallback = false;
+ synchronized (mLock) {
+ doCallback = mReplacementNodeIsReadyOrFailed
+ && mSetPrefetchFromOriginalWindowCalled;
+ if (doCallback) {
+ nodesToReturn = replaceActionsLocked(mPrefetchedNodesFromOriginalWindow);
+ mSetPrefetchFromOriginalWindowCalled = false;
+ }
+ }
+ if (doCallback) {
+ try {
+ mServiceCallback.setPrefetchAccessibilityNodeInfoResult(
+ nodesToReturn, mInteractionId);
+ } catch (RemoteException re) {
+ if (DEBUG) {
+ Slog.e(LOG_TAG, "Failed to setFindAccessibilityNodeInfosResult");
}
}
- recycleReplaceActionNodesLocked();
- nodesToReturn = (mNodesFromOriginalWindow == null)
- ? null : new ArrayList<>(mNodesFromOriginalWindow);
- mDone = true;
}
- try {
- mServiceCallback.setFindAccessibilityNodeInfosResult(nodesToReturn, mInteractionId);
- } catch (RemoteException re) {
- if (DEBUG) {
- Slog.e(LOG_TAG, "Failed to setFindAccessibilityNodeInfosResult");
+ }
+
+ @GuardedBy("mLock")
+ private List<AccessibilityNodeInfo> replaceActionsLocked(List<AccessibilityNodeInfo> infos) {
+ if (infos != null) {
+ for (int i = 0; i < infos.size(); i++) {
+ replaceActionsOnInfoLocked(infos.get(i));
}
}
+ return (infos == null)
+ ? null : new ArrayList<>(infos);
}
@GuardedBy("mLock")
@@ -204,40 +257,22 @@ public class ActionReplacingCallback extends IAccessibilityInteractionConnection
info.setDismissable(false);
// We currently only replace actions for the root node
if ((info.getSourceNodeId() == AccessibilityNodeInfo.ROOT_NODE_ID)
- && mNodesWithReplacementActions != null) {
- // This list should always contain a single node with the root ID
- for (int i = 0; i < mNodesWithReplacementActions.size(); i++) {
- AccessibilityNodeInfo nodeWithReplacementActions =
- mNodesWithReplacementActions.get(i);
- if (nodeWithReplacementActions.getSourceNodeId()
- == AccessibilityNodeInfo.ROOT_NODE_ID) {
- List<AccessibilityAction> actions = nodeWithReplacementActions.getActionList();
- if (actions != null) {
- for (int j = 0; j < actions.size(); j++) {
- info.addAction(actions.get(j));
- }
- // The PIP needs to be able to take accessibility focus
- info.addAction(AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS);
- info.addAction(AccessibilityAction.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
- }
- info.setClickable(nodeWithReplacementActions.isClickable());
- info.setFocusable(nodeWithReplacementActions.isFocusable());
- info.setContextClickable(nodeWithReplacementActions.isContextClickable());
- info.setScrollable(nodeWithReplacementActions.isScrollable());
- info.setLongClickable(nodeWithReplacementActions.isLongClickable());
- info.setDismissable(nodeWithReplacementActions.isDismissable());
+ && mNodeWithReplacementActions != null) {
+ List<AccessibilityAction> actions = mNodeWithReplacementActions.getActionList();
+ if (actions != null) {
+ for (int j = 0; j < actions.size(); j++) {
+ info.addAction(actions.get(j));
}
+ // The PIP needs to be able to take accessibility focus
+ info.addAction(AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS);
+ info.addAction(AccessibilityAction.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
}
+ info.setClickable(mNodeWithReplacementActions.isClickable());
+ info.setFocusable(mNodeWithReplacementActions.isFocusable());
+ info.setContextClickable(mNodeWithReplacementActions.isContextClickable());
+ info.setScrollable(mNodeWithReplacementActions.isScrollable());
+ info.setLongClickable(mNodeWithReplacementActions.isLongClickable());
+ info.setDismissable(mNodeWithReplacementActions.isDismissable());
}
}
-
- @GuardedBy("mLock")
- private void recycleReplaceActionNodesLocked() {
- if (mNodesWithReplacementActions == null) return;
- for (int i = mNodesWithReplacementActions.size() - 1; i >= 0; i--) {
- AccessibilityNodeInfo nodeWithReplacementAction = mNodesWithReplacementActions.get(i);
- nodeWithReplacementAction.recycle();
- }
- mNodesWithReplacementActions = null;
- }
}
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityInteractionControllerNodeRequestsTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityInteractionControllerNodeRequestsTest.java
new file mode 100644
index 000000000000..170f561aa2da
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityInteractionControllerNodeRequestsTest.java
@@ -0,0 +1,581 @@
+/*
+ * Copyright (C) 2021 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 com.android.server.accessibility;
+
+
+import static android.view.accessibility.AccessibilityNodeInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
+import static android.view.accessibility.AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS;
+import static android.view.accessibility.AccessibilityNodeInfo.FLAG_PREFETCH_SIBLINGS;
+import static android.view.accessibility.AccessibilityNodeInfo.ROOT_NODE_ID;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.app.Instrumentation;
+import android.content.Context;
+import android.os.RemoteException;
+import android.view.AccessibilityInteractionController;
+import android.view.View;
+import android.view.ViewRootImpl;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityNodeIdManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeProvider;
+import android.view.accessibility.IAccessibilityInteractionConnectionCallback;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Tests that verify expected node and prefetched node results when finding a view by node id. We
+ * send some requests to the controller via View methods to control message timing.
+ */
+@RunWith(AndroidJUnit4.class)
+public class AccessibilityInteractionControllerNodeRequestsTest {
+ private AccessibilityInteractionController mAccessibilityInteractionController;
+ @Mock
+ private IAccessibilityInteractionConnectionCallback mMockClientCallback1;
+ @Mock
+ private IAccessibilityInteractionConnectionCallback mMockClientCallback2;
+
+ @Captor
+ private ArgumentCaptor<AccessibilityNodeInfo> mFindInfoCaptor;
+ @Captor private ArgumentCaptor<List<AccessibilityNodeInfo>> mPrefetchInfoListCaptor;
+
+ private final Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation();
+ private static final int MOCK_CLIENT_1_THREAD_AND_PROCESS_ID = 1;
+ private static final int MOCK_CLIENT_2_THREAD_AND_PROCESS_ID = 2;
+
+ private static final String FRAME_LAYOUT_DESCRIPTION = "frameLayout";
+ private static final String TEXT_VIEW_1_DESCRIPTION = "textView1";
+ private static final String TEXT_VIEW_2_DESCRIPTION = "textView2";
+
+ private TestFrameLayout mFrameLayout;
+ private TestTextView mTextView1;
+ private TestTextView2 mTextView2;
+
+ private boolean mSendClient1RequestForTextAfterTextPrefetched;
+ private boolean mSendClient2RequestForTextAfterTextPrefetched;
+ private boolean mSendRequestForTextAndIncludeUnImportantViews;
+ private int mMockClient1InteractionId;
+ private int mMockClient2InteractionId;
+
+ @Before
+ public void setUp() throws Throwable {
+ MockitoAnnotations.initMocks(this);
+
+ mInstrumentation.runOnMainSync(() -> {
+ final Context context = mInstrumentation.getTargetContext();
+ final ViewRootImpl viewRootImpl = new ViewRootImpl(context, context.getDisplay());
+
+ mFrameLayout = new TestFrameLayout(context);
+ mTextView1 = new TestTextView(context);
+ mTextView2 = new TestTextView2(context);
+
+ mFrameLayout.addView(mTextView1);
+ mFrameLayout.addView(mTextView2);
+
+ // The controller retrieves views through this manager, and registration happens on
+ // when attached to a window, which we don't have. We can simply reference FrameLayout
+ // with ROOT_NODE_ID
+ AccessibilityNodeIdManager.getInstance().registerViewWithId(
+ mTextView1, mTextView1.getAccessibilityViewId());
+ AccessibilityNodeIdManager.getInstance().registerViewWithId(
+ mTextView2, mTextView2.getAccessibilityViewId());
+
+ try {
+ viewRootImpl.setView(mFrameLayout, new WindowManager.LayoutParams(), null);
+
+ } catch (WindowManager.BadTokenException e) {
+ // activity isn't running, we will ignore BadTokenException.
+ }
+
+ mAccessibilityInteractionController =
+ new AccessibilityInteractionController(viewRootImpl);
+ });
+
+ }
+
+ @After
+ public void tearDown() throws Throwable {
+ AccessibilityNodeIdManager.getInstance().unregisterViewWithId(
+ mTextView1.getAccessibilityViewId());
+ AccessibilityNodeIdManager.getInstance().unregisterViewWithId(
+ mTextView2.getAccessibilityViewId());
+ }
+
+ /**
+ * Tests a basic request for the root node with prefetch flag
+ * {@link AccessibilityNodeInfo#FLAG_PREFETCH_DESCENDANTS}
+ *
+ * @throws RemoteException
+ */
+ @Test
+ public void testFindRootView_withOneClient_shouldReturnRootNodeAndPrefetchDescendants()
+ throws RemoteException {
+ // Request for our FrameLayout
+ sendNodeRequestToController(ROOT_NODE_ID, mMockClientCallback1,
+ mMockClient1InteractionId, FLAG_PREFETCH_DESCENDANTS);
+ mInstrumentation.waitForIdleSync();
+
+ // Verify we get FrameLayout
+ verify(mMockClientCallback1).setFindAccessibilityNodeInfoResult(
+ mFindInfoCaptor.capture(), eq(mMockClient1InteractionId));
+ AccessibilityNodeInfo infoSentToService = mFindInfoCaptor.getValue();
+ assertEquals(FRAME_LAYOUT_DESCRIPTION, infoSentToService.getContentDescription());
+
+ verify(mMockClientCallback1).setPrefetchAccessibilityNodeInfoResult(
+ mPrefetchInfoListCaptor.capture(), eq(mMockClient1InteractionId));
+ // The descendants are our two TextViews
+ List<AccessibilityNodeInfo> prefetchedNodes = mPrefetchInfoListCaptor.getValue();
+ assertEquals(2, prefetchedNodes.size());
+ assertEquals(TEXT_VIEW_1_DESCRIPTION, prefetchedNodes.get(0).getContentDescription());
+ assertEquals(TEXT_VIEW_2_DESCRIPTION, prefetchedNodes.get(1).getContentDescription());
+
+ }
+
+ /**
+ * Tests a basic request for TestTextView1's node with prefetch flag
+ * {@link AccessibilityNodeInfo#FLAG_PREFETCH_SIBLINGS}
+ *
+ * @throws RemoteException
+ */
+ @Test
+ public void testFindTextView_withOneClient_shouldReturnNodeAndPrefetchedSiblings()
+ throws RemoteException {
+ // Request for TextView1
+ sendNodeRequestToController(AccessibilityNodeInfo.makeNodeId(
+ mTextView1.getAccessibilityViewId(), AccessibilityNodeProvider.HOST_VIEW_ID),
+ mMockClientCallback1, mMockClient1InteractionId, FLAG_PREFETCH_SIBLINGS);
+ mInstrumentation.waitForIdleSync();
+
+ // Verify we get TextView1
+ verify(mMockClientCallback1).setFindAccessibilityNodeInfoResult(
+ mFindInfoCaptor.capture(), eq(mMockClient1InteractionId));
+ AccessibilityNodeInfo infoSentToService = mFindInfoCaptor.getValue();
+ assertEquals(TEXT_VIEW_1_DESCRIPTION, infoSentToService.getContentDescription());
+
+ // Verify the prefetched sibling of TextView1 is TextView2
+ verify(mMockClientCallback1).setPrefetchAccessibilityNodeInfoResult(
+ mPrefetchInfoListCaptor.capture(), eq(mMockClient1InteractionId));
+ // TextView2 is the prefetched sibling
+ List<AccessibilityNodeInfo> prefetchedNodes = mPrefetchInfoListCaptor.getValue();
+ assertEquals(1, prefetchedNodes.size());
+ assertEquals(TEXT_VIEW_2_DESCRIPTION, prefetchedNodes.get(0).getContentDescription());
+ }
+
+ /**
+ * Tests a series of controller requests to prevent prefetching.
+ * Request 1: Client 1 requests the root node
+ * Request 2: When the root node is initialized in
+ * {@link TestFrameLayout#onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo)},
+ * Client 2 requests TestTextView1's node
+ *
+ * Request 2 on the queue prevents prefetching for Request 1.
+ *
+ * @throws RemoteException
+ */
+ @Test
+ public void testFindRootAndTextNodes_withTwoClients_shouldPreventClient1Prefetch()
+ throws RemoteException {
+ mFrameLayout.setAccessibilityDelegate(new View.AccessibilityDelegate() {
+ @Override
+ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(host, info);
+ final long nodeId = AccessibilityNodeInfo.makeNodeId(
+ mTextView1.getAccessibilityViewId(),
+ AccessibilityNodeProvider.HOST_VIEW_ID);
+
+ // Enqueue a request when this node is found from a different service for
+ // TextView1
+ sendNodeRequestToController(nodeId, mMockClientCallback2,
+ mMockClient2InteractionId, FLAG_PREFETCH_SIBLINGS);
+ }
+ });
+ // Client 1 request for FrameLayout
+ sendNodeRequestToController(ROOT_NODE_ID, mMockClientCallback1,
+ mMockClient1InteractionId, FLAG_PREFETCH_DESCENDANTS);
+
+ mInstrumentation.waitForIdleSync();
+
+ // Verify client 1 gets FrameLayout
+ verify(mMockClientCallback1).setFindAccessibilityNodeInfoResult(
+ mFindInfoCaptor.capture(), eq(mMockClient1InteractionId));
+ AccessibilityNodeInfo infoSentToService = mFindInfoCaptor.getValue();
+ assertEquals(FRAME_LAYOUT_DESCRIPTION, infoSentToService.getContentDescription());
+
+ // The second request is put in the queue in the FrameLayout's onInitializeA11yNodeInfo,
+ // meaning prefetching is interrupted and does not even begin for the first request
+ verify(mMockClientCallback1, never())
+ .setPrefetchAccessibilityNodeInfoResult(anyList(), anyInt());
+
+ // Verify client 2 gets TextView1
+ verify(mMockClientCallback2).setFindAccessibilityNodeInfoResult(
+ mFindInfoCaptor.capture(), eq(mMockClient2InteractionId));
+ infoSentToService = mFindInfoCaptor.getValue();
+ assertEquals(TEXT_VIEW_1_DESCRIPTION, infoSentToService.getContentDescription());
+
+ // Verify the prefetched sibling of TextView1 is TextView2 (FLAG_PREFETCH_SIBLINGS)
+ verify(mMockClientCallback2).setPrefetchAccessibilityNodeInfoResult(
+ mPrefetchInfoListCaptor.capture(), eq(mMockClient2InteractionId));
+ List<AccessibilityNodeInfo> prefetchedNodes = mPrefetchInfoListCaptor.getValue();
+ assertEquals(1, prefetchedNodes.size());
+ assertEquals(TEXT_VIEW_2_DESCRIPTION, prefetchedNodes.get(0).getContentDescription());
+ }
+
+ /**
+ * Tests a series of controller same-service requests to interrupt prefetching and satisfy a
+ * pending node request.
+ * Request 1: Request the root node
+ * Request 2: When TextTextView1's node is initialized as part of Request 1's prefetching,
+ * request TestTextView1's node
+ *
+ * Request 1 prefetches TestTextView1's node, is interrupted by a pending request, and checks
+ * if its prefetched nodes satisfy any pending requests. It satisfies Request 2's request for
+ * TestTextView1's node. Request 2 is fulfilled, so it is removed from queue and does not
+ * prefetch.
+ *
+ * @throws RemoteException
+ */
+ @Test
+ public void testFindRootAndTextNode_withOneClient_shouldInterruptPrefetchAndSatisfyPendingMsg()
+ throws RemoteException {
+ mSendClient1RequestForTextAfterTextPrefetched = true;
+
+ mTextView1.setAccessibilityDelegate(new View.AccessibilityDelegate(){
+ @Override
+ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(host, info);
+ info.setContentDescription(TEXT_VIEW_1_DESCRIPTION);
+ final long nodeId = AccessibilityNodeInfo.makeNodeId(
+ mTextView1.getAccessibilityViewId(),
+ AccessibilityNodeProvider.HOST_VIEW_ID);
+
+ if (mSendClient1RequestForTextAfterTextPrefetched) {
+ // Prevent a loop when processing second request
+ mSendClient1RequestForTextAfterTextPrefetched = false;
+ // TextView1 is prefetched here after the FrameLayout is found. Now enqueue a
+ // same-client request for TextView1
+ sendNodeRequestToController(nodeId, mMockClientCallback1,
+ ++mMockClient1InteractionId, FLAG_PREFETCH_SIBLINGS);
+
+ }
+ }
+ });
+ // Client 1 requests FrameLayout
+ sendNodeRequestToController(ROOT_NODE_ID, mMockClientCallback1,
+ mMockClient1InteractionId, FLAG_PREFETCH_DESCENDANTS);
+
+ // Flush out all messages
+ mInstrumentation.waitForIdleSync();
+
+ // When TextView1 is prefetched for FrameLayout, we put a message on the queue in
+ // TextView1's onInitializeA11yNodeInfo that requests for TextView1. The service thus get
+ // two node results for FrameLayout and TextView1.
+ verify(mMockClientCallback1, times(2))
+ .setFindAccessibilityNodeInfoResult(mFindInfoCaptor.capture(), anyInt());
+
+ List<AccessibilityNodeInfo> foundNodes = mFindInfoCaptor.getAllValues();
+ assertEquals(FRAME_LAYOUT_DESCRIPTION, foundNodes.get(0).getContentDescription());
+ assertEquals(TEXT_VIEW_1_DESCRIPTION, foundNodes.get(1).getContentDescription());
+
+ // The controller will look at FrameLayout's prefetched nodes and find matching nodes in
+ // pending requests. The prefetched TextView1 matches the second request. The second
+ // request was removed from queue and prefetching for this request never occurred.
+ verify(mMockClientCallback1, times(1))
+ .setPrefetchAccessibilityNodeInfoResult(mPrefetchInfoListCaptor.capture(),
+ eq(mMockClient1InteractionId - 1));
+ List<AccessibilityNodeInfo> prefetchedNodes = mPrefetchInfoListCaptor.getValue();
+ assertEquals(1, prefetchedNodes.size());
+ assertEquals(TEXT_VIEW_1_DESCRIPTION, prefetchedNodes.get(0).getContentDescription());
+ }
+
+ /**
+ * Like above, but tests a series of controller requests from different services to interrupt
+ * prefetching and satisfy a pending node request.
+ *
+ * @throws RemoteException
+ */
+ @Test
+ public void testFindRootAndTextNode_withTwoClients_shouldInterruptPrefetchAndSatisfyPendingMsg()
+ throws RemoteException {
+ mSendClient2RequestForTextAfterTextPrefetched = true;
+ mTextView1.setAccessibilityDelegate(new View.AccessibilityDelegate(){
+ @Override
+ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(host, info);
+ info.setContentDescription(TEXT_VIEW_1_DESCRIPTION);
+ final long nodeId = AccessibilityNodeInfo.makeNodeId(
+ mTextView1.getAccessibilityViewId(),
+ AccessibilityNodeProvider.HOST_VIEW_ID);
+
+ if (mSendClient2RequestForTextAfterTextPrefetched) {
+ mSendClient2RequestForTextAfterTextPrefetched = false;
+ // TextView1 is prefetched here. Now enqueue client 2's request for
+ // TextView1
+ sendNodeRequestToController(nodeId, mMockClientCallback2,
+ mMockClient2InteractionId, FLAG_PREFETCH_SIBLINGS);
+ }
+ }
+ });
+ // Client 1 requests FrameLayout
+ sendNodeRequestToController(ROOT_NODE_ID, mMockClientCallback1,
+ mMockClient1InteractionId, FLAG_PREFETCH_DESCENDANTS);
+
+ mInstrumentation.waitForIdleSync();
+
+ // Verify client 1 gets FrameLayout
+ verify(mMockClientCallback1, times(1))
+ .setFindAccessibilityNodeInfoResult(mFindInfoCaptor.capture(), anyInt());
+ assertEquals(FRAME_LAYOUT_DESCRIPTION,
+ mFindInfoCaptor.getValue().getContentDescription());
+
+ // Verify client 1 has prefetched nodes
+ verify(mMockClientCallback1, times(1))
+ .setPrefetchAccessibilityNodeInfoResult(mPrefetchInfoListCaptor.capture(),
+ eq(mMockClient1InteractionId));
+
+ // Verify client 1's only prefetched node is TextView1
+ List<AccessibilityNodeInfo> prefetchedNodes = mPrefetchInfoListCaptor.getValue();
+ assertEquals(1, prefetchedNodes.size());
+ assertEquals(TEXT_VIEW_1_DESCRIPTION, prefetchedNodes.get(0).getContentDescription());
+
+ // Verify client 2 gets TextView1
+ verify(mMockClientCallback2, times(1))
+ .setFindAccessibilityNodeInfoResult(mFindInfoCaptor.capture(), anyInt());
+
+ assertEquals(TEXT_VIEW_1_DESCRIPTION, mFindInfoCaptor.getValue().getContentDescription());
+
+ // The second request was removed from queue and prefetching for this client request never
+ // occurred as it was satisfied.
+ verify(mMockClientCallback2, never())
+ .setPrefetchAccessibilityNodeInfoResult(anyList(), anyInt());
+
+ }
+
+ @Test
+ public void testFindNodeById_withTwoDifferentPrefetchFlags_shouldNotSatisfyPendingRequest()
+ throws RemoteException {
+ mSendRequestForTextAndIncludeUnImportantViews = true;
+ mTextView1.setAccessibilityDelegate(new View.AccessibilityDelegate(){
+ @Override
+ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(host, info);
+ info.setContentDescription(TEXT_VIEW_1_DESCRIPTION);
+ final long nodeId = AccessibilityNodeInfo.makeNodeId(
+ mTextView1.getAccessibilityViewId(),
+ AccessibilityNodeProvider.HOST_VIEW_ID);
+
+ if (mSendRequestForTextAndIncludeUnImportantViews) {
+ mSendRequestForTextAndIncludeUnImportantViews = false;
+ // TextView1 is prefetched here for client 1. Now enqueue a request from a
+ // different client that holds different fetch flags for TextView1
+ sendNodeRequestToController(nodeId, mMockClientCallback2,
+ mMockClient2InteractionId,
+ FLAG_PREFETCH_SIBLINGS | FLAG_INCLUDE_NOT_IMPORTANT_VIEWS);
+ }
+ }
+ });
+
+ // Mockito does not make copies of objects when called. It holds references, so
+ // the captor would point to client 2's results after all requests are processed. Verify
+ // prefetched node immediately
+ doAnswer(invocation -> {
+ List<AccessibilityNodeInfo> prefetched = invocation.getArgument(0);
+ assertEquals(TEXT_VIEW_1_DESCRIPTION, prefetched.get(0).getContentDescription());
+ return null;
+ }).when(mMockClientCallback1).setPrefetchAccessibilityNodeInfoResult(anyList(),
+ eq(mMockClient1InteractionId));
+
+ // Client 1 requests FrameLayout
+ sendNodeRequestToController(ROOT_NODE_ID, mMockClientCallback1,
+ mMockClient1InteractionId, FLAG_PREFETCH_DESCENDANTS);
+
+ mInstrumentation.waitForIdleSync();
+
+ // Verify client 1 gets FrameLayout
+ verify(mMockClientCallback1, times(1))
+ .setFindAccessibilityNodeInfoResult(mFindInfoCaptor.capture(),
+ eq(mMockClient1InteractionId));
+
+ assertEquals(FRAME_LAYOUT_DESCRIPTION,
+ mFindInfoCaptor.getValue().getContentDescription());
+
+ // Verify client 1 has prefetched results. The only prefetched node is TextView1
+ // (from above doAnswer)
+ verify(mMockClientCallback1, times(1))
+ .setPrefetchAccessibilityNodeInfoResult(mPrefetchInfoListCaptor.capture(),
+ eq(mMockClient1InteractionId));
+
+ // Verify client 2 gets TextView1
+ verify(mMockClientCallback2, times(1))
+ .setFindAccessibilityNodeInfoResult(mFindInfoCaptor.capture(),
+ eq(mMockClient2InteractionId));
+ assertEquals(TEXT_VIEW_1_DESCRIPTION,
+ mFindInfoCaptor.getValue().getContentDescription());
+ // Verify client 2 has TextView2 as a prefetched node
+ verify(mMockClientCallback2, times(1))
+ .setPrefetchAccessibilityNodeInfoResult(mPrefetchInfoListCaptor.capture(),
+ eq(mMockClient2InteractionId));
+ List<AccessibilityNodeInfo> prefetchedNode = mPrefetchInfoListCaptor.getValue();
+ assertEquals(1, prefetchedNode.size());
+ assertEquals(TEXT_VIEW_2_DESCRIPTION, prefetchedNode.get(0).getContentDescription());
+ }
+
+ private void sendNodeRequestToController(long requestedNodeId,
+ IAccessibilityInteractionConnectionCallback callback, int interactionId,
+ int prefetchFlags) {
+ final int processAndThreadId = callback == mMockClientCallback1
+ ? MOCK_CLIENT_1_THREAD_AND_PROCESS_ID
+ : MOCK_CLIENT_2_THREAD_AND_PROCESS_ID;
+
+ mAccessibilityInteractionController.findAccessibilityNodeInfoByAccessibilityIdClientThread(
+ requestedNodeId,
+ null, interactionId,
+ callback, prefetchFlags,
+ processAndThreadId,
+ processAndThreadId, null, null);
+
+ }
+
+ private class TestFrameLayout extends FrameLayout {
+
+ TestFrameLayout(Context context) {
+ super(context);
+ }
+
+ @Override
+ public int getWindowVisibility() {
+ // We aren't attached to a window so let's pretend
+ return VISIBLE;
+ }
+
+ @Override
+ public boolean isShown() {
+ // Controller check
+ return true;
+ }
+
+ @Override
+ public int getAccessibilityViewId() {
+ // static id doesn't reset after tests so return the same one
+ return 0;
+ }
+
+ @Override
+ public void addChildrenForAccessibility(ArrayList<View> outChildren) {
+ // ViewGroup#addChildrenForAccessbility sorting logic will switch these two
+ outChildren.add(mTextView1);
+ outChildren.add(mTextView2);
+ }
+
+ @Override
+ public boolean includeForAccessibility() {
+ return true;
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setContentDescription(FRAME_LAYOUT_DESCRIPTION);
+ }
+ }
+
+ private class TestTextView extends TextView {
+ TestTextView(Context context) {
+ super(context);
+ }
+
+ @Override
+ public int getWindowVisibility() {
+ return VISIBLE;
+ }
+
+ @Override
+ public boolean isShown() {
+ return true;
+ }
+
+ @Override
+ public int getAccessibilityViewId() {
+ return 1;
+ }
+
+ @Override
+ public boolean includeForAccessibility() {
+ return true;
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setContentDescription(TEXT_VIEW_1_DESCRIPTION);
+ }
+ }
+
+ private class TestTextView2 extends TextView {
+ TestTextView2(Context context) {
+ super(context);
+ }
+
+ @Override
+ public int getWindowVisibility() {
+ return VISIBLE;
+ }
+
+ @Override
+ public boolean isShown() {
+ return true;
+ }
+
+ @Override
+ public int getAccessibilityViewId() {
+ return 2;
+ }
+
+ @Override
+ public boolean includeForAccessibility() {
+ return true;
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setContentDescription(TEXT_VIEW_2_DESCRIPTION);
+ }
+ }
+}