|  | // Copyright 2014 The Chromium Authors. All rights reserved. | 
|  | // Use of this source code is governed by a BSD-style license that can be | 
|  | // found in the LICENSE file. | 
|  |  | 
|  | #include "chrome/browser/ui/views/session_crashed_bubble_view.h" | 
|  |  | 
|  | #include <stddef.h> | 
|  | #include <string> | 
|  | #include <utility> | 
|  | #include <vector> | 
|  |  | 
|  | #include "base/bind.h" | 
|  | #include "base/bind_helpers.h" | 
|  | #include "base/macros.h" | 
|  | #include "base/metrics/histogram_macros.h" | 
|  | #include "base/task_runner_util.h" | 
|  | #include "build/build_config.h" | 
|  | #include "chrome/browser/metrics/metrics_reporting_state.h" | 
|  | #include "chrome/browser/prefs/session_startup_pref.h" | 
|  | #include "chrome/browser/sessions/session_restore.h" | 
|  | #include "chrome/browser/ui/browser_dialogs.h" | 
|  | #include "chrome/browser/ui/browser_list.h" | 
|  | #include "chrome/browser/ui/browser_list_observer.h" | 
|  | #include "chrome/browser/ui/bubble_anchor_util.h" | 
|  | #include "chrome/browser/ui/tabs/tab_strip_model.h" | 
|  | #include "chrome/browser/ui/views/chrome_layout_provider.h" | 
|  | #include "chrome/browser/ui/views/chrome_typography.h" | 
|  | #include "chrome/browser/ui/views/frame/app_menu_button.h" | 
|  | #include "chrome/browser/ui/views/frame/browser_view.h" | 
|  | #include "chrome/browser/ui/views/frame/toolbar_button_provider.h" | 
|  | #include "chrome/browser/ui/views/toolbar/toolbar_view.h" | 
|  | #include "chrome/grit/chromium_strings.h" | 
|  | #include "chrome/grit/generated_resources.h" | 
|  | #include "chrome/installer/util/google_update_settings.h" | 
|  | #include "components/strings/grit/components_chromium_strings.h" | 
|  | #include "components/strings/grit/components_strings.h" | 
|  | #include "content/public/browser/browser_context.h" | 
|  | #include "content/public/browser/browser_thread.h" | 
|  | #include "ui/base/buildflags.h" | 
|  | #include "ui/base/l10n/l10n_util.h" | 
|  | #include "ui/views/controls/button/checkbox.h" | 
|  | #include "ui/views/controls/button/menu_button.h" | 
|  | #include "ui/views/controls/label.h" | 
|  | #include "ui/views/controls/separator.h" | 
|  | #include "ui/views/controls/styled_label.h" | 
|  | #include "ui/views/layout/box_layout.h" | 
|  | #include "ui/views/layout/grid_layout.h" | 
|  | #include "ui/views/widget/widget.h" | 
|  |  | 
|  | namespace { | 
|  |  | 
|  | enum SessionCrashedBubbleHistogramValue { | 
|  | SESSION_CRASHED_BUBBLE_SHOWN, | 
|  | SESSION_CRASHED_BUBBLE_ERROR, | 
|  | SESSION_CRASHED_BUBBLE_RESTORED, | 
|  | SESSION_CRASHED_BUBBLE_ALREADY_UMA_OPTIN, | 
|  | SESSION_CRASHED_BUBBLE_UMA_OPTIN, | 
|  | SESSION_CRASHED_BUBBLE_HELP, | 
|  | SESSION_CRASHED_BUBBLE_IGNORED, | 
|  | SESSION_CRASHED_BUBBLE_OPTIN_BAR_SHOWN, | 
|  | SESSION_CRASHED_BUBBLE_STARTUP_PAGES, | 
|  | SESSION_CRASHED_BUBBLE_MAX, | 
|  | }; | 
|  |  | 
|  | void RecordBubbleHistogramValue(SessionCrashedBubbleHistogramValue value) { | 
|  | UMA_HISTOGRAM_ENUMERATION( | 
|  | "SessionCrashed.Bubble", value, SESSION_CRASHED_BUBBLE_MAX); | 
|  | } | 
|  |  | 
|  | bool DoesSupportConsentCheck() { | 
|  | #if defined(GOOGLE_CHROME_BUILD) | 
|  | return true; | 
|  | #else | 
|  | return false; | 
|  | #endif | 
|  | } | 
|  |  | 
|  | // Returns the app menu view, except when the browser window is Cocoa; Cocoa | 
|  | // browser windows always have a null anchor view and use | 
|  | // GetSessionCrashedBubbleAnchorRect() instead. | 
|  | views::View* GetSessionCrashedBubbleAnchorView(Browser* browser) { | 
|  | return BrowserView::GetBrowserViewForBrowser(browser) | 
|  | ->toolbar_button_provider() | 
|  | ->GetAppMenuButton(); | 
|  | } | 
|  |  | 
|  | gfx::Rect GetSessionCrashedBubbleAnchorRect(Browser* browser) { | 
|  | return gfx::Rect(); | 
|  | } | 
|  |  | 
|  | }  // namespace | 
|  |  | 
|  | // A helper class that listens to browser removal event. | 
|  | class SessionCrashedBubbleView::BrowserRemovalObserver | 
|  | : public BrowserListObserver { | 
|  | public: | 
|  | explicit BrowserRemovalObserver(Browser* browser) : browser_(browser) { | 
|  | DCHECK(browser_); | 
|  | BrowserList::AddObserver(this); | 
|  | } | 
|  |  | 
|  | ~BrowserRemovalObserver() override { BrowserList::RemoveObserver(this); } | 
|  |  | 
|  | // Overridden from BrowserListObserver. | 
|  | void OnBrowserRemoved(Browser* browser) override { | 
|  | if (browser == browser_) | 
|  | browser_ = NULL; | 
|  | } | 
|  |  | 
|  | Browser* browser() const { return browser_; } | 
|  |  | 
|  | private: | 
|  | Browser* browser_; | 
|  |  | 
|  | DISALLOW_COPY_AND_ASSIGN(BrowserRemovalObserver); | 
|  | }; | 
|  |  | 
|  | // static | 
|  | bool SessionCrashedBubble::Show(Browser* browser) { | 
|  | DCHECK_CURRENTLY_ON(content::BrowserThread::UI); | 
|  | if (browser->profile()->IsOffTheRecord()) | 
|  | return true; | 
|  |  | 
|  | // Observes browser removal event and will be deallocated in ShowForReal. | 
|  | std::unique_ptr<SessionCrashedBubbleView::BrowserRemovalObserver> | 
|  | browser_observer( | 
|  | new SessionCrashedBubbleView::BrowserRemovalObserver(browser)); | 
|  |  | 
|  | if (DoesSupportConsentCheck()) { | 
|  | base::PostTaskAndReplyWithResult( | 
|  | GoogleUpdateSettings::CollectStatsConsentTaskRunner(), FROM_HERE, | 
|  | base::Bind(&GoogleUpdateSettings::GetCollectStatsConsent), | 
|  | base::Bind(&SessionCrashedBubbleView::ShowForReal, | 
|  | base::Passed(&browser_observer))); | 
|  | } else { | 
|  | SessionCrashedBubbleView::ShowForReal(std::move(browser_observer), false); | 
|  | } | 
|  | return true; | 
|  | } | 
|  |  | 
|  | // static | 
|  | void SessionCrashedBubbleView::ShowForReal( | 
|  | std::unique_ptr<BrowserRemovalObserver> browser_observer, | 
|  | bool uma_opted_in_already) { | 
|  | // Determine whether or not the UMA opt-in option should be offered. It is | 
|  | // offered only when it is a Google chrome build, user hasn't opted in yet, | 
|  | // and the preference is modifiable by the user. | 
|  | bool offer_uma_optin = false; | 
|  |  | 
|  | if (DoesSupportConsentCheck() && !uma_opted_in_already) | 
|  | offer_uma_optin = !IsMetricsReportingPolicyManaged(); | 
|  |  | 
|  | Browser* browser = browser_observer->browser(); | 
|  |  | 
|  | if (!browser || !browser->tab_strip_model()->GetActiveWebContents()) { | 
|  | RecordBubbleHistogramValue(SESSION_CRASHED_BUBBLE_ERROR); | 
|  | return; | 
|  | } | 
|  |  | 
|  | SessionCrashedBubbleView* crash_bubble = new SessionCrashedBubbleView( | 
|  | GetSessionCrashedBubbleAnchorView(browser), | 
|  | GetSessionCrashedBubbleAnchorRect(browser), browser, offer_uma_optin); | 
|  | views::BubbleDialogDelegateView::CreateBubble(crash_bubble)->Show(); | 
|  |  | 
|  | RecordBubbleHistogramValue(SESSION_CRASHED_BUBBLE_SHOWN); | 
|  | if (uma_opted_in_already) | 
|  | RecordBubbleHistogramValue(SESSION_CRASHED_BUBBLE_ALREADY_UMA_OPTIN); | 
|  | } | 
|  |  | 
|  | SessionCrashedBubbleView::SessionCrashedBubbleView(views::View* anchor_view, | 
|  | const gfx::Rect& anchor_rect, | 
|  | Browser* browser, | 
|  | bool offer_uma_optin) | 
|  | : BubbleDialogDelegateView(anchor_view, views::BubbleBorder::TOP_RIGHT), | 
|  | browser_(browser), | 
|  | uma_option_(NULL), | 
|  | offer_uma_optin_(offer_uma_optin), | 
|  | ignored_(true) { | 
|  | set_close_on_deactivate(false); | 
|  | chrome::RecordDialogCreation(chrome::DialogIdentifier::SESSION_CRASHED); | 
|  |  | 
|  | if (!anchor_view) { | 
|  | SetAnchorRect(anchor_rect); | 
|  | set_parent_window( | 
|  | platform_util::GetViewForWindow(browser->window()->GetNativeWindow())); | 
|  | } | 
|  | } | 
|  |  | 
|  | SessionCrashedBubbleView::~SessionCrashedBubbleView() { | 
|  | } | 
|  |  | 
|  | base::string16 SessionCrashedBubbleView::GetWindowTitle() const { | 
|  | return l10n_util::GetStringUTF16(IDS_SESSION_CRASHED_BUBBLE_TITLE); | 
|  | } | 
|  |  | 
|  | bool SessionCrashedBubbleView::ShouldShowWindowTitle() const { | 
|  | return true; | 
|  | } | 
|  |  | 
|  | bool SessionCrashedBubbleView::ShouldShowCloseButton() const { | 
|  | return true; | 
|  | } | 
|  |  | 
|  | void SessionCrashedBubbleView::OnWidgetDestroying(views::Widget* widget) { | 
|  | if (ignored_) | 
|  | RecordBubbleHistogramValue(SESSION_CRASHED_BUBBLE_IGNORED); | 
|  | BubbleDialogDelegateView::OnWidgetDestroying(widget); | 
|  | } | 
|  |  | 
|  | void SessionCrashedBubbleView::Init() { | 
|  | ChromeLayoutProvider* provider = ChromeLayoutProvider::Get(); | 
|  | SetLayoutManager(std::make_unique<views::BoxLayout>( | 
|  | views::BoxLayout::kVertical, gfx::Insets(), | 
|  | provider->GetDistanceMetric(views::DISTANCE_UNRELATED_CONTROL_VERTICAL))); | 
|  |  | 
|  | // Description text label. | 
|  | views::Label* text_label = new views::Label( | 
|  | l10n_util::GetStringUTF16(IDS_SESSION_CRASHED_VIEW_MESSAGE)); | 
|  | text_label->SetMultiLine(true); | 
|  | text_label->SetLineHeight(20); | 
|  | text_label->SetHorizontalAlignment(gfx::ALIGN_LEFT); | 
|  | text_label->SizeToFit( | 
|  | provider->GetDistanceMetric( | 
|  | ChromeDistanceMetric::DISTANCE_BUBBLE_PREFERRED_WIDTH) - | 
|  | margins().width()); | 
|  | AddChildView(text_label); | 
|  |  | 
|  | if (offer_uma_optin_) | 
|  | AddChildView(CreateUmaOptInView()); | 
|  | } | 
|  |  | 
|  | views::View* SessionCrashedBubbleView::CreateUmaOptInView() { | 
|  | RecordBubbleHistogramValue(SESSION_CRASHED_BUBBLE_OPTIN_BAR_SHOWN); | 
|  |  | 
|  | // The text to the right of the checkbox. | 
|  | size_t offset; | 
|  | base::string16 link_text = | 
|  | l10n_util::GetStringUTF16(IDS_SESSION_CRASHED_BUBBLE_UMA_LINK_TEXT); | 
|  | base::string16 uma_text = l10n_util::GetStringFUTF16( | 
|  | IDS_SESSION_CRASHED_VIEW_UMA_OPTIN, | 
|  | link_text, | 
|  | &offset); | 
|  | views::StyledLabel* uma_label = new views::StyledLabel(uma_text, this); | 
|  | uma_label->AddStyleRange(gfx::Range(offset, offset + link_text.length()), | 
|  | views::StyledLabel::RangeStyleInfo::CreateForLink()); | 
|  | views::StyledLabel::RangeStyleInfo uma_style; | 
|  | uma_style.text_style = STYLE_SECONDARY; | 
|  | gfx::Range before_link_range(0, offset); | 
|  | if (!before_link_range.is_empty()) | 
|  | uma_label->AddStyleRange(before_link_range, uma_style); | 
|  | gfx::Range after_link_range(offset + link_text.length(), uma_text.length()); | 
|  | if (!after_link_range.is_empty()) | 
|  | uma_label->AddStyleRange(after_link_range, uma_style); | 
|  | // Shift the text down by 1px to align with the checkbox. | 
|  | uma_label->SetBorder(views::CreateEmptyBorder(1, 0, 0, 0)); | 
|  |  | 
|  | // Checkbox for metric reporting setting. | 
|  | uma_option_ = new views::Checkbox(base::string16()); | 
|  | uma_option_->SetChecked(false); | 
|  | uma_option_->SetAssociatedLabel(uma_label); | 
|  |  | 
|  | // Create a view to hold the checkbox and the text. | 
|  | views::View* uma_view = new views::View(); | 
|  | views::GridLayout* uma_layout = | 
|  | uma_view->SetLayoutManager(std::make_unique<views::GridLayout>(uma_view)); | 
|  |  | 
|  | const int kReportColumnSetId = 0; | 
|  | views::ColumnSet* cs = uma_layout->AddColumnSet(kReportColumnSetId); | 
|  | cs->AddColumn(views::GridLayout::CENTER, views::GridLayout::LEADING, | 
|  | views::GridLayout::kFixedSize, views::GridLayout::USE_PREF, 0, | 
|  | 0); | 
|  | cs->AddPaddingColumn(views::GridLayout::kFixedSize, | 
|  | ChromeLayoutProvider::Get()->GetDistanceMetric( | 
|  | views::DISTANCE_RELATED_LABEL_HORIZONTAL)); | 
|  | cs->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL, 1.0, | 
|  | views::GridLayout::USE_PREF, 0, 0); | 
|  |  | 
|  | uma_layout->StartRow(views::GridLayout::kFixedSize, kReportColumnSetId); | 
|  | uma_layout->AddView(uma_option_); | 
|  | uma_layout->AddView(uma_label); | 
|  |  | 
|  | return uma_view; | 
|  | } | 
|  |  | 
|  | bool SessionCrashedBubbleView::Accept() { | 
|  | RestorePreviousSession(); | 
|  | return true; | 
|  | } | 
|  |  | 
|  | // The cancel button is used as an option to open the startup pages instead of | 
|  | // restoring the previous session. | 
|  | bool SessionCrashedBubbleView::Cancel() { | 
|  | OpenStartupPages(); | 
|  | return true; | 
|  | } | 
|  |  | 
|  | bool SessionCrashedBubbleView::Close() { | 
|  | // Don't default to Accept() just because that's the only choice. Instead, do | 
|  | // nothing. | 
|  | return true; | 
|  | } | 
|  |  | 
|  | int SessionCrashedBubbleView::GetDialogButtons() const { | 
|  | int buttons = ui::DIALOG_BUTTON_OK; | 
|  | // Offer the option to open the startup pages using the cancel button, but | 
|  | // only when the user has selected the URLS option, and set at least one url. | 
|  | SessionStartupPref session_startup_pref = | 
|  | SessionStartupPref::GetStartupPref(browser_->profile()); | 
|  | if (session_startup_pref.type == SessionStartupPref::URLS && | 
|  | !session_startup_pref.urls.empty()) { | 
|  | buttons |= ui::DIALOG_BUTTON_CANCEL; | 
|  | } | 
|  | return buttons; | 
|  | } | 
|  |  | 
|  | base::string16 SessionCrashedBubbleView::GetDialogButtonLabel( | 
|  | ui::DialogButton button) const { | 
|  | if (button == ui::DIALOG_BUTTON_OK) | 
|  | return l10n_util::GetStringUTF16(IDS_SESSION_CRASHED_VIEW_RESTORE_BUTTON); | 
|  | DCHECK_EQ(ui::DIALOG_BUTTON_CANCEL, button); | 
|  | return l10n_util::GetStringUTF16( | 
|  | IDS_SESSION_CRASHED_VIEW_STARTUP_PAGES_BUTTON); | 
|  | } | 
|  |  | 
|  | void SessionCrashedBubbleView::StyledLabelLinkClicked(views::StyledLabel* label, | 
|  | const gfx::Range& range, | 
|  | int event_flags) { | 
|  | browser_->OpenURL(content::OpenURLParams( | 
|  | GURL("https://support.google.com/chrome/answer/96817"), | 
|  | content::Referrer(), WindowOpenDisposition::NEW_FOREGROUND_TAB, | 
|  | ui::PAGE_TRANSITION_LINK, false)); | 
|  | RecordBubbleHistogramValue(SESSION_CRASHED_BUBBLE_HELP); | 
|  | } | 
|  |  | 
|  | void SessionCrashedBubbleView::RestorePreviousSession() { | 
|  | ignored_ = false; | 
|  | MaybeEnableUma(); | 
|  | CloseBubble(); | 
|  |  | 
|  | RecordBubbleHistogramValue(SESSION_CRASHED_BUBBLE_RESTORED); | 
|  | // Restoring tabs has side effects, so it's preferable to do it after the | 
|  | // bubble was closed. | 
|  | SessionRestore::RestoreSessionAfterCrash(browser_); | 
|  | } | 
|  |  | 
|  | void SessionCrashedBubbleView::OpenStartupPages() { | 
|  | ignored_ = false; | 
|  | MaybeEnableUma(); | 
|  | CloseBubble(); | 
|  |  | 
|  | RecordBubbleHistogramValue(SESSION_CRASHED_BUBBLE_STARTUP_PAGES); | 
|  | // Opening tabs has side effects, so it's preferable to do it after the bubble | 
|  | // was closed. | 
|  | SessionRestore::OpenStartupPagesAfterCrash(browser_); | 
|  | } | 
|  |  | 
|  | void SessionCrashedBubbleView::MaybeEnableUma() { | 
|  | // Record user's choice for opt-in in to UMA. | 
|  | // There's no opt-out choice in the crash restore bubble. | 
|  | if (uma_option_ && uma_option_->checked()) { | 
|  | ChangeMetricsReportingState(true); | 
|  | RecordBubbleHistogramValue(SESSION_CRASHED_BUBBLE_UMA_OPTIN); | 
|  | } | 
|  | } | 
|  |  | 
|  | void SessionCrashedBubbleView::CloseBubble() { | 
|  | GetWidget()->Close(); | 
|  | } |