1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "chrome/browser/ui/unload_controller.h"
7 #include "base/message_loop/message_loop.h"
8 #include "chrome/browser/chrome_notification_types.h"
9 #include "chrome/browser/devtools/devtools_window.h"
10 #include "chrome/browser/ui/browser.h"
11 #include "chrome/browser/ui/browser_tabstrip.h"
12 #include "chrome/browser/ui/tabs/tab_strip_model.h"
13 #include "content/public/browser/notification_service.h"
14 #include "content/public/browser/notification_source.h"
15 #include "content/public/browser/notification_types.h"
16 #include "content/public/browser/render_view_host.h"
17 #include "content/public/browser/web_contents.h"
21 ////////////////////////////////////////////////////////////////////////////////
22 // UnloadController, public:
24 UnloadController::UnloadController(Browser
* browser
)
26 is_attempting_to_close_browser_(false),
28 browser_
->tab_strip_model()->AddObserver(this);
31 UnloadController::~UnloadController() {
32 browser_
->tab_strip_model()->RemoveObserver(this);
35 bool UnloadController::CanCloseContents(content::WebContents
* contents
) {
36 // Don't try to close the tab when the whole browser is being closed, since
37 // that avoids the fast shutdown path where we just kill all the renderers.
38 if (is_attempting_to_close_browser_
)
39 ClearUnloadState(contents
, true);
40 return !is_attempting_to_close_browser_
||
41 is_calling_before_unload_handlers();
45 bool UnloadController::ShouldRunUnloadEventsHelper(
46 content::WebContents
* contents
) {
47 // If |contents| is being inspected, devtools needs to intercept beforeunload
49 return DevToolsWindow::GetInstanceForInspectedRenderViewHost(
50 contents
->GetRenderViewHost()) != NULL
;
54 bool UnloadController::RunUnloadEventsHelper(content::WebContents
* contents
) {
55 // If there's a devtools window attached to |contents|,
56 // we would like devtools to call its own beforeunload handlers first,
57 // and then call beforeunload handlers for |contents|.
58 // See DevToolsWindow::InterceptPageBeforeUnload for details.
59 if (DevToolsWindow::InterceptPageBeforeUnload(contents
)) {
62 // If the WebContents is not connected yet, then there's no unload
63 // handler we can fire even if the WebContents has an unload listener.
64 // One case where we hit this is in a tab that has an infinite loop
66 if (contents
->NeedToFireBeforeUnload()) {
67 // If the page has unload listeners, then we tell the renderer to fire
68 // them. Once they have fired, we'll get a message back saying whether
69 // to proceed closing the page or not, which sends us back to this method
70 // with the NeedToFireBeforeUnload bit cleared.
71 contents
->GetRenderViewHost()->FirePageBeforeUnload(false);
77 bool UnloadController::BeforeUnloadFired(content::WebContents
* contents
,
80 DevToolsWindow::OnPageCloseCanceled(contents
);
82 if (!is_attempting_to_close_browser_
) {
84 contents
->SetClosedByUserGesture(false);
90 contents
->SetClosedByUserGesture(false);
94 if (RemoveFromSet(&tabs_needing_before_unload_fired_
, contents
)) {
95 // Now that beforeunload has fired, put the tab on the queue to fire
97 tabs_needing_unload_fired_
.insert(contents
);
99 // We want to handle firing the unload event ourselves since we want to
100 // fire all the beforeunload events before attempting to fire the unload
101 // events should the user cancel closing the browser.
108 bool UnloadController::ShouldCloseWindow() {
109 if (HasCompletedUnloadProcessing())
112 // Special case for when we quit an application. The devtools window can
113 // close if it's beforeunload event has already fired which will happen due
114 // to the interception of it's content's beforeunload.
115 if (browser_
->is_devtools() &&
116 DevToolsWindow::HasFiredBeforeUnloadEventForDevToolsBrowser(browser_
)) {
120 // The behavior followed here varies based on the current phase of the
121 // operation and whether a batched shutdown is in progress.
123 // If there are tabs with outstanding beforeunload handlers:
124 // 1. If a batched shutdown is in progress: return false.
125 // This is to prevent interference with batched shutdown already in
127 // 2. Otherwise: start sending beforeunload events and return false.
129 // Otherwise, If there are no tabs with outstanding beforeunload handlers:
130 // 3. If a batched shutdown is in progress: start sending unload events and
132 // 4. Otherwise: return true.
133 is_attempting_to_close_browser_
= true;
135 bool need_beforeunload_fired
= TabsNeedBeforeUnloadFired();
136 if (need_beforeunload_fired
== is_calling_before_unload_handlers())
137 return !need_beforeunload_fired
;
140 on_close_confirmed_
.Reset();
141 ProcessPendingTabs();
145 bool UnloadController::CallBeforeUnloadHandlers(
146 const base::Callback
<void(bool)>& on_close_confirmed
) {
147 // The devtools browser gets its beforeunload events as the results of
148 // intercepting events from the inspected tab, so don't send them here as
150 if (browser_
->is_devtools() || HasCompletedUnloadProcessing() ||
151 !TabsNeedBeforeUnloadFired())
154 is_attempting_to_close_browser_
= true;
155 on_close_confirmed_
= on_close_confirmed
;
157 ProcessPendingTabs();
161 void UnloadController::ResetBeforeUnloadHandlers() {
162 if (!is_calling_before_unload_handlers())
167 bool UnloadController::TabsNeedBeforeUnloadFired() {
168 if (tabs_needing_before_unload_fired_
.empty()) {
169 for (int i
= 0; i
< browser_
->tab_strip_model()->count(); ++i
) {
170 content::WebContents
* contents
=
171 browser_
->tab_strip_model()->GetWebContentsAt(i
);
172 bool should_fire_beforeunload
= contents
->NeedToFireBeforeUnload() ||
173 DevToolsWindow::NeedsToInterceptBeforeUnload(contents
);
174 if (!ContainsKey(tabs_needing_unload_fired_
, contents
) &&
175 should_fire_beforeunload
) {
176 tabs_needing_before_unload_fired_
.insert(contents
);
180 return !tabs_needing_before_unload_fired_
.empty();
183 void UnloadController::CancelWindowClose() {
184 // Closing of window can be canceled from a beforeunload handler.
185 DCHECK(is_attempting_to_close_browser_
);
186 tabs_needing_before_unload_fired_
.clear();
187 for (UnloadListenerSet::iterator it
= tabs_needing_unload_fired_
.begin();
188 it
!= tabs_needing_unload_fired_
.end(); ++it
) {
189 DevToolsWindow::OnPageCloseCanceled(*it
);
191 tabs_needing_unload_fired_
.clear();
192 if (is_calling_before_unload_handlers()) {
193 base::Callback
<void(bool)> on_close_confirmed
= on_close_confirmed_
;
194 on_close_confirmed_
.Reset();
195 on_close_confirmed
.Run(false);
197 is_attempting_to_close_browser_
= false;
199 content::NotificationService::current()->Notify(
200 chrome::NOTIFICATION_BROWSER_CLOSE_CANCELLED
,
201 content::Source
<Browser
>(browser_
),
202 content::NotificationService::NoDetails());
205 ////////////////////////////////////////////////////////////////////////////////
206 // UnloadController, content::NotificationObserver implementation:
208 void UnloadController::Observe(int type
,
209 const content::NotificationSource
& source
,
210 const content::NotificationDetails
& details
) {
212 case content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED
:
213 if (is_attempting_to_close_browser_
) {
214 ClearUnloadState(content::Source
<content::WebContents
>(source
).ptr(),
215 false); // See comment for ClearUnloadState().
219 NOTREACHED() << "Got a notification we didn't register for.";
223 ////////////////////////////////////////////////////////////////////////////////
224 // UnloadController, TabStripModelObserver implementation:
226 void UnloadController::TabInsertedAt(content::WebContents
* contents
,
229 TabAttachedImpl(contents
);
232 void UnloadController::TabDetachedAt(content::WebContents
* contents
,
234 TabDetachedImpl(contents
);
237 void UnloadController::TabReplacedAt(TabStripModel
* tab_strip_model
,
238 content::WebContents
* old_contents
,
239 content::WebContents
* new_contents
,
241 TabDetachedImpl(old_contents
);
242 TabAttachedImpl(new_contents
);
245 void UnloadController::TabStripEmpty() {
246 // Set is_attempting_to_close_browser_ here, so that extensions, etc, do not
247 // attempt to add tabs to the browser before it closes.
248 is_attempting_to_close_browser_
= true;
251 ////////////////////////////////////////////////////////////////////////////////
252 // UnloadController, private:
254 void UnloadController::TabAttachedImpl(content::WebContents
* contents
) {
255 // If the tab crashes in the beforeunload or unload handler, it won't be
256 // able to ack. But we know we can close it.
259 content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED
,
260 content::Source
<content::WebContents
>(contents
));
263 void UnloadController::TabDetachedImpl(content::WebContents
* contents
) {
264 if (is_attempting_to_close_browser_
)
265 ClearUnloadState(contents
, false);
266 registrar_
.Remove(this,
267 content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED
,
268 content::Source
<content::WebContents
>(contents
));
271 void UnloadController::ProcessPendingTabs() {
272 if (!is_attempting_to_close_browser_
) {
273 // Because we might invoke this after a delay it's possible for the value of
274 // is_attempting_to_close_browser_ to have changed since we scheduled the
279 if (HasCompletedUnloadProcessing()) {
280 // We've finished all the unload events and can proceed to close the
282 browser_
->OnWindowClosing();
286 // Process beforeunload tabs first. When that queue is empty, process
288 if (!tabs_needing_before_unload_fired_
.empty()) {
289 content::WebContents
* web_contents
=
290 *(tabs_needing_before_unload_fired_
.begin());
291 // Null check render_view_host here as this gets called on a PostTask and
292 // the tab's render_view_host may have been nulled out.
293 if (web_contents
->GetRenderViewHost()) {
294 // If there's a devtools window attached to |web_contents|,
295 // we would like devtools to call its own beforeunload handlers first,
296 // and then call beforeunload handlers for |web_contents|.
297 // See DevToolsWindow::InterceptPageBeforeUnload for details.
298 if (!DevToolsWindow::InterceptPageBeforeUnload(web_contents
))
299 web_contents
->GetRenderViewHost()->FirePageBeforeUnload(false);
301 ClearUnloadState(web_contents
, true);
303 } else if (is_calling_before_unload_handlers()) {
304 on_close_confirmed_
.Run(true);
305 } else if (!tabs_needing_unload_fired_
.empty()) {
306 // We've finished firing all beforeunload events and can proceed with unload
308 // TODO(ojan): We should add a call to browser_shutdown::OnShutdownStarting
309 // somewhere around here so that we have accurate measurements of shutdown
311 // TODO(ojan): We can probably fire all the unload events in parallel and
312 // get a perf benefit from that in the cases where the tab hangs in it's
313 // unload handler or takes a long time to page in.
314 content::WebContents
* web_contents
= *(tabs_needing_unload_fired_
.begin());
315 // Null check render_view_host here as this gets called on a PostTask and
316 // the tab's render_view_host may have been nulled out.
317 if (web_contents
->GetRenderViewHost()) {
318 web_contents
->GetRenderViewHost()->ClosePage();
320 ClearUnloadState(web_contents
, true);
327 bool UnloadController::HasCompletedUnloadProcessing() const {
328 return is_attempting_to_close_browser_
&&
329 tabs_needing_before_unload_fired_
.empty() &&
330 tabs_needing_unload_fired_
.empty();
333 bool UnloadController::RemoveFromSet(UnloadListenerSet
* set
,
334 content::WebContents
* web_contents
) {
335 DCHECK(is_attempting_to_close_browser_
);
337 UnloadListenerSet::iterator iter
=
338 std::find(set
->begin(), set
->end(), web_contents
);
339 if (iter
!= set
->end()) {
346 void UnloadController::ClearUnloadState(content::WebContents
* web_contents
,
348 if (is_attempting_to_close_browser_
) {
349 RemoveFromSet(&tabs_needing_before_unload_fired_
, web_contents
);
350 RemoveFromSet(&tabs_needing_unload_fired_
, web_contents
);
352 ProcessPendingTabs();
354 base::MessageLoop::current()->PostTask(
356 base::Bind(&UnloadController::ProcessPendingTabs
,
357 weak_factory_
.GetWeakPtr()));
362 } // namespace chrome