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