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
48using views::GridLayout;
49
50namespace {
51
52// Fixed width of the column holding the description label of the bubble.
53const int kWidthOfDescriptionText = 320;
54
55// Distance between checkbox and the text to the right of it.
56const 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.
60const SkColor kBackgroundColor = SkColorSetRGB(245, 245, 245);
61const SkColor kTextColor = SkColorSetRGB(102, 102, 102);
62
63// The Finch study name and group name that enables session crashed bubble UI.
64const char kEnableBubbleUIFinchName[] = "EnableSessionCrashedBubbleUI";
65const char kEnableBubbleUIGroupEnabled[] = "Enabled";
66
67enum 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
79void 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.
85bool 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.
99class 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
126void 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
152void 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
194SessionCrashedBubbleView::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
217SessionCrashedBubbleView::~SessionCrashedBubbleView() {
218  browser_->tab_strip_model()->RemoveObserver(this);
219}
220
221views::View* SessionCrashedBubbleView::GetInitiallyFocusedView() {
222  return restore_button_;
223}
224
225base::string16 SessionCrashedBubbleView::GetWindowTitle() const {
226  return l10n_util::GetStringUTF16(IDS_SESSION_CRASHED_BUBBLE_TITLE);
227}
228
229bool SessionCrashedBubbleView::ShouldShowWindowTitle() const {
230  return true;
231}
232
233bool SessionCrashedBubbleView::ShouldShowCloseButton() const {
234  return true;
235}
236
237void SessionCrashedBubbleView::OnWidgetDestroying(views::Widget* widget) {
238  if (!restored_)
239    RecordBubbleHistogramValue(SESSION_CRASHED_BUBBLE_IGNORED);
240  BubbleDelegateView::OnWidgetDestroying(widget);
241}
242
243void 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
309views::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
366void SessionCrashedBubbleView::ButtonPressed(views::Button* sender,
367                                             const ui::Event& event) {
368  DCHECK_EQ(sender, restore_button_);
369  RestorePreviousSession(sender);
370}
371
372void 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
383void SessionCrashedBubbleView::DidStartNavigationToPendingEntry(
384      const GURL& url,
385      content::NavigationController::ReloadType reload_type) {
386  started_navigation_ = true;
387}
388
389void SessionCrashedBubbleView::DidFinishLoad(
390    content::RenderFrameHost* render_frame_host,
391    const GURL& validated_url) {
392  if (started_navigation_)
393    CloseBubble();
394}
395
396void SessionCrashedBubbleView::WasShown() {
397  GetWidget()->Show();
398}
399
400void SessionCrashedBubbleView::WasHidden() {
401  GetWidget()->Hide();
402}
403
404void 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
412void SessionCrashedBubbleView::TabDetachedAt(content::WebContents* contents,
413                                             int index) {
414  if (web_contents_ == contents)
415    CloseBubble();
416}
417
418void 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
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