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
17package android.webkit;
18
19import android.app.Activity;
20import android.app.AlertDialog;
21import android.content.Context;
22import android.content.DialogInterface;
23import android.os.Handler;
24import android.os.Looper;
25import android.os.Message;
26import android.os.Process;
27import android.webkit.WebViewCore.EventHub;
28
29import java.util.HashSet;
30import java.util.Iterator;
31import 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.
36class 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                    .setIcon(android.R.drawable.ic_dialog_alert)
274                    .show();
275        }
276    }
277}
278