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/devtools/devtools_window.h"
     10 #include "chrome/browser/ui/browser.h"
     11 #include "chrome/browser/ui/browser_tabstrip.h"
     12 #include "chrome/browser/ui/tabs/tab_strip_model.h"
     13 #include "content/public/browser/notification_service.h"
     14 #include "content/public/browser/notification_source.h"
     15 #include "content/public/browser/notification_types.h"
     16 #include "content/public/browser/render_view_host.h"
     17 #include "content/public/browser/web_contents.h"
     18 
     19 namespace chrome {
     20 
     21 ////////////////////////////////////////////////////////////////////////////////
     22 // UnloadController, public:
     23 
     24 UnloadController::UnloadController(Browser* browser)
     25     : browser_(browser),
     26       is_attempting_to_close_browser_(false),
     27       weak_factory_(this) {
     28   browser_->tab_strip_model()->AddObserver(this);
     29 }
     30 
     31 UnloadController::~UnloadController() {
     32   browser_->tab_strip_model()->RemoveObserver(this);
     33 }
     34 
     35 bool UnloadController::CanCloseContents(content::WebContents* contents) {
     36   // Don't try to close the tab when the whole browser is being closed, since
     37   // that avoids the fast shutdown path where we just kill all the renderers.
     38   if (is_attempting_to_close_browser_)
     39     ClearUnloadState(contents, true);
     40   return !is_attempting_to_close_browser_ ||
     41       is_calling_before_unload_handlers();
     42 }
     43 
     44 // static
     45 bool UnloadController::ShouldRunUnloadEventsHelper(
     46     content::WebContents* contents) {
     47   // If |contents| is being inspected, devtools needs to intercept beforeunload
     48   // events.
     49   return DevToolsWindow::GetInstanceForInspectedRenderViewHost(
     50       contents->GetRenderViewHost()) != NULL;
     51 }
     52 
     53 // static
     54 bool UnloadController::RunUnloadEventsHelper(content::WebContents* contents) {
     55   // If there's a devtools window attached to |contents|,
     56   // we would like devtools to call its own beforeunload handlers first,
     57   // and then call beforeunload handlers for |contents|.
     58   // See DevToolsWindow::InterceptPageBeforeUnload for details.
     59   if (DevToolsWindow::InterceptPageBeforeUnload(contents)) {
     60     return true;
     61   }
     62   // If the WebContents is not connected yet, then there's no unload
     63   // handler we can fire even if the WebContents has an unload listener.
     64   // One case where we hit this is in a tab that has an infinite loop
     65   // before load.
     66   if (contents->NeedToFireBeforeUnload()) {
     67     // If the page has unload listeners, then we tell the renderer to fire
     68     // them. Once they have fired, we'll get a message back saying whether
     69     // to proceed closing the page or not, which sends us back to this method
     70     // with the NeedToFireBeforeUnload bit cleared.
     71     contents->DispatchBeforeUnload(false);
     72     return true;
     73   }
     74   return false;
     75 }
     76 
     77 bool UnloadController::BeforeUnloadFired(content::WebContents* contents,
     78                                          bool proceed) {
     79   if (!proceed)
     80     DevToolsWindow::OnPageCloseCanceled(contents);
     81 
     82   if (!is_attempting_to_close_browser_) {
     83     if (!proceed)
     84       contents->SetClosedByUserGesture(false);
     85     return proceed;
     86   }
     87 
     88   if (!proceed) {
     89     CancelWindowClose();
     90     contents->SetClosedByUserGesture(false);
     91     return false;
     92   }
     93 
     94   if (RemoveFromSet(&tabs_needing_before_unload_fired_, contents)) {
     95     // Now that beforeunload has fired, put the tab on the queue to fire
     96     // unload.
     97     tabs_needing_unload_fired_.insert(contents);
     98     ProcessPendingTabs();
     99     // We want to handle firing the unload event ourselves since we want to
    100     // fire all the beforeunload events before attempting to fire the unload
    101     // events should the user cancel closing the browser.
    102     return false;
    103   }
    104 
    105   return true;
    106 }
    107 
    108 bool UnloadController::ShouldCloseWindow() {
    109   if (HasCompletedUnloadProcessing())
    110     return true;
    111 
    112   // Special case for when we quit an application. The devtools window can
    113   // close if it's beforeunload event has already fired which will happen due
    114   // to the interception of it's content's beforeunload.
    115   if (browser_->is_devtools() &&
    116       DevToolsWindow::HasFiredBeforeUnloadEventForDevToolsBrowser(browser_)) {
    117     return true;
    118   }
    119 
    120   // The behavior followed here varies based on the current phase of the
    121   // operation and whether a batched shutdown is in progress.
    122   //
    123   // If there are tabs with outstanding beforeunload handlers:
    124   // 1. If a batched shutdown is in progress: return false.
    125   //    This is to prevent interference with batched shutdown already in
    126   //    progress.
    127   // 2. Otherwise: start sending beforeunload events and return false.
    128   //
    129   // Otherwise, If there are no tabs with outstanding beforeunload handlers:
    130   // 3. If a batched shutdown is in progress: start sending unload events and
    131   //    return false.
    132   // 4. Otherwise: return true.
    133   is_attempting_to_close_browser_ = true;
    134   // Cases 1 and 4.
    135   bool need_beforeunload_fired = TabsNeedBeforeUnloadFired();
    136   if (need_beforeunload_fired == is_calling_before_unload_handlers())
    137     return !need_beforeunload_fired;
    138 
    139   // Cases 2 and 3.
    140   on_close_confirmed_.Reset();
    141   ProcessPendingTabs();
    142   return false;
    143 }
    144 
    145 bool UnloadController::CallBeforeUnloadHandlers(
    146     const base::Callback<void(bool)>& on_close_confirmed) {
    147   // The devtools browser gets its beforeunload events as the results of
    148   // intercepting events from the inspected tab, so don't send them here as
    149   // well.
    150   if (browser_->is_devtools() || HasCompletedUnloadProcessing() ||
    151       !TabsNeedBeforeUnloadFired())
    152     return false;
    153 
    154   is_attempting_to_close_browser_ = true;
    155   on_close_confirmed_ = on_close_confirmed;
    156 
    157   ProcessPendingTabs();
    158   return true;
    159 }
    160 
    161 void UnloadController::ResetBeforeUnloadHandlers() {
    162   if (!is_calling_before_unload_handlers())
    163     return;
    164   CancelWindowClose();
    165 }
    166 
    167 bool UnloadController::TabsNeedBeforeUnloadFired() {
    168   if (tabs_needing_before_unload_fired_.empty()) {
    169     for (int i = 0; i < browser_->tab_strip_model()->count(); ++i) {
    170       content::WebContents* contents =
    171           browser_->tab_strip_model()->GetWebContentsAt(i);
    172       bool should_fire_beforeunload = contents->NeedToFireBeforeUnload() ||
    173           DevToolsWindow::NeedsToInterceptBeforeUnload(contents);
    174       if (!ContainsKey(tabs_needing_unload_fired_, contents) &&
    175           should_fire_beforeunload) {
    176         tabs_needing_before_unload_fired_.insert(contents);
    177       }
    178     }
    179   }
    180   return !tabs_needing_before_unload_fired_.empty();
    181 }
    182 
    183 void UnloadController::CancelWindowClose() {
    184   // Closing of window can be canceled from a beforeunload handler.
    185   DCHECK(is_attempting_to_close_browser_);
    186   tabs_needing_before_unload_fired_.clear();
    187   for (UnloadListenerSet::iterator it = tabs_needing_unload_fired_.begin();
    188       it != tabs_needing_unload_fired_.end(); ++it) {
    189     DevToolsWindow::OnPageCloseCanceled(*it);
    190   }
    191   tabs_needing_unload_fired_.clear();
    192   if (is_calling_before_unload_handlers()) {
    193     base::Callback<void(bool)> on_close_confirmed = on_close_confirmed_;
    194     on_close_confirmed_.Reset();
    195     on_close_confirmed.Run(false);
    196   }
    197   is_attempting_to_close_browser_ = false;
    198 
    199   content::NotificationService::current()->Notify(
    200       chrome::NOTIFICATION_BROWSER_CLOSE_CANCELLED,
    201       content::Source<Browser>(browser_),
    202       content::NotificationService::NoDetails());
    203 }
    204 
    205 ////////////////////////////////////////////////////////////////////////////////
    206 // UnloadController, content::NotificationObserver implementation:
    207 
    208 void UnloadController::Observe(int type,
    209                                const content::NotificationSource& source,
    210                                const content::NotificationDetails& details) {
    211   switch (type) {
    212     case content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED:
    213       if (is_attempting_to_close_browser_) {
    214         ClearUnloadState(content::Source<content::WebContents>(source).ptr(),
    215                          false);  // See comment for ClearUnloadState().
    216       }
    217       break;
    218     default:
    219       NOTREACHED() << "Got a notification we didn't register for.";
    220   }
    221 }
    222 
    223 ////////////////////////////////////////////////////////////////////////////////
    224 // UnloadController, TabStripModelObserver implementation:
    225 
    226 void UnloadController::TabInsertedAt(content::WebContents* contents,
    227                                      int index,
    228                                      bool foreground) {
    229   TabAttachedImpl(contents);
    230 }
    231 
    232 void UnloadController::TabDetachedAt(content::WebContents* contents,
    233                                      int index) {
    234   TabDetachedImpl(contents);
    235 }
    236 
    237 void UnloadController::TabReplacedAt(TabStripModel* tab_strip_model,
    238                                      content::WebContents* old_contents,
    239                                      content::WebContents* new_contents,
    240                                      int index) {
    241   TabDetachedImpl(old_contents);
    242   TabAttachedImpl(new_contents);
    243 }
    244 
    245 void UnloadController::TabStripEmpty() {
    246   // Set is_attempting_to_close_browser_ here, so that extensions, etc, do not
    247   // attempt to add tabs to the browser before it closes.
    248   is_attempting_to_close_browser_ = true;
    249 }
    250 
    251 ////////////////////////////////////////////////////////////////////////////////
    252 // UnloadController, private:
    253 
    254 void UnloadController::TabAttachedImpl(content::WebContents* contents) {
    255   // If the tab crashes in the beforeunload or unload handler, it won't be
    256   // able to ack. But we know we can close it.
    257   registrar_.Add(
    258       this,
    259       content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED,
    260       content::Source<content::WebContents>(contents));
    261 }
    262 
    263 void UnloadController::TabDetachedImpl(content::WebContents* contents) {
    264   if (is_attempting_to_close_browser_)
    265     ClearUnloadState(contents, false);
    266   registrar_.Remove(this,
    267                     content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED,
    268                     content::Source<content::WebContents>(contents));
    269 }
    270 
    271 void UnloadController::ProcessPendingTabs() {
    272   if (!is_attempting_to_close_browser_) {
    273     // Because we might invoke this after a delay it's possible for the value of
    274     // is_attempting_to_close_browser_ to have changed since we scheduled the
    275     // task.
    276     return;
    277   }
    278 
    279   if (HasCompletedUnloadProcessing()) {
    280     // We've finished all the unload events and can proceed to close the
    281     // browser.
    282     browser_->OnWindowClosing();
    283     return;
    284   }
    285 
    286   // Process beforeunload tabs first. When that queue is empty, process
    287   // unload tabs.
    288   if (!tabs_needing_before_unload_fired_.empty()) {
    289     content::WebContents* web_contents =
    290         *(tabs_needing_before_unload_fired_.begin());
    291     // Null check render_view_host here as this gets called on a PostTask and
    292     // the tab's render_view_host may have been nulled out.
    293     if (web_contents->GetRenderViewHost()) {
    294       // If there's a devtools window attached to |web_contents|,
    295       // we would like devtools to call its own beforeunload handlers first,
    296       // and then call beforeunload handlers for |web_contents|.
    297       // See DevToolsWindow::InterceptPageBeforeUnload for details.
    298       if (!DevToolsWindow::InterceptPageBeforeUnload(web_contents))
    299         web_contents->DispatchBeforeUnload(false);
    300     } else {
    301       ClearUnloadState(web_contents, true);
    302     }
    303   } else if (is_calling_before_unload_handlers()) {
    304     on_close_confirmed_.Run(true);
    305   } else if (!tabs_needing_unload_fired_.empty()) {
    306     // We've finished firing all beforeunload events and can proceed with unload
    307     // events.
    308     // TODO(ojan): We should add a call to browser_shutdown::OnShutdownStarting
    309     // somewhere around here so that we have accurate measurements of shutdown
    310     // time.
    311     // TODO(ojan): We can probably fire all the unload events in parallel and
    312     // get a perf benefit from that in the cases where the tab hangs in it's
    313     // unload handler or takes a long time to page in.
    314     content::WebContents* web_contents = *(tabs_needing_unload_fired_.begin());
    315     // Null check render_view_host here as this gets called on a PostTask and
    316     // the tab's render_view_host may have been nulled out.
    317     if (web_contents->GetRenderViewHost()) {
    318       web_contents->GetRenderViewHost()->ClosePage();
    319     } else {
    320       ClearUnloadState(web_contents, true);
    321     }
    322   } else {
    323     NOTREACHED();
    324   }
    325 }
    326 
    327 bool UnloadController::HasCompletedUnloadProcessing() const {
    328   return is_attempting_to_close_browser_ &&
    329       tabs_needing_before_unload_fired_.empty() &&
    330       tabs_needing_unload_fired_.empty();
    331 }
    332 
    333 bool UnloadController::RemoveFromSet(UnloadListenerSet* set,
    334                                      content::WebContents* web_contents) {
    335   DCHECK(is_attempting_to_close_browser_);
    336 
    337   UnloadListenerSet::iterator iter =
    338       std::find(set->begin(), set->end(), web_contents);
    339   if (iter != set->end()) {
    340     set->erase(iter);
    341     return true;
    342   }
    343   return false;
    344 }
    345 
    346 void UnloadController::ClearUnloadState(content::WebContents* web_contents,
    347                                         bool process_now) {
    348   if (is_attempting_to_close_browser_) {
    349     RemoveFromSet(&tabs_needing_before_unload_fired_, web_contents);
    350     RemoveFromSet(&tabs_needing_unload_fired_, web_contents);
    351     if (process_now) {
    352       ProcessPendingTabs();
    353     } else {
    354       base::MessageLoop::current()->PostTask(
    355           FROM_HERE,
    356           base::Bind(&UnloadController::ProcessPendingTabs,
    357                      weak_factory_.GetWeakPtr()));
    358     }
    359   }
    360 }
    361 
    362 }  // namespace chrome
    363