• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
start(Handler webCoreThreadHandler)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 
registerWebView(WebViewClassic w)74     public synchronized static void registerWebView(WebViewClassic w) {
75         if (sInstance != null) {
76             sInstance.addWebView(w);
77         }
78     }
79 
unregisterWebView(WebViewClassic w)80     public synchronized static void unregisterWebView(WebViewClassic w) {
81         if (sInstance != null) {
82             sInstance.removeWebView(w);
83         }
84     }
85 
pause()86     public synchronized static void pause() {
87         if (sInstance != null) {
88             sInstance.pauseWatchdog();
89         }
90     }
91 
resume()92     public synchronized static void resume() {
93         if (sInstance != null) {
94             sInstance.resumeWatchdog();
95         }
96     }
97 
addWebView(WebViewClassic w)98     private void addWebView(WebViewClassic w) {
99         if (mWebViews == null) {
100             mWebViews = new HashSet<WebViewClassic>();
101         }
102         mWebViews.add(w);
103     }
104 
removeWebView(WebViewClassic w)105     private void removeWebView(WebViewClassic w) {
106         mWebViews.remove(w);
107     }
108 
WebCoreThreadWatchdog(Handler webCoreThreadHandler)109     private WebCoreThreadWatchdog(Handler webCoreThreadHandler) {
110         mWebCoreThreadHandler = webCoreThreadHandler;
111     }
112 
pauseWatchdog()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 
resumeWatchdog()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 
createHandler()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
run()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 
PageNotRespondingRunnable(Context context, Handler watchdogHandler)231         public PageNotRespondingRunnable(Context context, Handler watchdogHandler) {
232             mContext = context;
233             mWatchdogHandler = watchdogHandler;
234         }
235 
236         @Override
run()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