From d1b525354fbe064d1111f569944a2992a48f01ee Mon Sep 17 00:00:00 2001 From: Mark Renouf Date: Fri, 30 Jul 2021 09:41:32 -0400 Subject: [PATCH] Enables scroll capture for WebView Bug: 190731582 Test: manual, long screenshot Gmail message Change-Id: I16cd568ab155d308e444c448cc77e538c6415061 --- .../internal/view/ScrollCaptureInternal.java | 38 ++++--- .../internal/view/WebViewCaptureHelper.java | 100 ++++++++++++++++++ 2 files changed, 126 insertions(+), 12 deletions(-) create mode 100644 core/java/com/android/internal/view/WebViewCaptureHelper.java diff --git a/core/java/com/android/internal/view/ScrollCaptureInternal.java b/core/java/com/android/internal/view/ScrollCaptureInternal.java index e3a9fda7b000f..72b5488f4bac4 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.util.Log; import android.view.ScrollCaptureCallback; import android.view.View; import android.view.ViewGroup; +import android.webkit.WebView; import android.widget.ListView; /** @@ -43,7 +44,7 @@ public class ScrollCaptureInternal { 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 class ScrollCaptureInternal { 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 @@ public class ScrollCaptureInternal { * 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 @@ public class ScrollCaptureInternal { 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 @@ public class ScrollCaptureInternal { } 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 0000000000000..e6a311cfcda5c --- /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 { + 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); + } + +} +