[android] Fire accessibility events when scrolling sublayers.

Cherry-pick http://crrev.com/48973004

> This enables the Android WebView to fire accessibility events for
> sub-layer scrolling. Currently this is only limited to touch-driven
> scrolling (JavaScript-initiated scrolling only works for the root
> layer at the moment).
>
> BUG=312318
> android-only change, trybots are happy
> NOTRY=true
>
> Committed: https://src.chromium.org/viewvc/chrome?view=rev&revision=232097

BUG: 10969501
Change-Id: I53fef2b655ff5e77e0924c4edc529e44f5b626ec
diff --git a/android_webview/java/src/org/chromium/android_webview/AwContents.java b/android_webview/java/src/org/chromium/android_webview/AwContents.java
index c429bc0..66348e3 100644
--- a/android_webview/java/src/org/chromium/android_webview/AwContents.java
+++ b/android_webview/java/src/org/chromium/android_webview/AwContents.java
@@ -162,6 +162,7 @@
     private OverScrollGlow mOverScrollGlow;
     // This can be accessed on any thread after construction. See AwContentsIoThreadClient.
     private final AwSettings mSettings;
+    private final ScrollAccessibilityHelper mScrollAccessibilityHelper;
 
     private boolean mIsPaused;
     private boolean mIsViewVisible;
@@ -387,7 +388,6 @@
             mScrollOffsetManager.onFlingStartGesture(velocityX, velocityY);
         }
 
-
         @Override
         public void onFlingCancelGesture() {
             mScrollOffsetManager.onFlingCancelGesture();
@@ -397,6 +397,11 @@
         public void onUnhandledFlingStartEvent() {
             mScrollOffsetManager.onUnhandledFlingStartEvent();
         }
+
+        @Override
+        public void onScrollUpdateGestureConsumed() {
+            mScrollAccessibilityHelper.postViewScrolledAccessibilityEventCallback();
+        }
     }
 
     //--------------------------------------------------------------------------------------------
@@ -514,6 +519,7 @@
         mSettings.setDIPScale(mDIPScale);
         mScrollOffsetManager = new AwScrollOffsetManager(new AwScrollOffsetManagerDelegate(),
                 new OverScroller(mContainerView.getContext()));
+        mScrollAccessibilityHelper = new ScrollAccessibilityHelper(mContainerView);
 
         setOverScrollMode(mContainerView.getOverScrollMode());
         setScrollBarStyle(mInternalAccessAdapter.super_getScrollBarStyle());
@@ -1008,6 +1014,10 @@
      * @see View#onScrollChanged(int,int)
      */
     public void onContainerViewScrollChanged(int l, int t, int oldl, int oldt) {
+        // A side-effect of View.onScrollChanged is that the scroll accessibility event being sent
+        // by the base class implementation. This is completely hidden from the base classes and
+        // cannot be prevented, which is why we need the code below.
+        mScrollAccessibilityHelper.removePostedViewScrolledAccessibilityEventCallback();
         mScrollOffsetManager.onContainerViewScrollChanged(l, t);
     }
 
@@ -1549,6 +1559,8 @@
           mComponentCallbacks = null;
         }
 
+        mScrollAccessibilityHelper.removePostedCallbacks();
+
         if (mPendingDetachCleanupReferences != null) {
             for (int i = 0; i < mPendingDetachCleanupReferences.size(); ++i) {
                 mPendingDetachCleanupReferences.get(i).cleanupNow();
diff --git a/android_webview/java/src/org/chromium/android_webview/ScrollAccessibilityHelper.java b/android_webview/java/src/org/chromium/android_webview/ScrollAccessibilityHelper.java
new file mode 100644
index 0000000..d866dc9
--- /dev/null
+++ b/android_webview/java/src/org/chromium/android_webview/ScrollAccessibilityHelper.java
@@ -0,0 +1,79 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.android_webview;
+
+import android.os.Handler;
+import android.os.Message;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+
+/**
+ * Helper used to post the VIEW_SCROLLED accessibility event.
+ *
+ * TODO(mkosiba): Investigate whether this is behavior we want to share with the chrome/ layer.
+ * TODO(mkosiba): We currently don't handle JS-initiated scrolling for layers other than the root
+ * layer.
+ */
+class ScrollAccessibilityHelper {
+    // This is copied straight out of android.view.ViewConfiguration.
+    private static final long SEND_RECURRING_ACCESSIBILITY_EVENTS_INTERVAL_MILLIS = 100;
+
+    private class HandlerCallback implements Handler.Callback {
+        public static final int MSG_VIEW_SCROLLED = 1;
+
+        private View mEventSender;
+
+        public HandlerCallback(View eventSender) {
+            mEventSender = eventSender;
+        }
+
+        @Override
+        public boolean handleMessage(Message msg) {
+            switch(msg.what) {
+                case MSG_VIEW_SCROLLED:
+                    mMsgViewScrolledQueued = false;
+                    mEventSender.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SCROLLED);
+                    break;
+                default:
+                    throw new IllegalStateException(
+                            "AccessibilityInjector: unhandled message: " + msg.what);
+            }
+            return true;
+        }
+    }
+
+    private Handler mHandler;
+    private boolean mMsgViewScrolledQueued;
+
+    public ScrollAccessibilityHelper(View eventSender) {
+        mHandler = new Handler(new HandlerCallback(eventSender));
+    }
+
+    /**
+     * Post a callback to send a {@link AccessibilityEvent#TYPE_VIEW_SCROLLED} event.
+     * This event is sent at most once every
+     * {@link ViewConfiguration#getSendRecurringAccessibilityEventsInterval()}
+     */
+    public void postViewScrolledAccessibilityEventCallback() {
+        if (mMsgViewScrolledQueued)
+            return;
+        mMsgViewScrolledQueued = true;
+
+        Message msg = mHandler.obtainMessage(HandlerCallback.MSG_VIEW_SCROLLED);
+        mHandler.sendMessageDelayed(msg, SEND_RECURRING_ACCESSIBILITY_EVENTS_INTERVAL_MILLIS);
+    }
+
+    public void removePostedViewScrolledAccessibilityEventCallback() {
+        if (!mMsgViewScrolledQueued)
+            return;
+        mMsgViewScrolledQueued = false;
+
+        mHandler.removeMessages(HandlerCallback.MSG_VIEW_SCROLLED);
+    }
+
+    public void removePostedCallbacks() {
+        removePostedViewScrolledAccessibilityEventCallback();
+    }
+}
diff --git a/android_webview/javatests/src/org/chromium/android_webview/test/AndroidScrollIntegrationTest.java b/android_webview/javatests/src/org/chromium/android_webview/test/AndroidScrollIntegrationTest.java
index 2dad113..08c6c08 100644
--- a/android_webview/javatests/src/org/chromium/android_webview/test/AndroidScrollIntegrationTest.java
+++ b/android_webview/javatests/src/org/chromium/android_webview/test/AndroidScrollIntegrationTest.java
@@ -280,9 +280,6 @@
         final int targetScrollYPix = (int) Math.ceil(targetScrollYCss * deviceDIPScale);
         final JavascriptEventObserver onscrollObserver = new JavascriptEventObserver();
 
-        Log.w("AndroidScrollIntegrationTest", String.format("scroll in Js (%d, %d) -> (%d, %d)",
-                    targetScrollXCss, targetScrollYCss, targetScrollXPix, targetScrollYPix));
-
         getInstrumentation().runOnMainSync(new Runnable() {
             @Override
             public void run() {
@@ -691,4 +688,77 @@
                 break;
         }
     }
+
+    private static class TestGestureStateListener implements ContentViewCore.GestureStateListener {
+        private CallbackHelper mOnScrollUpdateGestureConsumedHelper = new CallbackHelper();
+
+        public CallbackHelper getOnScrollUpdateGestureConsumedHelper() {
+            return mOnScrollUpdateGestureConsumedHelper;
+        }
+
+        @Override
+        public void onPinchGestureStart() {
+        }
+
+        @Override
+        public void onPinchGestureEnd() {
+        }
+
+        @Override
+        public void onFlingStartGesture(int velocityX, int velocityY) {
+        }
+
+        @Override
+        public void onFlingCancelGesture() {
+        }
+
+        @Override
+        public void onUnhandledFlingStartEvent() {
+        }
+
+        @Override
+        public void onScrollUpdateGestureConsumed() {
+            mOnScrollUpdateGestureConsumedHelper.notifyCalled();
+        }
+    }
+
+    @SmallTest
+    @Feature({"AndroidWebView"})
+    public void testTouchScrollingConsumesScrollByGesture() throws Throwable {
+        final TestAwContentsClient contentsClient = new TestAwContentsClient();
+        final ScrollTestContainerView testContainerView =
+            (ScrollTestContainerView) createAwTestContainerViewOnMainSync(contentsClient);
+        final TestGestureStateListener testGestureStateListener = new TestGestureStateListener();
+        enableJavaScriptOnUiThread(testContainerView.getAwContents());
+
+        final int dragSteps = 10;
+        final int dragStepSize = 24;
+        // Watch out when modifying - if the y or x delta aren't big enough vertical or horizontal
+        // scroll snapping will kick in.
+        final int targetScrollXPix = dragStepSize * dragSteps;
+        final int targetScrollYPix = dragStepSize * dragSteps;
+
+        loadTestPageAndWaitForFirstFrame(testContainerView, contentsClient, null,
+                "<div>" +
+                "  <div style=\"width:10000px; height: 10000px;\"> force scrolling </div>" +
+                "</div>");
+
+        getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                testContainerView.getContentViewCore().setGestureStateListener(
+                        testGestureStateListener);
+            }
+        });
+        final CallbackHelper onScrollUpdateGestureConsumedHelper =
+            testGestureStateListener.getOnScrollUpdateGestureConsumedHelper();
+
+        final int callCount = onScrollUpdateGestureConsumedHelper.getCallCount();
+        AwTestTouchUtils.dragCompleteView(testContainerView,
+                0, -targetScrollXPix, // these need to be negative as we're scrolling down.
+                0, -targetScrollYPix,
+                dragSteps,
+                null /* completionLatch */);
+        onScrollUpdateGestureConsumedHelper.waitForCallback(callCount);
+    }
 }
diff --git a/android_webview/test/shell/src/org/chromium/android_webview/test/AwTestContainerView.java b/android_webview/test/shell/src/org/chromium/android_webview/test/AwTestContainerView.java
index e3c5077..6090143 100644
--- a/android_webview/test/shell/src/org/chromium/android_webview/test/AwTestContainerView.java
+++ b/android_webview/test/shell/src/org/chromium/android_webview/test/AwTestContainerView.java
@@ -8,14 +8,18 @@
 import android.content.res.Configuration;
 import android.graphics.Canvas;
 import android.graphics.Rect;
+import android.os.Bundle;
+import android.util.Log;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeProvider;
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.InputConnection;
 import android.widget.FrameLayout;
-import android.util.Log;
 
 import org.chromium.android_webview.AwContents;
 import org.chromium.content.browser.ContentViewCore;
@@ -147,6 +151,32 @@
         super.onDraw(canvas);
     }
 
+    @Override
+    public AccessibilityNodeProvider getAccessibilityNodeProvider() {
+        AccessibilityNodeProvider provider =
+            mAwContents.getAccessibilityNodeProvider();
+        return provider == null ? super.getAccessibilityNodeProvider() : provider;
+    }
+
+    @Override
+    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfo(info);
+        info.setClassName(AwContents.class.getName());
+        mAwContents.onInitializeAccessibilityNodeInfo(info);
+    }
+
+    @Override
+    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+        super.onInitializeAccessibilityEvent(event);
+        event.setClassName(AwContents.class.getName());
+        mAwContents.onInitializeAccessibilityEvent(event);
+    }
+
+    @Override
+    public boolean performAccessibilityAction(int action, Bundle arguments) {
+        return mAwContents.performAccessibilityAction(action, arguments);
+    }
+
     // TODO: AwContents could define a generic class that holds an implementation similar to
     // the one below.
     private class InternalAccessAdapter implements AwContents.InternalAccessDelegate {
diff --git a/content/browser/android/content_view_core_impl.cc b/content/browser/android/content_view_core_impl.cc
index 31ab2d2..d3b83c3 100644
--- a/content/browser/android/content_view_core_impl.cc
+++ b/content/browser/android/content_view_core_impl.cc
@@ -487,6 +487,14 @@
   Java_ContentViewCore_unhandledFlingStartEvent(env, j_obj.obj());
 }
 
+void ContentViewCoreImpl::OnScrollUpdateGestureConsumed() {
+  JNIEnv* env = AttachCurrentThread();
+  ScopedJavaLocalRef<jobject> j_obj = java_ref_.get(env);
+  if (j_obj.is_null())
+    return;
+  Java_ContentViewCore_onScrollUpdateGestureConsumed(env, j_obj.obj());
+}
+
 void ContentViewCoreImpl::HasTouchEventHandlers(bool need_touch_events) {
   JNIEnv* env = AttachCurrentThread();
   ScopedJavaLocalRef<jobject> j_obj = java_ref_.get(env);
diff --git a/content/browser/android/content_view_core_impl.h b/content/browser/android/content_view_core_impl.h
index a20365b..907d96c 100644
--- a/content/browser/android/content_view_core_impl.h
+++ b/content/browser/android/content_view_core_impl.h
@@ -256,6 +256,7 @@
   bool HasFocus();
   void ConfirmTouchEvent(InputEventAckState ack_result);
   void UnhandledFlingStartEvent();
+  void OnScrollUpdateGestureConsumed();
   void HasTouchEventHandlers(bool need_touch_events);
   void OnSelectionChanged(const std::string& text);
   void OnSelectionBoundsChanged(
diff --git a/content/browser/renderer_host/render_widget_host_view_android.cc b/content/browser/renderer_host/render_widget_host_view_android.cc
index 8e88540..fde357b 100644
--- a/content/browser/renderer_host/render_widget_host_view_android.cc
+++ b/content/browser/renderer_host/render_widget_host_view_android.cc
@@ -932,6 +932,10 @@
 void RenderWidgetHostViewAndroid::GestureEventAck(
     int gesture_event_type,
     InputEventAckState ack_result) {
+  if (gesture_event_type == WebKit::WebInputEvent::GestureScrollUpdate &&
+      ack_result == INPUT_EVENT_ACK_STATE_CONSUMED) {
+    content_view_core_->OnScrollUpdateGestureConsumed();
+  }
   if (gesture_event_type == WebKit::WebInputEvent::GestureFlingStart &&
       ack_result == INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS) {
     content_view_core_->UnhandledFlingStartEvent();
diff --git a/content/public/android/java/src/org/chromium/content/browser/ContentViewCore.java b/content/public/android/java/src/org/chromium/content/browser/ContentViewCore.java
index 2cfd201..2989264 100644
--- a/content/public/android/java/src/org/chromium/content/browser/ContentViewCore.java
+++ b/content/public/android/java/src/org/chromium/content/browser/ContentViewCore.java
@@ -176,8 +176,8 @@
     }
 
     /**
-     * An interface that allows the embedder to be notified when the pinch gesture starts and
-     * stops.
+     * An interface that allows the embedder to be notified of events and state changes related to
+     * gesture processing.
      */
     public interface GestureStateListener {
         /**
@@ -204,6 +204,14 @@
          * Called when a fling event was not handled by the renderer.
          */
         void onUnhandledFlingStartEvent();
+
+        /**
+         * Called to indicate that a scroll update gesture had been consumed by the page.
+         * This callback is called whenever any layer is scrolled (like a frame or div). It is
+         * not called when a JS touch handler consumes the event (preventDefault), it is not called
+         * for JS-initiated scrolling.
+         */
+        void onScrollUpdateGestureConsumed();
     }
 
     /**
@@ -1266,6 +1274,14 @@
         }
     }
 
+    @SuppressWarnings("unused")
+    @CalledByNative
+    private void onScrollUpdateGestureConsumed() {
+        if (mGestureStateListener != null) {
+            mGestureStateListener.onScrollUpdateGestureConsumed();
+        }
+    }
+
     @Override
     public boolean sendGesture(int type, long timeMs, int x, int y, boolean lastInputEventForVSync,
                                Bundle b) {