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