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