// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "chrome/browser/ui/unload_controller.h" #include "base/logging.h" #include "base/message_loop.h" #include "chrome/browser/ui/browser.h" #include "chrome/browser/ui/browser_tabstrip.h" #include "chrome/browser/ui/tabs/tab_strip_model.h" #include "chrome/browser/ui/tabs/tab_strip_model_delegate.h" #include "chrome/common/chrome_notification_types.h" #include "content/public/browser/notification_service.h" #include "content/public/browser/notification_source.h" #include "content/public/browser/notification_types.h" #include "content/public/browser/render_view_host.h" #include "content/public/browser/web_contents.h" #include "content/public/browser/web_contents_delegate.h" namespace chrome { //////////////////////////////////////////////////////////////////////////////// // DetachedWebContentsDelegate will delete web contents when they close. class UnloadController::DetachedWebContentsDelegate : public content::WebContentsDelegate { public: DetachedWebContentsDelegate() { } virtual ~DetachedWebContentsDelegate() { } private: // WebContentsDelegate implementation. virtual bool ShouldSuppressDialogs() OVERRIDE { return true; // Return true so dialogs are suppressed. } virtual void CloseContents(content::WebContents* source) OVERRIDE { // Finished detached close. // UnloadController will observe |NOTIFICATION_WEB_CONTENTS_DISCONNECTED|. delete source; } DISALLOW_COPY_AND_ASSIGN(DetachedWebContentsDelegate); }; //////////////////////////////////////////////////////////////////////////////// // UnloadController, public: UnloadController::UnloadController(Browser* browser) : browser_(browser), tab_needing_before_unload_ack_(NULL), is_attempting_to_close_browser_(false), detached_delegate_(new DetachedWebContentsDelegate()), weak_factory_(this) { browser_->tab_strip_model()->AddObserver(this); } UnloadController::~UnloadController() { browser_->tab_strip_model()->RemoveObserver(this); } bool UnloadController::CanCloseContents(content::WebContents* contents) { // Don't try to close the tab when the whole browser is being closed, since // that avoids the fast shutdown path where we just kill all the renderers. return !is_attempting_to_close_browser_; } bool UnloadController::BeforeUnloadFired(content::WebContents* contents, bool proceed) { if (!is_attempting_to_close_browser_) { if (!proceed) { contents->SetClosedByUserGesture(false); } else { // No more dialogs are possible, so remove the tab and finish // running unload listeners asynchrounously. browser_->tab_strip_model()->delegate()->CreateHistoricalTab(contents); DetachWebContents(contents); } return proceed; } if (!proceed) { CancelWindowClose(); contents->SetClosedByUserGesture(false); return false; } if (tab_needing_before_unload_ack_ == contents) { // Now that beforeunload has fired, queue the tab to fire unload. tab_needing_before_unload_ack_ = NULL; tabs_needing_unload_.insert(contents); ProcessPendingTabs(); // We want to handle firing the unload event ourselves since we want to // fire all the beforeunload events before attempting to fire the unload // events should the user cancel closing the browser. return false; } return true; } bool UnloadController::ShouldCloseWindow() { if (HasCompletedUnloadProcessing()) return true; is_attempting_to_close_browser_ = true; if (!TabsNeedBeforeUnloadFired()) return true; ProcessPendingTabs(); return false; } bool UnloadController::TabsNeedBeforeUnloadFired() { if (!tabs_needing_before_unload_.empty() || tab_needing_before_unload_ack_ != NULL) return true; if (!tabs_needing_unload_.empty()) return false; for (int i = 0; i < browser_->tab_strip_model()->count(); ++i) { content::WebContents* contents = browser_->tab_strip_model()->GetWebContentsAt(i); if (contents->NeedToFireBeforeUnload()) tabs_needing_before_unload_.insert(contents); } return !tabs_needing_before_unload_.empty(); } //////////////////////////////////////////////////////////////////////////////// // UnloadController, content::NotificationObserver implementation: void UnloadController::Observe(int type, const content::NotificationSource& source, const content::NotificationDetails& details) { switch (type) { case content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED: { registrar_.Remove(this, content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED, source); content::WebContents* contents = content::Source<content::WebContents>(source).ptr(); ClearUnloadState(contents); break; } default: NOTREACHED() << "Got a notification we didn't register for."; } } //////////////////////////////////////////////////////////////////////////////// // UnloadController, TabStripModelObserver implementation: void UnloadController::TabInsertedAt(content::WebContents* contents, int index, bool foreground) { TabAttachedImpl(contents); } void UnloadController::TabDetachedAt(content::WebContents* contents, int index) { TabDetachedImpl(contents); } void UnloadController::TabReplacedAt(TabStripModel* tab_strip_model, content::WebContents* old_contents, content::WebContents* new_contents, int index) { TabDetachedImpl(old_contents); TabAttachedImpl(new_contents); } void UnloadController::TabStripEmpty() { // Set is_attempting_to_close_browser_ here, so that extensions, etc, do not // attempt to add tabs to the browser before it closes. is_attempting_to_close_browser_ = true; } //////////////////////////////////////////////////////////////////////////////// // UnloadController, private: void UnloadController::TabAttachedImpl(content::WebContents* contents) { // If the tab crashes in the beforeunload or unload handler, it won't be // able to ack. But we know we can close it. registrar_.Add( this, content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED, content::Source<content::WebContents>(contents)); } void UnloadController::TabDetachedImpl(content::WebContents* contents) { if (tabs_needing_unload_ack_.find(contents) != tabs_needing_unload_ack_.end()) { // Tab needs unload to complete. // It will send |NOTIFICATION_WEB_CONTENTS_DISCONNECTED| when done. return; } // If WEB_CONTENTS_DISCONNECTED was received then the notification may have // already been unregistered. const content::NotificationSource& source = content::Source<content::WebContents>(contents); if (registrar_.IsRegistered(this, content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED, source)) { registrar_.Remove(this, content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED, source); } if (is_attempting_to_close_browser_) ClearUnloadState(contents); } bool UnloadController::DetachWebContents(content::WebContents* contents) { int index = browser_->tab_strip_model()->GetIndexOfWebContents(contents); if (index != TabStripModel::kNoTab && contents->NeedToFireBeforeUnload()) { tabs_needing_unload_ack_.insert(contents); browser_->tab_strip_model()->DetachWebContentsAt(index); contents->SetDelegate(detached_delegate_.get()); contents->OnUnloadDetachedStarted(); return true; } return false; } void UnloadController::ProcessPendingTabs() { if (!is_attempting_to_close_browser_) { // Because we might invoke this after a delay it's possible for the value of // is_attempting_to_close_browser_ to have changed since we scheduled the // task. return; } if (tab_needing_before_unload_ack_ != NULL) { // Wait for |BeforeUnloadFired| before proceeding. return; } // Process a beforeunload handler. if (!tabs_needing_before_unload_.empty()) { WebContentsSet::iterator it = tabs_needing_before_unload_.begin(); content::WebContents* contents = *it; tabs_needing_before_unload_.erase(it); // Null check render_view_host here as this gets called on a PostTask and // the tab's render_view_host may have been nulled out. if (contents->GetRenderViewHost()) { tab_needing_before_unload_ack_ = contents; contents->OnCloseStarted(); contents->GetRenderViewHost()->FirePageBeforeUnload(false); } else { ProcessPendingTabs(); } return; } // Process all the unload handlers. (The beforeunload handlers have finished.) if (!tabs_needing_unload_.empty()) { browser_->OnWindowClosing(); // Run unload handlers detached since no more interaction is possible. WebContentsSet::iterator it = tabs_needing_unload_.begin(); while (it != tabs_needing_unload_.end()) { WebContentsSet::iterator current = it++; content::WebContents* contents = *current; tabs_needing_unload_.erase(current); // Null check render_view_host here as this gets called on a PostTask // and the tab's render_view_host may have been nulled out. if (contents->GetRenderViewHost()) { contents->OnUnloadStarted(); DetachWebContents(contents); contents->GetRenderViewHost()->ClosePage(); } } // Get the browser hidden. if (browser_->tab_strip_model()->empty()) { browser_->TabStripEmpty(); } else { browser_->tab_strip_model()->CloseAllTabs(); // tabs not needing unload } return; } if (HasCompletedUnloadProcessing()) { browser_->OnWindowClosing(); // Get the browser closed. if (browser_->tab_strip_model()->empty()) { browser_->TabStripEmpty(); } else { // There may be tabs if the last tab needing beforeunload crashed. browser_->tab_strip_model()->CloseAllTabs(); } return; } } bool UnloadController::HasCompletedUnloadProcessing() const { return is_attempting_to_close_browser_ && tabs_needing_before_unload_.empty() && tab_needing_before_unload_ack_ == NULL && tabs_needing_unload_.empty() && tabs_needing_unload_ack_.empty(); } void UnloadController::CancelWindowClose() { // Closing of window can be canceled from a beforeunload handler. DCHECK(is_attempting_to_close_browser_); tabs_needing_before_unload_.clear(); if (tab_needing_before_unload_ack_ != NULL) { tab_needing_before_unload_ack_->OnCloseCanceled(); tab_needing_before_unload_ack_ = NULL; } for (WebContentsSet::iterator it = tabs_needing_unload_.begin(); it != tabs_needing_unload_.end(); it++) { content::WebContents* contents = *it; contents->OnCloseCanceled(); } tabs_needing_unload_.clear(); // No need to clear tabs_needing_unload_ack_. Those tabs are already detached. is_attempting_to_close_browser_ = false; content::NotificationService::current()->Notify( chrome::NOTIFICATION_BROWSER_CLOSE_CANCELLED, content::Source<Browser>(browser_), content::NotificationService::NoDetails()); } void UnloadController::ClearUnloadState(content::WebContents* contents) { if (tabs_needing_unload_ack_.erase(contents) > 0) { if (HasCompletedUnloadProcessing()) PostTaskForProcessPendingTabs(); return; } if (!is_attempting_to_close_browser_) return; if (tab_needing_before_unload_ack_ == contents) { tab_needing_before_unload_ack_ = NULL; PostTaskForProcessPendingTabs(); return; } if (tabs_needing_before_unload_.erase(contents) > 0 || tabs_needing_unload_.erase(contents) > 0) { if (tab_needing_before_unload_ack_ == NULL) PostTaskForProcessPendingTabs(); } } void UnloadController::PostTaskForProcessPendingTabs() { base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(&UnloadController::ProcessPendingTabs, weak_factory_.GetWeakPtr())); } } // namespace chrome