Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 3d005785 authored by Ben Murdoch's avatar Ben Murdoch Committed by Android (Google) Code Review
Browse files

Merge "Prompt the user to terminate unresponsive pages."

parents 07d7d5a2 e2e20835
Loading
Loading
Loading
Loading
+241 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2012 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.webkit;

import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.Process;
import android.webkit.WebViewCore.EventHub;

// A Runnable that will monitor if the WebCore thread is still
// processing messages by pinging it every so often. It is safe
// to call the public methods of this class from any thread.
class WebCoreThreadWatchdog implements Runnable {

    // A message with this id is sent by the WebCore thread to notify the
    // Watchdog that the WebCore thread is still processing messages
    // (i.e. everything is OK).
    private static final int IS_ALIVE = 100;

    // This message is placed in the Watchdog's queue and removed when we
    // receive an IS_ALIVE. If it is ever processed, we consider the
    // WebCore thread unresponsive.
    private static final int TIMED_OUT = 101;

    // Message to tell the Watchdog thread to terminate.
    private static final int QUIT = 102;

    // Wait 10s after hearing back from the WebCore thread before checking it's still alive.
    private static final int HEARTBEAT_PERIOD = 10 * 1000;

    // If there's no callback from the WebCore thread for 30s, prompt the user the page has
    // become unresponsive.
    private static final int TIMEOUT_PERIOD = 30 * 1000;

    // After the first timeout, use a shorter period before re-prompting the user.
    private static final int SUBSEQUENT_TIMEOUT_PERIOD = 15 * 1000;

    private Context mContext;
    private Handler mWebCoreThreadHandler;
    private Handler mHandler;
    private boolean mPaused;
    private boolean mPendingQuit;

    private static WebCoreThreadWatchdog sInstance;

    public synchronized static WebCoreThreadWatchdog start(Context context,
            Handler webCoreThreadHandler) {
        if (sInstance == null) {
            sInstance = new WebCoreThreadWatchdog(context, webCoreThreadHandler);
            new Thread(sInstance, "WebCoreThreadWatchdog").start();
        }
        return sInstance;
    }

    public synchronized static void updateContext(Context context) {
        if (sInstance != null) {
            sInstance.setContext(context);
        }
    }

    public synchronized static void pause() {
        if (sInstance != null) {
            sInstance.pauseWatchdog();
        }
    }

    public synchronized static void resume() {
        if (sInstance != null) {
            sInstance.resumeWatchdog();
        }
    }

    public synchronized static void quit() {
        if (sInstance != null) {
            sInstance.quitWatchdog();
        }
    }

    private void setContext(Context context) {
        mContext = context;
    }

    private WebCoreThreadWatchdog(Context context, Handler webCoreThreadHandler) {
        mContext = context;
        mWebCoreThreadHandler = webCoreThreadHandler;
    }

    private void quitWatchdog() {
        if (mHandler == null) {
            // The thread hasn't started yet, so set a flag to stop it starting.
            mPendingQuit = true;
            return;
        }
        // Clear any pending messages, and then post a quit to the WatchDog handler.
        mHandler.removeMessages(TIMED_OUT);
        mHandler.removeMessages(IS_ALIVE);
        mWebCoreThreadHandler.removeMessages(EventHub.HEARTBEAT);
        mHandler.obtainMessage(QUIT).sendToTarget();
    }

    private void pauseWatchdog() {
        mPaused = true;

        if (mHandler == null) {
            return;
        }

        mHandler.removeMessages(TIMED_OUT);
        mHandler.removeMessages(IS_ALIVE);
        mWebCoreThreadHandler.removeMessages(EventHub.HEARTBEAT);
    }

    private void resumeWatchdog() {
        if (!mPaused) {
            // Do nothing if we get a call to resume without being paused.
            // This can happen during the initialisation of the WebView.
            return;
        }

        mPaused = false;

        if (mHandler == null) {
            return;
        }

        mWebCoreThreadHandler.obtainMessage(EventHub.HEARTBEAT,
                mHandler.obtainMessage(IS_ALIVE)).sendToTarget();
        mHandler.sendMessageDelayed(mHandler.obtainMessage(TIMED_OUT), TIMEOUT_PERIOD);
    }

    private boolean createHandler() {
        synchronized (WebCoreThreadWatchdog.class) {
            if (mPendingQuit) {
                return false;
            }

            mHandler = new Handler() {
                @Override
                public void handleMessage(Message msg) {
                    switch (msg.what) {
                    case IS_ALIVE:
                        synchronized(WebCoreThreadWatchdog.class) {
                            if (mPaused) {
                                return;
                            }

                            // The WebCore thread still seems alive. Reset the countdown timer.
                            removeMessages(TIMED_OUT);
                            sendMessageDelayed(obtainMessage(TIMED_OUT), TIMEOUT_PERIOD);
                            mWebCoreThreadHandler.sendMessageDelayed(
                                    mWebCoreThreadHandler.obtainMessage(EventHub.HEARTBEAT,
                                            mHandler.obtainMessage(IS_ALIVE)),
                                    HEARTBEAT_PERIOD);
                        }
                        break;

                    case TIMED_OUT:
                        new AlertDialog.Builder(mContext)
                            .setMessage(com.android.internal.R.string.webpage_unresponsive)
                            .setPositiveButton(com.android.internal.R.string.force_close,
                                    new DialogInterface.OnClickListener() {
                                        @Override
                                        public void onClick(DialogInterface dialog, int which) {
                                        // User chose to force close.
                                        Process.killProcess(Process.myPid());
                                    }
                                })
                            .setNegativeButton(com.android.internal.R.string.wait,
                                    new DialogInterface.OnClickListener() {
                                        @Override
                                        public void onClick(DialogInterface dialog, int which) {
                                            // The user chose to wait. The last HEARTBEAT message
                                            // will still be in the WebCore thread's queue, so all
                                            // we need to do is post another TIMED_OUT so that the
                                            // user will get prompted again if the WebCore thread
                                            // doesn't sort itself out.
                                            sendMessageDelayed(obtainMessage(TIMED_OUT),
                                                    SUBSEQUENT_TIMEOUT_PERIOD);
                                       }
                                    })
                            .setOnCancelListener(new DialogInterface.OnCancelListener() {
                                    @Override
                                    public void onCancel(DialogInterface dialog) {
                                        sendMessageDelayed(obtainMessage(TIMED_OUT),
                                                SUBSEQUENT_TIMEOUT_PERIOD);
                                    }
                            })
                            .setIcon(android.R.drawable.ic_dialog_alert)
                            .show();
                        break;

                    case QUIT:
                        Looper.myLooper().quit();
                        break;
                    }
                }
            };

            return true;
        }
    }

    @Override
    public void run() {
        Looper.prepare();

        if (!createHandler()) {
            return;
        }

        // Send the initial control to WebViewCore and start the timeout timer as long as we aren't
        // paused.
        synchronized (WebCoreThreadWatchdog.class) {
            if (!mPaused) {
                mWebCoreThreadHandler.obtainMessage(EventHub.HEARTBEAT,
                        mHandler.obtainMessage(IS_ALIVE)).sendToTarget();
                mHandler.sendMessageDelayed(mHandler.obtainMessage(TIMED_OUT), TIMEOUT_PERIOD);
            }
        }

        Looper.loop();
    }
}
+10 −0
Original line number Diff line number Diff line
@@ -3358,6 +3358,7 @@ public class WebView extends AbsoluteLayout
            }

            cancelSelectDialog();
            WebCoreThreadWatchdog.pause();
        }
    }

@@ -3390,6 +3391,15 @@ public class WebView extends AbsoluteLayout
                nativeSetPauseDrawing(mNativeClass, false);
            }
        }
        // Ensure that the watchdog has a currently valid Context to be able to display
        // a prompt dialog. For example, if the Activity was finished whilst the WebCore
        // thread was blocked and the Activity is started again, we may reuse the blocked
        // thread, but we'll have a new Activity.
        WebCoreThreadWatchdog.updateContext(mContext);
        // We get a call to onResume for new WebViews (i.e. mIsPaused will be false). We need
        // to ensure that the Watchdog thread is running for the new WebView, so call
        // it outside the if block above.
        WebCoreThreadWatchdog.resume();
    }

    /**
+14 −0
Original line number Diff line number Diff line
@@ -166,6 +166,10 @@ public final class WebViewCore {
                           "creation.");
                    Log.e(LOGTAG, Log.getStackTraceString(e));
                }

                // Start the singleton watchdog which will monitor the WebCore thread
                // to verify it's still processing messages.
                WebCoreThreadWatchdog.start(context, sWebCoreHandler);
            }
        }
        // Create an EventHub to handle messages before and after the thread is
@@ -755,6 +759,13 @@ public final class WebViewCore {
                                }
                                BrowserFrame.sJavaBridge.updateProxy((ProxyProperties)msg.obj);
                                break;

                            case EventHub.HEARTBEAT:
                                // Ping back the watchdog to let it know we're still processing
                                // messages.
                                Message m = (Message)msg.obj;
                                m.sendToTarget();
                                break;
                        }
                    }
                };
@@ -1078,6 +1089,8 @@ public final class WebViewCore {

        static final int NOTIFY_ANIMATION_STARTED = 196;

        static final int HEARTBEAT = 197;

        // private message ids
        private static final int DESTROY =     200;

@@ -1156,6 +1169,7 @@ public final class WebViewCore {
                                mSettings.onDestroyed();
                                mNativeClass = 0;
                                mWebView = null;
                                WebCoreThreadWatchdog.quit();
                            }
                            break;

+2 −0
Original line number Diff line number Diff line
@@ -2568,6 +2568,8 @@
    <string name="report">Report</string>
    <!-- Button allowing the user to choose to wait for an application that is not responding to become responsive again. -->
    <string name="wait">Wait</string>
    <!-- Text of the alert that is displayed when a web page is not responding. [CHAR-LIMIT=NONE] -->
    <string name="webpage_unresponsive">The page has become unresponsive.\n\nDo you want to close it?</string>
    <!-- [CHAR LIMIT=25] Title of the alert when application launches on top of another. -->
    <string name="launch_warning_title">App redirected</string>
    <!-- [CHAR LIMIT=50] Title of the alert when application launches on top of another. -->