Home | History | Annotate | Download | only in views
      1 // Copyright 2014 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/views/session_crashed_bubble_view.h"
      6 
      7 #include <vector>
      8 
      9 #include "base/bind.h"
     10 #include "base/bind_helpers.h"
     11 #include "base/command_line.h"
     12 #include "base/metrics/field_trial.h"
     13 #include "base/metrics/histogram.h"
     14 #include "base/prefs/pref_service.h"
     15 #include "chrome/browser/browser_process.h"
     16 #include "chrome/browser/chrome_notification_types.h"
     17 #include "chrome/browser/metrics/metrics_reporting_state.h"
     18 #include "chrome/browser/sessions/session_restore.h"
     19 #include "chrome/browser/ui/browser_list.h"
     20 #include "chrome/browser/ui/browser_list_observer.h"
     21 #include "chrome/browser/ui/startup/session_crashed_bubble.h"
     22 #include "chrome/browser/ui/startup/startup_browser_creator_impl.h"
     23 #include "chrome/browser/ui/tabs/tab_strip_model.h"
     24 #include "chrome/browser/ui/views/frame/browser_view.h"
     25 #include "chrome/browser/ui/views/toolbar/toolbar_view.h"
     26 #include "chrome/common/chrome_switches.h"
     27 #include "chrome/common/pref_names.h"
     28 #include "chrome/common/url_constants.h"
     29 #include "chrome/grit/chromium_strings.h"
     30 #include "chrome/grit/generated_resources.h"
     31 #include "chrome/grit/google_chrome_strings.h"
     32 #include "chrome/installer/util/google_update_settings.h"
     33 #include "content/public/browser/browser_context.h"
     34 #include "content/public/browser/browser_thread.h"
     35 #include "content/public/browser/notification_source.h"
     36 #include "content/public/browser/web_contents.h"
     37 #include "ui/base/l10n/l10n_util.h"
     38 #include "ui/views/bubble/bubble_frame_view.h"
     39 #include "ui/views/controls/button/checkbox.h"
     40 #include "ui/views/controls/button/label_button.h"
     41 #include "ui/views/controls/label.h"
     42 #include "ui/views/controls/separator.h"
     43 #include "ui/views/controls/styled_label.h"
     44 #include "ui/views/layout/grid_layout.h"
     45 #include "ui/views/layout/layout_constants.h"
     46 #include "ui/views/widget/widget.h"
     47 
     48 using views::GridLayout;
     49 
     50 namespace {
     51 
     52 // Fixed width of the column holding the description label of the bubble.
     53 const int kWidthOfDescriptionText = 320;
     54 
     55 // Distance between checkbox and the text to the right of it.
     56 const int kCheckboxTextDistance = 4;
     57 
     58 // The color of the text and background of the sub panel to offer UMA optin.
     59 // These values match the BookmarkSyncPromoView colors.
     60 const SkColor kBackgroundColor = SkColorSetRGB(245, 245, 245);
     61 const SkColor kTextColor = SkColorSetRGB(102, 102, 102);
     62 
     63 // The Finch study name and group name that enables session crashed bubble UI.
     64 const char kEnableBubbleUIFinchName[] = "EnableSessionCrashedBubbleUI";
     65 const char kEnableBubbleUIGroupEnabled[] = "Enabled";
     66 
     67 enum SessionCrashedBubbleHistogramValue {
     68   SESSION_CRASHED_BUBBLE_SHOWN,
     69   SESSION_CRASHED_BUBBLE_ERROR,
     70   SESSION_CRASHED_BUBBLE_RESTORED,
     71   SESSION_CRASHED_BUBBLE_ALREADY_UMA_OPTIN,
     72   SESSION_CRASHED_BUBBLE_UMA_OPTIN,
     73   SESSION_CRASHED_BUBBLE_HELP,
     74   SESSION_CRASHED_BUBBLE_IGNORED,
     75   SESSION_CRASHED_BUBBLE_OPTIN_BAR_SHOWN,
     76   SESSION_CRASHED_BUBBLE_MAX,
     77 };
     78 
     79 void RecordBubbleHistogramValue(SessionCrashedBubbleHistogramValue value) {
     80   UMA_HISTOGRAM_ENUMERATION(
     81       "SessionCrashed.Bubble", value, SESSION_CRASHED_BUBBLE_MAX);
     82 }
     83 
     84 // Whether or not the bubble UI should be used.
     85 bool IsBubbleUIEnabled() {
     86   const base::CommandLine& command_line = *CommandLine::ForCurrentProcess();
     87   if (command_line.HasSwitch(switches::kDisableSessionCrashedBubble))
     88     return false;
     89   if (command_line.HasSwitch(switches::kEnableSessionCrashedBubble))
     90     return true;
     91   const std::string group_name = base::FieldTrialList::FindFullName(
     92       kEnableBubbleUIFinchName);
     93   return group_name == kEnableBubbleUIGroupEnabled;
     94 }
     95 
     96 }  // namespace
     97 
     98 // A helper class that listens to browser removal event.
     99 class SessionCrashedBubbleView::BrowserRemovalObserver
    100     : public chrome::BrowserListObserver {
    101  public:
    102   explicit BrowserRemovalObserver(Browser* browser) : browser_(browser) {
    103     DCHECK(browser_);
    104     BrowserList::AddObserver(this);
    105   }
    106 
    107   virtual ~BrowserRemovalObserver() {
    108     BrowserList::RemoveObserver(this);
    109   }
    110 
    111   // Overridden from chrome::BrowserListObserver.
    112   virtual void OnBrowserRemoved(Browser* browser) OVERRIDE {
    113     if (browser == browser_)
    114       browser_ = NULL;
    115   }
    116 
    117   Browser* browser() const { return browser_; }
    118 
    119  private:
    120   Browser* browser_;
    121 
    122   DISALLOW_COPY_AND_ASSIGN(BrowserRemovalObserver);
    123 };
    124 
    125 // static
    126 void SessionCrashedBubbleView::Show(Browser* browser) {
    127   DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
    128   if (browser->profile()->IsOffTheRecord())
    129     return;
    130 
    131   // Observes browser removal event and will be deallocated in ShowForReal.
    132   scoped_ptr<BrowserRemovalObserver> browser_observer(
    133       new BrowserRemovalObserver(browser));
    134 
    135 // Stats collection only applies to Google Chrome builds.
    136 #if defined(GOOGLE_CHROME_BUILD)
    137   // Schedule a task to run GoogleUpdateSettings::GetCollectStatsConsent() on
    138   // FILE thread, since it does IO. Then, call
    139   // SessionCrashedBubbleView::ShowForReal with the result.
    140   content::BrowserThread::PostTaskAndReplyWithResult(
    141       content::BrowserThread::FILE,
    142       FROM_HERE,
    143       base::Bind(&GoogleUpdateSettings::GetCollectStatsConsent),
    144       base::Bind(&SessionCrashedBubbleView::ShowForReal,
    145                  base::Passed(&browser_observer)));
    146 #else
    147   SessionCrashedBubbleView::ShowForReal(browser_observer.Pass(), false);
    148 #endif  // defined(GOOGLE_CHROME_BUILD)
    149 }
    150 
    151 // static
    152 void SessionCrashedBubbleView::ShowForReal(
    153     scoped_ptr<BrowserRemovalObserver> browser_observer,
    154     bool uma_opted_in_already) {
    155   // Determine whether or not the UMA opt-in option should be offered. It is
    156   // offered only when it is a Google chrome build, user hasn't opted in yet,
    157   // and the preference is modifiable by the user.
    158   bool offer_uma_optin = false;
    159 
    160 #if defined(GOOGLE_CHROME_BUILD)
    161   if (!uma_opted_in_already) {
    162     offer_uma_optin = g_browser_process->local_state()->FindPreference(
    163         prefs::kMetricsReportingEnabled)->IsUserModifiable();
    164   }
    165 #endif  // defined(GOOGLE_CHROME_BUILD)
    166 
    167   Browser* browser = browser_observer->browser();
    168 
    169   if (!browser) {
    170     RecordBubbleHistogramValue(SESSION_CRASHED_BUBBLE_ERROR);
    171     return;
    172   }
    173 
    174   views::View* anchor_view =
    175       BrowserView::GetBrowserViewForBrowser(browser)->toolbar()->app_menu();
    176   content::WebContents* web_contents =
    177       browser->tab_strip_model()->GetActiveWebContents();
    178 
    179   if (!web_contents) {
    180     RecordBubbleHistogramValue(SESSION_CRASHED_BUBBLE_ERROR);
    181     return;
    182   }
    183 
    184   SessionCrashedBubbleView* crash_bubble =
    185       new SessionCrashedBubbleView(anchor_view, browser, web_contents,
    186                                    offer_uma_optin);
    187   views::BubbleDelegateView::CreateBubble(crash_bubble)->Show();
    188 
    189   RecordBubbleHistogramValue(SESSION_CRASHED_BUBBLE_SHOWN);
    190   if (uma_opted_in_already)
    191     RecordBubbleHistogramValue(SESSION_CRASHED_BUBBLE_ALREADY_UMA_OPTIN);
    192 }
    193 
    194 SessionCrashedBubbleView::SessionCrashedBubbleView(
    195     views::View* anchor_view,
    196     Browser* browser,
    197     content::WebContents* web_contents,
    198     bool offer_uma_optin)
    199     : BubbleDelegateView(anchor_view, views::BubbleBorder::TOP_RIGHT),
    200       content::WebContentsObserver(web_contents),
    201       browser_(browser),
    202       web_contents_(web_contents),
    203       restore_button_(NULL),
    204       uma_option_(NULL),
    205       offer_uma_optin_(offer_uma_optin),
    206       started_navigation_(false),
    207       restored_(false) {
    208   set_close_on_deactivate(false);
    209   registrar_.Add(
    210       this,
    211       chrome::NOTIFICATION_TAB_CLOSING,
    212       content::Source<content::NavigationController>(&(
    213           web_contents->GetController())));
    214   browser->tab_strip_model()->AddObserver(this);
    215 }
    216 
    217 SessionCrashedBubbleView::~SessionCrashedBubbleView() {
    218   browser_->tab_strip_model()->RemoveObserver(this);
    219 }
    220 
    221 views::View* SessionCrashedBubbleView::GetInitiallyFocusedView() {
    222   return restore_button_;
    223 }
    224 
    225 base::string16 SessionCrashedBubbleView::GetWindowTitle() const {
    226   return l10n_util::GetStringUTF16(IDS_SESSION_CRASHED_BUBBLE_TITLE);
    227 }
    228 
    229 bool SessionCrashedBubbleView::ShouldShowWindowTitle() const {
    230   return true;
    231 }
    232 
    233 bool SessionCrashedBubbleView::ShouldShowCloseButton() const {
    234   return true;
    235 }
    236 
    237 void SessionCrashedBubbleView::OnWidgetDestroying(views::Widget* widget) {
    238   if (!restored_)
    239     RecordBubbleHistogramValue(SESSION_CRASHED_BUBBLE_IGNORED);
    240   BubbleDelegateView::OnWidgetDestroying(widget);
    241 }
    242 
    243 void SessionCrashedBubbleView::Init() {
    244   // Description text label.
    245   views::Label* text_label = new views::Label(
    246       l10n_util::GetStringUTF16(IDS_SESSION_CRASHED_VIEW_MESSAGE));
    247   text_label->SetMultiLine(true);
    248   text_label->SetLineHeight(20);
    249   text_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
    250   text_label->SizeToFit(kWidthOfDescriptionText);
    251 
    252   // Restore button.
    253   restore_button_ = new views::LabelButton(
    254       this, l10n_util::GetStringUTF16(IDS_SESSION_CRASHED_VIEW_RESTORE_BUTTON));
    255   restore_button_->SetStyle(views::Button::STYLE_BUTTON);
    256   restore_button_->SetIsDefault(true);
    257 
    258   GridLayout* layout = new GridLayout(this);
    259   SetLayoutManager(layout);
    260 
    261   // Text row.
    262   const int kTextColumnSetId = 0;
    263   views::ColumnSet* cs = layout->AddColumnSet(kTextColumnSetId);
    264   cs->AddPaddingColumn(0, GetBubbleFrameView()->GetTitleInsets().left());
    265   cs->AddColumn(GridLayout::FILL, GridLayout::FILL, 1,
    266                 GridLayout::FIXED, kWidthOfDescriptionText, 0);
    267   cs->AddPaddingColumn(0, GetBubbleFrameView()->GetTitleInsets().left());
    268 
    269   // Restore button row.
    270   const int kButtonColumnSetId = 1;
    271   cs = layout->AddColumnSet(kButtonColumnSetId);
    272   cs->AddColumn(GridLayout::TRAILING, GridLayout::CENTER, 1,
    273                 GridLayout::USE_PREF, 0, 0);
    274   cs->AddPaddingColumn(0, GetBubbleFrameView()->GetTitleInsets().left());
    275 
    276   layout->StartRow(0, kTextColumnSetId);
    277   layout->AddView(text_label);
    278   layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
    279 
    280   layout->StartRow(0, kButtonColumnSetId);
    281   layout->AddView(restore_button_);
    282   layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
    283 
    284   int bottom_margin = 1;
    285 
    286   // Metrics reporting option.
    287   if (offer_uma_optin_) {
    288     const int kUMAOptionColumnSetId = 2;
    289     cs = layout->AddColumnSet(kUMAOptionColumnSetId);
    290     cs->AddColumn(
    291         GridLayout::FILL, GridLayout::FILL, 1, GridLayout::USE_PREF, 0, 0);
    292     layout->StartRow(1, kUMAOptionColumnSetId);
    293     layout->AddView(new views::Separator(views::Separator::HORIZONTAL));
    294     layout->StartRow(1, kUMAOptionColumnSetId);
    295     layout->AddView(CreateUMAOptinView());
    296 
    297     // Since the UMA optin row has a different background than the default
    298     // background color of bubbles, the bottom margin has to be 0 to make sure
    299     // the background extends to the bottom edge of the bubble.
    300     bottom_margin = 0;
    301 
    302     RecordBubbleHistogramValue(SESSION_CRASHED_BUBBLE_OPTIN_BAR_SHOWN);
    303   }
    304 
    305   set_margins(gfx::Insets(1, 0, bottom_margin, 0));
    306   Layout();
    307 }
    308 
    309 views::View* SessionCrashedBubbleView::CreateUMAOptinView() {
    310   // Checkbox for metric reporting setting.
    311   // Since the text to the right of the checkbox can't be a simple string (needs
    312   // a hyperlink in it), this checkbox contains an empty string as its label,
    313   // and the real text will be added as a separate view.
    314   uma_option_ = new views::Checkbox(base::string16());
    315   uma_option_->SetChecked(false);
    316 
    317   // The text to the right of the checkbox.
    318   size_t offset;
    319   base::string16 link_text =
    320       l10n_util::GetStringUTF16(IDS_SESSION_CRASHED_BUBBLE_UMA_LINK_TEXT);
    321   base::string16 uma_text = l10n_util::GetStringFUTF16(
    322       IDS_SESSION_CRASHED_VIEW_UMA_OPTIN,
    323       link_text,
    324       &offset);
    325   views::StyledLabel* uma_label = new views::StyledLabel(uma_text, this);
    326   views::StyledLabel::RangeStyleInfo link_style =
    327       views::StyledLabel::RangeStyleInfo::CreateForLink();
    328   link_style.font_style = gfx::Font::NORMAL;
    329   uma_label->AddStyleRange(gfx::Range(offset, offset + link_text.length()),
    330                            link_style);
    331   views::StyledLabel::RangeStyleInfo uma_style;
    332   uma_style.color = kTextColor;
    333   gfx::Range before_link_range(0, offset);
    334   if (!before_link_range.is_empty())
    335     uma_label->AddStyleRange(before_link_range, uma_style);
    336   gfx::Range after_link_range(offset + link_text.length(), uma_text.length());
    337   if (!after_link_range.is_empty())
    338     uma_label->AddStyleRange(after_link_range, uma_style);
    339 
    340   // Create a view to hold the checkbox and the text.
    341   views::View* uma_view = new views::View();
    342   GridLayout* uma_layout = new GridLayout(uma_view);
    343   uma_view->SetLayoutManager(uma_layout);
    344 
    345   uma_view->set_background(
    346       views::Background::CreateSolidBackground(kBackgroundColor));
    347   int inset_left = GetBubbleFrameView()->GetTitleInsets().left();
    348   uma_layout->SetInsets(views::kRelatedControlVerticalSpacing, inset_left,
    349                         views::kRelatedControlVerticalSpacing, inset_left);
    350 
    351   const int kReportColumnSetId = 0;
    352   views::ColumnSet* cs = uma_layout->AddColumnSet(kReportColumnSetId);
    353   cs->AddColumn(GridLayout::CENTER, GridLayout::LEADING, 0,
    354                 GridLayout::USE_PREF, 0, 0);
    355   cs->AddPaddingColumn(0, kCheckboxTextDistance);
    356   cs->AddColumn(GridLayout::FILL, GridLayout::FILL, 0,
    357                 GridLayout::FIXED, kWidthOfDescriptionText, 0);
    358 
    359   uma_layout->StartRow(0, kReportColumnSetId);
    360   uma_layout->AddView(uma_option_);
    361   uma_layout->AddView(uma_label);
    362 
    363   return uma_view;
    364 }
    365 
    366 void SessionCrashedBubbleView::ButtonPressed(views::Button* sender,
    367                                              const ui::Event& event) {
    368   DCHECK_EQ(sender, restore_button_);
    369   RestorePreviousSession(sender);
    370 }
    371 
    372 void SessionCrashedBubbleView::StyledLabelLinkClicked(const gfx::Range& range,
    373                                                       int event_flags) {
    374   browser_->OpenURL(content::OpenURLParams(
    375       GURL("https://support.google.com/chrome/answer/96817"),
    376       content::Referrer(),
    377       NEW_FOREGROUND_TAB,
    378       ui::PAGE_TRANSITION_LINK,
    379       false));
    380   RecordBubbleHistogramValue(SESSION_CRASHED_BUBBLE_HELP);
    381 }
    382 
    383 void SessionCrashedBubbleView::DidStartNavigationToPendingEntry(
    384       const GURL& url,
    385       content::NavigationController::ReloadType reload_type) {
    386   started_navigation_ = true;
    387 }
    388 
    389 void SessionCrashedBubbleView::DidFinishLoad(
    390     content::RenderFrameHost* render_frame_host,
    391     const GURL& validated_url) {
    392   if (started_navigation_)
    393     CloseBubble();
    394 }
    395 
    396 void SessionCrashedBubbleView::WasShown() {
    397   GetWidget()->Show();
    398 }
    399 
    400 void SessionCrashedBubbleView::WasHidden() {
    401   GetWidget()->Hide();
    402 }
    403 
    404 void SessionCrashedBubbleView::Observe(
    405     int type,
    406     const content::NotificationSource& source,
    407     const content::NotificationDetails& details) {
    408   if (type == chrome::NOTIFICATION_TAB_CLOSING)
    409     CloseBubble();
    410 }
    411 
    412 void SessionCrashedBubbleView::TabDetachedAt(content::WebContents* contents,
    413                                              int index) {
    414   if (web_contents_ == contents)
    415     CloseBubble();
    416 }
    417 
    418 void SessionCrashedBubbleView::RestorePreviousSession(views::Button* sender) {
    419   SessionRestore::RestoreSessionAfterCrash(browser_);
    420   RecordBubbleHistogramValue(SESSION_CRASHED_BUBBLE_RESTORED);
    421   restored_ = true;
    422 
    423   // Record user's choice for opting in to UMA.
    424   // There's no opting-out choice in the crash restore bubble.
    425   if (uma_option_ && uma_option_->checked()) {
    426     InitiateMetricsReportingChange(true, OnMetricsReportingCallbackType());
    427     RecordBubbleHistogramValue(SESSION_CRASHED_BUBBLE_UMA_OPTIN);
    428   }
    429   CloseBubble();
    430 }
    431 
    432 void SessionCrashedBubbleView::CloseBubble() {
    433   GetWidget()->Close();
    434 }
    435 
    436 bool ShowSessionCrashedBubble(Browser* browser) {
    437   if (IsBubbleUIEnabled()) {
    438     SessionCrashedBubbleView::Show(browser);
    439     return true;
    440   }
    441   return false;
    442 }
    443