chromium/chromeos/ash/components/phonehub/phone_hub_structured_metrics_logger.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 "chromeos/ash/components/phonehub/phone_hub_structured_metrics_logger.h"

#include <string>

#include "ash/public/cpp/network_config_service.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/i18n/rtl.h"
#include "base/system/sys_info.h"
#include "base/uuid.h"
#include "chromeos/ash/components/phonehub/pref_names.h"
#include "chromeos/ash/components/phonehub/proto/phonehub_api.pb.h"
#include "components/metrics/structured/structured_events.h"
#include "components/metrics/structured/structured_metrics_client.h"
#include "components/metrics/structured/structured_metrics_features.h"
#include "crypto/sha2.h"
#include "device/bluetooth/floss/floss_features.h"

namespace ash::phonehub {

void PhoneHubStructuredMetricsLogger::RegisterPrefs(
    PrefRegistrySimple* registry) {
  registry->RegisterStringPref(prefs::kPhoneManufacturer, std::string());
  registry->RegisterStringPref(prefs::kPhoneModel, std::string());
  registry->RegisterStringPref(prefs::kPhoneLocale, std::string());
  registry->RegisterStringPref(prefs::kPhonePseudonymousId, std::string());
  registry->RegisterInt64Pref(prefs::kPhoneAmbientApkVersion, 0);
  registry->RegisterInt64Pref(prefs::kPhoneGmsCoreVersion, 0);
  registry->RegisterIntegerPref(prefs::kPhoneAndroidVersion, 0);
  registry->RegisterIntegerPref(
      prefs::kPhoneProfileType,
      -1);  // Make default value different from normal default profile type 0
  registry->RegisterTimePref(prefs::kPhoneInfoLastUpdatedTime, base::Time());
  registry->RegisterStringPref(prefs::kChromebookPseudonymousId, std::string());
  registry->RegisterTimePref(prefs::kPseudonymousIdRotationDate, base::Time());
}

PhoneHubStructuredMetricsLogger::PhoneHubStructuredMetricsLogger(
    PrefService* pref_service)
    : bluetooth_stack_(floss::features::IsFlossEnabled()
                           ? BluetoothStack::kFloss
                           : BluetoothStack::kBlueZ),
      chromebook_locale_(base::i18n::GetConfiguredLocale()),
      pref_service_(pref_service) {
  ash::GetNetworkConfigService(
      cros_network_config_.BindNewPipeAndPassReceiver());
}
PhoneHubStructuredMetricsLogger::~PhoneHubStructuredMetricsLogger() = default;

void PhoneHubStructuredMetricsLogger::LogPhoneHubDiscoveryStarted(
    DiscoveryEntryPoint entry_point) {
  if (!base::FeatureList::IsEnabled(
          metrics::structured::kPhoneHubStructuredMetrics)) {
    return;
  }
  UpdateIdentifiersIfNeeded();
  auto metric =
      ::metrics::structured::events::v2::phone_hub::DiscoveryStarted();
  // Populate chromebook related information
  metric.SetSessionId(phone_hub_session_id_);
  metric.SetTimestamp(
      base::Time::NowFromSystemTime().InMillisecondsSinceUnixEpoch());
  metric.SetDiscoveryEntrypoint(static_cast<int>(entry_point));
  ::metrics::structured::StructuredMetricsClient::Record(std::move(metric));
}

void PhoneHubStructuredMetricsLogger::LogDiscoveryAttempt(
    secure_channel::mojom::DiscoveryResult result,
    std::optional<secure_channel::mojom::DiscoveryErrorCode> error_code) {
  if (!base::FeatureList::IsEnabled(
          metrics::structured::kPhoneHubStructuredMetrics)) {
    return;
  }
  UpdateIdentifiersIfNeeded();
  auto metric =
      ::metrics::structured::events::v2::phone_hub::DiscoveryFinished();
  // Populate chromebook related information
  metric.SetSessionId(phone_hub_session_id_);
  metric.SetTimestamp(
      base::Time::NowFromSystemTime().InMillisecondsSinceUnixEpoch());

  metric.SetDiscoeryResult(static_cast<int>(result));
  if (error_code.has_value()) {
    metric.SetDiscoveryResultErrorCode(static_cast<int>(error_code.value()));
  }
  ::metrics::structured::StructuredMetricsClient::Record(std::move(metric));
}

void PhoneHubStructuredMetricsLogger::LogNearbyConnectionState(
    secure_channel::mojom::NearbyConnectionStep step,
    secure_channel::mojom::NearbyConnectionStepResult result) {
  if (!base::FeatureList::IsEnabled(
          metrics::structured::kPhoneHubStructuredMetrics)) {
    return;
  }
  UpdateIdentifiersIfNeeded();
  if (step == secure_channel::mojom::NearbyConnectionStep::kUpgradedToWebRtc &&
      result == secure_channel::mojom::NearbyConnectionStepResult::kSuccess) {
    medium_ = Medium::kWebRTC;
    UploadDeviceInfo();
  }
  auto metric =
      ::metrics::structured::events::v2::phone_hub::NearbyConnection();
  // Populate chromebook related information
  metric.SetSessionId(phone_hub_session_id_);
  metric.SetTimestamp(
      base::Time::NowFromSystemTime().InMillisecondsSinceUnixEpoch());

  metric.SetNearbyConnectionStep(static_cast<int>(step));
  metric.SetNearbyConnectionStepResult(static_cast<int>(result));
  ::metrics::structured::StructuredMetricsClient::Record(std::move(metric));
}

void PhoneHubStructuredMetricsLogger::LogSecureChannelState(
    secure_channel::mojom::SecureChannelState state) {
  if (!base::FeatureList::IsEnabled(
          metrics::structured::kPhoneHubStructuredMetrics)) {
    return;
  }
  UpdateIdentifiersIfNeeded();
  auto metric = ::metrics::structured::events::v2::phone_hub::
      SecureChannelAuthentication();
  // Populate chromebook related information
  metric.SetSessionId(phone_hub_session_id_);
  metric.SetTimestamp(
      base::Time::NowFromSystemTime().InMillisecondsSinceUnixEpoch());

  metric.SetSecureChannelAuthenticationState(static_cast<int>(state));
  ::metrics::structured::StructuredMetricsClient::Record(std::move(metric));
}

void PhoneHubStructuredMetricsLogger::LogPhoneHubMessageEvent(
    proto::MessageType message_type,
    PhoneHubMessageDirection message_direction) {
  if (!base::FeatureList::IsEnabled(
          metrics::structured::kPhoneHubStructuredMetrics)) {
    return;
  }
  UpdateIdentifiersIfNeeded();
  auto metric = ::metrics::structured::events::v2::phone_hub::PhoneHubMessage();
  // Populate chromebook related information
  metric.SetSessionId(phone_hub_session_id_);
  metric.SetTimestamp(
      base::Time::NowFromSystemTime().InMillisecondsSinceUnixEpoch());

  metric.SetPhoneHubMessageType(static_cast<int>(message_type));
  metric.SetPhoneHubMessageDirection(static_cast<int>(message_direction));
  ::metrics::structured::StructuredMetricsClient::Record(std::move(metric));
}

void PhoneHubStructuredMetricsLogger::LogPhoneHubUiStateUpdated(
    PhoneHubUiState ui_state) {
  if (!base::FeatureList::IsEnabled(
          metrics::structured::kPhoneHubStructuredMetrics)) {
    return;
  }
  UpdateIdentifiersIfNeeded();
  auto metric =
      ::metrics::structured::events::v2::phone_hub::PhoneHubUiUpdate();
  // Populate chromebook related information
  metric.SetSessionId(phone_hub_session_id_);
  metric.SetTimestamp(
      base::Time::NowFromSystemTime().InMillisecondsSinceUnixEpoch());

  metric.SetPhoneHubUiState(static_cast<int>(ui_state));
  ::metrics::structured::StructuredMetricsClient::Record(std::move(metric));
}

void PhoneHubStructuredMetricsLogger::ProcessPhoneInformation(
    const proto::PhoneProperties& phone_properties) {
  if (!base::FeatureList::IsEnabled(
          metrics::structured::kPhoneHubStructuredMetrics)) {
    return;
  }
  bool phone_info_updated = false;
  if (phone_properties.has_pseudonymous_id_next_rotation_date()) {
    base::Time pseudonymous_id_rotation_date =
        base::Time::FromMillisecondsSinceUnixEpoch(
            phone_properties.pseudonymous_id_next_rotation_date());
    if (pref_service_->GetTime(prefs::kPseudonymousIdRotationDate).is_null() ||
        pseudonymous_id_rotation_date <
            pref_service_->GetTime(prefs::kPseudonymousIdRotationDate)) {
      pref_service_->SetTime(prefs::kPseudonymousIdRotationDate,
                             pseudonymous_id_rotation_date);
    }
  }
  if (phone_properties.has_phone_pseudonymous_id()) {
    if (pref_service_->GetString(prefs::kPhonePseudonymousId).empty() ||
        pref_service_->GetString(prefs::kPhonePseudonymousId) !=
            phone_properties.phone_pseudonymous_id()) {
      pref_service_->SetString(prefs::kPhonePseudonymousId,
                               phone_properties.phone_pseudonymous_id());
      phone_info_updated = true;
    }
  }
  if (phone_properties.has_phone_manufacturer()) {
    if (pref_service_->GetString(prefs::kPhoneManufacturer).empty() ||
        pref_service_->GetString(prefs::kPhoneManufacturer) !=
            phone_properties.phone_manufacturer()) {
      pref_service_->SetString(prefs::kPhoneManufacturer,
                               phone_properties.phone_manufacturer());
      phone_info_updated = true;
    }
  }
  if (phone_properties.has_phone_model()) {
    if (pref_service_->GetString(prefs::kPhoneModel).empty() ||
        pref_service_->GetString(prefs::kPhoneModel) !=
            phone_properties.phone_model()) {
      pref_service_->SetString(prefs::kPhoneModel,
                               phone_properties.phone_model());
      phone_info_updated = true;
    }
  }

  if (pref_service_->GetInt64(prefs::kPhoneGmsCoreVersion) !=
      phone_properties.gmscore_version()) {
    pref_service_->SetInt64(prefs::kPhoneGmsCoreVersion,
                            phone_properties.gmscore_version());
    phone_info_updated = true;
  }

  if (pref_service_->GetInteger(prefs::kPhoneAndroidVersion) !=
      phone_properties.android_version()) {
    pref_service_->SetInteger(prefs::kPhoneAndroidVersion,
                              phone_properties.android_version());
    phone_info_updated = true;
  }

  if (phone_properties.has_ambient_version() &&
      pref_service_->GetInt64(prefs::kPhoneAmbientApkVersion) !=
          phone_properties.ambient_version()) {
    pref_service_->SetInt64(prefs::kPhoneAmbientApkVersion,
                            phone_properties.ambient_version());
    phone_info_updated = true;
  }

  if (phone_properties.has_network_status()) {
    if (!phone_network_status_.has_value() ||
        phone_properties.network_status() != phone_network_status_.value()) {
      phone_info_updated = true;
      phone_network_status_ = phone_properties.network_status();
    }
    if (phone_network_status_ == proto::NetworkStatus::CELLULAR) {
      network_state_ = NetworkState::kPhoneOnCellular;
    } else if (phone_network_status_ == proto::NetworkStatus::WIFI) {
      if (phone_properties.has_ssid() &&
          phone_network_ssid_ != phone_properties.ssid()) {
        phone_network_ssid_ = phone_properties.ssid();

      cros_network_config_->GetNetworkStateList(
          chromeos::network_config::mojom::NetworkFilter::New(
              chromeos::network_config::mojom::FilterType::kActive,
              chromeos::network_config::mojom::NetworkType::kWiFi,
              chromeos::network_config::mojom::kNoLimit),
          base::BindOnce(
              &PhoneHubStructuredMetricsLogger::OnNetworkStateListFetched,
              base::Unretained(this)));
      }
    } else {
      network_state_ = NetworkState::kDifferentNetwork;
    }
  }

  if (pref_service_->GetInteger(prefs::kPhoneProfileType) !=
      phone_properties.profile_type()) {
    pref_service_->SetInteger(prefs::kPhoneProfileType,
                              phone_properties.profile_type());
    phone_info_updated = true;
  }

  if (phone_properties.has_locale()) {
    if (pref_service_->GetString(prefs::kPhoneLocale).empty() ||
        pref_service_->GetString(prefs::kPhoneLocale) !=
            phone_properties.locale()) {
      pref_service_->SetString(prefs::kPhoneLocale, phone_properties.locale());
      phone_info_updated = true;
    }
  }
  pref_service_->SetTime(prefs::kPhoneInfoLastUpdatedTime,
                         base::Time::NowFromSystemTime());
  if (phone_info_updated) {
    UploadDeviceInfo();
  }
}

void PhoneHubStructuredMetricsLogger::UpdateIdentifiersIfNeeded() {
  if (!base::FeatureList::IsEnabled(
          metrics::structured::kPhoneHubStructuredMetrics)) {
    return;
  }
  if (pref_service_->GetTime(prefs::kPseudonymousIdRotationDate).is_null() ||
      pref_service_->GetTime(prefs::kPseudonymousIdRotationDate) <=
          base::Time::NowFromSystemTime()) {
    ResetCachedInformation();
  }
  if (pref_service_->GetString(prefs::kChromebookPseudonymousId).empty()) {
    pref_service_->SetString(
        prefs::kChromebookPseudonymousId,
        base::Uuid::GenerateRandomV4().AsLowercaseString());
  }
  if (pref_service_->GetTime(prefs::kPseudonymousIdRotationDate).is_null() ||
      pref_service_->GetTime(prefs::kPseudonymousIdRotationDate) >
          (base::Time::NowFromSystemTime() +
           kMaxStructuredMetricsPseudonymousIdDays)) {
    pref_service_->SetTime(prefs::kPseudonymousIdRotationDate,
                           base::Time::NowFromSystemTime() +
                               kMaxStructuredMetricsPseudonymousIdDays);
  }
  if (phone_hub_session_id_.empty()) {
    phone_hub_session_id_ = base::Uuid::GenerateRandomV4().AsLowercaseString();
    UploadDeviceInfo();
  }
}

void PhoneHubStructuredMetricsLogger::ResetCachedInformation() {
  if (!base::FeatureList::IsEnabled(
          metrics::structured::kPhoneHubStructuredMetrics)) {
    return;
  }
  phone_network_status_ = std::nullopt;
  pref_service_->SetInteger(prefs::kPhoneProfileType, -1);
  phone_network_ssid_ = std::nullopt;
  pref_service_->SetInt64(prefs::kPhoneGmsCoreVersion, 0);
  pref_service_->SetInteger(prefs::kPhoneAndroidVersion, 0);
  pref_service_->SetInt64(prefs::kPhoneAmbientApkVersion, 0);
  pref_service_->SetTime(prefs::kPhoneInfoLastUpdatedTime, base::Time());
  pref_service_->SetString(prefs::kPhonePseudonymousId, std::string());
  pref_service_->SetString(prefs::kPhoneManufacturer, std::string());
  pref_service_->SetString(prefs::kPhoneModel, std::string());
  pref_service_->SetString(prefs::kPhoneLocale, std::string());

  network_state_ = NetworkState::kUnknown;
  pref_service_->SetString(prefs::kChromebookPseudonymousId, std::string());
  pref_service_->SetTime(prefs::kPseudonymousIdRotationDate, base::Time());

  ResetSessionId();
}

void PhoneHubStructuredMetricsLogger::ResetSessionId() {
  if (!base::FeatureList::IsEnabled(
          metrics::structured::kPhoneHubStructuredMetrics)) {
    return;
  }
  phone_hub_session_id_ = std::string();
}

void PhoneHubStructuredMetricsLogger::SetChromebookInfo(
    proto::CrosState& cros_state_message) {
  if (!base::FeatureList::IsEnabled(
          metrics::structured::kPhoneHubStructuredMetrics)) {
    return;
  }

  if (!phone_hub_session_id_.empty()) {
    cros_state_message.set_phone_hub_session_id(phone_hub_session_id_);
  }
  if (!pref_service_->GetTime(prefs::kPseudonymousIdRotationDate).is_null()) {
    cros_state_message.set_pseudonymous_id_next_rotation_date(
        pref_service_->GetTime(prefs::kPseudonymousIdRotationDate)
            .InMillisecondsSinceUnixEpoch());
  }
}

void PhoneHubStructuredMetricsLogger::OnNetworkStateListFetched(
    std::vector<chromeos::network_config::mojom::NetworkStatePropertiesPtr>
        networks) {
  for (const auto& network : networks) {
    if (network->type == chromeos::network_config::mojom::NetworkType::kWiFi) {
      std::string hashed_wifi_ssid =
          crypto::SHA256HashString(network->type_state->get_wifi()->ssid);
      if (phone_network_ssid_.has_value() &&
          phone_network_ssid_.value() == hashed_wifi_ssid) {
        network_state_ = NetworkState::kSameNetwork;
      } else {
        network_state_ = NetworkState::kDifferentNetwork;
      }
      UploadDeviceInfo();
      return;
    }
  }
  network_state_ = NetworkState::kDifferentNetwork;
  UploadDeviceInfo();
}

void PhoneHubStructuredMetricsLogger::UploadDeviceInfo() {
  if (!base::FeatureList::IsEnabled(
          metrics::structured::kPhoneHubStructuredMetrics) ||
      phone_hub_session_id_.empty()) {
    return;
  }
  auto metric = ::metrics::structured::events::v2::phone_hub::SessionDetails();
  // Populate chromebook related information
  metric.SetSessionId(phone_hub_session_id_);
  metric.SetTimestamp(
      base::Time::NowFromSystemTime().InMillisecondsSinceUnixEpoch());
  metric.SetConnectionMedium(static_cast<int>(medium_));
  metric.SetChromebookBluetoothStack(static_cast<int>(bluetooth_stack_));
  metric.SetDevicesNetworkState(static_cast<int>(network_state_));
  metric.SetChromebookLocale(chromebook_locale_);
  metric.SetChromebookPseudonymousId(
      pref_service_->GetString(prefs::kChromebookPseudonymousId));

  // Populate connected phone information, if available.
  if (!pref_service_->GetString(prefs::kPhoneManufacturer).empty()) {
    metric.SetPhoneManufacturer(
        pref_service_->GetString(prefs::kPhoneManufacturer));
  }
  if (!pref_service_->GetString(prefs::kPhoneModel).empty()) {
    metric.SetPhoneModel(pref_service_->GetString(prefs::kPhoneModel));
  }
  if (pref_service_->GetInteger(prefs::kPhoneAndroidVersion) != 0) {
    metric.SetPhoneAndroidVersion(
        pref_service_->GetInteger(prefs::kPhoneAndroidVersion));
  }
  if (pref_service_->GetInt64(prefs::kPhoneGmsCoreVersion) != 0) {
    metric.SetPhoneGmsCoreVersion(
        pref_service_->GetInt64(prefs::kPhoneGmsCoreVersion));
  }
  if (pref_service_->GetInt64(prefs::kPhoneAmbientApkVersion) != 0) {
    metric.SetPhoneAmbientApkVersion(
        pref_service_->GetInt64(prefs::kPhoneAmbientApkVersion));
  }
  if (phone_network_status_.has_value()) {
    metric.SetPhoneNetworkStatus(
        static_cast<int>(phone_network_status_.value()));
  }
  if (!pref_service_->GetString(prefs::kPhoneLocale).empty()) {
    metric.SetPhoneLocale(pref_service_->GetString(prefs::kPhoneLocale));
  }
  if (!pref_service_->GetString(prefs::kPhonePseudonymousId).empty()) {
    metric.SetPhonePseudonymousId(
        pref_service_->GetString(prefs::kPhonePseudonymousId));
  }
  if (!pref_service_->GetTime(prefs::kPhoneInfoLastUpdatedTime).is_null()) {
    metric.SetPhoneInfoLastUpdatedTimestamp(
        pref_service_->GetTime(prefs::kPhoneInfoLastUpdatedTime)
            .InMillisecondsSinceUnixEpoch());
  }
  if (pref_service_->GetInteger(prefs::kPhoneProfileType) != -1) {
    metric.SetPhoneProfile(pref_service_->GetInteger(prefs::kPhoneProfileType));
  }
  ::metrics::structured::StructuredMetricsClient::Record(std::move(metric));
}
}  // namespace ash::phonehub