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