chromium/chromeos/ash/components/network/cellular_policy_handler.h

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

#ifndef CHROMEOS_ASH_COMPONENTS_NETWORK_CELLULAR_POLICY_HANDLER_H_
#define CHROMEOS_ASH_COMPONENTS_NETWORK_CELLULAR_POLICY_HANDLER_H_

#include <optional>

#include "base/component_export.h"
#include "base/containers/queue.h"
#include "base/gtest_prod_util.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/scoped_observation.h"
#include "base/timer/timer.h"
#include "base/values.h"
#include "chromeos/ash/components/network/cellular_esim_profile_handler.h"
#include "chromeos/ash/components/network/network_state_handler.h"
#include "chromeos/ash/components/network/network_state_handler_observer.h"
#include "chromeos/ash/components/network/policy_util.h"
#include "net/base/backoff_entry.h"

namespace dbus {
class ObjectPath;
}  // namespace dbus

namespace ash {

class CellularESimInstaller;
class CellularInhibitor;
class NetworkProfileHandler;
class NetworkStateHandler;
class ManagedCellularPrefHandler;
class ManagedNetworkConfigurationHandler;
enum class HermesResponseStatus;

// This class encapsulates the logic for installing eSIM profiles configured by
// policy. Installation requests are added to a queue, and each request will be
// retried a fixed number of times with a retry delay between each attempt.
class COMPONENT_EXPORT(CHROMEOS_NETWORK) CellularPolicyHandler
    : public HermesManagerClient::Observer,
      public CellularESimProfileHandler::Observer,
      public NetworkStateHandlerObserver {
 public:
  CellularPolicyHandler();
  CellularPolicyHandler(const CellularPolicyHandler&) = delete;
  CellularPolicyHandler& operator=(const CellularPolicyHandler&) = delete;
  ~CellularPolicyHandler() override;

  void Init(CellularESimProfileHandler* cellular_esim_profile_handler,
            CellularESimInstaller* cellular_esim_installer,
            CellularInhibitor* cellular_inhibitor,
            NetworkProfileHandler* network_profile_handler,
            NetworkStateHandler* network_state_handler,
            ManagedCellularPrefHandler* managed_cellular_pref_handler,
            ManagedNetworkConfigurationHandler*
                managed_network_configuration_handler);

  // Installs the policy eSIM profile defined in |onc_config|. The Shill service
  // configuration will be updated to match the GUID provided by |onc_config|
  // and to include the ICCID of the installed profile. Installations are
  // performed using a queue, and each installation will be retried a fix number
  // of times.
  void InstallESim(const base::Value::Dict& onc_config);

 private:
  // This enum allows us to treat a retry differently depending on what the
  // reason for retrying is.
  enum class InstallRetryReason {
    kMissingNonCellularConnectivity = 0,
    kInternalError = 1,
    kUserError = 2,
    kOther = 3,
  };

  // HermesUserErrorCodes indicate errors made by the user. These can be due
  // to bad input or a valid input that has already been successfully processed.
  // In such errors, we do not attempt to retry.
  const std::array<HermesResponseStatus, 4> kHermesUserErrorCodes = {
      HermesResponseStatus::kErrorAlreadyDisabled,
      HermesResponseStatus::kErrorAlreadyEnabled,
      HermesResponseStatus::kErrorInvalidActivationCode,
      HermesResponseStatus::kErrorInvalidIccid};

  // HermesInternalErrorCodes indicate system failure during the installation
  // process. These error can happen due to code bugs or reasons unrelated to
  // user input. In these cases, we retry using an exponental backoff policy to
  // attempt the installation again.
  const std::array<HermesResponseStatus, 7> kHermesInternalErrorCodes = {
      HermesResponseStatus::kErrorUnknown,
      HermesResponseStatus::kErrorInternalLpaFailure,
      HermesResponseStatus::kErrorWrongState,
      HermesResponseStatus::kErrorSendApduFailure,
      HermesResponseStatus::kErrorUnexpectedModemManagerState,
      HermesResponseStatus::kErrorModemMessageProcessing,
      HermesResponseStatus::kErrorPendingProfile};

  friend class CellularPolicyHandlerTest;

  // Represents policy eSIM install request parameters. Requests are queued and
  // processed one at a time. |activation_code| represents the SM-DP+ activation
  // code that will be used to install the eSIM profile, and |onc_config| is the
  // ONC configuration of the cellular policy.
  struct InstallPolicyESimRequest {
    InstallPolicyESimRequest(policy_util::SmdxActivationCode activation_code,
                             const base::Value::Dict& onc_config);
    InstallPolicyESimRequest(const InstallPolicyESimRequest&) = delete;
    InstallPolicyESimRequest& operator=(const InstallPolicyESimRequest&) =
        delete;
    ~InstallPolicyESimRequest();

    const policy_util::SmdxActivationCode activation_code;
    base::Value::Dict onc_config;
    net::BackoffEntry retry_backoff;
  };

  // HermesManagerClient::Observer:
  void OnAvailableEuiccListChanged() override;

  // CellularESimProfileHandler::Observer:
  void OnESimProfileListUpdated() override;

  // NetworkStateHandlerObserver:
  void DeviceListChanged() override;
  void OnShuttingDown() override;

  // These functions implement the functionality necessary to interact with the
  // queue of policy eSIM installation requests.
  void ResumeInstallIfNeeded();
  void ProcessRequests();
  void ScheduleRetryAndProcessRequests(
      std::unique_ptr<InstallPolicyESimRequest> request,
      InstallRetryReason reason);
  void PushRequestAndProcess(std::unique_ptr<InstallPolicyESimRequest> request);
  void PopRequest();
  void PopAndProcessRequests();

  // Attempts to install the first request in the queue. This function is
  // responsible for ensuring that both a cellular device and Hermes are
  // available. Further, this function will also refresh the list of installed
  // eSIM profiles so that we can properly determine whether an eSIM profile has
  // already been installed for the request.
  void AttemptInstallESim();

  // Actually responsible for kicking off the installation process. This
  // function will configure the Shill service that corresponds to the profile
  // that will be installed, and will ensure that we have non-cellular internet
  // connectivity.
  void PerformInstallESim(const dbus::ObjectPath& euicc_path,
                          base::Value::Dict new_shill_properties);

  void OnRefreshProfileList(
      const dbus::ObjectPath& euicc_path,
      base::Value::Dict new_shill_properties,
      std::unique_ptr<CellularInhibitor::InhibitLock> inhibit_lock);
  void OnConfigureESimService(std::optional<dbus::ObjectPath> service_path);
  void OnInhibitedForRefreshSmdxProfiles(
      const dbus::ObjectPath& euicc_path,
      base::Value::Dict new_shill_properties,
      std::unique_ptr<CellularInhibitor::InhibitLock> inhibit_lock);
  void OnRefreshSmdxProfiles(
      const dbus::ObjectPath& euicc_path,
      base::Value::Dict new_shill_properties,
      std::unique_ptr<CellularInhibitor::InhibitLock> inhibit_lock,
      base::TimeTicks start_time,
      HermesResponseStatus status,
      const std::vector<dbus::ObjectPath>& profile_paths);
  void CompleteRefreshSmdxProfiles(
      const dbus::ObjectPath& euicc_path,
      base::Value::Dict new_shill_properties,
      std::unique_ptr<CellularInhibitor::InhibitLock> inhibit_lock,
      HermesResponseStatus status,
      const std::vector<dbus::ObjectPath>& profile_paths);
  void OnESimProfileInstallAttemptComplete(
      HermesResponseStatus hermes_status,
      std::optional<dbus::ObjectPath> profile_path,
      std::optional<std::string> service_path);
  void OnWaitTimeout();

  base::Value::Dict GetNewShillProperties();
  const policy_util::SmdxActivationCode& GetCurrentActivationCode() const;
  std::optional<dbus::ObjectPath> FindExistingMatchingESimProfile(
      const std::string& iccid);
  // Return std::nullopt if no or empty iccid is found in the policy ONC.
  std::optional<std::string> GetIccidFromPolicyONC();
  bool HasNonCellularInternetConnectivity();
  InstallRetryReason HermesResponseStatusToRetryReason(
      HermesResponseStatus status) const;

  raw_ptr<CellularESimProfileHandler> cellular_esim_profile_handler_ = nullptr;
  raw_ptr<CellularESimInstaller> cellular_esim_installer_ = nullptr;
  raw_ptr<CellularInhibitor> cellular_inhibitor_ = nullptr;
  raw_ptr<NetworkProfileHandler> network_profile_handler_ = nullptr;
  raw_ptr<NetworkStateHandler> network_state_handler_ = nullptr;
  base::ScopedObservation<NetworkStateHandler, NetworkStateHandlerObserver>
      network_state_handler_observer_{this};
  raw_ptr<ManagedCellularPrefHandler, DanglingUntriaged>
      managed_cellular_pref_handler_ = nullptr;
  raw_ptr<ManagedNetworkConfigurationHandler, DanglingUntriaged>
      managed_network_configuration_handler_ = nullptr;

  bool is_installing_ = false;

  // While Hermes is the source of truth for the EUICC state, Chrome maintains a
  // cache of the installed eSIM profiles. To ensure we properly detect when a
  // profile has already been installed for a particular request we force a
  // refresh of the profile cache before each installation.
  bool need_refresh_profile_list_ = true;

  base::circular_deque<std::unique_ptr<InstallPolicyESimRequest>>
      remaining_install_requests_;

  base::OneShotTimer wait_timer_;

  base::ScopedObservation<HermesManagerClient, HermesManagerClient::Observer>
      hermes_observation_{this};
  base::ScopedObservation<CellularESimProfileHandler,
                          CellularESimProfileHandler::Observer>
      cellular_esim_profile_handler_observation_{this};

  base::WeakPtrFactory<CellularPolicyHandler> weak_ptr_factory_{this};
};

}  // namespace ash

#endif  // CHROMEOS_ASH_COMPONENTS_NETWORK_CELLULAR_POLICY_HANDLER_H_