1 // Copyright 2013 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/fast_unload_controller.h" 6 7 #include "base/logging.h" 8 #include "base/message_loop/message_loop.h" 9 #include "chrome/browser/chrome_notification_types.h" 10 #include "chrome/browser/ui/browser.h" 11 #include "chrome/browser/ui/browser_tabstrip.h" 12 #include "chrome/browser/ui/tab_contents/core_tab_helper.h" 13 #include "chrome/browser/ui/tabs/tab_strip_model.h" 14 #include "chrome/browser/ui/tabs/tab_strip_model_delegate.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" 20 #include "content/public/browser/web_contents_delegate.h" 21 22 namespace chrome { 23 24 25 //////////////////////////////////////////////////////////////////////////////// 26 // DetachedWebContentsDelegate will delete web contents when they close. 27 class FastUnloadController::DetachedWebContentsDelegate 28 : public content::WebContentsDelegate { 29 public: 30 DetachedWebContentsDelegate() { } 31 virtual ~DetachedWebContentsDelegate() { } 32 33 private: 34 // WebContentsDelegate implementation. 35 virtual bool ShouldSuppressDialogs() OVERRIDE { 36 return true; // Return true so dialogs are suppressed. 37 } 38 39 virtual void CloseContents(content::WebContents* source) OVERRIDE { 40 // Finished detached close. 41 // FastUnloadController will observe 42 // |NOTIFICATION_WEB_CONTENTS_DISCONNECTED|. 43 delete source; 44 } 45 46 DISALLOW_COPY_AND_ASSIGN(DetachedWebContentsDelegate); 47 }; 48 49 //////////////////////////////////////////////////////////////////////////////// 50 // FastUnloadController, public: 51 52 FastUnloadController::FastUnloadController(Browser* browser) 53 : browser_(browser), 54 tab_needing_before_unload_ack_(NULL), 55 is_attempting_to_close_browser_(false), 56 detached_delegate_(new DetachedWebContentsDelegate()), 57 weak_factory_(this) { 58 browser_->tab_strip_model()->AddObserver(this); 59 } 60 61 FastUnloadController::~FastUnloadController() { 62 browser_->tab_strip_model()->RemoveObserver(this); 63 } 64 65 bool FastUnloadController::CanCloseContents(content::WebContents* contents) { 66 // Don't try to close the tab when the whole browser is being closed, since 67 // that avoids the fast shutdown path where we just kill all the renderers. 68 return !is_attempting_to_close_browser_; 69 } 70 71 bool FastUnloadController::BeforeUnloadFired(content::WebContents* contents, 72 bool proceed) { 73 if (!is_attempting_to_close_browser_) { 74 if (!proceed) { 75 contents->SetClosedByUserGesture(false); 76 } else { 77 // No more dialogs are possible, so remove the tab and finish 78 // running unload listeners asynchrounously. 79 browser_->tab_strip_model()->delegate()->CreateHistoricalTab(contents); 80 DetachWebContents(contents); 81 } 82 return proceed; 83 } 84 85 if (!proceed) { 86 CancelWindowClose(); 87 contents->SetClosedByUserGesture(false); 88 return false; 89 } 90 91 if (tab_needing_before_unload_ack_ == contents) { 92 // Now that beforeunload has fired, queue the tab to fire unload. 93 tab_needing_before_unload_ack_ = NULL; 94 tabs_needing_unload_.insert(contents); 95 ProcessPendingTabs(); 96 // We want to handle firing the unload event ourselves since we want to 97 // fire all the beforeunload events before attempting to fire the unload 98 // events should the user cancel closing the browser. 99 return false; 100 } 101 102 return true; 103 } 104 105 bool FastUnloadController::ShouldCloseWindow() { 106 if (HasCompletedUnloadProcessing()) 107 return true; 108 109 is_attempting_to_close_browser_ = true; 110 111 if (!TabsNeedBeforeUnloadFired()) 112 return true; 113 114 ProcessPendingTabs(); 115 return false; 116 } 117 118 bool FastUnloadController::TabsNeedBeforeUnloadFired() { 119 if (!tabs_needing_before_unload_.empty() || 120 tab_needing_before_unload_ack_ != NULL) 121 return true; 122 123 if (!tabs_needing_unload_.empty()) 124 return false; 125 126 for (int i = 0; i < browser_->tab_strip_model()->count(); ++i) { 127 content::WebContents* contents = 128 browser_->tab_strip_model()->GetWebContentsAt(i); 129 if (contents->NeedToFireBeforeUnload()) 130 tabs_needing_before_unload_.insert(contents); 131 } 132 return !tabs_needing_before_unload_.empty(); 133 } 134 135 //////////////////////////////////////////////////////////////////////////////// 136 // FastUnloadController, content::NotificationObserver implementation: 137 138 void FastUnloadController::Observe( 139 int type, 140 const content::NotificationSource& source, 141 const content::NotificationDetails& details) { 142 switch (type) { 143 case content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED: { 144 registrar_.Remove(this, 145 content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED, 146 source); 147 content::WebContents* contents = 148 content::Source<content::WebContents>(source).ptr(); 149 ClearUnloadState(contents); 150 break; 151 } 152 default: 153 NOTREACHED() << "Got a notification we didn't register for."; 154 } 155 } 156 157 //////////////////////////////////////////////////////////////////////////////// 158 // FastUnloadController, TabStripModelObserver implementation: 159 160 void FastUnloadController::TabInsertedAt(content::WebContents* contents, 161 int index, 162 bool foreground) { 163 TabAttachedImpl(contents); 164 } 165 166 void FastUnloadController::TabDetachedAt(content::WebContents* contents, 167 int index) { 168 TabDetachedImpl(contents); 169 } 170 171 void FastUnloadController::TabReplacedAt(TabStripModel* tab_strip_model, 172 content::WebContents* old_contents, 173 content::WebContents* new_contents, 174 int index) { 175 TabDetachedImpl(old_contents); 176 TabAttachedImpl(new_contents); 177 } 178 179 void FastUnloadController::TabStripEmpty() { 180 // Set is_attempting_to_close_browser_ here, so that extensions, etc, do not 181 // attempt to add tabs to the browser before it closes. 182 is_attempting_to_close_browser_ = true; 183 } 184 185 //////////////////////////////////////////////////////////////////////////////// 186 // FastUnloadController, private: 187 188 void FastUnloadController::TabAttachedImpl(content::WebContents* contents) { 189 // If the tab crashes in the beforeunload or unload handler, it won't be 190 // able to ack. But we know we can close it. 191 registrar_.Add( 192 this, 193 content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED, 194 content::Source<content::WebContents>(contents)); 195 } 196 197 void FastUnloadController::TabDetachedImpl(content::WebContents* contents) { 198 if (tabs_needing_unload_ack_.find(contents) != 199 tabs_needing_unload_ack_.end()) { 200 // Tab needs unload to complete. 201 // It will send |NOTIFICATION_WEB_CONTENTS_DISCONNECTED| when done. 202 return; 203 } 204 205 // If WEB_CONTENTS_DISCONNECTED was received then the notification may have 206 // already been unregistered. 207 const content::NotificationSource& source = 208 content::Source<content::WebContents>(contents); 209 if (registrar_.IsRegistered(this, 210 content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED, 211 source)) { 212 registrar_.Remove(this, 213 content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED, 214 source); 215 } 216 217 if (is_attempting_to_close_browser_) 218 ClearUnloadState(contents); 219 } 220 221 bool FastUnloadController::DetachWebContents(content::WebContents* contents) { 222 int index = browser_->tab_strip_model()->GetIndexOfWebContents(contents); 223 if (index != TabStripModel::kNoTab && 224 contents->NeedToFireBeforeUnload()) { 225 tabs_needing_unload_ack_.insert(contents); 226 browser_->tab_strip_model()->DetachWebContentsAt(index); 227 contents->SetDelegate(detached_delegate_.get()); 228 CoreTabHelper* core_tab_helper = CoreTabHelper::FromWebContents(contents); 229 core_tab_helper->OnUnloadDetachedStarted(); 230 return true; 231 } 232 return false; 233 } 234 235 void FastUnloadController::ProcessPendingTabs() { 236 if (!is_attempting_to_close_browser_) { 237 // Because we might invoke this after a delay it's possible for the value of 238 // is_attempting_to_close_browser_ to have changed since we scheduled the 239 // task. 240 return; 241 } 242 243 if (tab_needing_before_unload_ack_ != NULL) { 244 // Wait for |BeforeUnloadFired| before proceeding. 245 return; 246 } 247 248 // Process a beforeunload handler. 249 if (!tabs_needing_before_unload_.empty()) { 250 WebContentsSet::iterator it = tabs_needing_before_unload_.begin(); 251 content::WebContents* contents = *it; 252 tabs_needing_before_unload_.erase(it); 253 // Null check render_view_host here as this gets called on a PostTask and 254 // the tab's render_view_host may have been nulled out. 255 if (contents->GetRenderViewHost()) { 256 tab_needing_before_unload_ack_ = contents; 257 258 CoreTabHelper* core_tab_helper = CoreTabHelper::FromWebContents(contents); 259 core_tab_helper->OnCloseStarted(); 260 261 contents->GetRenderViewHost()->FirePageBeforeUnload(false); 262 } else { 263 ProcessPendingTabs(); 264 } 265 return; 266 } 267 268 // Process all the unload handlers. (The beforeunload handlers have finished.) 269 if (!tabs_needing_unload_.empty()) { 270 browser_->OnWindowClosing(); 271 272 // Run unload handlers detached since no more interaction is possible. 273 WebContentsSet::iterator it = tabs_needing_unload_.begin(); 274 while (it != tabs_needing_unload_.end()) { 275 WebContentsSet::iterator current = it++; 276 content::WebContents* contents = *current; 277 tabs_needing_unload_.erase(current); 278 // Null check render_view_host here as this gets called on a PostTask 279 // and the tab's render_view_host may have been nulled out. 280 if (contents->GetRenderViewHost()) { 281 CoreTabHelper* core_tab_helper = 282 CoreTabHelper::FromWebContents(contents); 283 core_tab_helper->OnUnloadStarted(); 284 DetachWebContents(contents); 285 contents->GetRenderViewHost()->ClosePage(); 286 } 287 } 288 289 // Get the browser hidden. 290 if (browser_->tab_strip_model()->empty()) { 291 browser_->TabStripEmpty(); 292 } else { 293 browser_->tab_strip_model()->CloseAllTabs(); // tabs not needing unload 294 } 295 return; 296 } 297 298 if (HasCompletedUnloadProcessing()) { 299 browser_->OnWindowClosing(); 300 301 // Get the browser closed. 302 if (browser_->tab_strip_model()->empty()) { 303 browser_->TabStripEmpty(); 304 } else { 305 // There may be tabs if the last tab needing beforeunload crashed. 306 browser_->tab_strip_model()->CloseAllTabs(); 307 } 308 return; 309 } 310 } 311 312 bool FastUnloadController::HasCompletedUnloadProcessing() const { 313 return is_attempting_to_close_browser_ && 314 tabs_needing_before_unload_.empty() && 315 tab_needing_before_unload_ack_ == NULL && 316 tabs_needing_unload_.empty() && 317 tabs_needing_unload_ack_.empty(); 318 } 319 320 void FastUnloadController::CancelWindowClose() { 321 // Closing of window can be canceled from a beforeunload handler. 322 DCHECK(is_attempting_to_close_browser_); 323 tabs_needing_before_unload_.clear(); 324 if (tab_needing_before_unload_ack_ != NULL) { 325 326 CoreTabHelper* core_tab_helper = 327 CoreTabHelper::FromWebContents(tab_needing_before_unload_ack_); 328 core_tab_helper->OnCloseCanceled(); 329 tab_needing_before_unload_ack_ = NULL; 330 } 331 for (WebContentsSet::iterator it = tabs_needing_unload_.begin(); 332 it != tabs_needing_unload_.end(); it++) { 333 content::WebContents* contents = *it; 334 335 CoreTabHelper* core_tab_helper = CoreTabHelper::FromWebContents(contents); 336 core_tab_helper->OnCloseCanceled(); 337 } 338 tabs_needing_unload_.clear(); 339 340 // No need to clear tabs_needing_unload_ack_. Those tabs are already detached. 341 342 is_attempting_to_close_browser_ = false; 343 344 content::NotificationService::current()->Notify( 345 chrome::NOTIFICATION_BROWSER_CLOSE_CANCELLED, 346 content::Source<Browser>(browser_), 347 content::NotificationService::NoDetails()); 348 } 349 350 void FastUnloadController::ClearUnloadState(content::WebContents* contents) { 351 if (tabs_needing_unload_ack_.erase(contents) > 0) { 352 if (HasCompletedUnloadProcessing()) 353 PostTaskForProcessPendingTabs(); 354 return; 355 } 356 357 if (!is_attempting_to_close_browser_) 358 return; 359 360 if (tab_needing_before_unload_ack_ == contents) { 361 tab_needing_before_unload_ack_ = NULL; 362 PostTaskForProcessPendingTabs(); 363 return; 364 } 365 366 if (tabs_needing_before_unload_.erase(contents) > 0 || 367 tabs_needing_unload_.erase(contents) > 0) { 368 if (tab_needing_before_unload_ack_ == NULL) 369 PostTaskForProcessPendingTabs(); 370 } 371 } 372 373 void FastUnloadController::PostTaskForProcessPendingTabs() { 374 base::MessageLoop::current()->PostTask( 375 FROM_HERE, 376 base::Bind(&FastUnloadController::ProcessPendingTabs, 377 weak_factory_.GetWeakPtr())); 378 } 379 380 } // namespace chrome 381