chromium/chromeos/ash/components/network/metrics/hotspot_metrics_helper.cc

// 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.

#include "chromeos/ash/components/network/metrics/hotspot_metrics_helper.h"

#include "base/metrics/histogram_functions.h"
#include "chromeos/ash/components/login/login_state/login_state.h"
#include "chromeos/ash/components/network/enterprise_managed_metadata_store.h"
#include "chromeos/ash/components/network/hotspot_configuration_handler.h"
#include "chromeos/ash/components/network/hotspot_controller.h"
#include "chromeos/ash/components/network/network_event_log.h"

namespace ash {

// static
const base::TimeDelta HotspotMetricsHelper::kLogAllowStatusAtLoginTimeout =
    base::Seconds(30);

// static
const char HotspotMetricsHelper::kHotspotAllowStatusHistogram[] =
    "Network.Ash.Hotspot.Upstream.Cellular.Capability.AllowStatus";

// static
const char HotspotMetricsHelper::kHotspotAllowStatusAtLoginHistogram[] =
    "Network.Ash.Hotspot.Upstream.Cellular.Capability.AllowStatusAtLogin";

// static
const char HotspotMetricsHelper::kHotspotEnableResultHistogram[] =
    "Network.Ash.Hotspot.Upstream.Cellular.EnableHotspot.OperationResult";

// static
const char HotspotMetricsHelper::kHotspotDisableResultHistogram[] =
    "Network.Ash.Hotspot.Upstream.Cellular.DisableHotspot.OperationResult";

// static
const char HotspotMetricsHelper::kHotspotSetConfigResultHistogram[] =
    "Network.Ash.Hotspot.SetConfig.OperationResult";

// static
const char HotspotMetricsHelper::kHotspotCheckReadinessResultHistogram[] =
    "Network.Ash.Hotspot.Upstream.Cellular.CheckReadiness.OperationResult";

// static
const char HotspotMetricsHelper::kHotspotUsageConfigAutoDisable[] =
    "Network.Ash.Hotspot.Upstream.Cellular.Usage.Config.AutoDisable";

// static
const char HotspotMetricsHelper::kHotspotUsageConfigMAR[] =
    "Network.Ash.Hotspot.Upstream.Cellular.Usage.Config.MAR";

// static
const char HotspotMetricsHelper::kHotspotUsageConfigCompatibilityMode[] =
    "Network.Ash.Hotspot.Upstream.Cellular.Usage.Config.CompatibilityMode";

// static
const char HotspotMetricsHelper::kHotspotUsageDuration[] =
    "Network.Ash.Hotspot.Upstream.Cellular.Usage.Duration";

// static
const char HotspotMetricsHelper::kHotspotMaxClientCount[] =
    "Network.Ash.Hotspot.Upstream.Cellular.Usage.MaxConnectedDeviceCount";

// static
const char HotspotMetricsHelper::kHotspotIsDeviceManaged[] =
    "Network.Ash.Hotspot.Upstream.Cellular.Usage."
    "ManagedStateWhenHotspotEnabled";

// static
const char HotspotMetricsHelper::kHotspotEnableLatency[] =
    "Network.Ash.Hotspot.Upstream.Cellular.EnableHotspot.Latency";

// static
const char HotspotMetricsHelper::kHotspotUpstreamStatusWhenEnabled[] =
    "Network.Ash.Hotspot.Upstream.Cellular.Enabled.UpstreamStatus";

// static
const char HotspotMetricsHelper::kHotspotDisableReasonHistogram[] =
    "Network.Ash.Hotspot.Upstream.Cellular.Disabled.Reason";

// static
void HotspotMetricsHelper::RecordSetTetheringEnabledResult(
    bool enabled,
    hotspot_config::mojom::HotspotControlResult result) {
  HotspotMetricsSetEnabledResult metrics_result =
      GetSetEnabledMetricsResult(result);
  if (enabled) {
    base::UmaHistogramEnumeration(kHotspotEnableResultHistogram,
                                  metrics_result);
    return;
  }
  base::UmaHistogramEnumeration(kHotspotDisableResultHistogram, metrics_result);
}

// static
void HotspotMetricsHelper::RecordCheckTetheringReadinessResult(
    HotspotCapabilitiesProvider::CheckTetheringReadinessResult result) {
  base::UmaHistogramEnumeration(kHotspotCheckReadinessResultHistogram,
                                GetCheckReadinessMetricsResult(result));
}

// static
void HotspotMetricsHelper::RecordSetHotspotConfigResult(
    hotspot_config::mojom::SetHotspotConfigResult result,
    const std::string& shill_error) {
  base::UmaHistogramEnumeration(kHotspotSetConfigResultHistogram,
                                GetSetConfigMetricsResult(result, shill_error));
}

// static
void HotspotMetricsHelper::RecordEnableHotspotLatency(
    const base::TimeDelta& latency) {
  base::UmaHistogramMediumTimes(kHotspotEnableLatency, latency);
}

// static
HotspotMetricsHelper::HotspotMetricsSetEnabledResult
HotspotMetricsHelper::GetSetEnabledMetricsResult(
    const hotspot_config::mojom::HotspotControlResult& result) {
  using hotspot_config::mojom::HotspotControlResult;

  switch (result) {
    case HotspotControlResult::kSuccess:
      return HotspotMetricsSetEnabledResult::kSuccess;
    case HotspotControlResult::kNotAllowed:
      return HotspotMetricsSetEnabledResult::kNotAllowed;
    case HotspotControlResult::kReadinessCheckFailed:
      return HotspotMetricsSetEnabledResult::kReadinessCheckFailure;
    case HotspotControlResult::kDisableWifiFailed:
      return HotspotMetricsSetEnabledResult::kDisableWifiFailure;
    case HotspotControlResult::kInvalidConfiguration:
      return HotspotMetricsSetEnabledResult::kInvalidConfiguration;
    case HotspotControlResult::kUpstreamNotAvailable:
      return HotspotMetricsSetEnabledResult::kUpstreamNotAvailable;
    case HotspotControlResult::kNetworkSetupFailure:
      return HotspotMetricsSetEnabledResult::kNetworkSetupFailure;
    case HotspotControlResult::kDownstreamWifiFailure:
      return HotspotMetricsSetEnabledResult::kDownstreamWifiFailure;
    case HotspotControlResult::kUpstreamFailure:
      return HotspotMetricsSetEnabledResult::kUpstreamFailure;
    case HotspotControlResult::kShillOperationFailed:
      return HotspotMetricsSetEnabledResult::kShillOperationFailure;
    case HotspotControlResult::kAlreadyFulfilled:
      return HotspotMetricsSetEnabledResult::kAlreadyFulfilled;
    case HotspotControlResult::kAborted:
      return HotspotMetricsSetEnabledResult::kAborted;
    case HotspotControlResult::kInvalid:
      return HotspotMetricsSetEnabledResult::kInvalid;
    default:
      return HotspotMetricsSetEnabledResult::kUnknownFailure;
  }
}

// static
HotspotMetricsHelper::HotspotMetricsCheckReadinessResult
HotspotMetricsHelper::GetCheckReadinessMetricsResult(
    const HotspotCapabilitiesProvider::CheckTetheringReadinessResult& result) {
  using CheckReadinessResult =
      HotspotCapabilitiesProvider::CheckTetheringReadinessResult;

  switch (result) {
    case CheckReadinessResult::kReady:
      return HotspotMetricsCheckReadinessResult::kReady;
    case CheckReadinessResult::kNotAllowedByCarrier:
      return HotspotMetricsCheckReadinessResult::kNotAllowedByCarrier;
    case CheckReadinessResult::kNotAllowedOnFW:
      return HotspotMetricsCheckReadinessResult::kNotAllowedOnFW;
    case CheckReadinessResult::kNotAllowedOnVariant:
      return HotspotMetricsCheckReadinessResult::kNotAllowedOnVariant;
    case CheckReadinessResult::kNotAllowedUserNotEntitled:
      return HotspotMetricsCheckReadinessResult::kNotAllowedUserNotEntitled;
    case CheckReadinessResult::kNotAllowed:
      return HotspotMetricsCheckReadinessResult::kNotAllowed;
    case CheckReadinessResult::kUpstreamNetworkNotAvailable:
      return HotspotMetricsCheckReadinessResult::kUpstreamNetworkNotAvailable;
    case CheckReadinessResult::kShillOperationFailed:
      return HotspotMetricsCheckReadinessResult::kShillOperationFailed;
    case CheckReadinessResult::kUnknownResult:
      return HotspotMetricsCheckReadinessResult::kUnknownResult;
  }
  NOTREACHED_IN_MIGRATION() << "Unknown check tethering readiness result.";
}

// static
HotspotMetricsHelper::HotspotMetricsSetConfigResult
HotspotMetricsHelper::GetSetConfigMetricsResult(
    const hotspot_config::mojom::SetHotspotConfigResult& result,
    const std::string& shill_error) {
  using hotspot_config::mojom::SetHotspotConfigResult;

  switch (result) {
    case SetHotspotConfigResult::kSuccess:
      return HotspotMetricsSetConfigResult::kSuccess;
    case SetHotspotConfigResult::kFailedNotLogin:
      return HotspotMetricsSetConfigResult::kFailedNotLogin;
    case SetHotspotConfigResult::kFailedInvalidConfiguration:
      return HotspotMetricsSetConfigResult::kFailedInvalidConfiguration;
    case SetHotspotConfigResult::kFailedShillOperation:
      if (shill_error == shill::kErrorResultInvalidArguments) {
        return HotspotMetricsSetConfigResult::kFailedInvalidArgument;
      } else if (shill_error == shill::kErrorResultIllegalOperation) {
        return HotspotMetricsSetConfigResult::kFailedIllegalOperation;
      } else if (shill_error == shill::kErrorResultPermissionDenied) {
        return HotspotMetricsSetConfigResult::kFailedPermissionDenied;
      } else if (shill_error == shill::kErrorResultFailure) {
        return HotspotMetricsSetConfigResult::kFailedShillOperation;
      }
      return HotspotMetricsSetConfigResult::kFailedUnknownShillError;
  }
  NOTREACHED_IN_MIGRATION() << "Unknown set hotspot config result.";
}

// static
HotspotMetricsHelper::HotspotMetricsDisableReason
HotspotMetricsHelper::GetMetricsDisableReason(
    const hotspot_config::mojom::DisableReason& reason) {
  using hotspot_config::mojom::DisableReason;

  switch (reason) {
    case DisableReason::kAutoDisabled:
      return HotspotMetricsDisableReason::kAutoDisabled;
    case DisableReason::kInternalError:
      return HotspotMetricsDisableReason::kInternalError;
    case DisableReason::kUserInitiated:
      return HotspotMetricsDisableReason::kUserInitiated;
    case DisableReason::kWifiEnabled:
      return HotspotMetricsDisableReason::kWifiEnabled;
    case DisableReason::kProhibitedByPolicy:
      return HotspotMetricsDisableReason::kProhibitedByPolicy;
    case DisableReason::kUpstreamNetworkNotAvailable:
      return HotspotMetricsDisableReason::kUpstreamNetworkNotAvailable;
    case DisableReason::kSuspended:
      return HotspotMetricsDisableReason::kSuspended;
    case DisableReason::kRestart:
      return HotspotMetricsDisableReason::kRestart;
    case DisableReason::kUpstreamNoInternet:
      return HotspotMetricsDisableReason::kUpstreamNoInternet;
    case DisableReason::kDownstreamLinkDisconnect:
      return HotspotMetricsDisableReason::kDownstreamLinkDisconnect;
    case DisableReason::kDownstreamNetworkDisconnect:
      return HotspotMetricsDisableReason::kDownstreamNetworkDisconnect;
    case DisableReason::kStartTimeout:
      return HotspotMetricsDisableReason::kStartTimeout;
    case DisableReason::kUpstreamNotAvailable:
      return HotspotMetricsDisableReason::kUpstreamNotAvailable;
    case DisableReason::kUnknownError:
      return HotspotMetricsDisableReason::kUnknownError;
  }
  NOTREACHED_IN_MIGRATION() << "Unknown hotspot disable reason.";
}

HotspotMetricsHelper::HotspotMetricsHelper() = default;

HotspotMetricsHelper::~HotspotMetricsHelper() {
  // Log related metrics, namely usage duration and max connected client count
  // if the user logout while hotspot is active.
  if (is_hotspot_active_) {
    LogUsageDuration();
    LogMaxClientCount();
  }
  if (hotspot_capabilities_provider_ &&
      hotspot_capabilities_provider_->HasObserver(this)) {
    hotspot_capabilities_provider_->RemoveObserver(this);
  }
  if (hotspot_state_handler_ && hotspot_state_handler_->HasObserver(this)) {
    hotspot_state_handler_->RemoveObserver(this);
  }

  if (LoginState::IsInitialized()) {
    LoginState::Get()->RemoveObserver(this);
  }
}

void HotspotMetricsHelper::Init(
    EnterpriseManagedMetadataStore* enterprise_managed_metadata_store,
    HotspotCapabilitiesProvider* hotspot_capabilities_provider,
    HotspotStateHandler* hotspot_state_handler,
    HotspotController* hotspot_controller,
    HotspotConfigurationHandler* hotspot_configuration_handler,
    HotspotEnabledStateNotifier* hotspot_enabled_state_notifier,
    NetworkStateHandler* network_state_handler) {
  enterprise_managed_metadata_store_ = enterprise_managed_metadata_store;
  hotspot_state_handler_ = hotspot_state_handler;
  hotspot_state_handler_->AddObserver(this);
  hotspot_capabilities_provider_ = hotspot_capabilities_provider;
  hotspot_capabilities_provider_->AddObserver(this);
  hotspot_configuration_handler_ = hotspot_configuration_handler;
  hotspot_enabled_state_notifier_ = hotspot_enabled_state_notifier;
  network_state_handler_ = network_state_handler;

  if (LoginState::IsInitialized()) {
    LoginState::Get()->AddObserver(this);
    LoggedInStateChanged();
  }

  hotspot_enabled_state_notifier->ObserveEnabledStateChanges(
      hotspot_enabled_state_notifier_receiver_.BindNewPipeAndPassRemote());
}

void HotspotMetricsHelper::OnHotspotCapabilitiesChanged() {
  LogAllowStatus();
}

void HotspotMetricsHelper::LoggedInStateChanged() {
  if (!LoginState::Get()->IsUserLoggedIn()) {
    timer_.Stop();
    is_metrics_logged_ = false;
    return;
  }

  timer_.Start(FROM_HERE, kLogAllowStatusAtLoginTimeout, this,
               &HotspotMetricsHelper::LogAllowStatusAtLogin);
}

void HotspotMetricsHelper::LogAllowStatus() {
  std::optional<HotspotMetricsAllowStatus> metrics_allow_status =
      GetMetricsAllowStatus();
  if (!metrics_allow_status) {
    return;
  }

  base::UmaHistogramEnumeration(kHotspotAllowStatusHistogram,
                                *metrics_allow_status);
}

void HotspotMetricsHelper::LogAllowStatusAtLogin() {
  if (is_metrics_logged_) {
    return;
  }

  std::optional<HotspotMetricsAllowStatus> metrics_allow_status =
      GetMetricsAllowStatus();
  if (!metrics_allow_status) {
    return;
  }

  base::UmaHistogramEnumeration(kHotspotAllowStatusAtLoginHistogram,
                                *metrics_allow_status);
  is_metrics_logged_ = true;
}

std::optional<HotspotMetricsHelper::HotspotMetricsAllowStatus>
HotspotMetricsHelper::GetMetricsAllowStatus() {
  using hotspot_config::mojom::HotspotAllowStatus;

  HotspotAllowStatus allow_status =
      hotspot_capabilities_provider_->GetHotspotCapabilities().allow_status;

  switch (allow_status) {
    case HotspotAllowStatus::kDisallowedNoWiFiDownstream:
      return HotspotMetricsAllowStatus::kDisallowedWiFiDownstreamNotSupported;
    case HotspotAllowStatus::kDisallowedNoWiFiSecurityModes:
      return HotspotMetricsAllowStatus::kDisallowedNoWiFiSecurityModes;
    case HotspotAllowStatus::kDisallowedNoMobileData:
      return HotspotMetricsAllowStatus::kDisallowedNoMobileData;
    case HotspotAllowStatus::kDisallowedReadinessCheckFail:
      return HotspotMetricsAllowStatus::kDisallowedReadinessCheckFail;
    case HotspotAllowStatus::kDisallowedByPolicy:
      return HotspotMetricsAllowStatus::kDisallowedByPolicy;
    case HotspotAllowStatus::kAllowed:
      return HotspotMetricsAllowStatus::kAllowed;
    case HotspotAllowStatus::kDisallowedNoCellularUpstream:
      // Do not emit kDisallowedNoCellularUpstream which means the device is
      // not cellular capable. Otherwise, it would drown out the metric.
      return std::nullopt;
  }
}

void HotspotMetricsHelper::LogUsageConfig() {
  auto hotspot_config = hotspot_configuration_handler_->GetHotspotConfig();
  if (!hotspot_config) {
    NET_LOG(ERROR) << "Error getting hotspot config when hotspot is turned on.";
    return;
  }
  base::UmaHistogramBoolean(kHotspotUsageConfigAutoDisable,
                            hotspot_config->auto_disable);
  base::UmaHistogramBoolean(
      kHotspotUsageConfigCompatibilityMode,
      hotspot_config->band == hotspot_config::mojom::WiFiBand::k2_4GHz);
  base::UmaHistogramBoolean(kHotspotUsageConfigMAR,
                            hotspot_config->bssid_randomization);
}

void HotspotMetricsHelper::LogUsageDuration() {
  if (!usage_timer_) {
    NET_LOG(ERROR) << "Hotspot usage timer has not been started.";
    return;
  }
  const base::TimeDelta usage_duration = usage_timer_->Elapsed();
  base::UmaHistogramLongTimes(kHotspotUsageDuration, usage_duration);
}

void HotspotMetricsHelper::LogMaxClientCount() {
  base::UmaHistogramCounts100(kHotspotMaxClientCount, max_client_count_);
}

void HotspotMetricsHelper::LogIsDeviceManaged() {
  bool is_enterprise_managed =
      enterprise_managed_metadata_store_->is_enterprise_managed();
  base::UmaHistogramBoolean(kHotspotIsDeviceManaged, is_enterprise_managed);
}

void HotspotMetricsHelper::LogUpstreamStatus() {
  const NetworkState* connected_cellular_network =
      network_state_handler_->ConnectedNetworkByType(
          NetworkTypePattern::Cellular());
  if (!connected_cellular_network) {
    base::UmaHistogramEnumeration(
        kHotspotUpstreamStatusWhenEnabled,
        HotspotMetricsUpstreamStatus::kWifiWithCellularNotConnected);
    return;
  }
  base::UmaHistogramEnumeration(
      kHotspotUpstreamStatusWhenEnabled,
      HotspotMetricsUpstreamStatus::kWifiWithCellularConnected);
}

void HotspotMetricsHelper::LogDisableReason(
    const hotspot_config::mojom::DisableReason& reason) {
  base::UmaHistogramEnumeration(kHotspotDisableReasonHistogram,
                                GetMetricsDisableReason(reason));
}

void HotspotMetricsHelper::OnHotspotTurnedOn() {
  is_hotspot_active_ = true;
  LogUpstreamStatus();
  LogUsageConfig();
  LogIsDeviceManaged();

  usage_timer_ = base::ElapsedTimer();
  max_client_count_ = 0;
}

void HotspotMetricsHelper::OnHotspotTurnedOff(
    hotspot_config::mojom::DisableReason reason) {
  is_hotspot_active_ = false;
  LogDisableReason(reason);
  LogUsageDuration();
  LogMaxClientCount();
  max_client_count_ = 0;
}

void HotspotMetricsHelper::OnHotspotStatusChanged() {
  size_t client_count = hotspot_state_handler_->GetHotspotActiveClientCount();
  max_client_count_ = std::max(max_client_count_, client_count);
}

}  // namespace ash