chromium/ash/picker/metrics/picker_session_metrics.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/picker/metrics/picker_session_metrics.h"

#include "ash/constants/ash_pref_names.h"
#include "ash/public/cpp/picker/picker_category.h"
#include "ash/public/cpp/picker/picker_search_result.h"
#include "base/functional/overloaded.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "components/metrics/structured/structured_events.h"
#include "components/metrics/structured/structured_metrics_client.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "ui/base/ime/text_input_client.h"

namespace ash {
namespace {

namespace cros_events = metrics::structured::events::v2::cr_os_events;

constexpr int kCapsLockCountThreshold = 20;

cros_events::PickerInputFieldType GetInputFieldType(
    ui::TextInputClient* client) {
  if (client == nullptr) {
    return cros_events::PickerInputFieldType::NONE;
  }
  switch (client->GetTextInputType()) {
    case ui::TEXT_INPUT_TYPE_NONE:
      return cros_events::PickerInputFieldType::NONE;
    case ui::TEXT_INPUT_TYPE_TEXT:
    case ui::TEXT_INPUT_TYPE_TEXT_AREA:
      if (client->CanInsertImage()) {
        return cros_events::PickerInputFieldType::RICH_TEXT;
      } else {
        return cros_events::PickerInputFieldType::PLAIN_TEXT;
      }
    case ui::TEXT_INPUT_TYPE_PASSWORD:
      return cros_events::PickerInputFieldType::PASSWORD;
    case ui::TEXT_INPUT_TYPE_SEARCH:
      return cros_events::PickerInputFieldType::SEARCH;
    case ui::TEXT_INPUT_TYPE_EMAIL:
      return cros_events::PickerInputFieldType::EMAIL;
    case ui::TEXT_INPUT_TYPE_NUMBER:
      return cros_events::PickerInputFieldType::NUMBER;
    case ui::TEXT_INPUT_TYPE_TELEPHONE:
      return cros_events::PickerInputFieldType::TELEPHONE;
    case ui::TEXT_INPUT_TYPE_URL:
      return cros_events::PickerInputFieldType::URL;
    case ui::TEXT_INPUT_TYPE_DATE:
    case ui::TEXT_INPUT_TYPE_DATE_TIME:
    case ui::TEXT_INPUT_TYPE_DATE_TIME_LOCAL:
    case ui::TEXT_INPUT_TYPE_MONTH:
    case ui::TEXT_INPUT_TYPE_TIME:
    case ui::TEXT_INPUT_TYPE_WEEK:
    case ui::TEXT_INPUT_TYPE_DATE_TIME_FIELD:
      return cros_events::PickerInputFieldType::DATE_TIME;
    case ui::TEXT_INPUT_TYPE_CONTENT_EDITABLE:
    case ui::TEXT_INPUT_TYPE_NULL:
      return cros_events::PickerInputFieldType::OTHER;
  }
}

int GetSelectionLength(ui::TextInputClient* client) {
  if (!client) {
    return 0;
  }
  gfx::Range selection_range;
  client->GetEditableSelectionRange(&selection_range);
  if (selection_range.IsValid() && !selection_range.is_empty()) {
    return selection_range.length();
  }
  return 0;
}

cros_events::PickerSessionOutcome ConvertToCrosEventSessionOutcome(
    PickerSessionMetrics::SessionOutcome outcome) {
  switch (outcome) {
    case PickerSessionMetrics::SessionOutcome::kUnknown:
      return cros_events::PickerSessionOutcome::UNKNOWN;
    case PickerSessionMetrics::SessionOutcome::kInsertedOrCopied:
      return cros_events::PickerSessionOutcome::INSERTED_OR_COPIED;
    case PickerSessionMetrics::SessionOutcome::kAbandoned:
      return cros_events::PickerSessionOutcome::ABANDONED;
    case PickerSessionMetrics::SessionOutcome::kRedirected:
      return cros_events::PickerSessionOutcome::REDIRECTED;
    case PickerSessionMetrics::SessionOutcome::kFormat:
      return cros_events::PickerSessionOutcome::FORMAT;
    case PickerSessionMetrics::SessionOutcome::kOpenFile:
      return cros_events::PickerSessionOutcome::OPEN_FILE;
    case PickerSessionMetrics::SessionOutcome::kOpenLink:
      return cros_events::PickerSessionOutcome::OPEN_LINK;
    case PickerSessionMetrics::SessionOutcome::kCreate:
      return cros_events::PickerSessionOutcome::CREATE;
  }
}

cros_events::PickerAction ConvertToCrosEventAction(
    std::optional<PickerCategory> action) {
  if (!action.has_value()) {
    return cros_events::PickerAction::UNKNOWN;
  }
  switch (*action) {
    case PickerCategory::kEditorWrite:
      return cros_events::PickerAction::OPEN_EDITOR_WRITE;
    case PickerCategory::kEditorRewrite:
      return cros_events::PickerAction::OPEN_EDITOR_REWRITE;
    case PickerCategory::kLinks:
      return cros_events::PickerAction::OPEN_LINKS;
    case PickerCategory::kEmojisGifs:
    case PickerCategory::kEmojis:
      return cros_events::PickerAction::OPEN_EXPRESSIONS;
    case PickerCategory::kClipboard:
      return cros_events::PickerAction::OPEN_CLIPBOARD;
    case PickerCategory::kDriveFiles:
      return cros_events::PickerAction::OPEN_DRIVE_FILES;
    case PickerCategory::kLocalFiles:
      return cros_events::PickerAction::OPEN_LOCAL_FILES;
    case PickerCategory::kDatesTimes:
      return cros_events::PickerAction::OPEN_DATES_TIMES;
    case PickerCategory::kUnitsMaths:
      return cros_events::PickerAction::OPEN_UNITS_MATHS;
  }
}

cros_events::PickerResultSource GetResultSource(
    std::optional<PickerSearchResult> result) {
  if (!result.has_value()) {
    return cros_events::PickerResultSource::UNKNOWN;
  }
  using ReturnType = cros_events::PickerResultSource;
  return std::visit(
      base::Overloaded{
          [](const PickerTextResult& data) {
            switch (data.source) {
              case PickerTextResult::Source::kUnknown:
                return cros_events::PickerResultSource::UNKNOWN;
              case PickerTextResult::Source::kDate:
                return cros_events::PickerResultSource::DATES_TIMES;
              case PickerTextResult::Source::kMath:
                return cros_events::PickerResultSource::UNITS_MATHS;
              case PickerTextResult::Source::kCaseTransform:
                return cros_events::PickerResultSource::CASE_TRANSFORM;
              case PickerTextResult::Source::kOmnibox:
                return cros_events::PickerResultSource::OMNIBOX;
            }
          },
          [](const PickerEmojiResult& data) {
            return cros_events::PickerResultSource::EMOJI;
          },
          [](const PickerClipboardResult& data) {
            return cros_events::PickerResultSource::CLIPBOARD;
          },
          [](const PickerBrowsingHistoryResult& data) {
            return cros_events::PickerResultSource::OMNIBOX;
          },
          [](const PickerLocalFileResult& data) {
            return cros_events::PickerResultSource::LOCAL_FILES;
          },
          [](const PickerDriveFileResult& data) {
            return cros_events::PickerResultSource::DRIVE_FILES;
          },
          [](const PickerCategoryResult& data) -> ReturnType { NOTREACHED(); },
          [](const PickerSearchRequestResult& data) -> ReturnType {
            NOTREACHED();
          },
          [](const PickerEditorResult& data) -> ReturnType { NOTREACHED(); },
          [](const PickerNewWindowResult& data) -> ReturnType {
            return cros_events::PickerResultSource::UNKNOWN;
          },
          [](const PickerCapsLockResult& data) -> ReturnType {
            return cros_events::PickerResultSource::UNKNOWN;
          },
          [](const PickerCaseTransformResult& data) -> ReturnType {
            return cros_events::PickerResultSource::CASE_TRANSFORM;
          },
      },
      *result);
}

cros_events::PickerResultType GetResultType(
    std::optional<PickerSearchResult> result) {
  if (!result.has_value()) {
    return cros_events::PickerResultType::UNKNOWN;
  }
  using ReturnType = cros_events::PickerResultType;
  return std::visit(
      base::Overloaded{
          [](const PickerTextResult& data) {
            return cros_events::PickerResultType::TEXT;
          },
          [](const PickerEmojiResult& data) {
            switch (data.type) {
              case PickerEmojiResult::Type::kEmoji:
                return cros_events::PickerResultType::EMOJI;
              case PickerEmojiResult::Type::kSymbol:
                return cros_events::PickerResultType::SYMBOL;
              case PickerEmojiResult::Type::kEmoticon:
                return cros_events::PickerResultType::EMOTICON;
            }
          },
          [](const PickerClipboardResult& data) {
            switch (data.display_format) {
              case PickerClipboardResult::DisplayFormat::kFile:
                return cros_events::PickerResultType::CLIPBOARD_FILE;
              case PickerClipboardResult::DisplayFormat::kText:
              case PickerClipboardResult::DisplayFormat::kUrl:
                return cros_events::PickerResultType::CLIPBOARD_TEXT;
              case PickerClipboardResult::DisplayFormat::kImage:
                return cros_events::PickerResultType::CLIPBOARD_IMAGE;
              case PickerClipboardResult::DisplayFormat::kHtml:
                return cros_events::PickerResultType::CLIPBOARD_HTML;
            }
          },
          [](const PickerBrowsingHistoryResult& data) {
            return cros_events::PickerResultType::LINK;
          },
          [](const PickerLocalFileResult& data) {
            return cros_events::PickerResultType::LOCAL_FILE;
          },
          [](const PickerDriveFileResult& data) {
            return cros_events::PickerResultType::DRIVE_FILE;
          },
          [](const PickerCategoryResult& data) -> ReturnType { NOTREACHED(); },
          [](const PickerSearchRequestResult& data) -> ReturnType {
            NOTREACHED();
          },
          [](const PickerEditorResult& data) -> ReturnType { NOTREACHED(); },
          [](const PickerNewWindowResult& data) -> ReturnType {
            return cros_events::PickerResultType::UNKNOWN;
          },
          [](const PickerCapsLockResult& data) -> ReturnType {
            return cros_events::PickerResultType::UNKNOWN;
          },
          [](const PickerCaseTransformResult& data) -> ReturnType {
            return cros_events::PickerResultType::TEXT;
          },
      },
      *result);
}

}  // namespace

PickerSessionMetrics::PickerSessionMetrics() = default;

PickerSessionMetrics::PickerSessionMetrics(PrefService* prefs)
    : prefs_(prefs) {}

PickerSessionMetrics::~PickerSessionMetrics() {
  OnFinishSession();
}

void PickerSessionMetrics::RegisterProfilePrefs(PrefRegistrySimple* registry) {
  registry->RegisterIntegerPref(prefs::kPickerCapsLockSelectedCountPrefName, 0);
  registry->RegisterIntegerPref(prefs::kPickerCapsLockDislayedCountPrefName, 0);
}

void PickerSessionMetrics::SetOutcome(SessionOutcome outcome) {
  if (outcome_ == SessionOutcome::kUnknown) {
    outcome_ = outcome;
  }
}

void PickerSessionMetrics::SetSelectedCategory(PickerCategory category) {
  if (!last_category_.has_value()) {
    last_category_ = category;
  }
}

void PickerSessionMetrics::SetSelectedResult(PickerSearchResult selected_result,
                                             int index) {
  if (!selected_result_.has_value()) {
    selected_result_ = std::move(selected_result);
    result_index_ = index;
  }
}

void PickerSessionMetrics::UpdateSearchQuery(std::u16string_view search_query) {
  int new_length = static_cast<int>(search_query.length());
  search_query_total_edits_ += abs(new_length - search_query_length_);
  search_query_length_ = new_length;
}

void PickerSessionMetrics::OnStartSession(ui::TextInputClient* client) {
  metrics::structured::StructuredMetricsClient::Record(
      std::move(cros_events::Picker_StartSession()
                    .SetInputFieldType(GetInputFieldType(client))
                    .SetSelectionLength(
                        static_cast<int64_t>(GetSelectionLength(client)))));
}

void PickerSessionMetrics::OnFinishSession() {
  if (caps_lock_displayed_) {
    UpdateCapLockPrefs(
        selected_result_.has_value() &&
        std::holds_alternative<PickerCapsLockResult>(*selected_result_));
  }
  base::UmaHistogramEnumeration("Ash.Picker.Session.Outcome", outcome_);
  metrics::structured::StructuredMetricsClient::Record(
      cros_events::Picker_FinishSession()
          .SetOutcome(ConvertToCrosEventSessionOutcome(outcome_))
          .SetAction(ConvertToCrosEventAction(last_category_))
          .SetResultSource(GetResultSource(std::move(selected_result_)))
          .SetResultType(GetResultType(std::move(selected_result_)))
          .SetTotalEdits(search_query_total_edits_)
          .SetFinalQuerySize(search_query_length_)
          .SetResultIndex(result_index_));
}

void PickerSessionMetrics::SetCapsLockDisplayed(bool displayed) {
  caps_lock_displayed_ = displayed;
}

void PickerSessionMetrics::UpdateCapLockPrefs(bool caps_lock_selected) {
  if (prefs_ == nullptr) {
    return;
  }
  int caps_lock_displayed_count =
      prefs_->GetInteger(prefs::kPickerCapsLockDislayedCountPrefName) + 1;
  int caps_lock_selected_count =
      prefs_->GetInteger(prefs::kPickerCapsLockSelectedCountPrefName);
  if (caps_lock_selected) {
    ++caps_lock_selected_count;
  }
  // We will only use caps_lock_selected_count / caps_lock_displayed_count to
  // decide the position of caps lock toggle. We halves both numbers so that
  // they don't grow infinitely and later usages have more weights in decision
  // making. The remainders in division is not significant in our use cases.
  if (caps_lock_displayed_count >= kCapsLockCountThreshold) {
    caps_lock_displayed_count /= 2;
    caps_lock_selected_count /= 2;
  }
  prefs_->SetInteger(prefs::kPickerCapsLockDislayedCountPrefName,
                     caps_lock_displayed_count);
  prefs_->SetInteger(prefs::kPickerCapsLockSelectedCountPrefName,
                     caps_lock_selected_count);
}

}  // namespace ash