blob: 763ce8afb7417f3711e0adfa40c3e528e2bdde33 [file] [log] [blame]
// 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 <vector>
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/command_line.h"
#include "base/metrics/field_trial.h"
#include "base/metrics/histogram.h"
#include "base/prefs/pref_service.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/chrome_notification_types.h"
#include "chrome/browser/metrics/metrics_reporting_state.h"
#include "chrome/browser/sessions/session_restore.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/browser_list_observer.h"
#include "chrome/browser/ui/startup/session_crashed_bubble.h"
#include "chrome/browser/ui/startup/startup_browser_creator_impl.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/toolbar/toolbar_view.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/pref_names.h"
#include "chrome/common/url_constants.h"
#include "chrome/installer/util/google_update_settings.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/notification_source.h"
#include "content/public/browser/web_contents.h"
#include "grit/chromium_strings.h"
#include "grit/generated_resources.h"
#include "grit/google_chrome_strings.h"
#include "grit/ui_resources.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/views/bubble/bubble_frame_view.h"
#include "ui/views/controls/button/checkbox.h"
#include "ui/views/controls/button/label_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/grid_layout.h"
#include "ui/views/layout/layout_constants.h"
#include "ui/views/widget/widget.h"
using views::GridLayout;
namespace {
// Fixed width of the column holding the description label of the bubble.
const int kWidthOfDescriptionText = 320;
// Distance between checkbox and the text to the right of it.
const int kCheckboxTextDistance = 4;
// The color of the text and background of the sub panel to offer UMA optin.
// These values match the BookmarkSyncPromoView colors.
const SkColor kBackgroundColor = SkColorSetRGB(245, 245, 245);
const SkColor kTextColor = SkColorSetRGB(102, 102, 102);
// The Finch study name and group name that enables session crashed bubble UI.
const char kEnableBubbleUIFinchName[] = "EnableSessionCrashedBubbleUI";
const char kEnableBubbleUIGroupEnabled[] = "Enabled";
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_MAX,
};
void RecordBubbleHistogramValue(SessionCrashedBubbleHistogramValue value) {
UMA_HISTOGRAM_ENUMERATION(
"SessionCrashed.Bubble", value, SESSION_CRASHED_BUBBLE_MAX);
}
// Whether or not the bubble UI should be used.
bool IsBubbleUIEnabled() {
const base::CommandLine& command_line = *CommandLine::ForCurrentProcess();
if (command_line.HasSwitch(switches::kDisableSessionCrashedBubble))
return false;
if (command_line.HasSwitch(switches::kEnableSessionCrashedBubble))
return true;
const std::string group_name = base::FieldTrialList::FindFullName(
kEnableBubbleUIFinchName);
return group_name == kEnableBubbleUIGroupEnabled;
}
} // namespace
// A helper class that listens to browser removal event.
class SessionCrashedBubbleView::BrowserRemovalObserver
: public chrome::BrowserListObserver {
public:
explicit BrowserRemovalObserver(Browser* browser) : browser_(browser) {
DCHECK(browser_);
BrowserList::AddObserver(this);
}
virtual ~BrowserRemovalObserver() {
BrowserList::RemoveObserver(this);
}
// Overridden from chrome::BrowserListObserver.
virtual void OnBrowserRemoved(Browser* browser) OVERRIDE {
if (browser == browser_)
browser_ = NULL;
}
Browser* browser() const { return browser_; }
private:
Browser* browser_;
DISALLOW_COPY_AND_ASSIGN(BrowserRemovalObserver);
};
// static
void SessionCrashedBubbleView::Show(Browser* browser) {
DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
if (browser->profile()->IsOffTheRecord())
return;
// Observes browser removal event and will be deallocated in ShowForReal.
scoped_ptr<BrowserRemovalObserver> browser_observer(
new BrowserRemovalObserver(browser));
// Stats collection only applies to Google Chrome builds.
#if defined(GOOGLE_CHROME_BUILD)
// Schedule a task to run GoogleUpdateSettings::GetCollectStatsConsent() on
// FILE thread, since it does IO. Then, call
// SessionCrashedBubbleView::ShowForReal with the result.
content::BrowserThread::PostTaskAndReplyWithResult(
content::BrowserThread::FILE,
FROM_HERE,
base::Bind(&GoogleUpdateSettings::GetCollectStatsConsent),
base::Bind(&SessionCrashedBubbleView::ShowForReal,
base::Passed(&browser_observer)));
#else
SessionCrashedBubbleView::ShowForReal(browser_observer.Pass(), false);
#endif // defined(GOOGLE_CHROME_BUILD)
}
// static
void SessionCrashedBubbleView::ShowForReal(
scoped_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 defined(GOOGLE_CHROME_BUILD)
if (!uma_opted_in_already) {
offer_uma_optin = g_browser_process->local_state()->FindPreference(
prefs::kMetricsReportingEnabled)->IsUserModifiable();
}
#endif // defined(GOOGLE_CHROME_BUILD)
Browser* browser = browser_observer->browser();
if (!browser) {
RecordBubbleHistogramValue(SESSION_CRASHED_BUBBLE_ERROR);
return;
}
views::View* anchor_view =
BrowserView::GetBrowserViewForBrowser(browser)->toolbar()->app_menu();
content::WebContents* web_contents =
browser->tab_strip_model()->GetActiveWebContents();
if (!web_contents) {
RecordBubbleHistogramValue(SESSION_CRASHED_BUBBLE_ERROR);
return;
}
SessionCrashedBubbleView* crash_bubble =
new SessionCrashedBubbleView(anchor_view, browser, web_contents,
offer_uma_optin);
views::BubbleDelegateView::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,
Browser* browser,
content::WebContents* web_contents,
bool offer_uma_optin)
: BubbleDelegateView(anchor_view, views::BubbleBorder::TOP_RIGHT),
content::WebContentsObserver(web_contents),
browser_(browser),
web_contents_(web_contents),
restore_button_(NULL),
uma_option_(NULL),
offer_uma_optin_(offer_uma_optin),
started_navigation_(false),
restored_(false) {
set_close_on_deactivate(false);
registrar_.Add(
this,
chrome::NOTIFICATION_TAB_CLOSING,
content::Source<content::NavigationController>(&(
web_contents->GetController())));
browser->tab_strip_model()->AddObserver(this);
}
SessionCrashedBubbleView::~SessionCrashedBubbleView() {
browser_->tab_strip_model()->RemoveObserver(this);
}
views::View* SessionCrashedBubbleView::GetInitiallyFocusedView() {
return restore_button_;
}
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 (!restored_)
RecordBubbleHistogramValue(SESSION_CRASHED_BUBBLE_IGNORED);
BubbleDelegateView::OnWidgetDestroying(widget);
}
void SessionCrashedBubbleView::Init() {
// 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(kWidthOfDescriptionText);
// Restore button.
restore_button_ = new views::LabelButton(
this, l10n_util::GetStringUTF16(IDS_SESSION_CRASHED_VIEW_RESTORE_BUTTON));
restore_button_->SetStyle(views::Button::STYLE_BUTTON);
restore_button_->SetIsDefault(true);
GridLayout* layout = new GridLayout(this);
SetLayoutManager(layout);
// Text row.
const int kTextColumnSetId = 0;
views::ColumnSet* cs = layout->AddColumnSet(kTextColumnSetId);
cs->AddPaddingColumn(0, GetBubbleFrameView()->GetTitleInsets().left());
cs->AddColumn(GridLayout::FILL, GridLayout::FILL, 1,
GridLayout::FIXED, kWidthOfDescriptionText, 0);
cs->AddPaddingColumn(0, GetBubbleFrameView()->GetTitleInsets().left());
// Restore button row.
const int kButtonColumnSetId = 1;
cs = layout->AddColumnSet(kButtonColumnSetId);
cs->AddColumn(GridLayout::TRAILING, GridLayout::CENTER, 1,
GridLayout::USE_PREF, 0, 0);
cs->AddPaddingColumn(0, GetBubbleFrameView()->GetTitleInsets().left());
layout->StartRow(0, kTextColumnSetId);
layout->AddView(text_label);
layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
layout->StartRow(0, kButtonColumnSetId);
layout->AddView(restore_button_);
layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
// Metrics reporting option.
if (offer_uma_optin_) {
CreateUmaOptinView(layout);
RecordBubbleHistogramValue(SESSION_CRASHED_BUBBLE_OPTIN_BAR_SHOWN);
}
set_margins(gfx::Insets());
Layout();
}
void SessionCrashedBubbleView::CreateUmaOptinView(GridLayout* layout) {
// Checkbox for metric reporting setting.
// Since the text to the right of the checkbox can't be a simple string (needs
// a hyperlink in it), this checkbox contains an empty string as its label,
// and the real text will be added as a separate view.
uma_option_ = new views::Checkbox(base::string16());
uma_option_->SetChecked(false);
uma_option_->set_background(
views::Background::CreateSolidBackground(kBackgroundColor));
// 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->set_background(
views::Background::CreateSolidBackground(kBackgroundColor));
views::StyledLabel::RangeStyleInfo link_style =
views::StyledLabel::RangeStyleInfo::CreateForLink();
link_style.font_style = gfx::Font::NORMAL;
uma_label->AddStyleRange(gfx::Range(offset, offset + link_text.length()),
link_style);
views::StyledLabel::RangeStyleInfo uma_style;
uma_style.color = kTextColor;
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);
// We use a border instead of padding so that the background color reaches
// the edges of the bubble.
const gfx::Insets title_insets = GetBubbleFrameView()->GetTitleInsets();
uma_option_->SetBorder(views::Border::CreateSolidSidedBorder(
0, title_insets.left(), 0, 0, kBackgroundColor));
uma_label->SetBorder(views::Border::CreateSolidSidedBorder(
views::kRelatedControlVerticalSpacing, kCheckboxTextDistance,
views::kRelatedControlVerticalSpacing, title_insets.left(),
kBackgroundColor));
// Separator.
const int kSeparatorColumnSetId = 2;
views::ColumnSet* cs = layout->AddColumnSet(kSeparatorColumnSetId);
cs->AddColumn(GridLayout::FILL, GridLayout::FILL, 1,
GridLayout::USE_PREF, 0, 0);
// Reporting row.
const int kReportColumnSetId = 3;
cs = layout->AddColumnSet(kReportColumnSetId);
cs->AddColumn(GridLayout::CENTER, GridLayout::FILL, 0,
GridLayout::USE_PREF, 0, 0);
cs->AddColumn(GridLayout::FILL, GridLayout::FILL, 0,
GridLayout::FIXED, kWidthOfDescriptionText, 0);
layout->StartRow(0, kSeparatorColumnSetId);
layout->AddView(new views::Separator(views::Separator::HORIZONTAL));
layout->StartRow(0, kReportColumnSetId);
layout->AddView(uma_option_);
layout->AddView(uma_label);
}
void SessionCrashedBubbleView::ButtonPressed(views::Button* sender,
const ui::Event& event) {
DCHECK_EQ(sender, restore_button_);
RestorePreviousSession(sender);
}
void SessionCrashedBubbleView::StyledLabelLinkClicked(const gfx::Range& range,
int event_flags) {
browser_->OpenURL(content::OpenURLParams(
GURL("https://support.google.com/chrome/answer/96817"),
content::Referrer(),
NEW_FOREGROUND_TAB,
content::PAGE_TRANSITION_LINK,
false));
RecordBubbleHistogramValue(SESSION_CRASHED_BUBBLE_HELP);
}
void SessionCrashedBubbleView::DidStartNavigationToPendingEntry(
const GURL& url,
content::NavigationController::ReloadType reload_type) {
started_navigation_ = true;
}
void SessionCrashedBubbleView::DidFinishLoad(
content::RenderFrameHost* render_frame_host,
const GURL& validated_url) {
if (started_navigation_)
CloseBubble();
}
void SessionCrashedBubbleView::WasShown() {
GetWidget()->Show();
}
void SessionCrashedBubbleView::WasHidden() {
GetWidget()->Hide();
}
void SessionCrashedBubbleView::Observe(
int type,
const content::NotificationSource& source,
const content::NotificationDetails& details) {
if (type == chrome::NOTIFICATION_TAB_CLOSING)
CloseBubble();
}
void SessionCrashedBubbleView::TabDetachedAt(content::WebContents* contents,
int index) {
if (web_contents_ == contents)
CloseBubble();
}
void SessionCrashedBubbleView::RestorePreviousSession(views::Button* sender) {
SessionRestore::RestoreSessionAfterCrash(browser_);
RecordBubbleHistogramValue(SESSION_CRASHED_BUBBLE_RESTORED);
restored_ = true;
// Record user's choice for opting in to UMA.
// There's no opting-out choice in the crash restore bubble.
if (uma_option_ && uma_option_->checked()) {
// TODO: Clean up function ResolveMetricsReportingEnabled so that user pref
// is stored automatically.
ResolveMetricsReportingEnabled(true);
g_browser_process->local_state()->SetBoolean(
prefs::kMetricsReportingEnabled, true);
RecordBubbleHistogramValue(SESSION_CRASHED_BUBBLE_UMA_OPTIN);
}
CloseBubble();
}
void SessionCrashedBubbleView::CloseBubble() {
GetWidget()->Close();
}
bool ShowSessionCrashedBubble(Browser* browser) {
if (IsBubbleUIEnabled()) {
SessionCrashedBubbleView::Show(browser);
return true;
}
return false;
}