session_crashed_bubble_view.cc revision 6d86b77056ed63eb6871182f42a9fd5f07550f90
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/prefs/pref_service.h"
14#include "chrome/browser/browser_process.h"
15#include "chrome/browser/chrome_notification_types.h"
16#include "chrome/browser/sessions/session_restore.h"
17#include "chrome/browser/ui/browser_list.h"
18#include "chrome/browser/ui/browser_list_observer.h"
19#include "chrome/browser/ui/options/options_util.h"
20#include "chrome/browser/ui/startup/session_crashed_bubble.h"
21#include "chrome/browser/ui/startup/startup_browser_creator_impl.h"
22#include "chrome/browser/ui/tabs/tab_strip_model.h"
23#include "chrome/browser/ui/views/frame/browser_view.h"
24#include "chrome/browser/ui/views/toolbar/toolbar_view.h"
25#include "chrome/common/chrome_switches.h"
26#include "chrome/common/pref_names.h"
27#include "chrome/common/url_constants.h"
28#include "chrome/installer/util/google_update_settings.h"
29#include "content/public/browser/browser_context.h"
30#include "content/public/browser/browser_thread.h"
31#include "content/public/browser/notification_source.h"
32#include "content/public/browser/web_contents.h"
33#include "grit/chromium_strings.h"
34#include "grit/generated_resources.h"
35#include "grit/google_chrome_strings.h"
36#include "grit/ui_resources.h"
37#include "ui/base/l10n/l10n_util.h"
38#include "ui/base/resource/resource_bundle.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
68bool ShouldOfferMetricsReporting() {
69// Stats collection only applies to Google Chrome builds.
70#if defined(GOOGLE_CHROME_BUILD)
71  // Only show metrics reporting option if user didn't already consent to it.
72  if (GoogleUpdateSettings::GetCollectStatsConsent())
73    return false;
74  return g_browser_process->local_state()->FindPreference(
75      prefs::kMetricsReportingEnabled)->IsUserModifiable();
76#else
77  return false;
78#endif  // defined(GOOGLE_CHROME_BUILD)
79}
80
81// Whether or not the bubble UI should be used.
82bool IsBubbleUIEnabled() {
83  const base::CommandLine& command_line = *CommandLine::ForCurrentProcess();
84  if (command_line.HasSwitch(switches::kDisableSessionCrashedBubble))
85    return false;
86  if (command_line.HasSwitch(switches::kEnableSessionCrashedBubble))
87    return true;
88  const std::string group_name = base::FieldTrialList::FindFullName(
89      kEnableBubbleUIFinchName);
90  return group_name == kEnableBubbleUIGroupEnabled;
91}
92
93}  // namespace
94
95// A helper class that listens to browser removal event.
96class SessionCrashedBubbleView::BrowserRemovalObserver
97    : public chrome::BrowserListObserver {
98 public:
99  explicit BrowserRemovalObserver(Browser* browser) : browser_(browser) {
100    DCHECK(browser_);
101    BrowserList::AddObserver(this);
102  }
103
104  virtual ~BrowserRemovalObserver() {
105    BrowserList::RemoveObserver(this);
106  }
107
108  // Overridden from chrome::BrowserListObserver.
109  virtual void OnBrowserRemoved(Browser* browser) OVERRIDE {
110    if (browser == browser_)
111      browser_ = NULL;
112  }
113
114  Browser* browser() const { return browser_; }
115
116 private:
117  Browser* browser_;
118
119  DISALLOW_COPY_AND_ASSIGN(BrowserRemovalObserver);
120};
121
122// static
123void SessionCrashedBubbleView::Show(Browser* browser) {
124  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
125  if (browser->profile()->IsOffTheRecord())
126    return;
127
128  // Observes browser removal event and will be deallocated in ShowForReal.
129  scoped_ptr<BrowserRemovalObserver> browser_observer(
130      new BrowserRemovalObserver(browser));
131
132  // Schedule a task to run ShouldOfferMetricsReporting() on FILE thread, since
133  // GoogleUpdateSettings::GetCollectStatsConsent() does IO. Then, call
134  // SessionCrashedBubbleView::ShowForReal with the result.
135  content::BrowserThread::PostTaskAndReplyWithResult(
136      content::BrowserThread::FILE,
137      FROM_HERE,
138      base::Bind(&ShouldOfferMetricsReporting),
139      base::Bind(&SessionCrashedBubbleView::ShowForReal,
140                 base::Passed(&browser_observer)));
141}
142
143// static
144void SessionCrashedBubbleView::ShowForReal(
145    scoped_ptr<BrowserRemovalObserver> browser_observer,
146    bool offer_uma_optin) {
147  Browser* browser = browser_observer->browser();
148
149  if (!browser)
150    return;
151
152  views::View* anchor_view =
153      BrowserView::GetBrowserViewForBrowser(browser)->toolbar()->app_menu();
154  content::WebContents* web_contents =
155      browser->tab_strip_model()->GetActiveWebContents();
156
157  if (!web_contents)
158    return;
159
160  SessionCrashedBubbleView* crash_bubble =
161      new SessionCrashedBubbleView(anchor_view, browser, web_contents,
162                                   offer_uma_optin);
163  views::BubbleDelegateView::CreateBubble(crash_bubble)->Show();
164}
165
166SessionCrashedBubbleView::SessionCrashedBubbleView(
167    views::View* anchor_view,
168    Browser* browser,
169    content::WebContents* web_contents,
170    bool offer_uma_optin)
171    : BubbleDelegateView(anchor_view, views::BubbleBorder::TOP_RIGHT),
172      content::WebContentsObserver(web_contents),
173      browser_(browser),
174      web_contents_(web_contents),
175      restore_button_(NULL),
176      uma_option_(NULL),
177      offer_uma_optin_(offer_uma_optin),
178      started_navigation_(false) {
179  set_close_on_deactivate(false);
180  registrar_.Add(
181      this,
182      chrome::NOTIFICATION_TAB_CLOSING,
183      content::Source<content::NavigationController>(&(
184          web_contents->GetController())));
185  browser->tab_strip_model()->AddObserver(this);
186}
187
188SessionCrashedBubbleView::~SessionCrashedBubbleView() {
189  browser_->tab_strip_model()->RemoveObserver(this);
190}
191
192views::View* SessionCrashedBubbleView::GetInitiallyFocusedView() {
193  return restore_button_;
194}
195
196base::string16 SessionCrashedBubbleView::GetWindowTitle() const {
197  return l10n_util::GetStringUTF16(IDS_SESSION_CRASHED_BUBBLE_TITLE);
198}
199
200bool SessionCrashedBubbleView::ShouldShowWindowTitle() const {
201  return true;
202}
203
204bool SessionCrashedBubbleView::ShouldShowCloseButton() const {
205  return true;
206}
207
208void SessionCrashedBubbleView::Init() {
209  // Description text label.
210  views::Label* text_label = new views::Label(
211      l10n_util::GetStringUTF16(IDS_SESSION_CRASHED_VIEW_MESSAGE));
212  text_label->SetMultiLine(true);
213  text_label->SetLineHeight(20);
214  text_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
215  text_label->SizeToFit(kWidthOfDescriptionText);
216
217  // Restore button.
218  restore_button_ = new views::LabelButton(
219      this, l10n_util::GetStringUTF16(IDS_SESSION_CRASHED_VIEW_RESTORE_BUTTON));
220  restore_button_->SetStyle(views::Button::STYLE_BUTTON);
221  restore_button_->SetIsDefault(true);
222
223  GridLayout* layout = new GridLayout(this);
224  SetLayoutManager(layout);
225
226  // Text row.
227  const int kTextColumnSetId = 0;
228  views::ColumnSet* cs = layout->AddColumnSet(kTextColumnSetId);
229  cs->AddPaddingColumn(0, GetBubbleFrameView()->GetTitleInsets().left());
230  cs->AddColumn(GridLayout::FILL, GridLayout::FILL, 1,
231                GridLayout::FIXED, kWidthOfDescriptionText, 0);
232  cs->AddPaddingColumn(0, GetBubbleFrameView()->GetTitleInsets().left());
233
234  // Restore button row.
235  const int kButtonColumnSetId = 1;
236  cs = layout->AddColumnSet(kButtonColumnSetId);
237  cs->AddColumn(GridLayout::TRAILING, GridLayout::CENTER, 1,
238                GridLayout::USE_PREF, 0, 0);
239  cs->AddPaddingColumn(0, GetBubbleFrameView()->GetTitleInsets().left());
240
241  layout->StartRow(0, kTextColumnSetId);
242  layout->AddView(text_label);
243  layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
244
245  layout->StartRow(0, kButtonColumnSetId);
246  layout->AddView(restore_button_);
247  layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
248
249  // Metrics reporting option.
250  if (offer_uma_optin_)
251    CreateUmaOptinView(layout);
252
253  set_margins(gfx::Insets());
254  Layout();
255}
256
257void SessionCrashedBubbleView::CreateUmaOptinView(GridLayout* layout) {
258  // Checkbox for metric reporting setting.
259  // Since the text to the right of the checkbox can't be a simple string (needs
260  // a hyperlink in it), this checkbox contains an empty string as its label,
261  // and the real text will be added as a separate view.
262  uma_option_ = new views::Checkbox(base::string16());
263  uma_option_->SetChecked(false);
264  uma_option_->set_background(
265      views::Background::CreateSolidBackground(kBackgroundColor));
266
267  // The text to the right of the checkbox.
268  size_t offset;
269  base::string16 link_text =
270      l10n_util::GetStringUTF16(IDS_SESSION_CRASHED_BUBBLE_UMA_LINK_TEXT);
271  base::string16 uma_text = l10n_util::GetStringFUTF16(
272      IDS_SESSION_CRASHED_VIEW_UMA_OPTIN,
273      link_text,
274      &offset);
275  views::StyledLabel* uma_label = new views::StyledLabel(uma_text, this);
276  uma_label->set_background(
277      views::Background::CreateSolidBackground(kBackgroundColor));
278  views::StyledLabel::RangeStyleInfo link_style =
279      views::StyledLabel::RangeStyleInfo::CreateForLink();
280  link_style.font_style = gfx::Font::NORMAL;
281  uma_label->AddStyleRange(gfx::Range(offset, offset + link_text.length()),
282                           link_style);
283  views::StyledLabel::RangeStyleInfo uma_style;
284  uma_style.color = kTextColor;
285  gfx::Range before_link_range(0, offset);
286  if (!before_link_range.is_empty())
287    uma_label->AddStyleRange(before_link_range, uma_style);
288  gfx::Range after_link_range(offset + link_text.length(), uma_text.length());
289  if (!after_link_range.is_empty())
290    uma_label->AddStyleRange(after_link_range, uma_style);
291
292  // We use a border instead of padding so that the background color reaches
293  // the edges of the bubble.
294  const gfx::Insets title_insets = GetBubbleFrameView()->GetTitleInsets();
295  uma_option_->SetBorder(views::Border::CreateSolidSidedBorder(
296      0, title_insets.left(), 0, 0, kBackgroundColor));
297  uma_label->SetBorder(views::Border::CreateSolidSidedBorder(
298      views::kRelatedControlVerticalSpacing, kCheckboxTextDistance,
299      views::kRelatedControlVerticalSpacing, title_insets.left(),
300      kBackgroundColor));
301
302  // Separator.
303  const int kSeparatorColumnSetId = 2;
304  views::ColumnSet* cs = layout->AddColumnSet(kSeparatorColumnSetId);
305  cs->AddColumn(GridLayout::FILL, GridLayout::FILL, 1,
306                GridLayout::USE_PREF, 0, 0);
307
308  // Reporting row.
309  const int kReportColumnSetId = 3;
310  cs = layout->AddColumnSet(kReportColumnSetId);
311  cs->AddColumn(GridLayout::CENTER, GridLayout::FILL, 0,
312                GridLayout::USE_PREF, 0, 0);
313  cs->AddColumn(GridLayout::FILL, GridLayout::FILL, 0,
314                GridLayout::FIXED, kWidthOfDescriptionText, 0);
315
316  layout->StartRow(0, kSeparatorColumnSetId);
317  layout->AddView(new views::Separator(views::Separator::HORIZONTAL));
318  layout->StartRow(0, kReportColumnSetId);
319  layout->AddView(uma_option_);
320  layout->AddView(uma_label);
321}
322
323void SessionCrashedBubbleView::ButtonPressed(views::Button* sender,
324                                             const ui::Event& event) {
325  DCHECK_EQ(sender, restore_button_);
326  RestorePreviousSession(sender);
327}
328
329void SessionCrashedBubbleView::StyledLabelLinkClicked(const gfx::Range& range,
330                                                      int event_flags) {
331  browser_->OpenURL(content::OpenURLParams(
332      GURL("https://support.google.com/chrome/answer/96817"),
333      content::Referrer(),
334      NEW_FOREGROUND_TAB,
335      content::PAGE_TRANSITION_LINK,
336      false));
337}
338
339void SessionCrashedBubbleView::DidStartNavigationToPendingEntry(
340      const GURL& url,
341      content::NavigationController::ReloadType reload_type) {
342  started_navigation_ = true;
343}
344
345void SessionCrashedBubbleView::DidFinishLoad(
346      int64 frame_id,
347      const GURL& validated_url,
348      bool is_main_frame,
349      content::RenderViewHost* render_view_host) {
350  if (started_navigation_)
351    CloseBubble();
352}
353
354void SessionCrashedBubbleView::WasShown() {
355  GetWidget()->Show();
356}
357
358void SessionCrashedBubbleView::WasHidden() {
359  GetWidget()->Hide();
360}
361
362void SessionCrashedBubbleView::Observe(
363    int type,
364    const content::NotificationSource& source,
365    const content::NotificationDetails& details) {
366  if (type == chrome::NOTIFICATION_TAB_CLOSING)
367    CloseBubble();
368}
369
370void SessionCrashedBubbleView::TabDetachedAt(content::WebContents* contents,
371                                             int index) {
372  if (web_contents_ == contents)
373    CloseBubble();
374}
375
376void SessionCrashedBubbleView::RestorePreviousSession(views::Button* sender) {
377  SessionRestore::RestoreSessionAfterCrash(browser_);
378
379  // Record user's choice for opting in to UMA.
380  // There's no opting-out choice in the crash restore bubble.
381  if (uma_option_ && uma_option_->checked()) {
382    // TODO: Clean up function ResolveMetricsReportingEnabled so that user pref
383    // is stored automatically.
384    OptionsUtil::ResolveMetricsReportingEnabled(true);
385    g_browser_process->local_state()->SetBoolean(
386        prefs::kMetricsReportingEnabled, true);
387  }
388  CloseBubble();
389}
390
391void SessionCrashedBubbleView::CloseBubble() {
392  GetWidget()->Close();
393}
394
395bool ShowSessionCrashedBubble(Browser* browser) {
396  if (IsBubbleUIEnabled()) {
397    SessionCrashedBubbleView::Show(browser);
398    return true;
399  }
400  return false;
401}
402