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