Enables scroll capture for WebView

Bug: 190731582
Test: manual, long screenshot Gmail message
Change-Id: I16cd568ab155d308e444c448cc77e538c6415061
diff --git a/core/java/com/android/internal/view/ScrollCaptureInternal.java b/core/java/com/android/internal/view/ScrollCaptureInternal.java
index e3a9fda..72b5488 100644
--- a/core/java/com/android/internal/view/ScrollCaptureInternal.java
+++ b/core/java/com/android/internal/view/ScrollCaptureInternal.java
@@ -25,6 +25,7 @@
 import android.view.ScrollCaptureCallback;
 import android.view.View;
 import android.view.ViewGroup;
+import android.webkit.WebView;
 import android.widget.ListView;
 
 /**
@@ -43,7 +44,7 @@
     private static final int DOWN = 1;
 
     /**
-     * Not a ViewGroup, or cannot scroll according to View APIs.
+     * Cannot scroll according to {@link View#canScrollVertically}.
      */
     public static final int TYPE_FIXED = 0;
 
@@ -60,7 +61,7 @@
     public static final int TYPE_RECYCLING = 2;
 
     /**
-     * The ViewGroup scrolls, but has no child views in
+     * Unknown scrollable view with no child views (or not a subclass of ViewGroup).
      */
     private static final int TYPE_OPAQUE = 3;
 
@@ -73,16 +74,6 @@
      * as excluded during scroll capture search.
      */
     private static int detectScrollingType(View view) {
-        // Must be a ViewGroup
-        if (!(view instanceof ViewGroup)) {
-            if (DEBUG_VERBOSE) {
-                Log.v(TAG, "hint: not a subclass of ViewGroup");
-            }
-            return TYPE_FIXED;
-        }
-        if (DEBUG_VERBOSE) {
-            Log.v(TAG, "hint: is a subclass of ViewGroup");
-        }
         // Confirm that it can scroll.
         if (!(view.canScrollVertically(DOWN) || view.canScrollVertically(UP))) {
             // Nothing to scroll here, move along.
@@ -94,6 +85,17 @@
         if (DEBUG_VERBOSE) {
             Log.v(TAG, "hint: can be scrolled up or down");
         }
+        // Must be a ViewGroup
+        if (!(view instanceof ViewGroup)) {
+            if (DEBUG_VERBOSE) {
+                Log.v(TAG, "hint: not a subclass of ViewGroup");
+            }
+            return TYPE_OPAQUE;
+        }
+        if (DEBUG_VERBOSE) {
+            Log.v(TAG, "hint: is a subclass of ViewGroup");
+        }
+
         // ScrollViews accept only a single child.
         if (((ViewGroup) view).getChildCount() > 1) {
             if (DEBUG_VERBOSE) {
@@ -188,6 +190,18 @@
                 }
                 return new ScrollCaptureViewSupport<>((ViewGroup) view,
                         new RecyclerViewCaptureHelper());
+            case TYPE_OPAQUE:
+                if (DEBUG) {
+                    Log.d(TAG, "scroll capture: FOUND " + view.getClass().getName()
+                            + "[" + resolveId(view.getContext(), view.getId()) + "]"
+                            + " -> TYPE_OPAQUE");
+                }
+                if (view instanceof WebView) {
+                    Log.d(TAG, "scroll capture: Using WebView support");
+                    return new ScrollCaptureViewSupport<>((WebView) view,
+                            new WebViewCaptureHelper());
+                }
+                break;
             case TYPE_FIXED:
                 // ignore
                 break;
diff --git a/core/java/com/android/internal/view/WebViewCaptureHelper.java b/core/java/com/android/internal/view/WebViewCaptureHelper.java
new file mode 100644
index 0000000..e6a311c
--- /dev/null
+++ b/core/java/com/android/internal/view/WebViewCaptureHelper.java
@@ -0,0 +1,100 @@
+/*
+ * 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.internal.view;
+
+import static android.util.MathUtils.constrain;
+
+import static java.lang.Math.max;
+import static java.lang.Math.min;
+
+import android.annotation.NonNull;
+import android.graphics.Rect;
+import android.webkit.WebView;
+
+/**
+ * ScrollCapture for WebView.
+ */
+class WebViewCaptureHelper implements ScrollCaptureViewHelper<WebView> {
+    private static final String TAG = "WebViewScrollCapture";
+
+    private final Rect mRequestWebViewLocal = new Rect();
+    private final Rect mWebViewBounds = new Rect();
+
+    private int mOriginScrollY;
+    private int mOriginScrollX;
+
+    @Override
+    public boolean onAcceptSession(@NonNull WebView view) {
+        return view.isVisibleToUser()
+                && (view.getContentHeight() * view.getScale()) > view.getHeight();
+    }
+
+    @Override
+    public void onPrepareForStart(@NonNull WebView view, @NonNull Rect scrollBounds) {
+        mOriginScrollX = view.getScrollX();
+        mOriginScrollY = view.getScrollY();
+    }
+
+    @NonNull
+    @Override
+    public ScrollResult onScrollRequested(@NonNull WebView view, @NonNull Rect scrollBounds,
+            @NonNull Rect requestRect) {
+
+        int scrollDelta = view.getScrollY() - mOriginScrollY;
+
+        ScrollResult result = new ScrollResult();
+        result.requestedArea = new Rect(requestRect);
+        result.availableArea = new Rect();
+        result.scrollDelta = scrollDelta;
+
+        mWebViewBounds.set(0, 0, view.getWidth(), view.getHeight());
+
+        if (!view.isVisibleToUser()) {
+            return result;
+        }
+
+        // Map the request into local coordinates
+        mRequestWebViewLocal.set(requestRect);
+        mRequestWebViewLocal.offset(0, -scrollDelta);
+
+        // Offset to center the rect vertically, clamp to available content
+        int upLimit = min(0, -view.getScrollY());
+        int contentHeightPx = (int) (view.getContentHeight() * view.getScale());
+        int downLimit = max(0, (contentHeightPx - view.getHeight()) - view.getScrollY());
+        int scrollToCenter = mRequestWebViewLocal.centerY() - mWebViewBounds.centerY();
+        int scrollMovement = constrain(scrollToCenter, upLimit, downLimit);
+
+        // Scroll and update relative based on  the new position
+        view.scrollBy(mOriginScrollX, scrollMovement);
+        scrollDelta = view.getScrollY() - mOriginScrollY;
+        mRequestWebViewLocal.offset(0, -scrollMovement);
+        result.scrollDelta = scrollDelta;
+
+        if (mRequestWebViewLocal.intersect(mWebViewBounds)) {
+            result.availableArea = new Rect(mRequestWebViewLocal);
+            result.availableArea.offset(0, result.scrollDelta);
+        }
+        return result;
+    }
+
+    @Override
+    public void onPrepareForEnd(@NonNull WebView view) {
+        view.scrollTo(mOriginScrollX, mOriginScrollY);
+    }
+
+}
+