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

Commit e2e20835 authored by Ben Murdoch's avatar Ben Murdoch
Browse files

Prompt the user to terminate unresponsive pages.

Run a background "watchdog" thread that will check for
the WebCore thread stopping processing messages (this
can happen if a piece of JavaScript goes into an infinite
loop, for example) and offer to close the WebView app when
it detects such a case, much like the normal system ANR.

Bug: 2563868
Change-Id: Ic74813b1e630d657c340a7017a4b0814071eb041
parent 7a939077
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
@@ -3317,6 +3317,7 @@ public class WebView extends AbsoluteLayout
            }

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

@@ -3349,6 +3350,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
@@ -743,6 +747,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;
                        }
                    }
                };
@@ -1064,6 +1075,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;

@@ -1142,6 +1155,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. -->