1 // Copyright 2013 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/fast_unload_controller.h"
7 #include "base/logging.h"
8 #include "base/message_loop/message_loop.h"
9 #include "chrome/browser/chrome_notification_types.h"
10 #include "chrome/browser/devtools/devtools_window.h"
11 #include "chrome/browser/ui/browser.h"
12 #include "chrome/browser/ui/browser_tabstrip.h"
13 #include "chrome/browser/ui/tab_contents/core_tab_helper.h"
14 #include "chrome/browser/ui/tabs/tab_strip_model.h"
15 #include "chrome/browser/ui/tabs/tab_strip_model_delegate.h"
16 #include "content/public/browser/notification_service.h"
17 #include "content/public/browser/notification_source.h"
18 #include "content/public/browser/notification_types.h"
19 #include "content/public/browser/render_view_host.h"
20 #include "content/public/browser/web_contents.h"
21 #include "content/public/browser/web_contents_delegate.h"
26 ////////////////////////////////////////////////////////////////////////////////
27 // DetachedWebContentsDelegate will delete web contents when they close.
28 class FastUnloadController::DetachedWebContentsDelegate
29 : public content::WebContentsDelegate
{
31 DetachedWebContentsDelegate() { }
32 virtual ~DetachedWebContentsDelegate() { }
35 // WebContentsDelegate implementation.
36 virtual bool ShouldSuppressDialogs() OVERRIDE
{
37 return true; // Return true so dialogs are suppressed.
40 virtual void CloseContents(content::WebContents
* source
) OVERRIDE
{
41 // Finished detached close.
42 // FastUnloadController will observe
43 // |NOTIFICATION_WEB_CONTENTS_DISCONNECTED|.
47 DISALLOW_COPY_AND_ASSIGN(DetachedWebContentsDelegate
);
50 ////////////////////////////////////////////////////////////////////////////////
51 // FastUnloadController, public:
53 FastUnloadController::FastUnloadController(Browser
* browser
)
55 tab_needing_before_unload_ack_(NULL
),
56 is_attempting_to_close_browser_(false),
57 detached_delegate_(new DetachedWebContentsDelegate()),
59 browser_
->tab_strip_model()->AddObserver(this);
62 FastUnloadController::~FastUnloadController() {
63 browser_
->tab_strip_model()->RemoveObserver(this);
66 bool FastUnloadController::CanCloseContents(content::WebContents
* contents
) {
67 // Don't try to close the tab when the whole browser is being closed, since
68 // that avoids the fast shutdown path where we just kill all the renderers.
69 return !is_attempting_to_close_browser_
||
70 is_calling_before_unload_handlers();
74 bool FastUnloadController::ShouldRunUnloadEventsHelper(
75 content::WebContents
* contents
) {
76 // If |contents| is being inspected, devtools needs to intercept beforeunload
78 return DevToolsWindow::GetInstanceForInspectedRenderViewHost(
79 contents
->GetRenderViewHost()) != NULL
;
83 bool FastUnloadController::RunUnloadEventsHelper(
84 content::WebContents
* contents
) {
85 // If there's a devtools window attached to |contents|,
86 // we would like devtools to call its own beforeunload handlers first,
87 // and then call beforeunload handlers for |contents|.
88 // See DevToolsWindow::InterceptPageBeforeUnload for details.
89 if (DevToolsWindow::InterceptPageBeforeUnload(contents
)) {
92 // If the WebContents is not connected yet, then there's no unload
93 // handler we can fire even if the WebContents has an unload listener.
94 // One case where we hit this is in a tab that has an infinite loop
96 if (contents
->NeedToFireBeforeUnload()) {
97 // If the page has unload listeners, then we tell the renderer to fire
98 // them. Once they have fired, we'll get a message back saying whether
99 // to proceed closing the page or not, which sends us back to this method
100 // with the NeedToFireBeforeUnload bit cleared.
101 contents
->GetRenderViewHost()->FirePageBeforeUnload(false);
107 bool FastUnloadController::BeforeUnloadFired(content::WebContents
* contents
,
110 DevToolsWindow::OnPageCloseCanceled(contents
);
112 if (!is_attempting_to_close_browser_
) {
114 contents
->SetClosedByUserGesture(false);
116 // No more dialogs are possible, so remove the tab and finish
117 // running unload listeners asynchrounously.
118 browser_
->tab_strip_model()->delegate()->CreateHistoricalTab(contents
);
119 DetachWebContents(contents
);
126 contents
->SetClosedByUserGesture(false);
130 if (tab_needing_before_unload_ack_
== contents
) {
131 // Now that beforeunload has fired, queue the tab to fire unload.
132 tab_needing_before_unload_ack_
= NULL
;
133 tabs_needing_unload_
.insert(contents
);
134 ProcessPendingTabs();
135 // We want to handle firing the unload event ourselves since we want to
136 // fire all the beforeunload events before attempting to fire the unload
137 // events should the user cancel closing the browser.
144 bool FastUnloadController::ShouldCloseWindow() {
145 if (HasCompletedUnloadProcessing())
148 // Special case for when we quit an application. The Devtools window can
149 // close if it's beforeunload event has already fired which will happen due
150 // to the interception of it's content's beforeunload.
151 if (browser_
->is_devtools() &&
152 DevToolsWindow::HasFiredBeforeUnloadEventForDevToolsBrowser(browser_
)) {
156 // The behavior followed here varies based on the current phase of the
157 // operation and whether a batched shutdown is in progress.
159 // If there are tabs with outstanding beforeunload handlers:
160 // 1. If a batched shutdown is in progress: return false.
161 // This is to prevent interference with batched shutdown already in
163 // 2. Otherwise: start sending beforeunload events and return false.
165 // Otherwise, If there are no tabs with outstanding beforeunload handlers:
166 // 3. If a batched shutdown is in progress: start sending unload events and
168 // 4. Otherwise: return true.
169 is_attempting_to_close_browser_
= true;
171 bool need_beforeunload_fired
= TabsNeedBeforeUnloadFired();
172 if (need_beforeunload_fired
== is_calling_before_unload_handlers())
173 return !need_beforeunload_fired
;
176 on_close_confirmed_
.Reset();
177 ProcessPendingTabs();
181 bool FastUnloadController::CallBeforeUnloadHandlers(
182 const base::Callback
<void(bool)>& on_close_confirmed
) {
183 // The devtools browser gets its beforeunload events as the results of
184 // intercepting events from the inspected tab, so don't send them here as well.
185 if (browser_
->is_devtools() || !TabsNeedBeforeUnloadFired())
188 on_close_confirmed_
= on_close_confirmed
;
189 is_attempting_to_close_browser_
= true;
190 ProcessPendingTabs();
194 void FastUnloadController::ResetBeforeUnloadHandlers() {
195 if (!is_calling_before_unload_handlers())
200 bool FastUnloadController::TabsNeedBeforeUnloadFired() {
201 if (!tabs_needing_before_unload_
.empty() ||
202 tab_needing_before_unload_ack_
!= NULL
)
205 if (!is_calling_before_unload_handlers() && !tabs_needing_unload_
.empty())
208 for (int i
= 0; i
< browser_
->tab_strip_model()->count(); ++i
) {
209 content::WebContents
* contents
=
210 browser_
->tab_strip_model()->GetWebContentsAt(i
);
211 bool should_fire_beforeunload
= contents
->NeedToFireBeforeUnload() ||
212 DevToolsWindow::NeedsToInterceptBeforeUnload(contents
);
213 if (!ContainsKey(tabs_needing_unload_
, contents
) &&
214 !ContainsKey(tabs_needing_unload_ack_
, contents
) &&
215 tab_needing_before_unload_ack_
!= contents
&&
216 should_fire_beforeunload
)
217 tabs_needing_before_unload_
.insert(contents
);
219 return !tabs_needing_before_unload_
.empty();
222 ////////////////////////////////////////////////////////////////////////////////
223 // FastUnloadController, content::NotificationObserver implementation:
225 void FastUnloadController::Observe(
227 const content::NotificationSource
& source
,
228 const content::NotificationDetails
& details
) {
230 case content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED
: {
231 registrar_
.Remove(this,
232 content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED
,
234 content::WebContents
* contents
=
235 content::Source
<content::WebContents
>(source
).ptr();
236 ClearUnloadState(contents
);
240 NOTREACHED() << "Got a notification we didn't register for.";
244 ////////////////////////////////////////////////////////////////////////////////
245 // FastUnloadController, TabStripModelObserver implementation:
247 void FastUnloadController::TabInsertedAt(content::WebContents
* contents
,
250 TabAttachedImpl(contents
);
253 void FastUnloadController::TabDetachedAt(content::WebContents
* contents
,
255 TabDetachedImpl(contents
);
258 void FastUnloadController::TabReplacedAt(TabStripModel
* tab_strip_model
,
259 content::WebContents
* old_contents
,
260 content::WebContents
* new_contents
,
262 TabDetachedImpl(old_contents
);
263 TabAttachedImpl(new_contents
);
266 void FastUnloadController::TabStripEmpty() {
267 // Set is_attempting_to_close_browser_ here, so that extensions, etc, do not
268 // attempt to add tabs to the browser before it closes.
269 is_attempting_to_close_browser_
= true;
272 ////////////////////////////////////////////////////////////////////////////////
273 // FastUnloadController, private:
275 void FastUnloadController::TabAttachedImpl(content::WebContents
* contents
) {
276 // If the tab crashes in the beforeunload or unload handler, it won't be
277 // able to ack. But we know we can close it.
280 content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED
,
281 content::Source
<content::WebContents
>(contents
));
284 void FastUnloadController::TabDetachedImpl(content::WebContents
* contents
) {
285 if (tabs_needing_unload_ack_
.find(contents
) !=
286 tabs_needing_unload_ack_
.end()) {
287 // Tab needs unload to complete.
288 // It will send |NOTIFICATION_WEB_CONTENTS_DISCONNECTED| when done.
292 // If WEB_CONTENTS_DISCONNECTED was received then the notification may have
293 // already been unregistered.
294 const content::NotificationSource
& source
=
295 content::Source
<content::WebContents
>(contents
);
296 if (registrar_
.IsRegistered(this,
297 content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED
,
299 registrar_
.Remove(this,
300 content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED
,
304 if (is_attempting_to_close_browser_
)
305 ClearUnloadState(contents
);
308 bool FastUnloadController::DetachWebContents(content::WebContents
* contents
) {
309 int index
= browser_
->tab_strip_model()->GetIndexOfWebContents(contents
);
310 if (index
!= TabStripModel::kNoTab
&&
311 contents
->NeedToFireBeforeUnload()) {
312 tabs_needing_unload_ack_
.insert(contents
);
313 browser_
->tab_strip_model()->DetachWebContentsAt(index
);
314 contents
->SetDelegate(detached_delegate_
.get());
315 CoreTabHelper
* core_tab_helper
= CoreTabHelper::FromWebContents(contents
);
316 core_tab_helper
->OnUnloadDetachedStarted();
322 void FastUnloadController::ProcessPendingTabs() {
323 if (!is_attempting_to_close_browser_
) {
324 // Because we might invoke this after a delay it's possible for the value of
325 // is_attempting_to_close_browser_ to have changed since we scheduled the
330 if (tab_needing_before_unload_ack_
!= NULL
) {
331 // Wait for |BeforeUnloadFired| before proceeding.
335 // Process a beforeunload handler.
336 if (!tabs_needing_before_unload_
.empty()) {
337 WebContentsSet::iterator it
= tabs_needing_before_unload_
.begin();
338 content::WebContents
* contents
= *it
;
339 tabs_needing_before_unload_
.erase(it
);
340 // Null check render_view_host here as this gets called on a PostTask and
341 // the tab's render_view_host may have been nulled out.
342 if (contents
->GetRenderViewHost()) {
343 tab_needing_before_unload_ack_
= contents
;
345 CoreTabHelper
* core_tab_helper
= CoreTabHelper::FromWebContents(contents
);
346 core_tab_helper
->OnCloseStarted();
348 // If there's a devtools window attached to |contents|,
349 // we would like devtools to call its own beforeunload handlers first,
350 // and then call beforeunload handlers for |contents|.
351 // See DevToolsWindow::InterceptPageBeforeUnload for details.
352 if (!DevToolsWindow::InterceptPageBeforeUnload(contents
))
353 contents
->GetRenderViewHost()->FirePageBeforeUnload(false);
355 ProcessPendingTabs();
360 if (is_calling_before_unload_handlers()) {
361 on_close_confirmed_
.Run(true);
364 // Process all the unload handlers. (The beforeunload handlers have finished.)
365 if (!tabs_needing_unload_
.empty()) {
366 browser_
->OnWindowClosing();
368 // Run unload handlers detached since no more interaction is possible.
369 WebContentsSet::iterator it
= tabs_needing_unload_
.begin();
370 while (it
!= tabs_needing_unload_
.end()) {
371 WebContentsSet::iterator current
= it
++;
372 content::WebContents
* contents
= *current
;
373 tabs_needing_unload_
.erase(current
);
374 // Null check render_view_host here as this gets called on a PostTask
375 // and the tab's render_view_host may have been nulled out.
376 if (contents
->GetRenderViewHost()) {
377 CoreTabHelper
* core_tab_helper
=
378 CoreTabHelper::FromWebContents(contents
);
379 core_tab_helper
->OnUnloadStarted();
380 DetachWebContents(contents
);
381 contents
->GetRenderViewHost()->ClosePage();
385 // Get the browser hidden.
386 if (browser_
->tab_strip_model()->empty()) {
387 browser_
->TabStripEmpty();
389 browser_
->tab_strip_model()->CloseAllTabs(); // tabs not needing unload
394 if (HasCompletedUnloadProcessing()) {
395 browser_
->OnWindowClosing();
397 // Get the browser closed.
398 if (browser_
->tab_strip_model()->empty()) {
399 browser_
->TabStripEmpty();
401 // There may be tabs if the last tab needing beforeunload crashed.
402 browser_
->tab_strip_model()->CloseAllTabs();
408 bool FastUnloadController::HasCompletedUnloadProcessing() const {
409 return is_attempting_to_close_browser_
&&
410 tabs_needing_before_unload_
.empty() &&
411 tab_needing_before_unload_ack_
== NULL
&&
412 tabs_needing_unload_
.empty() &&
413 tabs_needing_unload_ack_
.empty();
416 void FastUnloadController::CancelWindowClose() {
417 // Closing of window can be canceled from a beforeunload handler.
418 DCHECK(is_attempting_to_close_browser_
);
419 tabs_needing_before_unload_
.clear();
420 if (tab_needing_before_unload_ack_
!= NULL
) {
421 CoreTabHelper
* core_tab_helper
=
422 CoreTabHelper::FromWebContents(tab_needing_before_unload_ack_
);
423 core_tab_helper
->OnCloseCanceled();
424 DevToolsWindow::OnPageCloseCanceled(tab_needing_before_unload_ack_
);
425 tab_needing_before_unload_ack_
= NULL
;
427 for (WebContentsSet::iterator it
= tabs_needing_unload_
.begin();
428 it
!= tabs_needing_unload_
.end(); it
++) {
429 content::WebContents
* contents
= *it
;
431 CoreTabHelper
* core_tab_helper
= CoreTabHelper::FromWebContents(contents
);
432 core_tab_helper
->OnCloseCanceled();
433 DevToolsWindow::OnPageCloseCanceled(contents
);
435 tabs_needing_unload_
.clear();
437 // No need to clear tabs_needing_unload_ack_. Those tabs are already detached.
439 if (is_calling_before_unload_handlers()) {
440 base::Callback
<void(bool)> on_close_confirmed
= on_close_confirmed_
;
441 on_close_confirmed_
.Reset();
442 on_close_confirmed
.Run(false);
445 is_attempting_to_close_browser_
= false;
447 content::NotificationService::current()->Notify(
448 chrome::NOTIFICATION_BROWSER_CLOSE_CANCELLED
,
449 content::Source
<Browser
>(browser_
),
450 content::NotificationService::NoDetails());
453 void FastUnloadController::ClearUnloadState(content::WebContents
* contents
) {
454 if (tabs_needing_unload_ack_
.erase(contents
) > 0) {
455 if (HasCompletedUnloadProcessing())
456 PostTaskForProcessPendingTabs();
460 if (!is_attempting_to_close_browser_
)
463 if (tab_needing_before_unload_ack_
== contents
) {
464 tab_needing_before_unload_ack_
= NULL
;
465 PostTaskForProcessPendingTabs();
469 if (tabs_needing_before_unload_
.erase(contents
) > 0 ||
470 tabs_needing_unload_
.erase(contents
) > 0) {
471 if (tab_needing_before_unload_ack_
== NULL
)
472 PostTaskForProcessPendingTabs();
476 void FastUnloadController::PostTaskForProcessPendingTabs() {
477 base::MessageLoop::current()->PostTask(
479 base::Bind(&FastUnloadController::ProcessPendingTabs
,
480 weak_factory_
.GetWeakPtr()));
483 } // namespace chrome