ExtensionSyncService: listen for relevant changes instead of being explicitly called...
[chromium-blink-merge.git] / chrome / browser / ui / unload_controller.cc
blob4dfcf3633a123951e42894430c0a9e06f2bef200
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/location.h"
8 #include "base/single_thread_task_runner.h"
9 #include "base/thread_task_runner_handle.h"
10 #include "chrome/browser/chrome_notification_types.h"
11 #include "chrome/browser/devtools/devtools_window.h"
12 #include "chrome/browser/ui/browser.h"
13 #include "chrome/browser/ui/browser_tabstrip.h"
14 #include "chrome/browser/ui/tabs/tab_strip_model.h"
15 #include "content/public/browser/notification_service.h"
16 #include "content/public/browser/notification_source.h"
17 #include "content/public/browser/notification_types.h"
18 #include "content/public/browser/render_view_host.h"
19 #include "content/public/browser/web_contents.h"
21 namespace chrome {
23 ////////////////////////////////////////////////////////////////////////////////
24 // UnloadController, public:
26 UnloadController::UnloadController(Browser* browser)
27 : browser_(browser),
28 is_attempting_to_close_browser_(false),
29 weak_factory_(this) {
30 browser_->tab_strip_model()->AddObserver(this);
33 UnloadController::~UnloadController() {
34 browser_->tab_strip_model()->RemoveObserver(this);
37 bool UnloadController::CanCloseContents(content::WebContents* contents) {
38 // Don't try to close the tab when the whole browser is being closed, since
39 // that avoids the fast shutdown path where we just kill all the renderers.
40 if (is_attempting_to_close_browser_)
41 ClearUnloadState(contents, true);
42 return !is_attempting_to_close_browser_ ||
43 is_calling_before_unload_handlers();
46 // static
47 bool UnloadController::ShouldRunUnloadEventsHelper(
48 content::WebContents* contents) {
49 // If |contents| is being inspected, devtools needs to intercept beforeunload
50 // events.
51 return DevToolsWindow::GetInstanceForInspectedWebContents(contents) != NULL;
54 // static
55 bool UnloadController::RunUnloadEventsHelper(content::WebContents* contents) {
56 // If there's a devtools window attached to |contents|,
57 // we would like devtools to call its own beforeunload handlers first,
58 // and then call beforeunload handlers for |contents|.
59 // See DevToolsWindow::InterceptPageBeforeUnload for details.
60 if (DevToolsWindow::InterceptPageBeforeUnload(contents)) {
61 return true;
63 // If the WebContents is not connected yet, then there's no unload
64 // handler we can fire even if the WebContents has an unload listener.
65 // One case where we hit this is in a tab that has an infinite loop
66 // before load.
67 if (contents->NeedToFireBeforeUnload()) {
68 // If the page has unload listeners, then we tell the renderer to fire
69 // them. Once they have fired, we'll get a message back saying whether
70 // to proceed closing the page or not, which sends us back to this method
71 // with the NeedToFireBeforeUnload bit cleared.
72 contents->DispatchBeforeUnload(false);
73 return true;
75 return false;
78 bool UnloadController::BeforeUnloadFired(content::WebContents* contents,
79 bool proceed) {
80 if (!proceed)
81 DevToolsWindow::OnPageCloseCanceled(contents);
83 if (!is_attempting_to_close_browser_) {
84 if (!proceed)
85 contents->SetClosedByUserGesture(false);
86 return proceed;
89 if (!proceed) {
90 CancelWindowClose();
91 contents->SetClosedByUserGesture(false);
92 return false;
95 if (RemoveFromSet(&tabs_needing_before_unload_fired_, contents)) {
96 // Now that beforeunload has fired, put the tab on the queue to fire
97 // unload.
98 tabs_needing_unload_fired_.insert(contents);
99 ProcessPendingTabs();
100 // We want to handle firing the unload event ourselves since we want to
101 // fire all the beforeunload events before attempting to fire the unload
102 // events should the user cancel closing the browser.
103 return false;
106 return true;
109 bool UnloadController::ShouldCloseWindow() {
110 if (HasCompletedUnloadProcessing())
111 return true;
113 // Special case for when we quit an application. The devtools window can
114 // close if it's beforeunload event has already fired which will happen due
115 // to the interception of it's content's beforeunload.
116 if (browser_->is_devtools() &&
117 DevToolsWindow::HasFiredBeforeUnloadEventForDevToolsBrowser(browser_)) {
118 return true;
121 // The behavior followed here varies based on the current phase of the
122 // operation and whether a batched shutdown is in progress.
124 // If there are tabs with outstanding beforeunload handlers:
125 // 1. If a batched shutdown is in progress: return false.
126 // This is to prevent interference with batched shutdown already in
127 // progress.
128 // 2. Otherwise: start sending beforeunload events and return false.
130 // Otherwise, If there are no tabs with outstanding beforeunload handlers:
131 // 3. If a batched shutdown is in progress: start sending unload events and
132 // return false.
133 // 4. Otherwise: return true.
134 is_attempting_to_close_browser_ = true;
135 // Cases 1 and 4.
136 bool need_beforeunload_fired = TabsNeedBeforeUnloadFired();
137 if (need_beforeunload_fired == is_calling_before_unload_handlers())
138 return !need_beforeunload_fired;
140 // Cases 2 and 3.
141 on_close_confirmed_.Reset();
142 ProcessPendingTabs();
143 return false;
146 bool UnloadController::CallBeforeUnloadHandlers(
147 const base::Callback<void(bool)>& on_close_confirmed) {
148 // The devtools browser gets its beforeunload events as the results of
149 // intercepting events from the inspected tab, so don't send them here as
150 // well.
151 if (browser_->is_devtools() || HasCompletedUnloadProcessing() ||
152 !TabsNeedBeforeUnloadFired())
153 return false;
155 is_attempting_to_close_browser_ = true;
156 on_close_confirmed_ = on_close_confirmed;
158 ProcessPendingTabs();
159 return true;
162 void UnloadController::ResetBeforeUnloadHandlers() {
163 if (!is_calling_before_unload_handlers())
164 return;
165 CancelWindowClose();
168 bool UnloadController::TabsNeedBeforeUnloadFired() {
169 if (tabs_needing_before_unload_fired_.empty()) {
170 for (int i = 0; i < browser_->tab_strip_model()->count(); ++i) {
171 content::WebContents* contents =
172 browser_->tab_strip_model()->GetWebContentsAt(i);
173 bool should_fire_beforeunload = contents->NeedToFireBeforeUnload() ||
174 DevToolsWindow::NeedsToInterceptBeforeUnload(contents);
175 if (!ContainsKey(tabs_needing_unload_fired_, contents) &&
176 should_fire_beforeunload) {
177 tabs_needing_before_unload_fired_.insert(contents);
181 return !tabs_needing_before_unload_fired_.empty();
184 void UnloadController::CancelWindowClose() {
185 // Closing of window can be canceled from a beforeunload handler.
186 DCHECK(is_attempting_to_close_browser_);
187 tabs_needing_before_unload_fired_.clear();
188 for (UnloadListenerSet::iterator it = tabs_needing_unload_fired_.begin();
189 it != tabs_needing_unload_fired_.end(); ++it) {
190 DevToolsWindow::OnPageCloseCanceled(*it);
192 tabs_needing_unload_fired_.clear();
193 if (is_calling_before_unload_handlers()) {
194 base::Callback<void(bool)> on_close_confirmed = on_close_confirmed_;
195 on_close_confirmed_.Reset();
196 on_close_confirmed.Run(false);
198 is_attempting_to_close_browser_ = false;
200 content::NotificationService::current()->Notify(
201 chrome::NOTIFICATION_BROWSER_CLOSE_CANCELLED,
202 content::Source<Browser>(browser_),
203 content::NotificationService::NoDetails());
206 ////////////////////////////////////////////////////////////////////////////////
207 // UnloadController, content::NotificationObserver implementation:
209 void UnloadController::Observe(int type,
210 const content::NotificationSource& source,
211 const content::NotificationDetails& details) {
212 switch (type) {
213 case content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED:
214 if (is_attempting_to_close_browser_) {
215 ClearUnloadState(content::Source<content::WebContents>(source).ptr(),
216 false); // See comment for ClearUnloadState().
218 break;
219 default:
220 NOTREACHED() << "Got a notification we didn't register for.";
224 ////////////////////////////////////////////////////////////////////////////////
225 // UnloadController, TabStripModelObserver implementation:
227 void UnloadController::TabInsertedAt(content::WebContents* contents,
228 int index,
229 bool foreground) {
230 TabAttachedImpl(contents);
233 void UnloadController::TabDetachedAt(content::WebContents* contents,
234 int index) {
235 TabDetachedImpl(contents);
238 void UnloadController::TabReplacedAt(TabStripModel* tab_strip_model,
239 content::WebContents* old_contents,
240 content::WebContents* new_contents,
241 int index) {
242 TabDetachedImpl(old_contents);
243 TabAttachedImpl(new_contents);
246 void UnloadController::TabStripEmpty() {
247 // Set is_attempting_to_close_browser_ here, so that extensions, etc, do not
248 // attempt to add tabs to the browser before it closes.
249 is_attempting_to_close_browser_ = true;
252 ////////////////////////////////////////////////////////////////////////////////
253 // UnloadController, private:
255 void UnloadController::TabAttachedImpl(content::WebContents* contents) {
256 // If the tab crashes in the beforeunload or unload handler, it won't be
257 // able to ack. But we know we can close it.
258 registrar_.Add(
259 this,
260 content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED,
261 content::Source<content::WebContents>(contents));
264 void UnloadController::TabDetachedImpl(content::WebContents* contents) {
265 if (is_attempting_to_close_browser_)
266 ClearUnloadState(contents, false);
267 registrar_.Remove(this,
268 content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED,
269 content::Source<content::WebContents>(contents));
272 void UnloadController::ProcessPendingTabs() {
273 if (!is_attempting_to_close_browser_) {
274 // Because we might invoke this after a delay it's possible for the value of
275 // is_attempting_to_close_browser_ to have changed since we scheduled the
276 // task.
277 return;
280 if (HasCompletedUnloadProcessing()) {
281 // We've finished all the unload events and can proceed to close the
282 // browser.
283 browser_->OnWindowClosing();
284 return;
287 // Process beforeunload tabs first. When that queue is empty, process
288 // unload tabs.
289 if (!tabs_needing_before_unload_fired_.empty()) {
290 content::WebContents* web_contents =
291 *(tabs_needing_before_unload_fired_.begin());
292 // Null check render_view_host here as this gets called on a PostTask and
293 // the tab's render_view_host may have been nulled out.
294 if (web_contents->GetRenderViewHost()) {
295 // If there's a devtools window attached to |web_contents|,
296 // we would like devtools to call its own beforeunload handlers first,
297 // and then call beforeunload handlers for |web_contents|.
298 // See DevToolsWindow::InterceptPageBeforeUnload for details.
299 if (!DevToolsWindow::InterceptPageBeforeUnload(web_contents))
300 web_contents->DispatchBeforeUnload(false);
301 } else {
302 ClearUnloadState(web_contents, true);
304 } else if (is_calling_before_unload_handlers()) {
305 base::Callback<void(bool)> on_close_confirmed = on_close_confirmed_;
306 // Reset |on_close_confirmed_| in case the callback tests
307 // |is_calling_before_unload_handlers()|, we want to return that calling
308 // is complete.
309 if (tabs_needing_unload_fired_.empty())
310 on_close_confirmed_.Reset();
311 on_close_confirmed.Run(true);
312 } else if (!tabs_needing_unload_fired_.empty()) {
313 // We've finished firing all beforeunload events and can proceed with unload
314 // events.
315 // TODO(ojan): We should add a call to browser_shutdown::OnShutdownStarting
316 // somewhere around here so that we have accurate measurements of shutdown
317 // time.
318 // TODO(ojan): We can probably fire all the unload events in parallel and
319 // get a perf benefit from that in the cases where the tab hangs in it's
320 // unload handler or takes a long time to page in.
321 content::WebContents* web_contents = *(tabs_needing_unload_fired_.begin());
322 // Null check render_view_host here as this gets called on a PostTask and
323 // the tab's render_view_host may have been nulled out.
324 if (web_contents->GetRenderViewHost()) {
325 web_contents->ClosePage();
326 } else {
327 ClearUnloadState(web_contents, true);
329 } else {
330 NOTREACHED();
334 bool UnloadController::HasCompletedUnloadProcessing() const {
335 return is_attempting_to_close_browser_ &&
336 tabs_needing_before_unload_fired_.empty() &&
337 tabs_needing_unload_fired_.empty();
340 bool UnloadController::RemoveFromSet(UnloadListenerSet* set,
341 content::WebContents* web_contents) {
342 DCHECK(is_attempting_to_close_browser_);
344 UnloadListenerSet::iterator iter =
345 std::find(set->begin(), set->end(), web_contents);
346 if (iter != set->end()) {
347 set->erase(iter);
348 return true;
350 return false;
353 void UnloadController::ClearUnloadState(content::WebContents* web_contents,
354 bool process_now) {
355 if (is_attempting_to_close_browser_) {
356 RemoveFromSet(&tabs_needing_before_unload_fired_, web_contents);
357 RemoveFromSet(&tabs_needing_unload_fired_, web_contents);
358 if (process_now) {
359 ProcessPendingTabs();
360 } else {
361 base::ThreadTaskRunnerHandle::Get()->PostTask(
362 FROM_HERE, base::Bind(&UnloadController::ProcessPendingTabs,
363 weak_factory_.GetWeakPtr()));
368 } // namespace chrome