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