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->DispatchBeforeUnload(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 bool FastUnloadController::HasCompletedUnloadProcessing() const {
    223   return is_attempting_to_close_browser_ &&
    224       tabs_needing_before_unload_.empty() &&
    225       tab_needing_before_unload_ack_ == NULL &&
    226       tabs_needing_unload_.empty() &&
    227       tabs_needing_unload_ack_.empty();
    228 }
    229 
    230 void FastUnloadController::CancelWindowClose() {
    231   // Closing of window can be canceled from a beforeunload handler.
    232   DCHECK(is_attempting_to_close_browser_);
    233   tabs_needing_before_unload_.clear();
    234   if (tab_needing_before_unload_ack_ != NULL) {
    235     CoreTabHelper* core_tab_helper =
    236         CoreTabHelper::FromWebContents(tab_needing_before_unload_ack_);
    237     core_tab_helper->OnCloseCanceled();
    238     DevToolsWindow::OnPageCloseCanceled(tab_needing_before_unload_ack_);
    239     tab_needing_before_unload_ack_ = NULL;
    240   }
    241   for (WebContentsSet::iterator it = tabs_needing_unload_.begin();
    242        it != tabs_needing_unload_.end(); it++) {
    243     content::WebContents* contents = *it;
    244 
    245     CoreTabHelper* core_tab_helper = CoreTabHelper::FromWebContents(contents);
    246     core_tab_helper->OnCloseCanceled();
    247     DevToolsWindow::OnPageCloseCanceled(contents);
    248   }
    249   tabs_needing_unload_.clear();
    250 
    251   // No need to clear tabs_needing_unload_ack_. Those tabs are already detached.
    252 
    253   if (is_calling_before_unload_handlers()) {
    254     base::Callback<void(bool)> on_close_confirmed = on_close_confirmed_;
    255     on_close_confirmed_.Reset();
    256     on_close_confirmed.Run(false);
    257   }
    258 
    259   is_attempting_to_close_browser_ = false;
    260 
    261   content::NotificationService::current()->Notify(
    262       chrome::NOTIFICATION_BROWSER_CLOSE_CANCELLED,
    263       content::Source<Browser>(browser_),
    264       content::NotificationService::NoDetails());
    265 }
    266 
    267 ////////////////////////////////////////////////////////////////////////////////
    268 // FastUnloadController, content::NotificationObserver implementation:
    269 
    270 void FastUnloadController::Observe(
    271       int type,
    272       const content::NotificationSource& source,
    273       const content::NotificationDetails& details) {
    274   switch (type) {
    275     case content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED: {
    276       registrar_.Remove(this,
    277                         content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED,
    278                         source);
    279       content::WebContents* contents =
    280           content::Source<content::WebContents>(source).ptr();
    281       ClearUnloadState(contents);
    282       break;
    283     }
    284     default:
    285       NOTREACHED() << "Got a notification we didn't register for.";
    286   }
    287 }
    288 
    289 ////////////////////////////////////////////////////////////////////////////////
    290 // FastUnloadController, TabStripModelObserver implementation:
    291 
    292 void FastUnloadController::TabInsertedAt(content::WebContents* contents,
    293                                          int index,
    294                                          bool foreground) {
    295   TabAttachedImpl(contents);
    296 }
    297 
    298 void FastUnloadController::TabDetachedAt(content::WebContents* contents,
    299                                          int index) {
    300   TabDetachedImpl(contents);
    301 }
    302 
    303 void FastUnloadController::TabReplacedAt(TabStripModel* tab_strip_model,
    304                                          content::WebContents* old_contents,
    305                                          content::WebContents* new_contents,
    306                                          int index) {
    307   TabDetachedImpl(old_contents);
    308   TabAttachedImpl(new_contents);
    309 }
    310 
    311 void FastUnloadController::TabStripEmpty() {
    312   // Set is_attempting_to_close_browser_ here, so that extensions, etc, do not
    313   // attempt to add tabs to the browser before it closes.
    314   is_attempting_to_close_browser_ = true;
    315 }
    316 
    317 ////////////////////////////////////////////////////////////////////////////////
    318 // FastUnloadController, private:
    319 
    320 void FastUnloadController::TabAttachedImpl(content::WebContents* contents) {
    321   // If the tab crashes in the beforeunload or unload handler, it won't be
    322   // able to ack. But we know we can close it.
    323   registrar_.Add(
    324       this,
    325       content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED,
    326       content::Source<content::WebContents>(contents));
    327 }
    328 
    329 void FastUnloadController::TabDetachedImpl(content::WebContents* contents) {
    330   if (tabs_needing_unload_ack_.find(contents) !=
    331       tabs_needing_unload_ack_.end()) {
    332     // Tab needs unload to complete.
    333     // It will send |NOTIFICATION_WEB_CONTENTS_DISCONNECTED| when done.
    334     return;
    335   }
    336 
    337   // If WEB_CONTENTS_DISCONNECTED was received then the notification may have
    338   // already been unregistered.
    339   const content::NotificationSource& source =
    340       content::Source<content::WebContents>(contents);
    341   if (registrar_.IsRegistered(this,
    342                               content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED,
    343                               source)) {
    344     registrar_.Remove(this,
    345                       content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED,
    346                       source);
    347   }
    348 
    349   if (is_attempting_to_close_browser_)
    350     ClearUnloadState(contents);
    351 }
    352 
    353 bool FastUnloadController::DetachWebContents(content::WebContents* contents) {
    354   int index = browser_->tab_strip_model()->GetIndexOfWebContents(contents);
    355   if (index != TabStripModel::kNoTab &&
    356       contents->NeedToFireBeforeUnload()) {
    357     tabs_needing_unload_ack_.insert(contents);
    358     browser_->tab_strip_model()->DetachWebContentsAt(index);
    359     contents->SetDelegate(detached_delegate_.get());
    360     CoreTabHelper* core_tab_helper = CoreTabHelper::FromWebContents(contents);
    361     core_tab_helper->OnUnloadDetachedStarted();
    362     return true;
    363   }
    364   return false;
    365 }
    366 
    367 void FastUnloadController::ProcessPendingTabs() {
    368   if (!is_attempting_to_close_browser_) {
    369     // Because we might invoke this after a delay it's possible for the value of
    370     // is_attempting_to_close_browser_ to have changed since we scheduled the
    371     // task.
    372     return;
    373   }
    374 
    375   if (tab_needing_before_unload_ack_ != NULL) {
    376     // Wait for |BeforeUnloadFired| before proceeding.
    377     return;
    378   }
    379 
    380   // Process a beforeunload handler.
    381   if (!tabs_needing_before_unload_.empty()) {
    382     WebContentsSet::iterator it = tabs_needing_before_unload_.begin();
    383     content::WebContents* contents = *it;
    384     tabs_needing_before_unload_.erase(it);
    385     // Null check render_view_host here as this gets called on a PostTask and
    386     // the tab's render_view_host may have been nulled out.
    387     if (contents->GetRenderViewHost()) {
    388       tab_needing_before_unload_ack_ = contents;
    389 
    390       CoreTabHelper* core_tab_helper = CoreTabHelper::FromWebContents(contents);
    391       core_tab_helper->OnCloseStarted();
    392 
    393       // If there's a devtools window attached to |contents|,
    394       // we would like devtools to call its own beforeunload handlers first,
    395       // and then call beforeunload handlers for |contents|.
    396       // See DevToolsWindow::InterceptPageBeforeUnload for details.
    397       if (!DevToolsWindow::InterceptPageBeforeUnload(contents))
    398         contents->DispatchBeforeUnload(false);
    399     } else {
    400       ProcessPendingTabs();
    401     }
    402     return;
    403   }
    404 
    405   if (is_calling_before_unload_handlers()) {
    406     on_close_confirmed_.Run(true);
    407     return;
    408   }
    409   // Process all the unload handlers. (The beforeunload handlers have finished.)
    410   if (!tabs_needing_unload_.empty()) {
    411     browser_->OnWindowClosing();
    412 
    413     // Run unload handlers detached since no more interaction is possible.
    414     WebContentsSet::iterator it = tabs_needing_unload_.begin();
    415     while (it != tabs_needing_unload_.end()) {
    416       WebContentsSet::iterator current = it++;
    417       content::WebContents* contents = *current;
    418       tabs_needing_unload_.erase(current);
    419       // Null check render_view_host here as this gets called on a PostTask
    420       // and the tab's render_view_host may have been nulled out.
    421       if (contents->GetRenderViewHost()) {
    422         CoreTabHelper* core_tab_helper =
    423             CoreTabHelper::FromWebContents(contents);
    424         core_tab_helper->OnUnloadStarted();
    425         DetachWebContents(contents);
    426         contents->GetRenderViewHost()->ClosePage();
    427       }
    428     }
    429 
    430     // Get the browser hidden.
    431     if (browser_->tab_strip_model()->empty()) {
    432       browser_->TabStripEmpty();
    433     } else {
    434       browser_->tab_strip_model()->CloseAllTabs();  // tabs not needing unload
    435     }
    436     return;
    437   }
    438 
    439   if (HasCompletedUnloadProcessing()) {
    440     browser_->OnWindowClosing();
    441 
    442     // Get the browser closed.
    443     if (browser_->tab_strip_model()->empty()) {
    444       browser_->TabStripEmpty();
    445     } else {
    446       // There may be tabs if the last tab needing beforeunload crashed.
    447       browser_->tab_strip_model()->CloseAllTabs();
    448     }
    449     return;
    450   }
    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