Home | History | Annotate | Download | only in ui
      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/devtools/devtools_window.h"
     11 #include "chrome/browser/ui/browser.h"
     12 #include "chrome/browser/ui/browser_tabstrip.h"
     13 #include "chrome/browser/ui/tab_contents/core_tab_helper.h"
     14 #include "chrome/browser/ui/tabs/tab_strip_model.h"
     15 #include "chrome/browser/ui/tabs/tab_strip_model_delegate.h"
     16 #include "content/public/browser/notification_service.h"
     17 #include "content/public/browser/notification_source.h"
     18 #include "content/public/browser/notification_types.h"
     19 #include "content/public/browser/render_view_host.h"
     20 #include "content/public/browser/web_contents.h"
     21 #include "content/public/browser/web_contents_delegate.h"
     22 
     23 namespace chrome {
     24 
     25 
     26 ////////////////////////////////////////////////////////////////////////////////
     27 // DetachedWebContentsDelegate will delete web contents when they close.
     28 class FastUnloadController::DetachedWebContentsDelegate
     29     : public content::WebContentsDelegate {
     30  public:
     31   DetachedWebContentsDelegate() { }
     32   virtual ~DetachedWebContentsDelegate() { }
     33 
     34  private:
     35   // WebContentsDelegate implementation.
     36   virtual bool ShouldSuppressDialogs() OVERRIDE {
     37     return true;  // Return true so dialogs are suppressed.
     38   }
     39 
     40   virtual void CloseContents(content::WebContents* source) OVERRIDE {
     41     // Finished detached close.
     42     // FastUnloadController will observe
     43     // |NOTIFICATION_WEB_CONTENTS_DISCONNECTED|.
     44     delete source;
     45   }
     46 
     47   DISALLOW_COPY_AND_ASSIGN(DetachedWebContentsDelegate);
     48 };
     49 
     50 ////////////////////////////////////////////////////////////////////////////////
     51 // FastUnloadController, public:
     52 
     53 FastUnloadController::FastUnloadController(Browser* browser)
     54     : browser_(browser),
     55       tab_needing_before_unload_ack_(NULL),
     56       is_attempting_to_close_browser_(false),
     57       detached_delegate_(new DetachedWebContentsDelegate()),
     58       weak_factory_(this) {
     59   browser_->tab_strip_model()->AddObserver(this);
     60 }
     61 
     62 FastUnloadController::~FastUnloadController() {
     63   browser_->tab_strip_model()->RemoveObserver(this);
     64 }
     65 
     66 bool FastUnloadController::CanCloseContents(content::WebContents* contents) {
     67   // Don't try to close the tab when the whole browser is being closed, since
     68   // that avoids the fast shutdown path where we just kill all the renderers.
     69   return !is_attempting_to_close_browser_ ||
     70       is_calling_before_unload_handlers();
     71 }
     72 
     73 // static
     74 bool FastUnloadController::ShouldRunUnloadEventsHelper(
     75     content::WebContents* contents) {
     76   // If |contents| is being inspected, devtools needs to intercept beforeunload
     77   // events.
     78   return DevToolsWindow::GetInstanceForInspectedRenderViewHost(
     79       contents->GetRenderViewHost()) != NULL;
     80 }
     81 
     82 // static
     83 bool FastUnloadController::RunUnloadEventsHelper(
     84     content::WebContents* contents) {
     85   // If there's a devtools window attached to |contents|,
     86   // we would like devtools to call its own beforeunload handlers first,
     87   // and then call beforeunload handlers for |contents|.
     88   // See DevToolsWindow::InterceptPageBeforeUnload for details.
     89   if (DevToolsWindow::InterceptPageBeforeUnload(contents)) {
     90     return true;
     91   }
     92   // If the WebContents is not connected yet, then there's no unload
     93   // handler we can fire even if the WebContents has an unload listener.
     94   // One case where we hit this is in a tab that has an infinite loop
     95   // before load.
     96   if (contents->NeedToFireBeforeUnload()) {
     97     // If the page has unload listeners, then we tell the renderer to fire
     98     // them. Once they have fired, we'll get a message back saying whether
     99     // to proceed closing the page or not, which sends us back to this method
    100     // with the NeedToFireBeforeUnload bit cleared.
    101     contents->GetRenderViewHost()->FirePageBeforeUnload(false);
    102     return true;
    103   }
    104   return false;
    105 }
    106 
    107 bool FastUnloadController::BeforeUnloadFired(content::WebContents* contents,
    108                                              bool proceed) {
    109   if (!proceed)
    110     DevToolsWindow::OnPageCloseCanceled(contents);
    111 
    112   if (!is_attempting_to_close_browser_) {
    113     if (!proceed) {
    114       contents->SetClosedByUserGesture(false);
    115     } else {
    116       // No more dialogs are possible, so remove the tab and finish
    117       // running unload listeners asynchrounously.
    118       browser_->tab_strip_model()->delegate()->CreateHistoricalTab(contents);
    119       DetachWebContents(contents);
    120     }
    121     return proceed;
    122   }
    123 
    124   if (!proceed) {
    125     CancelWindowClose();
    126     contents->SetClosedByUserGesture(false);
    127     return false;
    128   }
    129 
    130   if (tab_needing_before_unload_ack_ == contents) {
    131     // Now that beforeunload has fired, queue the tab to fire unload.
    132     tab_needing_before_unload_ack_ = NULL;
    133     tabs_needing_unload_.insert(contents);
    134     ProcessPendingTabs();
    135     // We want to handle firing the unload event ourselves since we want to
    136     // fire all the beforeunload events before attempting to fire the unload
    137     // events should the user cancel closing the browser.
    138     return false;
    139   }
    140 
    141   return true;
    142 }
    143 
    144 bool FastUnloadController::ShouldCloseWindow() {
    145   if (HasCompletedUnloadProcessing())
    146     return true;
    147 
    148   // Special case for when we quit an application. The Devtools window can
    149   // close if it's beforeunload event has already fired which will happen due
    150   // to the interception of it's content's beforeunload.
    151   if (browser_->is_devtools() &&
    152       DevToolsWindow::HasFiredBeforeUnloadEventForDevToolsBrowser(browser_)) {
    153     return true;
    154   }
    155 
    156   // The behavior followed here varies based on the current phase of the
    157   // operation and whether a batched shutdown is in progress.
    158   //
    159   // If there are tabs with outstanding beforeunload handlers:
    160   // 1. If a batched shutdown is in progress: return false.
    161   //    This is to prevent interference with batched shutdown already in
    162   //    progress.
    163   // 2. Otherwise: start sending beforeunload events and return false.
    164   //
    165   // Otherwise, If there are no tabs with outstanding beforeunload handlers:
    166   // 3. If a batched shutdown is in progress: start sending unload events and
    167   //    return false.
    168   // 4. Otherwise: return true.
    169   is_attempting_to_close_browser_ = true;
    170   // Cases 1 and 4.
    171   bool need_beforeunload_fired = TabsNeedBeforeUnloadFired();
    172   if (need_beforeunload_fired == is_calling_before_unload_handlers())
    173     return !need_beforeunload_fired;
    174 
    175   // Cases 2 and 3.
    176   on_close_confirmed_.Reset();
    177   ProcessPendingTabs();
    178   return false;
    179 }
    180 
    181 bool FastUnloadController::CallBeforeUnloadHandlers(
    182     const base::Callback<void(bool)>& on_close_confirmed) {
    183 // The devtools browser gets its beforeunload events as the results of
    184 // intercepting events from the inspected tab, so don't send them here as well.
    185   if (browser_->is_devtools() || !TabsNeedBeforeUnloadFired())
    186     return false;
    187 
    188   on_close_confirmed_ = on_close_confirmed;
    189   is_attempting_to_close_browser_ = true;
    190   ProcessPendingTabs();
    191   return true;
    192 }
    193 
    194 void FastUnloadController::ResetBeforeUnloadHandlers() {
    195   if (!is_calling_before_unload_handlers())
    196     return;
    197   CancelWindowClose();
    198 }
    199 
    200 bool FastUnloadController::TabsNeedBeforeUnloadFired() {
    201   if (!tabs_needing_before_unload_.empty() ||
    202       tab_needing_before_unload_ack_ != NULL)
    203     return true;
    204 
    205   if (!is_calling_before_unload_handlers() && !tabs_needing_unload_.empty())
    206     return false;
    207 
    208   for (int i = 0; i < browser_->tab_strip_model()->count(); ++i) {
    209     content::WebContents* contents =
    210         browser_->tab_strip_model()->GetWebContentsAt(i);
    211     bool should_fire_beforeunload = contents->NeedToFireBeforeUnload() ||
    212         DevToolsWindow::NeedsToInterceptBeforeUnload(contents);
    213     if (!ContainsKey(tabs_needing_unload_, contents) &&
    214         !ContainsKey(tabs_needing_unload_ack_, contents) &&
    215         tab_needing_before_unload_ack_ != contents &&
    216         should_fire_beforeunload)
    217       tabs_needing_before_unload_.insert(contents);
    218   }
    219   return !tabs_needing_before_unload_.empty();
    220 }
    221 
    222 ////////////////////////////////////////////////////////////////////////////////
    223 // FastUnloadController, content::NotificationObserver implementation:
    224 
    225 void FastUnloadController::Observe(
    226       int type,
    227       const content::NotificationSource& source,
    228       const content::NotificationDetails& details) {
    229   switch (type) {
    230     case content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED: {
    231       registrar_.Remove(this,
    232                         content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED,
    233                         source);
    234       content::WebContents* contents =
    235           content::Source<content::WebContents>(source).ptr();
    236       ClearUnloadState(contents);
    237       break;
    238     }
    239     default:
    240       NOTREACHED() << "Got a notification we didn't register for.";
    241   }
    242 }
    243 
    244 ////////////////////////////////////////////////////////////////////////////////
    245 // FastUnloadController, TabStripModelObserver implementation:
    246 
    247 void FastUnloadController::TabInsertedAt(content::WebContents* contents,
    248                                          int index,
    249                                          bool foreground) {
    250   TabAttachedImpl(contents);
    251 }
    252 
    253 void FastUnloadController::TabDetachedAt(content::WebContents* contents,
    254                                          int index) {
    255   TabDetachedImpl(contents);
    256 }
    257 
    258 void FastUnloadController::TabReplacedAt(TabStripModel* tab_strip_model,
    259                                          content::WebContents* old_contents,
    260                                          content::WebContents* new_contents,
    261                                          int index) {
    262   TabDetachedImpl(old_contents);
    263   TabAttachedImpl(new_contents);
    264 }
    265 
    266 void FastUnloadController::TabStripEmpty() {
    267   // Set is_attempting_to_close_browser_ here, so that extensions, etc, do not
    268   // attempt to add tabs to the browser before it closes.
    269   is_attempting_to_close_browser_ = true;
    270 }
    271 
    272 ////////////////////////////////////////////////////////////////////////////////
    273 // FastUnloadController, private:
    274 
    275 void FastUnloadController::TabAttachedImpl(content::WebContents* contents) {
    276   // If the tab crashes in the beforeunload or unload handler, it won't be
    277   // able to ack. But we know we can close it.
    278   registrar_.Add(
    279       this,
    280       content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED,
    281       content::Source<content::WebContents>(contents));
    282 }
    283 
    284 void FastUnloadController::TabDetachedImpl(content::WebContents* contents) {
    285   if (tabs_needing_unload_ack_.find(contents) !=
    286       tabs_needing_unload_ack_.end()) {
    287     // Tab needs unload to complete.
    288     // It will send |NOTIFICATION_WEB_CONTENTS_DISCONNECTED| when done.
    289     return;
    290   }
    291 
    292   // If WEB_CONTENTS_DISCONNECTED was received then the notification may have
    293   // already been unregistered.
    294   const content::NotificationSource& source =
    295       content::Source<content::WebContents>(contents);
    296   if (registrar_.IsRegistered(this,
    297                               content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED,
    298                               source)) {
    299     registrar_.Remove(this,
    300                       content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED,
    301                       source);
    302   }
    303 
    304   if (is_attempting_to_close_browser_)
    305     ClearUnloadState(contents);
    306 }
    307 
    308 bool FastUnloadController::DetachWebContents(content::WebContents* contents) {
    309   int index = browser_->tab_strip_model()->GetIndexOfWebContents(contents);
    310   if (index != TabStripModel::kNoTab &&
    311       contents->NeedToFireBeforeUnload()) {
    312     tabs_needing_unload_ack_.insert(contents);
    313     browser_->tab_strip_model()->DetachWebContentsAt(index);
    314     contents->SetDelegate(detached_delegate_.get());
    315     CoreTabHelper* core_tab_helper = CoreTabHelper::FromWebContents(contents);
    316     core_tab_helper->OnUnloadDetachedStarted();
    317     return true;
    318   }
    319   return false;
    320 }
    321 
    322 void FastUnloadController::ProcessPendingTabs() {
    323   if (!is_attempting_to_close_browser_) {
    324     // Because we might invoke this after a delay it's possible for the value of
    325     // is_attempting_to_close_browser_ to have changed since we scheduled the
    326     // task.
    327     return;
    328   }
    329 
    330   if (tab_needing_before_unload_ack_ != NULL) {
    331     // Wait for |BeforeUnloadFired| before proceeding.
    332     return;
    333   }
    334 
    335   // Process a beforeunload handler.
    336   if (!tabs_needing_before_unload_.empty()) {
    337     WebContentsSet::iterator it = tabs_needing_before_unload_.begin();
    338     content::WebContents* contents = *it;
    339     tabs_needing_before_unload_.erase(it);
    340     // Null check render_view_host here as this gets called on a PostTask and
    341     // the tab's render_view_host may have been nulled out.
    342     if (contents->GetRenderViewHost()) {
    343       tab_needing_before_unload_ack_ = contents;
    344 
    345       CoreTabHelper* core_tab_helper = CoreTabHelper::FromWebContents(contents);
    346       core_tab_helper->OnCloseStarted();
    347 
    348       // If there's a devtools window attached to |contents|,
    349       // we would like devtools to call its own beforeunload handlers first,
    350       // and then call beforeunload handlers for |contents|.
    351       // See DevToolsWindow::InterceptPageBeforeUnload for details.
    352       if (!DevToolsWindow::InterceptPageBeforeUnload(contents))
    353         contents->GetRenderViewHost()->FirePageBeforeUnload(false);
    354     } else {
    355       ProcessPendingTabs();
    356     }
    357     return;
    358   }
    359 
    360   if (is_calling_before_unload_handlers()) {
    361     on_close_confirmed_.Run(true);
    362     return;
    363   }
    364   // Process all the unload handlers. (The beforeunload handlers have finished.)
    365   if (!tabs_needing_unload_.empty()) {
    366     browser_->OnWindowClosing();
    367 
    368     // Run unload handlers detached since no more interaction is possible.
    369     WebContentsSet::iterator it = tabs_needing_unload_.begin();
    370     while (it != tabs_needing_unload_.end()) {
    371       WebContentsSet::iterator current = it++;
    372       content::WebContents* contents = *current;
    373       tabs_needing_unload_.erase(current);
    374       // Null check render_view_host here as this gets called on a PostTask
    375       // and the tab's render_view_host may have been nulled out.
    376       if (contents->GetRenderViewHost()) {
    377         CoreTabHelper* core_tab_helper =
    378             CoreTabHelper::FromWebContents(contents);
    379         core_tab_helper->OnUnloadStarted();
    380         DetachWebContents(contents);
    381         contents->GetRenderViewHost()->ClosePage();
    382       }
    383     }
    384 
    385     // Get the browser hidden.
    386     if (browser_->tab_strip_model()->empty()) {
    387       browser_->TabStripEmpty();
    388     } else {
    389       browser_->tab_strip_model()->CloseAllTabs();  // tabs not needing unload
    390     }
    391     return;
    392   }
    393 
    394   if (HasCompletedUnloadProcessing()) {
    395     browser_->OnWindowClosing();
    396 
    397     // Get the browser closed.
    398     if (browser_->tab_strip_model()->empty()) {
    399       browser_->TabStripEmpty();
    400     } else {
    401       // There may be tabs if the last tab needing beforeunload crashed.
    402       browser_->tab_strip_model()->CloseAllTabs();
    403     }
    404     return;
    405   }
    406 }
    407 
    408 bool FastUnloadController::HasCompletedUnloadProcessing() const {
    409   return is_attempting_to_close_browser_ &&
    410       tabs_needing_before_unload_.empty() &&
    411       tab_needing_before_unload_ack_ == NULL &&
    412       tabs_needing_unload_.empty() &&
    413       tabs_needing_unload_ack_.empty();
    414 }
    415 
    416 void FastUnloadController::CancelWindowClose() {
    417   // Closing of window can be canceled from a beforeunload handler.
    418   DCHECK(is_attempting_to_close_browser_);
    419   tabs_needing_before_unload_.clear();
    420   if (tab_needing_before_unload_ack_ != NULL) {
    421     CoreTabHelper* core_tab_helper =
    422         CoreTabHelper::FromWebContents(tab_needing_before_unload_ack_);
    423     core_tab_helper->OnCloseCanceled();
    424     DevToolsWindow::OnPageCloseCanceled(tab_needing_before_unload_ack_);
    425     tab_needing_before_unload_ack_ = NULL;
    426   }
    427   for (WebContentsSet::iterator it = tabs_needing_unload_.begin();
    428        it != tabs_needing_unload_.end(); it++) {
    429     content::WebContents* contents = *it;
    430 
    431     CoreTabHelper* core_tab_helper = CoreTabHelper::FromWebContents(contents);
    432     core_tab_helper->OnCloseCanceled();
    433     DevToolsWindow::OnPageCloseCanceled(contents);
    434   }
    435   tabs_needing_unload_.clear();
    436 
    437   // No need to clear tabs_needing_unload_ack_. Those tabs are already detached.
    438 
    439   if (is_calling_before_unload_handlers()) {
    440     base::Callback<void(bool)> on_close_confirmed = on_close_confirmed_;
    441     on_close_confirmed_.Reset();
    442     on_close_confirmed.Run(false);
    443   }
    444 
    445   is_attempting_to_close_browser_ = false;
    446 
    447   content::NotificationService::current()->Notify(
    448       chrome::NOTIFICATION_BROWSER_CLOSE_CANCELLED,
    449       content::Source<Browser>(browser_),
    450       content::NotificationService::NoDetails());
    451 }
    452 
    453 void FastUnloadController::ClearUnloadState(content::WebContents* contents) {
    454   if (tabs_needing_unload_ack_.erase(contents) > 0) {
    455     if (HasCompletedUnloadProcessing())
    456       PostTaskForProcessPendingTabs();
    457     return;
    458   }
    459 
    460   if (!is_attempting_to_close_browser_)
    461     return;
    462 
    463   if (tab_needing_before_unload_ack_ == contents) {
    464     tab_needing_before_unload_ack_ = NULL;
    465     PostTaskForProcessPendingTabs();
    466     return;
    467   }
    468 
    469   if (tabs_needing_before_unload_.erase(contents) > 0 ||
    470       tabs_needing_unload_.erase(contents) > 0) {
    471     if (tab_needing_before_unload_ack_ == NULL)
    472       PostTaskForProcessPendingTabs();
    473   }
    474 }
    475 
    476 void FastUnloadController::PostTaskForProcessPendingTabs() {
    477   base::MessageLoop::current()->PostTask(
    478       FROM_HERE,
    479       base::Bind(&FastUnloadController::ProcessPendingTabs,
    480                  weak_factory_.GetWeakPtr()));
    481 }
    482 
    483 }  // namespace chrome
    484