chromium/content/public/android/java/src/org/chromium/content/browser/webcontents/WebContentsImpl.java

// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.content.browser.webcontents;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.Parcel;
import android.os.ParcelUuid;
import android.os.Parcelable;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.ViewStructure;

import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import org.jni_zero.CalledByNative;
import org.jni_zero.JNINamespace;
import org.jni_zero.NativeMethods;

import org.chromium.base.Callback;
import org.chromium.base.JavaExceptionReporter;
import org.chromium.base.Log;
import org.chromium.base.ObserverList;
import org.chromium.base.TerminationStatus;
import org.chromium.base.ThreadUtils;
import org.chromium.base.UserData;
import org.chromium.base.UserDataHost;
import org.chromium.blink_public.input.SelectionGranularity;
import org.chromium.cc.input.BrowserControlsOffsetTagsInfo;
import org.chromium.content.browser.AppWebMessagePort;
import org.chromium.content.browser.GestureListenerManagerImpl;
import org.chromium.content.browser.MediaSessionImpl;
import org.chromium.content.browser.RenderCoordinatesImpl;
import org.chromium.content.browser.RenderWidgetHostViewImpl;
import org.chromium.content.browser.ViewEventSinkImpl;
import org.chromium.content.browser.WindowEventObserver;
import org.chromium.content.browser.WindowEventObserverManager;
import org.chromium.content.browser.accessibility.ViewStructureBuilder;
import org.chromium.content.browser.framehost.RenderFrameHostDelegate;
import org.chromium.content.browser.framehost.RenderFrameHostImpl;
import org.chromium.content.browser.input.ImeAdapterImpl;
import org.chromium.content.browser.selection.SelectionPopupControllerImpl;
import org.chromium.content_public.browser.ChildProcessImportance;
import org.chromium.content_public.browser.GlobalRenderFrameHostId;
import org.chromium.content_public.browser.ImageDownloadCallback;
import org.chromium.content_public.browser.JavaScriptCallback;
import org.chromium.content_public.browser.MessagePayload;
import org.chromium.content_public.browser.MessagePort;
import org.chromium.content_public.browser.NavigationController;
import org.chromium.content_public.browser.RenderFrameHost;
import org.chromium.content_public.browser.StylusWritingHandler;
import org.chromium.content_public.browser.StylusWritingImeCallback;
import org.chromium.content_public.browser.ViewEventSink.InternalAccessDelegate;
import org.chromium.content_public.browser.Visibility;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.browser.WebContentsInternals;
import org.chromium.content_public.browser.WebContentsObserver;
import org.chromium.content_public.browser.back_forward_transition.AnimationStage;
import org.chromium.ui.OverscrollRefreshHandler;
import org.chromium.ui.base.EventForwarder;
import org.chromium.ui.base.ViewAndroidDelegate;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.mojom.VirtualKeyboardMode;
import org.chromium.url.GURL;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;

/**
 * The WebContentsImpl Java wrapper to allow communicating with the native WebContentsImpl object.
 */
@JNINamespace("content")
public class WebContentsImpl implements WebContents, RenderFrameHostDelegate, WindowEventObserver {
    private static final String TAG = "WebContentsImpl";

    private static final String PARCEL_VERSION_KEY = "version";
    private static final String PARCEL_WEBCONTENTS_KEY = "webcontents";
    private static final String PARCEL_PROCESS_GUARD_KEY = "processguard";

    private static final long PARCELABLE_VERSION_ID = 0;
    // Non-final for testing purposes, so resetting of the UUID can happen.
    private static UUID sParcelableUUID = UUID.randomUUID();

    /**
     * Used to reset the internal tracking for whether or not a serialized {@link WebContents}
     * was created in this process or not.
     */
    public static void invalidateSerializedWebContentsForTesting() {
        sParcelableUUID = UUID.randomUUID();
    }

    /**
     * A {@link android.os.Parcelable.Creator} instance that is used to build {@link
     * WebContentsImpl} objects from a {@link Parcel}.
     */
    // TODO(crbug.com/40479664): Fix this properly.
    @SuppressLint("ParcelClassLoader")
    public static final Parcelable.Creator<WebContents> CREATOR =
            new Parcelable.Creator<WebContents>() {
                @Override
                public WebContents createFromParcel(Parcel source) {
                    Bundle bundle = source.readBundle();

                    // Check the version.
                    if (bundle.getLong(PARCEL_VERSION_KEY, -1) != 0) return null;

                    // Check that we're in the same process.
                    ParcelUuid parcelUuid = bundle.getParcelable(PARCEL_PROCESS_GUARD_KEY);
                    if (sParcelableUUID.compareTo(parcelUuid.getUuid()) != 0) return null;

                    // Attempt to retrieve the WebContents object from the native pointer.
                    return WebContentsImplJni.get()
                            .fromNativePtr(bundle.getLong(PARCEL_WEBCONTENTS_KEY));
                }

                @Override
                public WebContents[] newArray(int size) {
                    return new WebContents[size];
                }
            };

    /**
     * Factory interface passed to {@link #getOrSetUserData()} for instantiation of
     * class as user data.
     *
     * Constructor method reference comes handy for class Foo to provide the factory.
     * Use lazy initialization to avoid having to generate too many anonymous references.
     *
     * <code>
     * public class Foo {
     *     static final class FoofactoryLazyHolder {
     *         private static final UserDataFactory<Foo> INSTANCE = Foo::new;
     *     }
     *     ....
     *
     *     webContents.getOrsetUserData(Foo.class, FooFactoryLazyHolder.INSTANCE);
     *
     *     ....
     * }
     * </code>
     *
     * @param <T> Class to instantiate.
     */
    public interface UserDataFactory<T> {
        T create(WebContents webContents);
    }

    // Note this list may be incomplete. Frames that never had to initialize java side would
    // not have an entry here. This is here mainly to keep the java RenderFrameHosts alive, since
    // native side generally cannot safely hold strong references to them.
    private final List<RenderFrameHostImpl> mFrames = new ArrayList<>();

    private long mNativeWebContentsAndroid;
    private NavigationController mNavigationController;

    // Lazily created proxy observer for handling all Java-based WebContentsObservers.
    private WebContentsObserverProxy mObserverProxy;

    // The media session for this WebContents. It is constructed by the native MediaSession and has
    // the same life time as native MediaSession.
    private MediaSessionImpl mMediaSession;

    class SmartClipCallback {
        public SmartClipCallback(final Handler smartClipHandler) {
            mHandler = smartClipHandler;
        }

        public void onSmartClipDataExtracted(String text, String html, Rect clipRect) {
            RenderCoordinatesImpl coordinateSpace = getRenderCoordinates();
            clipRect.offset(0, (int) coordinateSpace.getContentOffsetYPix());
            Bundle bundle = new Bundle();
            bundle.putString("url", getVisibleUrl().getSpec());
            bundle.putString("title", getTitle());
            bundle.putString("text", text);
            bundle.putString("html", html);
            bundle.putParcelable("rect", clipRect);

            Message msg = Message.obtain(mHandler, 0);
            msg.setData(bundle);
            msg.sendToTarget();
        }

        final Handler mHandler;
    }

    private SmartClipCallback mSmartClipCallback;

    private EventForwarder mEventForwarder;

    private StylusWritingHandler mStylusWritingHandler;

    // Cached copy of all positions and scales as reported by the renderer.
    private RenderCoordinatesImpl mRenderCoordinates;

    private InternalsHolder mInternalsHolder;

    private String mProductVersion;

    private boolean mInitialized;

    // Remember the stack for clearing native the native stack for debugging use after destroy.
    private Throwable mNativeDestroyThrowable;

    private ObserverList<Runnable> mTearDownDialogOverlaysHandlers;

    private static class WebContentsInternalsImpl implements WebContentsInternals {
        public UserDataHost userDataHost;
        public ViewAndroidDelegate viewAndroidDelegate;
    }

    private WebContentsImpl(
            long nativeWebContentsAndroid, NavigationController navigationController) {
        assert nativeWebContentsAndroid != 0;
        mNativeWebContentsAndroid = nativeWebContentsAndroid;
        mNavigationController = navigationController;
    }

    @CalledByNative
    @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
    public static WebContentsImpl create(
            long nativeWebContentsAndroid, NavigationController navigationController) {
        return new WebContentsImpl(nativeWebContentsAndroid, navigationController);
    }

    @Override
    public void setDelegates(
            String productVersion,
            ViewAndroidDelegate viewDelegate,
            InternalAccessDelegate accessDelegate,
            WindowAndroid windowAndroid,
            InternalsHolder internalsHolder) {
        assert internalsHolder != null;

        mProductVersion = productVersion;

        WebContentsInternalsImpl internals;
        if (mInternalsHolder != null) {
            internals = (WebContentsInternalsImpl) mInternalsHolder.get();
        } else {
            internals = new WebContentsInternalsImpl();
            internals.userDataHost = new UserDataHost();
        }
        mInternalsHolder = internalsHolder;
        mInternalsHolder.set(internals);

        if (mRenderCoordinates == null) {
            mRenderCoordinates = new RenderCoordinatesImpl();
        }

        mInitialized = true;

        setViewAndroidDelegate(viewDelegate);
        setTopLevelNativeWindow(windowAndroid);

        if (accessDelegate == null) {
            accessDelegate = new EmptyInternalAccessDelegate();
        }
        ViewEventSinkImpl.from(this).setAccessDelegate(accessDelegate);

        if (windowAndroid != null) {
            getRenderCoordinates().setDeviceScaleFactor(windowAndroid.getDisplay().getDipScale());
        }

        // Create GestureListenerManagerImpl so it updates `mRenderCoordinates`.
        GestureListenerManagerImpl.fromWebContents(this);
    }

    @Override
    public void clearJavaWebContentsObservers() {
        // Clear all the Android specific observers.
        if (mObserverProxy != null) {
            mObserverProxy.destroy();
            mObserverProxy = null;
        }
    }

    @Nullable
    public Context getContext() {
        assert mInitialized;

        WindowAndroid window = getTopLevelNativeWindow();
        return window != null ? window.getContext().get() : null;
    }

    public String getProductVersion() {
        assert mInitialized;
        return mProductVersion;
    }

    @CalledByNative
    @VisibleForTesting
    void clearNativePtr() {
        mNativeDestroyThrowable = new RuntimeException("clearNativePtr");
        mNativeWebContentsAndroid = 0;
        mNavigationController = null;
        if (mObserverProxy != null) {
            mObserverProxy.destroy();
            mObserverProxy = null;
        }
    }

    // =================== RenderFrameHostDelegate overrides ===================
    @Override
    public void renderFrameCreated(RenderFrameHostImpl host) {
        assert !mFrames.contains(host);
        mFrames.add(host);
    }

    @Override
    public void renderFrameDeleted(RenderFrameHostImpl host) {
        assert mFrames.contains(host);
        mFrames.remove(host);
    }

    // ================= end RenderFrameHostDelegate overrides =================

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        // This is wrapped in a Bundle so that failed deserialization attempts don't corrupt the
        // overall Parcel.  If we failed a UUID or Version check and didn't read the rest of the
        // fields it would corrupt the serialized stream.
        Bundle data = new Bundle();
        data.putLong(PARCEL_VERSION_KEY, PARCELABLE_VERSION_ID);
        data.putParcelable(PARCEL_PROCESS_GUARD_KEY, new ParcelUuid(sParcelableUUID));
        data.putLong(PARCEL_WEBCONTENTS_KEY, mNativeWebContentsAndroid);
        dest.writeBundle(data);
    }

    @CalledByNative
    private long getNativePointer() {
        return mNativeWebContentsAndroid;
    }

    @Override
    public WindowAndroid getTopLevelNativeWindow() {
        checkNotDestroyed();
        return WebContentsImplJni.get().getTopLevelNativeWindow(mNativeWebContentsAndroid);
    }

    @Override
    public void setTopLevelNativeWindow(WindowAndroid windowAndroid) {
        checkNotDestroyed();
        WebContentsImplJni.get().setTopLevelNativeWindow(mNativeWebContentsAndroid, windowAndroid);
        WindowEventObserverManager.from(this).onWindowAndroidChanged(windowAndroid);
        if (mObserverProxy != null) mObserverProxy.onTopLevelNativeWindowChanged(windowAndroid);
    }

    @Override
    public ViewAndroidDelegate getViewAndroidDelegate() {
        WebContentsInternals internals = mInternalsHolder.get();
        if (internals == null) return null;
        return ((WebContentsInternalsImpl) internals).viewAndroidDelegate;
    }

    private void setViewAndroidDelegate(ViewAndroidDelegate viewDelegate) {
        checkNotDestroyed();
        WebContentsInternals internals = mInternalsHolder.get();
        assert internals != null;
        WebContentsInternalsImpl impl = (WebContentsInternalsImpl) internals;
        impl.viewAndroidDelegate = viewDelegate;
        WebContentsImplJni.get().setViewAndroidDelegate(mNativeWebContentsAndroid, viewDelegate);
    }

    @Override
    public void destroy() {
        // Note that |WebContents.destroy| is not guaranteed to be invoked.
        // Any resource release relying on this method will likely be leaked.

        if (!ThreadUtils.runningOnUiThread()) {
            throw new IllegalStateException("Attempting to destroy WebContents on non-UI thread");
        }

        if (mObserverProxy != null && mObserverProxy.isHandlingObserverCall()) {
            throw new RuntimeException(
                    "Attempting to destroy WebContents while handling observer calls");
        }

        if (mNativeWebContentsAndroid != 0) {
            WebContentsImplJni.get().destroyWebContents(mNativeWebContentsAndroid);
        }
    }

    @Override
    public boolean isDestroyed() {
        return mNativeWebContentsAndroid == 0
                || WebContentsImplJni.get().isBeingDestroyed(mNativeWebContentsAndroid);
    }

    @Override
    public void clearNativeReference() {
        if (mNativeWebContentsAndroid != 0) {
            WebContentsImplJni.get().clearNativeReference(mNativeWebContentsAndroid);
        }
    }

    @Override
    public NavigationController getNavigationController() {
        return mNavigationController;
    }

    @Override
    public RenderFrameHost getMainFrame() {
        checkNotDestroyed();
        return WebContentsImplJni.get().getMainFrame(mNativeWebContentsAndroid);
    }

    @Override
    public RenderFrameHost getFocusedFrame() {
        checkNotDestroyed();
        return WebContentsImplJni.get().getFocusedFrame(mNativeWebContentsAndroid);
    }

    @Override
    public boolean isFocusedElementEditable() {
        checkNotDestroyed();
        return WebContentsImplJni.get().isFocusedElementEditable(mNativeWebContentsAndroid);
    }

    @Override
    public RenderFrameHost getRenderFrameHostFromId(GlobalRenderFrameHostId id) {
        checkNotDestroyed();
        return WebContentsImplJni.get()
                .getRenderFrameHostFromId(
                        mNativeWebContentsAndroid, id.childId(), id.frameRoutingId());
    }

    // The RenderFrameHosts that are every RenderFrameHost in this WebContents.
    // See C++'s WebContents::ForEachRenderFrameHost for details.
    public List<RenderFrameHost> getAllRenderFrameHosts() {
        checkNotDestroyed();
        RenderFrameHost[] frames =
                WebContentsImplJni.get().getAllRenderFrameHosts(mNativeWebContentsAndroid);
        return Collections.unmodifiableList(Arrays.asList(frames));
    }

    @CalledByNative
    private static RenderFrameHost[] createRenderFrameHostArray(int size) {
        return new RenderFrameHost[size];
    }

    @CalledByNative
    private static void addRenderFrameHostToArray(
            RenderFrameHost[] frames, int index, RenderFrameHost frame) {
        frames[index] = frame;
    }

    /**
     * Thrown by reportDanglingPtrToBrowserContext(), indicating that WebContentsImpl is deleted
     * after its BrowserContext.
     */
    private static class DanglingPointerException extends RuntimeException {
        DanglingPointerException(String msg, Throwable causedBy) {
            super(msg, causedBy);
        }
    }

    @CalledByNative
    private static void reportDanglingPtrToBrowserContext(Throwable creator) {
        JavaExceptionReporter.reportException(
                new DanglingPointerException(
                        "Dangling pointer to BrowserContext in WebContents", creator));
    }

    @Override
    public @Nullable RenderWidgetHostViewImpl getRenderWidgetHostView() {
        if (mNativeWebContentsAndroid == 0) return null;
        RenderWidgetHostViewImpl rwhvi =
                WebContentsImplJni.get().getRenderWidgetHostView(mNativeWebContentsAndroid);
        if (rwhvi == null || rwhvi.isDestroyed()) return null;

        return rwhvi;
    }

    @Override
    public @Visibility int getVisibility() {
        checkNotDestroyed();
        return WebContentsImplJni.get().getVisibility(mNativeWebContentsAndroid);
    }

    @Override
    public void updateWebContentsVisibility(@Visibility int visibility) {
        checkNotDestroyed();
        WebContentsImplJni.get().updateWebContentsVisibility(mNativeWebContentsAndroid, visibility);
    }

    @Override
    public String getTitle() {
        checkNotDestroyed();
        return WebContentsImplJni.get().getTitle(mNativeWebContentsAndroid);
    }

    @Override
    public GURL getVisibleUrl() {
        checkNotDestroyed();
        return WebContentsImplJni.get().getVisibleURL(mNativeWebContentsAndroid);
    }

    @Override
    @VirtualKeyboardMode.EnumType
    public int getVirtualKeyboardMode() {
        checkNotDestroyed();
        return WebContentsImplJni.get().getVirtualKeyboardMode(mNativeWebContentsAndroid);
    }

    @Override
    public String getEncoding() {
        checkNotDestroyed();
        return WebContentsImplJni.get().getEncoding(mNativeWebContentsAndroid);
    }

    @Override
    public boolean isLoading() {
        checkNotDestroyed();
        return WebContentsImplJni.get().isLoading(mNativeWebContentsAndroid);
    }

    @Override
    public boolean shouldShowLoadingUI() {
        checkNotDestroyed();
        return WebContentsImplJni.get().shouldShowLoadingUI(mNativeWebContentsAndroid);
    }

    @Override
    public boolean hasUncommittedNavigationInPrimaryMainFrame() {
        checkNotDestroyed();
        return WebContentsImplJni.get()
                .hasUncommittedNavigationInPrimaryMainFrame(mNativeWebContentsAndroid);
    }

    @Override
    public void dispatchBeforeUnload(boolean autoCancel) {
        if (mNativeWebContentsAndroid == 0) return;
        WebContentsImplJni.get().dispatchBeforeUnload(mNativeWebContentsAndroid, autoCancel);
    }

    @Override
    public void stop() {
        checkNotDestroyed();
        WebContentsImplJni.get().stop(mNativeWebContentsAndroid);
    }

    /** Cut the selected content. */
    public void cut() {
        checkNotDestroyed();
        WebContentsImplJni.get().cut(mNativeWebContentsAndroid);
    }

    /** Copy the selected content. */
    public void copy() {
        checkNotDestroyed();
        WebContentsImplJni.get().copy(mNativeWebContentsAndroid);
    }

    /** Paste content from the clipboard. */
    public void paste() {
        checkNotDestroyed();
        WebContentsImplJni.get().paste(mNativeWebContentsAndroid);
    }

    /** Paste content from the clipboard without format. */
    public void pasteAsPlainText() {
        checkNotDestroyed();
        WebContentsImplJni.get().pasteAsPlainText(mNativeWebContentsAndroid);
    }

    /** Replace the selected text with the {@code word}. */
    public void replace(String word) {
        checkNotDestroyed();
        WebContentsImplJni.get().replace(mNativeWebContentsAndroid, word);
    }

    /** Select all content. */
    public void selectAll() {
        checkNotDestroyed();
        WebContentsImplJni.get().selectAll(mNativeWebContentsAndroid);
    }

    /** Collapse the selection to the end of selection range. */
    public void collapseSelection() {
        // collapseSelection may get triggered when certain selection-related widgets
        // are destroyed. As the timing for such destruction is unpredictable,
        // safely guard against this case.
        if (isDestroyed()) return;
        WebContentsImplJni.get().collapseSelection(mNativeWebContentsAndroid);
    }

    @Override
    public void onHide() {
        checkNotDestroyed();
        SelectionPopupControllerImpl controller = getSelectionPopupController();
        if (controller != null) controller.hidePopupsAndPreserveSelection();
        WebContentsImplJni.get().onHide(mNativeWebContentsAndroid);
    }

    @Override
    public void onShow() {
        checkNotDestroyed();
        SelectionPopupControllerImpl controller = getSelectionPopupController();
        if (controller != null) controller.restoreSelectionPopupsIfNecessary();
        WebContentsImplJni.get().onShow(mNativeWebContentsAndroid);
    }

    private SelectionPopupControllerImpl getSelectionPopupController() {
        return SelectionPopupControllerImpl.fromWebContents(this);
    }

    @Override
    public void setImportance(@ChildProcessImportance int primaryMainFrameImportance) {
        checkNotDestroyed();
        WebContentsImplJni.get()
                .setImportance(mNativeWebContentsAndroid, primaryMainFrameImportance);
    }

    @Override
    public void suspendAllMediaPlayers() {
        checkNotDestroyed();
        WebContentsImplJni.get().suspendAllMediaPlayers(mNativeWebContentsAndroid);
    }

    @Override
    public void setAudioMuted(boolean mute) {
        checkNotDestroyed();
        WebContentsImplJni.get().setAudioMuted(mNativeWebContentsAndroid, mute);
    }

    @Override
    public boolean isAudioMuted() {
        checkNotDestroyed();
        return WebContentsImplJni.get().isAudioMuted(mNativeWebContentsAndroid);
    }

    @Override
    public boolean focusLocationBarByDefault() {
        checkNotDestroyed();
        return WebContentsImplJni.get().focusLocationBarByDefault(mNativeWebContentsAndroid);
    }

    @Override
    public boolean isFullscreenForCurrentTab() {
        checkNotDestroyed();
        return WebContentsImplJni.get().isFullscreenForCurrentTab(mNativeWebContentsAndroid);
    }

    @Override
    public void exitFullscreen() {
        checkNotDestroyed();
        WebContentsImplJni.get().exitFullscreen(mNativeWebContentsAndroid);
    }

    @Override
    public void scrollFocusedEditableNodeIntoView() {
        checkNotDestroyed();
        // The native side keeps track of whether the zoom and scroll actually occurred. It is
        // more efficient to do it this way and sometimes fire an unnecessary message rather
        // than synchronize with the renderer and always have an additional message.
        WebContentsImplJni.get().scrollFocusedEditableNodeIntoView(mNativeWebContentsAndroid);
    }

    @Override
    public void selectAroundCaret(
            @SelectionGranularity int granularity,
            boolean shouldShowHandle,
            boolean shouldShowContextMenu,
            int startOffset,
            int endOffset,
            int surroundingTextLength) {
        checkNotDestroyed();
        WebContentsImplJni.get()
                .selectAroundCaret(
                        mNativeWebContentsAndroid,
                        granularity,
                        shouldShowHandle,
                        shouldShowContextMenu,
                        startOffset,
                        endOffset,
                        surroundingTextLength);
    }

    @Override
    public void adjustSelectionByCharacterOffset(
            int startAdjust, int endAdjust, boolean showSelectionMenu) {
        WebContentsImplJni.get()
                .adjustSelectionByCharacterOffset(
                        mNativeWebContentsAndroid, startAdjust, endAdjust, showSelectionMenu);
    }

    @Override
    public GURL getLastCommittedUrl() {
        checkNotDestroyed();
        return WebContentsImplJni.get().getLastCommittedURL(mNativeWebContentsAndroid);
    }

    @Override
    public boolean isIncognito() {
        checkNotDestroyed();
        return WebContentsImplJni.get().isIncognito(mNativeWebContentsAndroid);
    }

    @Override
    public void resumeLoadingCreatedWebContents() {
        checkNotDestroyed();
        WebContentsImplJni.get().resumeLoadingCreatedWebContents(mNativeWebContentsAndroid);
    }

    @Override
    public void evaluateJavaScript(String script, JavaScriptCallback callback) {
        ThreadUtils.assertOnUiThread();
        if (isDestroyed() || script == null) return;
        WebContentsImplJni.get().evaluateJavaScript(mNativeWebContentsAndroid, script, callback);
    }

    @Override
    public void evaluateJavaScriptForTests(String script, JavaScriptCallback callback) {
        ThreadUtils.assertOnUiThread();
        if (script == null) return;
        checkNotDestroyed();
        WebContentsImplJni.get()
                .evaluateJavaScriptForTests(mNativeWebContentsAndroid, script, callback);
    }

    @Override
    public void addMessageToDevToolsConsole(int level, String message) {
        checkNotDestroyed();
        WebContentsImplJni.get()
                .addMessageToDevToolsConsole(mNativeWebContentsAndroid, level, message);
    }

    @Override
    public void postMessageToMainFrame(
            MessagePayload messagePayload,
            String sourceOrigin,
            String targetOrigin,
            MessagePort[] ports) {
        if (ports != null) {
            for (MessagePort port : ports) {
                if (port.isClosed() || port.isTransferred()) {
                    throw new IllegalStateException("Port is already closed or transferred");
                }
                if (port.isStarted()) {
                    throw new IllegalStateException("Port is already started");
                }
            }
        }
        // Treat "*" as a wildcard. Internally, a wildcard is a empty string.
        if (targetOrigin.equals("*")) {
            targetOrigin = "";
        }
        WebContentsImplJni.get()
                .postMessageToMainFrame(
                        mNativeWebContentsAndroid,
                        messagePayload,
                        sourceOrigin,
                        targetOrigin,
                        ports);
    }

    @Override
    public AppWebMessagePort[] createMessageChannel() throws IllegalStateException {
        return AppWebMessagePort.createPair();
    }

    @Override
    public boolean hasAccessedInitialDocument() {
        checkNotDestroyed();
        return WebContentsImplJni.get().hasAccessedInitialDocument(mNativeWebContentsAndroid);
    }

    @Override
    public boolean hasViewTransitionOptIn() {
        checkNotDestroyed();
        return WebContentsImplJni.get().hasViewTransitionOptIn(mNativeWebContentsAndroid);
    }

    @CalledByNative
    private static void onEvaluateJavaScriptResult(String jsonResult, JavaScriptCallback callback) {
        callback.handleJavaScriptResult(jsonResult);
    }

    @Override
    public int getThemeColor() {
        checkNotDestroyed();
        return WebContentsImplJni.get().getThemeColor(mNativeWebContentsAndroid);
    }

    @Override
    public int getBackgroundColor() {
        checkNotDestroyed();
        return WebContentsImplJni.get().getBackgroundColor(mNativeWebContentsAndroid);
    }

    @Override
    public float getLoadProgress() {
        checkNotDestroyed();
        return WebContentsImplJni.get().getLoadProgress(mNativeWebContentsAndroid);
    }

    @Override
    public void requestSmartClipExtract(int x, int y, int width, int height) {
        if (mSmartClipCallback == null) return;
        checkNotDestroyed();
        RenderCoordinatesImpl coordinateSpace = getRenderCoordinates();
        y = y - (int) coordinateSpace.getContentOffsetYPix();
        WebContentsImplJni.get()
                .requestSmartClipExtract(
                        mNativeWebContentsAndroid, mSmartClipCallback, x, y, width, height);
    }

    @Override
    public void setSmartClipResultHandler(final Handler smartClipHandler) {
        if (smartClipHandler == null) {
            mSmartClipCallback = null;
            return;
        }
        mSmartClipCallback = new SmartClipCallback(smartClipHandler);
    }

    @CalledByNative
    private static void onSmartClipDataExtracted(
            String text,
            String html,
            int left,
            int top,
            int right,
            int bottom,
            SmartClipCallback callback) {
        callback.onSmartClipDataExtracted(text, html, new Rect(left, top, right, bottom));
    }

    /**
     * Requests a snapshop of accessibility tree. The result is provided asynchronously
     * using the callback
     * @param callback The callback to be called when the snapshot is ready. The callback
     *                 cannot be null.
     */
    public void requestAccessibilitySnapshot(ViewStructure root, Runnable doneCallback) {
        checkNotDestroyed();
        ViewStructureBuilder builder = new ViewStructureBuilder(mRenderCoordinates);

        WebContentsImplJni.get()
                .requestAccessibilitySnapshot(
                        mNativeWebContentsAndroid, root, builder, doneCallback);
    }

    public void simulateRendererKilledForTesting() {
        if (mObserverProxy != null) {
            mObserverProxy.primaryMainFrameRenderProcessGone(TerminationStatus.PROCESS_WAS_KILLED);
        }
    }

    @Override
    public void setStylusWritingHandler(StylusWritingHandler stylusWritingHandler) {
        mStylusWritingHandler = stylusWritingHandler;
        if (mNativeWebContentsAndroid == 0) return;
        WebContentsImplJni.get()
                .setStylusHandwritingEnabled(
                        mNativeWebContentsAndroid, mStylusWritingHandler != null);
    }

    @Override
    public StylusWritingImeCallback getStylusWritingImeCallback() {
        ImeAdapterImpl imeAdapter = ImeAdapterImpl.fromWebContents(this);
        if (imeAdapter == null) return null;
        return imeAdapter.getStylusWritingImeCallback();
    }

    public StylusWritingHandler getStylusWritingHandler() {
        return mStylusWritingHandler;
    }

    @Override
    public EventForwarder getEventForwarder() {
        assert mNativeWebContentsAndroid != 0;
        if (mEventForwarder == null) {
            checkNotDestroyed();
            mEventForwarder =
                    WebContentsImplJni.get().getOrCreateEventForwarder(mNativeWebContentsAndroid);
            mEventForwarder.setStylusWritingDelegate(
                    new EventForwarder.StylusWritingDelegate() {
                        @Override
                        public boolean handleTouchEvent(MotionEvent motionEvent) {
                            return mStylusWritingHandler != null
                                    && mStylusWritingHandler.handleTouchEvent(
                                            motionEvent,
                                            getViewAndroidDelegate().getContainerView());
                        }

                        @Override
                        public void handleHoverEvent(MotionEvent motionEvent) {
                            if (mStylusWritingHandler != null) {
                                mStylusWritingHandler.handleHoverEvent(
                                        motionEvent, getViewAndroidDelegate().getContainerView());
                            }
                        }
                    });
        }
        return mEventForwarder;
    }

    @Override
    public void addObserver(WebContentsObserver observer) {
        assert mNativeWebContentsAndroid != 0;
        if (mObserverProxy == null) mObserverProxy = new WebContentsObserverProxy(this);
        mObserverProxy.addObserver(observer);
    }

    @Override
    public void removeObserver(WebContentsObserver observer) {
        if (mObserverProxy == null) return;
        mObserverProxy.removeObserver(observer);
    }

    @Override
    public void setOverscrollRefreshHandler(OverscrollRefreshHandler handler) {
        checkNotDestroyed();
        WebContentsImplJni.get().setOverscrollRefreshHandler(mNativeWebContentsAndroid, handler);
    }

    @Override
    public void setSpatialNavigationDisabled(boolean disabled) {
        checkNotDestroyed();
        WebContentsImplJni.get().setSpatialNavigationDisabled(mNativeWebContentsAndroid, disabled);
    }

    @Override
    public int downloadImage(
            GURL url,
            boolean isFavicon,
            int maxBitmapSize,
            boolean bypassCache,
            ImageDownloadCallback callback) {
        checkNotDestroyed();
        return WebContentsImplJni.get()
                .downloadImage(
                        mNativeWebContentsAndroid,
                        url,
                        isFavicon,
                        maxBitmapSize,
                        bypassCache,
                        callback);
    }

    @CalledByNative
    private void onDownloadImageFinished(
            ImageDownloadCallback callback,
            int id,
            int httpStatusCode,
            GURL imageUrl,
            List<Bitmap> bitmaps,
            List<Rect> sizes) {
        callback.onFinishDownloadImage(id, httpStatusCode, imageUrl, bitmaps, sizes);
    }

    @Override
    public void setHasPersistentVideo(boolean value) {
        checkNotDestroyed();
        WebContentsImplJni.get().setHasPersistentVideo(mNativeWebContentsAndroid, value);
    }

    @Override
    public boolean hasActiveEffectivelyFullscreenVideo() {
        checkNotDestroyed();
        return WebContentsImplJni.get()
                .hasActiveEffectivelyFullscreenVideo(mNativeWebContentsAndroid);
    }

    @Override
    public boolean isPictureInPictureAllowedForFullscreenVideo() {
        checkNotDestroyed();
        return WebContentsImplJni.get()
                .isPictureInPictureAllowedForFullscreenVideo(mNativeWebContentsAndroid);
    }

    @Override
    public @Nullable Rect getFullscreenVideoSize() {
        checkNotDestroyed();
        return WebContentsImplJni.get().getFullscreenVideoSize(mNativeWebContentsAndroid);
    }

    @Override
    public void setSize(int width, int height) {
        checkNotDestroyed();
        WebContentsImplJni.get().setSize(mNativeWebContentsAndroid, width, height);
    }

    @Override
    public int getWidth() {
        checkNotDestroyed();
        return WebContentsImplJni.get().getWidth(mNativeWebContentsAndroid);
    }

    @Override
    public int getHeight() {
        checkNotDestroyed();
        return WebContentsImplJni.get().getHeight(mNativeWebContentsAndroid);
    }

    @CalledByNative
    private final void setMediaSession(MediaSessionImpl mediaSession) {
        mMediaSession = mediaSession;
    }

    @CalledByNative
    private static List<Bitmap> createBitmapList() {
        return new ArrayList<Bitmap>();
    }

    @CalledByNative
    private static void addToBitmapList(List<Bitmap> bitmaps, Bitmap bitmap) {
        bitmaps.add(bitmap);
    }

    @CalledByNative
    private static List<Rect> createSizeList() {
        return new ArrayList<Rect>();
    }

    @CalledByNative
    private static void createSizeAndAddToList(List<Rect> sizes, int width, int height) {
        sizes.add(new Rect(0, 0, width, height));
    }

    @CalledByNative
    private static Rect createSize(int width, int height) {
        return new Rect(0, 0, width, height);
    }

    /** Returns {@link RenderCoordinatesImpl}. */
    public RenderCoordinatesImpl getRenderCoordinates() {
        return mRenderCoordinates;
    }

    /**
     * Retrieves or stores a user data object for this WebContents.
     * @param key Class instance of the object used as the key.
     * @param userDataFactory Factory that creates an object of the generic class. A new object
     *        is created if it hasn't been created and non-null factory is given.
     * @return The created or retrieved user data object. Can be null if the object was
     *         not created yet, or {@code userDataFactory} is null, or the internal data
     *         storage is already garbage-collected.
     */
    public <T extends UserData> T getOrSetUserData(
            Class<T> key, UserDataFactory<T> userDataFactory) {
        // For tests that go without calling |initialize|.
        if (!mInitialized) return null;

        UserDataHost userDataHost = getUserDataHost();

        // Map can be null after WebView gets gc'ed on its way to destruction.
        if (userDataHost == null) {
            Log.d(TAG, "UserDataHost can't be found");
            return null;
        }

        T data = userDataHost.getUserData(key);
        if (data == null && userDataFactory != null) {
            assert userDataHost.getUserData(key) == null; // Do not allow overwriting

            T object = userDataFactory.create(this);
            assert key.isInstance(object);

            // Retrieves from the map again to return null in case |setUserData| fails
            // to store the object.
            data = userDataHost.setUserData(key, object);
        }
        return key.cast(data);
    }

    /** Convenience method to initialize test state. Only use for testing. */
    public void initializeForTesting() {
        if (mInternalsHolder == null) {
            mInternalsHolder = WebContents.createDefaultInternalsHolder();
        }
        WebContentsInternalsImpl internals = (WebContentsInternalsImpl) mInternalsHolder.get();
        if (internals == null) {
            internals = new WebContentsInternalsImpl();
            internals.userDataHost = new UserDataHost();
        }
        mInternalsHolder.set(internals);
        mInitialized = true;
    }

    /** Convenience method to set user data. Only use for testing. */
    public <T extends UserData> void setUserDataForTesting(Class<T> key, T userData) {
        // Be sure to call initializeForTesting() first.
        assert mInitialized;

        WebContentsInternalsImpl internals = (WebContentsInternalsImpl) mInternalsHolder.get();
        internals.userDataHost.setUserData(key, userData);
    }

    public <T extends UserData> void removeUserData(Class<T> key) {
        UserDataHost userDataHost = getUserDataHost();
        if (userDataHost == null) return;
        userDataHost.removeUserData(key);
    }

    /**
     * @return {@code UserDataHost} that contains internal user data. {@code null} if
     *         it is already gc'ed.
     */
    private UserDataHost getUserDataHost() {
        if (mInternalsHolder == null) return null;
        WebContentsInternals internals = mInternalsHolder.get();
        if (internals == null) return null;
        return ((WebContentsInternalsImpl) internals).userDataHost;
    }

    // WindowEventObserver

    @Override
    public void onRotationChanged(int rotation) {
        if (mNativeWebContentsAndroid == 0) return;
        int rotationDegrees = 0;
        switch (rotation) {
            case Surface.ROTATION_0:
                rotationDegrees = 0;
                break;
            case Surface.ROTATION_90:
                rotationDegrees = 90;
                break;
            case Surface.ROTATION_180:
                rotationDegrees = 180;
                break;
            case Surface.ROTATION_270:
                rotationDegrees = -90;
                break;
            default:
                throw new IllegalStateException(
                        "Display.getRotation() shouldn't return that value");
        }
        WebContentsImplJni.get()
                .sendOrientationChangeEvent(mNativeWebContentsAndroid, rotationDegrees);
    }

    @Override
    public void onDIPScaleChanged(float dipScale) {
        if (mNativeWebContentsAndroid == 0) return;
        mRenderCoordinates.setDeviceScaleFactor(dipScale);
        WebContentsImplJni.get().onScaleFactorChanged(mNativeWebContentsAndroid);
    }

    @Override
    public void setFocus(boolean hasFocus) {
        if (mNativeWebContentsAndroid == 0) return;
        WebContentsImplJni.get().setFocus(mNativeWebContentsAndroid, hasFocus);
    }

    @Override
    public void setDisplayCutoutSafeArea(Rect insets) {
        if (mNativeWebContentsAndroid == 0) return;
        WebContentsImplJni.get()
                .setDisplayCutoutSafeArea(
                        mNativeWebContentsAndroid,
                        insets.top,
                        insets.left,
                        insets.bottom,
                        insets.right);
    }

    @Override
    public void notifyRendererPreferenceUpdate() {
        if (mNativeWebContentsAndroid == 0) return;
        WebContentsImplJni.get().notifyRendererPreferenceUpdate(mNativeWebContentsAndroid);
    }

    @Override
    public void notifyBrowserControlsHeightChanged() {
        if (mNativeWebContentsAndroid == 0) return;
        WebContentsImplJni.get().notifyBrowserControlsHeightChanged(mNativeWebContentsAndroid);
    }

    @Override
    public void tearDownDialogOverlays() {
        if (mTearDownDialogOverlaysHandlers == null) return;
        Iterator<Runnable> it = mTearDownDialogOverlaysHandlers.iterator();
        while (it.hasNext()) {
            Runnable handler = it.next();
            handler.run();
        }
    }

    @Override
    public boolean needToFireBeforeUnloadOrUnloadEvents() {
        if (mNativeWebContentsAndroid == 0) return false;
        return WebContentsImplJni.get()
                .needToFireBeforeUnloadOrUnloadEvents(mNativeWebContentsAndroid);
    }

    public void addTearDownDialogOverlaysHandler(Runnable handler) {
        if (mTearDownDialogOverlaysHandlers == null) {
            mTearDownDialogOverlaysHandlers = new ObserverList<>();
        }

        assert !mTearDownDialogOverlaysHandlers.hasObserver(handler);
        mTearDownDialogOverlaysHandlers.addObserver(handler);
    }

    public void removeTearDownDialogOverlaysHandler(Runnable handler) {
        assert mTearDownDialogOverlaysHandlers != null;
        assert mTearDownDialogOverlaysHandlers.hasObserver(handler);

        mTearDownDialogOverlaysHandlers.removeObserver(handler);
    }

    @Override
    public void onContentForNavigationEntryShown() {
        checkNotDestroyed();
        WebContentsImplJni.get().onContentForNavigationEntryShown(mNativeWebContentsAndroid);
    }

    @Override
    @AnimationStage
    public int getCurrentBackForwardTransitionStage() {
        checkNotDestroyed();
        return WebContentsImplJni.get()
                .getCurrentBackForwardTransitionStage(mNativeWebContentsAndroid);
    }

    @Override
    public void setLongPressLinkSelectText(boolean enabled) {
        checkNotDestroyed();
        WebContentsImplJni.get().setLongPressLinkSelectText(mNativeWebContentsAndroid, enabled);
    }

    @Override
    public void notifyControlsConstraintsChanged(
            BrowserControlsOffsetTagsInfo oldOffsetTagsInfo,
            BrowserControlsOffsetTagsInfo offsetTagsInfo) {
        if (mNativeWebContentsAndroid == 0) return;
        WebContentsImplJni.get()
                .notifyControlsConstraintsChanged(
                        mNativeWebContentsAndroid, oldOffsetTagsInfo, offsetTagsInfo);
    }

    private void checkNotDestroyed() {
        if (mNativeWebContentsAndroid != 0) return;
        throw new IllegalStateException(
                "Native WebContents already destroyed", mNativeDestroyThrowable);
    }

    @Override
    public void captureContentAsBitmapForTesting(Callback<Bitmap> callback) {
        WebContentsImplJni.get()
                .captureContentAsBitmapForTesting(mNativeWebContentsAndroid, callback);
    }

    @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
    @NativeMethods
    public interface Natives {

        // This is static to avoid exposing a public destroy method on the native side of this
        // class.
        void destroyWebContents(long webContentsAndroidPtr);

        WebContents fromNativePtr(long webContentsAndroidPtr);

        void clearNativeReference(long nativeWebContentsAndroid);

        WindowAndroid getTopLevelNativeWindow(long nativeWebContentsAndroid);

        void setTopLevelNativeWindow(long nativeWebContentsAndroid, WindowAndroid windowAndroid);

        RenderFrameHost getMainFrame(long nativeWebContentsAndroid);

        RenderFrameHost getFocusedFrame(long nativeWebContentsAndroid);

        boolean isFocusedElementEditable(long nativeWebContentsAndroid);

        RenderFrameHost getRenderFrameHostFromId(
                long nativeWebContentsAndroid, int renderProcessId, int renderFrameId);

        RenderFrameHost[] getAllRenderFrameHosts(long nativeWebContentsAndroid);

        RenderWidgetHostViewImpl getRenderWidgetHostView(long nativeWebContentsAndroid);

        @Visibility
        int getVisibility(long nativeWebContentsAndroid);

        void updateWebContentsVisibility(long nativeWebContentsAndroid, int visibility);

        String getTitle(long nativeWebContentsAndroid);

        GURL getVisibleURL(long nativeWebContentsAndroid);

        int getVirtualKeyboardMode(long nativeWebContentsAndroid);

        String getEncoding(long nativeWebContentsAndroid);

        boolean isLoading(long nativeWebContentsAndroid);

        boolean shouldShowLoadingUI(long nativeWebContentsAndroid);

        boolean hasUncommittedNavigationInPrimaryMainFrame(long nativeWebContentsAndroid);

        void dispatchBeforeUnload(long nativeWebContentsAndroid, boolean autoCancel);

        void stop(long nativeWebContentsAndroid);

        void cut(long nativeWebContentsAndroid);

        void copy(long nativeWebContentsAndroid);

        void paste(long nativeWebContentsAndroid);

        void pasteAsPlainText(long nativeWebContentsAndroid);

        void replace(long nativeWebContentsAndroid, String word);

        void selectAll(long nativeWebContentsAndroid);

        void collapseSelection(long nativeWebContentsAndroid);

        void onHide(long nativeWebContentsAndroid);

        void onShow(long nativeWebContentsAndroid);

        void setImportance(long nativeWebContentsAndroid, int importance);

        void suspendAllMediaPlayers(long nativeWebContentsAndroid);

        void setAudioMuted(long nativeWebContentsAndroid, boolean mute);

        boolean isAudioMuted(long nativeWebContentsAndroid);

        boolean focusLocationBarByDefault(long nativeWebContentsAndroid);

        boolean isFullscreenForCurrentTab(long nativeWebContentsAndroid);

        void exitFullscreen(long nativeWebContentsAndroid);

        void scrollFocusedEditableNodeIntoView(long nativeWebContentsAndroid);

        void selectAroundCaret(
                long nativeWebContentsAndroid,
                int granularity,
                boolean shouldShowHandle,
                boolean shouldShowContextMenu,
                int startOffset,
                int endOffset,
                int surroundingTextLength);

        void adjustSelectionByCharacterOffset(
                long nativeWebContentsAndroid,
                int startAdjust,
                int endAdjust,
                boolean showSelectionMenu);

        GURL getLastCommittedURL(long nativeWebContentsAndroid);

        boolean isIncognito(long nativeWebContentsAndroid);

        void resumeLoadingCreatedWebContents(long nativeWebContentsAndroid);

        void evaluateJavaScript(
                long nativeWebContentsAndroid, String script, JavaScriptCallback callback);

        void evaluateJavaScriptForTests(
                long nativeWebContentsAndroid, String script, JavaScriptCallback callback);

        void addMessageToDevToolsConsole(long nativeWebContentsAndroid, int level, String message);

        void postMessageToMainFrame(
                long nativeWebContentsAndroid,
                MessagePayload payload,
                String sourceOrigin,
                String targetOrigin,
                MessagePort[] ports);

        boolean hasAccessedInitialDocument(long nativeWebContentsAndroid);

        boolean hasViewTransitionOptIn(long nativeWebContentsAndroid);

        int getThemeColor(long nativeWebContentsAndroid);

        int getBackgroundColor(long nativeWebContentsAndroid);

        float getLoadProgress(long nativeWebContentsAndroid);

        void requestSmartClipExtract(
                long nativeWebContentsAndroid,
                SmartClipCallback callback,
                int x,
                int y,
                int width,
                int height);

        void requestAccessibilitySnapshot(
                long nativeWebContentsAndroid,
                ViewStructure viewStructureRoot,
                ViewStructureBuilder viewStructureBuilder,
                Runnable doneCallback);

        void setOverscrollRefreshHandler(
                long nativeWebContentsAndroid,
                OverscrollRefreshHandler nativeOverscrollRefreshHandler);

        void setSpatialNavigationDisabled(long nativeWebContentsAndroid, boolean disabled);

        void setStylusHandwritingEnabled(long nativeWebContentsAndroid, boolean enabled);

        int downloadImage(
                long nativeWebContentsAndroid,
                GURL url,
                boolean isFavicon,
                int maxBitmapSize,
                boolean bypassCache,
                ImageDownloadCallback callback);

        void setHasPersistentVideo(long nativeWebContentsAndroid, boolean value);

        boolean hasActiveEffectivelyFullscreenVideo(long nativeWebContentsAndroid);

        boolean isPictureInPictureAllowedForFullscreenVideo(long nativeWebContentsAndroid);

        Rect getFullscreenVideoSize(long nativeWebContentsAndroid);

        void setSize(long nativeWebContentsAndroid, int width, int height);

        int getWidth(long nativeWebContentsAndroid);

        int getHeight(long nativeWebContentsAndroid);

        EventForwarder getOrCreateEventForwarder(long nativeWebContentsAndroid);

        void setViewAndroidDelegate(
                long nativeWebContentsAndroid, ViewAndroidDelegate viewDelegate);

        void sendOrientationChangeEvent(long nativeWebContentsAndroid, int orientation);

        void onScaleFactorChanged(long nativeWebContentsAndroid);

        void setFocus(long nativeWebContentsAndroid, boolean focused);

        void setDisplayCutoutSafeArea(
                long nativeWebContentsAndroid, int top, int left, int bottom, int right);

        void notifyRendererPreferenceUpdate(long nativeWebContentsAndroid);

        void notifyBrowserControlsHeightChanged(long nativeWebContentsAndroid);

        boolean isBeingDestroyed(long nativeWebContentsAndroid);

        boolean needToFireBeforeUnloadOrUnloadEvents(long nativeWebContentsAndroid);

        void onContentForNavigationEntryShown(long nativeWebContentsAndroid);

        @AnimationStage
        int getCurrentBackForwardTransitionStage(long nativeWebContentsAndroid);

        void setLongPressLinkSelectText(long nativeWebContentsAndroid, boolean enabled);

        void notifyControlsConstraintsChanged(
                long nativeWebContentsAndroid,
                BrowserControlsOffsetTagsInfo oldOffsetTagsInfo,
                BrowserControlsOffsetTagsInfo offsetTagsInfo);

        void captureContentAsBitmapForTesting(
                long nativeWebContentsAndroid, Callback<Bitmap> callback);
    }
}