OSDN Git Service

Move A11y events throttling away from View(RootImpl)
authorEugene Susla <eugenesusla@google.com>
Thu, 1 Jun 2017 18:16:42 +0000 (11:16 -0700)
committerEugene Susla <eugenesusla@google.com>
Tue, 9 Jan 2018 21:28:59 +0000 (13:28 -0800)
..and also extract common code into a common superclass

This also preserves the order of the throttled events
(TYPE_VIEW_SCROLLED & TYPE_WINDOW_CONTENT_CHANGED) with regards to the rest
of events by flushing any pending throttled events immediately if another
event is requested to be sent.

Test: ensure no new a11y CTS failures
Change-Id: I948a16716521974393aaa1cf822d0a0324e9ce3a

16 files changed:
core/java/android/app/UiAutomation.java
core/java/android/view/View.java
core/java/android/view/ViewGroup.java
core/java/android/view/ViewRootImpl.java
core/java/android/view/accessibility/AccessibilityViewHierarchyState.java [new file with mode: 0644]
core/java/android/view/accessibility/SendViewScrolledAccessibilityEvent.java [new file with mode: 0644]
core/java/android/view/accessibility/SendWindowContentChangedAccessibilityEvent.java [new file with mode: 0644]
core/java/android/view/accessibility/ThrottlingAccessibilityEventSender.java [new file with mode: 0644]
core/java/android/widget/AbsListView.java
core/java/android/widget/AdapterView.java
core/java/android/widget/CheckedTextView.java
core/java/android/widget/CompoundButton.java
core/java/android/widget/TextView.java
core/java/com/android/internal/util/ObjectUtils.java
core/java/com/android/internal/widget/ResolverDrawerLayout.java
packages/PrintSpooler/src/com/android/printspooler/widget/PrintContentView.java

index 8f01685..ba39740 100644 (file)
@@ -24,7 +24,6 @@ import android.accessibilityservice.IAccessibilityServiceConnection;
 import android.annotation.NonNull;
 import android.annotation.TestApi;
 import android.graphics.Bitmap;
-import android.graphics.Canvas;
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.graphics.Region;
@@ -47,10 +46,14 @@ import android.view.accessibility.AccessibilityInteractionClient;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.view.accessibility.AccessibilityWindowInfo;
 import android.view.accessibility.IAccessibilityInteractionConnection;
+
+import com.android.internal.util.CollectionUtils;
+
 import libcore.io.IoUtils;
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.TimeoutException;
 
@@ -580,6 +583,8 @@ public final class UiAutomation {
         // Execute the command *without* the lock being held.
         command.run();
 
+        List<AccessibilityEvent> eventsReceived = Collections.emptyList();
+
         // Acquire the lock and wait for the event.
         try {
             // Wait for the event.
@@ -600,14 +605,14 @@ public final class UiAutomation {
                     if (filter.accept(event)) {
                         return event;
                     }
-                    event.recycle();
+                    eventsReceived = CollectionUtils.add(eventsReceived, event);
                 }
                 // Check if timed out and if not wait.
                 final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis;
                 final long remainingTimeMillis = timeoutMillis - elapsedTimeMillis;
                 if (remainingTimeMillis <= 0) {
                     throw new TimeoutException("Expected event not received within: "
-                            + timeoutMillis + " ms.");
+                            + timeoutMillis + " ms, among " + eventsReceived);
                 }
                 synchronized (mLock) {
                     if (mEventQueue.isEmpty()) {
@@ -620,6 +625,10 @@ public final class UiAutomation {
                 }
             }
         } finally {
+            for (int i = 0; i < CollectionUtils.size(eventsReceived); i++) {
+                AccessibilityEvent event = eventsReceived.get(i);
+                event.recycle();
+            }
             synchronized (mLock) {
                 mWaitingForEventDelivery = false;
                 mEventQueue.clear();
index cc63a62..c02688e 100644 (file)
@@ -4407,7 +4407,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
     private CheckForLongPress mPendingCheckForLongPress;
     private CheckForTap mPendingCheckForTap = null;
     private PerformClick mPerformClick;
-    private SendViewScrolledAccessibilityEvent mSendViewScrolledAccessibilityEvent;
 
     private UnsetPressedState mUnsetPressedState;
 
@@ -7156,7 +7155,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
         if (gainFocus) {
             sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
         } else {
-            notifyViewAccessibilityStateChangedIfNeeded(
+            notifyAccessibilityStateChanged(
                     AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
         }
 
@@ -8826,9 +8825,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
         final boolean nonEmptyDesc = contentDescription != null && contentDescription.length() > 0;
         if (nonEmptyDesc && getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
             setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
-            notifySubtreeAccessibilityStateChangedIfNeeded();
+            notifyAccessibilitySubtreeChanged();
         } else {
-            notifyViewAccessibilityStateChangedIfNeeded(
+            notifyAccessibilityStateChanged(
                     AccessibilityEvent.CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION);
         }
     }
@@ -8861,7 +8860,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
             return;
         }
         mAccessibilityTraversalBeforeId = beforeId;
-        notifyViewAccessibilityStateChangedIfNeeded(
+        notifyAccessibilityStateChanged(
                 AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
     }
 
@@ -8905,7 +8904,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
             return;
         }
         mAccessibilityTraversalAfterId = afterId;
-        notifyViewAccessibilityStateChangedIfNeeded(
+        notifyAccessibilityStateChanged(
                 AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
     }
 
@@ -8948,7 +8947,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
                 && mID == View.NO_ID) {
             mID = generateViewId();
         }
-        notifyViewAccessibilityStateChangedIfNeeded(
+        notifyAccessibilityStateChanged(
                 AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
     }
 
@@ -10442,8 +10441,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
 
         if (pflags3 != mPrivateFlags3) {
             mPrivateFlags3 = pflags3;
-            notifyViewAccessibilityStateChangedIfNeeded(
-                    AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+            notifyAccessibilityStateChanged(AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
         }
     }
 
@@ -11261,7 +11259,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
             mPrivateFlags2 &= ~PFLAG2_ACCESSIBILITY_LIVE_REGION_MASK;
             mPrivateFlags2 |= (mode << PFLAG2_ACCESSIBILITY_LIVE_REGION_SHIFT)
                     & PFLAG2_ACCESSIBILITY_LIVE_REGION_MASK;
-            notifyViewAccessibilityStateChangedIfNeeded(
+            notifyAccessibilityStateChanged(
                     AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
         }
     }
@@ -11319,9 +11317,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
             mPrivateFlags2 |= (mode << PFLAG2_IMPORTANT_FOR_ACCESSIBILITY_SHIFT)
                     & PFLAG2_IMPORTANT_FOR_ACCESSIBILITY_MASK;
             if (!maySkipNotify || oldIncludeForAccessibility != includeForAccessibility()) {
-                notifySubtreeAccessibilityStateChangedIfNeeded();
+                notifyAccessibilitySubtreeChanged();
             } else {
-                notifyViewAccessibilityStateChangedIfNeeded(
+                notifyAccessibilityStateChanged(
                         AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
             }
         }
@@ -11496,25 +11494,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
      *
      * @hide
      */
-    public void notifyViewAccessibilityStateChangedIfNeeded(int changeType) {
-        if (!AccessibilityManager.getInstance(mContext).isEnabled() || mAttachInfo == null) {
-            return;
-        }
-        // If this is a live region, we should send a subtree change event
-        // from this view immediately. Otherwise, we can let it propagate up.
-        if (getAccessibilityLiveRegion() != ACCESSIBILITY_LIVE_REGION_NONE) {
-            final AccessibilityEvent event = AccessibilityEvent.obtain();
-            event.setEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
-            event.setContentChangeTypes(changeType);
-            sendAccessibilityEventUnchecked(event);
-        } else if (mParent != null) {
-            try {
-                mParent.notifySubtreeAccessibilityStateChanged(this, this, changeType);
-            } catch (AbstractMethodError e) {
-                Log.e(VIEW_LOG_TAG, mParent.getClass().getSimpleName() +
-                        " does not fully implement ViewParent", e);
-            }
-        }
+    public void notifyAccessibilityStateChanged(int changeType) {
+        notifyAccessibilityStateChanged(this, changeType);
     }
 
     /**
@@ -11528,20 +11509,23 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
      *
      * @hide
      */
-    public void notifySubtreeAccessibilityStateChangedIfNeeded() {
+    public void notifyAccessibilitySubtreeChanged() {
+        if ((mPrivateFlags2 & PFLAG2_SUBTREE_ACCESSIBILITY_STATE_CHANGED) == 0) {
+            mPrivateFlags2 |= PFLAG2_SUBTREE_ACCESSIBILITY_STATE_CHANGED;
+            notifyAccessibilityStateChanged(AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
+        }
+    }
+
+    void notifyAccessibilityStateChanged(View source, int changeType) {
         if (!AccessibilityManager.getInstance(mContext).isEnabled() || mAttachInfo == null) {
             return;
         }
-        if ((mPrivateFlags2 & PFLAG2_SUBTREE_ACCESSIBILITY_STATE_CHANGED) == 0) {
-            mPrivateFlags2 |= PFLAG2_SUBTREE_ACCESSIBILITY_STATE_CHANGED;
-            if (mParent != null) {
-                try {
-                    mParent.notifySubtreeAccessibilityStateChanged(
-                            this, this, AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
-                } catch (AbstractMethodError e) {
-                    Log.e(VIEW_LOG_TAG, mParent.getClass().getSimpleName() +
-                            " does not fully implement ViewParent", e);
-                }
+        if (mParent != null) {
+            try {
+                mParent.notifySubtreeAccessibilityStateChanged(this, source, changeType);
+            } catch (AbstractMethodError e) {
+                Log.e(VIEW_LOG_TAG, mParent.getClass().getSimpleName()
+                        + " does not fully implement ViewParent", e);
             }
         }
     }
@@ -11563,8 +11547,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
     /**
      * Reset the flag indicating the accessibility state of the subtree rooted
      * at this view changed.
+     *
+     * @hide
      */
-    void resetSubtreeAccessibilityStateChanged() {
+    public void resetSubtreeAccessibilityStateChanged() {
         mPrivateFlags2 &= ~PFLAG2_SUBTREE_ACCESSIBILITY_STATE_CHANGED;
     }
 
@@ -11725,7 +11711,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
                         || getAccessibilitySelectionEnd() != end)
                         && (start == end)) {
                     setAccessibilitySelection(start, end);
-                    notifyViewAccessibilityStateChangedIfNeeded(
+                    notifyAccessibilityStateChanged(
                             AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
                     return true;
                 }
@@ -13713,7 +13699,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
                         ((!(mParent instanceof ViewGroup)) || ((ViewGroup) mParent).isShown())) {
                     dispatchVisibilityAggregated(newVisibility == VISIBLE);
                 }
-                notifySubtreeAccessibilityStateChangedIfNeeded();
+                notifyAccessibilitySubtreeChanged();
             }
         }
 
@@ -13759,13 +13745,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
                     || (changed & CLICKABLE) != 0 || (changed & LONG_CLICKABLE) != 0
                     || (changed & CONTEXT_CLICKABLE) != 0) {
                 if (oldIncludeForAccessibility != includeForAccessibility()) {
-                    notifySubtreeAccessibilityStateChangedIfNeeded();
+                    notifyAccessibilitySubtreeChanged();
                 } else {
-                    notifyViewAccessibilityStateChangedIfNeeded(
+                    notifyAccessibilityStateChanged(
                             AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
                 }
             } else if ((changed & ENABLED_MASK) != 0) {
-                notifyViewAccessibilityStateChangedIfNeeded(
+                notifyAccessibilityStateChanged(
                         AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
             }
         }
@@ -13800,10 +13786,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
      * @param oldt Previous vertical scroll origin.
      */
     protected void onScrollChanged(int l, int t, int oldl, int oldt) {
-        notifySubtreeAccessibilityStateChangedIfNeeded();
+        notifyAccessibilitySubtreeChanged();
 
-        if (AccessibilityManager.getInstance(mContext).isEnabled()) {
-            postSendViewScrolledAccessibilityEventCallback(l - oldl, t - oldt);
+        ViewRootImpl root = getViewRootImpl();
+        if (root != null) {
+            root.getAccessibilityState()
+                    .getSendViewScrolledAccessibilityEvent()
+                    .post(this, /* dx */ l - oldl, /* dy */ t - oldt);
         }
 
         mBackgroundSizeChanged = true;
@@ -14199,7 +14188,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
             invalidateViewProperty(false, true);
 
             invalidateParentIfNeededAndWasQuickRejected();
-            notifySubtreeAccessibilityStateChangedIfNeeded();
+            notifyAccessibilitySubtreeChanged();
         }
     }
 
@@ -14243,7 +14232,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
             invalidateViewProperty(false, true);
 
             invalidateParentIfNeededAndWasQuickRejected();
-            notifySubtreeAccessibilityStateChangedIfNeeded();
+            notifyAccessibilitySubtreeChanged();
         }
     }
 
@@ -14287,7 +14276,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
             invalidateViewProperty(false, true);
 
             invalidateParentIfNeededAndWasQuickRejected();
-            notifySubtreeAccessibilityStateChangedIfNeeded();
+            notifyAccessibilitySubtreeChanged();
         }
     }
 
@@ -14324,7 +14313,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
             invalidateViewProperty(false, true);
 
             invalidateParentIfNeededAndWasQuickRejected();
-            notifySubtreeAccessibilityStateChangedIfNeeded();
+            notifyAccessibilitySubtreeChanged();
         }
     }
 
@@ -14361,7 +14350,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
             invalidateViewProperty(false, true);
 
             invalidateParentIfNeededAndWasQuickRejected();
-            notifySubtreeAccessibilityStateChangedIfNeeded();
+            notifyAccessibilitySubtreeChanged();
         }
     }
 
@@ -14564,7 +14553,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
         if (mTransformationInfo.mAlpha != alpha) {
             // Report visibility changes, which can affect children, to accessibility
             if ((alpha == 0) ^ (mTransformationInfo.mAlpha == 0)) {
-                notifySubtreeAccessibilityStateChangedIfNeeded();
+                notifyAccessibilitySubtreeChanged();
             }
             mTransformationInfo.mAlpha = alpha;
             if (onSetAlpha((int) (alpha * 255))) {
@@ -15066,7 +15055,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
             invalidateViewProperty(false, true);
 
             invalidateParentIfNeededAndWasQuickRejected();
-            notifySubtreeAccessibilityStateChangedIfNeeded();
+            notifyAccessibilitySubtreeChanged();
         }
     }
 
@@ -15100,7 +15089,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
             invalidateViewProperty(false, true);
 
             invalidateParentIfNeededAndWasQuickRejected();
-            notifySubtreeAccessibilityStateChangedIfNeeded();
+            notifyAccessibilitySubtreeChanged();
         }
     }
 
@@ -15270,7 +15259,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
     public void invalidateOutline() {
         rebuildOutline();
 
-        notifySubtreeAccessibilityStateChangedIfNeeded();
+        notifyAccessibilitySubtreeChanged();
         invalidateViewProperty(false, false);
     }
 
@@ -15465,7 +15454,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
                 }
                 invalidateParentIfNeeded();
             }
-            notifySubtreeAccessibilityStateChangedIfNeeded();
+            notifyAccessibilitySubtreeChanged();
         }
     }
 
@@ -15513,7 +15502,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
                 }
                 invalidateParentIfNeeded();
             }
-            notifySubtreeAccessibilityStateChangedIfNeeded();
+            notifyAccessibilitySubtreeChanged();
         }
     }
 
@@ -16391,18 +16380,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
     }
 
     /**
-     * Post a callback to send a {@link AccessibilityEvent#TYPE_VIEW_SCROLLED} event.
-     * This event is sent at most once every
-     * {@link ViewConfiguration#getSendRecurringAccessibilityEventsInterval()}.
-     */
-    private void postSendViewScrolledAccessibilityEventCallback(int dx, int dy) {
-        if (mSendViewScrolledAccessibilityEvent == null) {
-            mSendViewScrolledAccessibilityEvent = new SendViewScrolledAccessibilityEvent();
-        }
-        mSendViewScrolledAccessibilityEvent.post(dx, dy);
-    }
-
-    /**
      * Called by a parent to request that a child update its values for mScrollX
      * and mScrollY if necessary. This will typically be done if the child is
      * animating a scroll using a {@link android.widget.Scroller Scroller}
@@ -17657,7 +17634,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
         removeUnsetPressCallback();
         removeLongPressCallback();
         removePerformClickCallback();
-        cancel(mSendViewScrolledAccessibilityEvent);
+        if (mAttachInfo != null
+                && mAttachInfo.mViewRootImpl.mAccessibilityState != null
+                && mAttachInfo.mViewRootImpl.mAccessibilityState.isScrollEventSenderInitialized()) {
+            mAttachInfo.mViewRootImpl.mAccessibilityState
+                    .getSendViewScrolledAccessibilityEvent()
+                    .cancelIfPendingFor(this);
+        }
         stopNestedScroll();
 
         // Anything that started animating right before detach should already
@@ -20256,7 +20239,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
                 mForegroundInfo.mBoundsChanged = true;
             }
 
-            notifySubtreeAccessibilityStateChangedIfNeeded();
+            notifyAccessibilitySubtreeChanged();
         }
         return changed;
     }
@@ -21683,7 +21666,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
             if (selected) {
                 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
             } else {
-                notifyViewAccessibilityStateChangedIfNeeded(
+                notifyAccessibilityStateChanged(
                         AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
             }
         }
@@ -26240,53 +26223,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
     }
 
     /**
-     * Resuable callback for sending
-     * {@link AccessibilityEvent#TYPE_VIEW_SCROLLED} accessibility event.
-     */
-    private class SendViewScrolledAccessibilityEvent implements Runnable {
-        public volatile boolean mIsPending;
-        public int mDeltaX;
-        public int mDeltaY;
-
-        public void post(int dx, int dy) {
-            mDeltaX += dx;
-            mDeltaY += dy;
-            if (!mIsPending) {
-                mIsPending = true;
-                postDelayed(this, ViewConfiguration.getSendRecurringAccessibilityEventsInterval());
-            }
-        }
-
-        @Override
-        public void run() {
-            if (AccessibilityManager.getInstance(mContext).isEnabled()) {
-                AccessibilityEvent event = AccessibilityEvent.obtain(
-                        AccessibilityEvent.TYPE_VIEW_SCROLLED);
-                event.setScrollDeltaX(mDeltaX);
-                event.setScrollDeltaY(mDeltaY);
-                sendAccessibilityEventUnchecked(event);
-            }
-            reset();
-        }
-
-        private void reset() {
-            mIsPending = false;
-            mDeltaX = 0;
-            mDeltaY = 0;
-        }
-    }
-
-    /**
-     * Remove the pending callback for sending a
-     * {@link AccessibilityEvent#TYPE_VIEW_SCROLLED} accessibility event.
-     */
-    private void cancel(@Nullable SendViewScrolledAccessibilityEvent callback) {
-        if (callback == null || !callback.mIsPending) return;
-        removeCallbacks(callback);
-        callback.reset();
-    }
-
-    /**
      * <p>
      * This class represents a delegate that can be registered in a {@link View}
      * to enhance accessibility support via composition rather via inheritance.
index 122df93..a19e1ab 100644 (file)
@@ -3637,44 +3637,34 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
         return ViewGroup.class.getName();
     }
 
-    @Override
-    public void notifySubtreeAccessibilityStateChanged(View child, View source, int changeType) {
-        // If this is a live region, we should send a subtree change event
-        // from this view. Otherwise, we can let it propagate up.
-        if (getAccessibilityLiveRegion() != ACCESSIBILITY_LIVE_REGION_NONE) {
-            notifyViewAccessibilityStateChangedIfNeeded(
-                    AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
-        } else if (mParent != null) {
-            try {
-                mParent.notifySubtreeAccessibilityStateChanged(this, source, changeType);
-            } catch (AbstractMethodError e) {
-                Log.e(VIEW_LOG_TAG, mParent.getClass().getSimpleName() +
-                        " does not fully implement ViewParent", e);
-            }
-        }
-    }
-
     /** @hide */
     @Override
-    public void notifySubtreeAccessibilityStateChangedIfNeeded() {
+    public void notifyAccessibilitySubtreeChanged() {
         if (!AccessibilityManager.getInstance(mContext).isEnabled() || mAttachInfo == null) {
             return;
         }
         // If something important for a11y is happening in this subtree, make sure it's dispatched
         // from a view that is important for a11y so it doesn't get lost.
-        if ((getImportantForAccessibility() != IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS)
-                && !isImportantForAccessibility() && (getChildCount() > 0)) {
+        if (getImportantForAccessibility() != IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+                && !isImportantForAccessibility()
+                && getChildCount() > 0) {
             ViewParent a11yParent = getParentForAccessibility();
             if (a11yParent instanceof View) {
-                ((View) a11yParent).notifySubtreeAccessibilityStateChangedIfNeeded();
+                ((View) a11yParent).notifyAccessibilitySubtreeChanged();
                 return;
             }
         }
-        super.notifySubtreeAccessibilityStateChangedIfNeeded();
+        super.notifyAccessibilitySubtreeChanged();
     }
 
     @Override
-    void resetSubtreeAccessibilityStateChanged() {
+    public void notifySubtreeAccessibilityStateChanged(View child, View source, int changeType) {
+        notifyAccessibilityStateChanged(source, changeType);
+    }
+
+    /** @hide */
+    @Override
+    public void resetSubtreeAccessibilityStateChanged() {
         super.resetSubtreeAccessibilityStateChanged();
         View[] children = mChildren;
         final int childCount = mChildrenCount;
@@ -5086,7 +5076,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
         }
 
         if (child.getVisibility() != View.GONE) {
-            notifySubtreeAccessibilityStateChangedIfNeeded();
+            notifyAccessibilitySubtreeChanged();
         }
 
         if (mTransientIndices != null) {
@@ -5356,7 +5346,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
         dispatchViewRemoved(view);
 
         if (view.getVisibility() != View.GONE) {
-            notifySubtreeAccessibilityStateChangedIfNeeded();
+            notifyAccessibilitySubtreeChanged();
         }
 
         int transientCount = mTransientIndices == null ? 0 : mTransientIndices.size();
@@ -6075,7 +6065,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
         if (invalidate) {
             invalidateViewProperty(false, false);
         }
-        notifySubtreeAccessibilityStateChangedIfNeeded();
+        notifyAccessibilitySubtreeChanged();
     }
 
     @Override
index 6c5091c..f81a4c3 100644 (file)
@@ -89,9 +89,11 @@ import android.view.accessibility.AccessibilityManager.HighTextContrastChangeLis
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
 import android.view.accessibility.AccessibilityNodeProvider;
+import android.view.accessibility.AccessibilityViewHierarchyState;
 import android.view.accessibility.AccessibilityWindowInfo;
 import android.view.accessibility.IAccessibilityInteractionConnection;
 import android.view.accessibility.IAccessibilityInteractionConnectionCallback;
+import android.view.accessibility.ThrottlingAccessibilityEventSender;
 import android.view.animation.AccelerateDecelerateInterpolator;
 import android.view.animation.Interpolator;
 import android.view.inputmethod.InputMethodManager;
@@ -113,7 +115,6 @@ import java.io.OutputStream;
 import java.io.PrintWriter;
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
-import java.util.HashSet;
 import java.util.concurrent.CountDownLatch;
 
 /**
@@ -460,10 +461,6 @@ public final class ViewRootImpl implements ViewParent,
             new AccessibilityInteractionConnectionManager();
     final HighContrastTextManager mHighContrastTextManager;
 
-    SendWindowContentChangedAccessibilityEvent mSendWindowContentChangedAccessibilityEvent;
-
-    HashSet<View> mTempHashSet;
-
     private final int mDensity;
     private final int mNoncompatDensity;
 
@@ -478,6 +475,8 @@ public final class ViewRootImpl implements ViewParent,
 
     private boolean mNeedsRendererSetup;
 
+    protected AccessibilityViewHierarchyState mAccessibilityState;
+
     /**
      * Consistency verifier for debugging purposes.
      */
@@ -7262,11 +7261,9 @@ public final class ViewRootImpl implements ViewParent,
      * {@link ViewConfiguration#getSendRecurringAccessibilityEventsInterval()}.
      */
     private void postSendWindowContentChangedCallback(View source, int changeType) {
-        if (mSendWindowContentChangedAccessibilityEvent == null) {
-            mSendWindowContentChangedAccessibilityEvent =
-                new SendWindowContentChangedAccessibilityEvent();
-        }
-        mSendWindowContentChangedAccessibilityEvent.runOrPost(source, changeType);
+        getAccessibilityState()
+                .getSendWindowContentChangedAccessibilityEvent()
+                .runOrPost(source, changeType);
     }
 
     /**
@@ -7274,9 +7271,18 @@ public final class ViewRootImpl implements ViewParent,
      * {@link AccessibilityEvent#TYPE_WINDOW_CONTENT_CHANGED} event.
      */
     private void removeSendWindowContentChangedCallback() {
-        if (mSendWindowContentChangedAccessibilityEvent != null) {
-            mHandler.removeCallbacks(mSendWindowContentChangedAccessibilityEvent);
+        if (mAccessibilityState != null
+                && mAccessibilityState.isWindowContentChangedEventSenderInitialized()) {
+            ThrottlingAccessibilityEventSender.cancelIfPending(
+                    mAccessibilityState.getSendWindowContentChangedAccessibilityEvent());
+        }
+    }
+
+    AccessibilityViewHierarchyState getAccessibilityState() {
+        if (mAccessibilityState == null) {
+            mAccessibilityState = new AccessibilityViewHierarchyState();
         }
+        return mAccessibilityState;
     }
 
     @Override
@@ -7314,12 +7320,8 @@ public final class ViewRootImpl implements ViewParent,
             return false;
         }
 
-        // Immediately flush pending content changed event (if any) to preserve event order
-        if (event.getEventType() != AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
-                && mSendWindowContentChangedAccessibilityEvent != null
-                && mSendWindowContentChangedAccessibilityEvent.mSource != null) {
-            mSendWindowContentChangedAccessibilityEvent.removeCallbacksAndRun();
-        }
+        // Send any pending event to prevent reordering
+        flushPendingAccessibilityEvents();
 
         // Intercept accessibility focus events fired by virtual nodes to keep
         // track of accessibility focus position in such nodes.
@@ -7363,6 +7365,19 @@ public final class ViewRootImpl implements ViewParent,
         return true;
     }
 
+    /** @hide */
+    public void flushPendingAccessibilityEvents() {
+        if (mAccessibilityState != null) {
+            if (mAccessibilityState.isScrollEventSenderInitialized()) {
+                mAccessibilityState.getSendViewScrolledAccessibilityEvent().sendNowIfPending();
+            }
+            if (mAccessibilityState.isWindowContentChangedEventSenderInitialized()) {
+                mAccessibilityState.getSendWindowContentChangedAccessibilityEvent()
+                        .sendNowIfPending();
+            }
+        }
+    }
+
     /**
      * Updates the focused virtual view, when necessary, in response to a
      * content changed event.
@@ -7497,39 +7512,6 @@ public final class ViewRootImpl implements ViewParent,
         return View.TEXT_ALIGNMENT_RESOLVED_DEFAULT;
     }
 
-    private View getCommonPredecessor(View first, View second) {
-        if (mTempHashSet == null) {
-            mTempHashSet = new HashSet<View>();
-        }
-        HashSet<View> seen = mTempHashSet;
-        seen.clear();
-        View firstCurrent = first;
-        while (firstCurrent != null) {
-            seen.add(firstCurrent);
-            ViewParent firstCurrentParent = firstCurrent.mParent;
-            if (firstCurrentParent instanceof View) {
-                firstCurrent = (View) firstCurrentParent;
-            } else {
-                firstCurrent = null;
-            }
-        }
-        View secondCurrent = second;
-        while (secondCurrent != null) {
-            if (seen.contains(secondCurrent)) {
-                seen.clear();
-                return secondCurrent;
-            }
-            ViewParent secondCurrentParent = secondCurrent.mParent;
-            if (secondCurrentParent instanceof View) {
-                secondCurrent = (View) secondCurrentParent;
-            } else {
-                secondCurrent = null;
-            }
-        }
-        seen.clear();
-        return null;
-    }
-
     void checkThread() {
         if (mThread != Thread.currentThread()) {
             throw new CalledFromWrongThreadException(
@@ -8140,80 +8122,6 @@ public final class ViewRootImpl implements ViewParent,
         }
     }
 
-    private class SendWindowContentChangedAccessibilityEvent implements Runnable {
-        private int mChangeTypes = 0;
-
-        public View mSource;
-        public long mLastEventTimeMillis;
-
-        @Override
-        public void run() {
-            // Protect against re-entrant code and attempt to do the right thing in the case that
-            // we're multithreaded.
-            View source = mSource;
-            mSource = null;
-            if (source == null) {
-                Log.e(TAG, "Accessibility content change has no source");
-                return;
-            }
-            // The accessibility may be turned off while we were waiting so check again.
-            if (AccessibilityManager.getInstance(mContext).isEnabled()) {
-                mLastEventTimeMillis = SystemClock.uptimeMillis();
-                AccessibilityEvent event = AccessibilityEvent.obtain();
-                event.setEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
-                event.setContentChangeTypes(mChangeTypes);
-                source.sendAccessibilityEventUnchecked(event);
-            } else {
-                mLastEventTimeMillis = 0;
-            }
-            // In any case reset to initial state.
-            source.resetSubtreeAccessibilityStateChanged();
-            mChangeTypes = 0;
-        }
-
-        public void runOrPost(View source, int changeType) {
-            if (mHandler.getLooper() != Looper.myLooper()) {
-                CalledFromWrongThreadException e = new CalledFromWrongThreadException("Only the "
-                        + "original thread that created a view hierarchy can touch its views.");
-                // TODO: Throw the exception
-                Log.e(TAG, "Accessibility content change on non-UI thread. Future Android "
-                        + "versions will throw an exception.", e);
-                // Attempt to recover. This code does not eliminate the thread safety issue, but
-                // it should force any issues to happen near the above log.
-                mHandler.removeCallbacks(this);
-                if (mSource != null) {
-                    // Dispatch whatever was pending. It's still possible that the runnable started
-                    // just before we removed the callbacks, and bad things will happen, but at
-                    // least they should happen very close to the logged error.
-                    run();
-                }
-            }
-            if (mSource != null) {
-                // If there is no common predecessor, then mSource points to
-                // a removed view, hence in this case always prefer the source.
-                View predecessor = getCommonPredecessor(mSource, source);
-                mSource = (predecessor != null) ? predecessor : source;
-                mChangeTypes |= changeType;
-                return;
-            }
-            mSource = source;
-            mChangeTypes = changeType;
-            final long timeSinceLastMillis = SystemClock.uptimeMillis() - mLastEventTimeMillis;
-            final long minEventIntevalMillis =
-                    ViewConfiguration.getSendRecurringAccessibilityEventsInterval();
-            if (timeSinceLastMillis >= minEventIntevalMillis) {
-                removeCallbacksAndRun();
-            } else {
-                mHandler.postDelayed(this, minEventIntevalMillis - timeSinceLastMillis);
-            }
-        }
-
-        public void removeCallbacksAndRun() {
-            mHandler.removeCallbacks(this);
-            run();
-        }
-    }
-
     private static class KeyFallbackManager {
 
         // This is used to ensure that key-fallback events are only dispatched once. We attempt
diff --git a/core/java/android/view/accessibility/AccessibilityViewHierarchyState.java b/core/java/android/view/accessibility/AccessibilityViewHierarchyState.java
new file mode 100644 (file)
index 0000000..447fafa
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2017 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 android.view.accessibility;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+/**
+ * Accessibility-related state of a {@link android.view.ViewRootImpl}
+ *
+ * @hide
+ */
+public class AccessibilityViewHierarchyState {
+    private @Nullable SendViewScrolledAccessibilityEvent mSendViewScrolledAccessibilityEvent;
+    private @Nullable SendWindowContentChangedAccessibilityEvent
+            mSendWindowContentChangedAccessibilityEvent;
+
+    /**
+     * @return a {@link SendViewScrolledAccessibilityEvent}, creating one if needed
+     */
+    public @NonNull SendViewScrolledAccessibilityEvent getSendViewScrolledAccessibilityEvent() {
+        if (mSendViewScrolledAccessibilityEvent == null) {
+            mSendViewScrolledAccessibilityEvent = new SendViewScrolledAccessibilityEvent();
+        }
+        return mSendViewScrolledAccessibilityEvent;
+    }
+
+    public boolean isScrollEventSenderInitialized() {
+        return mSendViewScrolledAccessibilityEvent != null;
+    }
+
+    /**
+     * @return a {@link SendWindowContentChangedAccessibilityEvent}, creating one if needed
+     */
+    public @NonNull SendWindowContentChangedAccessibilityEvent
+            getSendWindowContentChangedAccessibilityEvent() {
+        if (mSendWindowContentChangedAccessibilityEvent == null) {
+            mSendWindowContentChangedAccessibilityEvent =
+                    new SendWindowContentChangedAccessibilityEvent();
+        }
+        return mSendWindowContentChangedAccessibilityEvent;
+    }
+
+    public boolean isWindowContentChangedEventSenderInitialized() {
+        return mSendWindowContentChangedAccessibilityEvent != null;
+    }
+}
diff --git a/core/java/android/view/accessibility/SendViewScrolledAccessibilityEvent.java b/core/java/android/view/accessibility/SendViewScrolledAccessibilityEvent.java
new file mode 100644 (file)
index 0000000..40a1b6a
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2017 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 android.view.accessibility;
+
+
+import android.annotation.NonNull;
+import android.view.View;
+
+/**
+ * Sender for {@link AccessibilityEvent#TYPE_VIEW_SCROLLED} accessibility event.
+ *
+ * @hide
+ */
+public class SendViewScrolledAccessibilityEvent extends ThrottlingAccessibilityEventSender {
+
+    public int mDeltaX;
+    public int mDeltaY;
+
+    /**
+     * Post a scroll event to be sent for the given view
+     */
+    public void post(View source, int dx, int dy) {
+        if (!isPendingFor(source)) sendNowIfPending();
+
+        mDeltaX += dx;
+        mDeltaY += dy;
+
+        if (!isPendingFor(source)) scheduleFor(source);
+    }
+
+    @Override
+    protected void performSendEvent(@NonNull View source) {
+        AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_SCROLLED);
+        event.setScrollDeltaX(mDeltaX);
+        event.setScrollDeltaY(mDeltaY);
+        source.sendAccessibilityEventUnchecked(event);
+    }
+
+    @Override
+    protected void resetState(@NonNull View source) {
+        mDeltaX = 0;
+        mDeltaY = 0;
+    }
+}
diff --git a/core/java/android/view/accessibility/SendWindowContentChangedAccessibilityEvent.java b/core/java/android/view/accessibility/SendWindowContentChangedAccessibilityEvent.java
new file mode 100644 (file)
index 0000000..df38fba
--- /dev/null
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2017 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 android.view.accessibility;
+
+
+import static com.android.internal.util.ObjectUtils.firstNotNull;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.view.View;
+import android.view.ViewParent;
+
+import java.util.HashSet;
+
+/**
+ * @hide
+ */
+public class SendWindowContentChangedAccessibilityEvent
+        extends ThrottlingAccessibilityEventSender {
+
+    private int mChangeTypes = 0;
+
+    private HashSet<View> mTempHashSet;
+
+    @Override
+    protected void performSendEvent(@NonNull View source) {
+        AccessibilityEvent event = AccessibilityEvent.obtain();
+        event.setEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
+        event.setContentChangeTypes(mChangeTypes);
+        source.sendAccessibilityEventUnchecked(event);
+    }
+
+    @Override
+    protected void resetState(@Nullable View source) {
+        if (source != null) {
+            source.resetSubtreeAccessibilityStateChanged();
+        }
+        mChangeTypes = 0;
+    }
+
+    /**
+     * Post the {@link AccessibilityEvent#TYPE_WINDOW_CONTENT_CHANGED} event with the given
+     * {@link AccessibilityEvent#getContentChangeTypes change type} for the given view
+     */
+    public void runOrPost(View source, int changeType) {
+        if (source.getAccessibilityLiveRegion() != View.ACCESSIBILITY_LIVE_REGION_NONE) {
+            sendNowIfPending();
+            mChangeTypes = changeType;
+            sendNow(source);
+        } else {
+            mChangeTypes |= changeType;
+            scheduleFor(source);
+        }
+    }
+
+    @Override
+    protected @Nullable View tryMerge(@NonNull View oldSource, @NonNull View newSource) {
+        // If there is no common predecessor, then oldSource points to
+        // a removed view, hence in this case always prefer the newSource.
+        return firstNotNull(
+                getCommonPredecessor(oldSource, newSource),
+                newSource);
+    }
+
+    private View getCommonPredecessor(View first, View second) {
+        if (mTempHashSet == null) {
+            mTempHashSet = new HashSet<>();
+        }
+        HashSet<View> seen = mTempHashSet;
+        seen.clear();
+        View firstCurrent = first;
+        while (firstCurrent != null) {
+            seen.add(firstCurrent);
+            ViewParent firstCurrentParent = firstCurrent.getParent();
+            if (firstCurrentParent instanceof View) {
+                firstCurrent = (View) firstCurrentParent;
+            } else {
+                firstCurrent = null;
+            }
+        }
+        View secondCurrent = second;
+        while (secondCurrent != null) {
+            if (seen.contains(secondCurrent)) {
+                seen.clear();
+                return secondCurrent;
+            }
+            ViewParent secondCurrentParent = secondCurrent.getParent();
+            if (secondCurrentParent instanceof View) {
+                secondCurrent = (View) secondCurrentParent;
+            } else {
+                secondCurrent = null;
+            }
+        }
+        seen.clear();
+        return null;
+    }
+}
diff --git a/core/java/android/view/accessibility/ThrottlingAccessibilityEventSender.java b/core/java/android/view/accessibility/ThrottlingAccessibilityEventSender.java
new file mode 100644 (file)
index 0000000..66fa301
--- /dev/null
@@ -0,0 +1,248 @@
+/*
+ * Copyright (C) 2017 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 android.view.accessibility;
+
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewRootImpl;
+import android.view.ViewRootImpl.CalledFromWrongThreadException;
+
+/**
+ * A throttling {@link AccessibilityEvent} sender that relies on its currently associated
+ * 'source' view's {@link View#postDelayed delayed execution} to delay and possibly
+ * {@link #tryMerge merge} together any events that come in less than
+ * {@link ViewConfiguration#getSendRecurringAccessibilityEventsInterval
+ * the configured amount of milliseconds} apart.
+ *
+ * The suggested usage is to create a singleton extending this class, holding any state specific to
+ * the particular event type that the subclass represents, and have an 'entrypoint' method that
+ * delegates to {@link #scheduleFor(View)}.
+ * For example:
+ *
+ * {@code
+ *     public void post(View view, String text, int resId) {
+ *         mText = text;
+ *         mId = resId;
+ *         scheduleFor(view);
+ *     }
+ * }
+ *
+ * @see #scheduleFor(View)
+ * @see #tryMerge(View, View)
+ * @see #performSendEvent(View)
+ * @hide
+ */
+public abstract class ThrottlingAccessibilityEventSender {
+
+    private static final boolean DEBUG = false;
+    private static final String LOG_TAG = "ThrottlingA11ySender";
+
+    View mSource;
+    private long mLastSendTimeMillis = Long.MIN_VALUE;
+    private boolean mIsPending = false;
+
+    private final Runnable mWorker = () -> {
+        View source = mSource;
+        if (DEBUG) Log.d(LOG_TAG, thisClass() + ".run(mSource = " + source + ")");
+
+        if (!checkAndResetIsPending() || source == null) {
+            resetStateInternal();
+            return;
+        }
+
+        // Accessibility may be turned off while we were waiting
+        if (isAccessibilityEnabled(source)) {
+            mLastSendTimeMillis = SystemClock.uptimeMillis();
+            performSendEvent(source);
+        }
+        resetStateInternal();
+    };
+
+    /**
+     * Populate and send an {@link AccessibilityEvent} using the given {@code source} view, as well
+     * as any extra data from this instance's state.
+     *
+     * Send the event via {@link View#sendAccessibilityEventUnchecked(AccessibilityEvent)} or
+     * {@link View#sendAccessibilityEvent(int)} on the provided {@code source} view to allow for
+     * overrides of those methods on {@link View} subclasses to take effect, and/or make sure that
+     * an {@link View#getAccessibilityDelegate() accessibility delegate} is not ignored if any.
+     */
+    protected abstract void performSendEvent(@NonNull View source);
+
+    /**
+     * Perform optional cleanup after {@link #performSendEvent}
+     *
+     * @param source the view this event was associated with
+     */
+    protected abstract void resetState(@Nullable View source);
+
+    /**
+     * Attempt to merge the pending events for source views {@code oldSource} and {@code newSource}
+     * into one, with source set to the resulting {@link View}
+     *
+     * A result of {@code null} means merger is not possible, resulting in the currently pending
+     * event being flushed before proceeding.
+     */
+    protected @Nullable View tryMerge(@NonNull View oldSource, @NonNull View newSource) {
+        return null;
+    }
+
+    /**
+     * Schedules a {@link #performSendEvent} with the source {@link View} set to given
+     * {@code source}
+     *
+     * If an event is already scheduled a {@link #tryMerge merge} will be attempted.
+     * If merging is not possible (as indicated by the null result from {@link #tryMerge}),
+     * the currently scheduled event will be {@link #sendNow sent immediately} and the new one
+     * will be scheduled afterwards.
+     */
+    protected final void scheduleFor(@NonNull View source) {
+        if (DEBUG) Log.d(LOG_TAG, thisClass() + ".scheduleFor(source = " + source + ")");
+
+        Handler uiHandler = source.getHandler();
+        if (uiHandler == null || uiHandler.getLooper() != Looper.myLooper()) {
+            CalledFromWrongThreadException e = new CalledFromWrongThreadException(
+                    "Expected to be called from main thread but was called from "
+                            + Thread.currentThread());
+            // TODO: Throw the exception
+            Log.e(LOG_TAG, "Accessibility content change on non-UI thread. Future Android "
+                    + "versions will throw an exception.", e);
+        }
+
+        if (!isAccessibilityEnabled(source)) return;
+
+        if (mIsPending) {
+            View merged = tryMerge(mSource, source);
+            if (merged != null) {
+                setSource(merged);
+                return;
+            } else {
+                sendNow();
+            }
+        }
+
+        setSource(source);
+
+        final long timeSinceLastMillis = SystemClock.uptimeMillis() - mLastSendTimeMillis;
+        final long minEventIntervalMillis =
+                ViewConfiguration.getSendRecurringAccessibilityEventsInterval();
+        if (timeSinceLastMillis >= minEventIntervalMillis) {
+            sendNow();
+        } else {
+            mSource.postDelayed(mWorker, minEventIntervalMillis - timeSinceLastMillis);
+        }
+    }
+
+    static boolean isAccessibilityEnabled(@NonNull View contextProvider) {
+        return AccessibilityManager.getInstance(contextProvider.getContext()).isEnabled();
+    }
+
+    protected final void sendNow(View source) {
+        setSource(source);
+        sendNow();
+    }
+
+    private void sendNow() {
+        mSource.removeCallbacks(mWorker);
+        mWorker.run();
+    }
+
+    /**
+     * Flush the event if one is pending
+     */
+    public void sendNowIfPending() {
+        if (mIsPending) sendNow();
+    }
+
+    /**
+     * Cancel the event if one is pending and is for the given view
+     */
+    public final void cancelIfPendingFor(@NonNull View source) {
+        if (isPendingFor(source)) cancelIfPending(this);
+    }
+
+    /**
+     * @return whether an event is currently pending for the given source view
+     */
+    protected final boolean isPendingFor(@Nullable View source) {
+        return mIsPending && mSource == source;
+    }
+
+    /**
+     * Cancel the event if one is not null and pending
+     */
+    public static void cancelIfPending(@Nullable ThrottlingAccessibilityEventSender sender) {
+        if (sender == null || !sender.checkAndResetIsPending()) return;
+        sender.mSource.removeCallbacks(sender.mWorker);
+        sender.resetStateInternal();
+    }
+
+    void resetStateInternal() {
+        if (DEBUG) Log.d(LOG_TAG, thisClass() + ".resetStateInternal()");
+
+        resetState(mSource);
+        setSource(null);
+    }
+
+    boolean checkAndResetIsPending() {
+        if (mIsPending) {
+            mIsPending = false;
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    private void setSource(@Nullable View source) {
+        if (DEBUG) Log.d(LOG_TAG, thisClass() + ".setSource(" + source + ")");
+
+        if (source == null && mIsPending) {
+            Log.e(LOG_TAG, "mSource nullified while callback still pending: " + this);
+            return;
+        }
+
+        if (source != null && !mIsPending) {
+            // At most one can be pending at any given time
+            View oldSource = mSource;
+            if (oldSource != null) {
+                ViewRootImpl viewRootImpl = oldSource.getViewRootImpl();
+                if (viewRootImpl != null) {
+                    viewRootImpl.flushPendingAccessibilityEvents();
+                }
+            }
+            mIsPending = true;
+        }
+        mSource = source;
+    }
+
+    String thisClass() {
+        return getClass().getSimpleName();
+    }
+
+    @Override
+    public String toString() {
+        return thisClass() + "(" + mSource + ")";
+    }
+
+}
index e0c897d..6bee58f 100644 (file)
@@ -6849,7 +6849,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
             // detached and we do not allow detached views to fire accessibility
             // events. So we are announcing that the subtree changed giving a chance
             // to clients holding on to a view in this subtree to refresh it.
-            notifyViewAccessibilityStateChangedIfNeeded(
+            notifyAccessibilityStateChanged(
                     AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
 
             // Don't scrap views that have transient state.
index 6c19256..08374cb 100644 (file)
@@ -1093,7 +1093,7 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup {
             checkSelectionChanged();
         }
 
-        notifySubtreeAccessibilityStateChangedIfNeeded();
+        notifyAccessibilitySubtreeChanged();
     }
 
     /**
index 92bfd56..af01a3e 100644 (file)
@@ -132,7 +132,7 @@ public class CheckedTextView extends TextView implements Checkable {
         if (mChecked != checked) {
             mChecked = checked;
             refreshDrawableState();
-            notifyViewAccessibilityStateChangedIfNeeded(
+            notifyAccessibilityStateChanged(
                     AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
         }
     }
index 0762b15..e57f153 100644 (file)
@@ -158,7 +158,7 @@ public abstract class CompoundButton extends Button implements Checkable {
             mCheckedFromResource = false;
             mChecked = checked;
             refreshDrawableState();
-            notifyViewAccessibilityStateChangedIfNeeded(
+            notifyAccessibilityStateChanged(
                     AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
 
             // Avoid infinite recursions if setChecked() is called from a listener
index 1e17f34..821ae07 100644 (file)
@@ -2360,7 +2360,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
         setText(mText);
 
         if (hasPasswordTransformationMethod()) {
-            notifyViewAccessibilityStateChangedIfNeeded(
+            notifyAccessibilityStateChanged(
                     AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
         }
 
@@ -5419,7 +5419,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
         sendOnTextChanged(text, 0, oldlen, textLength);
         onTextChanged(text, 0, oldlen, textLength);
 
-        notifyViewAccessibilityStateChangedIfNeeded(AccessibilityEvent.CONTENT_CHANGE_TYPE_TEXT);
+        notifyAccessibilityStateChanged(AccessibilityEvent.CONTENT_CHANGE_TYPE_TEXT);
 
         if (needEditableForNotification) {
             sendAfterTextChanged((Editable) text);
@@ -6151,7 +6151,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
     public void setError(CharSequence error, Drawable icon) {
         createEditorIfNeeded();
         mEditor.setError(error, icon);
-        notifyViewAccessibilityStateChangedIfNeeded(
+        notifyAccessibilityStateChanged(
                 AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
     }
 
index 59e5a64..379602a 100644 (file)
@@ -29,6 +29,9 @@ public class ObjectUtils {
         return a != null ? a : Preconditions.checkNotNull(b);
     }
 
+    /**
+     * Compares two {@link Nullable} objects with {@code null} values considered the smallest
+     */
     public static <T extends Comparable> int compare(@Nullable T a, @Nullable T b) {
         if (a != null) {
             return (b != null) ? a.compareTo(b) : 1;
@@ -36,4 +39,13 @@ public class ObjectUtils {
             return (b != null) ? -1 : 0;
         }
     }
+
+    /**
+     * @return {@code null} if the given instance is not of the given calss, or the given
+     *         instance otherwise
+     */
+    @Nullable
+    public static <S, T extends S> T castOrNull(@Nullable S instance, @NonNull Class<T> c) {
+        return c.isInstance(instance) ? (T) instance : null;
+    }
 }
index 7635a72..6f2246a 100644 (file)
@@ -22,9 +22,7 @@ import com.android.internal.R;
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.graphics.Canvas;
-import android.graphics.Color;
 import android.graphics.Rect;
-import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
 import android.os.Bundle;
 import android.os.Parcel;
@@ -504,7 +502,7 @@ public class ResolverDrawerLayout extends ViewGroup {
     }
 
     private void onCollapsedChanged(boolean isCollapsed) {
-        notifyViewAccessibilityStateChangedIfNeeded(
+        notifyAccessibilityStateChanged(
                 AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
 
         if (mScrollIndicatorDrawable != null) {
index 8b00ed0..faa10cc 100644 (file)
@@ -23,6 +23,7 @@ import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.inputmethod.InputMethodManager;
+
 import com.android.printspooler.R;
 
 /**
@@ -410,7 +411,7 @@ public final class PrintContentView extends ViewGroup implements View.OnClickLis
 
             mPrintButton.offsetTopAndBottom(dy);
 
-            mDraggableContent.notifySubtreeAccessibilityStateChangedIfNeeded();
+            mDraggableContent.notifyAccessibilitySubtreeChanged();
 
             onDragProgress(progress);
         }