blob: 9ac19f95d17f608adbee9c2b3d7ba45fa121ffde [file]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/exo/ui_lock_controller.h"
#include <memory>
#include "ash/bluetooth_devices_observer.h"
#include "ash/constants/ash_features.h"
#include "ash/public/cpp/keyboard/keyboard_controller.h"
#include "ash/public/cpp/session/session_controller.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/wm/window_state.h"
#include "ash/wm/window_state_observer.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/scoped_observation.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "chromeos/ui/base/window_properties.h"
#include "components/exo/pointer.h"
#include "components/exo/seat.h"
#include "components/exo/shell_surface_util.h"
#include "components/exo/surface.h"
#include "components/exo/wm_helper.h"
#include "components/fullscreen_control/subtle_notification_view.h"
#include "components/strings/grit/components_strings.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/window.h"
#include "ui/aura/window_observer.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/user_activity/user_activity_detector.h"
#include "ui/base/user_activity/user_activity_observer.h"
#include "ui/events/devices/device_data_manager.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/strings/grit/ui_strings.h"
#include "ui/views/widget/widget.h"
namespace {
// Fullscreen and pointer-lock notifications hide after a timeout and are shown
// again when the relevant state is entered again.
// Duration to show notifications.
constexpr auto kNotificationDuration = base::Seconds(4);
// Position of Esc notification from top of screen.
const int kEscNotificationTopPx = 45;
// Once the pointer capture notification has finished showing without
// being interrupted, don't show it again until this long has passed.
constexpr auto kPointerCaptureNotificationCooldown = base::Minutes(5);
constexpr auto kReshowNotificationsWhenIdleFor = base::Minutes(5);
constexpr int kUILockControllerSeatObserverPriority = 1;
static_assert(
exo::Seat::IsValidObserverPriority(kUILockControllerSeatObserverPriority),
"kUILockCOntrollerSeatObserverPriority is not in the valid range");
bool IsUILockControllerEnabled(aura::Window* window) {
if (!window)
return false;
if (window->GetProperty(chromeos::kUseOverviewToExitFullscreen) ||
window->GetProperty(chromeos::kUseOverviewToExitPointerLock)) {
return true;
}
return false;
}
// Return true if an external keyboard is attached to the device.
//
// Note: May mistakenly return true when certain non-keyboard devices are
// attached; see crbug/882410, crbug/884096.
//
// Copied from ash/keyboard/virtual_keyboard_controller.cc.
// TODO(cpelling): Refactor to avoid duplicating this logic.
bool HasExternalKeyboard() {
ui::DeviceDataManager* device_data_manager =
ui::DeviceDataManager::GetInstance();
ash::BluetoothDevicesObserver bluetooth(base::DoNothing());
for (const ui::InputDevice& device :
device_data_manager->GetKeyboardDevices()) {
ui::InputDeviceType type = device.type;
if (type == ui::InputDeviceType::INPUT_DEVICE_USB ||
(type == ui::InputDeviceType::INPUT_DEVICE_BLUETOOTH &&
bluetooth.IsConnectedBluetoothDevice(device))) {
return true;
}
}
return false;
}
// Creates the separator view between bubble views of modifiers and key.
std::unique_ptr<views::View> CreateIconView(const gfx::VectorIcon& icon) {
constexpr int kIconSize = 28;
std::unique_ptr<views::ImageView> view = std::make_unique<views::ImageView>();
gfx::ImageSkia image = gfx::CreateVectorIcon(icon, SK_ColorWHITE);
view->SetImage(ui::ImageModel::FromImageSkia(image));
view->SetImageSize(gfx::Size(kIconSize, kIconSize));
return view;
}
// Create and position Esc notification.
views::Widget* CreateEscNotification(
aura::Window* parent,
int message_id,
std::initializer_list<int> key_message_ids) {
auto content_view = std::make_unique<SubtleNotificationView>();
std::vector<std::u16string> key_names;
std::vector<std::unique_ptr<views::View>> icons;
for (int key_message_id : key_message_ids) {
key_names.push_back(l10n_util::GetStringUTF16(key_message_id));
if (key_message_id == IDS_APP_OVERVIEW_KEY) {
icons.push_back(CreateIconView(ash::kKsvOverviewIcon));
} else {
icons.push_back(nullptr);
}
}
content_view->UpdateContent(
l10n_util::GetStringFUTF16(message_id, key_names, nullptr),
std::move(icons));
gfx::Size size = content_view->GetPreferredSize({});
views::Widget* popup = SubtleNotificationView::CreatePopupWidget(
parent, std::move(content_view));
popup->SetZOrderLevel(ui::ZOrderLevel::kSecuritySurface);
gfx::Rect bounds = parent->GetBoundsInScreen();
int y = bounds.y() + kEscNotificationTopPx;
bounds.ClampToCenteredSize(size);
bounds.set_y(y);
popup->SetBounds(bounds);
return popup;
}
// Owns the widgets for messages prompting to exit fullscreen or pointer lock.
// Owned as a window property.
class ExitNotifier : public exo::UILockController::Notifier,
public aura::WindowObserver,
public ash::WindowStateObserver {
public:
explicit ExitNotifier(exo::UILockController* controller, aura::Window* window)
: window_(window) {
controller_observation_.Observe(controller);
window_observation_.Observe(window);
ash::WindowState* window_state = ash::WindowState::Get(window);
window_state_observation_.Observe(window_state);
if (window_state->IsFullscreen())
OnFullscreen();
}
ExitNotifier(const ExitNotifier&) = delete;
ExitNotifier& operator=(const ExitNotifier&) = delete;
~ExitNotifier() override {
want_pointer_capture_notification_ = false;
OnExitFullscreen();
ClosePointerCaptureNotification();
}
void OnPointerCaptureEnabled() {
pointer_is_captured_ = true;
MaybeShowPointerCaptureNotification();
}
void OnPointerCaptureDisabled() { pointer_is_captured_ = false; }
// If this window is currently in a state that would have triggered a
// notification when entered, re-show that notification as a reminder.
void NotifyAgain() override {
// Always reset the notification cooldown, to ensure notifications show in
// the case where pointer lock is not currently active but will be soon.
next_pointer_notify_time_ = base::TimeTicks::Now();
ash::WindowState* window_state = ash::WindowState::Get(window_);
if (window_state->IsFullscreen()) {
OnFullscreen();
} else if (pointer_is_captured_) {
MaybeShowPointerCaptureNotification();
}
}
// Notify again but this only notifies again the fullscreen notifier.
void NotifyAgainForFullscreen() {
ash::WindowState* window_state = ash::WindowState::Get(window_);
if (window_state->IsFullscreen()) {
OnFullscreen();
}
}
void OnWindowDestroying(aura::Window* window) override {
window_observation_.Reset();
window_state_observation_.Reset();
}
void OnUILockControllerDestroying() override {
controller_observation_.Reset();
}
views::Widget* fullscreen_esc_notification() {
return fullscreen_esc_notification_;
}
views::Widget* pointer_capture_notification() {
return pointer_capture_notification_;
}
private:
void MaybeShowPointerCaptureNotification() {
// Respect cooldown.
if (base::TimeTicks::Now() < next_pointer_notify_time_)
return;
want_pointer_capture_notification_ = true;
// Don't show in fullscreen; the fullscreen notification will show and is
// prioritized.
ash::WindowState* window_state = ash::WindowState::Get(window_);
if (window_state->IsFullscreen())
return;
if (pointer_capture_notification_) {
pointer_capture_notification_->CloseWithReason(
views::Widget::ClosedReason::kUnspecified);
}
if (HasExternalKeyboard()) {
if (ash::KeyboardController::Get()->AreTopRowKeysFunctionKeys()) {
pointer_capture_notification_ =
CreateEscNotification(window_, IDS_PRESS_TO_EXIT_MOUSELOCK_TWO_KEYS,
{IDS_APP_META_KEY, IDS_APP_F5_KEY});
} else {
pointer_capture_notification_ = CreateEscNotification(
window_, IDS_PRESS_TO_EXIT_MOUSELOCK, {IDS_APP_F5_KEY});
}
} else {
if (ash::KeyboardController::Get()->AreTopRowKeysFunctionKeys()) {
pointer_capture_notification_ =
CreateEscNotification(window_, IDS_PRESS_TO_EXIT_MOUSELOCK_TWO_KEYS,
{IDS_APP_SEARCH_KEY, IDS_APP_OVERVIEW_KEY});
} else {
pointer_capture_notification_ = CreateEscNotification(
window_, IDS_PRESS_TO_EXIT_MOUSELOCK, {IDS_APP_OVERVIEW_KEY});
}
}
pointer_capture_notification_->Show();
// Close Esc notification after 4s.
pointer_capture_notify_timer_.Start(
FROM_HERE, kNotificationDuration,
base::BindOnce(&ExitNotifier::OnPointerCaptureNotifyTimerFinished,
base::Unretained(this)));
}
void ClosePointerCaptureNotification() {
pointer_capture_notify_timer_.Stop();
if (pointer_capture_notification_) {
pointer_capture_notification_->CloseWithReason(
views::Widget::ClosedReason::kUnspecified);
pointer_capture_notification_ = nullptr;
}
}
void OnPointerCaptureNotifyTimerFinished() {
// Start the cooldown when the timer successfully elapses, to ensure the
// notification was shown for a sufficiently long time.
next_pointer_notify_time_ =
base::TimeTicks::Now() + kPointerCaptureNotificationCooldown;
ClosePointerCaptureNotification();
want_pointer_capture_notification_ = false;
}
// Overridden from ash::WindowStateObserver:
void OnPostWindowStateTypeChange(
ash::WindowState* window_state,
chromeos::WindowStateType old_type) override {
DCHECK_EQ(window_, window_state->window());
if (window_state->IsFullscreen()) {
OnFullscreen();
} else {
OnExitFullscreen();
}
}
void OnFullscreen() {
if (!window_->GetProperty(chromeos::kUseOverviewToExitFullscreen)) {
return;
}
// Only show Esc notification when window is active.
if (window_ != exo::WMHelper::GetInstance()->GetActiveWindow())
return;
// Fullscreen notifications override pointer capture notifications.
ClosePointerCaptureNotification();
if (fullscreen_esc_notification_) {
fullscreen_esc_notification_->CloseWithReason(
views::Widget::ClosedReason::kUnspecified);
}
if (HasExternalKeyboard()) {
if (ash::KeyboardController::Get()->AreTopRowKeysFunctionKeys()) {
fullscreen_esc_notification_ = CreateEscNotification(
window_, IDS_FULLSCREEN_PRESS_TO_EXIT_FULLSCREEN_TWO_KEYS,
{IDS_APP_META_KEY, IDS_APP_F5_KEY});
} else {
fullscreen_esc_notification_ = CreateEscNotification(
window_, IDS_FULLSCREEN_PRESS_TO_EXIT_FULLSCREEN, {IDS_APP_F5_KEY});
}
} else {
if (ash::KeyboardController::Get()->AreTopRowKeysFunctionKeys()) {
fullscreen_esc_notification_ = CreateEscNotification(
window_, IDS_FULLSCREEN_PRESS_TO_EXIT_FULLSCREEN_TWO_KEYS,
{IDS_APP_SEARCH_KEY, IDS_APP_OVERVIEW_KEY});
} else {
fullscreen_esc_notification_ = CreateEscNotification(
window_, IDS_FULLSCREEN_PRESS_TO_EXIT_FULLSCREEN,
{IDS_APP_OVERVIEW_KEY});
}
}
fullscreen_esc_notification_->Show();
// Close Esc notification after 4s.
fullscreen_notify_timer_.Start(
FROM_HERE, kNotificationDuration,
base::BindOnce(&ExitNotifier::CloseFullscreenEscNotification,
base::Unretained(this)));
}
void OnExitFullscreen() {
CloseFullscreenEscNotification();
}
void CloseFullscreenEscNotification() {
if (!fullscreen_esc_notification_)
return;
fullscreen_esc_notification_->CloseWithReason(
views::Widget::ClosedReason::kUnspecified);
fullscreen_esc_notification_ = nullptr;
// If a pointer capture notification was previously requested and didn't
// show (or didn't complete its timer), show it now.
//
// This is to prevent the following scenario:
// 1. App goes fullscreen
// 2. App immediately requests pointer capture; no notification is shown,
// since the fullscreen notification is already visible.
// 3. App immediately unfullscreens; the fullscreen notification closes.
//
// Without this check, the app would have gained pointer capture without
// any notification showing.
if (want_pointer_capture_notification_)
MaybeShowPointerCaptureNotification();
}
const raw_ptr<aura::Window> window_;
raw_ptr<views::Widget> fullscreen_esc_notification_ = nullptr;
raw_ptr<views::Widget> pointer_capture_notification_ = nullptr;
bool want_pointer_capture_notification_ = false;
bool pointer_is_captured_ = false;
base::OneShotTimer fullscreen_notify_timer_;
base::OneShotTimer pointer_capture_notify_timer_;
base::TimeTicks next_pointer_notify_time_;
base::ScopedObservation<aura::Window, aura::WindowObserver>
window_observation_{this};
base::ScopedObservation<ash::WindowState, ash::WindowStateObserver>
window_state_observation_{this};
base::ScopedObservation<exo::UILockController,
exo::UILockController::Notifier>
controller_observation_{this};
};
} // namespace
DEFINE_UI_CLASS_PROPERTY_TYPE(ExitNotifier*)
namespace exo {
namespace {
DEFINE_OWNED_UI_CLASS_PROPERTY_KEY(ExitNotifier, kExitNotifierKey)
ExitNotifier* GetExitNotifier(UILockController* controller,
aura::Window* window,
bool create) {
if (!window)
return nullptr;
aura::Window* toplevel = window->GetToplevelWindow();
if (!IsUILockControllerEnabled(toplevel))
return nullptr;
ExitNotifier* notifier = toplevel->GetProperty(kExitNotifierKey);
if (!notifier && create) {
// Object is owned as a window property.
notifier = toplevel->SetProperty(
kExitNotifierKey, std::make_unique<ExitNotifier>(controller, toplevel));
}
return notifier;
}
} // namespace
UILockController::UILockController(Seat* seat) : seat_(seat) {
last_activity_time_ = base::TimeTicks::Now();
seat_->AddObserver(this, kUILockControllerSeatObserverPriority);
WMHelper::GetInstance()->AddPowerObserver(this);
auto* session_controller = ash::SessionController::Get();
if (session_controller)
session_controller->AddObserver(this);
ui::UserActivityDetector::Get()->AddObserver(this);
}
UILockController::~UILockController() {
ui::UserActivityDetector::Get()->RemoveObserver(this);
auto* session_controller = ash::SessionController::Get();
if (session_controller)
session_controller->RemoveObserver(this);
WMHelper::GetInstance()->RemovePowerObserver(this);
seat_->RemoveObserver(this);
for (Notifier& notifier : notifiers_)
notifier.OnUILockControllerDestroying();
}
void UILockController::SuspendDone() {
ReshowAllNotifications();
}
void UILockController::ScreenBrightnessChanged(double percent) {
// Show alert when the device returns from low (epsilon) brightness which
// covers three cases.
// 1. The device returns from sleep.
// 2. The device lid is opened (with sleep on).
// 3. The device returns from low display brightness.
double epsilon = std::numeric_limits<double>::epsilon();
if (percent <= epsilon) {
device_in_dark_ = true;
} else {
if (device_in_dark_)
ReshowAllNotifications();
device_in_dark_ = false;
}
}
void UILockController::LidEventReceived(bool opened) {
// Show alert when the lid is opened. This also covers the case when the user
// turns off "Sleep when cover is closed".
if (opened)
ReshowAllNotifications();
}
void UILockController::OnLockStateChanged(bool locked) {
if (!locked)
ReshowAllNotifications();
}
void UILockController::OnSurfaceFocused(Surface* gained_focus,
Surface* lost_focus,
bool has_focused_surface) {
if (gained_focus) {
ExitNotifier* exit_notifier =
GetExitNotifier(this, gained_focus->window(), true);
if (exit_notifier) {
exit_notifier->NotifyAgainForFullscreen();
}
}
}
void UILockController::OnPointerCaptureEnabled(Pointer* pointer,
aura::Window* window) {
aura::Window* toplevel = window ? window->GetToplevelWindow() : nullptr;
if (!toplevel ||
!toplevel->GetProperty(chromeos::kUseOverviewToExitPointerLock))
return;
captured_pointers_.insert(pointer);
ExitNotifier* notifier = GetExitNotifier(this, window, false);
if (notifier)
notifier->OnPointerCaptureEnabled();
}
void UILockController::OnPointerCaptureDisabled(Pointer* pointer,
aura::Window* window) {
if (captured_pointers_.empty())
return;
captured_pointers_.erase(pointer);
if (captured_pointers_.empty()) {
ExitNotifier* notifier = GetExitNotifier(this, window, false);
if (notifier)
notifier->OnPointerCaptureDisabled();
}
}
void UILockController::OnUserActivity(const ui::Event* event) {
base::TimeTicks now = base::TimeTicks::Now();
if (now - last_activity_time_ >= kReshowNotificationsWhenIdleFor) {
ReshowAllNotifications();
}
last_activity_time_ = now;
}
views::Widget* UILockController::GetPointerCaptureNotificationForTesting(
aura::Window* window) {
return window->GetProperty(kExitNotifierKey)->pointer_capture_notification();
}
views::Widget* UILockController::GetEscNotificationForTesting(
aura::Window* window) {
return window->GetProperty(kExitNotifierKey)->fullscreen_esc_notification();
}
void UILockController::AddObserver(UILockController::Notifier* notifier) {
notifiers_.AddObserver(notifier);
}
void UILockController::RemoveObserver(UILockController::Notifier* notifier) {
notifiers_.RemoveObserver(notifier);
}
void UILockController::ReshowAllNotifications() {
VLOG(1) << "ReshowAllNotifications";
for (Notifier& notifier : notifiers_)
notifier.NotifyAgain();
}
} // namespace exo