Revert "Service requests can interrupt node prefetching"

This reverts commit 327de4b45c.

Reason for revert: b/182016372

Change-Id: I67ec01fc2f47b05a2dc11e7951c11a708fdbeebf
This commit is contained in:
Sally Yuen
2021-03-08 19:49:37 +00:00
parent 327de4b45c
commit 373028a524
6 changed files with 242 additions and 1030 deletions

View File

@@ -86,12 +86,6 @@ 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>();
@@ -119,9 +113,6 @@ public final class AccessibilityInteractionController {
private AddNodeInfosForViewId mAddNodeInfosForViewId;
@GuardedBy("mLock")
private ArrayList<Message> mPendingFindNodeByIdMessages;
@GuardedBy("mLock")
private int mNumActiveRequestPreparers;
@GuardedBy("mLock")
@@ -137,7 +128,6 @@ 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,
@@ -187,11 +177,7 @@ public final class AccessibilityInteractionController {
args.arg4 = arguments;
message.obj = args;
synchronized (mLock) {
mPendingFindNodeByIdMessages.add(message);
scheduleMessage(message, interrogatingPid, interrogatingTid,
CONSIDER_REQUEST_PREPARERS);
}
scheduleMessage(message, interrogatingPid, interrogatingTid, CONSIDER_REQUEST_PREPARERS);
}
/**
@@ -329,9 +315,6 @@ public final class AccessibilityInteractionController {
}
private void findAccessibilityNodeInfoByAccessibilityIdUiThread(Message message) {
synchronized (mLock) {
mPendingFindNodeByIdMessages.remove(message);
}
final int flags = message.arg1;
SomeArgs args = (SomeArgs) message.obj;
@@ -346,58 +329,22 @@ public final class AccessibilityInteractionController {
args.recycle();
View rootView = null;
AccessibilityNodeInfo rootNode = null;
List<AccessibilityNodeInfo> infos = mTempAccessibilityNodeInfoList;
infos.clear();
try {
if (mViewRootImpl.mView == null || mViewRootImpl.mAttachInfo == null) {
return;
}
mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = flags;
rootView = findViewByAccessibilityId(accessibilityViewId);
if (rootView != null && isShown(rootView)) {
rootNode = populateAccessibilityNodeInfoForView(
rootView, arguments, virtualDescendantId);
final View root = findViewByAccessibilityId(accessibilityViewId);
if (root != null && isShown(root)) {
mPrefetcher.prefetchAccessibilityNodeInfos(
root, virtualDescendantId, flags, infos, arguments);
}
} finally {
updateInfoForViewportAndReturnFindNodeResult(
rootNode == null ? null : AccessibilityNodeInfo.obtain(rootNode),
callback, interactionId, spec, interactiveRegion);
updateInfosForViewportAndReturnFindNodeResult(
infos, 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,
@@ -456,7 +403,6 @@ public final class AccessibilityInteractionController {
mAddNodeInfosForViewId.reset();
}
} finally {
mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = 0;
updateInfosForViewportAndReturnFindNodeResult(
infos, callback, interactionId, spec, interactiveRegion);
}
@@ -539,7 +485,6 @@ public final class AccessibilityInteractionController {
}
}
} finally {
mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = 0;
updateInfosForViewportAndReturnFindNodeResult(
infos, callback, interactionId, spec, interactiveRegion);
}
@@ -631,7 +576,6 @@ public final class AccessibilityInteractionController {
}
}
} finally {
mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = 0;
updateInfoForViewportAndReturnFindNodeResult(
focused, callback, interactionId, spec, interactiveRegion);
}
@@ -686,7 +630,6 @@ public final class AccessibilityInteractionController {
}
}
} finally {
mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = 0;
updateInfoForViewportAndReturnFindNodeResult(
next, callback, interactionId, spec, interactiveRegion);
}
@@ -843,6 +786,33 @@ 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) {
@@ -863,6 +833,17 @@ 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;
@@ -910,6 +891,17 @@ 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;
@@ -983,46 +975,18 @@ 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();
@@ -1032,80 +996,22 @@ 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) {
updateInfoForViewPort(info, spec, interactiveRegion);
returnFindNodeResult(info, callback, interactionId);
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 */
}
}
private boolean handleClickableSpanActionUiThread(
@@ -1148,45 +1054,56 @@ public final class AccessibilityInteractionController {
private final ArrayList<View> mTempViewList = new ArrayList<View>();
public void prefetchAccessibilityNodeInfos(View view, AccessibilityNodeInfo root,
int virtualViewId, int fetchFlags, List<AccessibilityNodeInfo> outInfos) {
if (root == null) {
return;
}
public void prefetchAccessibilityNodeInfos(View view, int virtualViewId, int fetchFlags,
List<AccessibilityNodeInfo> outInfos, Bundle arguments) {
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) {
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);
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);
}
}
} else {
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);
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 (ENFORCE_NODE_TREE_CONSISTENT) {
enforceNodeTreeConsistent(root, outInfos);
enforceNodeTreeConsistent(outInfos);
}
}
private boolean shouldStopPrefetching(List prefetchededInfos) {
return mHandler.hasUserInteractiveMessagesWaiting()
|| prefetchededInfos.size() >= MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE;
}
private void enforceNodeTreeConsistent(
AccessibilityNodeInfo root, List<AccessibilityNodeInfo> nodes) {
private void enforceNodeTreeConsistent(List<AccessibilityNodeInfo> nodes) {
LongSparseArray<AccessibilityNodeInfo> nodeMap =
new LongSparseArray<AccessibilityNodeInfo>();
final int nodeCount = nodes.size();
@@ -1197,6 +1114,7 @@ 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;
@@ -1263,11 +1181,9 @@ public final class AccessibilityInteractionController {
private void prefetchPredecessorsOfRealNode(View view,
List<AccessibilityNodeInfo> outInfos) {
if (shouldStopPrefetching(outInfos)) {
return;
}
ViewParent parent = view.getParentForAccessibility();
while (parent instanceof View && !shouldStopPrefetching(outInfos)) {
while (parent instanceof View
&& outInfos.size() < MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) {
View parentView = (View) parent;
AccessibilityNodeInfo info = parentView.createAccessibilityNodeInfo();
if (info != null) {
@@ -1279,9 +1195,6 @@ 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;
@@ -1291,7 +1204,7 @@ public final class AccessibilityInteractionController {
parentGroup.addChildrenForAccessibility(children);
final int childCount = children.size();
for (int i = 0; i < childCount; i++) {
if (shouldStopPrefetching(outInfos)) {
if (outInfos.size() >= MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) {
return;
}
View child = children.get(i);
@@ -1319,7 +1232,7 @@ public final class AccessibilityInteractionController {
private void prefetchDescendantsOfRealNode(View root,
List<AccessibilityNodeInfo> outInfos) {
if (shouldStopPrefetching(outInfos) || !(root instanceof ViewGroup)) {
if (!(root instanceof ViewGroup)) {
return;
}
HashMap<View, AccessibilityNodeInfo> addedChildren =
@@ -1330,7 +1243,7 @@ public final class AccessibilityInteractionController {
root.addChildrenForAccessibility(children);
final int childCount = children.size();
for (int i = 0; i < childCount; i++) {
if (shouldStopPrefetching(outInfos)) {
if (outInfos.size() >= MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) {
return;
}
View child = children.get(i);
@@ -1355,7 +1268,7 @@ public final class AccessibilityInteractionController {
} finally {
children.clear();
}
if (!shouldStopPrefetching(outInfos)) {
if (outInfos.size() < MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) {
for (Map.Entry<View, AccessibilityNodeInfo> entry : addedChildren.entrySet()) {
View addedChild = entry.getKey();
AccessibilityNodeInfo virtualRoot = entry.getValue();
@@ -1377,7 +1290,7 @@ public final class AccessibilityInteractionController {
long parentNodeId = root.getParentNodeId();
int accessibilityViewId = AccessibilityNodeInfo.getAccessibilityViewId(parentNodeId);
while (accessibilityViewId != AccessibilityNodeInfo.UNDEFINED_ITEM_ID) {
if (shouldStopPrefetching(outInfos)) {
if (outInfos.size() >= MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) {
return;
}
final int virtualDescendantId =
@@ -1422,7 +1335,7 @@ public final class AccessibilityInteractionController {
if (parent != null) {
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
if (shouldStopPrefetching(outInfos)) {
if (outInfos.size() >= MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) {
return;
}
final long childNodeId = parent.getChildId(i);
@@ -1447,7 +1360,7 @@ public final class AccessibilityInteractionController {
final int initialOutInfosSize = outInfos.size();
final int childCount = root.getChildCount();
for (int i = 0; i < childCount; i++) {
if (shouldStopPrefetching(outInfos)) {
if (outInfos.size() >= MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) {
return;
}
final long childNodeId = root.getChildId(i);
@@ -1457,7 +1370,7 @@ public final class AccessibilityInteractionController {
outInfos.add(child);
}
}
if (!shouldStopPrefetching(outInfos)) {
if (outInfos.size() < MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) {
final int addedChildCount = outInfos.size() - initialOutInfosSize;
for (int i = 0; i < addedChildCount; i++) {
AccessibilityNodeInfo child = outInfos.get(initialOutInfosSize + i);
@@ -1566,10 +1479,6 @@ 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,9 +23,7 @@ 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;
@@ -115,8 +113,6 @@ public final class AccessibilityInteractionClient
private final Object mInstanceLock = new Object();
private Handler mMainHandler;
private volatile int mInteractionId = -1;
private AccessibilityNodeInfo mFindAccessibilityNodeInfoResult;
@@ -127,11 +123,6 @@ 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.
*/
@@ -206,10 +197,6 @@ public final class AccessibilityInteractionClient
private AccessibilityInteractionClient() {
/* reducing constructor visibility */
Looper mainLooper = Looper.getMainLooper();
if (mainLooper != null) {
mMainHandler = new Handler(mainLooper);
}
}
/**
@@ -464,16 +451,16 @@ public final class AccessibilityInteractionClient
Binder.restoreCallingIdentity(identityToken);
}
if (packageNames != null) {
AccessibilityNodeInfo info =
getFindAccessibilityNodeInfoResultAndClear(interactionId);
if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_MASK) != 0
&& info != null) {
setInteractionWaitingForPrefetchResult(interactionId, connectionId,
packageNames);
}
finalizeAndCacheAccessibilityNodeInfo(info, connectionId,
List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear(
interactionId);
finalizeAndCacheAccessibilityNodeInfos(infos, connectionId,
bypassCache, packageNames);
return info;
if (infos != null && !infos.isEmpty()) {
for (int i = 1; i < infos.size(); i++) {
infos.get(i).recycle();
}
return infos.get(0);
}
}
} else {
if (DEBUG) {
@@ -487,15 +474,6 @@ 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);
@@ -850,59 +828,6 @@ 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,15 +46,6 @@ 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,6 +33,9 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import java.util.Arrays;
import java.util.List;
/**
* Tests for AccessibilityInteractionClient
*/
@@ -62,7 +65,7 @@ public class AccessibilityInteractionClientTest {
final long accessibilityNodeId = 0x4321L;
AccessibilityNodeInfo nodeFromConnection = AccessibilityNodeInfo.obtain();
nodeFromConnection.setSourceNodeId(accessibilityNodeId, windowId);
mMockConnection.mInfoToReturn = nodeFromConnection;
mMockConnection.mInfosToReturn = Arrays.asList(nodeFromConnection);
AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance();
AccessibilityNodeInfo node = client.findAccessibilityNodeInfoByAccessibilityId(
MOCK_CONNECTION_ID, windowId, accessibilityNodeId, true, 0, null);
@@ -72,7 +75,7 @@ public class AccessibilityInteractionClientTest {
}
private static class MockConnection extends AccessibilityServiceConnectionImpl {
AccessibilityNodeInfo mInfoToReturn;
List<AccessibilityNodeInfo> mInfosToReturn;
@Override
public String[] findAccessibilityNodeInfoByAccessibilityId(int accessibilityWindowId,
@@ -80,7 +83,7 @@ public class AccessibilityInteractionClientTest {
IAccessibilityInteractionConnectionCallback callback, int flags, long threadId,
Bundle arguments) {
try {
callback.setFindAccessibilityNodeInfoResult(mInfoToReturn, interactionId);
callback.setFindAccessibilityNodeInfosResult(mInfosToReturn, interactionId);
} catch (RemoteException e) {
throw new RuntimeException(e);
}

View File

@@ -40,34 +40,29 @@ 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")
private boolean mReplacementNodeIsReadyOrFailed;
@GuardedBy("mLock")
AccessibilityNodeInfo mNodeWithReplacementActions;
List<AccessibilityNodeInfo> mNodesWithReplacementActions;
@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 mSetFindNodesFromOriginalWindowCalled = false;
boolean mSingleNodeCallbackHappened;
// Keep track of whether or not we've been called back for multiple node
@GuardedBy("mLock")
List<AccessibilityNodeInfo> mPrefetchedNodesFromOriginalWindow;
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 mSetPrefetchFromOriginalWindowCalled = false;
boolean mDone;
public ActionReplacingCallback(IAccessibilityInteractionConnectionCallback serviceCallback,
IAccessibilityInteractionConnection connectionWithReplacementActions,
@@ -75,20 +70,19 @@ 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,
mNodeWithReplacementActionsInteractionId, this, 0,
AccessibilityNodeInfo.ROOT_NODE_ID, null, interactionId + 1, this, 0,
interrogatingPid, interrogatingTid, null, null);
} catch (RemoteException re) {
if (DEBUG) {
Slog.e(LOG_TAG, "Error calling findAccessibilityNodeInfoByAccessibilityId()");
}
mReplacementNodeIsReadyOrFailed = true;
// Pretend we already got a (null) list of replacement nodes
mMultiNodeCallbackHappened = true;
} finally {
Binder.restoreCallingIdentity(identityToken);
}
@@ -96,67 +90,46 @@ public class ActionReplacingCallback extends IAccessibilityInteractionConnection
@Override
public void setFindAccessibilityNodeInfoResult(AccessibilityNodeInfo info, int interactionId) {
synchronized (mLock) {
boolean readyForCallback;
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) {
synchronized (mLock) {
boolean callbackForSingleNode;
boolean callbackForMultipleNodes;
synchronized(mLock) {
if (interactionId == mInteractionId) {
mNodesFromOriginalWindow = infos;
mSetFindNodesFromOriginalWindowCalled = true;
} else if (interactionId == mNodeWithReplacementActionsInteractionId) {
setNodeWithReplacementActionsFromList(infos);
mReplacementNodeIsReadyOrFailed = true;
} else if (interactionId == mInteractionId + 1) {
mNodesWithReplacementActions = infos;
} else {
Slog.e(LOG_TAG, "Callback with unexpected interactionId");
return;
}
callbackForSingleNode = mSingleNodeCallbackHappened;
callbackForMultipleNodes = mMultiNodeCallbackHappened;
mMultiNodeCallbackHappened = true;
}
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 (callbackForSingleNode) {
replaceInfoActionsAndCallService();
}
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;
}
if (callbackForMultipleNodes) {
replaceInfosActionsAndCallService();
}
}
@@ -169,81 +142,55 @@ public class ActionReplacingCallback extends IAccessibilityInteractionConnection
private void replaceInfoActionsAndCallService() {
final AccessibilityNodeInfo nodeToReturn;
boolean doCallback = false;
synchronized (mLock) {
doCallback = mReplacementNodeIsReadyOrFailed
&& mSetFindNodeFromOriginalWindowCalled;
if (doCallback && mNodeFromOriginalWindow != null) {
replaceActionsOnInfoLocked(mNodeFromOriginalWindow);
mSetFindNodeFromOriginalWindowCalled = false;
}
nodeToReturn = mNodeFromOriginalWindow;
}
if (doCallback) {
try {
mServiceCallback.setFindAccessibilityNodeInfoResult(nodeToReturn, mInteractionId);
} catch (RemoteException re) {
if (mDone) {
if (DEBUG) {
Slog.e(LOG_TAG, "Failed to setFindAccessibilityNodeInfoResult");
Slog.e(LOG_TAG, "Extra callback");
}
return;
}
if (mNodeFromOriginalWindow != null) {
replaceActionsOnInfoLocked(mNodeFromOriginalWindow);
}
recycleReplaceActionNodesLocked();
nodeToReturn = mNodeFromOriginalWindow;
mDone = true;
}
try {
mServiceCallback.setFindAccessibilityNodeInfoResult(nodeToReturn, mInteractionId);
} catch (RemoteException re) {
if (DEBUG) {
Slog.e(LOG_TAG, "Failed to setFindAccessibilityNodeInfoResult");
}
}
}
private void replaceInfosActionsAndCallService() {
List<AccessibilityNodeInfo> nodesToReturn = null;
boolean doCallback = false;
final List<AccessibilityNodeInfo> nodesToReturn;
synchronized (mLock) {
doCallback = mReplacementNodeIsReadyOrFailed
&& mSetFindNodesFromOriginalWindowCalled;
if (doCallback) {
nodesToReturn = replaceActionsLocked(mNodesFromOriginalWindow);
mSetFindNodesFromOriginalWindowCalled = false;
}
}
if (doCallback) {
try {
mServiceCallback.setFindAccessibilityNodeInfosResult(nodesToReturn, mInteractionId);
} catch (RemoteException re) {
if (mDone) {
if (DEBUG) {
Slog.e(LOG_TAG, "Failed to setFindAccessibilityNodeInfosResult");
Slog.e(LOG_TAG, "Extra callback");
}
return;
}
if (mNodesFromOriginalWindow != null) {
for (int i = 0; i < mNodesFromOriginalWindow.size(); i++) {
replaceActionsOnInfoLocked(mNodesFromOriginalWindow.get(i));
}
}
recycleReplaceActionNodesLocked();
nodesToReturn = (mNodesFromOriginalWindow == null)
? null : new ArrayList<>(mNodesFromOriginalWindow);
mDone = true;
}
}
private void replacePrefetchInfosActionsAndCallService() {
List<AccessibilityNodeInfo> nodesToReturn = null;
boolean doCallback = false;
synchronized (mLock) {
doCallback = mReplacementNodeIsReadyOrFailed
&& mSetPrefetchFromOriginalWindowCalled;
if (doCallback) {
nodesToReturn = replaceActionsLocked(mPrefetchedNodesFromOriginalWindow);
mSetPrefetchFromOriginalWindowCalled = false;
try {
mServiceCallback.setFindAccessibilityNodeInfosResult(nodesToReturn, mInteractionId);
} catch (RemoteException re) {
if (DEBUG) {
Slog.e(LOG_TAG, "Failed to setFindAccessibilityNodeInfosResult");
}
}
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")
@@ -257,22 +204,40 @@ public class ActionReplacingCallback extends IAccessibilityInteractionConnection
info.setDismissable(false);
// We currently only replace actions for the root node
if ((info.getSourceNodeId() == AccessibilityNodeInfo.ROOT_NODE_ID)
&& mNodeWithReplacementActions != null) {
List<AccessibilityAction> actions = mNodeWithReplacementActions.getActionList();
if (actions != null) {
for (int j = 0; j < actions.size(); j++) {
info.addAction(actions.get(j));
&& 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());
}
// 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

@@ -1,581 +0,0 @@
/*
* 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);
}
}
}