diff options
6 files changed, 1026 insertions, 238 deletions
diff --git a/core/java/android/view/AccessibilityInteractionController.java b/core/java/android/view/AccessibilityInteractionController.java index 9473845b15e6..0499f39f2fe4 100644 --- a/core/java/android/view/AccessibilityInteractionController.java +++ b/core/java/android/view/AccessibilityInteractionController.java @@ -86,12 +86,19 @@ 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>(); private final Object mLock = new Object(); - private final PrivateHandler mHandler; + @VisibleForTesting + public final PrivateHandler mHandler; private final ViewRootImpl mViewRootImpl; @@ -114,6 +121,9 @@ public final class AccessibilityInteractionController { private AddNodeInfosForViewId mAddNodeInfosForViewId; @GuardedBy("mLock") + private ArrayList<Message> mPendingFindNodeByIdMessages; + + @GuardedBy("mLock") private int mNumActiveRequestPreparers; @GuardedBy("mLock") private List<MessageHolder> mMessagesWaitingForRequestPreparer; @@ -128,6 +138,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,6 +188,9 @@ public final class AccessibilityInteractionController { args.arg4 = arguments; message.obj = args; + 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, @@ -402,6 +455,7 @@ public final class AccessibilityInteractionController { mAddNodeInfosForViewId.reset(); } } finally { + mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = 0; updateInfosForViewportAndReturnFindNodeResult( infos, callback, interactionId, spec, interactiveRegion); } @@ -484,6 +538,7 @@ public final class AccessibilityInteractionController { } } } finally { + mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = 0; updateInfosForViewportAndReturnFindNodeResult( infos, callback, interactionId, spec, interactiveRegion); } @@ -575,6 +630,7 @@ public final class AccessibilityInteractionController { } } } finally { + mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = 0; updateInfoForViewportAndReturnFindNodeResult( focused, callback, interactionId, spec, interactiveRegion); } @@ -629,6 +685,7 @@ public final class AccessibilityInteractionController { } } } finally { + mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = 0; updateInfoForViewportAndReturnFindNodeResult( next, callback, interactionId, spec, interactiveRegion); } @@ -785,33 +842,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) { @@ -832,17 +862,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; @@ -890,17 +909,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; @@ -974,18 +982,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(); @@ -995,22 +1031,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( @@ -1053,56 +1147,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(); @@ -1113,7 +1196,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; @@ -1180,9 +1262,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) { @@ -1194,6 +1278,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; @@ -1203,7 +1290,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); @@ -1231,7 +1318,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 = @@ -1242,7 +1329,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); @@ -1267,7 +1354,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(); @@ -1289,7 +1376,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 = @@ -1334,7 +1421,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); @@ -1359,7 +1446,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); @@ -1369,7 +1456,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); @@ -1478,6 +1565,10 @@ public final class AccessibilityInteractionController { boolean hasAccessibilityCallback(Message message) { return message.what < FIRST_NO_ACCESSIBILITY_CALLBACK_MSG ? true : false; } + + boolean hasUserInteractiveMessagesWaiting() { + return hasMessagesOrCallbacks(); + } } private final class AddNodeInfosForViewId implements Predicate<View> { diff --git a/core/java/android/view/accessibility/AccessibilityInteractionClient.java b/core/java/android/view/accessibility/AccessibilityInteractionClient.java index f63749be6df2..9556c25575cd 100644 --- a/core/java/android/view/accessibility/AccessibilityInteractionClient.java +++ b/core/java/android/view/accessibility/AccessibilityInteractionClient.java @@ -23,7 +23,9 @@ import android.compat.annotation.UnsupportedAppUsage; import android.os.Binder; import android.os.Build; import android.os.Bundle; +import android.os.Handler; import android.os.IBinder; +import android.os.Looper; import android.os.Message; import android.os.Process; import android.os.RemoteException; @@ -113,6 +115,8 @@ public final class AccessibilityInteractionClient private final Object mInstanceLock = new Object(); + private Handler mMainHandler; + private volatile int mInteractionId = -1; private AccessibilityNodeInfo mFindAccessibilityNodeInfoResult; @@ -123,6 +127,11 @@ public final class AccessibilityInteractionClient private Message mSameThreadMessage; + private int mInteractionIdWaitingForPrefetchResult; + private int mConnectionIdWaitingForPrefetchResult; + private String[] mPackageNamesForNextPrefetchResult; + private Runnable mPrefetchResultRunnable; + /** * @return The client for the current thread. */ @@ -197,6 +206,9 @@ public final class AccessibilityInteractionClient private AccessibilityInteractionClient() { /* reducing constructor visibility */ + if (Looper.getMainLooper() != null) { + mMainHandler = new Handler(Looper.getMainLooper()); + } } /** @@ -451,16 +463,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 +486,15 @@ public final class AccessibilityInteractionClient return null; } + private void setInteractionWaitingForPrefetchResult(int interactionId, int connectionId, + String[] packageNames) { + synchronized (mInstanceLock) { + mInteractionIdWaitingForPrefetchResult = interactionId; + mConnectionIdWaitingForPrefetchResult = connectionId; + mPackageNamesForNextPrefetchResult = packageNames; + } + } + private static String idToString(int accessibilityWindowId, long accessibilityNodeId) { return accessibilityWindowId + "/" + AccessibilityNodeInfo.idToString(accessibilityNodeId); @@ -829,6 +850,60 @@ public final class AccessibilityInteractionClient } /** + * {@inheritDoc} + */ + @Override + public void setPrefetchAccessibilityNodeInfoResult(@NonNull List<AccessibilityNodeInfo> infos, + int interactionId) { + List<AccessibilityNodeInfo> infosCopy = null; + int mConnectionIdWaitingForPrefetchResultCopy = -1; + String[] mPackageNamesForNextPrefetchResultCopy = null; + + synchronized (mInstanceLock) { + if (!infos.isEmpty() && mInteractionIdWaitingForPrefetchResult == interactionId) { + if (mMainHandler != null) { + if (mPrefetchResultRunnable != null) { + mMainHandler.removeCallbacks(mPrefetchResultRunnable); + mPrefetchResultRunnable = null; + } + /** + * TODO(b/180957109): AccessibilityCache is prone to deadlocks + * We post caching the prefetched nodes in the main thread. Using the binder + * thread results in "Long monitor contention with owner main" logs where + * service response times may exceed 5 seconds. This is due to the cache calling + * out to the system when refreshing nodes with the lock held. + */ + mPrefetchResultRunnable = () -> finalizeAndCacheAccessibilityNodeInfos( + infos, mConnectionIdWaitingForPrefetchResult, false, + mPackageNamesForNextPrefetchResult); + mMainHandler.post(mPrefetchResultRunnable); + + } else { + for (AccessibilityNodeInfo info : infos) { + infosCopy.add(new AccessibilityNodeInfo(info)); + } + mConnectionIdWaitingForPrefetchResultCopy = + mConnectionIdWaitingForPrefetchResult; + mPackageNamesForNextPrefetchResultCopy = + new String[mPackageNamesForNextPrefetchResult.length]; + for (int i = 0; i < mPackageNamesForNextPrefetchResult.length; i++) { + mPackageNamesForNextPrefetchResultCopy[i] = + mPackageNamesForNextPrefetchResult[i]; + + } + } + } + + } + + if (infosCopy != null) { + finalizeAndCacheAccessibilityNodeInfos( + infosCopy, mConnectionIdWaitingForPrefetchResultCopy, false, + mPackageNamesForNextPrefetchResultCopy); + } + } + + /** * Gets the result of a request to perform an accessibility action. * * @param interactionId The interaction id to match the result with the request. diff --git a/core/java/android/view/accessibility/IAccessibilityInteractionConnectionCallback.aidl b/core/java/android/view/accessibility/IAccessibilityInteractionConnectionCallback.aidl index 049bb31adbb1..231e75a19a06 100644 --- a/core/java/android/view/accessibility/IAccessibilityInteractionConnectionCallback.aidl +++ b/core/java/android/view/accessibility/IAccessibilityInteractionConnectionCallback.aidl @@ -47,6 +47,15 @@ oneway interface IAccessibilityInteractionConnectionCallback { int interactionId); /** + * Sets the result of a prefetch request that returns {@link AccessibilityNodeInfo}s. + * + * @param root The {@link AccessibilityNodeInfo} for which the prefetching is based off of. + * @param infos The result {@link AccessibilityNodeInfo}s. + */ + void setPrefetchAccessibilityNodeInfoResult( + in List<AccessibilityNodeInfo> infos, int interactionId); + + /** * Sets the result of a request to perform an accessibility action. * * @param Whether the action was performed. diff --git a/core/tests/coretests/src/android/view/accessibility/AccessibilityInteractionClientTest.java b/core/tests/coretests/src/android/view/accessibility/AccessibilityInteractionClientTest.java index ab24f89015c7..7e1e7f4bdd7f 100644 --- a/core/tests/coretests/src/android/view/accessibility/AccessibilityInteractionClientTest.java +++ b/core/tests/coretests/src/android/view/accessibility/AccessibilityInteractionClientTest.java @@ -33,9 +33,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import java.util.Arrays; -import java.util.List; - /** * Tests for AccessibilityInteractionClient */ @@ -65,7 +62,7 @@ public class AccessibilityInteractionClientTest { final long accessibilityNodeId = 0x4321L; AccessibilityNodeInfo nodeFromConnection = AccessibilityNodeInfo.obtain(); nodeFromConnection.setSourceNodeId(accessibilityNodeId, windowId); - mMockConnection.mInfosToReturn = Arrays.asList(nodeFromConnection); + mMockConnection.mInfoToReturn = nodeFromConnection; AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance(); AccessibilityNodeInfo node = client.findAccessibilityNodeInfoByAccessibilityId( MOCK_CONNECTION_ID, windowId, accessibilityNodeId, true, 0, null); @@ -75,7 +72,7 @@ public class AccessibilityInteractionClientTest { } private static class MockConnection extends AccessibilityServiceConnectionImpl { - List<AccessibilityNodeInfo> mInfosToReturn; + AccessibilityNodeInfo mInfoToReturn; @Override public String[] findAccessibilityNodeInfoByAccessibilityId(int accessibilityWindowId, @@ -83,7 +80,7 @@ public class AccessibilityInteractionClientTest { IAccessibilityInteractionConnectionCallback callback, int flags, long threadId, Bundle arguments) { try { - callback.setFindAccessibilityNodeInfosResult(mInfosToReturn, interactionId); + callback.setFindAccessibilityNodeInfoResult(mInfoToReturn, interactionId); } catch (RemoteException e) { throw new RuntimeException(e); } diff --git a/services/accessibility/java/com/android/server/accessibility/ActionReplacingCallback.java b/services/accessibility/java/com/android/server/accessibility/ActionReplacingCallback.java index bafb641dcc9e..6828dd916701 100644 --- a/services/accessibility/java/com/android/server/accessibility/ActionReplacingCallback.java +++ b/services/accessibility/java/com/android/server/accessibility/ActionReplacingCallback.java @@ -40,29 +40,34 @@ public class ActionReplacingCallback extends IAccessibilityInteractionConnection private final IAccessibilityInteractionConnectionCallback mServiceCallback; private final IAccessibilityInteractionConnection mConnectionWithReplacementActions; private final int mInteractionId; + private final int mNodeWithReplacementActionsInteractionId; private final Object mLock = new Object(); @GuardedBy("mLock") - List<AccessibilityNodeInfo> mNodesWithReplacementActions; + private boolean mReplacementNodeIsReadyOrFailed; + + @GuardedBy("mLock") + AccessibilityNodeInfo mNodeWithReplacementActions; @GuardedBy("mLock") List<AccessibilityNodeInfo> mNodesFromOriginalWindow; @GuardedBy("mLock") + boolean mSetFindNodeFromOriginalWindowCalled = false; + + @GuardedBy("mLock") AccessibilityNodeInfo mNodeFromOriginalWindow; - // Keep track of whether or not we've been called back for a single node @GuardedBy("mLock") - boolean mSingleNodeCallbackHappened; + boolean mSetFindNodesFromOriginalWindowCalled = false; + - // Keep track of whether or not we've been called back for multiple node @GuardedBy("mLock") - boolean mMultiNodeCallbackHappened; + List<AccessibilityNodeInfo> mPrefetchedNodesFromOriginalWindow; - // We shouldn't get any more callbacks after we've called back the original service, but - // keep track to make sure we catch such strange things @GuardedBy("mLock") - boolean mDone; + boolean mSetPrefetchFromOriginalWindowCalled = false; + public ActionReplacingCallback(IAccessibilityInteractionConnectionCallback serviceCallback, IAccessibilityInteractionConnection connectionWithReplacementActions, @@ -70,19 +75,20 @@ public class ActionReplacingCallback extends IAccessibilityInteractionConnection mServiceCallback = serviceCallback; mConnectionWithReplacementActions = connectionWithReplacementActions; mInteractionId = interactionId; + mNodeWithReplacementActionsInteractionId = interactionId + 1; // Request the root node of the replacing window final long identityToken = Binder.clearCallingIdentity(); try { mConnectionWithReplacementActions.findAccessibilityNodeInfoByAccessibilityId( - AccessibilityNodeInfo.ROOT_NODE_ID, null, interactionId + 1, this, 0, + AccessibilityNodeInfo.ROOT_NODE_ID, null, + mNodeWithReplacementActionsInteractionId, this, 0, interrogatingPid, interrogatingTid, null, null); } catch (RemoteException re) { if (DEBUG) { Slog.e(LOG_TAG, "Error calling findAccessibilityNodeInfoByAccessibilityId()"); } - // Pretend we already got a (null) list of replacement nodes - mMultiNodeCallbackHappened = true; + mReplacementNodeIsReadyOrFailed = true; } finally { Binder.restoreCallingIdentity(identityToken); } @@ -90,46 +96,67 @@ public class ActionReplacingCallback extends IAccessibilityInteractionConnection @Override public void setFindAccessibilityNodeInfoResult(AccessibilityNodeInfo info, int interactionId) { - boolean readyForCallback; - synchronized(mLock) { + synchronized (mLock) { if (interactionId == mInteractionId) { mNodeFromOriginalWindow = info; + mSetFindNodeFromOriginalWindowCalled = true; + } else if (interactionId == mNodeWithReplacementActionsInteractionId) { + mNodeWithReplacementActions = info; + mReplacementNodeIsReadyOrFailed = true; } else { Slog.e(LOG_TAG, "Callback with unexpected interactionId"); return; } - - mSingleNodeCallbackHappened = true; - readyForCallback = mMultiNodeCallbackHappened; - } - if (readyForCallback) { - replaceInfoActionsAndCallService(); } + replaceInfoActionsAndCallServiceIfReady(); } @Override public void setFindAccessibilityNodeInfosResult(List<AccessibilityNodeInfo> infos, int interactionId) { - boolean callbackForSingleNode; - boolean callbackForMultipleNodes; - synchronized(mLock) { + synchronized (mLock) { if (interactionId == mInteractionId) { mNodesFromOriginalWindow = infos; - } else if (interactionId == mInteractionId + 1) { - mNodesWithReplacementActions = infos; + mSetFindNodesFromOriginalWindowCalled = true; + } else if (interactionId == mNodeWithReplacementActionsInteractionId) { + setNodeWithReplacementActionsFromList(infos); + mReplacementNodeIsReadyOrFailed = true; } else { Slog.e(LOG_TAG, "Callback with unexpected interactionId"); return; } - callbackForSingleNode = mSingleNodeCallbackHappened; - callbackForMultipleNodes = mMultiNodeCallbackHappened; - mMultiNodeCallbackHappened = true; } - if (callbackForSingleNode) { - replaceInfoActionsAndCallService(); + replaceInfoActionsAndCallServiceIfReady(); + } + + @Override + public void setPrefetchAccessibilityNodeInfoResult(List<AccessibilityNodeInfo> infos, + int interactionId) + throws RemoteException { + synchronized (mLock) { + if (interactionId == mInteractionId) { + mPrefetchedNodesFromOriginalWindow = infos; + mSetPrefetchFromOriginalWindowCalled = true; + } else { + Slog.e(LOG_TAG, "Callback with unexpected interactionId"); + return; + } } - if (callbackForMultipleNodes) { - replaceInfosActionsAndCallService(); + replaceInfoActionsAndCallServiceIfReady(); + } + + private void replaceInfoActionsAndCallServiceIfReady() { + replaceInfoActionsAndCallService(); + replaceInfosActionsAndCallService(); + replacePrefetchInfosActionsAndCallService(); + } + + private void setNodeWithReplacementActionsFromList(List<AccessibilityNodeInfo> infos) { + for (int i = 0; i < infos.size(); i++) { + AccessibilityNodeInfo info = infos.get(i); + if (info.getSourceNodeId() == AccessibilityNodeInfo.ROOT_NODE_ID) { + mNodeWithReplacementActions = info; + } } } @@ -142,55 +169,81 @@ public class ActionReplacingCallback extends IAccessibilityInteractionConnection private void replaceInfoActionsAndCallService() { final AccessibilityNodeInfo nodeToReturn; + boolean doCallback = false; synchronized (mLock) { - if (mDone) { - if (DEBUG) { - Slog.e(LOG_TAG, "Extra callback"); - } - return; - } - if (mNodeFromOriginalWindow != null) { + doCallback = mReplacementNodeIsReadyOrFailed + && mSetFindNodeFromOriginalWindowCalled; + if (doCallback && mNodeFromOriginalWindow != null) { replaceActionsOnInfoLocked(mNodeFromOriginalWindow); + mSetFindNodeFromOriginalWindowCalled = false; } - recycleReplaceActionNodesLocked(); nodeToReturn = mNodeFromOriginalWindow; - mDone = true; } - try { - mServiceCallback.setFindAccessibilityNodeInfoResult(nodeToReturn, mInteractionId); - } catch (RemoteException re) { - if (DEBUG) { - Slog.e(LOG_TAG, "Failed to setFindAccessibilityNodeInfoResult"); + if (doCallback) { + try { + mServiceCallback.setFindAccessibilityNodeInfoResult(nodeToReturn, mInteractionId); + } catch (RemoteException re) { + if (DEBUG) { + Slog.e(LOG_TAG, "Failed to setFindAccessibilityNodeInfoResult"); + } } } } private void replaceInfosActionsAndCallService() { - final List<AccessibilityNodeInfo> nodesToReturn; + List<AccessibilityNodeInfo> nodesToReturn = null; + boolean doCallback = false; synchronized (mLock) { - if (mDone) { + doCallback = mReplacementNodeIsReadyOrFailed + && mSetFindNodesFromOriginalWindowCalled; + if (doCallback) { + nodesToReturn = replaceActionsLocked(mNodesFromOriginalWindow); + mSetFindNodesFromOriginalWindowCalled = false; + } + } + if (doCallback) { + try { + mServiceCallback.setFindAccessibilityNodeInfosResult(nodesToReturn, mInteractionId); + } catch (RemoteException re) { if (DEBUG) { - Slog.e(LOG_TAG, "Extra callback"); + Slog.e(LOG_TAG, "Failed to setFindAccessibilityNodeInfosResult"); } - return; } - if (mNodesFromOriginalWindow != null) { - for (int i = 0; i < mNodesFromOriginalWindow.size(); i++) { - replaceActionsOnInfoLocked(mNodesFromOriginalWindow.get(i)); + } + } + + private void replacePrefetchInfosActionsAndCallService() { + List<AccessibilityNodeInfo> nodesToReturn = null; + boolean doCallback = false; + synchronized (mLock) { + doCallback = mReplacementNodeIsReadyOrFailed + && mSetPrefetchFromOriginalWindowCalled; + if (doCallback) { + nodesToReturn = replaceActionsLocked(mPrefetchedNodesFromOriginalWindow); + mSetPrefetchFromOriginalWindowCalled = false; + } + } + if (doCallback) { + try { + mServiceCallback.setPrefetchAccessibilityNodeInfoResult( + nodesToReturn, mInteractionId); + } catch (RemoteException re) { + if (DEBUG) { + Slog.e(LOG_TAG, "Failed to setFindAccessibilityNodeInfosResult"); } } - recycleReplaceActionNodesLocked(); - nodesToReturn = (mNodesFromOriginalWindow == null) - ? null : new ArrayList<>(mNodesFromOriginalWindow); - mDone = true; } - try { - mServiceCallback.setFindAccessibilityNodeInfosResult(nodesToReturn, mInteractionId); - } catch (RemoteException re) { - if (DEBUG) { - Slog.e(LOG_TAG, "Failed to setFindAccessibilityNodeInfosResult"); + } + + @GuardedBy("mLock") + private List<AccessibilityNodeInfo> replaceActionsLocked(List<AccessibilityNodeInfo> infos) { + if (infos != null) { + for (int i = 0; i < infos.size(); i++) { + replaceActionsOnInfoLocked(infos.get(i)); } } + return (infos == null) + ? null : new ArrayList<>(infos); } @GuardedBy("mLock") @@ -204,40 +257,22 @@ public class ActionReplacingCallback extends IAccessibilityInteractionConnection info.setDismissable(false); // We currently only replace actions for the root node if ((info.getSourceNodeId() == AccessibilityNodeInfo.ROOT_NODE_ID) - && mNodesWithReplacementActions != null) { - // This list should always contain a single node with the root ID - for (int i = 0; i < mNodesWithReplacementActions.size(); i++) { - AccessibilityNodeInfo nodeWithReplacementActions = - mNodesWithReplacementActions.get(i); - if (nodeWithReplacementActions.getSourceNodeId() - == AccessibilityNodeInfo.ROOT_NODE_ID) { - List<AccessibilityAction> actions = nodeWithReplacementActions.getActionList(); - if (actions != null) { - for (int j = 0; j < actions.size(); j++) { - info.addAction(actions.get(j)); - } - // The PIP needs to be able to take accessibility focus - info.addAction(AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS); - info.addAction(AccessibilityAction.ACTION_CLEAR_ACCESSIBILITY_FOCUS); - } - info.setClickable(nodeWithReplacementActions.isClickable()); - info.setFocusable(nodeWithReplacementActions.isFocusable()); - info.setContextClickable(nodeWithReplacementActions.isContextClickable()); - info.setScrollable(nodeWithReplacementActions.isScrollable()); - info.setLongClickable(nodeWithReplacementActions.isLongClickable()); - info.setDismissable(nodeWithReplacementActions.isDismissable()); + && mNodeWithReplacementActions != null) { + List<AccessibilityAction> actions = mNodeWithReplacementActions.getActionList(); + if (actions != null) { + for (int j = 0; j < actions.size(); j++) { + info.addAction(actions.get(j)); } + // The PIP needs to be able to take accessibility focus + info.addAction(AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS); + info.addAction(AccessibilityAction.ACTION_CLEAR_ACCESSIBILITY_FOCUS); } + info.setClickable(mNodeWithReplacementActions.isClickable()); + info.setFocusable(mNodeWithReplacementActions.isFocusable()); + info.setContextClickable(mNodeWithReplacementActions.isContextClickable()); + info.setScrollable(mNodeWithReplacementActions.isScrollable()); + info.setLongClickable(mNodeWithReplacementActions.isLongClickable()); + info.setDismissable(mNodeWithReplacementActions.isDismissable()); } } - - @GuardedBy("mLock") - private void recycleReplaceActionNodesLocked() { - if (mNodesWithReplacementActions == null) return; - for (int i = mNodesWithReplacementActions.size() - 1; i >= 0; i--) { - AccessibilityNodeInfo nodeWithReplacementAction = mNodesWithReplacementActions.get(i); - nodeWithReplacementAction.recycle(); - } - mNodesWithReplacementActions = null; - } } diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityInteractionControllerNodeRequestsTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityInteractionControllerNodeRequestsTest.java new file mode 100644 index 000000000000..170f561aa2da --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityInteractionControllerNodeRequestsTest.java @@ -0,0 +1,581 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.accessibility; + + +import static android.view.accessibility.AccessibilityNodeInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS; +import static android.view.accessibility.AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS; +import static android.view.accessibility.AccessibilityNodeInfo.FLAG_PREFETCH_SIBLINGS; +import static android.view.accessibility.AccessibilityNodeInfo.ROOT_NODE_ID; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.app.Instrumentation; +import android.content.Context; +import android.os.RemoteException; +import android.view.AccessibilityInteractionController; +import android.view.View; +import android.view.ViewRootImpl; +import android.view.WindowManager; +import android.view.accessibility.AccessibilityNodeIdManager; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeProvider; +import android.view.accessibility.IAccessibilityInteractionConnectionCallback; +import android.widget.FrameLayout; +import android.widget.TextView; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.List; + +/** + * Tests that verify expected node and prefetched node results when finding a view by node id. We + * send some requests to the controller via View methods to control message timing. + */ +@RunWith(AndroidJUnit4.class) +public class AccessibilityInteractionControllerNodeRequestsTest { + private AccessibilityInteractionController mAccessibilityInteractionController; + @Mock + private IAccessibilityInteractionConnectionCallback mMockClientCallback1; + @Mock + private IAccessibilityInteractionConnectionCallback mMockClientCallback2; + + @Captor + private ArgumentCaptor<AccessibilityNodeInfo> mFindInfoCaptor; + @Captor private ArgumentCaptor<List<AccessibilityNodeInfo>> mPrefetchInfoListCaptor; + + private final Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation(); + private static final int MOCK_CLIENT_1_THREAD_AND_PROCESS_ID = 1; + private static final int MOCK_CLIENT_2_THREAD_AND_PROCESS_ID = 2; + + private static final String FRAME_LAYOUT_DESCRIPTION = "frameLayout"; + private static final String TEXT_VIEW_1_DESCRIPTION = "textView1"; + private static final String TEXT_VIEW_2_DESCRIPTION = "textView2"; + + private TestFrameLayout mFrameLayout; + private TestTextView mTextView1; + private TestTextView2 mTextView2; + + private boolean mSendClient1RequestForTextAfterTextPrefetched; + private boolean mSendClient2RequestForTextAfterTextPrefetched; + private boolean mSendRequestForTextAndIncludeUnImportantViews; + private int mMockClient1InteractionId; + private int mMockClient2InteractionId; + + @Before + public void setUp() throws Throwable { + MockitoAnnotations.initMocks(this); + + mInstrumentation.runOnMainSync(() -> { + final Context context = mInstrumentation.getTargetContext(); + final ViewRootImpl viewRootImpl = new ViewRootImpl(context, context.getDisplay()); + + mFrameLayout = new TestFrameLayout(context); + mTextView1 = new TestTextView(context); + mTextView2 = new TestTextView2(context); + + mFrameLayout.addView(mTextView1); + mFrameLayout.addView(mTextView2); + + // The controller retrieves views through this manager, and registration happens on + // when attached to a window, which we don't have. We can simply reference FrameLayout + // with ROOT_NODE_ID + AccessibilityNodeIdManager.getInstance().registerViewWithId( + mTextView1, mTextView1.getAccessibilityViewId()); + AccessibilityNodeIdManager.getInstance().registerViewWithId( + mTextView2, mTextView2.getAccessibilityViewId()); + + try { + viewRootImpl.setView(mFrameLayout, new WindowManager.LayoutParams(), null); + + } catch (WindowManager.BadTokenException e) { + // activity isn't running, we will ignore BadTokenException. + } + + mAccessibilityInteractionController = + new AccessibilityInteractionController(viewRootImpl); + }); + + } + + @After + public void tearDown() throws Throwable { + AccessibilityNodeIdManager.getInstance().unregisterViewWithId( + mTextView1.getAccessibilityViewId()); + AccessibilityNodeIdManager.getInstance().unregisterViewWithId( + mTextView2.getAccessibilityViewId()); + } + + /** + * Tests a basic request for the root node with prefetch flag + * {@link AccessibilityNodeInfo#FLAG_PREFETCH_DESCENDANTS} + * + * @throws RemoteException + */ + @Test + public void testFindRootView_withOneClient_shouldReturnRootNodeAndPrefetchDescendants() + throws RemoteException { + // Request for our FrameLayout + sendNodeRequestToController(ROOT_NODE_ID, mMockClientCallback1, + mMockClient1InteractionId, FLAG_PREFETCH_DESCENDANTS); + mInstrumentation.waitForIdleSync(); + + // Verify we get FrameLayout + verify(mMockClientCallback1).setFindAccessibilityNodeInfoResult( + mFindInfoCaptor.capture(), eq(mMockClient1InteractionId)); + AccessibilityNodeInfo infoSentToService = mFindInfoCaptor.getValue(); + assertEquals(FRAME_LAYOUT_DESCRIPTION, infoSentToService.getContentDescription()); + + verify(mMockClientCallback1).setPrefetchAccessibilityNodeInfoResult( + mPrefetchInfoListCaptor.capture(), eq(mMockClient1InteractionId)); + // The descendants are our two TextViews + List<AccessibilityNodeInfo> prefetchedNodes = mPrefetchInfoListCaptor.getValue(); + assertEquals(2, prefetchedNodes.size()); + assertEquals(TEXT_VIEW_1_DESCRIPTION, prefetchedNodes.get(0).getContentDescription()); + assertEquals(TEXT_VIEW_2_DESCRIPTION, prefetchedNodes.get(1).getContentDescription()); + + } + + /** + * Tests a basic request for TestTextView1's node with prefetch flag + * {@link AccessibilityNodeInfo#FLAG_PREFETCH_SIBLINGS} + * + * @throws RemoteException + */ + @Test + public void testFindTextView_withOneClient_shouldReturnNodeAndPrefetchedSiblings() + throws RemoteException { + // Request for TextView1 + sendNodeRequestToController(AccessibilityNodeInfo.makeNodeId( + mTextView1.getAccessibilityViewId(), AccessibilityNodeProvider.HOST_VIEW_ID), + mMockClientCallback1, mMockClient1InteractionId, FLAG_PREFETCH_SIBLINGS); + mInstrumentation.waitForIdleSync(); + + // Verify we get TextView1 + verify(mMockClientCallback1).setFindAccessibilityNodeInfoResult( + mFindInfoCaptor.capture(), eq(mMockClient1InteractionId)); + AccessibilityNodeInfo infoSentToService = mFindInfoCaptor.getValue(); + assertEquals(TEXT_VIEW_1_DESCRIPTION, infoSentToService.getContentDescription()); + + // Verify the prefetched sibling of TextView1 is TextView2 + verify(mMockClientCallback1).setPrefetchAccessibilityNodeInfoResult( + mPrefetchInfoListCaptor.capture(), eq(mMockClient1InteractionId)); + // TextView2 is the prefetched sibling + List<AccessibilityNodeInfo> prefetchedNodes = mPrefetchInfoListCaptor.getValue(); + assertEquals(1, prefetchedNodes.size()); + assertEquals(TEXT_VIEW_2_DESCRIPTION, prefetchedNodes.get(0).getContentDescription()); + } + + /** + * Tests a series of controller requests to prevent prefetching. + * Request 1: Client 1 requests the root node + * Request 2: When the root node is initialized in + * {@link TestFrameLayout#onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo)}, + * Client 2 requests TestTextView1's node + * + * Request 2 on the queue prevents prefetching for Request 1. + * + * @throws RemoteException + */ + @Test + public void testFindRootAndTextNodes_withTwoClients_shouldPreventClient1Prefetch() + throws RemoteException { + mFrameLayout.setAccessibilityDelegate(new View.AccessibilityDelegate() { + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + final long nodeId = AccessibilityNodeInfo.makeNodeId( + mTextView1.getAccessibilityViewId(), + AccessibilityNodeProvider.HOST_VIEW_ID); + + // Enqueue a request when this node is found from a different service for + // TextView1 + sendNodeRequestToController(nodeId, mMockClientCallback2, + mMockClient2InteractionId, FLAG_PREFETCH_SIBLINGS); + } + }); + // Client 1 request for FrameLayout + sendNodeRequestToController(ROOT_NODE_ID, mMockClientCallback1, + mMockClient1InteractionId, FLAG_PREFETCH_DESCENDANTS); + + mInstrumentation.waitForIdleSync(); + + // Verify client 1 gets FrameLayout + verify(mMockClientCallback1).setFindAccessibilityNodeInfoResult( + mFindInfoCaptor.capture(), eq(mMockClient1InteractionId)); + AccessibilityNodeInfo infoSentToService = mFindInfoCaptor.getValue(); + assertEquals(FRAME_LAYOUT_DESCRIPTION, infoSentToService.getContentDescription()); + + // The second request is put in the queue in the FrameLayout's onInitializeA11yNodeInfo, + // meaning prefetching is interrupted and does not even begin for the first request + verify(mMockClientCallback1, never()) + .setPrefetchAccessibilityNodeInfoResult(anyList(), anyInt()); + + // Verify client 2 gets TextView1 + verify(mMockClientCallback2).setFindAccessibilityNodeInfoResult( + mFindInfoCaptor.capture(), eq(mMockClient2InteractionId)); + infoSentToService = mFindInfoCaptor.getValue(); + assertEquals(TEXT_VIEW_1_DESCRIPTION, infoSentToService.getContentDescription()); + + // Verify the prefetched sibling of TextView1 is TextView2 (FLAG_PREFETCH_SIBLINGS) + verify(mMockClientCallback2).setPrefetchAccessibilityNodeInfoResult( + mPrefetchInfoListCaptor.capture(), eq(mMockClient2InteractionId)); + List<AccessibilityNodeInfo> prefetchedNodes = mPrefetchInfoListCaptor.getValue(); + assertEquals(1, prefetchedNodes.size()); + assertEquals(TEXT_VIEW_2_DESCRIPTION, prefetchedNodes.get(0).getContentDescription()); + } + + /** + * Tests a series of controller same-service requests to interrupt prefetching and satisfy a + * pending node request. + * Request 1: Request the root node + * Request 2: When TextTextView1's node is initialized as part of Request 1's prefetching, + * request TestTextView1's node + * + * Request 1 prefetches TestTextView1's node, is interrupted by a pending request, and checks + * if its prefetched nodes satisfy any pending requests. It satisfies Request 2's request for + * TestTextView1's node. Request 2 is fulfilled, so it is removed from queue and does not + * prefetch. + * + * @throws RemoteException + */ + @Test + public void testFindRootAndTextNode_withOneClient_shouldInterruptPrefetchAndSatisfyPendingMsg() + throws RemoteException { + mSendClient1RequestForTextAfterTextPrefetched = true; + + mTextView1.setAccessibilityDelegate(new View.AccessibilityDelegate(){ + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + info.setContentDescription(TEXT_VIEW_1_DESCRIPTION); + final long nodeId = AccessibilityNodeInfo.makeNodeId( + mTextView1.getAccessibilityViewId(), + AccessibilityNodeProvider.HOST_VIEW_ID); + + if (mSendClient1RequestForTextAfterTextPrefetched) { + // Prevent a loop when processing second request + mSendClient1RequestForTextAfterTextPrefetched = false; + // TextView1 is prefetched here after the FrameLayout is found. Now enqueue a + // same-client request for TextView1 + sendNodeRequestToController(nodeId, mMockClientCallback1, + ++mMockClient1InteractionId, FLAG_PREFETCH_SIBLINGS); + + } + } + }); + // Client 1 requests FrameLayout + sendNodeRequestToController(ROOT_NODE_ID, mMockClientCallback1, + mMockClient1InteractionId, FLAG_PREFETCH_DESCENDANTS); + + // Flush out all messages + mInstrumentation.waitForIdleSync(); + + // When TextView1 is prefetched for FrameLayout, we put a message on the queue in + // TextView1's onInitializeA11yNodeInfo that requests for TextView1. The service thus get + // two node results for FrameLayout and TextView1. + verify(mMockClientCallback1, times(2)) + .setFindAccessibilityNodeInfoResult(mFindInfoCaptor.capture(), anyInt()); + + List<AccessibilityNodeInfo> foundNodes = mFindInfoCaptor.getAllValues(); + assertEquals(FRAME_LAYOUT_DESCRIPTION, foundNodes.get(0).getContentDescription()); + assertEquals(TEXT_VIEW_1_DESCRIPTION, foundNodes.get(1).getContentDescription()); + + // The controller will look at FrameLayout's prefetched nodes and find matching nodes in + // pending requests. The prefetched TextView1 matches the second request. The second + // request was removed from queue and prefetching for this request never occurred. + verify(mMockClientCallback1, times(1)) + .setPrefetchAccessibilityNodeInfoResult(mPrefetchInfoListCaptor.capture(), + eq(mMockClient1InteractionId - 1)); + List<AccessibilityNodeInfo> prefetchedNodes = mPrefetchInfoListCaptor.getValue(); + assertEquals(1, prefetchedNodes.size()); + assertEquals(TEXT_VIEW_1_DESCRIPTION, prefetchedNodes.get(0).getContentDescription()); + } + + /** + * Like above, but tests a series of controller requests from different services to interrupt + * prefetching and satisfy a pending node request. + * + * @throws RemoteException + */ + @Test + public void testFindRootAndTextNode_withTwoClients_shouldInterruptPrefetchAndSatisfyPendingMsg() + throws RemoteException { + mSendClient2RequestForTextAfterTextPrefetched = true; + mTextView1.setAccessibilityDelegate(new View.AccessibilityDelegate(){ + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + info.setContentDescription(TEXT_VIEW_1_DESCRIPTION); + final long nodeId = AccessibilityNodeInfo.makeNodeId( + mTextView1.getAccessibilityViewId(), + AccessibilityNodeProvider.HOST_VIEW_ID); + + if (mSendClient2RequestForTextAfterTextPrefetched) { + mSendClient2RequestForTextAfterTextPrefetched = false; + // TextView1 is prefetched here. Now enqueue client 2's request for + // TextView1 + sendNodeRequestToController(nodeId, mMockClientCallback2, + mMockClient2InteractionId, FLAG_PREFETCH_SIBLINGS); + } + } + }); + // Client 1 requests FrameLayout + sendNodeRequestToController(ROOT_NODE_ID, mMockClientCallback1, + mMockClient1InteractionId, FLAG_PREFETCH_DESCENDANTS); + + mInstrumentation.waitForIdleSync(); + + // Verify client 1 gets FrameLayout + verify(mMockClientCallback1, times(1)) + .setFindAccessibilityNodeInfoResult(mFindInfoCaptor.capture(), anyInt()); + assertEquals(FRAME_LAYOUT_DESCRIPTION, + mFindInfoCaptor.getValue().getContentDescription()); + + // Verify client 1 has prefetched nodes + verify(mMockClientCallback1, times(1)) + .setPrefetchAccessibilityNodeInfoResult(mPrefetchInfoListCaptor.capture(), + eq(mMockClient1InteractionId)); + + // Verify client 1's only prefetched node is TextView1 + List<AccessibilityNodeInfo> prefetchedNodes = mPrefetchInfoListCaptor.getValue(); + assertEquals(1, prefetchedNodes.size()); + assertEquals(TEXT_VIEW_1_DESCRIPTION, prefetchedNodes.get(0).getContentDescription()); + + // Verify client 2 gets TextView1 + verify(mMockClientCallback2, times(1)) + .setFindAccessibilityNodeInfoResult(mFindInfoCaptor.capture(), anyInt()); + + assertEquals(TEXT_VIEW_1_DESCRIPTION, mFindInfoCaptor.getValue().getContentDescription()); + + // The second request was removed from queue and prefetching for this client request never + // occurred as it was satisfied. + verify(mMockClientCallback2, never()) + .setPrefetchAccessibilityNodeInfoResult(anyList(), anyInt()); + + } + + @Test + public void testFindNodeById_withTwoDifferentPrefetchFlags_shouldNotSatisfyPendingRequest() + throws RemoteException { + mSendRequestForTextAndIncludeUnImportantViews = true; + mTextView1.setAccessibilityDelegate(new View.AccessibilityDelegate(){ + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + info.setContentDescription(TEXT_VIEW_1_DESCRIPTION); + final long nodeId = AccessibilityNodeInfo.makeNodeId( + mTextView1.getAccessibilityViewId(), + AccessibilityNodeProvider.HOST_VIEW_ID); + + if (mSendRequestForTextAndIncludeUnImportantViews) { + mSendRequestForTextAndIncludeUnImportantViews = false; + // TextView1 is prefetched here for client 1. Now enqueue a request from a + // different client that holds different fetch flags for TextView1 + sendNodeRequestToController(nodeId, mMockClientCallback2, + mMockClient2InteractionId, + FLAG_PREFETCH_SIBLINGS | FLAG_INCLUDE_NOT_IMPORTANT_VIEWS); + } + } + }); + + // Mockito does not make copies of objects when called. It holds references, so + // the captor would point to client 2's results after all requests are processed. Verify + // prefetched node immediately + doAnswer(invocation -> { + List<AccessibilityNodeInfo> prefetched = invocation.getArgument(0); + assertEquals(TEXT_VIEW_1_DESCRIPTION, prefetched.get(0).getContentDescription()); + return null; + }).when(mMockClientCallback1).setPrefetchAccessibilityNodeInfoResult(anyList(), + eq(mMockClient1InteractionId)); + + // Client 1 requests FrameLayout + sendNodeRequestToController(ROOT_NODE_ID, mMockClientCallback1, + mMockClient1InteractionId, FLAG_PREFETCH_DESCENDANTS); + + mInstrumentation.waitForIdleSync(); + + // Verify client 1 gets FrameLayout + verify(mMockClientCallback1, times(1)) + .setFindAccessibilityNodeInfoResult(mFindInfoCaptor.capture(), + eq(mMockClient1InteractionId)); + + assertEquals(FRAME_LAYOUT_DESCRIPTION, + mFindInfoCaptor.getValue().getContentDescription()); + + // Verify client 1 has prefetched results. The only prefetched node is TextView1 + // (from above doAnswer) + verify(mMockClientCallback1, times(1)) + .setPrefetchAccessibilityNodeInfoResult(mPrefetchInfoListCaptor.capture(), + eq(mMockClient1InteractionId)); + + // Verify client 2 gets TextView1 + verify(mMockClientCallback2, times(1)) + .setFindAccessibilityNodeInfoResult(mFindInfoCaptor.capture(), + eq(mMockClient2InteractionId)); + assertEquals(TEXT_VIEW_1_DESCRIPTION, + mFindInfoCaptor.getValue().getContentDescription()); + // Verify client 2 has TextView2 as a prefetched node + verify(mMockClientCallback2, times(1)) + .setPrefetchAccessibilityNodeInfoResult(mPrefetchInfoListCaptor.capture(), + eq(mMockClient2InteractionId)); + List<AccessibilityNodeInfo> prefetchedNode = mPrefetchInfoListCaptor.getValue(); + assertEquals(1, prefetchedNode.size()); + assertEquals(TEXT_VIEW_2_DESCRIPTION, prefetchedNode.get(0).getContentDescription()); + } + + private void sendNodeRequestToController(long requestedNodeId, + IAccessibilityInteractionConnectionCallback callback, int interactionId, + int prefetchFlags) { + final int processAndThreadId = callback == mMockClientCallback1 + ? MOCK_CLIENT_1_THREAD_AND_PROCESS_ID + : MOCK_CLIENT_2_THREAD_AND_PROCESS_ID; + + mAccessibilityInteractionController.findAccessibilityNodeInfoByAccessibilityIdClientThread( + requestedNodeId, + null, interactionId, + callback, prefetchFlags, + processAndThreadId, + processAndThreadId, null, null); + + } + + private class TestFrameLayout extends FrameLayout { + + TestFrameLayout(Context context) { + super(context); + } + + @Override + public int getWindowVisibility() { + // We aren't attached to a window so let's pretend + return VISIBLE; + } + + @Override + public boolean isShown() { + // Controller check + return true; + } + + @Override + public int getAccessibilityViewId() { + // static id doesn't reset after tests so return the same one + return 0; + } + + @Override + public void addChildrenForAccessibility(ArrayList<View> outChildren) { + // ViewGroup#addChildrenForAccessbility sorting logic will switch these two + outChildren.add(mTextView1); + outChildren.add(mTextView2); + } + + @Override + public boolean includeForAccessibility() { + return true; + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setContentDescription(FRAME_LAYOUT_DESCRIPTION); + } + } + + private class TestTextView extends TextView { + TestTextView(Context context) { + super(context); + } + + @Override + public int getWindowVisibility() { + return VISIBLE; + } + + @Override + public boolean isShown() { + return true; + } + + @Override + public int getAccessibilityViewId() { + return 1; + } + + @Override + public boolean includeForAccessibility() { + return true; + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setContentDescription(TEXT_VIEW_1_DESCRIPTION); + } + } + + private class TestTextView2 extends TextView { + TestTextView2(Context context) { + super(context); + } + + @Override + public int getWindowVisibility() { + return VISIBLE; + } + + @Override + public boolean isShown() { + return true; + } + + @Override + public int getAccessibilityViewId() { + return 2; + } + + @Override + public boolean includeForAccessibility() { + return true; + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setContentDescription(TEXT_VIEW_2_DESCRIPTION); + } + } +} |