// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/system/toast/toast_manager_impl.h"
#include "ash/public/cpp/system/scoped_toast_pause.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/metrics/histogram_functions.h"
#include "base/ranges/algorithm.h"
#include "base/strings/stringprintf.h"
#include "base/time/time.h"
namespace ash {
namespace {
constexpr char NotifierFrameworkToastHistogram[] =
"Ash.NotifierFramework.Toast";
// Used in histogram names.
std::string GetToastDismissedTimeRange(const base::TimeDelta& time) {
if (time <= base::Seconds(2))
return "Within2s";
// Toast default duration is 6s, but with animation it's usually
// around ~6.2s, so recording 7s as the default case.
if (time <= base::Seconds(7))
return "Within7s";
return "After7s";
}
} // namespace
///////////////////////////////////////////////////////////////////////////////
// PausableTimer:
// Timer class that owns a `base::OneShotTimer` that can be paused and resumed
// by the `ToastManagerImpl` to continue with the remainder of its duration.
// Different from `base::RetainingOneShotTimer` in that restarting it will set
// the duration for the remainder of the time that it had left when it was
// paused.
class ToastManagerImpl::PausableTimer {
public:
PausableTimer() = default;
PausableTimer(const PausableTimer&) = delete;
PausableTimer& operator=(const PausableTimer&) = delete;
~PausableTimer() = default;
// Returns whether `timer_` is running.
bool IsRunning() const { return timer_.IsRunning(); }
// Starts `timer_` with a duration of `duration` and a scheduled task of
// `task`.
void Start(base::TimeDelta duration, base::RepeatingClosure task) {
DCHECK(!duration.is_max());
DCHECK(task);
DCHECK(!IsRunning());
duration_remaining_ = duration;
task_ = task;
timer_.Start(FROM_HERE, duration_remaining_, task_);
time_last_started_ = base::TimeTicks::Now();
}
// Stops the timer, allowing for the user to call `Resume` at a later time to
// continue the timer.
void Pause() {
DCHECK(IsRunning());
timer_.Stop();
duration_remaining_ -= base::TimeTicks::Now() - time_last_started_;
}
// Restarts the timer with a duration of `duration_remaining_`.
void Resume() { Start(duration_remaining_, task_); }
// Fully stops the timer without leaving a chance to call `Resume` later.
void Stop() {
task_.Reset();
duration_remaining_ = base::Seconds(0);
timer_.Stop();
}
private:
// Task that will be run when `timer_` has elapsed.
base::RepeatingClosure task_;
// Time remaining for the timer. Allows for us to calculate how much time is
// remaining when the timer is paused.
base::TimeDelta duration_remaining_;
// Tracks when `timer_` was last started so that we can calculate
// `duration_remaining_` if the timer is later paused.
base::TimeTicks time_last_started_;
// A timer that will run `task_` when `duration_remaining_` has elapsed if
// it is not paused before then.
base::OneShotTimer timer_;
};
///////////////////////////////////////////////////////////////////////////////
// ToastManagerImpl:
ToastManagerImpl::ToastManagerImpl()
: current_toast_expiration_timer_(std::make_unique<PausableTimer>()),
locked_(Shell::Get()->session_controller()->IsScreenLocked()) {
Shell::Get()->AddShellObserver(this);
}
ToastManagerImpl::~ToastManagerImpl() {
Shell::Get()->RemoveShellObserver(this);
// If there are live `ToastOverlay`s, destroying `current_toast_data_` can
// call into the `ToastOverlay`s and then back into `ToastManagerImpl`, which
// then tries to destroy the already-being-destroyed `current_toast_data_`.
CloseAllToastsWithoutAnimation();
}
void ToastManagerImpl::Show(ToastData data) {
std::string_view id = data.id;
DCHECK(!id.empty());
LOG(ERROR) << "Show toast called, toast id: " << id;
// If `pause_counter_` is greater than 0, no toasts should be shown.
if (pause_counter_ > 0) {
LOG(ERROR)
<< "Toast not shown, pause_counter_ is creater than 0, toast id: "
<< id;
return;
}
auto existing_toast = base::ranges::find(queue_, id, &ToastData::id);
if (existing_toast != queue_.end()) {
// Assigns given `data` to existing queued toast, but keeps the existing
// toast's `time_created` value.
const base::TimeTicks old_time_created = existing_toast->time_created;
*existing_toast = std::move(data);
existing_toast->time_created = old_time_created;
} else {
if (IsToastShown(id)) {
// Replace the visible toast by adding the new toast data to the front of
// the queue and hiding the visible toast. Once the visible toast finishes
// hiding, the new toast will be displayed.
queue_.emplace_front(std::move(data));
CloseAllToastsWithAnimation();
return;
}
queue_.emplace_back(std::move(data));
}
if (queue_.size() == 1 && !HasActiveToasts())
ShowLatest();
}
void ToastManagerImpl::Cancel(std::string_view id) {
if (IsToastShown(id)) {
CloseAllToastsWithAnimation();
return;
}
auto cancelled_toast = base::ranges::find(queue_, id, &ToastData::id);
if (cancelled_toast != queue_.end())
queue_.erase(cancelled_toast);
}
bool ToastManagerImpl::RequestFocusOnActiveToastDismissButton(
std::string_view id) {
CHECK(IsToastShown(id));
for (auto& [_, overlay] : root_window_to_overlay_) {
if (overlay && overlay->RequestFocusOnActiveToastDismissButton()) {
return true;
}
}
return false;
}
bool ToastManagerImpl::IsToastShown(std::string_view id) const {
return HasActiveToasts() && current_toast_data_ &&
current_toast_data_->id == id;
}
bool ToastManagerImpl::IsToastDismissButtonFocused(std::string_view id) const {
if (!IsToastShown(id)) {
return false;
}
for (const auto& [_, overlay] : root_window_to_overlay_) {
if (overlay && overlay->IsDismissButtonFocused()) {
return true;
}
}
return false;
}
std::unique_ptr<ScopedToastPause> ToastManagerImpl::CreateScopedPause() {
return std::make_unique<ScopedToastPause>();
}
void ToastManagerImpl::CloseToast() {
const base::TimeDelta user_journey_time =
base::TimeTicks::Now() - current_toast_data_->time_start_showing;
const std::string time_range = GetToastDismissedTimeRange(user_journey_time);
base::UmaHistogramEnumeration(
base::StringPrintf("%s.Dismissed.%s", NotifierFrameworkToastHistogram,
time_range.c_str()),
current_toast_data_->catalog_name);
CloseAllToastsWithoutAnimation();
current_toast_data_.reset();
current_toast_expiration_timer_->Stop();
// Show the next toast if available.
// Note that don't show during the lock state is changing, since we reshow
// manually after the state is changed. See OnLockStateChanged.
if (!queue_.empty())
ShowLatest();
}
void ToastManagerImpl::OnToastHoverStateChanged(bool is_hovering) {
DCHECK(current_toast_data_->persist_on_hover);
if (is_hovering != current_toast_expiration_timer_->IsRunning())
return;
is_hovering ? current_toast_expiration_timer_->Pause()
: current_toast_expiration_timer_->Resume();
}
void ToastManagerImpl::OnSessionStateChanged(
session_manager::SessionState state) {
locked_ = state != session_manager::SessionState::ACTIVE;
current_toast_data_.reset();
CloseAllToastsWithoutAnimation();
}
void ToastManagerImpl::ShowLatest() {
DCHECK(!HasActiveToasts());
DCHECK(!current_toast_data_);
auto it = locked_ ? base::ranges::find(queue_, true,
&ToastData::visible_on_lock_screen)
: queue_.begin();
if (it == queue_.end())
return;
current_toast_data_ = std::move(*it);
queue_.erase(it);
LOG(ERROR) << "Showing latest toast, toast id: " << current_toast_data_->id;
serial_++;
if (current_toast_data_->show_on_all_root_windows) {
for (aura::Window* root_window : Shell::GetAllRootWindows()) {
CreateToastOverlayForRoot(root_window);
}
} else {
CreateToastOverlayForRoot(Shell::GetRootWindowForNewWindows());
}
DCHECK(!current_toast_expiration_timer_->IsRunning());
current_toast_expiration_timer_->Start(
current_toast_data_->duration,
base::BindRepeating(&ToastManagerImpl::CloseAllToastsWithAnimation,
base::Unretained(this)));
base::UmaHistogramEnumeration("Ash.NotifierFramework.Toast.ShownCount",
current_toast_data_->catalog_name);
base::UmaHistogramMediumTimes(
"Ash.NotifierFramework.Toast.TimeInQueue",
base::TimeTicks::Now() - current_toast_data_->time_created);
}
void ToastManagerImpl::CreateToastOverlayForRoot(aura::Window* root_window) {
auto& new_overlay = root_window_to_overlay_[root_window];
DCHECK(!new_overlay);
DCHECK(current_toast_data_);
new_overlay = std::make_unique<ToastOverlay>(
/*delegate=*/this, *current_toast_data_, root_window);
new_overlay->Show(true);
// We only want to record this value when the first instance of the toast is
// initialized.
if (current_toast_data_->time_start_showing.is_null())
current_toast_data_->time_start_showing = base::TimeTicks::Now();
}
void ToastManagerImpl::CloseAllToastsWithAnimation() {
for (auto& [_, overlay] : root_window_to_overlay_) {
if (overlay) {
overlay->Show(false);
}
}
}
void ToastManagerImpl::CloseAllToastsWithoutAnimation() {
for (auto& [_, overlay] : root_window_to_overlay_) {
overlay.reset();
}
// `OnClosed` (the other place where we stop the
// `current_toast_expiration_timer_`) is only called when the toast is being
// closed with animation, so we still want to stop the timer here for when it
// is not animating to close.
current_toast_expiration_timer_->Stop();
}
bool ToastManagerImpl::HasActiveToasts() const {
for (const auto& [_, overlay] : root_window_to_overlay_) {
if (overlay) {
return true;
}
}
return false;
}
ToastOverlay* ToastManagerImpl::GetCurrentOverlayForTesting(
aura::Window* root_window) {
return root_window_to_overlay_[root_window].get();
}
void ToastManagerImpl::OnRootWindowAdded(aura::Window* root_window) {
if (HasActiveToasts() && current_toast_data_ &&
current_toast_data_->show_on_all_root_windows) {
CreateToastOverlayForRoot(root_window);
}
}
void ToastManagerImpl::OnRootWindowWillShutdown(aura::Window* root_window) {
// If the toast only exists in the root window that is being closed, inform
// the manager that the toast should be closed.
if (root_window_to_overlay_[root_window] &&
!current_toast_data_->show_on_all_root_windows) {
CloseToast();
}
root_window_to_overlay_.erase(root_window);
}
void ToastManagerImpl::Pause() {
++pause_counter_;
// Immediately closes all the toasts. Since `OnClosed` will not be called,
// manually resets `current_toast_data_` and `queue_`.
CloseAllToastsWithoutAnimation();
current_toast_data_.reset();
queue_.clear();
}
void ToastManagerImpl::Resume() {
CHECK_GT(pause_counter_, 0);
--pause_counter_;
}
} // namespace ash