unload_controller.cc revision eb525c5499e34cc9c4b825d6d9e75bb07cc06ace
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.
4
5#include "chrome/browser/ui/unload_controller.h"
6
7#include "base/message_loop.h"
8#include "chrome/browser/ui/browser.h"
9#include "chrome/browser/ui/browser_tabstrip.h"
10#include "chrome/browser/ui/tabs/tab_strip_model.h"
11#include "chrome/common/chrome_notification_types.h"
12#include "content/public/browser/notification_service.h"
13#include "content/public/browser/notification_source.h"
14#include "content/public/browser/notification_types.h"
15#include "content/public/browser/render_view_host.h"
16#include "content/public/browser/web_contents.h"
17
18namespace chrome {
19
20////////////////////////////////////////////////////////////////////////////////
21// UnloadController, public:
22
23UnloadController::UnloadController(Browser* browser)
24    : browser_(browser),
25      is_attempting_to_close_browser_(false),
26      weak_factory_(this) {
27  browser_->tab_strip_model()->AddObserver(this);
28}
29
30UnloadController::~UnloadController() {
31  browser_->tab_strip_model()->RemoveObserver(this);
32}
33
34bool UnloadController::CanCloseContents(content::WebContents* contents) {
35  // Don't try to close the tab when the whole browser is being closed, since
36  // that avoids the fast shutdown path where we just kill all the renderers.
37  if (is_attempting_to_close_browser_)
38    ClearUnloadState(contents, true);
39  return !is_attempting_to_close_browser_;
40}
41
42bool UnloadController::BeforeUnloadFired(content::WebContents* contents,
43                                         bool proceed) {
44  if (!is_attempting_to_close_browser_) {
45    if (!proceed)
46      contents->SetClosedByUserGesture(false);
47    return proceed;
48  }
49
50  if (!proceed) {
51    CancelWindowClose();
52    contents->SetClosedByUserGesture(false);
53    return false;
54  }
55
56  if (RemoveFromSet(&tabs_needing_before_unload_fired_, contents)) {
57    // Now that beforeunload has fired, put the tab on the queue to fire
58    // unload.
59    tabs_needing_unload_fired_.insert(contents);
60    ProcessPendingTabs();
61    // We want to handle firing the unload event ourselves since we want to
62    // fire all the beforeunload events before attempting to fire the unload
63    // events should the user cancel closing the browser.
64    return false;
65  }
66
67  return true;
68}
69
70bool UnloadController::ShouldCloseWindow() {
71  if (HasCompletedUnloadProcessing())
72    return true;
73
74  is_attempting_to_close_browser_ = true;
75
76  if (!TabsNeedBeforeUnloadFired())
77    return true;
78
79  ProcessPendingTabs();
80  return false;
81}
82
83bool UnloadController::TabsNeedBeforeUnloadFired() {
84  if (tabs_needing_before_unload_fired_.empty()) {
85    for (int i = 0; i < browser_->tab_strip_model()->count(); ++i) {
86      content::WebContents* contents =
87          browser_->tab_strip_model()->GetWebContentsAt(i);
88      if (contents->NeedToFireBeforeUnload())
89        tabs_needing_before_unload_fired_.insert(contents);
90    }
91  }
92  return !tabs_needing_before_unload_fired_.empty();
93}
94
95////////////////////////////////////////////////////////////////////////////////
96// UnloadController, content::NotificationObserver implementation:
97
98void UnloadController::Observe(int type,
99                               const content::NotificationSource& source,
100                               const content::NotificationDetails& details) {
101  switch (type) {
102    case content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED:
103      if (is_attempting_to_close_browser_) {
104        ClearUnloadState(content::Source<content::WebContents>(source).ptr(),
105                         false);  // See comment for ClearUnloadState().
106      }
107      break;
108    default:
109      NOTREACHED() << "Got a notification we didn't register for.";
110  }
111}
112
113////////////////////////////////////////////////////////////////////////////////
114// UnloadController, TabStripModelObserver implementation:
115
116void UnloadController::TabInsertedAt(content::WebContents* contents,
117                                     int index,
118                                     bool foreground) {
119  TabAttachedImpl(contents);
120}
121
122void UnloadController::TabDetachedAt(content::WebContents* contents,
123                                     int index) {
124  TabDetachedImpl(contents);
125}
126
127void UnloadController::TabReplacedAt(TabStripModel* tab_strip_model,
128                                     content::WebContents* old_contents,
129                                     content::WebContents* new_contents,
130                                     int index) {
131  TabDetachedImpl(old_contents);
132  TabAttachedImpl(new_contents);
133}
134
135void UnloadController::TabStripEmpty() {
136  // Set is_attempting_to_close_browser_ here, so that extensions, etc, do not
137  // attempt to add tabs to the browser before it closes.
138  is_attempting_to_close_browser_ = true;
139}
140
141////////////////////////////////////////////////////////////////////////////////
142// UnloadController, private:
143
144void UnloadController::TabAttachedImpl(content::WebContents* contents) {
145  // If the tab crashes in the beforeunload or unload handler, it won't be
146  // able to ack. But we know we can close it.
147  registrar_.Add(
148      this,
149      content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED,
150      content::Source<content::WebContents>(contents));
151}
152
153void UnloadController::TabDetachedImpl(content::WebContents* contents) {
154  if (is_attempting_to_close_browser_)
155    ClearUnloadState(contents, false);
156  registrar_.Remove(this,
157                    content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED,
158                    content::Source<content::WebContents>(contents));
159}
160
161void UnloadController::ProcessPendingTabs() {
162  if (!is_attempting_to_close_browser_) {
163    // Because we might invoke this after a delay it's possible for the value of
164    // is_attempting_to_close_browser_ to have changed since we scheduled the
165    // task.
166    return;
167  }
168
169  if (HasCompletedUnloadProcessing()) {
170    // We've finished all the unload events and can proceed to close the
171    // browser.
172    browser_->OnWindowClosing();
173    return;
174  }
175
176  // Process beforeunload tabs first. When that queue is empty, process
177  // unload tabs.
178  if (!tabs_needing_before_unload_fired_.empty()) {
179    content::WebContents* web_contents =
180        *(tabs_needing_before_unload_fired_.begin());
181    // Null check render_view_host here as this gets called on a PostTask and
182    // the tab's render_view_host may have been nulled out.
183    if (web_contents->GetRenderViewHost()) {
184      web_contents->GetRenderViewHost()->FirePageBeforeUnload(false);
185    } else {
186      ClearUnloadState(web_contents, true);
187    }
188  } else if (!tabs_needing_unload_fired_.empty()) {
189    // We've finished firing all beforeunload events and can proceed with unload
190    // events.
191    // TODO(ojan): We should add a call to browser_shutdown::OnShutdownStarting
192    // somewhere around here so that we have accurate measurements of shutdown
193    // time.
194    // TODO(ojan): We can probably fire all the unload events in parallel and
195    // get a perf benefit from that in the cases where the tab hangs in it's
196    // unload handler or takes a long time to page in.
197    content::WebContents* web_contents = *(tabs_needing_unload_fired_.begin());
198    // Null check render_view_host here as this gets called on a PostTask and
199    // the tab's render_view_host may have been nulled out.
200    if (web_contents->GetRenderViewHost()) {
201      web_contents->GetRenderViewHost()->ClosePage();
202    } else {
203      ClearUnloadState(web_contents, true);
204    }
205  } else {
206    NOTREACHED();
207  }
208}
209
210bool UnloadController::HasCompletedUnloadProcessing() const {
211  return is_attempting_to_close_browser_ &&
212      tabs_needing_before_unload_fired_.empty() &&
213      tabs_needing_unload_fired_.empty();
214}
215
216void UnloadController::CancelWindowClose() {
217  // Closing of window can be canceled from a beforeunload handler.
218  DCHECK(is_attempting_to_close_browser_);
219  tabs_needing_before_unload_fired_.clear();
220  tabs_needing_unload_fired_.clear();
221  is_attempting_to_close_browser_ = false;
222
223  content::NotificationService::current()->Notify(
224      chrome::NOTIFICATION_BROWSER_CLOSE_CANCELLED,
225      content::Source<Browser>(browser_),
226      content::NotificationService::NoDetails());
227}
228
229bool UnloadController::RemoveFromSet(UnloadListenerSet* set,
230                                     content::WebContents* web_contents) {
231  DCHECK(is_attempting_to_close_browser_);
232
233  UnloadListenerSet::iterator iter =
234      std::find(set->begin(), set->end(), web_contents);
235  if (iter != set->end()) {
236    set->erase(iter);
237    return true;
238  }
239  return false;
240}
241
242void UnloadController::ClearUnloadState(content::WebContents* web_contents,
243                                        bool process_now) {
244  if (is_attempting_to_close_browser_) {
245    RemoveFromSet(&tabs_needing_before_unload_fired_, web_contents);
246    RemoveFromSet(&tabs_needing_unload_fired_, web_contents);
247    if (process_now) {
248      ProcessPendingTabs();
249    } else {
250      base::MessageLoop::current()->PostTask(
251          FROM_HERE,
252          base::Bind(&UnloadController::ProcessPendingTabs,
253                     weak_factory_.GetWeakPtr()));
254    }
255  }
256}
257
258}  // namespace chrome
259