blob: 5c0e5809c450867a95fd8432d2e3893a7e74f184 [file] [log] [blame]
// Copyright 2012 The Chromium Authors
// 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/exclusive_access_bubble_views.h"
#include <utility>
#include "base/i18n/case_conversion.h"
#include "base/location.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "build/build_config.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/ui/browser_element_identifiers.h"
#include "chrome/browser/ui/exclusive_access/exclusive_access_context.h"
#include "chrome/browser/ui/exclusive_access/exclusive_access_manager.h"
#include "chrome/browser/ui/exclusive_access/fullscreen_controller.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/browser/ui/url_identity.h"
#include "chrome/browser/ui/views/exclusive_access_bubble_views_context.h"
#include "chrome/browser/ui/views/frame/immersive_mode_controller.h"
#include "chrome/browser/ui/views/frame/top_container_view.h"
#include "chrome/grit/generated_resources.h"
#include "components/fullscreen_control/fullscreen_features.h"
#include "components/fullscreen_control/subtle_notification_view.h"
#include "content/public/browser/web_contents.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/events/keycodes/keyboard_codes.h"
#include "ui/gfx/animation/slide_animation.h"
#include "ui/strings/grit/ui_strings.h"
#include "ui/views/view.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/widget/widget.h"
#include "url/origin.h"
#if BUILDFLAG(IS_WIN)
#include "ui/base/l10n/l10n_util_win.h"
#endif
namespace {
// Returns whether `type` indicates a tab-initiated fullscreen mode.
bool IsTabFullscreenType(ExclusiveAccessBubbleType type) {
return type == EXCLUSIVE_ACCESS_BUBBLE_TYPE_FULLSCREEN_EXIT_INSTRUCTION ||
type ==
EXCLUSIVE_ACCESS_BUBBLE_TYPE_FULLSCREEN_POINTERLOCK_EXIT_INSTRUCTION ||
type == EXCLUSIVE_ACCESS_BUBBLE_TYPE_KEYBOARD_LOCK_EXIT_INSTRUCTION;
}
} // namespace
constexpr UrlIdentity::TypeSet kUrlIdentityAllowedTypes = {
UrlIdentity::Type::kDefault, UrlIdentity::Type::kFile,
UrlIdentity::Type::kIsolatedWebApp, UrlIdentity::Type::kChromeExtension};
constexpr UrlIdentity::FormatOptions kUrlIdentityOptions{
.default_options = {
UrlIdentity::DefaultFormatOptions::kOmitCryptographicScheme}};
ExclusiveAccessBubbleViews::ExclusiveAccessBubbleViews(
ExclusiveAccessBubbleViewsContext* context,
const ExclusiveAccessBubbleParams& params,
ExclusiveAccessBubbleHideCallback first_hide_callback)
: ExclusiveAccessBubble(params),
bubble_view_context_(context),
first_hide_callback_(std::move(first_hide_callback)),
animation_(new gfx::SlideAnimation(this)) {
// Create the contents view.
auto content_view = std::make_unique<SubtleNotificationView>();
view_ = content_view.get();
view_->SetProperty(views::kElementIdentifierKey,
kExclusiveAccessBubbleViewElementId);
#if BUILDFLAG(IS_CHROMEOS)
// Technically the exit fullscreen key on ChromeOS is F11 and the
// "Fullscreen" key on the keyboard is just translated to F11 or F4 (which
// is also a toggle-fullscreen command on ChromeOS). However most Chromebooks
// have media keys - including "fullscreen" - but not function keys, so
// instructing the user to "Press [F11] to exit fullscreen" isn't useful.
//
// An obvious solution might be to change the primary accelerator to the
// fullscreen key, but since translation to a function key is done at system
// level we can't actually do that. Instead we provide specific messaging for
// the platform here. (See crbug.com/1110468 for details.)
browser_fullscreen_exit_accelerator_ =
l10n_util::GetStringUTF16(IDS_APP_FULLSCREEN_KEY);
#else
ui::Accelerator accelerator(ui::VKEY_UNKNOWN, ui::EF_NONE);
bool got_accelerator =
bubble_view_context_->GetAcceleratorProvider()
->GetAcceleratorForCommandId(IDC_FULLSCREEN, &accelerator);
DCHECK(got_accelerator);
browser_fullscreen_exit_accelerator_ = accelerator.GetShortcutText();
#endif
UpdateViewContent(params_.type);
// Initialize the popup.
popup_ = SubtleNotificationView::CreatePopupWidget(
bubble_view_context_->GetBubbleParentView(), std::move(content_view));
gfx::Rect popup_rect = GetPopupRect();
gfx::Size size = popup_rect.size();
// Bounds are in screen coordinates.
popup_->SetBounds(popup_rect);
// Why is this special enough to require the "security surface" level? A
// decision was made a long time ago to not require confirmation when a site
// asks to go fullscreen, and that's not changing. However, a site going
// fullscreen is a big security risk, allowing phishing and other UI fakery.
// This bubble is the only defense that Chromium can provide against this
// attack, so it's important to order it above everything.
//
// On some platforms, pages can put themselves into fullscreen and then
// trigger other elements to cover up this bubble, elements that aren't fully
// under Chromium's control. See https://crbug.com/927150 for an example.
popup_->SetZOrderLevel(ui::ZOrderLevel::kSecuritySurface);
view_->SetBounds(0, 0, size.width(), size.height());
popup_->AddObserver(this);
ShowAndStartTimers();
const bool entering_tab_fullscreen = IsTabFullscreenType(params.type);
// If the tab enters fullscreen without any recent user interaction, re-show
// the bubble on the first user input event, by clearing the snooze time.
content::WebContents* tab = bubble_view_context_->GetExclusiveAccessManager()
->fullscreen_controller()
->exclusive_access_tab();
if (entering_tab_fullscreen && tab && !tab->HasRecentInteraction()) {
snooze_until_ = base::TimeTicks::Min();
}
}
ExclusiveAccessBubbleViews::~ExclusiveAccessBubbleViews() {
RunHideCallbackIfNeeded(ExclusiveAccessBubbleHideReason::kInterrupted);
popup_->RemoveObserver(this);
// This is tricky. We may be in an ATL message handler stack, in which case
// the popup cannot be deleted yet. We also can't set the popup's ownership
// model to NATIVE_WIDGET_OWNS_WIDGET because if the user closed the last tab
// while in fullscreen mode, Windows has already destroyed the popup HWND by
// the time we get here, and thus either the popup will already have been
// deleted (if we set this in our constructor) or the popup will never get
// another OnFinalMessage() call (if not, as currently). So instead, we tell
// the popup to synchronously hide, and then asynchronously close and delete
// itself.
popup_->Close();
base::SingleThreadTaskRunner::GetCurrentDefault()->DeleteSoon(FROM_HERE,
popup_.get());
CHECK(!views::WidgetObserver::IsInObserverList());
}
void ExclusiveAccessBubbleViews::Update(
const ExclusiveAccessBubbleParams& params,
ExclusiveAccessBubbleHideCallback first_hide_callback) {
DCHECK(EXCLUSIVE_ACCESS_BUBBLE_TYPE_NONE != params.type ||
params.has_download);
bool already_shown = IsShowing() || IsVisible();
if (params_.type == params.type && params_.origin == params.origin &&
!params.force_update && already_shown) {
return;
}
// Show the notification about overriding only if:
// 1. There was a notification visible earlier, and
// 2. Exactly one of the previous and current notifications has a download,
// or the previous notification was about an override itself.
// If both the previous and current notifications have a download, but
// neither is an override, then we don't need to show an override.
notify_overridden_ =
already_shown &&
(notify_overridden_ || (params.has_download ^ params_.has_download));
params_.has_download = params.has_download || notify_overridden_;
// Bubble maybe be re-used after timeout.
RunHideCallbackIfNeeded(ExclusiveAccessBubbleHideReason::kInterrupted);
first_hide_callback_ = std::move(first_hide_callback);
const bool entering_tab_fullscreen =
!IsTabFullscreenType(params_.type) && IsTabFullscreenType(params.type);
params_.origin = params.origin;
// When a request to notify about a download is made, the bubble type
// should be preserved from the old value, and not be updated.
if (!params.has_download) {
params_.type = params.type;
}
UpdateViewContent(params_.type);
view_->SizeToPreferredSize();
popup_->SetBounds(GetPopupRect());
ShowAndStartTimers();
// If the tab enters fullscreen without any recent user interaction, re-show
// the bubble on the first user input event, by clearing the snooze time.
content::WebContents* tab = bubble_view_context_->GetExclusiveAccessManager()
->fullscreen_controller()
->exclusive_access_tab();
if (entering_tab_fullscreen && tab && !tab->HasRecentInteraction()) {
snooze_until_ = base::TimeTicks::Min();
}
}
void ExclusiveAccessBubbleViews::RepositionIfVisible() {
if (IsVisible()) {
UpdateBounds();
}
}
void ExclusiveAccessBubbleViews::HideImmediately() {
if (!IsShowing() && !popup_->IsVisible()) {
return;
}
RunHideCallbackIfNeeded(ExclusiveAccessBubbleHideReason::kInterrupted);
animation_->SetSlideDuration(base::Milliseconds(150));
animation_->Hide();
}
bool ExclusiveAccessBubbleViews::IsShowing() const {
return animation_->is_animating() && animation_->IsShowing();
}
views::View* ExclusiveAccessBubbleViews::GetView() {
return view_;
}
void ExclusiveAccessBubbleViews::UpdateBounds() {
gfx::Rect popup_rect(GetPopupRect());
if (!popup_rect.IsEmpty()) {
popup_->SetBounds(popup_rect);
view_->SetY(popup_rect.height() - view_->height());
}
}
namespace {
std::optional<std::u16string> OriginDisplayName(Profile* profile,
const url::Origin& origin) {
if (origin.opaque() ||
!base::FeatureList::IsEnabled(features::kFullscreenBubbleShowOrigin)) {
return std::nullopt;
}
return UrlIdentity::CreateFromUrl(profile, origin.GetURL(),
kUrlIdentityAllowedTypes,
kUrlIdentityOptions)
.name;
}
} // namespace
void ExclusiveAccessBubbleViews::UpdateViewContent(
ExclusiveAccessBubbleType bubble_type) {
DCHECK(params_.has_download ||
EXCLUSIVE_ACCESS_BUBBLE_TYPE_NONE != bubble_type);
std::u16string accelerator;
bool should_show_browser_acc =
(params_.has_download &&
bubble_type == EXCLUSIVE_ACCESS_BUBBLE_TYPE_NONE) ||
exclusive_access_bubble::IsExclusiveAccessModeBrowserFullscreen(
bubble_type);
if (should_show_browser_acc &&
!base::FeatureList::IsEnabled(
features::kPressAndHoldEscToExitBrowserFullscreen)) {
accelerator = browser_fullscreen_exit_accelerator_;
} else {
accelerator = l10n_util::GetStringUTF16(IDS_APP_ESC_KEY);
#if BUILDFLAG(IS_MAC)
// Mac keyboards use lowercase for the non-letter keys, and since the key is
// placed in a box to make it look like a keyboard key it looks weird to not
// follow suit.
accelerator = base::i18n::ToLower(accelerator);
#endif
}
// This string *may* contain the name of the key surrounded in pipe characters
// ('|'), which should be drawn graphically as a key, not displayed literally.
// `accelerator` is the name of the key to exit fullscreen mode.
view_->UpdateContent(exclusive_access_bubble::GetInstructionTextForType(
params_.type, accelerator,
OriginDisplayName(bubble_view_context_->GetExclusiveAccessManager()
->context()
->GetProfile(),
params_.origin),
params_.has_download, notify_overridden_));
}
bool ExclusiveAccessBubbleViews::IsVisible() const {
#if BUILDFLAG(IS_MAC)
// Due to a quirk on the Mac, the popup will not be visible for a short period
// of time after it is shown (it's asynchronous) so if we don't check the
// value of the animation we'll have a stale version of the bounds when we
// show it and it will appear in the wrong place - typically where the window
// was located before going to fullscreen.
return (popup_->IsVisible() || animation_->GetCurrentValue() > 0.0);
#else
return (popup_->IsVisible());
#endif
}
void ExclusiveAccessBubbleViews::AnimationProgressed(
const gfx::Animation* animation) {
float opacity = static_cast<float>(animation_->CurrentValueBetween(0.0, 1.0));
if (opacity == 0) {
popup_->Hide();
} else {
popup_->Show();
popup_->SetOpacity(opacity);
}
}
void ExclusiveAccessBubbleViews::AnimationEnded(
const gfx::Animation* animation) {
if (animation_->IsShowing()) {
GetView()->NotifyAccessibilityEventDeprecated(ax::mojom::Event::kAlert,
true);
}
AnimationProgressed(animation);
}
gfx::Rect ExclusiveAccessBubbleViews::GetPopupRect() const {
gfx::Size size(view_->GetPreferredSize());
gfx::Rect widget_bounds = bubble_view_context_->GetClientAreaBoundsInScreen();
int x = widget_bounds.x() + (widget_bounds.width() - size.width()) / 2;
int top_container_bottom = widget_bounds.y();
#if BUILDFLAG(IS_CHROMEOS)
if (bubble_view_context_->IsImmersiveModeEnabled()) {
// Skip querying the top container height in CrOS non-immersive fullscreen
// because:
// - The top container height is always zero in non-immersive fullscreen.
// - Querying the top container height may return the height before entering
// fullscreen because layout is disabled while entering fullscreen.
// A visual glitch due to the delayed layout is avoided in immersive
// fullscreen because entering fullscreen starts with the top container
// revealed. When revealed, the top container has the same height as before
// entering fullscreen.
top_container_bottom =
bubble_view_context_->GetTopContainerBoundsInScreen().bottom();
}
#endif
// Space between top of screen and popup.
static constexpr int kPopupTopPx = 45;
// |desired_top| is the top of the bubble area including the shadow.
const int desired_top = kPopupTopPx - view_->GetInsets().top();
const int y = top_container_bottom + desired_top;
return gfx::Rect(gfx::Point(x, y), size);
}
void ExclusiveAccessBubbleViews::Hide() {
// This function is guarded by the `ExclusiveAccessBubble::hide_timeout_`
// timer, so the bubble has been displayed for at least
// `ExclusiveAccessBubble::kShowTime`.
DCHECK(!hide_timeout_.IsRunning());
RunHideCallbackIfNeeded(ExclusiveAccessBubbleHideReason::kTimeout);
animation_->SetSlideDuration(base::Milliseconds(700));
animation_->Hide();
}
void ExclusiveAccessBubbleViews::Show() {
if (animation_->IsShowing()) {
return;
}
animation_->SetSlideDuration(base::Milliseconds(350));
animation_->Show();
}
void ExclusiveAccessBubbleViews::OnWidgetDestroyed(views::Widget* widget) {
// Although SubtleNotificationView uses WIDGET_OWNS_NATIVE_WIDGET, a close can
// originate from the OS or some Chrome shutdown codepaths that bypass the
// destructor.
views::Widget* popup_on_stack = popup_;
DCHECK(popup_on_stack->HasObserver(this));
// Get ourselves destroyed. Calling ExitExclusiveAccess() won't work because
// the parent window might be destroyed as well, so asking it to exit
// fullscreen would be a bad idea.
bubble_view_context_->DestroyAnyExclusiveAccessBubble();
// Note: |this| is destroyed on the line above. Check that the destructor was
// invoked. This is safe to do since |popup_| is deleted via a posted task.
DCHECK(!popup_on_stack->HasObserver(this));
}
void ExclusiveAccessBubbleViews::RunHideCallbackIfNeeded(
ExclusiveAccessBubbleHideReason reason) {
if (first_hide_callback_) {
std::move(first_hide_callback_).Run(reason);
}
}