普通文本  |  484行  |  17.09 KB

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