chromium/android_webview/glue/java/src/com/android/webview/chromium/WebViewContentsClientAdapter.java

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

package com.android.webview.chromium;

import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Picture;
import android.net.Uri;
import android.net.http.SslError;
import android.os.Build;
import android.os.Handler;
import android.os.Message;
import android.view.KeyEvent;
import android.view.View;
import android.view.WindowManager;
import android.webkit.ClientCertRequest;
import android.webkit.ConsoleMessage;
import android.webkit.DownloadListener;
import android.webkit.GeolocationPermissions;
import android.webkit.JsDialogHelper;
import android.webkit.JsPromptResult;
import android.webkit.JsResult;
import android.webkit.PermissionRequest;
import android.webkit.RenderProcessGoneDetail;
import android.webkit.SslErrorHandler;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebResourceResponse;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.webkit.WebViewDelegate;

import org.chromium.android_webview.AwConsoleMessage;
import org.chromium.android_webview.AwContentsClient;
import org.chromium.android_webview.AwContentsClientBridge;
import org.chromium.android_webview.AwGeolocationPermissions;
import org.chromium.android_webview.AwHistogramRecorder;
import org.chromium.android_webview.AwHttpAuthHandler;
import org.chromium.android_webview.AwRenderProcessGoneDetail;
import org.chromium.android_webview.JsPromptResultReceiver;
import org.chromium.android_webview.JsResultReceiver;
import org.chromium.android_webview.R;
import org.chromium.android_webview.common.Lifetime;
import org.chromium.android_webview.permission.AwPermissionRequest;
import org.chromium.android_webview.permission.Resource;
import org.chromium.base.Callback;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.PathUtils;
import org.chromium.base.TraceEvent;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.metrics.ScopedSysTraceEvent;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.components.embedder_support.util.WebResourceResponseInfo;
import org.chromium.content_public.browser.util.DialogTypeRecorder;

import java.lang.ref.WeakReference;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.WeakHashMap;
import java.util.regex.Pattern;

/**
 * An adapter class that forwards the callbacks from {@link ContentViewClient}
 * to the appropriate {@link WebViewClient} or {@link WebChromeClient}.
 *
 * An instance of this class is associated with one {@link WebViewChromium}
 * instance. A WebViewChromium is a WebView implementation provider (that is
 * android.webkit.WebView delegates all functionality to it) and has exactly
 * one corresponding {@link ContentView} instance.
 *
 * A {@link ContentViewClient} may be shared between multiple {@link ContentView}s,
 * and hence multiple WebViews. Many WebViewClient methods pass the source
 * WebView as an argument. This means that we either need to pass the
 * corresponding ContentView to the corresponding ContentViewClient methods,
 * or use an instance of ContentViewClientAdapter per WebViewChromium, to
 * allow the source WebView to be injected by ContentViewClientAdapter. We
 * choose the latter, because it makes for a cleaner design.
 */
@Lifetime.WebView
class WebViewContentsClientAdapter extends SharedWebViewContentsClientAdapter {
    // The WebChromeClient instance that was passed to WebView.setContentViewClient().
    private WebChromeClient mWebChromeClient;
    // The listener receiving find-in-page API results.
    private WebView.FindListener mFindListener;
    // The listener receiving notifications of screen updates.
    private WebView.PictureListener mPictureListener;
    // Whether the picture listener is invalidate only (i.e. receives a null Picture)
    private boolean mPictureListenerInvalidateOnly;

    private DownloadListener mDownloadListener;

    private Handler mUiThreadHandler;

    private static final int NEW_WEBVIEW_CREATED = 100;

    private WeakHashMap<AwPermissionRequest, WeakReference<PermissionRequestAdapter>>
            mOngoingPermissionRequests;

    // Pattern to match URLs that WebView internally handles as asset or
    // resource lookups.
    private static final Pattern FILE_ANDROID_ASSET_PATTERN =
            Pattern.compile("^file:/*android_(asset|res).*");

    /**
     * Adapter constructor.
     *
     * @param webView the {@link WebView} instance that this adapter is serving.
     */
    @SuppressWarnings("HandlerLeak")
    WebViewContentsClientAdapter(
            WebView webView, Context context, WebViewDelegate webViewDelegate) {
        super(webView, webViewDelegate, context);
        try (ScopedSysTraceEvent event =
                ScopedSysTraceEvent.scoped("WebView.APICallback.WebViewClient.constructor")) {
            // See //android_webview/docs/how-does-on-create-window-work.md for more details.
            mUiThreadHandler =
                    new Handler() {
                        @Override
                        public void handleMessage(Message msg) {
                            switch (msg.what) {
                                case NEW_WEBVIEW_CREATED:
                                    WebView.WebViewTransport t = (WebView.WebViewTransport) msg.obj;
                                    WebView newWebView = t.getWebView();
                                    if (newWebView == mWebView) {
                                        throw new IllegalArgumentException(
                                                "Parent WebView cannot host its own popup window."
                                                        + " Please use"
                                                        + " WebSettings.setSupportMultipleWindows("
                                                        + "false)");
                                    }

                                    if (newWebView != null
                                            && newWebView.copyBackForwardList().getSize() != 0) {
                                        throw new IllegalArgumentException(
                                                "New WebView for popup window must not have been "
                                                        + " previously navigated.");
                                    }

                                    WebViewChromium.completeWindowCreation(mWebView, newWebView);
                                    break;
                                default:
                                    throw new IllegalStateException();
                            }
                        }
                    };
        }
    }

    void setWebChromeClient(WebChromeClient client) {
        mWebChromeClient = client;
    }

    WebChromeClient getWebChromeClient() {
        return mWebChromeClient;
    }

    void setDownloadListener(DownloadListener listener) {
        mDownloadListener = listener;
    }

    void setFindListener(WebView.FindListener listener) {
        mFindListener = listener;
    }

    void setPictureListener(WebView.PictureListener listener, boolean invalidateOnly) {
        mPictureListener = listener;
        mPictureListenerInvalidateOnly = invalidateOnly;
    }

    // --------------------------------------------------------------------------------------------
    //                        Adapter for all the methods.
    // --------------------------------------------------------------------------------------------

    /** @see AwContentsClient#getVisitedHistory. */
    @Override
    public void getVisitedHistory(Callback<String[]> callback) {
        try (TraceEvent event =
                TraceEvent.scoped("WebView.APICallback.WebViewClient.getVisitedHistory")) {
            AwHistogramRecorder.recordCallbackInvocation(
                    AwHistogramRecorder.WebViewCallbackType.GET_VISITED_HISTORY);
            if (mWebChromeClient != null) {
                if (TRACE) Log.i(TAG, "getVisitedHistory");
                mWebChromeClient.getVisitedHistory(
                        callback == null ? null : value -> callback.onResult(value));
            }
        }
    }

    /** @see AwContentsClient#doUpdateVisiteHistory(String, boolean) */
    @Override
    public void doUpdateVisitedHistory(String url, boolean isReload) {
        try (TraceEvent event =
                TraceEvent.scoped("WebView.APICallback.WebViewClient.doUpdateVisitedHistory")) {
            AwHistogramRecorder.recordCallbackInvocation(
                    AwHistogramRecorder.WebViewCallbackType.DO_UPDATE_VISITED_HISTORY);
            if (TRACE) Log.i(TAG, "doUpdateVisitedHistory=" + url + " reload=" + isReload);
            mWebViewClient.doUpdateVisitedHistory(mWebView, url, isReload);
        }
    }

    /** @see AwContentsClient#onProgressChanged(int) */
    @Override
    public void onProgressChanged(int progress) {
        try (TraceEvent event =
                TraceEvent.scoped("WebView.APICallback.WebViewClient.onProgressChanged")) {
            AwHistogramRecorder.recordCallbackInvocation(
                    AwHistogramRecorder.WebViewCallbackType.ON_PROGRESS_CHANGED);
            if (mWebChromeClient != null) {
                if (TRACE) Log.i(TAG, "onProgressChanged=" + progress);
                mWebChromeClient.onProgressChanged(mWebView, progress);
            }
        }
    }

    /** @see AwContentsClient#shouldInterceptRequest(java.lang.String) */
    @Override
    public WebResourceResponseInfo shouldInterceptRequest(AwWebResourceRequest request) {
        try (TraceEvent event =
                TraceEvent.scoped("WebView.APICallback.WebViewClient.shouldInterceptRequest")) {
            AwHistogramRecorder.recordCallbackInvocation(
                    AwHistogramRecorder.WebViewCallbackType.SHOULD_INTERCEPT_REQUEST);
            if (TRACE) Log.i(TAG, "shouldInterceptRequest=" + request.url);
            WebResourceResponse response =
                    mWebViewClient.shouldInterceptRequest(
                            mWebView, new WebResourceRequestAdapter(request));
            if (response == null) return null;

            return new WebResourceResponseInfo(
                    response.getMimeType(),
                    response.getEncoding(),
                    response.getData(),
                    response.getStatusCode(),
                    response.getReasonPhrase(),
                    response.getResponseHeaders());
        }
    }

    /** @see AwContentsClient#onUnhandledKeyEvent(android.view.KeyEvent) */
    @Override
    public void onUnhandledKeyEvent(KeyEvent event) {
        try (TraceEvent traceEvent =
                TraceEvent.scoped("WebView.APICallback.WebViewClient.onUnhandledKeyEvent")) {
            AwHistogramRecorder.recordCallbackInvocation(
                    AwHistogramRecorder.WebViewCallbackType.ON_UNHANDLED_KEY_EVENT);
            if (TRACE) Log.i(TAG, "onUnhandledKeyEvent");
            mWebViewClient.onUnhandledKeyEvent(mWebView, event);
        }
    }

    /** @see AwContentsClient#onConsoleMessage(android.webkit.ConsoleMessage) */
    @Override
    public boolean onConsoleMessage(AwConsoleMessage consoleMessage) {
        try (TraceEvent event =
                TraceEvent.scoped("WebView.APICallback.WebViewClient.onConsoleMessage")) {
            AwHistogramRecorder.recordCallbackInvocation(
                    AwHistogramRecorder.WebViewCallbackType.ON_CONSOLE_MESSAGE);
            boolean result;
            if (mWebChromeClient != null) {
                if (TRACE) Log.i(TAG, "onConsoleMessage: " + consoleMessage.message());
                result = mWebChromeClient.onConsoleMessage(fromAwConsoleMessage(consoleMessage));
            } else {
                result = false;
            }
            return result;
        }
    }

    /** @see AwContentsClient#onFindResultReceived(int,int,boolean) */
    @Override
    public void onFindResultReceived(
            int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting) {
        try (TraceEvent event =
                TraceEvent.scoped("WebView.APICallback.WebViewClient.onFindResultReceived")) {
            if (mFindListener == null) return;
            if (TRACE) Log.i(TAG, "onFindResultReceived");
            mFindListener.onFindResultReceived(activeMatchOrdinal, numberOfMatches, isDoneCounting);
        }
    }

    /** @See AwContentsClient#onNewPicture(Picture) */
    @Override
    public void onNewPicture(Picture picture) {
        try (TraceEvent event =
                TraceEvent.scoped("WebView.APICallback.WebViewClient.onNewPicture")) {
            if (mPictureListener == null) return;
            if (TRACE) Log.i(TAG, "onNewPicture");
            mPictureListener.onNewPicture(mWebView, picture);
        }
    }

    @Override
    public void onLoadResource(String url) {
        try (TraceEvent event =
                TraceEvent.scoped("WebView.APICallback.WebViewClient.onLoadResource")) {
            if (TRACE) Log.i(TAG, "onLoadResource=" + url);
            mWebViewClient.onLoadResource(mWebView, url);

            // Record UMA for onLoadResource.
            AwHistogramRecorder.recordCallbackInvocation(
                    AwHistogramRecorder.WebViewCallbackType.ON_LOAD_RESOURCE);
        }
    }

    @Override
    public boolean onCreateWindow(boolean isDialog, boolean isUserGesture) {
        try (TraceEvent event =
                TraceEvent.scoped("WebView.APICallback.WebViewClient.onCreateWindow")) {
            AwHistogramRecorder.recordCallbackInvocation(
                    AwHistogramRecorder.WebViewCallbackType.ON_CREATE_WINDOW);
            Message m =
                    mUiThreadHandler.obtainMessage(
                            NEW_WEBVIEW_CREATED, mWebView.new WebViewTransport());
            boolean result;
            if (mWebChromeClient != null) {
                if (TRACE) Log.i(TAG, "onCreateWindow");
                result = mWebChromeClient.onCreateWindow(mWebView, isDialog, isUserGesture, m);
            } else {
                result = false;
            }
            return result;
        }
    }

    /** @see AwContentsClient#onCloseWindow() */
    @Override
    public void onCloseWindow() {
        try (TraceEvent event =
                TraceEvent.scoped("WebView.APICallback.WebViewClient.onCloseWindow")) {
            AwHistogramRecorder.recordCallbackInvocation(
                    AwHistogramRecorder.WebViewCallbackType.ON_CLOSE_WINDOW);
            if (mWebChromeClient != null) {
                if (TRACE) Log.i(TAG, "onCloseWindow");
                mWebChromeClient.onCloseWindow(mWebView);
            }
        }
    }

    /** @see AwContentsClient#onRequestFocus() */
    @Override
    public void onRequestFocus() {
        try (TraceEvent event =
                TraceEvent.scoped("WebView.APICallback.WebViewClient.onRequestFocus")) {
            AwHistogramRecorder.recordCallbackInvocation(
                    AwHistogramRecorder.WebViewCallbackType.ON_REQUEST_FOCUS);
            if (mWebChromeClient != null) {
                if (TRACE) Log.i(TAG, "onRequestFocus");
                mWebChromeClient.onRequestFocus(mWebView);
            }
        }
    }

    /** @see AwContentsClient#onReceivedTouchIconUrl(String url, boolean precomposed) */
    @Override
    public void onReceivedTouchIconUrl(String url, boolean precomposed) {
        try (TraceEvent event =
                TraceEvent.scoped("WebView.APICallback.WebViewClient.onReceivedTouchIconUrl")) {
            AwHistogramRecorder.recordCallbackInvocation(
                    AwHistogramRecorder.WebViewCallbackType.ON_RECEIVED_TOUCH_ICON_URL);
            if (mWebChromeClient != null) {
                if (TRACE) Log.i(TAG, "onReceivedTouchIconUrl=" + url);
                mWebChromeClient.onReceivedTouchIconUrl(mWebView, url, precomposed);
            }
        }
    }

    /** @see AwContentsClient#onReceivedIcon(Bitmap bitmap) */
    @Override
    public void onReceivedIcon(Bitmap bitmap) {
        try (TraceEvent event =
                TraceEvent.scoped("WebView.APICallback.WebViewClient.onReceivedIcon")) {
            AwHistogramRecorder.recordCallbackInvocation(
                    AwHistogramRecorder.WebViewCallbackType.ON_RECEIVED_ICON);
            if (mWebChromeClient != null) {
                if (TRACE) Log.i(TAG, "onReceivedIcon");
                mWebChromeClient.onReceivedIcon(mWebView, bitmap);
            }
        }
    }

    /** @see ContentViewClient#onPageStarted(String) */
    @Override
    public void onPageStarted(String url) {
        try (TraceEvent event =
                TraceEvent.scoped("WebView.APICallback.WebViewClient.onPageStarted")) {
            if (TRACE) Log.i(TAG, "onPageStarted=" + url);
            mWebViewClient.onPageStarted(mWebView, url, mWebView.getFavicon());

            // Record UMA for onPageStarted.
            AwHistogramRecorder.recordCallbackInvocation(
                    AwHistogramRecorder.WebViewCallbackType.ON_PAGE_STARTED);
        }
    }

    /** @see ContentViewClient#onPageFinished(String) */
    @Override
    public void onPageFinished(String url) {
        try (TraceEvent event =
                TraceEvent.scoped("WebView.APICallback.WebViewClient.onPageFinished")) {
            if (TRACE) Log.i(TAG, "onPageFinished=" + url);
            mWebViewClient.onPageFinished(mWebView, url);

            // Record UMA for onPageFinished.
            AwHistogramRecorder.recordCallbackInvocation(
                    AwHistogramRecorder.WebViewCallbackType.ON_PAGE_FINISHED);

            // See b/8208948
            // This fakes an onNewPicture callback after onPageFinished to allow
            // CTS tests to run in an un-flaky manner. This is required as the
            // path for sending Picture updates in Chromium are decoupled from the
            // page loading callbacks, i.e. the Chrome compositor may draw our
            // content and send the Picture before onPageStarted or onPageFinished
            // are invoked. The CTS harness discards any pictures it receives before
            // onPageStarted is invoked, so in the case we get the Picture before that and
            // no further updates after onPageStarted, we'll fail the test by timing
            // out waiting for a Picture.
            if (mPictureListener != null) {
                PostTask.postDelayedTask(
                        TaskTraits.UI_DEFAULT,
                        () -> {
                            if (mPictureListener != null) {
                                if (TRACE) {
                                    Log.i(TAG, "onNewPicture - from onPageFinished workaround.");
                                }
                                mPictureListener.onNewPicture(
                                        mWebView,
                                        mPictureListenerInvalidateOnly ? null : new Picture());
                            }
                        },
                        100);
            }
        }
    }

    /** @see ContentViewClient#onReceivedTitle(String) */
    @Override
    public void onReceivedTitle(String title) {
        try (TraceEvent event =
                TraceEvent.scoped("WebView.APICallback.WebViewClient.onReceivedTitle")) {
            AwHistogramRecorder.recordCallbackInvocation(
                    AwHistogramRecorder.WebViewCallbackType.ON_RECEIVED_TITLE);
            if (mWebChromeClient != null) {
                if (TRACE) Log.i(TAG, "onReceivedTitle=\"" + title + "\"");
                mWebChromeClient.onReceivedTitle(mWebView, title);
            }
        }
    }

    /** @see ContentViewClient#shouldOverrideKeyEvent(KeyEvent) */
    @Override
    public boolean shouldOverrideKeyEvent(KeyEvent event) {
        try (TraceEvent traceEvent =
                TraceEvent.scoped("WebView.APICallback.WebViewClient.shouldOverrideKeyEvent")) {
            AwHistogramRecorder.recordCallbackInvocation(
                    AwHistogramRecorder.WebViewCallbackType.SHOULD_OVERRIDE_KEY_EVENT);
            if (TRACE) Log.i(TAG, "shouldOverrideKeyEvent");
            return mWebViewClient.shouldOverrideKeyEvent(mWebView, event);
        }
    }

    /**
     * Returns true if a method with a given name and parameters is declared in a subclass
     * of a given baseclass.
     */
    private static <T> boolean isMethodDeclaredInSubClass(
            Class<T> baseClass,
            Class<? extends T> subClass,
            String name,
            Class<?>... parameterTypes) {
        try {
            return !subClass.getMethod(name, parameterTypes).getDeclaringClass().equals(baseClass);
        } catch (SecurityException e) {
            return false;
        } catch (NoSuchMethodException e) {
            return false;
        }
    }

    @Override
    public void onGeolocationPermissionsShowPrompt(
            String origin, AwGeolocationPermissions.Callback callback) {
        try (TraceEvent traceEvent =
                TraceEvent.scoped(
                        "WebView.APICallback.WebViewClient.onGeolocationPermissionsShowPrompt")) {
            AwHistogramRecorder.recordCallbackInvocation(
                    AwHistogramRecorder.WebViewCallbackType.ON_GEOLOCATION_PERMISSIONS_SHOW_PROMPT);
            if (mWebChromeClient == null) {
                callback.invoke(origin, false, false);
                return;
            }
            if (!isMethodDeclaredInSubClass(
                    WebChromeClient.class,
                    mWebChromeClient.getClass(),
                    "onGeolocationPermissionsShowPrompt",
                    String.class,
                    GeolocationPermissions.Callback.class)) {
                // The default WebChromeClient.onGeolocationPermissionsShowPrompt() implementation
                // is a NOOP (does not invoke the callback). Explicitly invoke the callback in
                // chromium code to deny the permission.
                callback.invoke(origin, false, false);
                return;
            }
            if (TRACE) Log.i(TAG, "onGeolocationPermissionsShowPrompt");
            final long requestStartTime = System.currentTimeMillis();
            GeolocationPermissions.Callback callbackWrapper =
                    (callbackOrigin, allow, retain) -> {
                        RecordHistogram.recordTimesHistogram(
                                "Android.WebView.OnGeolocationPermissionsShowPrompt.ResponseTime",
                                System.currentTimeMillis() - requestStartTime);
                        RecordHistogram.recordBooleanHistogram(
                                "Android.WebView.OnGeolocationPermissionsShowPrompt.Allow", allow);
                        RecordHistogram.recordBooleanHistogram(
                                "Android.WebView.OnGeolocationPermissionsShowPrompt.Retain",
                                retain);
                        callback.invoke(callbackOrigin, allow, retain);
                    };
            mWebChromeClient.onGeolocationPermissionsShowPrompt(
                    origin, callback == null ? null : callbackWrapper);
        }
    }

    @Override
    public void onGeolocationPermissionsHidePrompt() {
        try (TraceEvent event =
                TraceEvent.scoped(
                        "WebView.APICallback.WebViewClient.onGeolocationPermissionsHidePrompt")) {
            AwHistogramRecorder.recordCallbackInvocation(
                    AwHistogramRecorder.WebViewCallbackType.ON_GEOLOCATION_PERMISSIONS_HIDE_PROMPT);
            if (mWebChromeClient != null) {
                if (TRACE) Log.i(TAG, "onGeolocationPermissionsHidePrompt");
                mWebChromeClient.onGeolocationPermissionsHidePrompt();
            }
        }
    }

    @Override
    public void onPermissionRequest(AwPermissionRequest permissionRequest) {
        try (TraceEvent event =
                TraceEvent.scoped("WebView.APICallback.WebViewClient.onPermissionRequest")) {
            AwHistogramRecorder.recordCallbackInvocation(
                    AwHistogramRecorder.WebViewCallbackType.ON_PERMISSION_REQUEST);
            if (mWebChromeClient != null) {
                if (TRACE) Log.i(TAG, "onPermissionRequest");
                if (mOngoingPermissionRequests == null) {
                    mOngoingPermissionRequests = new WeakHashMap<>();
                }
                PermissionRequestAdapter adapter = new PermissionRequestAdapter(permissionRequest);
                mOngoingPermissionRequests.put(permissionRequest, new WeakReference<>(adapter));
                mWebChromeClient.onPermissionRequest(adapter);
            } else {
                // By default, we deny the permission.
                permissionRequest.deny();
            }
        }
    }

    @Override
    public void onPermissionRequestCanceled(AwPermissionRequest permissionRequest) {
        try (TraceEvent event =
                TraceEvent.scoped(
                        "WebView.APICallback.WebViewClient.onPermissionRequestCanceled")) {
            AwHistogramRecorder.recordCallbackInvocation(
                    AwHistogramRecorder.WebViewCallbackType.ON_PERMISSION_REQUEST_CANCELED);
            if (mWebChromeClient != null && mOngoingPermissionRequests != null) {
                if (TRACE) Log.i(TAG, "onPermissionRequestCanceled");
                WeakReference<PermissionRequestAdapter> weakRef =
                        mOngoingPermissionRequests.get(permissionRequest);
                // We don't hold strong reference to PermissionRequestAdpater and don't expect the
                // user only holds weak reference to it either, if so, user has no way to call
                // grant()/deny(), and no need to be notified the cancellation of request.
                if (weakRef != null) {
                    PermissionRequestAdapter adapter = weakRef.get();
                    if (adapter != null) mWebChromeClient.onPermissionRequestCanceled(adapter);
                }
            }
        }
    }

    private static class JsPromptResultReceiverAdapter implements JsResult.ResultReceiver {
        private JsPromptResultReceiver mChromePromptResultReceiver;
        private JsResultReceiver mChromeResultReceiver;
        // We hold onto the JsPromptResult here, just to avoid the need to downcast
        // in onJsResultComplete.
        private final JsPromptResult mPromptResult = new JsPromptResult(this);

        public JsPromptResultReceiverAdapter(JsPromptResultReceiver receiver) {
            mChromePromptResultReceiver = receiver;
        }

        public JsPromptResultReceiverAdapter(JsResultReceiver receiver) {
            mChromeResultReceiver = receiver;
        }

        public JsPromptResult getPromptResult() {
            return mPromptResult;
        }

        @Override
        public void onJsResultComplete(JsResult result) {
            if (mChromePromptResultReceiver != null) {
                if (mPromptResult.getResult()) {
                    mChromePromptResultReceiver.confirm(mPromptResult.getStringResult());
                } else {
                    mChromePromptResultReceiver.cancel();
                }
            } else {
                if (mPromptResult.getResult()) {
                    mChromeResultReceiver.confirm();
                } else {
                    mChromeResultReceiver.cancel();
                }
            }
        }
    }

    @Override
    public void handleJsAlert(String url, String message, JsResultReceiver receiver) {
        try (TraceEvent event =
                TraceEvent.scoped("WebView.APICallback.WebViewClient.handleJsAlert")) {
            AwHistogramRecorder.recordCallbackInvocation(
                    AwHistogramRecorder.WebViewCallbackType.ON_JS_ALERT);
            if (mWebChromeClient != null) {
                final JsPromptResult res =
                        new JsPromptResultReceiverAdapter(receiver).getPromptResult();
                if (TRACE) Log.i(TAG, "onJsAlert");
                if (!mWebChromeClient.onJsAlert(mWebView, url, message, res)) {
                    if (!showDefaultJsDialog(res, JsDialogHelper.ALERT, null, message, url)) {
                        receiver.cancel();
                    }
                }
            } else {
                receiver.cancel();
            }
        }
    }

    @Override
    public void handleJsBeforeUnload(String url, String message, JsResultReceiver receiver) {
        try (TraceEvent event =
                TraceEvent.scoped("WebView.APICallback.WebViewClient.handleJsBeforeUnload")) {
            AwHistogramRecorder.recordCallbackInvocation(
                    AwHistogramRecorder.WebViewCallbackType.ON_JS_BEFORE_UNLOAD);
            if (mWebChromeClient != null) {
                final JsPromptResult res =
                        new JsPromptResultReceiverAdapter(receiver).getPromptResult();
                if (TRACE) Log.i(TAG, "onJsBeforeUnload");
                if (!mWebChromeClient.onJsBeforeUnload(mWebView, url, message, res)) {
                    if (!showDefaultJsDialog(res, JsDialogHelper.UNLOAD, null, message, url)) {
                        receiver.cancel();
                    }
                }
            } else {
                receiver.cancel();
            }
        }
    }

    @Override
    public void handleJsConfirm(String url, String message, JsResultReceiver receiver) {
        try (TraceEvent event =
                TraceEvent.scoped("WebView.APICallback.WebViewClient.handleJsConfirm")) {
            AwHistogramRecorder.recordCallbackInvocation(
                    AwHistogramRecorder.WebViewCallbackType.ON_JS_CONFIRM);
            if (mWebChromeClient != null) {
                final JsPromptResult res =
                        new JsPromptResultReceiverAdapter(receiver).getPromptResult();
                if (TRACE) Log.i(TAG, "onJsConfirm");
                if (!mWebChromeClient.onJsConfirm(mWebView, url, message, res)) {
                    if (!showDefaultJsDialog(res, JsDialogHelper.CONFIRM, null, message, url)) {
                        receiver.cancel();
                    }
                }
            } else {
                receiver.cancel();
            }
        }
    }

    @Override
    public void handleJsPrompt(
            String url, String message, String defaultValue, JsPromptResultReceiver receiver) {
        try (TraceEvent event =
                TraceEvent.scoped("WebView.APICallback.WebViewClient.handleJsPrompt")) {
            AwHistogramRecorder.recordCallbackInvocation(
                    AwHistogramRecorder.WebViewCallbackType.ON_JS_PROMPT);
            if (mWebChromeClient != null) {
                final JsPromptResult res =
                        new JsPromptResultReceiverAdapter(receiver).getPromptResult();
                if (TRACE) Log.i(TAG, "onJsPrompt");
                if (!mWebChromeClient.onJsPrompt(mWebView, url, message, defaultValue, res)) {
                    if (!showDefaultJsDialog(
                            res, JsDialogHelper.PROMPT, defaultValue, message, url)) {
                        receiver.cancel();
                    }
                }
            } else {
                receiver.cancel();
            }
        }
    }

    /** Try to show the default JS dialog and return whether the dialog was shown. */
    private boolean showDefaultJsDialog(
            JsPromptResult res, int jsDialogType, String defaultValue, String message, String url) {
        // Note we must unwrap the Context here due to JsDialogHelper only using instanceof to
        // check if a Context is an Activity.
        Context activityContext = ContextUtils.activityFromContext(mContext);
        if (activityContext == null) {
            Log.w(TAG, "Unable to create JsDialog without an Activity");
            return false;
        }
        try {
            new JsDialogHelper(res, jsDialogType, defaultValue, message, url)
                    .showDialog(activityContext);
            DialogTypeRecorder.recordDialogType(DialogTypeRecorder.DialogType.JS_POPUP);
        } catch (WindowManager.BadTokenException e) {
            Log.w(
                    TAG,
                    "Unable to create JsDialog. Has this WebView outlived the Activity it was"
                            + " created with?");
            return false;
        }
        return true;
    }

    @Override
    public void onReceivedHttpAuthRequest(AwHttpAuthHandler handler, String host, String realm) {
        try (TraceEvent event =
                TraceEvent.scoped("WebView.APICallback.WebViewClient.onReceivedHttpAuthRequest")) {
            AwHistogramRecorder.recordCallbackInvocation(
                    AwHistogramRecorder.WebViewCallbackType.ON_RECEIVED_HTTP_AUTH_REQUEST);
            if (TRACE) Log.i(TAG, "onReceivedHttpAuthRequest=" + host);
            mWebViewClient.onReceivedHttpAuthRequest(
                    mWebView, new AwHttpAuthHandlerAdapter(handler), host, realm);
        }
    }

    @Override
    @SuppressWarnings("HandlerLeak")
    public void onReceivedSslError(final Callback<Boolean> callback, SslError error) {
        try (TraceEvent event =
                TraceEvent.scoped("WebView.APICallback.WebViewClient.onReceivedSslError")) {
            AwHistogramRecorder.recordCallbackInvocation(
                    AwHistogramRecorder.WebViewCallbackType.ON_RECEIVED_SSL_ERROR);
            SslErrorHandler handler =
                    new SslErrorHandler() {
                        @Override
                        public void proceed() {
                            callback.onResult(true);
                        }

                        @Override
                        public void cancel() {
                            callback.onResult(false);
                        }
                    };
            if (TRACE) Log.i(TAG, "onReceivedSslError");
            mWebViewClient.onReceivedSslError(mWebView, handler, error);
        }
    }

    private static class ClientCertRequestImpl extends ClientCertRequest {
        private final AwContentsClientBridge.ClientCertificateRequestCallback mCallback;
        private final String[] mKeyTypes;
        private final Principal[] mPrincipals;
        private final String mHost;
        private final int mPort;

        public ClientCertRequestImpl(
                AwContentsClientBridge.ClientCertificateRequestCallback callback,
                String[] keyTypes,
                Principal[] principals,
                String host,
                int port) {
            mCallback = callback;
            mKeyTypes = keyTypes;
            mPrincipals = principals;
            mHost = host;
            mPort = port;
        }

        @Override
        public String[] getKeyTypes() {
            // This is already a copy of native argument, so return directly.
            return mKeyTypes;
        }

        @Override
        public Principal[] getPrincipals() {
            // This is already a copy of native argument, so return directly.
            return mPrincipals;
        }

        @Override
        public String getHost() {
            return mHost;
        }

        @Override
        public int getPort() {
            return mPort;
        }

        @Override
        public void proceed(final PrivateKey privateKey, final X509Certificate[] chain) {
            mCallback.proceed(privateKey, chain);
        }

        @Override
        public void ignore() {
            mCallback.ignore();
        }

        @Override
        public void cancel() {
            mCallback.cancel();
        }
    }

    @Override
    public void onReceivedClientCertRequest(
            AwContentsClientBridge.ClientCertificateRequestCallback callback,
            String[] keyTypes,
            Principal[] principals,
            String host,
            int port) {
        if (TRACE) Log.i(TAG, "onReceivedClientCertRequest");
        try (TraceEvent event =
                TraceEvent.scoped(
                        "WebView.APICallback.WebViewClient.onReceivedClientCertRequest")) {
            AwHistogramRecorder.recordCallbackInvocation(
                    AwHistogramRecorder.WebViewCallbackType.ON_RECEIVED_CLIENT_CERT_REQUEST);
            final ClientCertRequestImpl request =
                    new ClientCertRequestImpl(callback, keyTypes, principals, host, port);
            mWebViewClient.onReceivedClientCertRequest(mWebView, request);
        }
    }

    @Override
    public void onReceivedLoginRequest(String realm, String account, String args) {
        try (TraceEvent event =
                TraceEvent.scoped("WebView.APICallback.WebViewClient.onReceivedLoginRequest")) {
            AwHistogramRecorder.recordCallbackInvocation(
                    AwHistogramRecorder.WebViewCallbackType.ON_RECEIVED_LOGIN_REQUEST);
            if (TRACE) Log.i(TAG, "onReceivedLoginRequest=" + realm);
            mWebViewClient.onReceivedLoginRequest(mWebView, realm, account, args);
        }
    }

    @Override
    public void onFormResubmission(Message dontResend, Message resend) {
        try (TraceEvent event =
                TraceEvent.scoped("WebView.APICallback.WebViewClient.onFormResubmission")) {
            AwHistogramRecorder.recordCallbackInvocation(
                    AwHistogramRecorder.WebViewCallbackType.ON_FORM_RESUBMISSION);
            if (TRACE) Log.i(TAG, "onFormResubmission");
            mWebViewClient.onFormResubmission(mWebView, dontResend, resend);
        }
    }

    @Override
    public void onDownloadStart(
            String url,
            String userAgent,
            String contentDisposition,
            String mimeType,
            long contentLength) {
        try (TraceEvent event =
                TraceEvent.scoped("WebView.APICallback.WebViewClient.onDownloadStart")) {
            if (mDownloadListener != null) {
                if (TRACE) Log.i(TAG, "onDownloadStart");
                mDownloadListener.onDownloadStart(
                        url, userAgent, contentDisposition, mimeType, contentLength);
            }
        }
    }

    @Override
    public void showFileChooser(
            final Callback<String[]> uploadFileCallback,
            final AwContentsClient.FileChooserParamsImpl fileChooserParams) {
        try (TraceEvent event =
                TraceEvent.scoped("WebView.APICallback.WebViewClient.showFileChooser")) {
            if (mWebChromeClient == null) {
                uploadFileCallback.onResult(null);
                return;
            }
            if (TRACE) Log.i(TAG, "showFileChooser");
            ValueCallback<Uri[]> callbackAdapter =
                    new ValueCallback<Uri[]>() {
                        private boolean mCompleted;

                        @Override
                        public void onReceiveValue(Uri[] uriList) {
                            if (mCompleted) {
                                throw new IllegalStateException(
                                        "showFileChooser result was already called");
                            }
                            mCompleted = true;
                            String[] s = null;
                            if (uriList != null) {
                                s = new String[uriList.length];
                                for (int i = 0; i < uriList.length; i++) {
                                    s[i] = uriList[i].toString();
                                    if ("file".equals(uriList[i].getScheme())
                                            && !FILE_ANDROID_ASSET_PATTERN
                                                    .matcher(s[i])
                                                    .matches()) {
                                        RecordHistogram.recordBooleanHistogram(
                                                "Android.WebView.FileChooserResultOutsideAppDataDir",
                                                PathUtils.isPathUnderAppDir(
                                                        uriList[i].getSchemeSpecificPart(),
                                                        mContext));
                                    }
                                }
                            }
                            uploadFileCallback.onResult(s);
                        }
                    };

            // Invoke the new callback introduced in Lollipop. If the app handles
            // it, we're done here.
            if (mWebChromeClient.onShowFileChooser(
                    mWebView, callbackAdapter, fromAwFileChooserParams(fileChooserParams))) {
                return;
            }

            // If the app did not handle it and we are running on Lollipop or newer, then
            // abort.
            if (mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.LOLLIPOP) {
                uploadFileCallback.onResult(null);
                return;
            }

            // Otherwise, for older apps, attempt to invoke the legacy (hidden) API for
            // backwards compatibility.
            ValueCallback<Uri> innerCallback =
                    new ValueCallback<Uri>() {
                        private boolean mCompleted;

                        @Override
                        public void onReceiveValue(Uri uri) {
                            if (mCompleted) {
                                throw new IllegalStateException(
                                        "showFileChooser result was already called");
                            }
                            mCompleted = true;
                            uploadFileCallback.onResult(
                                    uri == null ? null : new String[] {uri.toString()});
                        }
                    };
            if (TRACE) Log.i(TAG, "openFileChooser");
            mWebChromeClient.openFileChooser(
                    innerCallback,
                    fileChooserParams.getAcceptTypesString(),
                    fileChooserParams.isCaptureEnabled() ? "*" : "");
        }
    }

    @Override
    public void onScaleChangedScaled(float oldScale, float newScale) {
        try (TraceEvent event =
                TraceEvent.scoped("WebView.APICallback.WebViewClient.onScaleChangedScaled")) {
            AwHistogramRecorder.recordCallbackInvocation(
                    AwHistogramRecorder.WebViewCallbackType.ON_SCALE_CHANGED);
            if (TRACE) Log.i(TAG, " onScaleChangedScaled");
            mWebViewClient.onScaleChanged(mWebView, oldScale, newScale);
        }
    }

    @Override
    public void onShowCustomView(View view, final CustomViewCallback cb) {
        try (TraceEvent event =
                TraceEvent.scoped("WebView.APICallback.WebViewClient.onShowCustomView")) {
            AwHistogramRecorder.recordCallbackInvocation(
                    AwHistogramRecorder.WebViewCallbackType.ON_SHOW_CUSTOM_VIEW);
            if (mWebChromeClient != null) {
                if (TRACE) Log.i(TAG, "onShowCustomView");
                mWebChromeClient.onShowCustomView(
                        view, cb == null ? null : () -> cb.onCustomViewHidden());
            }
        }
    }

    @Override
    public void onHideCustomView() {
        try (TraceEvent event =
                TraceEvent.scoped("WebView.APICallback.WebViewClient.onHideCustomView")) {
            AwHistogramRecorder.recordCallbackInvocation(
                    AwHistogramRecorder.WebViewCallbackType.ON_HIDE_CUSTOM_VIEW);
            if (mWebChromeClient != null) {
                if (TRACE) Log.i(TAG, "onHideCustomView");
                mWebChromeClient.onHideCustomView();
            }
        }
    }

    @Override
    protected View getVideoLoadingProgressView() {
        try (TraceEvent event =
                TraceEvent.scoped(
                        "WebView.APICallback.WebViewClient.getVideoLoadingProgressView")) {
            View result;
            if (mWebChromeClient != null) {
                if (TRACE) Log.i(TAG, "getVideoLoadingProgressView");
                result = mWebChromeClient.getVideoLoadingProgressView();
            } else {
                result = null;
            }
            return result;
        }
    }

    @Override
    public Bitmap getDefaultVideoPoster() {
        try (TraceEvent event =
                TraceEvent.scoped("WebView.APICallback.WebViewClient.getDefaultVideoPoster")) {
            AwHistogramRecorder.recordCallbackInvocation(
                    AwHistogramRecorder.WebViewCallbackType.GET_DEFAULT_VIDEO_POSTER);
            Bitmap result = null;
            if (mWebChromeClient != null) {
                if (TRACE) Log.i(TAG, "getDefaultVideoPoster");
                result = mWebChromeClient.getDefaultVideoPoster();
            }
            if (result == null) {
                Bitmap poster =
                        BitmapFactory.decodeResource(
                                mContext.getResources(),
                                R.drawable.ic_play_circle_outline_black_48dp);

                // WebView relies on the application's resources from the context we have.
                // If the application does anything to change how these resources work,
                // this could result in us failing to retrieve the bitmap.
                // It is not a fix, and we could still run into other problems, but we
                // will fall back to an empty Bitmap rather than try use the resource we
                // couldn't retrieve to try to help apps that may run into this problem.
                // See crbug.com/329106309 for more information.
                if (poster != null) {
                    // The ic_play_circle_outline_black_48dp icon is transparent so we need to draw
                    // it on a gray background.
                    result =
                            Bitmap.createBitmap(
                                    poster.getWidth(), poster.getHeight(), poster.getConfig());
                    result.eraseColor(Color.GRAY);
                    Canvas canvas = new Canvas(result);
                    canvas.drawBitmap(poster, 0f, 0f, null);
                } else {
                    Log.w(TAG, "Unable to retrieve default video poster from resources");
                    result =
                            Bitmap.createBitmap(
                                    new int[] {Color.TRANSPARENT}, 1, 1, Bitmap.Config.ARGB_8888);
                }
            }
            return result;
        }
    }

    @Override
    public boolean onRenderProcessGone(final AwRenderProcessGoneDetail detail) {
        try (TraceEvent event =
                TraceEvent.scoped("WebView.APICallback.WebViewClient.onRenderProcessGone")) {
            AwHistogramRecorder.recordCallbackInvocation(
                    AwHistogramRecorder.WebViewCallbackType.ON_RENDER_PROCESS_GONE);
            return mWebViewClient.onRenderProcessGone(
                    mWebView,
                    new RenderProcessGoneDetail() {
                        @Override
                        public boolean didCrash() {
                            return detail.didCrash();
                        }

                        @Override
                        @SuppressWarnings("WrongConstant") // https://crbug.com/1509716
                        public int rendererPriorityAtExit() {
                            return detail.rendererPriority();
                        }
                    });
        }
    }

    private static class AwHttpAuthHandlerAdapter extends android.webkit.HttpAuthHandler {
        private AwHttpAuthHandler mAwHandler;

        public AwHttpAuthHandlerAdapter(AwHttpAuthHandler awHandler) {
            mAwHandler = awHandler;
        }

        @Override
        public void proceed(String username, String password) {
            if (username == null) {
                username = "";
            }

            if (password == null) {
                password = "";
            }
            mAwHandler.proceed(username, password);
        }

        @Override
        public void cancel() {
            mAwHandler.cancel();
        }

        @Override
        public boolean useHttpAuthUsernamePassword() {
            return mAwHandler.isFirstAttempt();
        }
    }

    /** Type adaptation class for PermissionRequest. */
    public static class PermissionRequestAdapter extends PermissionRequest {

        private static long toAwPermissionResources(String[] resources) {
            long result = 0;
            for (String resource : resources) {
                if (resource.equals(PermissionRequest.RESOURCE_VIDEO_CAPTURE)) {
                    result |= Resource.VIDEO_CAPTURE;
                } else if (resource.equals(PermissionRequest.RESOURCE_AUDIO_CAPTURE)) {
                    result |= Resource.AUDIO_CAPTURE;
                } else if (resource.equals(PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID)) {
                    result |= Resource.PROTECTED_MEDIA_ID;
                } else if (resource.equals(PermissionRequest.RESOURCE_MIDI_SYSEX)) {
                    result |= Resource.MIDI_SYSEX;
                }
            }
            return result;
        }

        private static String[] toPermissionResources(long resources) {
            ArrayList<String> result = new ArrayList<String>();
            if ((resources & Resource.VIDEO_CAPTURE) != 0) {
                result.add(PermissionRequest.RESOURCE_VIDEO_CAPTURE);
            }
            if ((resources & Resource.AUDIO_CAPTURE) != 0) {
                result.add(PermissionRequest.RESOURCE_AUDIO_CAPTURE);
            }
            if ((resources & Resource.PROTECTED_MEDIA_ID) != 0) {
                result.add(PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID);
            }
            if ((resources & Resource.MIDI_SYSEX) != 0) {
                result.add(PermissionRequest.RESOURCE_MIDI_SYSEX);
            }
            String[] resource_array = new String[result.size()];
            return result.toArray(resource_array);
        }

        private AwPermissionRequest mAwPermissionRequest;
        private final String[] mResources;

        private final long mCreationTime;

        public PermissionRequestAdapter(AwPermissionRequest awPermissionRequest) {
            assert awPermissionRequest != null;
            mAwPermissionRequest = awPermissionRequest;
            mResources = toPermissionResources(mAwPermissionRequest.getResources());
            mCreationTime = System.currentTimeMillis();
            RecordHistogram.recordCount100Histogram(
                    "Android.WebView.OnPermissionRequest.RequestedResourceCount",
                    mResources.length);
            // The resources result is a bitmask of size 2^5 (32 distinct values).
            RecordHistogram.recordSparseHistogram(
                    "Android.WebView.OnPermissionRequest.RequestedResources",
                    (int) mAwPermissionRequest.getResources());
        }

        @Override
        public Uri getOrigin() {
            return mAwPermissionRequest.getOrigin();
        }

        @Override
        public String[] getResources() {
            return mResources.clone();
        }

        @Override
        public void grant(String[] resources) {
            recordResponseTime();
            long requestedResource = mAwPermissionRequest.getResources();
            if ((requestedResource & toAwPermissionResources(resources)) == requestedResource) {
                recordPermissionResult(true);
                mAwPermissionRequest.grant();
            } else {
                recordPermissionResult(false);
                mAwPermissionRequest.deny();
            }
        }

        @Override
        public void deny() {
            recordResponseTime();
            recordPermissionResult(false);
            mAwPermissionRequest.deny();
        }

        private void recordPermissionResult(boolean granted) {
            RecordHistogram.recordBooleanHistogram(
                    "Android.WebView.OnPermissionRequest.Granted", granted);
        }

        /** Record the response time from the app to a histogram. */
        private void recordResponseTime() {
            long duration = System.currentTimeMillis() - mCreationTime;
            RecordHistogram.recordTimesHistogram(
                    "Android.WebView.OnPermissionRequest.ResponseTime", duration);
        }
    }

    public static WebChromeClient.FileChooserParams fromAwFileChooserParams(
            final AwContentsClient.FileChooserParamsImpl value) {
        if (value == null) {
            return null;
        }
        return new WebChromeClient.FileChooserParams() {
            @Override
            public int getMode() {
                return value.getMode();
            }

            @Override
            public String[] getAcceptTypes() {
                return value.getAcceptTypes();
            }

            @Override
            public boolean isCaptureEnabled() {
                return value.isCaptureEnabled();
            }

            @Override
            public CharSequence getTitle() {
                return value.getTitle();
            }

            @Override
            public String getFilenameHint() {
                return value.getFilenameHint();
            }

            @Override
            public Intent createIntent() {
                return value.createIntent();
            }
        };
    }

    private static ConsoleMessage fromAwConsoleMessage(AwConsoleMessage value) {
        if (value == null) {
            return null;
        }
        return new ConsoleMessage(
                value.message(),
                value.sourceId(),
                value.lineNumber(),
                fromAwMessageLevel(value.messageLevel()));
    }

    private static ConsoleMessage.MessageLevel fromAwMessageLevel(
            @AwConsoleMessage.MessageLevel int value) {
        switch (value) {
            case AwConsoleMessage.MESSAGE_LEVEL_TIP:
                return ConsoleMessage.MessageLevel.TIP;
            case AwConsoleMessage.MESSAGE_LEVEL_LOG:
                return ConsoleMessage.MessageLevel.LOG;
            case AwConsoleMessage.MESSAGE_LEVEL_WARNING:
                return ConsoleMessage.MessageLevel.WARNING;
            case AwConsoleMessage.MESSAGE_LEVEL_ERROR:
                return ConsoleMessage.MessageLevel.ERROR;
            case AwConsoleMessage.MESSAGE_LEVEL_DEBUG:
                return ConsoleMessage.MessageLevel.DEBUG;
            default:
                throw new IllegalArgumentException("Unsupported value: " + value);
        }
    }
}