chromium/chromeos/ash/components/network/cellular_esim_uninstall_handler.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_uninstall_handler.h"

#include <optional>

#include "base/containers/flat_set.h"
#include "base/metrics/histogram_functions.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/network/cellular_esim_profile_handler.h"
#include "chromeos/ash/components/network/cellular_inhibitor.h"
#include "chromeos/ash/components/network/device_state.h"
#include "chromeos/ash/components/network/hermes_metrics_util.h"
#include "chromeos/ash/components/network/managed_cellular_pref_handler.h"
#include "chromeos/ash/components/network/network_configuration_handler.h"
#include "chromeos/ash/components/network/network_connection_handler.h"
#include "chromeos/ash/components/network/network_handler.h"
#include "chromeos/ash/components/network/network_state.h"
#include "components/device_event_log/device_event_log.h"
#include "third_party/cros_system_api/dbus/hermes/dbus-constants.h"

namespace ash {

// static
const base::TimeDelta CellularESimUninstallHandler::kNetworkListWaitTimeout =
    base::Seconds(20);

CellularESimUninstallHandler::UninstallRequest::UninstallRequest(
    const std::optional<std::string>& iccid,
    const std::optional<dbus::ObjectPath>& esim_profile_path,
    const std::optional<dbus::ObjectPath>& euicc_path,
    bool reset_euicc,
    UninstallRequestCallback callback)
    : iccid(iccid),
      esim_profile_path(esim_profile_path),
      euicc_path(euicc_path),
      reset_euicc(reset_euicc),
      callback(std::move(callback)) {}
CellularESimUninstallHandler::UninstallRequest::~UninstallRequest() = default;

CellularESimUninstallHandler::CellularESimUninstallHandler() = default;
CellularESimUninstallHandler::~CellularESimUninstallHandler() {
  OnShuttingDown();
}

void CellularESimUninstallHandler::Init(
    CellularInhibitor* cellular_inhibitor,
    CellularESimProfileHandler* cellular_esim_profile_handler,
    ManagedCellularPrefHandler* managed_cellular_pref_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;
  managed_cellular_pref_handler_ = managed_cellular_pref_handler;
  network_configuration_handler_ = network_configuration_handler;
  network_connection_handler_ = network_connection_handler;
  network_state_handler_ = network_state_handler;

  network_state_handler_observer_.Observe(network_state_handler_.get());
}

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, /*reset_euicc=*/false,
      std::move(callback)));
  ProcessPendingUninstallRequests();
}

void CellularESimUninstallHandler::ResetEuiccMemory(
    const dbus::ObjectPath& euicc_path,
    UninstallRequestCallback callback) {
  uninstall_requests_.push_back(std::make_unique<UninstallRequest>(
      /*iccid=*/std::nullopt, /*esim_profile_path=*/std::nullopt, euicc_path,
      /*reset_euicc=*/true, std::move(callback)));
  ProcessPendingUninstallRequests();
}

void CellularESimUninstallHandler::NetworkListChanged() {
  if (state_ != UninstallState::kWaitingForNetworkListUpdate) {
    return;
  }
  // When removing multiple eSIM network services back to back after a Reset
  // EUICC, uninstall handler will be in waiting state till next network list
  // update before removing next configuration.
  network_list_wait_timer_.Stop();
  TransitionToUninstallState(UninstallState::kRemovingShillService);
  AttemptRemoveShillService();
}

void CellularESimUninstallHandler::OnShuttingDown() {
  network_state_handler_observer_.Reset();
  network_state_handler_ = nullptr;
}

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 for request "
                 << *uninstall_requests_.front();
  TransitionToUninstallState(UninstallState::kCheckingNetworkState);
  CheckActiveNetworkState();
}

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 "
                 << *uninstall_requests_.front() << ". Success = " << success;
  std::move(uninstall_requests_.front()->callback).Run(success);
  last_service_count_removal_for_testing_ =
      uninstall_requests_.front()->removed_service_paths.size();
  uninstall_requests_.pop_front();

  TransitionToUninstallState(UninstallState::kIdle);
  ProcessPendingUninstallRequests();
}

const NetworkState*
CellularESimUninstallHandler::GetNetworkStateForCurrentRequest() const {
  std::optional<std::string> current_request_iccid =
      uninstall_requests_.front()->iccid;

  if (!current_request_iccid) {
    return nullptr;
  }

  for (auto* const network : GetESimCellularNetworks()) {
    if (network->iccid() == current_request_iccid) {
      return network;
    }
  }

  return nullptr;
}

void CellularESimUninstallHandler::CheckActiveNetworkState() {
  DCHECK_EQ(state_, UninstallState::kCheckingNetworkState);

  const NetworkState* network = network_state_handler_->ActiveNetworkByType(
      NetworkTypePattern::Cellular());

  // If the network is connected, disconnect it before we attempt to uninstall
  // eSIM profiles.
  if (network && 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) {
  DCHECK_EQ(state_, UninstallState::kDisconnectingNetwork);

  NET_LOG(ERROR) << "Failed disconnecting network for request "
                 << *uninstall_requests_.front();
  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 request "
                   << *uninstall_requests_.front();
    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 request "
        << *uninstall_requests_.front();
    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);
  std::optional<dbus::ObjectPath> esim_profile_path;
  if (uninstall_requests_.front()->reset_euicc) {
    esim_profile_path = GetEnabledCellularESimProfilePath();
    // Skip disabling profile if there are no enabled profiles.
    if (!esim_profile_path) {
      TransitionToUninstallState(UninstallState::kUninstallingProfile);
      AttemptUninstallProfile();
      return;
    }
  } else {
    esim_profile_path = uninstall_requests_.front()->esim_profile_path;
  }
  HermesProfileClient::Get()->DisableCarrierProfile(
      *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 request "
                   << *uninstall_requests_.front();
    CompleteCurrentRequest(UninstallESimResult::kDisableProfileFailed);
    return;
  }

  TransitionToUninstallState(UninstallState::kUninstallingProfile);
  AttemptUninstallProfile();
}

void CellularESimUninstallHandler::AttemptUninstallProfile() {
  DCHECK_EQ(state_, UninstallState::kUninstallingProfile);

  if (uninstall_requests_.front()->reset_euicc) {
    base::flat_set<std::string> iccids =
        GetAllIccidsOnEuicc(*uninstall_requests_.front()->euicc_path);
    HermesEuiccClient::Get()->ResetMemory(
        *uninstall_requests_.front()->euicc_path,
        hermes::euicc::ResetOptions::kDeleteOperationalProfiles,
        base::BindOnce(&CellularESimUninstallHandler::OnUninstallProfile,
                       weak_ptr_factory_.GetWeakPtr(), iccids));
    return;
  }

  base::flat_set<std::string> iccids{*uninstall_requests_.front()->iccid};
  HermesEuiccClient::Get()->UninstallProfile(
      *uninstall_requests_.front()->euicc_path,
      *uninstall_requests_.front()->esim_profile_path,
      base::BindOnce(&CellularESimUninstallHandler::OnUninstallProfile,
                     weak_ptr_factory_.GetWeakPtr(), iccids));
}

void CellularESimUninstallHandler::OnUninstallProfile(
    const base::flat_set<std::string>& removed_iccids,
    HermesResponseStatus status) {
  DCHECK_EQ(state_, UninstallState::kUninstallingProfile);

  if (!uninstall_requests_.front()->reset_euicc) {
    hermes_metrics::LogUninstallProfileResult(status);
  }

  if (status != HermesResponseStatus::kSuccess) {
    NET_LOG(ERROR) << "Failed to uninstall profile for request "
                   << *uninstall_requests_.front();
    CompleteCurrentRequest(UninstallESimResult::kUninstallProfileFailed);
    return;
  }

  if (managed_cellular_pref_handler_) {
    for (const auto& iccid : removed_iccids) {
      managed_cellular_pref_handler_->RemoveESimMetadata(iccid);
    }
  }
  TransitionToUninstallState(UninstallState::kRemovingShillService);
  AttemptRemoveShillService();
}

void CellularESimUninstallHandler::AttemptRemoveShillService() {
  DCHECK_EQ(state_, UninstallState::kRemovingShillService);

  const NetworkState* network = nullptr;
  if (uninstall_requests_.front()->reset_euicc) {
    network = GetNextResetServiceToRemove();
    if (!network) {
      CompleteCurrentRequest(UninstallESimResult::kSuccess);
      return;
    }
  } else {
    network = GetNetworkStateForCurrentRequest();
    if (!network) {
      NET_LOG(ERROR) << "Unable to find eSIM network for request "
                     << *uninstall_requests_.front();
      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;
    }
  }

  NET_LOG(EVENT) << "Attempting to remove Shill service for network: "
                 << network->path();
  network_configuration_handler_->RemoveConfiguration(
      network->path(), std::nullopt,
      base::BindOnce(&CellularESimUninstallHandler::OnRemoveServiceSuccess,
                     weak_ptr_factory_.GetWeakPtr(), network->path()),
      base::BindOnce(&CellularESimUninstallHandler::OnRemoveServiceFailure,
                     weak_ptr_factory_.GetWeakPtr()));
}

void CellularESimUninstallHandler::OnRemoveServiceSuccess(
    const std::string& removed_service_path) {
  DCHECK_EQ(state_, UninstallState::kRemovingShillService);
  uninstall_requests_.front()->removed_service_paths.insert(
      removed_service_path);

  if (uninstall_requests_.front()->reset_euicc) {
    // Wait for next network list update before removing the next shill service.
    TransitionToUninstallState(UninstallState::kWaitingForNetworkListUpdate);
    network_list_wait_timer_.Start(
        FROM_HERE, kNetworkListWaitTimeout,
        base::BindOnce(&CellularESimUninstallHandler::OnNetworkListWaitTimeout,
                       weak_ptr_factory_.GetWeakPtr()));
    return;
  }

  CompleteCurrentRequest(UninstallESimResult::kSuccess);
}

void CellularESimUninstallHandler::OnRemoveServiceFailure(
    const std::string& error_name) {
  DCHECK_EQ(state_, UninstallState::kRemovingShillService);
  NET_LOG(ERROR) << "Error removing service for request "
                 << *uninstall_requests_.front() << ". Error: " << error_name;
  CompleteCurrentRequest(UninstallESimResult::kRemoveServiceFailed);
}

void CellularESimUninstallHandler::OnNetworkListWaitTimeout() {
  DCHECK_EQ(state_, UninstallState::kWaitingForNetworkListUpdate);
  NET_LOG(ERROR)
      << "Timedout waiting for network list update after removing service.";
  TransitionToUninstallState(UninstallState::kRemovingShillService);
  AttemptRemoveShillService();
}

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;
}

std::optional<dbus::ObjectPath>
CellularESimUninstallHandler::GetEnabledCellularESimProfilePath() {
  for (const auto& esim_profile :
       cellular_esim_profile_handler_->GetESimProfiles()) {
    if (esim_profile.state() == CellularESimProfile::State::kActive) {
      return esim_profile.path();
    }
  }
  return std::nullopt;
}

base::flat_set<std::string> CellularESimUninstallHandler::GetAllIccidsOnEuicc(
    const dbus::ObjectPath& euicc_path) {
  HermesEuiccClient::Properties* euicc_properties =
      HermesEuiccClient::Get()->GetProperties(euicc_path);
  const std::string& eid = euicc_properties->eid().value();
  base::flat_set<std::string> iccids;
  for (const auto& esim_profile :
       cellular_esim_profile_handler_->GetESimProfiles()) {
    if (esim_profile.eid() == eid) {
      iccids.emplace(esim_profile.iccid());
    }
  }
  return iccids;
}

const NetworkState* CellularESimUninstallHandler::GetNextResetServiceToRemove()
    const {
  HermesEuiccClient::Properties* euicc_properties =
      HermesEuiccClient::Get()->GetProperties(
          *uninstall_requests_.front()->euicc_path);
  const std::string& eid = euicc_properties->eid().value();
  for (const NetworkState* network : GetESimCellularNetworks()) {
    // Non Shill cellular services cannot be removed. They'll be automatically
    // removed when eSIM profile list updates.
    if (network->IsNonShillCellularNetwork()) {
      continue;
    }

    // b/249825186: When the success callback of
    // NetworkConfigurationHandler::RemoveConfiguration() is called on eSIM
    // cellular shill service(s), the service may be still be exposed as it
    // is still in the process of guaranteed destruction. Chrome relies on Shill
    // pushing an update to the kServiceCompleteList property, which it should
    // eventually, but removing a profile entry is a disk operation so it won't
    // be instantaneous. Successful
    // NetworkConfigurationHandler::RemoveConfiguration() calls to these
    // services should be ignored, as these loadable profile entries no
    // longer exist.
    if (uninstall_requests_.front()->removed_service_paths.contains(
            network->path())) {
      continue;
    }
    if (network->eid() == eid)
      return network;
  }
  return nullptr;
}

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;
    case CellularESimUninstallHandler::UninstallState::
        kWaitingForNetworkListUpdate:
      stream << "[Waiting for network list update]";
      break;
  }
  return stream;
}

std::ostream& operator<<(
    std::ostream& stream,
    const CellularESimUninstallHandler::UninstallRequest& request) {
  if (request.reset_euicc) {
    stream << "(ResetEuicc)";
  } else {
    stream << "(ICCID: " << *request.iccid << ")";
  }
  return stream;
}

}  // namespace ash