blob: 68e69c8cf655b6eca276dc31c2ba3abac65d4b63 [file] [log] [blame]
// Copyright 2019 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/page_info/safety_tip_page_info_bubble_view.h"
#include "base/bind.h"
#include "chrome/browser/platform_util.h"
#include "chrome/browser/reputation/reputation_service.h"
#include "chrome/browser/reputation/safety_tip_ui_helper.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/views/accessibility/non_accessible_image_view.h"
#include "chrome/browser/ui/views/bubble_anchor_util_views.h"
#include "chrome/browser/ui/views/chrome_layout_provider.h"
#include "chrome/browser/ui/views/page_info/page_info_view_factory.h"
#include "chrome/grit/theme_resources.h"
#include "components/security_state/core/security_state.h"
#include "components/strings/grit/components_strings.h"
#include "content/public/browser/navigation_handle.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/color_utils.h"
#include "ui/views/bubble/bubble_frame_view.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/controls/button/md_text_button.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/styled_label.h"
#include "ui/views/layout/grid_layout.h"
#include "ui/views/style/typography.h"
#include "ui/views/widget/widget.h"
#include "url/gurl.h"
namespace {
int GetSafetyTipBannerId(security_state::SafetyTipStatus safety_tip_status,
bool is_dark) {
switch (safety_tip_status) {
case security_state::SafetyTipStatus::kBadReputation:
case security_state::SafetyTipStatus::kLookalike:
return is_dark ? IDR_SAFETY_TIP_ILLUSTRATION_DARK
: IDR_SAFETY_TIP_ILLUSTRATION_LIGHT;
case security_state::SafetyTipStatus::kBadReputationIgnored:
case security_state::SafetyTipStatus::kLookalikeIgnored:
case security_state::SafetyTipStatus::kDigitalAssetLinkMatch:
case security_state::SafetyTipStatus::kBadKeyword:
case security_state::SafetyTipStatus::kUnknown:
case security_state::SafetyTipStatus::kNone:
NOTREACHED();
return 0;
}
}
} // namespace
SafetyTipPageInfoBubbleView::SafetyTipPageInfoBubbleView(
views::View* anchor_view,
const gfx::Rect& anchor_rect,
gfx::NativeView parent_window,
content::WebContents* web_contents,
security_state::SafetyTipStatus safety_tip_status,
const GURL& suggested_url,
base::OnceCallback<void(SafetyTipInteraction)> close_callback)
: PageInfoBubbleViewBase(anchor_view,
anchor_rect,
parent_window,
PageInfoBubbleViewBase::BUBBLE_SAFETY_TIP,
web_contents),
safety_tip_status_(safety_tip_status),
suggested_url_(suggested_url),
close_callback_(std::move(close_callback)) {
// Keep the bubble open until explicitly closed (or we navigate away, a tab is
// created over it, etc).
set_close_on_deactivate(false);
const std::u16string title_text =
GetSafetyTipTitle(safety_tip_status, suggested_url);
SetTitle(title_text);
views::BubbleDialogDelegateView::CreateBubble(this);
// Replace the original title view with our formatted title.
views::Label* original_title =
static_cast<views::Label*>(GetBubbleFrameView()->title());
views::StyledLabel::RangeStyleInfo name_style;
const auto kSizeDeltaInPixels = 3;
name_style.custom_font = original_title->GetDefaultFontList().Derive(
kSizeDeltaInPixels, gfx::Font::FontStyle::NORMAL,
gfx::Font::Weight::BOLD);
views::StyledLabel::RangeStyleInfo base_style;
base_style.custom_font = original_title->GetDefaultFontList().Derive(
kSizeDeltaInPixels, gfx::Font::FontStyle::NORMAL,
gfx::Font::Weight::NORMAL);
auto new_title = std::make_unique<views::StyledLabel>();
new_title->SetText(title_text);
new_title->AddStyleRange(gfx::Range(0, title_text.length()), name_style);
GetBubbleFrameView()->SetTitleView(std::move(new_title));
ChromeLayoutProvider* layout_provider = ChromeLayoutProvider::Get();
gfx::Insets insets = layout_provider->GetDialogInsetsForContentType(
views::DialogContentType::kText, views::DialogContentType::kText);
set_margins(gfx::Insets(0, 0, insets.bottom(), 0));
// Configure layout.
views::GridLayout* bubble_layout =
SetLayoutManager(std::make_unique<views::GridLayout>());
constexpr int kColumnId = 0;
views::ColumnSet* bubble_col_set = bubble_layout->AddColumnSet(kColumnId);
bubble_col_set->AddColumn(views::GridLayout::LEADING, views::GridLayout::FILL,
1.0, views::GridLayout::ColumnSize::kUsePreferred,
0, 0);
ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
const bool use_dark =
color_utils::IsDark(GetBubbleFrameView()->GetBackgroundColor());
const gfx::ImageSkia* image =
rb.GetNativeImageNamed(GetSafetyTipBannerId(safety_tip_status, use_dark))
.ToImageSkia();
auto image_view = std::make_unique<NonAccessibleImageView>();
image_view->SetImage(*image);
views::BubbleFrameView* frame_view = GetBubbleFrameView();
CHECK(frame_view);
frame_view->SetHeaderView(std::move(image_view));
auto bottom_view = std::make_unique<views::View>();
views::GridLayout* bottom_layout =
bottom_view->SetLayoutManager(std::make_unique<views::GridLayout>());
views::ColumnSet* bottom_column_set = bottom_layout->AddColumnSet(0);
bottom_column_set->AddPaddingColumn(views::GridLayout::kFixedSize,
insets.left());
bottom_column_set->AddColumn(
views::GridLayout::LEADING, views::GridLayout::FILL, 1.0,
views::GridLayout::ColumnSize::kUsePreferred, 0, 0);
bottom_column_set->AddPaddingColumn(views::GridLayout::kFixedSize,
insets.right());
// Add text description.
const int spacing =
layout_provider->GetDistanceMetric(DISTANCE_CONTROL_LIST_VERTICAL);
bottom_layout->StartRowWithPadding(views::GridLayout::kFixedSize, kColumnId,
views::GridLayout::kFixedSize, spacing);
auto text_label = std::make_unique<views::Label>(
GetSafetyTipDescription(safety_tip_status, suggested_url_));
text_label->SetMultiLine(true);
text_label->SetLineHeight(20);
text_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
text_label->SizeToFit(layout_provider->GetDistanceMetric(
views::DISTANCE_BUBBLE_PREFERRED_WIDTH) -
insets.left() - insets.right());
bottom_layout->AddView(std::move(text_label));
MaybeAddButtons(safety_tip_status, bottom_layout, spacing, kColumnId, insets);
bubble_layout->StartRow(views::GridLayout::kFixedSize, kColumnId);
bubble_layout->AddView(std::move(bottom_view));
Layout();
SizeToContents();
}
SafetyTipPageInfoBubbleView::~SafetyTipPageInfoBubbleView() {}
void SafetyTipPageInfoBubbleView::OnWidgetDestroying(views::Widget* widget) {
PageInfoBubbleViewBase::OnWidgetDestroying(widget);
switch (widget->closed_reason()) {
case views::Widget::ClosedReason::kUnspecified:
// Do not modify action_taken_. This may correspond to the
// WebContentsObserver functions below, in which case a more explicit
// action_taken_ is set. Otherwise, keep default of kNoAction.
break;
case views::Widget::ClosedReason::kLostFocus:
// We require that the user explicitly interact with the bubble, so do
// nothing in these cases.
break;
case views::Widget::ClosedReason::kAcceptButtonClicked:
// If they've left the site, we can still ignore the result; if they
// stumble there again, we should warn again.
break;
case views::Widget::ClosedReason::kEscKeyPressed:
action_taken_ = SafetyTipInteraction::kDismissWithEsc;
break;
case views::Widget::ClosedReason::kCloseButtonClicked:
action_taken_ = SafetyTipInteraction::kDismissWithClose;
break;
case views::Widget::ClosedReason::kCancelButtonClicked:
// I don't know why, but ESC sometimes generates kCancelButtonClicked.
action_taken_ = SafetyTipInteraction::kDismissWithEsc;
break;
}
std::move(close_callback_).Run(action_taken_);
}
void SafetyTipPageInfoBubbleView::ExecuteLeaveCommand() {
action_taken_ = SafetyTipInteraction::kLeaveSite;
LeaveSiteFromSafetyTip(
web_contents(),
safety_tip_status_ == security_state::SafetyTipStatus::kLookalike
? suggested_url_
: GURL());
}
void SafetyTipPageInfoBubbleView::OpenHelpCenter() {
action_taken_ = SafetyTipInteraction::kLearnMore;
OpenHelpCenterFromSafetyTip(web_contents());
}
void SafetyTipPageInfoBubbleView::RenderFrameDeleted(
content::RenderFrameHost* render_frame_host) {
if (render_frame_host != web_contents()->GetMainFrame()) {
return;
}
if (action_taken_ == SafetyTipInteraction::kNoAction) {
action_taken_ = SafetyTipInteraction::kCloseTab;
}
// There's no great ClosedReason for this, so we use kUnspecified to signal
// that a more specific action_taken_ may have already been set.
GetWidget()->CloseWithReason(views::Widget::ClosedReason::kUnspecified);
}
void SafetyTipPageInfoBubbleView::OnVisibilityChanged(
content::Visibility visibility) {
if (visibility != content::Visibility::HIDDEN) {
return;
}
if (action_taken_ == SafetyTipInteraction::kNoAction) {
action_taken_ = SafetyTipInteraction::kSwitchTab;
}
// There's no great ClosedReason for this, so we use kUnspecified to signal
// that a more specific action_taken_ may have already been set.
GetWidget()->CloseWithReason(views::Widget::ClosedReason::kUnspecified);
}
void SafetyTipPageInfoBubbleView::DidStartNavigation(
content::NavigationHandle* handle) {
if (!handle->IsInPrimaryMainFrame() || handle->IsSameDocument()) {
return;
}
if (action_taken_ == SafetyTipInteraction::kNoAction) {
action_taken_ = SafetyTipInteraction::kStartNewNavigation;
}
// There's no great ClosedReason for this, so we use kUnspecified to signal
// that a more specific action_taken_ may have already been set.
GetWidget()->CloseWithReason(views::Widget::ClosedReason::kUnspecified);
}
void SafetyTipPageInfoBubbleView::DidChangeVisibleSecurityState() {
// Do nothing. (Base class closes the bubble.)
}
void SafetyTipPageInfoBubbleView::MaybeAddButtons(
security_state::SafetyTipStatus safety_tip_status,
views::GridLayout* bottom_layout,
int spacing,
int column_id,
const gfx::Insets& insets) {
// Suspicious site safety tips don't have a call to action, as they are used
// for drawing users' attention to the omnibox to see if they leave the site
// on their own once they notice the omnibox. (https://crbug.com/1146471)
if (safety_tip_status == security_state::SafetyTipStatus::kBadReputation)
return;
ChromeLayoutProvider* layout_provider = ChromeLayoutProvider::Get();
// To make the rest of the layout simpler, they live in their own grid layout.
auto button_view = std::make_unique<views::View>();
views::GridLayout* button_layout =
button_view->SetLayoutManager(std::make_unique<views::GridLayout>());
views::ColumnSet* button_column_set = button_layout->AddColumnSet(0);
button_column_set->AddColumn(
views::GridLayout::LEADING, views::GridLayout::CENTER, 0.0,
views::GridLayout::ColumnSize::kUsePreferred, 0, 0);
button_column_set->AddPaddingColumn(1.f, 1);
button_column_set->AddColumn(
views::GridLayout::TRAILING, views::GridLayout::FILL, 0.0,
views::GridLayout::ColumnSize::kUsePreferred, 0, 0);
button_layout->StartRow(views::GridLayout::kFixedSize, column_id);
// More info button.
auto info_text =
l10n_util::GetStringUTF16(IDS_PAGE_INFO_SAFETY_TIP_MORE_INFO_LINK);
auto info_link = std::make_unique<views::StyledLabel>();
info_link->SetText(info_text);
views::StyledLabel::RangeStyleInfo link_style =
views::StyledLabel::RangeStyleInfo::CreateForLink(
base::BindRepeating(&SafetyTipPageInfoBubbleView::OpenHelpCenter,
base::Unretained(this)));
gfx::Range details_range(0, info_text.length());
info_link->AddStyleRange(details_range, link_style);
info_link->SizeToFit(0);
info_button_ = button_layout->AddView(std::move(info_link));
// Leave site button.
auto leave_button = std::make_unique<views::MdTextButton>(
base::BindRepeating(
[](SafetyTipPageInfoBubbleView* view) {
view->ExecuteLeaveCommand();
},
this),
l10n_util::GetStringUTF16(GetSafetyTipLeaveButtonId(safety_tip_status)));
leave_button->SetProminent(true);
leave_button->SetID(PageInfoViewFactory::VIEW_ID_PAGE_INFO_BUTTON_LEAVE_SITE);
leave_button_ = button_layout->AddView(std::move(leave_button));
bottom_layout->StartRowWithPadding(views::GridLayout::kFixedSize, column_id,
views::GridLayout::kFixedSize, spacing);
bottom_layout->AddView(std::move(button_view), 1, 1,
views::GridLayout::LEADING, views::GridLayout::LEADING,
layout_provider->GetDistanceMetric(
views::DISTANCE_BUBBLE_PREFERRED_WIDTH) -
insets.left() - insets.right(),
0);
}
void ShowSafetyTipDialog(
content::WebContents* web_contents,
security_state::SafetyTipStatus safety_tip_status,
const GURL& suggested_url,
base::OnceCallback<void(SafetyTipInteraction)> close_callback) {
Browser* browser = chrome::FindBrowserWithWebContents(web_contents);
if (!browser)
return;
bubble_anchor_util::AnchorConfiguration configuration =
bubble_anchor_util::GetPageInfoAnchorConfiguration(
browser, bubble_anchor_util::kLocationBar);
gfx::Rect anchor_rect =
configuration.anchor_view
? gfx::Rect()
: bubble_anchor_util::GetPageInfoAnchorRect(browser);
gfx::NativeWindow parent_window = browser->window()->GetNativeWindow();
gfx::NativeView parent_view = platform_util::GetViewForWindow(parent_window);
views::BubbleDialogDelegateView* bubble = new SafetyTipPageInfoBubbleView(
configuration.anchor_view, anchor_rect, parent_view, web_contents,
safety_tip_status, suggested_url, std::move(close_callback));
bubble->SetHighlightedButton(configuration.highlighted_button);
bubble->SetArrow(configuration.bubble_arrow);
bubble->GetWidget()->Show();
}
PageInfoBubbleViewBase* CreateSafetyTipBubbleForTesting(
gfx::NativeView parent_view,
content::WebContents* web_contents,
security_state::SafetyTipStatus safety_tip_status,
const GURL& suggested_url,
base::OnceCallback<void(SafetyTipInteraction)> close_callback) {
return new SafetyTipPageInfoBubbleView(
nullptr, gfx::Rect(), parent_view, web_contents, safety_tip_status,
suggested_url, std::move(close_callback));
}