Service requests can interrupt node prefetching

This is the re-merging of ag/12923546 (where most of that original
message is posted below), which includes various bug fixes.

Slow prefetch requests would block user interactive requests, creating
noticeable sluggishness and unresponsiveness in accessibility services,
especially on the web.

Let's make it so a user interactive requests stops prefetching.
We can't interrupt an API call, but we can stop in between API calls.

On the service side, we have to separate the prefetch callbacks from the
find callback. And we have to make it asynchronous. It does dispatch
intothe main thread, so the AccessibilityCache can remain single threaded.

When the calls are interrupted on the application side,
returnPendingFindAccessibilityNodeInfosInPrefetch checks the find
requests that are waiting in the queue, to see if they can be addressed
by the prefetch results. If they can be, we don't have to call into
potentially non-performant application code. We don't  check requests
that have differing prefetch flags (FLAG_INCLUDE_NOT_IMPORTANT_VIEWS,
FLAG_REPORT_VIEW_IDS) that would result in different caches.

We also make mPendingFindNodeByIdMessages thread-safe and ensure in
ActionReplacingCallback we don't return null results. Merged
ag/13246536, ag/13256330

Messages should be added to PrivateHandler and
mPendingFindNodeIdMessages at the same time to avoid a race condition
where we try removing a message from the handler before it's actually
enqueued. This was causing double recycling

UiAutomation does't require a main thread, so getMainLooper may
return null. In this case, instead of posting to the main looper,
we cache nodes on the binder thread (which is our ultimate goal).

Added tests to verify AccessibilityInteractionController interactions

Test: atest AccessibilityInteractionControllerNodeRequestsTest,
FrameworksServicesTests FrameworksCoreTests, CtsAccessibility, Manual
testing

Bug: b/176195360, b/175877007, b/175884343, b/178726546, b/175832139,
b/176195505, b/181701570

Change-Id: I66902f995f33f0236003faa439925ec72fcf6952
This commit is contained in:
Sally
2021-03-04 19:56:52 +00:00
parent 4e92407479
commit 327de4b45c
6 changed files with 1029 additions and 241 deletions

View File

@@ -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>();
@@ -113,6 +119,9 @@ public final class AccessibilityInteractionController {
private AddNodeInfosForViewId mAddNodeInfosForViewId;
@GuardedBy("mLock")
private ArrayList<Message> mPendingFindNodeByIdMessages;
@GuardedBy("mLock")
private int mNumActiveRequestPreparers;
@GuardedBy("mLock")
@@ -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> {

View File

@@ -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);
@@ -828,6 +850,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.
*

View File

@@ -46,6 +46,15 @@ oneway interface IAccessibilityInteractionConnectionCallback {
void setFindAccessibilityNodeInfosResult(in List<AccessibilityNodeInfo> infos,
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.
*

View File

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

View File

@@ -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;
// 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;
List<AccessibilityNodeInfo> mPrefetchedNodesFromOriginalWindow;
@GuardedBy("mLock")
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");
}
return;
}
if (mNodesFromOriginalWindow != null) {
for (int i = 0; i < mNodesFromOriginalWindow.size(); i++) {
replaceActionsOnInfoLocked(mNodesFromOriginalWindow.get(i));
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");
}
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");
}
}
}
}
@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;
}
}

View File

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