chromium/chromeos/ash/components/network/cellular_esim_installer.cc

// Copyright 2021 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/cellular_esim_installer.h"

#include <optional>

#include "ash/constants/ash_features.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/notreached.h"
#include "base/time/time.h"
#include "chromeos/ash/components/dbus/hermes/hermes_euicc_client.h"
#include "chromeos/ash/components/dbus/hermes/hermes_profile_client.h"
#include "chromeos/ash/components/dbus/hermes/hermes_response_status.h"
#include "chromeos/ash/components/dbus/shill/shill_manager_client.h"
#include "chromeos/ash/components/network/cellular_connection_handler.h"
#include "chromeos/ash/components/network/cellular_utils.h"
#include "chromeos/ash/components/network/hermes_metrics_util.h"
#include "chromeos/ash/components/network/metrics/cellular_network_metrics_logger.h"
#include "chromeos/ash/components/network/network_connection_handler.h"
#include "chromeos/ash/components/network/network_event_log.h"
#include "chromeos/ash/components/network/network_profile_handler.h"
#include "chromeos/ash/components/network/network_state_handler.h"
#include "chromeos/ash/components/network/network_ui_data.h"
#include "chromeos/ash/components/network/shill_property_util.h"
#include "chromeos/ash/services/cellular_setup/public/mojom/esim_manager.mojom.h"
#include "components/onc/onc_constants.h"
#include "third_party/cros_system_api/dbus/shill/dbus-constants.h"

using ash::cellular_setup::mojom::ProfileInstallMethod;

namespace ash {
namespace {

void AppendRequiredCellularProperties(
    const dbus::ObjectPath& euicc_path,
    const dbus::ObjectPath& profile_path,
    const NetworkProfile* profile,
    base::Value::Dict* shill_properties_to_update) {
  HermesEuiccClient::Properties* euicc_properties =
      HermesEuiccClient::Get()->GetProperties(euicc_path);
  HermesProfileClient::Properties* profile_properties =
      HermesProfileClient::Get()->GetProperties(profile_path);
  shill_properties_to_update->Set(shill::kTypeProperty, shill::kTypeCellular);
  shill_properties_to_update->Set(shill::kProfileProperty, profile->path);
  // Insert ICCID and EID to the shill properties.
  shill_properties_to_update->Set(shill::kIccidProperty,
                                  profile_properties->iccid().value());
  shill_properties_to_update->Set(shill::kEidProperty,
                                  euicc_properties->eid().value());
  // Use ICCID as GUID if no GUID found in the given properties.
  if (!shill_properties_to_update->FindString(shill::kGuidProperty)) {
    shill_properties_to_update->Set(shill::kGuidProperty,
                                    profile_properties->iccid().value());
  }
}

bool IsManagedNetwork(const base::Value::Dict& new_shill_properties) {
  std::unique_ptr<NetworkUIData> ui_data =
      shill_property_util::GetUIDataFromProperties(new_shill_properties);
  return ui_data && (ui_data->onc_source() == ::onc::ONC_SOURCE_DEVICE_POLICY ||
                     ui_data->onc_source() == ::onc::ONC_SOURCE_USER_POLICY);
}

CellularNetworkMetricsLogger::ESimUserInstallMethod ComputeUserInstallMethod(
    ProfileInstallMethod install_method) {
  switch (install_method) {
    case ProfileInstallMethod::kViaSmds:
      return CellularNetworkMetricsLogger::ESimUserInstallMethod::kViaSmds;
    case ProfileInstallMethod::kViaQrCodeAfterSmds:
      return CellularNetworkMetricsLogger::ESimUserInstallMethod::
          kViaQrCodeAfterSmds;
    case ProfileInstallMethod::kViaQrCodeSkippedSmds:
      return CellularNetworkMetricsLogger::ESimUserInstallMethod::
          kViaQrCodeSkippedSmds;
    case ProfileInstallMethod::kViaActivationCodeAfterSmds:
      return CellularNetworkMetricsLogger::ESimUserInstallMethod::
          kViaActivationCodeAfterSmds;
    case ProfileInstallMethod::kViaActivationCodeSkippedSmds:
      return CellularNetworkMetricsLogger::ESimUserInstallMethod::
          kViaActivationCodeSkippedSmds;
  }
  NOTREACHED();
}

CellularNetworkMetricsLogger::ESimPolicyInstallMethod
ComputePolicyInstallMethod(ProfileInstallMethod install_method) {
  switch (install_method) {
    case ProfileInstallMethod::kViaSmds:
      return CellularNetworkMetricsLogger::ESimPolicyInstallMethod::kViaSmds;
    // When installing an eSIM profile via policy we do not have the different
    // methods that are possible via the consumer flow. Default all SM-DP+
    // installation methods to |kViaSmdp|.
    case ProfileInstallMethod::kViaQrCodeAfterSmds:
      [[fallthrough]];
    case ProfileInstallMethod::kViaQrCodeSkippedSmds:
      [[fallthrough]];
    case ProfileInstallMethod::kViaActivationCodeAfterSmds:
      [[fallthrough]];
    case ProfileInstallMethod::kViaActivationCodeSkippedSmds:
      return CellularNetworkMetricsLogger::ESimPolicyInstallMethod::kViaSmdp;
  }
  NOTREACHED();
}

}  // namespace

// static
void CellularESimInstaller::RecordInstallESimProfileResult(
    std::optional<HermesResponseStatus> status,
    bool is_managed,
    bool is_initial_install,
    ProfileInstallMethod install_method) {
  const bool is_user_error =
      status.has_value() &&
      CellularNetworkMetricsLogger::HermesResponseStatusIsUserError(*status);
  const CellularNetworkMetricsLogger::ESimOperationResult result =
      CellularNetworkMetricsLogger::ComputeESimOperationResult(status);

  if (is_managed) {
    CellularNetworkMetricsLogger::LogESimPolicyInstallResult(
        ComputePolicyInstallMethod(install_method), result, is_initial_install,
        is_user_error);
    return;
  }
  CellularNetworkMetricsLogger::LogESimUserInstallResult(
      ComputeUserInstallMethod(install_method), result, is_user_error);
}

CellularESimInstaller::CellularESimInstaller() = default;

CellularESimInstaller::~CellularESimInstaller() = default;

void CellularESimInstaller::Init(
    CellularConnectionHandler* cellular_connection_handler,
    CellularInhibitor* cellular_inhibitor,
    NetworkConnectionHandler* network_connection_handler,
    NetworkProfileHandler* network_profile_handler,
    NetworkStateHandler* network_state_handler) {
  cellular_connection_handler_ = cellular_connection_handler;
  cellular_inhibitor_ = cellular_inhibitor;
  network_connection_handler_ = network_connection_handler;
  network_profile_handler_ = network_profile_handler;
  network_state_handler_ = network_state_handler;
}

void CellularESimInstaller::InstallProfileFromActivationCode(
    const std::string& activation_code,
    const std::string& confirmation_code,
    const dbus::ObjectPath& euicc_path,
    base::Value::Dict new_shill_properties,
    InstallProfileFromActivationCodeCallback callback,
    bool is_initial_install,
    ProfileInstallMethod install_method) {
  cellular_inhibitor_->InhibitCellularScanning(
      CellularInhibitor::InhibitReason::kInstallingProfile,
      base::BindOnce(
          &CellularESimInstaller::PerformInstallProfileFromActivationCode,
          weak_ptr_factory_.GetWeakPtr(), activation_code, confirmation_code,
          euicc_path, std::move(new_shill_properties), is_initial_install,
          install_method, std::move(callback)));
}

void CellularESimInstaller::PerformInstallProfileFromActivationCode(
    const std::string& activation_code,
    const std::string& confirmation_code,
    const dbus::ObjectPath& euicc_path,
    base::Value::Dict new_shill_properties,
    bool is_initial_install,
    ProfileInstallMethod install_method,
    InstallProfileFromActivationCodeCallback callback,
    std::unique_ptr<CellularInhibitor::InhibitLock> inhibit_lock) {
  if (!inhibit_lock) {
    NET_LOG(ERROR) << "Error inhibiting cellular device";

    RecordInstallESimProfileResult(
        /*status=*/std::nullopt, IsManagedNetwork(new_shill_properties),
        is_initial_install, install_method);

    std::move(callback).Run(HermesResponseStatus::kErrorWrongState,
                            /*profile_path=*/std::nullopt,
                            /*service_path=*/std::nullopt);
    return;
  }

  // TODO(crbug.com/1186682) Add a check for activation codes that are currently
  // being installed to prevent multiple attempts for the same activation code.
  NET_LOG(USER) << "Attempting installation with code " << activation_code;

  HermesEuiccClient::Get()->InstallProfileFromActivationCode(
      euicc_path, activation_code, confirmation_code,
      base::BindOnce(&CellularESimInstaller::OnProfileInstallResult,
                     weak_ptr_factory_.GetWeakPtr(), std::move(callback),
                     std::move(inhibit_lock), euicc_path,
                     std::move(new_shill_properties), is_initial_install,
                     install_method, base::Time::Now()));
}

void CellularESimInstaller::OnProfileInstallResult(
    InstallProfileFromActivationCodeCallback callback,
    std::unique_ptr<CellularInhibitor::InhibitLock> inhibit_lock,
    const dbus::ObjectPath& euicc_path,
    const base::Value::Dict& new_shill_properties,
    bool is_initial_install,
    ProfileInstallMethod install_method,
    const base::Time installation_start_time,
    HermesResponseStatus status,
    dbus::DBusResult dbusResult,
    const dbus::ObjectPath* profile_path) {
  hermes_metrics::LogInstallViaQrCodeResult(status, dbusResult,
                                            is_initial_install);

  RecordInstallESimProfileResult(status, IsManagedNetwork(new_shill_properties),
                                 is_initial_install, install_method);

  if (status != HermesResponseStatus::kSuccess) {
    NET_LOG(ERROR) << "Error Installing profile status=" << status;
    std::move(callback).Run(status, /*profile_path=*/std::nullopt,
                            /*service_path=*/std::nullopt);
    return;
  }

  UMA_HISTOGRAM_LONG_TIMES_100(
      "Network.Cellular.ESim.ProfileDownload.ActivationCode.Latency",
      base::Time::Now() - installation_start_time);

  pending_inhibit_locks_.emplace(*profile_path, std::move(inhibit_lock));
  ConfigureESimService(
      new_shill_properties, euicc_path, *profile_path,
      base::BindOnce(&CellularESimInstaller::EnableProfile,
                     weak_ptr_factory_.GetWeakPtr(), std::move(callback),
                     euicc_path, *profile_path));
}

void CellularESimInstaller::ConfigureESimService(
    const base::Value::Dict& new_shill_properties,
    const dbus::ObjectPath& euicc_path,
    const dbus::ObjectPath& profile_path,
    ConfigureESimServiceCallback callback) {
  const NetworkProfile* profile =
      cellular_utils::GetCellularProfile(network_profile_handler_);

  if (!profile) {
    NET_LOG(ERROR)
        << "Error configuring eSIM profile. Default profile not initialized.";
    std::move(callback).Run(
        /*service_path=*/std::nullopt);
    return;
  }

  base::Value::Dict properties_to_set = new_shill_properties.Clone();
  AppendRequiredCellularProperties(euicc_path, profile_path, profile,
                                   &properties_to_set);
  NET_LOG(EVENT)
      << "Creating shill configuration for eSim profile with properties: "
      << properties_to_set;

  auto callback_split = base::SplitOnceCallback(std::move(callback));
  ShillManagerClient::Get()->ConfigureServiceForProfile(
      dbus::ObjectPath(profile->path), properties_to_set,
      base::BindOnce(
          &CellularESimInstaller::OnShillConfigurationCreationSuccess,
          weak_ptr_factory_.GetWeakPtr(), std::move(callback_split.first)),
      base::BindOnce(
          &CellularESimInstaller::OnShillConfigurationCreationFailure,
          weak_ptr_factory_.GetWeakPtr(), std::move(callback_split.second)));
}

void CellularESimInstaller::OnShillConfigurationCreationSuccess(
    ConfigureESimServiceCallback callback,
    const dbus::ObjectPath& service_path) {
  NET_LOG(EVENT)
      << "Successfully creating shill configuration on service path: "
      << service_path.value();
  std::move(callback).Run(service_path);
}

void CellularESimInstaller::OnShillConfigurationCreationFailure(
    ConfigureESimServiceCallback callback,
    const std::string& error_name,
    const std::string& error_message) {
  NET_LOG(ERROR) << "Create shill configuration failed, error:" << error_name
                 << ", message: " << error_message;
  std::move(callback).Run(/*service_path=*/std::nullopt);
}

void CellularESimInstaller::EnableProfile(
    InstallProfileFromActivationCodeCallback callback,
    const dbus::ObjectPath& euicc_path,
    const dbus::ObjectPath& profile_path,
    std::optional<dbus::ObjectPath> service_path) {
  auto it = pending_inhibit_locks_.find(profile_path);
  DCHECK(it != pending_inhibit_locks_.end());

  auto callback_split = base::SplitOnceCallback(std::move(callback));
  cellular_connection_handler_
      ->PrepareNewlyInstalledCellularNetworkForConnection(
          euicc_path, profile_path, std::move(it->second),
          base::BindOnce(&CellularESimInstaller::
                             OnPrepareCellularNetworkForConnectionSuccess,
                         weak_ptr_factory_.GetWeakPtr(), profile_path,
                         std::move(callback_split.first)),
          base::BindOnce(&CellularESimInstaller::
                             OnPrepareCellularNetworkForConnectionFailure,
                         weak_ptr_factory_.GetWeakPtr(), profile_path,
                         std::move(callback_split.second)));
  pending_inhibit_locks_.erase(it);
}

void CellularESimInstaller::OnPrepareCellularNetworkForConnectionSuccess(
    const dbus::ObjectPath& profile_path,
    InstallProfileFromActivationCodeCallback callback,
    const std::string& service_path,
    bool auto_connected) {
  NET_LOG(EVENT) << "Successfully enabled installed profile on service path: "
                 << service_path;
  const NetworkState* network_state =
      network_state_handler_->GetNetworkState(service_path);
  if (!network_state) {
    HandleNewProfileEnableFailure(std::move(callback), profile_path,
                                  service_path,
                                  NetworkConnectionHandler::kErrorNotFound);
    return;
  }

  if (!network_state->IsConnectingOrConnected()) {
    // The connection could fail but the user will be notified of connection
    // failures separately.
    network_connection_handler_->ConnectToNetwork(
        service_path,
        /*success_callback=*/base::DoNothing(),
        /*error_callback=*/base::DoNothing(),
        /*check_error_state=*/false, ConnectCallbackMode::ON_STARTED);
  }

  std::move(callback).Run(HermesResponseStatus::kSuccess, profile_path,
                          service_path);
}

void CellularESimInstaller::OnPrepareCellularNetworkForConnectionFailure(
    const dbus::ObjectPath& profile_path,
    InstallProfileFromActivationCodeCallback callback,
    const std::string& service_path,
    const std::string& error_name) {
  HandleNewProfileEnableFailure(std::move(callback), profile_path, service_path,
                                error_name);
}

void CellularESimInstaller::HandleNewProfileEnableFailure(
    InstallProfileFromActivationCodeCallback callback,
    const dbus::ObjectPath& profile_path,
    const std::string& service_path,
    const std::string& error_name) {
  NET_LOG(ERROR) << "Error enabling newly created profile path="
                 << profile_path.value() << ", service path=" << service_path
                 << ", error_name=" << error_name;
  // Propagate |profile_path| and |service_path| so that the code that
  // initiated the installation can handle the case where the profile was
  // successfully installed, but the installation process failed for some
  // other reason e.g. failed to enable the profile.
  std::move(callback).Run(HermesResponseStatus::kErrorWrongState,
                          /*profile_path=*/profile_path,
                          /*service_path=*/service_path);
}

}  // namespace ash