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