chromium/ash/system/focus_mode/focus_mode_metrics_recorder.cc

// Copyright 2024 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/focus_mode/focus_mode_metrics_recorder.h"

#include "ash/constants/ash_pref_names.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/system/focus_mode/focus_mode_controller.h"
#include "ash/system/focus_mode/focus_mode_histogram_names.h"
#include "ash/system/focus_mode/focus_mode_util.h"
#include "base/metrics/histogram_functions.h"
#include "base/time/time.h"
#include "components/prefs/pref_service.h"
#include "ui/message_center/message_center.h"

namespace ash {
namespace {

const char* GetNameSuffixBySessionDuration(int session_duration) {
  if (session_duration <= 10) {
    return focus_mode_histogram_names::kShortSuffix;
  } else if (session_duration >= 30) {
    return focus_mode_histogram_names::kLongSuffix;
  }
  return focus_mode_histogram_names::kMediumSuffix;
}

void RecordInitialDurationHistogram(base::TimeDelta session_duration) {
  base::UmaHistogramCustomCounts(
      /*name=*/focus_mode_histogram_names::
          kInitialDurationOnSessionStartsHistogramName,
      /*sample=*/session_duration.InMinutes(),
      /*min=*/focus_mode_util::kMinimumDuration.InMinutes(),
      /*max=*/focus_mode_util::kMaximumDuration.InMinutes(), /*buckets=*/50);
}

void RecordStartSessionSourceHistogram(
    focus_mode_histogram_names::ToggleSource source) {
  switch (source) {
    case focus_mode_histogram_names::ToggleSource::kFocusPanel:
      base::UmaHistogramEnumeration(
          /*name=*/focus_mode_histogram_names::kStartSessionSourceHistogramName,
          /*sample=*/focus_mode_histogram_names::StartSessionSource::
              kFocusPanel);
      break;
    case focus_mode_histogram_names::ToggleSource::kFeaturePod:
      base::UmaHistogramEnumeration(
          /*name=*/focus_mode_histogram_names::kStartSessionSourceHistogramName,
          /*sample=*/focus_mode_histogram_names::StartSessionSource::
              kFeaturePod);
      break;
    case ash::focus_mode_histogram_names::ToggleSource::kContextualPanel:
      NOTREACHED();
  }
}

void RecordHasSelectedTaskOnSessionStartHistogram() {
  base::UmaHistogramBoolean(
      /*name=*/focus_mode_histogram_names::
          kHasSelectedTaskOnSessionStartHistogramName,
      /*sample=*/FocusModeController::Get()->HasSelectedTask());
}

void RecordTasksSelectedHistogram(const int tasks_selected_count) {
  base::UmaHistogramCounts100(
      focus_mode_histogram_names::kTasksSelectedHistogramName,
      tasks_selected_count);
}

void RecordTasksCompletedHistogram(const int tasks_completed_count) {
  base::UmaHistogramCounts100(
      focus_mode_histogram_names::kTasksCompletedHistogramName,
      tasks_completed_count);
}

void RecordPlaylistsPlayedHistogram(const int playlists_played_count) {
  base::UmaHistogramCounts100(
      focus_mode_histogram_names::kCountPlaylistsPlayedDuringSession,
      playlists_played_count);
}

void RecordDNDStateOnFocusEndHistogram(
    bool has_user_interactions_on_dnd_in_focus_session) {
  auto* message_center = message_center::MessageCenter::Get();
  const bool is_dnd_on = message_center->IsQuietMode();

  focus_mode_histogram_names::DNDStateOnFocusEndType type;
  if (is_dnd_on && message_center->GetLastQuietModeChangeSourceType() ==
                       message_center::QuietModeSourceType::kFocusMode) {
    type = focus_mode_histogram_names::DNDStateOnFocusEndType::kFocusModeOn;
  } else if (has_user_interactions_on_dnd_in_focus_session) {
    type = is_dnd_on
               ? focus_mode_histogram_names::DNDStateOnFocusEndType::kTurnedOn
               : focus_mode_histogram_names::DNDStateOnFocusEndType::kTurnedOff;
  } else {
    type =
        is_dnd_on
            ? focus_mode_histogram_names::DNDStateOnFocusEndType::kAlreadyOn
            : focus_mode_histogram_names::DNDStateOnFocusEndType::kAlreadyOff;
  }
  base::UmaHistogramEnumeration(
      /*name=*/focus_mode_histogram_names::kDNDStateOnFocusEndHistogramName,
      /*sample=*/type);
}

void RecordTimeAddedOnSessionEndHistogram(int initial_session_duration,
                                          int current_session_duration) {
  std::string histogram_name(
      focus_mode_histogram_names::kTimeAddedOnSessionEndPrefix);
  histogram_name.append(
      GetNameSuffixBySessionDuration(initial_session_duration));

  base::UmaHistogramCustomCounts(
      /*name=*/histogram_name,
      /*sample=*/current_session_duration - initial_session_duration,
      /*min=*/0,
      /*exclusive_max=*/focus_mode_util::kMaximumDuration.InMinutes(),
      /*buckets=*/50);
}

void RecordPercentCompletedHistogram(double progress,
                                     int final_session_duration) {
  std::string histogram_name(
      focus_mode_histogram_names::kPercentCompletedPrefix);
  histogram_name.append(GetNameSuffixBySessionDuration(final_session_duration));

  base::UmaHistogramPercentage(
      /*name=*/histogram_name,
      /*percent=*/(progress * 100));
}

void RecordSessionDurationHistogram(const int time_elapsed) {
  base::UmaHistogramCustomCounts(
      /*name=*/focus_mode_histogram_names::kSessionDurationHistogramName,
      /*sample=*/time_elapsed,
      /*min=*/0,
      /*exclusive_max=*/focus_mode_util::kMaximumDuration.InMinutes(),
      /*buckets=*/50);
}

// Return true if the current selected task is the task selected in the
// previous focus session.
bool IsPreviouslySelectedTask(const PrefService* active_user_prefs,
                              const TaskId& current_selected_task_id) {
  const auto& selected_task_dict =
      active_user_prefs->GetDict(prefs::kFocusModeSelectedTask);

  if (selected_task_dict.empty()) {
    return false;
  }

  const auto* previously_selected_task_id =
      selected_task_dict.FindString(focus_mode_util::kTaskIdKey);
  const auto* previously_selected_task_list_id =
      selected_task_dict.FindString(focus_mode_util::kTaskListIdKey);

  TaskId previous_id = {
      .list_id = previously_selected_task_list_id
                     ? *previously_selected_task_list_id
                     : "",
      .id = previously_selected_task_id ? *previously_selected_task_id : ""};

  return current_selected_task_id == previous_id;
}

void RecordStartedWithTaskHistogram(const TaskId& selected_task_id) {
  if (selected_task_id.empty()) {
    base::UmaHistogramEnumeration(
        /*name=*/focus_mode_histogram_names::
            kStartedWithTaskStatekHistogramName,
        /*sample=*/focus_mode_histogram_names::StartedWithTaskState::kNoTask);
    return;
  }

  PrefService* active_user_prefs =
      Shell::Get()->session_controller()->GetActivePrefService();
  if (!active_user_prefs) {
    // Can be null in tests.
    return;
  }

  base::UmaHistogramEnumeration(
      /*name=*/focus_mode_histogram_names::kStartedWithTaskStatekHistogramName,
      /*sample=*/IsPreviouslySelectedTask(active_user_prefs, selected_task_id)
          ? focus_mode_histogram_names::StartedWithTaskState::
                kPreviouslySelectedTask
          : focus_mode_histogram_names::StartedWithTaskState::
                kNewlySelectedTask);
}

void RecordSoundsPlayedDuringSessionHistogram(bool has_selected_soundscapes,
                                              bool has_selected_youtube_music) {
  focus_mode_histogram_names::PlaylistTypesSelectedDuringFocusSessionType type =
      focus_mode_histogram_names::PlaylistTypesSelectedDuringFocusSessionType::
          kNone;

  if (has_selected_soundscapes && has_selected_youtube_music) {
    type = focus_mode_histogram_names::
        PlaylistTypesSelectedDuringFocusSessionType::
            kYouTubeMusicAndSoundscapes;
  } else if (has_selected_soundscapes) {
    type = focus_mode_histogram_names::
        PlaylistTypesSelectedDuringFocusSessionType::kSoundscapes;
  } else if (has_selected_youtube_music) {
    type = focus_mode_histogram_names::
        PlaylistTypesSelectedDuringFocusSessionType::kYouTubeMusic;
  }

  base::UmaHistogramEnumeration(
      /*name=*/focus_mode_histogram_names::kPlaylistTypesSelectedDuringSession,
      /*sample=*/type);
}

void RecordExistingMediaPlayingOnStartHistogram() {
  const auto system_media_session_info =
      FocusModeController::Get()->GetSystemMediaSessionInfo();
  base::UmaHistogramBoolean(
      /*name=*/focus_mode_histogram_names::
          kStartedWithExistingMediaPlayingHistogramName,
      /*sample=*/system_media_session_info &&
          system_media_session_info->playback_state ==
              media_session::mojom::MediaPlaybackState::kPlaying);
}

void RecordPausedEventCountHistogram() {
  base::UmaHistogramCounts100(
      /*name=*/focus_mode_histogram_names::kMusicPausedEventsCount,
      /*sample=*/FocusModeController::Get()
          ->focus_mode_sounds_controller()
          ->paused_event_count());
}

}  // namespace

FocusModeMetricsRecorder::FocusModeMetricsRecorder(
    const base::TimeDelta& initial_session_duration)
    : initial_session_duration_(initial_session_duration) {
  message_center::MessageCenter::Get()->AddObserver(this);
}

FocusModeMetricsRecorder::~FocusModeMetricsRecorder() {
  message_center::MessageCenter::Get()->RemoveObserver(this);
}

void FocusModeMetricsRecorder::IncrementTasksSelectedCount() {
  tasks_selected_count_++;
}

void FocusModeMetricsRecorder::IncrementTasksCompletedCount() {
  tasks_completed_count_++;
}

void FocusModeMetricsRecorder::OnQuietModeChanged(bool in_quiet_mode) {
  // Only set the value if it's in a focus session and not triggered by
  // Focus Mode.
  if (FocusModeController::Get()->in_focus_session() &&
      message_center::MessageCenter::Get()
              ->GetLastQuietModeChangeSourceType() !=
          message_center::QuietModeSourceType::kFocusMode) {
    has_user_interactions_on_dnd_in_focus_session_ = true;
  }
}

void FocusModeMetricsRecorder::SetHasSelectedSoundType(
    const focus_mode_util::SelectedPlaylist& selected_playlist) {
  if (selected_playlist.empty()) {
    return;
  }

  switch (selected_playlist.type) {
    case focus_mode_util::SoundType::kSoundscape:
      has_selected_soundscapes_ = true;
      break;
    case focus_mode_util::SoundType::kYouTubeMusic:
      has_selected_youtube_music_ = true;
      break;
    case focus_mode_util::SoundType::kNone:
      NOTREACHED();
  }

  playlists_played_count_++;
}

void FocusModeMetricsRecorder::RecordHistogramsOnStart(
    focus_mode_histogram_names::ToggleSource source,
    const TaskId& selected_task_id) {
  RecordInitialDurationHistogram(
      /*session_duration=*/initial_session_duration_);
  RecordStartSessionSourceHistogram(source);
  // TODO(b/344594740): Remove `RecordHasSelectedTaskOnSessionStartHistogram()`.
  RecordHasSelectedTaskOnSessionStartHistogram();
  RecordStartedWithTaskHistogram(selected_task_id);
  RecordExistingMediaPlayingOnStartHistogram();
}

void FocusModeMetricsRecorder::RecordHistogramsOnEnd() {
  RecordTasksSelectedHistogram(tasks_selected_count_);
  RecordTasksCompletedHistogram(tasks_completed_count_);
  RecordPlaylistsPlayedHistogram(playlists_played_count_);
  RecordDNDStateOnFocusEndHistogram(
      has_user_interactions_on_dnd_in_focus_session_);

  auto session_snapshot =
      FocusModeController::Get()->GetSnapshot(base::Time::Now());
  RecordTimeAddedOnSessionEndHistogram(
      initial_session_duration_.InMinutes(),
      session_snapshot.session_duration.InMinutes());
  RecordPercentCompletedHistogram(
      session_snapshot.progress, session_snapshot.session_duration.InMinutes());
  RecordSessionDurationHistogram(session_snapshot.time_elapsed.InMinutes());

  RecordSoundsPlayedDuringSessionHistogram(has_selected_soundscapes_,
                                           has_selected_youtube_music_);
  RecordPausedEventCountHistogram();
}

void FocusModeMetricsRecorder::RecordHistogramOnEndingMoment(
    focus_mode_histogram_names::EndingMomentBubbleClosedReason reason) {
  base::UmaHistogramEnumeration(
      /*name=*/focus_mode_histogram_names::kEndingMomentBubbleActionHistogram,
      /*sample=*/reason);
}

}  // namespace ash