session_crashed_bubble_view.cc revision 5f1c94371a64b3196d4be9466099bb892df9b88e
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 "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 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