// Copyright 2013 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/fast_unload_controller.h"
#include "base/logging.h"
#include "base/message_loop/message_loop.h"
#include "chrome/browser/chrome_notification_types.h"
#include "chrome/browser/devtools/devtools_window.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_tabstrip.h"
#include "chrome/browser/ui/tab_contents/core_tab_helper.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/tabs/tab_strip_model_delegate.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 FastUnloadController::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.
// FastUnloadController will observe
// |NOTIFICATION_WEB_CONTENTS_DISCONNECTED|.
delete source;
}
DISALLOW_COPY_AND_ASSIGN(DetachedWebContentsDelegate);
};
////////////////////////////////////////////////////////////////////////////////
// FastUnloadController, public:
FastUnloadController::FastUnloadController(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);
}
FastUnloadController::~FastUnloadController() {
browser_->tab_strip_model()->RemoveObserver(this);
}
bool FastUnloadController::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_ ||
is_calling_before_unload_handlers();
}
// static
bool FastUnloadController::ShouldRunUnloadEventsHelper(
content::WebContents* contents) {
// If |contents| is being inspected, devtools needs to intercept beforeunload
// events.
return DevToolsWindow::GetInstanceForInspectedRenderViewHost(
contents->GetRenderViewHost()) != NULL;
}
// static
bool FastUnloadController::RunUnloadEventsHelper(
content::WebContents* contents) {
// If there's a devtools window attached to |contents|,
// we would like devtools to call its own beforeunload handlers first,
// and then call beforeunload handlers for |contents|.
// See DevToolsWindow::InterceptPageBeforeUnload for details.
if (DevToolsWindow::InterceptPageBeforeUnload(contents)) {
return true;
}
// If the WebContents is not connected yet, then there's no unload
// handler we can fire even if the WebContents has an unload listener.
// One case where we hit this is in a tab that has an infinite loop
// before load.
if (contents->NeedToFireBeforeUnload()) {
// If the page has unload listeners, then we tell the renderer to fire
// them. Once they have fired, we'll get a message back saying whether
// to proceed closing the page or not, which sends us back to this method
// with the NeedToFireBeforeUnload bit cleared.
contents->GetRenderViewHost()->FirePageBeforeUnload(false);
return true;
}
return false;
}
bool FastUnloadController::BeforeUnloadFired(content::WebContents* contents,
bool proceed) {
if (!proceed)
DevToolsWindow::OnPageCloseCanceled(contents);
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 FastUnloadController::ShouldCloseWindow() {
if (HasCompletedUnloadProcessing())
return true;
// Special case for when we quit an application. The Devtools window can
// close if it's beforeunload event has already fired which will happen due
// to the interception of it's content's beforeunload.
if (browser_->is_devtools() &&
DevToolsWindow::HasFiredBeforeUnloadEventForDevToolsBrowser(browser_)) {
return true;
}
// The behavior followed here varies based on the current phase of the
// operation and whether a batched shutdown is in progress.
//
// If there are tabs with outstanding beforeunload handlers:
// 1. If a batched shutdown is in progress: return false.
// This is to prevent interference with batched shutdown already in
// progress.
// 2. Otherwise: start sending beforeunload events and return false.
//
// Otherwise, If there are no tabs with outstanding beforeunload handlers:
// 3. If a batched shutdown is in progress: start sending unload events and
// return false.
// 4. Otherwise: return true.
is_attempting_to_close_browser_ = true;
// Cases 1 and 4.
bool need_beforeunload_fired = TabsNeedBeforeUnloadFired();
if (need_beforeunload_fired == is_calling_before_unload_handlers())
return !need_beforeunload_fired;
// Cases 2 and 3.
on_close_confirmed_.Reset();
ProcessPendingTabs();
return false;
}
bool FastUnloadController::CallBeforeUnloadHandlers(
const base::Callback<void(bool)>& on_close_confirmed) {
// The devtools browser gets its beforeunload events as the results of
// intercepting events from the inspected tab, so don't send them here as well.
if (browser_->is_devtools() || !TabsNeedBeforeUnloadFired())
return false;
on_close_confirmed_ = on_close_confirmed;
is_attempting_to_close_browser_ = true;
ProcessPendingTabs();
return true;
}
void FastUnloadController::ResetBeforeUnloadHandlers() {
if (!is_calling_before_unload_handlers())
return;
CancelWindowClose();
}
bool FastUnloadController::TabsNeedBeforeUnloadFired() {
if (!tabs_needing_before_unload_.empty() ||
tab_needing_before_unload_ack_ != NULL)
return true;
if (!is_calling_before_unload_handlers() && !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);
bool should_fire_beforeunload = contents->NeedToFireBeforeUnload() ||
DevToolsWindow::NeedsToInterceptBeforeUnload(contents);
if (!ContainsKey(tabs_needing_unload_, contents) &&
!ContainsKey(tabs_needing_unload_ack_, contents) &&
tab_needing_before_unload_ack_ != contents &&
should_fire_beforeunload)
tabs_needing_before_unload_.insert(contents);
}
return !tabs_needing_before_unload_.empty();
}
////////////////////////////////////////////////////////////////////////////////
// FastUnloadController, content::NotificationObserver implementation:
void FastUnloadController::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.";
}
}
////////////////////////////////////////////////////////////////////////////////
// FastUnloadController, TabStripModelObserver implementation:
void FastUnloadController::TabInsertedAt(content::WebContents* contents,
int index,
bool foreground) {
TabAttachedImpl(contents);
}
void FastUnloadController::TabDetachedAt(content::WebContents* contents,
int index) {
TabDetachedImpl(contents);
}
void FastUnloadController::TabReplacedAt(TabStripModel* tab_strip_model,
content::WebContents* old_contents,
content::WebContents* new_contents,
int index) {
TabDetachedImpl(old_contents);
TabAttachedImpl(new_contents);
}
void FastUnloadController::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;
}
////////////////////////////////////////////////////////////////////////////////
// FastUnloadController, private:
void FastUnloadController::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 FastUnloadController::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 FastUnloadController::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());
CoreTabHelper* core_tab_helper = CoreTabHelper::FromWebContents(contents);
core_tab_helper->OnUnloadDetachedStarted();
return true;
}
return false;
}
void FastUnloadController::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;
CoreTabHelper* core_tab_helper = CoreTabHelper::FromWebContents(contents);
core_tab_helper->OnCloseStarted();
// If there's a devtools window attached to |contents|,
// we would like devtools to call its own beforeunload handlers first,
// and then call beforeunload handlers for |contents|.
// See DevToolsWindow::InterceptPageBeforeUnload for details.
if (!DevToolsWindow::InterceptPageBeforeUnload(contents))
contents->GetRenderViewHost()->FirePageBeforeUnload(false);
} else {
ProcessPendingTabs();
}
return;
}
if (is_calling_before_unload_handlers()) {
on_close_confirmed_.Run(true);
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()) {
CoreTabHelper* core_tab_helper =
CoreTabHelper::FromWebContents(contents);
core_tab_helper->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 FastUnloadController::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 FastUnloadController::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) {
CoreTabHelper* core_tab_helper =
CoreTabHelper::FromWebContents(tab_needing_before_unload_ack_);
core_tab_helper->OnCloseCanceled();
DevToolsWindow::OnPageCloseCanceled(tab_needing_before_unload_ack_);
tab_needing_before_unload_ack_ = NULL;
}
for (WebContentsSet::iterator it = tabs_needing_unload_.begin();
it != tabs_needing_unload_.end(); it++) {
content::WebContents* contents = *it;
CoreTabHelper* core_tab_helper = CoreTabHelper::FromWebContents(contents);
core_tab_helper->OnCloseCanceled();
DevToolsWindow::OnPageCloseCanceled(contents);
}
tabs_needing_unload_.clear();
// No need to clear tabs_needing_unload_ack_. Those tabs are already detached.
if (is_calling_before_unload_handlers()) {
base::Callback<void(bool)> on_close_confirmed = on_close_confirmed_;
on_close_confirmed_.Reset();
on_close_confirmed.Run(false);
}
is_attempting_to_close_browser_ = false;
content::NotificationService::current()->Notify(
chrome::NOTIFICATION_BROWSER_CLOSE_CANCELLED,
content::Source<Browser>(browser_),
content::NotificationService::NoDetails());
}
void FastUnloadController::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 FastUnloadController::PostTaskForProcessPendingTabs() {
base::MessageLoop::current()->PostTask(
FROM_HERE,
base::Bind(&FastUnloadController::ProcessPendingTabs,
weak_factory_.GetWeakPtr()));
}
} // namespace chrome