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