Merge "Do not prefetching accessibility nodes while a view is scrolling" into rvc-dev
diff --git a/core/java/android/view/accessibility/AccessibilityInteractionClient.java b/core/java/android/view/accessibility/AccessibilityInteractionClient.java
index b4c8795..299ae2f0 100644
--- a/core/java/android/view/accessibility/AccessibilityInteractionClient.java
+++ b/core/java/android/view/accessibility/AccessibilityInteractionClient.java
@@ -31,7 +31,9 @@
 import android.util.Log;
 import android.util.LongSparseArray;
 import android.util.SparseArray;
+import android.util.SparseLongArray;
 import android.view.Display;
+import android.view.ViewConfiguration;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.ArrayUtils;
@@ -89,6 +91,9 @@
 
     private static final long TIMEOUT_INTERACTION_MILLIS = 5000;
 
+    private static final long DISABLE_PREFETCHING_FOR_SCROLLING_MILLIS =
+            (long) (ViewConfiguration.getSendRecurringAccessibilityEventsInterval() * 1.5);
+
     private static final Object sStaticLock = new Object();
 
     private static final LongSparseArray<AccessibilityInteractionClient> sClients =
@@ -97,6 +102,10 @@
     private static final SparseArray<IAccessibilityServiceConnection> sConnectionCache =
             new SparseArray<>();
 
+    /** List of timestamps which indicate the latest time an a11y service receives a scroll event
+        from a window, mapping from windowId -> timestamp. */
+    private static final SparseLongArray sScrollingWindows = new SparseLongArray();
+
     private static AccessibilityCache sAccessibilityCache =
             new AccessibilityCache(new AccessibilityCache.AccessibilityNodeRefresher());
 
@@ -422,6 +431,14 @@
                         Log.i(LOG_TAG, "Node cache miss for "
                                 + idToString(accessibilityWindowId, accessibilityNodeId));
                     }
+                } else {
+                    // No need to prefech nodes in bypass cache case.
+                    prefetchFlags &= ~AccessibilityNodeInfo.FLAG_PREFETCH_MASK;
+                }
+                // Skip prefetching if window is scrolling.
+                if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_MASK) != 0
+                        && isWindowScrolling(accessibilityWindowId)) {
+                    prefetchFlags &= ~AccessibilityNodeInfo.FLAG_PREFETCH_MASK;
                 }
                 final int interactionId = mInteractionIdCounter.getAndIncrement();
                 final long identityToken = Binder.clearCallingIdentity();
@@ -718,6 +735,18 @@
     }
 
     public void onAccessibilityEvent(AccessibilityEvent event) {
+        switch (event.getEventType()) {
+            case AccessibilityEvent.TYPE_VIEW_SCROLLED:
+                updateScrollingWindow(event.getWindowId(), SystemClock.uptimeMillis());
+                break;
+            case AccessibilityEvent.TYPE_WINDOWS_CHANGED:
+                if (event.getWindowChanges() == AccessibilityEvent.WINDOWS_CHANGE_REMOVED) {
+                    deleteScrollingWindow(event.getWindowId());
+                }
+                break;
+            default:
+                break;
+        }
         sAccessibilityCache.onAccessibilityEvent(event);
     }
 
@@ -986,4 +1015,48 @@
             Log.e(LOG_TAG, disconnectedCount + " Disconnected nodes.");
         }
     }
+
+    /**
+     * Update scroll event timestamp of a given window.
+     *
+     * @param windowId The window id.
+     * @param uptimeMillis Device uptime millis.
+     */
+    private void updateScrollingWindow(int windowId, long uptimeMillis) {
+        synchronized (sScrollingWindows) {
+            sScrollingWindows.put(windowId, uptimeMillis);
+        }
+    }
+
+    /**
+     * Remove a window from the scrolling windows list.
+     *
+     * @param windowId The window id.
+     */
+    private void deleteScrollingWindow(int windowId) {
+        synchronized (sScrollingWindows) {
+            sScrollingWindows.delete(windowId);
+        }
+    }
+
+    /**
+     * Whether or not the window is scrolling.
+     *
+     * @param windowId
+     * @return true if it's scrolling.
+     */
+    private boolean isWindowScrolling(int windowId) {
+        synchronized (sScrollingWindows) {
+            final long latestScrollingTime = sScrollingWindows.get(windowId);
+            if (latestScrollingTime == 0) {
+                return false;
+            }
+            final long currentUptime = SystemClock.uptimeMillis();
+            if (currentUptime > (latestScrollingTime + DISABLE_PREFETCHING_FOR_SCROLLING_MILLIS)) {
+                sScrollingWindows.delete(windowId);
+                return false;
+            }
+        }
+        return true;
+    }
 }
diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java
index eb4f9db..9f83862 100644
--- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java
+++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java
@@ -138,6 +138,9 @@
     public static final int FLAG_PREFETCH_DESCENDANTS = 0x00000004;
 
     /** @hide */
+    public static final int FLAG_PREFETCH_MASK = 0x00000007;
+
+    /** @hide */
     public static final int FLAG_INCLUDE_NOT_IMPORTANT_VIEWS = 0x00000008;
 
     /** @hide */