| // 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 <algorithm> |
| |
| #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/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 = std::ranges::find(queue_, id, &ToastData::id); |
| |
| if (existing_toast != queue_.end()) { |
| LOG(ERROR) |
| << "Toast requested show with a matching ID toast already queued."; |
| // 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)) { |
| LOG(ERROR) << "Toast with matching ID requested show while it was " |
| "already shown."; |
| // 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; |
| } |
| |
| LOG(ERROR) << "Placing Toast in the back of the queue."; |
| queue_.emplace_back(std::move(data)); |
| } |
| |
| if (queue_.size() == 1 && !HasActiveToasts()) { |
| LOG(ERROR) << "Toast is the only one in the queue after being requested, " |
| "so showing it."; |
| ShowLatest(); |
| } |
| } |
| |
| void ToastManagerImpl::Cancel(std::string_view id) { |
| if (IsToastShown(id)) { |
| CloseAllToastsWithAnimation(); |
| return; |
| } |
| |
| auto cancelled_toast = std::ranges::find(queue_, id, &ToastData::id); |
| if (cancelled_toast != queue_.end()) |
| queue_.erase(cancelled_toast); |
| } |
| |
| bool ToastManagerImpl::RequestFocusOnActiveToastButton(std::string_view id) { |
| CHECK(IsToastShown(id)); |
| for (auto& [_, overlay] : root_window_to_overlay_) { |
| if (overlay && overlay->RequestFocusOnActiveToastButton()) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| bool ToastManagerImpl::IsToastShown(std::string_view id) const { |
| return HasActiveToasts() && current_toast_data_ && |
| current_toast_data_->id == id; |
| } |
| |
| bool ToastManagerImpl::IsToastButtonFocused(std::string_view id) const { |
| if (!IsToastShown(id)) { |
| return false; |
| } |
| |
| for (const auto& [_, overlay] : root_window_to_overlay_) { |
| if (overlay && overlay->IsButtonFocused()) { |
| 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_ ? std::ranges::find(queue_, true, |
| &ToastData::visible_on_lock_screen) |
| : queue_.begin(); |
| if (it == queue_.end()) { |
| LOG(ERROR) << "Toast Queue empty."; |
| 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 |