Home | History | Annotate | Download | only in webkit
      1 /*
      2  * Copyright (C) 2012 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.webkit;
     18 
     19 import android.app.Activity;
     20 import android.app.AlertDialog;
     21 import android.content.Context;
     22 import android.content.DialogInterface;
     23 import android.os.Handler;
     24 import android.os.Looper;
     25 import android.os.Message;
     26 import android.os.Process;
     27 import android.webkit.WebViewCore.EventHub;
     28 
     29 import java.util.HashSet;
     30 import java.util.Iterator;
     31 import java.util.Set;
     32 
     33 // A Runnable that will monitor if the WebCore thread is still
     34 // processing messages by pinging it every so often. It is safe
     35 // to call the public methods of this class from any thread.
     36 class WebCoreThreadWatchdog implements Runnable {
     37 
     38     // A message with this id is sent by the WebCore thread to notify the
     39     // Watchdog that the WebCore thread is still processing messages
     40     // (i.e. everything is OK).
     41     private static final int IS_ALIVE = 100;
     42 
     43     // This message is placed in the Watchdog's queue and removed when we
     44     // receive an IS_ALIVE. If it is ever processed, we consider the
     45     // WebCore thread unresponsive.
     46     private static final int TIMED_OUT = 101;
     47 
     48     // Wait 10s after hearing back from the WebCore thread before checking it's still alive.
     49     private static final int HEARTBEAT_PERIOD = 10 * 1000;
     50 
     51     // If there's no callback from the WebCore thread for 30s, prompt the user the page has
     52     // become unresponsive.
     53     private static final int TIMEOUT_PERIOD = 30 * 1000;
     54 
     55     // After the first timeout, use a shorter period before re-prompting the user.
     56     private static final int SUBSEQUENT_TIMEOUT_PERIOD = 15 * 1000;
     57 
     58     private Handler mWebCoreThreadHandler;
     59     private Handler mHandler;
     60     private boolean mPaused;
     61 
     62     private Set<WebViewClassic> mWebViews;
     63 
     64     private static WebCoreThreadWatchdog sInstance;
     65 
     66     public synchronized static WebCoreThreadWatchdog start(Handler webCoreThreadHandler) {
     67         if (sInstance == null) {
     68             sInstance = new WebCoreThreadWatchdog(webCoreThreadHandler);
     69             new Thread(sInstance, "WebCoreThreadWatchdog").start();
     70         }
     71         return sInstance;
     72     }
     73 
     74     public synchronized static void registerWebView(WebViewClassic w) {
     75         if (sInstance != null) {
     76             sInstance.addWebView(w);
     77         }
     78     }
     79 
     80     public synchronized static void unregisterWebView(WebViewClassic w) {
     81         if (sInstance != null) {
     82             sInstance.removeWebView(w);
     83         }
     84     }
     85 
     86     public synchronized static void pause() {
     87         if (sInstance != null) {
     88             sInstance.pauseWatchdog();
     89         }
     90     }
     91 
     92     public synchronized static void resume() {
     93         if (sInstance != null) {
     94             sInstance.resumeWatchdog();
     95         }
     96     }
     97 
     98     private void addWebView(WebViewClassic w) {
     99         if (mWebViews == null) {
    100             mWebViews = new HashSet<WebViewClassic>();
    101         }
    102         mWebViews.add(w);
    103     }
    104 
    105     private void removeWebView(WebViewClassic w) {
    106         mWebViews.remove(w);
    107     }
    108 
    109     private WebCoreThreadWatchdog(Handler webCoreThreadHandler) {
    110         mWebCoreThreadHandler = webCoreThreadHandler;
    111     }
    112 
    113     private void pauseWatchdog() {
    114         mPaused = true;
    115 
    116         if (mHandler == null) {
    117             return;
    118         }
    119 
    120         mHandler.removeMessages(TIMED_OUT);
    121         mHandler.removeMessages(IS_ALIVE);
    122         mWebCoreThreadHandler.removeMessages(EventHub.HEARTBEAT);
    123     }
    124 
    125     private void resumeWatchdog() {
    126         if (!mPaused) {
    127             // Do nothing if we get a call to resume without being paused.
    128             // This can happen during the initialisation of the WebView.
    129             return;
    130         }
    131 
    132         mPaused = false;
    133 
    134         if (mHandler == null) {
    135             return;
    136         }
    137 
    138         mWebCoreThreadHandler.obtainMessage(EventHub.HEARTBEAT,
    139                 mHandler.obtainMessage(IS_ALIVE)).sendToTarget();
    140         mHandler.sendMessageDelayed(mHandler.obtainMessage(TIMED_OUT), TIMEOUT_PERIOD);
    141     }
    142 
    143     private void createHandler() {
    144         synchronized (WebCoreThreadWatchdog.class) {
    145             mHandler = new Handler() {
    146                 @Override
    147                 public void handleMessage(Message msg) {
    148                     switch (msg.what) {
    149                     case IS_ALIVE:
    150                         synchronized(WebCoreThreadWatchdog.class) {
    151                             if (mPaused) {
    152                                 return;
    153                             }
    154 
    155                             // The WebCore thread still seems alive. Reset the countdown timer.
    156                             removeMessages(TIMED_OUT);
    157                             sendMessageDelayed(obtainMessage(TIMED_OUT), TIMEOUT_PERIOD);
    158                             mWebCoreThreadHandler.sendMessageDelayed(
    159                                     mWebCoreThreadHandler.obtainMessage(EventHub.HEARTBEAT,
    160                                             mHandler.obtainMessage(IS_ALIVE)),
    161                                     HEARTBEAT_PERIOD);
    162                         }
    163                         break;
    164 
    165                     case TIMED_OUT:
    166                         boolean postedDialog = false;
    167                         synchronized (WebCoreThreadWatchdog.class) {
    168                             Iterator<WebViewClassic> it = mWebViews.iterator();
    169                             // Check each WebView we are aware of and find one that is capable of
    170                             // showing the user a prompt dialog.
    171                             while (it.hasNext()) {
    172                                 WebView activeView = it.next().getWebView();
    173 
    174                                 if (activeView.getWindowToken() != null &&
    175                                         activeView.getViewRootImpl() != null) {
    176                                     postedDialog = activeView.post(new PageNotRespondingRunnable(
    177                                             activeView.getContext(), this));
    178 
    179                                     if (postedDialog) {
    180                                         // We placed the message into the UI thread for an attached
    181                                         // WebView so we've made our best attempt to display the
    182                                         // "page not responding" dialog to the user. Although the
    183                                         // message is in the queue, there is no guarantee when/if
    184                                         // the runnable will execute. In the case that the runnable
    185                                         // never executes, the user will need to terminate the
    186                                         // process manually.
    187                                         break;
    188                                     }
    189                                 }
    190                             }
    191 
    192                             if (!postedDialog) {
    193                                 // There's no active webview we can use to show the dialog, so
    194                                 // wait again. If we never get a usable view, the user will
    195                                 // never get the chance to terminate the process, and will
    196                                 // need to do it manually.
    197                                 sendMessageDelayed(obtainMessage(TIMED_OUT),
    198                                         SUBSEQUENT_TIMEOUT_PERIOD);
    199                             }
    200                         }
    201                         break;
    202                     }
    203                 }
    204             };
    205         }
    206     }
    207 
    208     @Override
    209     public void run() {
    210         Looper.prepare();
    211 
    212         createHandler();
    213 
    214         // Send the initial control to WebViewCore and start the timeout timer as long as we aren't
    215         // paused.
    216         synchronized (WebCoreThreadWatchdog.class) {
    217             if (!mPaused) {
    218                 mWebCoreThreadHandler.obtainMessage(EventHub.HEARTBEAT,
    219                         mHandler.obtainMessage(IS_ALIVE)).sendToTarget();
    220                 mHandler.sendMessageDelayed(mHandler.obtainMessage(TIMED_OUT), TIMEOUT_PERIOD);
    221             }
    222         }
    223 
    224         Looper.loop();
    225     }
    226 
    227     private class PageNotRespondingRunnable implements Runnable {
    228         Context mContext;
    229         private Handler mWatchdogHandler;
    230 
    231         public PageNotRespondingRunnable(Context context, Handler watchdogHandler) {
    232             mContext = context;
    233             mWatchdogHandler = watchdogHandler;
    234         }
    235 
    236         @Override
    237         public void run() {
    238             // This must run on the UI thread as it is displaying an AlertDialog.
    239             assert Looper.getMainLooper().getThread() == Thread.currentThread();
    240             new AlertDialog.Builder(mContext)
    241                     .setMessage(com.android.internal.R.string.webpage_unresponsive)
    242                     .setPositiveButton(com.android.internal.R.string.force_close,
    243                             new DialogInterface.OnClickListener() {
    244                                 @Override
    245                                 public void onClick(DialogInterface dialog, int which) {
    246                                     // User chose to force close.
    247                                     Process.killProcess(Process.myPid());
    248                                 }
    249                             })
    250                     .setNegativeButton(com.android.internal.R.string.wait,
    251                             new DialogInterface.OnClickListener() {
    252                                 @Override
    253                                 public void onClick(DialogInterface dialog, int which) {
    254                                     // The user chose to wait. The last HEARTBEAT message
    255                                     // will still be in the WebCore thread's queue, so all
    256                                     // we need to do is post another TIMED_OUT so that the
    257                                     // user will get prompted again if the WebCore thread
    258                                     // doesn't sort itself out.
    259                                     mWatchdogHandler.sendMessageDelayed(
    260                                             mWatchdogHandler.obtainMessage(TIMED_OUT),
    261                                             SUBSEQUENT_TIMEOUT_PERIOD);
    262                                 }
    263                             })
    264                     .setOnCancelListener(
    265                             new DialogInterface.OnCancelListener() {
    266                                 @Override
    267                                 public void onCancel(DialogInterface dialog) {
    268                                     mWatchdogHandler.sendMessageDelayed(
    269                                             mWatchdogHandler.obtainMessage(TIMED_OUT),
    270                                             SUBSEQUENT_TIMEOUT_PERIOD);
    271                                 }
    272                             })
    273                     .setIconAttribute(android.R.attr.alertDialogIcon)
    274                     .show();
    275         }
    276     }
    277 }
    278