chromium/android_webview/java/src/org/chromium/android_webview/AwContentsClientCallbackHelper.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.android_webview;

import android.graphics.Picture;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;

import org.chromium.android_webview.common.Lifetime;
import org.chromium.android_webview.safe_browsing.AwSafeBrowsingResponse;
import org.chromium.base.Callback;
import org.chromium.components.embedder_support.util.WebResourceResponseInfo;

import java.util.concurrent.Callable;

/**
 * This class is responsible for calling certain client callbacks on the UI thread.
 *
 * Most callbacks do no go through here, but get forwarded to AwContentsClient directly. The
 * messages processed here may originate from the IO or UI thread.
 */
@Lifetime.WebView
public class AwContentsClientCallbackHelper {
    /** Interface to tell CallbackHelper to cancel posted callbacks. */
    public static interface CancelCallbackPoller {
        boolean shouldCancelAllCallbacks();
    }

    // TODO(boliu): Consider removing DownloadInfo and LoginRequestInfo by using native
    // MessageLoop to post directly to AwContents.

    private static class DownloadInfo {
        final String mUrl;
        final String mUserAgent;
        final String mContentDisposition;
        final String mMimeType;
        final long mContentLength;

        DownloadInfo(
                String url,
                String userAgent,
                String contentDisposition,
                String mimeType,
                long contentLength) {
            mUrl = url;
            mUserAgent = userAgent;
            mContentDisposition = contentDisposition;
            mMimeType = mimeType;
            mContentLength = contentLength;
        }
    }

    private static class LoginRequestInfo {
        final String mRealm;
        final String mAccount;
        final String mArgs;

        LoginRequestInfo(String realm, String account, String args) {
            mRealm = realm;
            mAccount = account;
            mArgs = args;
        }
    }

    private static class OnReceivedErrorInfo {
        final AwContentsClient.AwWebResourceRequest mRequest;
        final AwContentsClient.AwWebResourceError mError;

        OnReceivedErrorInfo(
                AwContentsClient.AwWebResourceRequest request,
                AwContentsClient.AwWebResourceError error) {
            mRequest = request;
            mError = error;
        }
    }

    private static class OnSafeBrowsingHitInfo {
        final AwContentsClient.AwWebResourceRequest mRequest;
        final int mThreatType;
        final Callback<AwSafeBrowsingResponse> mCallback;

        OnSafeBrowsingHitInfo(
                AwContentsClient.AwWebResourceRequest request,
                int threatType,
                Callback<AwSafeBrowsingResponse> callback) {
            mRequest = request;
            mThreatType = threatType;
            mCallback = callback;
        }
    }

    private static class OnReceivedHttpErrorInfo {
        final AwContentsClient.AwWebResourceRequest mRequest;
        final WebResourceResponseInfo mResponse;

        OnReceivedHttpErrorInfo(
                AwContentsClient.AwWebResourceRequest request, WebResourceResponseInfo response) {
            mRequest = request;
            mResponse = response;
        }
    }

    private static class DoUpdateVisitedHistoryInfo {
        final String mUrl;
        final boolean mIsReload;

        DoUpdateVisitedHistoryInfo(String url, boolean isReload) {
            mUrl = url;
            mIsReload = isReload;
        }
    }

    private static class OnFormResubmissionInfo {
        final Message mDontResend;
        final Message mResend;

        OnFormResubmissionInfo(Message dontResend, Message resend) {
            mDontResend = dontResend;
            mResend = resend;
        }
    }

    private static final int MSG_ON_LOAD_RESOURCE = 1;
    private static final int MSG_ON_PAGE_STARTED = 2;
    private static final int MSG_ON_DOWNLOAD_START = 3;
    private static final int MSG_ON_RECEIVED_LOGIN_REQUEST = 4;
    private static final int MSG_ON_RECEIVED_ERROR = 5;
    private static final int MSG_ON_NEW_PICTURE = 6;
    private static final int MSG_ON_SCALE_CHANGED_SCALED = 7;
    private static final int MSG_ON_RECEIVED_HTTP_ERROR = 8;
    private static final int MSG_ON_PAGE_FINISHED = 9;
    private static final int MSG_ON_RECEIVED_TITLE = 10;
    private static final int MSG_ON_PROGRESS_CHANGED = 11;
    private static final int MSG_SYNTHESIZE_PAGE_LOADING = 12;
    private static final int MSG_DO_UPDATE_VISITED_HISTORY = 13;
    private static final int MSG_ON_FORM_RESUBMISSION = 14;
    private static final int MSG_ON_SAFE_BROWSING_HIT = 15;

    // Minimum period allowed between consecutive onNewPicture calls, to rate-limit the callbacks.
    private static final long ON_NEW_PICTURE_MIN_PERIOD_MILLIS = 500;
    // Timestamp of the most recent onNewPicture callback.
    private long mLastPictureTime;
    // True when a onNewPicture callback is currenly in flight.
    private boolean mHasPendingOnNewPicture;

    private final AwContentsClient mContentsClient;

    private final Handler mHandler;

    private CancelCallbackPoller mCancelCallbackPoller;

    private class MyHandler extends Handler {
        private MyHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            if (mCancelCallbackPoller != null && mCancelCallbackPoller.shouldCancelAllCallbacks()) {
                removeCallbacksAndMessages(null);
                return;
            }

            switch (msg.what) {
                case MSG_ON_LOAD_RESOURCE:
                    {
                        final String url = (String) msg.obj;
                        mContentsClient.onLoadResource(url);
                        break;
                    }
                case MSG_ON_PAGE_STARTED:
                    {
                        final String url = (String) msg.obj;
                        mContentsClient.onPageStarted(url);
                        break;
                    }
                case MSG_ON_DOWNLOAD_START:
                    {
                        DownloadInfo info = (DownloadInfo) msg.obj;
                        mContentsClient.onDownloadStart(
                                info.mUrl,
                                info.mUserAgent,
                                info.mContentDisposition,
                                info.mMimeType,
                                info.mContentLength);
                        break;
                    }
                case MSG_ON_RECEIVED_LOGIN_REQUEST:
                    {
                        LoginRequestInfo info = (LoginRequestInfo) msg.obj;
                        mContentsClient.onReceivedLoginRequest(
                                info.mRealm, info.mAccount, info.mArgs);
                        break;
                    }
                case MSG_ON_RECEIVED_ERROR:
                    {
                        OnReceivedErrorInfo info = (OnReceivedErrorInfo) msg.obj;
                        mContentsClient.onReceivedError(info.mRequest, info.mError);
                        break;
                    }
                case MSG_ON_SAFE_BROWSING_HIT:
                    {
                        OnSafeBrowsingHitInfo info = (OnSafeBrowsingHitInfo) msg.obj;
                        mContentsClient.onSafeBrowsingHit(
                                info.mRequest, info.mThreatType, info.mCallback);
                        break;
                    }
                case MSG_ON_NEW_PICTURE:
                    {
                        Picture picture = null;
                        try {
                            if (msg.obj != null) picture = (Picture) ((Callable<?>) msg.obj).call();
                        } catch (Exception e) {
                            throw new RuntimeException("Error getting picture", e);
                        }
                        mContentsClient.onNewPicture(picture);
                        mLastPictureTime = SystemClock.uptimeMillis();
                        mHasPendingOnNewPicture = false;
                        break;
                    }
                case MSG_ON_SCALE_CHANGED_SCALED:
                    {
                        float oldScale = Float.intBitsToFloat(msg.arg1);
                        float newScale = Float.intBitsToFloat(msg.arg2);
                        mContentsClient.onScaleChangedScaled(oldScale, newScale);
                        break;
                    }
                case MSG_ON_RECEIVED_HTTP_ERROR:
                    {
                        OnReceivedHttpErrorInfo info = (OnReceivedHttpErrorInfo) msg.obj;
                        mContentsClient.onReceivedHttpError(info.mRequest, info.mResponse);
                        break;
                    }
                case MSG_ON_PAGE_FINISHED:
                    {
                        final String url = (String) msg.obj;
                        mContentsClient.onPageFinished(url);
                        break;
                    }
                case MSG_ON_RECEIVED_TITLE:
                    {
                        final String title = (String) msg.obj;
                        mContentsClient.onReceivedTitle(title);
                        break;
                    }
                case MSG_ON_PROGRESS_CHANGED:
                    {
                        mContentsClient.onProgressChanged(msg.arg1);
                        break;
                    }
                case MSG_SYNTHESIZE_PAGE_LOADING:
                    {
                        final String url = (String) msg.obj;
                        mContentsClient.onPageStarted(url);
                        mContentsClient.onLoadResource(url);
                        mContentsClient.onProgressChanged(100);
                        mContentsClient.onPageFinished(url);
                        break;
                    }
                case MSG_DO_UPDATE_VISITED_HISTORY:
                    {
                        final DoUpdateVisitedHistoryInfo info =
                                (DoUpdateVisitedHistoryInfo) msg.obj;
                        mContentsClient.doUpdateVisitedHistory(info.mUrl, info.mIsReload);
                        break;
                    }
                case MSG_ON_FORM_RESUBMISSION:
                    {
                        final OnFormResubmissionInfo info = (OnFormResubmissionInfo) msg.obj;
                        mContentsClient.onFormResubmission(info.mDontResend, info.mResend);
                        break;
                    }
                default:
                    throw new IllegalStateException(
                            "AwContentsClientCallbackHelper: unhandled message " + msg.what);
            }
        }
    }

    public AwContentsClientCallbackHelper(Looper looper, AwContentsClient contentsClient) {
        mHandler = new MyHandler(looper);
        mContentsClient = contentsClient;
    }

    // Public for tests.
    public void setCancelCallbackPoller(CancelCallbackPoller poller) {
        mCancelCallbackPoller = poller;
    }

    CancelCallbackPoller getCancelCallbackPoller() {
        return mCancelCallbackPoller;
    }

    public void postOnLoadResource(String url) {
        mHandler.sendMessage(mHandler.obtainMessage(MSG_ON_LOAD_RESOURCE, url));
    }

    public void postOnPageStarted(String url) {
        mHandler.sendMessage(mHandler.obtainMessage(MSG_ON_PAGE_STARTED, url));
    }

    public void postOnDownloadStart(
            String url,
            String userAgent,
            String contentDisposition,
            String mimeType,
            long contentLength) {
        DownloadInfo info =
                new DownloadInfo(url, userAgent, contentDisposition, mimeType, contentLength);
        mHandler.sendMessage(mHandler.obtainMessage(MSG_ON_DOWNLOAD_START, info));
    }

    public void postOnReceivedLoginRequest(String realm, String account, String args) {
        LoginRequestInfo info = new LoginRequestInfo(realm, account, args);
        mHandler.sendMessage(mHandler.obtainMessage(MSG_ON_RECEIVED_LOGIN_REQUEST, info));
    }

    public void postOnReceivedError(
            AwContentsClient.AwWebResourceRequest request,
            AwContentsClient.AwWebResourceError error) {
        OnReceivedErrorInfo info = new OnReceivedErrorInfo(request, error);
        mHandler.sendMessage(mHandler.obtainMessage(MSG_ON_RECEIVED_ERROR, info));
    }

    public void postOnSafeBrowsingHit(
            AwContentsClient.AwWebResourceRequest request,
            int threatType,
            Callback<AwSafeBrowsingResponse> callback) {
        OnSafeBrowsingHitInfo info = new OnSafeBrowsingHitInfo(request, threatType, callback);
        mHandler.sendMessage(mHandler.obtainMessage(MSG_ON_SAFE_BROWSING_HIT, info));
    }

    public void postOnNewPicture(Callable<Picture> pictureProvider) {
        if (mHasPendingOnNewPicture) return;
        mHasPendingOnNewPicture = true;
        long pictureTime =
                java.lang.Math.max(
                        mLastPictureTime + ON_NEW_PICTURE_MIN_PERIOD_MILLIS,
                        SystemClock.uptimeMillis());
        mHandler.sendMessageAtTime(
                mHandler.obtainMessage(MSG_ON_NEW_PICTURE, pictureProvider), pictureTime);
    }

    public void postOnScaleChangedScaled(float oldScale, float newScale) {
        // The float->int->float conversion here is to avoid unnecessary allocations. The
        // documentation states that intBitsToFloat(floatToIntBits(a)) == a for all values of a
        // (except for NaNs which are collapsed to a single canonical NaN, but we don't care for
        // that case).
        mHandler.sendMessage(
                mHandler.obtainMessage(
                        MSG_ON_SCALE_CHANGED_SCALED,
                        Float.floatToIntBits(oldScale),
                        Float.floatToIntBits(newScale)));
    }

    public void postOnReceivedHttpError(
            AwContentsClient.AwWebResourceRequest request, WebResourceResponseInfo response) {
        OnReceivedHttpErrorInfo info = new OnReceivedHttpErrorInfo(request, response);
        mHandler.sendMessage(mHandler.obtainMessage(MSG_ON_RECEIVED_HTTP_ERROR, info));
    }

    public void postOnPageFinished(String url) {
        mHandler.sendMessage(mHandler.obtainMessage(MSG_ON_PAGE_FINISHED, url));
    }

    public void postOnReceivedTitle(String title) {
        mHandler.sendMessage(mHandler.obtainMessage(MSG_ON_RECEIVED_TITLE, title));
    }

    public void postOnProgressChanged(int progress) {
        mHandler.sendMessage(mHandler.obtainMessage(MSG_ON_PROGRESS_CHANGED, progress, 0));
    }

    public void postSynthesizedPageLoadingForUrlBarUpdate(String url) {
        mHandler.sendMessage(mHandler.obtainMessage(MSG_SYNTHESIZE_PAGE_LOADING, url));
    }

    public void postDoUpdateVisitedHistory(String url, boolean isReload) {
        DoUpdateVisitedHistoryInfo info = new DoUpdateVisitedHistoryInfo(url, isReload);
        mHandler.sendMessage(mHandler.obtainMessage(MSG_DO_UPDATE_VISITED_HISTORY, info));
    }

    public void postOnFormResubmission(Message dontResend, Message resend) {
        OnFormResubmissionInfo info = new OnFormResubmissionInfo(dontResend, resend);
        mHandler.sendMessage(mHandler.obtainMessage(MSG_ON_FORM_RESUBMISSION, info));
    }

    void removeCallbacksAndMessages() {
        mHandler.removeCallbacksAndMessages(null);
    }
}