Home | History | Annotate | Download | only in ui
      1 // Copyright (c) 2012 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/unload_controller.h"
      6 
      7 #include "base/message_loop/message_loop.h"
      8 #include "chrome/browser/chrome_notification_types.h"
      9 #include "chrome/browser/ui/browser.h"
     10 #include "chrome/browser/ui/browser_tabstrip.h"
     11 #include "chrome/browser/ui/tabs/tab_strip_model.h"
     12 #include "content/public/browser/notification_service.h"
     13 #include "content/public/browser/notification_source.h"
     14 #include "content/public/browser/notification_types.h"
     15 #include "content/public/browser/render_view_host.h"
     16 #include "content/public/browser/web_contents.h"
     17 
     18 namespace chrome {
     19 
     20 ////////////////////////////////////////////////////////////////////////////////
     21 // UnloadController, public:
     22 
     23 UnloadController::UnloadController(Browser* browser)
     24     : browser_(browser),
     25       is_attempting_to_close_browser_(false),
     26       weak_factory_(this) {
     27   browser_->tab_strip_model()->AddObserver(this);
     28 }
     29 
     30 UnloadController::~UnloadController() {
     31   browser_->tab_strip_model()->RemoveObserver(this);
     32 }
     33 
     34 bool UnloadController::CanCloseContents(content::WebContents* contents) {
     35   // Don't try to close the tab when the whole browser is being closed, since
     36   // that avoids the fast shutdown path where we just kill all the renderers.
     37   if (is_attempting_to_close_browser_)
     38     ClearUnloadState(contents, true);
     39   return !is_attempting_to_close_browser_;
     40 }
     41 
     42 bool UnloadController::BeforeUnloadFired(content::WebContents* contents,
     43                                          bool proceed) {
     44   if (!is_attempting_to_close_browser_) {
     45     if (!proceed)
     46       contents->SetClosedByUserGesture(false);
     47     return proceed;
     48   }
     49 
     50   if (!proceed) {
     51     CancelWindowClose();
     52     contents->SetClosedByUserGesture(false);
     53     return false;
     54   }
     55 
     56   if (RemoveFromSet(&tabs_needing_before_unload_fired_, contents)) {
     57     // Now that beforeunload has fired, put the tab on the queue to fire
     58     // unload.
     59     tabs_needing_unload_fired_.insert(contents);
     60     ProcessPendingTabs();
     61     // We want to handle firing the unload event ourselves since we want to
     62     // fire all the beforeunload events before attempting to fire the unload
     63     // events should the user cancel closing the browser.
     64     return false;
     65   }
     66 
     67   return true;
     68 }
     69 
     70 bool UnloadController::ShouldCloseWindow() {
     71   if (HasCompletedUnloadProcessing())
     72     return true;
     73 
     74   is_attempting_to_close_browser_ = true;
     75 
     76   if (!TabsNeedBeforeUnloadFired())
     77     return true;
     78 
     79   ProcessPendingTabs();
     80   return false;
     81 }
     82 
     83 bool UnloadController::TabsNeedBeforeUnloadFired() {
     84   if (tabs_needing_before_unload_fired_.empty()) {
     85     for (int i = 0; i < browser_->tab_strip_model()->count(); ++i) {
     86       content::WebContents* contents =
     87           browser_->tab_strip_model()->GetWebContentsAt(i);
     88       if (contents->NeedToFireBeforeUnload())
     89         tabs_needing_before_unload_fired_.insert(contents);
     90     }
     91   }
     92   return !tabs_needing_before_unload_fired_.empty();
     93 }
     94 
     95 ////////////////////////////////////////////////////////////////////////////////
     96 // UnloadController, content::NotificationObserver implementation:
     97 
     98 void UnloadController::Observe(int type,
     99                                const content::NotificationSource& source,
    100                                const content::NotificationDetails& details) {
    101   switch (type) {
    102     case content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED:
    103       if (is_attempting_to_close_browser_) {
    104         ClearUnloadState(content::Source<content::WebContents>(source).ptr(),
    105                          false);  // See comment for ClearUnloadState().
    106       }
    107       break;
    108     default:
    109       NOTREACHED() << "Got a notification we didn't register for.";
    110   }
    111 }
    112 
    113 ////////////////////////////////////////////////////////////////////////////////
    114 // UnloadController, TabStripModelObserver implementation:
    115 
    116 void UnloadController::TabInsertedAt(content::WebContents* contents,
    117                                      int index,
    118                                      bool foreground) {
    119   TabAttachedImpl(contents);
    120 }
    121 
    122 void UnloadController::TabDetachedAt(content::WebContents* contents,
    123                                      int index) {
    124   TabDetachedImpl(contents);
    125 }
    126 
    127 void UnloadController::TabReplacedAt(TabStripModel* tab_strip_model,
    128                                      content::WebContents* old_contents,
    129                                      content::WebContents* new_contents,
    130                                      int index) {
    131   TabDetachedImpl(old_contents);
    132   TabAttachedImpl(new_contents);
    133 }
    134 
    135 void UnloadController::TabStripEmpty() {
    136   // Set is_attempting_to_close_browser_ here, so that extensions, etc, do not
    137   // attempt to add tabs to the browser before it closes.
    138   is_attempting_to_close_browser_ = true;
    139 }
    140 
    141 ////////////////////////////////////////////////////////////////////////////////
    142 // UnloadController, private:
    143 
    144 void UnloadController::TabAttachedImpl(content::WebContents* contents) {
    145   // If the tab crashes in the beforeunload or unload handler, it won't be
    146   // able to ack. But we know we can close it.
    147   registrar_.Add(
    148       this,
    149       content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED,
    150       content::Source<content::WebContents>(contents));
    151 }
    152 
    153 void UnloadController::TabDetachedImpl(content::WebContents* contents) {
    154   if (is_attempting_to_close_browser_)
    155     ClearUnloadState(contents, false);
    156   registrar_.Remove(this,
    157                     content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED,
    158                     content::Source<content::WebContents>(contents));
    159 }
    160 
    161 void UnloadController::ProcessPendingTabs() {
    162   if (!is_attempting_to_close_browser_) {
    163     // Because we might invoke this after a delay it's possible for the value of
    164     // is_attempting_to_close_browser_ to have changed since we scheduled the
    165     // task.
    166     return;
    167   }
    168 
    169   if (HasCompletedUnloadProcessing()) {
    170     // We've finished all the unload events and can proceed to close the
    171     // browser.
    172     browser_->OnWindowClosing();
    173     return;
    174   }
    175 
    176   // Process beforeunload tabs first. When that queue is empty, process
    177   // unload tabs.
    178   if (!tabs_needing_before_unload_fired_.empty()) {
    179     content::WebContents* web_contents =
    180         *(tabs_needing_before_unload_fired_.begin());
    181     // Null check render_view_host here as this gets called on a PostTask and
    182     // the tab's render_view_host may have been nulled out.
    183     if (web_contents->GetRenderViewHost()) {
    184       web_contents->GetRenderViewHost()->FirePageBeforeUnload(false);
    185     } else {
    186       ClearUnloadState(web_contents, true);
    187     }
    188   } else if (!tabs_needing_unload_fired_.empty()) {
    189     // We've finished firing all beforeunload events and can proceed with unload
    190     // events.
    191     // TODO(ojan): We should add a call to browser_shutdown::OnShutdownStarting
    192     // somewhere around here so that we have accurate measurements of shutdown
    193     // time.
    194     // TODO(ojan): We can probably fire all the unload events in parallel and
    195     // get a perf benefit from that in the cases where the tab hangs in it's
    196     // unload handler or takes a long time to page in.
    197     content::WebContents* web_contents = *(tabs_needing_unload_fired_.begin());
    198     // Null check render_view_host here as this gets called on a PostTask and
    199     // the tab's render_view_host may have been nulled out.
    200     if (web_contents->GetRenderViewHost()) {
    201       web_contents->GetRenderViewHost()->ClosePage();
    202     } else {
    203       ClearUnloadState(web_contents, true);
    204     }
    205   } else {
    206     NOTREACHED();
    207   }
    208 }
    209 
    210 bool UnloadController::HasCompletedUnloadProcessing() const {
    211   return is_attempting_to_close_browser_ &&
    212       tabs_needing_before_unload_fired_.empty() &&
    213       tabs_needing_unload_fired_.empty();
    214 }
    215 
    216 void UnloadController::CancelWindowClose() {
    217   // Closing of window can be canceled from a beforeunload handler.
    218   DCHECK(is_attempting_to_close_browser_);
    219   tabs_needing_before_unload_fired_.clear();
    220   tabs_needing_unload_fired_.clear();
    221   is_attempting_to_close_browser_ = false;
    222 
    223   content::NotificationService::current()->Notify(
    224       chrome::NOTIFICATION_BROWSER_CLOSE_CANCELLED,
    225       content::Source<Browser>(browser_),
    226       content::NotificationService::NoDetails());
    227 }
    228 
    229 bool UnloadController::RemoveFromSet(UnloadListenerSet* set,
    230                                      content::WebContents* web_contents) {
    231   DCHECK(is_attempting_to_close_browser_);
    232 
    233   UnloadListenerSet::iterator iter =
    234       std::find(set->begin(), set->end(), web_contents);
    235   if (iter != set->end()) {
    236     set->erase(iter);
    237     return true;
    238   }
    239   return false;
    240 }
    241 
    242 void UnloadController::ClearUnloadState(content::WebContents* web_contents,
    243                                         bool process_now) {
    244   if (is_attempting_to_close_browser_) {
    245     RemoveFromSet(&tabs_needing_before_unload_fired_, web_contents);
    246     RemoveFromSet(&tabs_needing_unload_fired_, web_contents);
    247     if (process_now) {
    248       ProcessPendingTabs();
    249     } else {
    250       base::MessageLoop::current()->PostTask(
    251           FROM_HERE,
    252           base::Bind(&UnloadController::ProcessPendingTabs,
    253                      weak_factory_.GetWeakPtr()));
    254     }
    255   }
    256 }
    257 
    258 }  // namespace chrome
    259