| // 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 |