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/ui/browser.h" 10 #include "chrome/browser/ui/browser_tabstrip.h" 11 #include "chrome/browser/ui/tabs/tab_strip_model.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 18 namespace chrome { 19 20 //////////////////////////////////////////////////////////////////////////////// 21 // UnloadController, public: 22 23 UnloadController::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 30 UnloadController::~UnloadController() { 31 browser_->tab_strip_model()->RemoveObserver(this); 32 } 33 34 bool 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 42 bool 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 70 bool 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 83 bool 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 98 void 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 116 void UnloadController::TabInsertedAt(content::WebContents* contents, 117 int index, 118 bool foreground) { 119 TabAttachedImpl(contents); 120 } 121 122 void UnloadController::TabDetachedAt(content::WebContents* contents, 123 int index) { 124 TabDetachedImpl(contents); 125 } 126 127 void 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 135 void 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 144 void 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 153 void 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 161 void 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 210 bool 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 216 void 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 229 bool 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 242 void 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