/* * 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.Activity; 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; import java.util.HashSet; import java.util.Iterator; import java.util.Set; // 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; // 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 Handler mWebCoreThreadHandler; private Handler mHandler; private boolean mPaused; private Set<WebViewClassic> mWebViews; private static WebCoreThreadWatchdog sInstance; public synchronized static WebCoreThreadWatchdog start(Handler webCoreThreadHandler) { if (sInstance == null) { sInstance = new WebCoreThreadWatchdog(webCoreThreadHandler); new Thread(sInstance, "WebCoreThreadWatchdog").start(); } return sInstance; } public synchronized static void registerWebView(WebViewClassic w) { if (sInstance != null) { sInstance.addWebView(w); } } public synchronized static void unregisterWebView(WebViewClassic w) { if (sInstance != null) { sInstance.removeWebView(w); } } public synchronized static void pause() { if (sInstance != null) { sInstance.pauseWatchdog(); } } public synchronized static void resume() { if (sInstance != null) { sInstance.resumeWatchdog(); } } private void addWebView(WebViewClassic w) { if (mWebViews == null) { mWebViews = new HashSet<WebViewClassic>(); } mWebViews.add(w); } private void removeWebView(WebViewClassic w) { mWebViews.remove(w); } private WebCoreThreadWatchdog(Handler webCoreThreadHandler) { mWebCoreThreadHandler = webCoreThreadHandler; } 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 void createHandler() { synchronized (WebCoreThreadWatchdog.class) { 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: boolean postedDialog = false; synchronized (WebCoreThreadWatchdog.class) { Iterator<WebViewClassic> it = mWebViews.iterator(); // Check each WebView we are aware of and find one that is capable of // showing the user a prompt dialog. while (it.hasNext()) { WebView activeView = it.next().getWebView(); if (activeView.getWindowToken() != null && activeView.getViewRootImpl() != null) { postedDialog = activeView.post(new PageNotRespondingRunnable( activeView.getContext(), this)); if (postedDialog) { // We placed the message into the UI thread for an attached // WebView so we've made our best attempt to display the // "page not responding" dialog to the user. Although the // message is in the queue, there is no guarantee when/if // the runnable will execute. In the case that the runnable // never executes, the user will need to terminate the // process manually. break; } } } if (!postedDialog) { // There's no active webview we can use to show the dialog, so // wait again. If we never get a usable view, the user will // never get the chance to terminate the process, and will // need to do it manually. sendMessageDelayed(obtainMessage(TIMED_OUT), SUBSEQUENT_TIMEOUT_PERIOD); } } break; } } }; } } @Override public void run() { Looper.prepare(); createHandler(); // 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(); } private class PageNotRespondingRunnable implements Runnable { Context mContext; private Handler mWatchdogHandler; public PageNotRespondingRunnable(Context context, Handler watchdogHandler) { mContext = context; mWatchdogHandler = watchdogHandler; } @Override public void run() { // This must run on the UI thread as it is displaying an AlertDialog. assert Looper.getMainLooper().getThread() == Thread.currentThread(); 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. mWatchdogHandler.sendMessageDelayed( mWatchdogHandler.obtainMessage(TIMED_OUT), SUBSEQUENT_TIMEOUT_PERIOD); } }) .setOnCancelListener( new DialogInterface.OnCancelListener() { @Override public void onCancel(DialogInterface dialog) { mWatchdogHandler.sendMessageDelayed( mWatchdogHandler.obtainMessage(TIMED_OUT), SUBSEQUENT_TIMEOUT_PERIOD); } }) .setIcon(android.R.drawable.ic_dialog_alert) .show(); } } }