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/ui/browser.h"
     11 #include "chrome/browser/ui/browser_tabstrip.h"
     12 #include "chrome/browser/ui/tab_contents/core_tab_helper.h"
     13 #include "chrome/browser/ui/tabs/tab_strip_model.h"
     14 #include "chrome/browser/ui/tabs/tab_strip_model_delegate.h"
     15 #include "content/public/browser/notification_service.h"
     16 #include "content/public/browser/notification_source.h"
     17 #include "content/public/browser/notification_types.h"
     18 #include "content/public/browser/render_view_host.h"
     19 #include "content/public/browser/web_contents.h"
     20 #include "content/public/browser/web_contents_delegate.h"
     21 
     22 namespace chrome {
     23 
     24 
     25 ////////////////////////////////////////////////////////////////////////////////
     26 // DetachedWebContentsDelegate will delete web contents when they close.
     27 class FastUnloadController::DetachedWebContentsDelegate
     28     : public content::WebContentsDelegate {
     29  public:
     30   DetachedWebContentsDelegate() { }
     31   virtual ~DetachedWebContentsDelegate() { }
     32 
     33  private:
     34   // WebContentsDelegate implementation.
     35   virtual bool ShouldSuppressDialogs() OVERRIDE {
     36     return true;  // Return true so dialogs are suppressed.
     37   }
     38 
     39   virtual void CloseContents(content::WebContents* source) OVERRIDE {
     40     // Finished detached close.
     41     // FastUnloadController will observe
     42     // |NOTIFICATION_WEB_CONTENTS_DISCONNECTED|.
     43     delete source;
     44   }
     45 
     46   DISALLOW_COPY_AND_ASSIGN(DetachedWebContentsDelegate);
     47 };
     48 
     49 ////////////////////////////////////////////////////////////////////////////////
     50 // FastUnloadController, public:
     51 
     52 FastUnloadController::FastUnloadController(Browser* browser)
     53     : browser_(browser),
     54       tab_needing_before_unload_ack_(NULL),
     55       is_attempting_to_close_browser_(false),
     56       detached_delegate_(new DetachedWebContentsDelegate()),
     57       weak_factory_(this) {
     58   browser_->tab_strip_model()->AddObserver(this);
     59 }
     60 
     61 FastUnloadController::~FastUnloadController() {
     62   browser_->tab_strip_model()->RemoveObserver(this);
     63 }
     64 
     65 bool FastUnloadController::CanCloseContents(content::WebContents* contents) {
     66   // Don't try to close the tab when the whole browser is being closed, since
     67   // that avoids the fast shutdown path where we just kill all the renderers.
     68   return !is_attempting_to_close_browser_;
     69 }
     70 
     71 bool FastUnloadController::BeforeUnloadFired(content::WebContents* contents,
     72                                              bool proceed) {
     73   if (!is_attempting_to_close_browser_) {
     74     if (!proceed) {
     75       contents->SetClosedByUserGesture(false);
     76     } else {
     77       // No more dialogs are possible, so remove the tab and finish
     78       // running unload listeners asynchrounously.
     79       browser_->tab_strip_model()->delegate()->CreateHistoricalTab(contents);
     80       DetachWebContents(contents);
     81     }
     82     return proceed;
     83   }
     84 
     85   if (!proceed) {
     86     CancelWindowClose();
     87     contents->SetClosedByUserGesture(false);
     88     return false;
     89   }
     90 
     91   if (tab_needing_before_unload_ack_ == contents) {
     92     // Now that beforeunload has fired, queue the tab to fire unload.
     93     tab_needing_before_unload_ack_ = NULL;
     94     tabs_needing_unload_.insert(contents);
     95     ProcessPendingTabs();
     96     // We want to handle firing the unload event ourselves since we want to
     97     // fire all the beforeunload events before attempting to fire the unload
     98     // events should the user cancel closing the browser.
     99     return false;
    100   }
    101 
    102   return true;
    103 }
    104 
    105 bool FastUnloadController::ShouldCloseWindow() {
    106   if (HasCompletedUnloadProcessing())
    107     return true;
    108 
    109   is_attempting_to_close_browser_ = true;
    110 
    111   if (!TabsNeedBeforeUnloadFired())
    112     return true;
    113 
    114   ProcessPendingTabs();
    115   return false;
    116 }
    117 
    118 bool FastUnloadController::TabsNeedBeforeUnloadFired() {
    119   if (!tabs_needing_before_unload_.empty() ||
    120       tab_needing_before_unload_ack_ != NULL)
    121     return true;
    122 
    123   if (!tabs_needing_unload_.empty())
    124     return false;
    125 
    126   for (int i = 0; i < browser_->tab_strip_model()->count(); ++i) {
    127     content::WebContents* contents =
    128         browser_->tab_strip_model()->GetWebContentsAt(i);
    129     if (contents->NeedToFireBeforeUnload())
    130       tabs_needing_before_unload_.insert(contents);
    131   }
    132   return !tabs_needing_before_unload_.empty();
    133 }
    134 
    135 ////////////////////////////////////////////////////////////////////////////////
    136 // FastUnloadController, content::NotificationObserver implementation:
    137 
    138 void FastUnloadController::Observe(
    139       int type,
    140       const content::NotificationSource& source,
    141       const content::NotificationDetails& details) {
    142   switch (type) {
    143     case content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED: {
    144       registrar_.Remove(this,
    145                         content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED,
    146                         source);
    147       content::WebContents* contents =
    148           content::Source<content::WebContents>(source).ptr();
    149       ClearUnloadState(contents);
    150       break;
    151     }
    152     default:
    153       NOTREACHED() << "Got a notification we didn't register for.";
    154   }
    155 }
    156 
    157 ////////////////////////////////////////////////////////////////////////////////
    158 // FastUnloadController, TabStripModelObserver implementation:
    159 
    160 void FastUnloadController::TabInsertedAt(content::WebContents* contents,
    161                                          int index,
    162                                          bool foreground) {
    163   TabAttachedImpl(contents);
    164 }
    165 
    166 void FastUnloadController::TabDetachedAt(content::WebContents* contents,
    167                                          int index) {
    168   TabDetachedImpl(contents);
    169 }
    170 
    171 void FastUnloadController::TabReplacedAt(TabStripModel* tab_strip_model,
    172                                          content::WebContents* old_contents,
    173                                          content::WebContents* new_contents,
    174                                          int index) {
    175   TabDetachedImpl(old_contents);
    176   TabAttachedImpl(new_contents);
    177 }
    178 
    179 void FastUnloadController::TabStripEmpty() {
    180   // Set is_attempting_to_close_browser_ here, so that extensions, etc, do not
    181   // attempt to add tabs to the browser before it closes.
    182   is_attempting_to_close_browser_ = true;
    183 }
    184 
    185 ////////////////////////////////////////////////////////////////////////////////
    186 // FastUnloadController, private:
    187 
    188 void FastUnloadController::TabAttachedImpl(content::WebContents* contents) {
    189   // If the tab crashes in the beforeunload or unload handler, it won't be
    190   // able to ack. But we know we can close it.
    191   registrar_.Add(
    192       this,
    193       content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED,
    194       content::Source<content::WebContents>(contents));
    195 }
    196 
    197 void FastUnloadController::TabDetachedImpl(content::WebContents* contents) {
    198   if (tabs_needing_unload_ack_.find(contents) !=
    199       tabs_needing_unload_ack_.end()) {
    200     // Tab needs unload to complete.
    201     // It will send |NOTIFICATION_WEB_CONTENTS_DISCONNECTED| when done.
    202     return;
    203   }
    204 
    205   // If WEB_CONTENTS_DISCONNECTED was received then the notification may have
    206   // already been unregistered.
    207   const content::NotificationSource& source =
    208       content::Source<content::WebContents>(contents);
    209   if (registrar_.IsRegistered(this,
    210                               content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED,
    211                               source)) {
    212     registrar_.Remove(this,
    213                       content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED,
    214                       source);
    215   }
    216 
    217   if (is_attempting_to_close_browser_)
    218     ClearUnloadState(contents);
    219 }
    220 
    221 bool FastUnloadController::DetachWebContents(content::WebContents* contents) {
    222   int index = browser_->tab_strip_model()->GetIndexOfWebContents(contents);
    223   if (index != TabStripModel::kNoTab &&
    224       contents->NeedToFireBeforeUnload()) {
    225     tabs_needing_unload_ack_.insert(contents);
    226     browser_->tab_strip_model()->DetachWebContentsAt(index);
    227     contents->SetDelegate(detached_delegate_.get());
    228     CoreTabHelper* core_tab_helper = CoreTabHelper::FromWebContents(contents);
    229     core_tab_helper->OnUnloadDetachedStarted();
    230     return true;
    231   }
    232   return false;
    233 }
    234 
    235 void FastUnloadController::ProcessPendingTabs() {
    236   if (!is_attempting_to_close_browser_) {
    237     // Because we might invoke this after a delay it's possible for the value of
    238     // is_attempting_to_close_browser_ to have changed since we scheduled the
    239     // task.
    240     return;
    241   }
    242 
    243   if (tab_needing_before_unload_ack_ != NULL) {
    244     // Wait for |BeforeUnloadFired| before proceeding.
    245     return;
    246   }
    247 
    248   // Process a beforeunload handler.
    249   if (!tabs_needing_before_unload_.empty()) {
    250     WebContentsSet::iterator it = tabs_needing_before_unload_.begin();
    251     content::WebContents* contents = *it;
    252     tabs_needing_before_unload_.erase(it);
    253     // Null check render_view_host here as this gets called on a PostTask and
    254     // the tab's render_view_host may have been nulled out.
    255     if (contents->GetRenderViewHost()) {
    256       tab_needing_before_unload_ack_ = contents;
    257 
    258       CoreTabHelper* core_tab_helper = CoreTabHelper::FromWebContents(contents);
    259       core_tab_helper->OnCloseStarted();
    260 
    261       contents->GetRenderViewHost()->FirePageBeforeUnload(false);
    262     } else {
    263       ProcessPendingTabs();
    264     }
    265     return;
    266   }
    267 
    268   // Process all the unload handlers. (The beforeunload handlers have finished.)
    269   if (!tabs_needing_unload_.empty()) {
    270     browser_->OnWindowClosing();
    271 
    272     // Run unload handlers detached since no more interaction is possible.
    273     WebContentsSet::iterator it = tabs_needing_unload_.begin();
    274     while (it != tabs_needing_unload_.end()) {
    275       WebContentsSet::iterator current = it++;
    276       content::WebContents* contents = *current;
    277       tabs_needing_unload_.erase(current);
    278       // Null check render_view_host here as this gets called on a PostTask
    279       // and the tab's render_view_host may have been nulled out.
    280       if (contents->GetRenderViewHost()) {
    281         CoreTabHelper* core_tab_helper =
    282             CoreTabHelper::FromWebContents(contents);
    283         core_tab_helper->OnUnloadStarted();
    284         DetachWebContents(contents);
    285         contents->GetRenderViewHost()->ClosePage();
    286       }
    287     }
    288 
    289     // Get the browser hidden.
    290     if (browser_->tab_strip_model()->empty()) {
    291       browser_->TabStripEmpty();
    292     } else {
    293       browser_->tab_strip_model()->CloseAllTabs();  // tabs not needing unload
    294     }
    295     return;
    296   }
    297 
    298   if (HasCompletedUnloadProcessing()) {
    299     browser_->OnWindowClosing();
    300 
    301     // Get the browser closed.
    302     if (browser_->tab_strip_model()->empty()) {
    303       browser_->TabStripEmpty();
    304     } else {
    305       // There may be tabs if the last tab needing beforeunload crashed.
    306       browser_->tab_strip_model()->CloseAllTabs();
    307     }
    308     return;
    309   }
    310 }
    311 
    312 bool FastUnloadController::HasCompletedUnloadProcessing() const {
    313   return is_attempting_to_close_browser_ &&
    314       tabs_needing_before_unload_.empty() &&
    315       tab_needing_before_unload_ack_ == NULL &&
    316       tabs_needing_unload_.empty() &&
    317       tabs_needing_unload_ack_.empty();
    318 }
    319 
    320 void FastUnloadController::CancelWindowClose() {
    321   // Closing of window can be canceled from a beforeunload handler.
    322   DCHECK(is_attempting_to_close_browser_);
    323   tabs_needing_before_unload_.clear();
    324   if (tab_needing_before_unload_ack_ != NULL) {
    325 
    326     CoreTabHelper* core_tab_helper =
    327         CoreTabHelper::FromWebContents(tab_needing_before_unload_ack_);
    328     core_tab_helper->OnCloseCanceled();
    329     tab_needing_before_unload_ack_ = NULL;
    330   }
    331   for (WebContentsSet::iterator it = tabs_needing_unload_.begin();
    332        it != tabs_needing_unload_.end(); it++) {
    333     content::WebContents* contents = *it;
    334 
    335     CoreTabHelper* core_tab_helper = CoreTabHelper::FromWebContents(contents);
    336     core_tab_helper->OnCloseCanceled();
    337   }
    338   tabs_needing_unload_.clear();
    339 
    340   // No need to clear tabs_needing_unload_ack_. Those tabs are already detached.
    341 
    342   is_attempting_to_close_browser_ = false;
    343 
    344   content::NotificationService::current()->Notify(
    345       chrome::NOTIFICATION_BROWSER_CLOSE_CANCELLED,
    346       content::Source<Browser>(browser_),
    347       content::NotificationService::NoDetails());
    348 }
    349 
    350 void FastUnloadController::ClearUnloadState(content::WebContents* contents) {
    351   if (tabs_needing_unload_ack_.erase(contents) > 0) {
    352     if (HasCompletedUnloadProcessing())
    353       PostTaskForProcessPendingTabs();
    354     return;
    355   }
    356 
    357   if (!is_attempting_to_close_browser_)
    358     return;
    359 
    360   if (tab_needing_before_unload_ack_ == contents) {
    361     tab_needing_before_unload_ack_ = NULL;
    362     PostTaskForProcessPendingTabs();
    363     return;
    364   }
    365 
    366   if (tabs_needing_before_unload_.erase(contents) > 0 ||
    367       tabs_needing_unload_.erase(contents) > 0) {
    368     if (tab_needing_before_unload_ack_ == NULL)
    369       PostTaskForProcessPendingTabs();
    370   }
    371 }
    372 
    373 void FastUnloadController::PostTaskForProcessPendingTabs() {
    374   base::MessageLoop::current()->PostTask(
    375       FROM_HERE,
    376       base::Bind(&FastUnloadController::ProcessPendingTabs,
    377                  weak_factory_.GetWeakPtr()));
    378 }
    379 
    380 }  // namespace chrome
    381