| // 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/ash/components/network/cellular_connection_handler.h" |
| |
| #include "base/bind.h" |
| #include "base/check.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_manager_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/hermes_metrics_util.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_state.h" |
| #include "chromeos/ash/components/network/network_type_pattern.h" |
| |
| namespace ash { |
| namespace { |
| |
| constexpr base::TimeDelta kWaitingForConnectableTimeout = base::Seconds(30); |
| |
| bool CanInitiateShillConnection(const NetworkState* network) { |
| // The network must be part of a Shill profile (i.e., it cannot be a "stub" |
| // network which is not exposed by Shill). |
| if (network->IsNonProfileType()) |
| return false; |
| |
| // The Connectable property must be set to true, indicating that the network |
| // is the active SIM profile in its slot. |
| return network->connectable(); |
| } |
| |
| absl::optional<dbus::ObjectPath> GetEuiccPath(const std::string& eid) { |
| const std::vector<dbus::ObjectPath>& euicc_paths = |
| HermesManagerClient::Get()->GetAvailableEuiccs(); |
| |
| for (const auto& euicc_path : euicc_paths) { |
| HermesEuiccClient::Properties* euicc_properties = |
| HermesEuiccClient::Get()->GetProperties(euicc_path); |
| if (euicc_properties && euicc_properties->eid().value() == eid) |
| return euicc_path; |
| } |
| |
| return absl::nullopt; |
| } |
| |
| absl::optional<dbus::ObjectPath> GetProfilePath(const std::string& eid, |
| const std::string& iccid) { |
| absl::optional<dbus::ObjectPath> euicc_path = GetEuiccPath(eid); |
| if (!euicc_path) |
| return absl::nullopt; |
| |
| HermesEuiccClient::Properties* euicc_properties = |
| HermesEuiccClient::Get()->GetProperties(*euicc_path); |
| if (!euicc_properties) |
| return absl::nullopt; |
| |
| const std::vector<dbus::ObjectPath>& profile_paths = |
| euicc_properties->installed_carrier_profiles().value(); |
| for (const auto& profile_path : profile_paths) { |
| HermesProfileClient::Properties* profile_properties = |
| HermesProfileClient::Get()->GetProperties(profile_path); |
| if (profile_properties && profile_properties->iccid().value() == iccid) |
| return profile_path; |
| } |
| |
| return absl::nullopt; |
| } |
| |
| } // namespace |
| |
| // static |
| absl::optional<std::string> CellularConnectionHandler::ResultToErrorString( |
| PrepareCellularConnectionResult result) { |
| switch (result) { |
| case PrepareCellularConnectionResult::kSuccess: |
| return absl::nullopt; |
| |
| case PrepareCellularConnectionResult::kCouldNotFindNetworkWithIccid: |
| return NetworkConnectionHandler::kErrorNotFound; |
| |
| case PrepareCellularConnectionResult::kInhibitFailed: |
| return NetworkConnectionHandler::kErrorCellularInhibitFailure; |
| |
| case PrepareCellularConnectionResult::kCouldNotFindRelevantEuicc: |
| [[fallthrough]]; |
| case PrepareCellularConnectionResult::kRefreshProfilesFailed: |
| [[fallthrough]]; |
| case PrepareCellularConnectionResult::kCouldNotFindRelevantESimProfile: |
| [[fallthrough]]; |
| case PrepareCellularConnectionResult::kEnableProfileFailed: |
| return NetworkConnectionHandler::kErrorESimProfileIssue; |
| |
| case PrepareCellularConnectionResult::kTimeoutWaitingForConnectable: |
| return NetworkConnectionHandler::kConnectableCellularTimeout; |
| } |
| } |
| |
| CellularConnectionHandler::ConnectionRequestMetadata::ConnectionRequestMetadata( |
| const std::string& iccid, |
| SuccessCallback success_callback, |
| ErrorCallback error_callback) |
| : iccid(iccid), |
| success_callback(std::move(success_callback)), |
| error_callback(std::move(error_callback)) {} |
| |
| CellularConnectionHandler::ConnectionRequestMetadata::ConnectionRequestMetadata( |
| const dbus::ObjectPath& euicc_path, |
| const dbus::ObjectPath& profile_path, |
| std::unique_ptr<CellularInhibitor::InhibitLock> inhibit_lock, |
| SuccessCallback success_callback, |
| ErrorCallback error_callback) |
| : euicc_path(euicc_path), |
| profile_path(profile_path), |
| inhibit_lock(std::move(inhibit_lock)), |
| success_callback(std::move(success_callback)), |
| error_callback(std::move(error_callback)) {} |
| |
| CellularConnectionHandler::ConnectionRequestMetadata:: |
| ~ConnectionRequestMetadata() = default; |
| |
| CellularConnectionHandler::CellularConnectionHandler() = default; |
| |
| CellularConnectionHandler::~CellularConnectionHandler() = default; |
| |
| void CellularConnectionHandler::Init( |
| NetworkStateHandler* network_state_handler, |
| CellularInhibitor* cellular_inhibitor, |
| CellularESimProfileHandler* cellular_esim_profile_handler) { |
| network_state_handler_ = network_state_handler; |
| cellular_inhibitor_ = cellular_inhibitor; |
| cellular_esim_profile_handler_ = cellular_esim_profile_handler; |
| |
| network_state_handler_observer_.Observe(network_state_handler_); |
| } |
| |
| void CellularConnectionHandler::PrepareExistingCellularNetworkForConnection( |
| const std::string& iccid, |
| SuccessCallback success_callback, |
| ErrorCallback error_callback) { |
| request_queue_.push(std::make_unique<ConnectionRequestMetadata>( |
| iccid, std::move(success_callback), std::move(error_callback))); |
| ProcessRequestQueue(); |
| } |
| |
| void CellularConnectionHandler:: |
| PrepareNewlyInstalledCellularNetworkForConnection( |
| const dbus::ObjectPath& euicc_path, |
| const dbus::ObjectPath& profile_path, |
| std::unique_ptr<CellularInhibitor::InhibitLock> inhibit_lock, |
| SuccessCallback success_callback, |
| ErrorCallback error_callback) { |
| request_queue_.push(std::make_unique<ConnectionRequestMetadata>( |
| euicc_path, profile_path, std::move(inhibit_lock), |
| std::move(success_callback), std::move(error_callback))); |
| ProcessRequestQueue(); |
| } |
| |
| void CellularConnectionHandler::NetworkListChanged() { |
| HandleNetworkPropertiesUpdate(); |
| } |
| |
| void CellularConnectionHandler::NetworkPropertiesUpdated( |
| const NetworkState* network) { |
| HandleNetworkPropertiesUpdate(); |
| } |
| |
| void CellularConnectionHandler::NetworkIdentifierTransitioned( |
| const std::string& old_service_path, |
| const std::string& new_service_path, |
| const std::string& old_guid, |
| const std::string& new_guid) { |
| HandleNetworkPropertiesUpdate(); |
| } |
| |
| void CellularConnectionHandler::ProcessRequestQueue() { |
| // No requests to process. |
| if (request_queue_.empty()) |
| return; |
| |
| // A request is already being processed; wait until that one is finished |
| // before processing any additional requests. |
| if (state_ != ConnectionState::kIdle) |
| return; |
| |
| const ConnectionRequestMetadata* current_request = |
| request_queue_.front().get(); |
| |
| // If the request has an ICCID, it is an existing network. Start by checking |
| // the service status. |
| if (current_request->iccid) { |
| TransitionToConnectionState(ConnectionState::kCheckingServiceStatus); |
| CheckServiceStatus(); |
| return; |
| } |
| |
| // Otherwise, this is a newly-installed profile, so we can skip straight to |
| // enabling it. |
| DCHECK(current_request->inhibit_lock); |
| DCHECK(current_request->euicc_path); |
| DCHECK(current_request->profile_path); |
| TransitionToConnectionState(ConnectionState::kEnablingProfile); |
| EnableProfile(); |
| } |
| |
| void CellularConnectionHandler::TransitionToConnectionState( |
| ConnectionState state) { |
| NET_LOG(EVENT) << "CellularConnectionHandler state: " << state_ << " => " |
| << state; |
| state_ = state; |
| } |
| |
| void CellularConnectionHandler::CompleteConnectionAttempt( |
| PrepareCellularConnectionResult result) { |
| DCHECK(state_ != ConnectionState::kIdle); |
| DCHECK(!request_queue_.empty()); |
| |
| base::UmaHistogramEnumeration( |
| "Network.Cellular.PrepareCellularConnection.OperationResult", result); |
| |
| if (timer_.IsRunning()) |
| timer_.Stop(); |
| |
| std::string service_path; |
| const NetworkState* network_state = GetNetworkStateForCurrentOperation(); |
| if (network_state) |
| service_path = network_state->path(); |
| |
| TransitionToConnectionState(ConnectionState::kIdle); |
| std::unique_ptr<ConnectionRequestMetadata> metadata = |
| std::move(request_queue_.front()); |
| request_queue_.pop(); |
| |
| const absl::optional<std::string> error_name = ResultToErrorString(result); |
| |
| if (error_name) { |
| std::move(metadata->error_callback).Run(service_path, *error_name); |
| } else if (service_path.empty()) { |
| std::move(metadata->error_callback) |
| .Run(service_path, NetworkConnectionHandler::kErrorNotFound); |
| } else { |
| std::move(metadata->success_callback).Run(service_path); |
| } |
| |
| ProcessRequestQueue(); |
| |
| // In case of errors, metadata will be destroyed at this point along with |
| // it's inhibit_lock and the cellular device will uninhibit automatically. |
| } |
| |
| const NetworkState* |
| CellularConnectionHandler::GetNetworkStateForCurrentOperation() const { |
| if (request_queue_.empty()) |
| return nullptr; |
| |
| std::string iccid; |
| const ConnectionRequestMetadata* current_request = |
| request_queue_.front().get(); |
| if (current_request->iccid) { |
| iccid = *current_request->iccid; |
| } else { |
| iccid = HermesProfileClient::Get() |
| ->GetProperties(*current_request->profile_path) |
| ->iccid() |
| .value(); |
| } |
| DCHECK(!iccid.empty()); |
| |
| NetworkStateHandler::NetworkStateList network_list; |
| network_state_handler_->GetVisibleNetworkListByType( |
| NetworkTypePattern::Cellular(), &network_list); |
| for (const NetworkState* network : network_list) { |
| if (network->iccid() == iccid) |
| return network; |
| } |
| |
| return nullptr; |
| } |
| |
| absl::optional<dbus::ObjectPath> |
| CellularConnectionHandler::GetEuiccPathForCurrentOperation() const { |
| const ConnectionRequestMetadata* current_request = |
| request_queue_.front().get(); |
| if (current_request->euicc_path) { |
| return current_request->euicc_path; |
| } |
| |
| const NetworkState* network_state = GetNetworkStateForCurrentOperation(); |
| if (!network_state) |
| return absl::nullopt; |
| |
| return GetEuiccPath(network_state->eid()); |
| } |
| |
| absl::optional<dbus::ObjectPath> |
| CellularConnectionHandler::GetProfilePathForCurrentOperation() const { |
| const ConnectionRequestMetadata* current_request = |
| request_queue_.front().get(); |
| if (current_request->profile_path) { |
| return current_request->profile_path; |
| } |
| |
| const NetworkState* network_state = GetNetworkStateForCurrentOperation(); |
| if (!network_state) |
| return absl::nullopt; |
| |
| return GetProfilePath(network_state->eid(), network_state->iccid()); |
| } |
| |
| void CellularConnectionHandler::CheckServiceStatus() { |
| DCHECK_EQ(state_, ConnectionState::kCheckingServiceStatus); |
| |
| const std::string& iccid = *request_queue_.front()->iccid; |
| |
| const NetworkState* network_state = GetNetworkStateForCurrentOperation(); |
| if (!network_state) { |
| NET_LOG(ERROR) << "Could not find network for ICCID " |
| << *request_queue_.front()->iccid; |
| CompleteConnectionAttempt( |
| PrepareCellularConnectionResult::kCouldNotFindNetworkWithIccid); |
| return; |
| } |
| |
| if (CanInitiateShillConnection(network_state)) { |
| NET_LOG(USER) << "Cellular service with ICCID " << iccid |
| << " is connectable"; |
| CompleteConnectionAttempt(PrepareCellularConnectionResult::kSuccess); |
| return; |
| } |
| |
| NET_LOG(USER) << "Starting cellular connection flow. ICCID: " << iccid |
| << ", Service path: " << network_state->path() |
| << ", EID: " << network_state->eid(); |
| |
| // If this is a pSIM network, we expect that Shill will eventually expose a |
| // connectable Service corresponding to this network. Invoking |
| // CheckForConnectable() starts a timeout for this process in case this never |
| // ends up occurring. |
| if (network_state->eid().empty()) { |
| NET_LOG(EVENT) << "Waiting for connectable pSIM network"; |
| TransitionToConnectionState(ConnectionState::kWaitingForConnectable); |
| CheckForConnectable(); |
| return; |
| } |
| |
| DCHECK(!request_queue_.front()->inhibit_lock); |
| TransitionToConnectionState(ConnectionState::kInhibitingScans); |
| cellular_inhibitor_->InhibitCellularScanning( |
| CellularInhibitor::InhibitReason::kConnectingToProfile, |
| base::BindOnce(&CellularConnectionHandler::OnInhibitScanResult, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void CellularConnectionHandler::OnInhibitScanResult( |
| std::unique_ptr<CellularInhibitor::InhibitLock> inhibit_lock) { |
| DCHECK_EQ(state_, ConnectionState::kInhibitingScans); |
| |
| if (!inhibit_lock) { |
| NET_LOG(ERROR) << "eSIM connection flow failed to inhibit scan"; |
| CompleteConnectionAttempt(PrepareCellularConnectionResult::kInhibitFailed); |
| return; |
| } |
| |
| request_queue_.front()->inhibit_lock = std::move(inhibit_lock); |
| TransitionToConnectionState( |
| ConnectionState::kRequestingProfilesBeforeEnabling); |
| RequestInstalledProfiles(); |
| } |
| |
| void CellularConnectionHandler::RequestInstalledProfiles() { |
| absl::optional<dbus::ObjectPath> euicc_path = |
| GetEuiccPathForCurrentOperation(); |
| if (!euicc_path) { |
| NET_LOG(ERROR) << "eSIM connection flow could not find relevant EUICC"; |
| CompleteConnectionAttempt( |
| PrepareCellularConnectionResult::kCouldNotFindRelevantEuicc); |
| return; |
| } |
| |
| cellular_esim_profile_handler_->RefreshProfileList( |
| *euicc_path, |
| base::BindOnce(&CellularConnectionHandler::OnRefreshProfileListResult, |
| weak_ptr_factory_.GetWeakPtr()), |
| std::move(request_queue_.front()->inhibit_lock)); |
| } |
| |
| void CellularConnectionHandler::OnRefreshProfileListResult( |
| std::unique_ptr<CellularInhibitor::InhibitLock> inhibit_lock) { |
| DCHECK_EQ(ConnectionState::kRequestingProfilesBeforeEnabling, state_); |
| |
| if (!inhibit_lock) { |
| NET_LOG(ERROR) << "eSIM connection flow failed to request profiles"; |
| CompleteConnectionAttempt( |
| PrepareCellularConnectionResult::kRefreshProfilesFailed); |
| return; |
| } |
| |
| request_queue_.front()->inhibit_lock = std::move(inhibit_lock); |
| |
| TransitionToConnectionState(ConnectionState::kEnablingProfile); |
| EnableProfile(); |
| } |
| |
| void CellularConnectionHandler::EnableProfile() { |
| absl::optional<dbus::ObjectPath> profile_path = |
| GetProfilePathForCurrentOperation(); |
| if (!profile_path) { |
| NET_LOG(ERROR) << "eSIM connection flow could not find profile"; |
| CompleteConnectionAttempt( |
| PrepareCellularConnectionResult::kCouldNotFindRelevantESimProfile); |
| return; |
| } |
| |
| HermesProfileClient::Get()->EnableCarrierProfile( |
| *profile_path, |
| base::BindOnce(&CellularConnectionHandler::OnEnableCarrierProfileResult, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void CellularConnectionHandler::OnEnableCarrierProfileResult( |
| HermesResponseStatus status) { |
| DCHECK_EQ(state_, ConnectionState::kEnablingProfile); |
| |
| hermes_metrics::LogEnableProfileResult(status); |
| |
| // If we try to enable and "fail" with an already-enabled error, count this as |
| // a success. |
| bool success = status == HermesResponseStatus::kSuccess || |
| status == HermesResponseStatus::kErrorAlreadyEnabled; |
| |
| if (!success) { |
| NET_LOG(ERROR) << "eSIM connection flow failed to enable profile"; |
| CompleteConnectionAttempt( |
| PrepareCellularConnectionResult::kEnableProfileFailed); |
| return; |
| } |
| |
| // Reset the inhibit_lock so that the device will be uninhibited |
| // automatically. |
| request_queue_.front()->inhibit_lock.reset(); |
| TransitionToConnectionState(ConnectionState::kWaitingForConnectable); |
| CheckForConnectable(); |
| } |
| |
| void CellularConnectionHandler::HandleNetworkPropertiesUpdate() { |
| if (state_ == ConnectionState::kWaitingForConnectable) |
| CheckForConnectable(); |
| } |
| |
| void CellularConnectionHandler::CheckForConnectable() { |
| DCHECK_EQ(state_, ConnectionState::kWaitingForConnectable); |
| |
| const NetworkState* network_state = GetNetworkStateForCurrentOperation(); |
| if (network_state && CanInitiateShillConnection(network_state)) { |
| CompleteConnectionAttempt(PrepareCellularConnectionResult::kSuccess); |
| return; |
| } |
| |
| // If network is not connectable or if network state is not available for a |
| // newly installed profile, start a timer and wait for the network to become |
| // available and connectable. |
| if (!timer_.IsRunning()) { |
| timer_.Start( |
| FROM_HERE, kWaitingForConnectableTimeout, |
| base::BindOnce(&CellularConnectionHandler::OnWaitForConnectableTimeout, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| } |
| |
| void CellularConnectionHandler::OnWaitForConnectableTimeout() { |
| DCHECK_EQ(state_, ConnectionState::kWaitingForConnectable); |
| NET_LOG(ERROR) << "Cellular connection timed out waiting for network to " |
| "become connectable"; |
| CompleteConnectionAttempt( |
| PrepareCellularConnectionResult::kTimeoutWaitingForConnectable); |
| } |
| |
| std::ostream& operator<<( |
| std::ostream& stream, |
| const CellularConnectionHandler::ConnectionState& state) { |
| switch (state) { |
| case CellularConnectionHandler::ConnectionState::kIdle: |
| stream << "[Idle]"; |
| break; |
| case CellularConnectionHandler::ConnectionState::kCheckingServiceStatus: |
| stream << "[Checking service status]"; |
| break; |
| case CellularConnectionHandler::ConnectionState::kInhibitingScans: |
| stream << "[Inhibiting scans]"; |
| break; |
| case CellularConnectionHandler::ConnectionState:: |
| kRequestingProfilesBeforeEnabling: |
| stream << "[Requesting profiles before enabling]"; |
| break; |
| case CellularConnectionHandler::ConnectionState::kEnablingProfile: |
| stream << "[Enabling profile]"; |
| break; |
| case CellularConnectionHandler::ConnectionState::kWaitingForConnectable: |
| stream << "[Waiting for network to become connectable]"; |
| break; |
| } |
| return stream; |
| } |
| |
| } // namespace ash |