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