chromium/ash/system/focus_mode/focus_mode_controller.h

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef ASH_SYSTEM_FOCUS_MODE_FOCUS_MODE_CONTROLLER_H_
#define ASH_SYSTEM_FOCUS_MODE_FOCUS_MODE_CONTROLLER_H_

#include <optional>

#include "ash/ash_export.h"
#include "ash/public/cpp/session/session_observer.h"
#include "ash/system/focus_mode/focus_mode_delegate.h"
#include "ash/system/focus_mode/focus_mode_histogram_names.h"
#include "ash/system/focus_mode/focus_mode_session.h"
#include "ash/system/focus_mode/focus_mode_tasks_model.h"
#include "ash/system/focus_mode/focus_mode_tasks_provider.h"
#include "ash/system/focus_mode/sounds/focus_mode_sounds_controller.h"
#include "base/observer_list.h"
#include "base/scoped_observation.h"
#include "base/time/time.h"
#include "base/timer/timer.h"

class PrefRegistrySimple;

namespace base {
class UnguessableToken;
}  // namespace base

namespace views {
class Widget;
}  // namespace views

namespace ash {

class AshWebView;
class FocusModeMetricsRecorder;
class FocusModeSoundsController;

// Controls starting and ending a Focus Mode session and its behavior. Also
// keeps track of the system state to restore after a Focus Mode session ends.
// Has a timer that runs while a session is active and notifies `observers_` on
// every timer tick.
class ASH_EXPORT FocusModeController
    : public SessionObserver,
      public FocusModeSoundsController::Observer,
      public FocusModeTasksModel::Observer,
      public FocusModeTasksModel::Delegate {
 public:
  class Observer : public base::CheckedObserver {
   public:
    // Called whenever Focus Mode changes as a result of user action or when the
    // session duration expires.
    virtual void OnFocusModeChanged(bool in_focus_session) = 0;

    // Called every `timer_` tick for updating UI elements during a Focus Mode
    // session.
    virtual void OnTimerTick(
        const FocusModeSession::Snapshot& session_snapshot) {}

    // Notifies when the session duration is changed in the focus panel without
    // an active session.
    virtual void OnInactiveSessionDurationChanged(
        const base::TimeDelta& session_duration) {}

    // Notifies clients every time the session duration is changed during an
    // active session.
    virtual void OnActiveSessionDurationChanged(
        const FocusModeSession::Snapshot& session_snapshot) {}
  };

  explicit FocusModeController(std::unique_ptr<FocusModeDelegate> delegate);
  FocusModeController(const FocusModeController&) = delete;
  FocusModeController& operator=(const FocusModeController&) = delete;
  ~FocusModeController() override;

  // Convenience function to get the controller instance, which is created and
  // owned by Shell.
  static FocusModeController* Get();

  // Verifies that the session duration hasn't reached `kMaximumDuration`.
  static bool CanExtendSessionDuration(
      const FocusModeSession::Snapshot& snapshot);

  // Registers user profile prefs with the specified `registry`.
  static void RegisterProfilePrefs(PrefRegistrySimple* registry);

  bool in_focus_session() const {
    return current_session_ && current_session_->GetState(base::Time::Now()) ==
                                   FocusModeSession::State::kOn;
  }
  bool in_ending_moment() const {
    return current_session_ && current_session_->GetState(base::Time::Now()) ==
                                   FocusModeSession::State::kEnding;
  }
  base::TimeDelta session_duration() const { return session_duration_; }
  bool turn_on_do_not_disturb() const { return turn_on_do_not_disturb_; }
  void set_turn_on_do_not_disturb(bool turn_on) {
    turn_on_do_not_disturb_ = turn_on;
  }
  const std::optional<FocusModeSession>& current_session() const {
    return current_session_;
  }
  size_t congratulatory_index() const { return congratulatory_index_; }

  FocusModeTasksModel& tasks_model() { return tasks_model_; }
  FocusModeSoundsController* focus_mode_sounds_controller() const {
    return focus_mode_sounds_controller_.get();
  }
  FocusModeDelegate* delegate() { return delegate_.get(); }

  void AddObserver(Observer* observer);
  void RemoveObserver(Observer* observer);

  // Starts or ends a focus session by a toggle `source`.
  void ToggleFocusMode(
      focus_mode_histogram_names::ToggleSource source =
          focus_mode_histogram_names::ToggleSource::kFocusPanel);

  // SessionObserver:
  void OnActiveUserSessionChanged(const AccountId& account_id) override;

  // FocusModeSoundsController::Observer:
  // Will close/create the media widget for an active focus session depending on
  // if there is a selected playlist or not.
  void OnSelectedPlaylistChanged() override;

  // FocusModeTasksModel::Observer:
  void OnSelectedTaskChanged(const std::optional<FocusModeTask>& task) override;
  void OnTasksUpdated(const std::vector<FocusModeTask>& tasks) override;
  void OnTaskCompleted(const FocusModeTask& completed_task) override;

  // FocusModeTasksModel::Delegate:
  void FetchTask(
      const TaskId& task_id,
      FocusModeTasksModel::Delegate::FetchTaskCallback callback) override;
  void FetchTasks() override;
  void AddTask(
      const FocusModeTasksModel::TaskUpdate& update,
      FocusModeTasksModel::Delegate::FetchTaskCallback callback) override;
  void UpdateTask(const FocusModeTasksModel::TaskUpdate& update) override;

  // Extends an active focus session by ten minutes by clicking the `+10 min`
  // button.
  void ExtendSessionDuration();

  // Resets the focus session state for when the session needs to end (i.e. the
  // user manually ends the session, or when the ending moment is terminated).
  // This ensures that states are all reverted (especially DND and UI elements).
  void ResetFocusSession();

  // Used when we want to extend the ending state indefinitely, and requires
  // direct user action to terminate the ending moment.
  void EnablePersistentEnding();

  // Sets a specific value for `session_duration_`. We have two different
  // notions of a session, so this one is only in charge of updating the session
  // duration that will be applied to the next active session. Also notifies
  // observers that the session duration was changed. An "inactive" session can
  // either be no `current_session_`, or if we are in the ending moment, since
  // the user should still be able to adjust and start a new session during that
  // time.
  void SetInactiveSessionDuration(const base::TimeDelta& new_session_duration);

  // Returns whether the user has ever started a focus session previously.
  bool HasStartedSessionBefore() const;

  // Creates and returns a snapshot of the current session based on `now`.
  // Returns a default struct if there is no session.
  FocusModeSession::Snapshot GetSnapshot(const base::Time& now) const;

  // Returns the session duration of either the current session, or what the
  // upcoming session will be set to.
  base::TimeDelta GetSessionDuration() const;

  // Returns the end time of an active session. This end time is meant to be
  // displayed, and may be different depending on the session state (e.g. the
  // ending moment needs to account for the extra duration).
  base::Time GetActualEndTime() const;

  // Stores the provided `task`.
  void SetSelectedTask(const FocusModeTask& task);

  // Returns whether there is a currently selected task.
  bool HasSelectedTask() const;

  // Marks the task as completed in the model.
  void CompleteTask();

  // Shows the ending moment nudge that is anchored to the focus mode tray. Only
  // show if there isn't already showing and if there is no tray bubble open.
  void MaybeShowEndingMomentNudge();

  // This is currently only used in testing to trigger an ending moment
  // immediately if there is an ongoing session.
  void TriggerEndingMomentImmediately();

  // Get the request id for the media session played for Focus Sounds.
  const base::UnguessableToken& GetMediaSessionRequestId();

  // If `create_media_widget` is true, we will assign a valid value to
  // `test_media_request_id_`; otherwise, we will reset it due to simulating no
  // media widget exists.
  void SetMediaSessionRequestIdForTesting(bool create_media_widget) {
    test_media_request_id_ = create_media_widget
                                 ? base::UnguessableToken::Create()
                                 : base::UnguessableToken::Null();
  }

  void RequestTasksUpdateForTesting();
  bool TasksProviderHasCachedTasksForTesting() const;

  media_session::mojom::MediaSessionInfoPtr GetSystemMediaSessionInfo();
  void SetSystemMediaSessionInfoForTesting(
      media_session::mojom::MediaSessionInfoPtr media_session_info) {
    test_media_session_info_ = std::move(media_session_info);
  }

 private:
  // Starts a focus session by updating UI elements, starting `timer_`, and
  // setting `current_session_` to the desired session duration and end time.
  void StartFocusSession(focus_mode_histogram_names::ToggleSource source);

  // Called every time a second passes on `timer_` while the session is active.
  void OnTimerTick();

  // This is called when the active user changes, and is important to update our
  // cached values in case different users have different stored preferences.
  void UpdateFromUserPrefs();

  // Called by `UpdateFromUserPrefs` to update our cached values for the active
  // user about the selected task.
  void UpdateSelectedTaskFromUserPrefs();

  // Called once a session starts. Saves the current selected settings to user
  // prefs so we can provide the same set-up the next time the user comes back
  // to Focus Mode.
  void SaveSettingsToUserPrefs();

  // Called once a session starts, and when a task is selected or deselected in
  // focus session.
  void SaveSelectedTaskSettingsToUserPrefs(
      const std::optional<FocusModeTask>& task);

  // Closes any open system tray bubbles. This is done whenever we start a focus
  // session.
  void CloseSystemTrayBubble();

  // Sets the visibility of the focus tray on the shelf.
  void SetFocusTrayVisibility(bool visible);

  // This tells us if there is an open focus mode tray bubble on any of the
  // displays.
  bool IsFocusTrayBubbleVisible() const;

  // Creates the media widget if one doesn't already exist and if there is a
  // selected playlist. Returns true if we create a new media widget.
  bool MaybeCreateMediaWidget();
  void CloseMediaWidget();

  // Called when the user extends the ending moment. This function will create a
  // new media widget, or resume playing the existing media.
  void PerformActionsForMusic();

  void OnTasksReceived(const std::vector<FocusModeTask>& tasks);

  // Gives Focus Mode access to the Google Tasks API.
  FocusModeTasksProvider tasks_provider_;

  FocusModeTasksModel tasks_model_;

  // This is the expected duration of a Focus Mode session once it starts.
  // Depends on previous session data (from user prefs) or user input.
  base::TimeDelta session_duration_;

  // This will dictate whether DND will be turned on when a Focus Mode session
  // starts. Depends on previous session data (from user prefs) or user input.
  bool turn_on_do_not_disturb_ = true;

  // This timer is used for keeping track of the Focus Mode session duration and
  // will trigger a callback every second during a session. It will terminate
  // once the session goes into the `kEnding` state, or if a user toggles off
  // Focus Mode.
  base::MetronomeTimer timer_;

  // This is used to track the current session, if any.
  std::optional<FocusModeSession> current_session_;

  // A random value between 0 and `focus_mode_util::kCongratulatoryTitleNum -
  // 1`.
  size_t congratulatory_index_ = 0;

  std::unique_ptr<FocusModeMetricsRecorder> focus_mode_metrics_recorder_;

  // This is used to display focus mode playlists. Playback controls will be
  // added later.
  std::unique_ptr<FocusModeSoundsController> focus_mode_sounds_controller_;

  // The media widget and its contents view.
  std::unique_ptr<views::Widget> media_widget_;
  raw_ptr<AshWebView> focus_mode_media_view_ = nullptr;

  // True if a playing selected playlist was paused automatically when entering
  // the ending moment. If `paused_by_ending_moment_` is true, after the user
  // extended the session, the selected playlist will resume playing if it's
  // still selected.
  bool paused_by_ending_moment_ = false;

  // The info about the current media session for testing. It will be null if
  // there isn't a current media session.
  media_session::mojom::MediaSessionInfoPtr test_media_session_info_;
  // The media session request id for testing.
  base::UnguessableToken test_media_request_id_ =
      base::UnguessableToken::Null();

  std::unique_ptr<FocusModeDelegate> delegate_;

  base::ScopedObservation<FocusModeTasksModel, FocusModeController>
      tasks_model_observation_{this};
  base::ObserverList<Observer> observers_;
  base::WeakPtrFactory<FocusModeController> weak_factory_{this};
};

}  // namespace ash

#endif  // ASH_SYSTEM_FOCUS_MODE_FOCUS_MODE_CONTROLLER_H_