text: // Copyright 2021 The Chromium Authors. All rights reserved.
  // Use of this source code is governed by a BSD-style license that can be
  // found in the LICENSE file.
  
  #include "chromeos/network/cellular_esim_uninstall_handler.h"
  
  #include "base/containers/flat_set.h"
  #include "base/metrics/histogram_functions.h"
  #include "chromeos/dbus/hermes/hermes_euicc_client.h"
  #include "chromeos/dbus/hermes/hermes_profile_client.h"
  #include "chromeos/network/cellular_esim_profile_handler.h"
  #include "chromeos/network/cellular_inhibitor.h"
  #include "chromeos/network/device_state.h"
  #include "chromeos/network/hermes_metrics_util.h"
  #include "chromeos/network/network_configuration_handler.h"
  #include "chromeos/network/network_connection_handler.h"
  #include "chromeos/network/network_handler.h"
  #include "chromeos/network/network_state.h"
  #include "components/device_event_log/device_event_log.h"
  #include "third_party/abseil-cpp/absl/types/optional.h"
  #include "third_party/cros_system_api/dbus/hermes/dbus-constants.h"
  
  namespace chromeos {
  
  CellularESimUninstallHandler::UninstallRequest::UninstallRequest(
      const std::string& iccid,
      const absl::optional<dbus::ObjectPath>& esim_profile_path,
      const absl::optional<dbus::ObjectPath>& euicc_path,
      UninstallRequestCallback callback)
      : iccid(iccid),
        esim_profile_path(esim_profile_path),
        euicc_path(euicc_path),
        callback(std::move(callback)) {}
  CellularESimUninstallHandler::UninstallRequest::~UninstallRequest() = default;
  
  CellularESimUninstallHandler::CellularESimUninstallHandler() = default;
  CellularESimUninstallHandler::~CellularESimUninstallHandler() = default;
  
  void CellularESimUninstallHandler::Init(
      CellularInhibitor* cellular_inhibitor,
      CellularESimProfileHandler* cellular_esim_profile_handler,
      NetworkConfigurationHandler* network_configuration_handler,
      NetworkConnectionHandler* network_connection_handler,
      NetworkStateHandler* network_state_handler) {
    cellular_inhibitor_ = cellular_inhibitor;
    cellular_esim_profile_handler_ = cellular_esim_profile_handler;
    network_configuration_handler_ = network_configuration_handler;
    network_connection_handler_ = network_connection_handler;
    network_state_handler_ = network_state_handler;
  }
  
  void CellularESimUninstallHandler::UninstallESim(
      const std::string& iccid,
      const dbus::ObjectPath& esim_profile_path,
      const dbus::ObjectPath& euicc_path,
      UninstallRequestCallback callback) {
    uninstall_requests_.push_back(std::make_unique<UninstallRequest>(
        iccid, esim_profile_path, euicc_path, std::move(callback)));
    ProcessPendingUninstallRequests();
  }
  
  void CellularESimUninstallHandler::ProcessPendingUninstallRequests() {
    // No requests to process.
    if (uninstall_requests_.empty())
      return;
  
    // Another uninstall request is in progress. Do not process a new request
    // until the previous one has completed
    if (state_ != UninstallState::kIdle)
      return;
  
    NET_LOG(EVENT) << "Starting eSIM uninstall. ICCID: "
                   << GetIccidForCurrentRequest();
    TransitionToUninstallState(UninstallState::kCheckingNetworkState);
    CheckNetworkState();
  }
  
  void CellularESimUninstallHandler::TransitionToUninstallState(
      UninstallState next_state) {
    NET_LOG(EVENT) << "CellularESimUninstallHandler state: " << state_ << " => "
                   << next_state;
    state_ = next_state;
  }
  
  void CellularESimUninstallHandler::CompleteCurrentRequest(
      UninstallESimResult result) {
    DCHECK(state_ != UninstallState::kIdle);
  
    base::UmaHistogramEnumeration(
        "Network.Cellular.ESim.UninstallProfile.OperationResult", result);
  
    const bool success = result == UninstallESimResult::kSuccess;
    NET_LOG(EVENT) << "Completed uninstall request for ICCID "
                   << GetIccidForCurrentRequest() << ". Success = " << success;
    std::move(uninstall_requests_.front()->callback).Run(success);
    uninstall_requests_.pop_front();
  
    TransitionToUninstallState(UninstallState::kIdle);
    ProcessPendingUninstallRequests();
  }
  
  const std::string& CellularESimUninstallHandler::GetIccidForCurrentRequest()
      const {
    return uninstall_requests_.front()->iccid;
  }
  
  const NetworkState*
  CellularESimUninstallHandler::GetNetworkStateForCurrentRequest() const {
    for (auto* const network : GetESimCellularNetworks()) {
      if (network->iccid() == GetIccidForCurrentRequest()) {
        return network;
      }
    }
  
    return nullptr;
  }
  
  void CellularESimUninstallHandler::CheckNetworkState() {
    DCHECK_EQ(state_, UninstallState::kCheckingNetworkState);
  
    const NetworkState* network = GetNetworkStateForCurrentRequest();
    if (!network) {
      NET_LOG(ERROR) << "Unable to find eSIM network with ICCID "
                     << GetIccidForCurrentRequest();
      CompleteCurrentRequest(UninstallESimResult::kNetworkNotFound);
      return;
    }
  
    // If there is no profile path in the request then this is a stale service.
    // Skip directly to configuration removal.
    if (!uninstall_requests_.front()->esim_profile_path) {
      TransitionToUninstallState(UninstallState::kRemovingShillService);
      AttemptRemoveShillService();
      return;
    }
  
    // If the network is connected, disconnect it before we attempt to uninstall
    // the associated profile.
    if (network->IsConnectedState()) {
      TransitionToUninstallState(UninstallState::kDisconnectingNetwork);
      AttemptNetworkDisconnect(network);
      return;
    }
  
    TransitionToUninstallState(UninstallState::kInhibitingShill);
    AttemptShillInhibit();
  }
  
  void CellularESimUninstallHandler::AttemptNetworkDisconnect(
      const NetworkState* network) {
    DCHECK_EQ(state_, UninstallState::kDisconnectingNetwork);
  
    network_connection_handler_->DisconnectNetwork(
        network->path(),
        base::BindOnce(&CellularESimUninstallHandler::OnDisconnectSuccess,
                       weak_ptr_factory_.GetWeakPtr()),
        base::BindOnce(&CellularESimUninstallHandler::OnDisconnectFailure,
                       weak_ptr_factory_.GetWeakPtr()));
  }
  
  void CellularESimUninstallHandler::OnDisconnectSuccess() {
    DCHECK_EQ(state_, UninstallState::kDisconnectingNetwork);
  
    TransitionToUninstallState(UninstallState::kInhibitingShill);
    AttemptShillInhibit();
  }
  
  void CellularESimUninstallHandler::OnDisconnectFailure(
      const std::string& error_name,
      std::unique_ptr<base::DictionaryValue> error_data) {
    DCHECK_EQ(state_, UninstallState::kDisconnectingNetwork);
  
    NET_LOG(ERROR) << "Failed disconnecting network with ICCID "
                   << GetIccidForCurrentRequest();
    CompleteCurrentRequest(UninstallESimResult::kDisconnectFailed);
  }
  
  void CellularESimUninstallHandler::AttemptShillInhibit() {
    DCHECK_EQ(state_, UninstallState::kInhibitingShill);
  
    cellular_inhibitor_->InhibitCellularScanning(
        CellularInhibitor::InhibitReason::kRemovingProfile,
        base::BindOnce(&CellularESimUninstallHandler::OnShillInhibit,
                       weak_ptr_factory_.GetWeakPtr()));
  }
  
  void CellularESimUninstallHandler::OnShillInhibit(
      std::unique_ptr<CellularInhibitor::InhibitLock> inhibit_lock) {
    DCHECK_EQ(state_, UninstallState::kInhibitingShill);
  
    if (!inhibit_lock) {
      NET_LOG(ERROR) << "Error inhbiting Shill during uninstall for ICCID "
                     << GetIccidForCurrentRequest();
      CompleteCurrentRequest(UninstallESimResult::kInhibitFailed);
      return;
    }
  
    // Save lock in the uninstall request so that it will be released when the
    // request is popped.
    uninstall_requests_.front()->inhibit_lock = std::move(inhibit_lock);
  
    TransitionToUninstallState(UninstallState::kRequestingInstalledProfiles);
    AttemptRequestInstalledProfiles();
  }
  
  void CellularESimUninstallHandler::AttemptRequestInstalledProfiles() {
    DCHECK_EQ(state_, UninstallState::kRequestingInstalledProfiles);
  
    cellular_esim_profile_handler_->RefreshProfileList(
        *uninstall_requests_.front()->euicc_path,
        base::BindOnce(&CellularESimUninstallHandler::OnRefreshProfileListResult,
                       weak_ptr_factory_.GetWeakPtr()),
        std::move(uninstall_requests_.front()->inhibit_lock));
  }
  
  void CellularESimUninstallHandler::OnRefreshProfileListResult(
      std::unique_ptr<CellularInhibitor::InhibitLock> inhibit_lock) {
    DCHECK_EQ(state_, UninstallState::kRequestingInstalledProfiles);
  
    if (!inhibit_lock) {
      NET_LOG(ERROR) << "Error refreshing profile list during uninstall for "
                     << "ICCID " << GetIccidForCurrentRequest();
      CompleteCurrentRequest(UninstallESimResult::kRefreshProfilesFailed);
      return;
    }
  
    // Save lock back to the uninstall request since we will continue to perform
    // additional eSIM operations.
    uninstall_requests_.front()->inhibit_lock = std::move(inhibit_lock);
  
    TransitionToUninstallState(UninstallState::kDisablingProfile);
    AttemptDisableProfile();
  }
  
  void CellularESimUninstallHandler::AttemptDisableProfile() {
    DCHECK_EQ(state_, UninstallState::kDisablingProfile);
    HermesProfileClient::Get()->DisableCarrierProfile(
        *uninstall_requests_.front()->esim_profile_path,
        base::BindOnce(&CellularESimUninstallHandler::OnDisableProfile,
                       weak_ptr_factory_.GetWeakPtr()));
  }
  
  void CellularESimUninstallHandler::OnDisableProfile(
      HermesResponseStatus status) {
    DCHECK_EQ(state_, UninstallState::kDisablingProfile);
  
    hermes_metrics::LogDisableProfileResult(status);
  
    bool success = status == HermesResponseStatus::kSuccess ||
                   status == HermesResponseStatus::kErrorAlreadyDisabled;
    if (!success) {
      NET_LOG(ERROR) << "Failed to disable profile for ICCID "
                     << GetIccidForCurrentRequest();
      CompleteCurrentRequest(UninstallESimResult::kDisableProfileFailed);
      return;
    }
  
    TransitionToUninstallState(UninstallState::kUninstallingProfile);
    AttemptUninstallProfile();
  }
  
  void CellularESimUninstallHandler::AttemptUninstallProfile() {
    DCHECK_EQ(state_, UninstallState::kUninstallingProfile);
  
    HermesEuiccClient::Get()->UninstallProfile(
        *uninstall_requests_.front()->euicc_path,
        *uninstall_requests_.front()->esim_profile_path,
        base::BindOnce(&CellularESimUninstallHandler::OnUninstallProfile,
                       weak_ptr_factory_.GetWeakPtr()));
  }
  
  void CellularESimUninstallHandler::OnUninstallProfile(
      HermesResponseStatus status) {
    DCHECK_EQ(state_, UninstallState::kUninstallingProfile);
  
    hermes_metrics::LogUninstallProfileResult(status);
  
    if (status != HermesResponseStatus::kSuccess) {
      NET_LOG(ERROR) << "Failed to uninstall profile for ICCID "
                     << GetIccidForCurrentRequest();
      CompleteCurrentRequest(UninstallESimResult::kUninstallProfileFailed);
      return;
    }
  
    TransitionToUninstallState(UninstallState::kRemovingShillService);
    AttemptRemoveShillService();
  }
  
  void CellularESimUninstallHandler::AttemptRemoveShillService() {
    DCHECK_EQ(state_, UninstallState::kRemovingShillService);
  
    const NetworkState* network = GetNetworkStateForCurrentRequest();
    if (!network) {
      NET_LOG(ERROR) << "Unable to find eSIM network with ICCID "
                     << GetIccidForCurrentRequest();
      CompleteCurrentRequest(UninstallESimResult::kRemoveServiceFailed);
      return;
    }
  
    // Return success immediately for non-shill eSIM cellular networks since we
    // don't know the actual shill service path. This stub non-shill service will
    // be removed automatically when the eSIM profile list updates.
    if (network->IsNonShillCellularNetwork()) {
      CompleteCurrentRequest(UninstallESimResult::kSuccess);
      return;
    }
  
    network_configuration_handler_->RemoveConfiguration(
        network->path(), absl::nullopt,
        base::BindOnce(&CellularESimUninstallHandler::OnRemoveServiceSuccess,
                       weak_ptr_factory_.GetWeakPtr()),
        base::BindOnce(&CellularESimUninstallHandler::OnRemoveServiceFailure,
                       weak_ptr_factory_.GetWeakPtr()));
  }
  
  void CellularESimUninstallHandler::OnRemoveServiceSuccess() {
    DCHECK_EQ(state_, UninstallState::kRemovingShillService);
    CompleteCurrentRequest(UninstallESimResult::kSuccess);
  }
  
  void CellularESimUninstallHandler::OnRemoveServiceFailure(
      const std::string& error_name,
      std::unique_ptr<base::DictionaryValue> error_data) {
    DCHECK_EQ(state_, UninstallState::kRemovingShillService);
    NET_LOG(ERROR) << "Error removing service with ICCID "
                   << GetIccidForCurrentRequest() << ". Error: " << error_name;
    CompleteCurrentRequest(UninstallESimResult::kRemoveServiceFailed);
  }
  
  NetworkStateHandler::NetworkStateList
  CellularESimUninstallHandler::GetESimCellularNetworks() const {
    NetworkStateHandler::NetworkStateList network_list;
    network_state_handler_->GetNetworkListByType(
        NetworkTypePattern::Cellular(), /*configured_only=*/false,
        /*visible_only=*/false, /*limit=*/0, &network_list);
  
    for (auto iter = network_list.begin(); iter != network_list.end();) {
      const NetworkState* network_state = *iter;
      if (network_state->eid().empty()) {
        iter = network_list.erase(iter);
      } else {
        iter++;
      }
    }
    return network_list;
  }
  
  bool CellularESimUninstallHandler::HasQueuedRequest(
      const std::string& iccid) const {
    const auto iter = std::find_if(
        uninstall_requests_.begin(), uninstall_requests_.end(),
        [&](const std::unique_ptr<UninstallRequest>& uninstall_request) {
          return uninstall_request->iccid == iccid;
        });
    return iter != uninstall_requests_.end();
  }
  
  std::ostream& operator<<(
      std::ostream& stream,
      const CellularESimUninstallHandler::UninstallState& state) {
    switch (state) {
      case CellularESimUninstallHandler::UninstallState::kIdle:
        stream << "[Idle]";
        break;
      case CellularESimUninstallHandler::UninstallState::kCheckingNetworkState:
        stream << "[Checking network state]";
        break;
      case CellularESimUninstallHandler::UninstallState::kInhibitingShill:
        stream << "[Inhibiting Shill]";
        break;
      case CellularESimUninstallHandler::UninstallState::
          kRequestingInstalledProfiles:
        stream << "[Requesting Installed Profiles]";
        break;
      case CellularESimUninstallHandler::UninstallState::kDisconnectingNetwork:
        stream << "[Disconnecting Network]";
        break;
      case CellularESimUninstallHandler::UninstallState::kDisablingProfile:
        stream << "[Disabling Profile]";
        break;
      case CellularESimUninstallHandler::UninstallState::kUninstallingProfile:
        stream << "[Uninstalling Profile]";
        break;
      case CellularESimUninstallHandler::UninstallState::kRemovingShillService:
        stream << "[Removing Shill Service]";
        break;
    }
    return stream;
  }
  
  }  // namespace chromeos