// Copyright 2020 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/phonehub/tether_controller_impl.h"
#include "chromeos/ash/components/multidevice/logging/logging.h"
#include "chromeos/ash/components/phonehub/phone_status_model.h"
#include "chromeos/ash/components/phonehub/user_action_recorder.h"
#include "chromeos/ash/components/phonehub/util/histogram_util.h"
#include "chromeos/ash/services/network_config/in_process_instance.h"
namespace ash {
namespace phonehub {
namespace {
using ::chromeos::network_config::mojom::ConnectionStateType;
using ::chromeos::network_config::mojom::DeviceStatePropertiesPtr;
using ::chromeos::network_config::mojom::FilterType;
using ::chromeos::network_config::mojom::NetworkType;
using ::chromeos::network_config::mojom::StartConnectResult;
using multidevice_setup::MultiDeviceSetupClient;
using multidevice_setup::mojom::Feature;
using multidevice_setup::mojom::FeatureState;
} // namespace
TetherControllerImpl::TetherNetworkConnector::TetherNetworkConnector() {
network_config::BindToInProcessInstance(
cros_network_config_.BindNewPipeAndPassReceiver());
}
TetherControllerImpl::TetherNetworkConnector::~TetherNetworkConnector() =
default;
void TetherControllerImpl::TetherNetworkConnector::StartConnect(
const std::string& guid,
StartConnectCallback callback) {
cros_network_config_->StartConnect(guid, std::move(callback));
}
void TetherControllerImpl::TetherNetworkConnector::StartDisconnect(
const std::string& guid,
StartDisconnectCallback callback) {
cros_network_config_->StartDisconnect(guid, std::move(callback));
}
void TetherControllerImpl::TetherNetworkConnector::GetNetworkStateList(
chromeos::network_config::mojom::NetworkFilterPtr filter,
GetNetworkStateListCallback callback) {
cros_network_config_->GetNetworkStateList(std::move(filter),
std::move(callback));
}
TetherControllerImpl::TetherControllerImpl(
PhoneModel* phone_model,
UserActionRecorder* user_action_recorder,
MultiDeviceSetupClient* multidevice_setup_client)
: TetherControllerImpl(
phone_model,
user_action_recorder,
multidevice_setup_client,
std::make_unique<TetherControllerImpl::TetherNetworkConnector>()) {}
TetherControllerImpl::TetherControllerImpl(
PhoneModel* phone_model,
UserActionRecorder* user_action_recorder,
MultiDeviceSetupClient* multidevice_setup_client,
std::unique_ptr<TetherControllerImpl::TetherNetworkConnector> connector)
: phone_model_(phone_model),
user_action_recorder_(user_action_recorder),
multidevice_setup_client_(multidevice_setup_client),
connector_(std::move(connector)) {
// Receive updates when devices (e.g., Tether, Ethernet, Wi-Fi) go on/offline
// This class only cares about Tether devices.
network_config::BindToInProcessInstance(
cros_network_config_.BindNewPipeAndPassReceiver());
cros_network_config_->AddObserver(receiver_.BindNewPipeAndPassRemote());
phone_model_->AddObserver(this);
multidevice_setup_client_->AddObserver(this);
// Compute current status.
status_ = ComputeStatus();
// Load the current tether network if it exists.
FetchVisibleTetherNetwork();
}
TetherControllerImpl::~TetherControllerImpl() {
phone_model_->RemoveObserver(this);
multidevice_setup_client_->RemoveObserver(this);
}
TetherController::Status TetherControllerImpl::GetStatus() const {
PA_LOG(VERBOSE) << __func__ << ": status = " << status_;
return status_;
}
void TetherControllerImpl::ScanForAvailableConnection() {
if (status_ != Status::kConnectionUnavailable) {
PA_LOG(WARNING) << "Received request to scan for available connection, but "
<< "a scan cannot be performed because the current status "
<< "is " << status_;
return;
}
PA_LOG(INFO) << "Scanning for available connection.";
cros_network_config_->RequestNetworkScan(NetworkType::kTether);
}
void TetherControllerImpl::AttemptConnection() {
if (status_ != Status::kConnectionUnavailable &&
status_ != Status::kConnectionAvailable) {
PA_LOG(WARNING) << "Received request to attempt a connection, but a "
<< "connection cannot be attempted because the current "
<< "status is " << status_;
return;
}
PA_LOG(INFO) << "Attempting connection; current status is " << status_;
user_action_recorder_->RecordTetherConnectionAttempt();
util::LogTetherConnectionResult(
util::TetherConnectionResult::kAttemptConnection);
is_attempting_connection_ = true;
FeatureState feature_state =
multidevice_setup_client_->GetFeatureState(Feature::kInstantTethering);
if (feature_state == FeatureState::kEnabledByUser) {
PerformConnectionAttempt();
return;
}
// The Tethering feature was disabled and must be enabled first, before a
// connection attempt can be made.
DCHECK(feature_state == FeatureState::kDisabledByUser);
AttemptTurningOnTethering();
}
void TetherControllerImpl::AttemptTurningOnTethering() {
SetConnectDisconnectStatus(
ConnectDisconnectStatus::kTurningOnInstantTethering);
multidevice_setup_client_->SetFeatureEnabledState(
Feature::kInstantTethering,
/*enabled=*/true,
/*auth_token=*/std::nullopt,
base::BindOnce(&TetherControllerImpl::OnSetFeatureEnabled,
weak_ptr_factory_.GetWeakPtr()));
}
void TetherControllerImpl::OnSetFeatureEnabled(bool success) {
if (connect_disconnect_status_ !=
ConnectDisconnectStatus::kTurningOnInstantTethering) {
return;
}
if (success) {
PerformConnectionAttempt();
return;
}
PA_LOG(WARNING) << "Failed to enable InstantTethering";
SetConnectDisconnectStatus(ConnectDisconnectStatus::kIdle);
}
void TetherControllerImpl::OnFeatureStatesChanged(
const MultiDeviceSetupClient::FeatureStatesMap& feature_states_map) {
FeatureState feature_state =
multidevice_setup_client_->GetFeatureState(Feature::kInstantTethering);
// The |connect_disconnect_status_| should always be
// ConnectDisconnectStatus::kIdle if the |feature_state| is anything other
// than |FeatureState::kEnabledByUser|. A |feature_status| other than
// |FeatureState::kEnabledByUser| would indicate that Instant Tethering became
// disabled or disallowed.
if (feature_state != FeatureState::kEnabledByUser) {
SetConnectDisconnectStatus(ConnectDisconnectStatus::kIdle);
} else if (connect_disconnect_status_ !=
ConnectDisconnectStatus::kTurningOnInstantTethering) {
UpdateStatus();
}
}
void TetherControllerImpl::PerformConnectionAttempt() {
if (!tether_network_.is_null()) {
StartConnect();
return;
}
SetConnectDisconnectStatus(
ConnectDisconnectStatus::kScanningForEligiblePhone);
cros_network_config_->RequestNetworkScan(NetworkType::kTether);
}
void TetherControllerImpl::StartConnect() {
DCHECK(!tether_network_.is_null());
SetConnectDisconnectStatus(
ConnectDisconnectStatus::kConnectingToEligiblePhone);
connector_->StartConnect(
tether_network_->guid,
base::BindOnce(&TetherControllerImpl::OnStartConnectCompleted,
weak_ptr_factory_.GetWeakPtr()));
}
void TetherControllerImpl::OnStartConnectCompleted(StartConnectResult result,
const std::string& message) {
if (result != StartConnectResult::kSuccess) {
PA_LOG(WARNING) << "Start connect failed with result " << result
<< " and message " << message;
}
if (connect_disconnect_status_ !=
ConnectDisconnectStatus::kConnectingToEligiblePhone) {
return;
}
// Note that OnVisibleTetherNetworkFetched() may not have called
// SetConnectDisconnectStatus() with kIdle at this point, so this should go
// ahead and do it.
SetConnectDisconnectStatus(ConnectDisconnectStatus::kIdle);
}
void TetherControllerImpl::Disconnect() {
if (status_ != Status::kConnecting && status_ != Status::kConnected) {
PA_LOG(WARNING) << "Received request to disconnect, but no connection or "
<< "connection attempt is in progress. Current status is "
<< status_;
return;
}
// If |status_| is Status::kConnecting, a tether network may not be available
// yet e.g this class may be in the process of enabling Instant Tethering.
if (tether_network_.is_null()) {
SetConnectDisconnectStatus(ConnectDisconnectStatus::kIdle);
return;
}
PA_LOG(INFO) << "Attempting disconnection; current status is " << status_;
SetConnectDisconnectStatus(ConnectDisconnectStatus::kDisconnecting);
connector_->StartDisconnect(
tether_network_->guid,
base::BindOnce(&TetherControllerImpl::OnDisconnectCompleted,
weak_ptr_factory_.GetWeakPtr()));
}
void TetherControllerImpl::OnModelChanged() {
UpdateStatus();
}
void TetherControllerImpl::OnDisconnectCompleted(bool success) {
if (connect_disconnect_status_ != ConnectDisconnectStatus::kDisconnecting)
return;
SetConnectDisconnectStatus(ConnectDisconnectStatus::kIdle);
// Fetch the tether network and its updated connection state, if it exists.
// By the time OnDisconnectCompleted() is called, the connection state is
// properly updated to ConnectionStateType::kDisconnected, so a fetch may be
// necessary to promptly update |tether_network_|, as neither
// OnActiveNetworksChanged() nor OnNetworkStateListChanged() may be called
// shortly afterwards with the latest network information.
FetchVisibleTetherNetwork();
if (!success)
PA_LOG(WARNING) << "Failed to disconnect tether network";
}
void TetherControllerImpl::OnActiveNetworksChanged(
std::vector<chromeos::network_config::mojom::NetworkStatePropertiesPtr>
networks) {
// Active networks either changed externally (e.g via OS Settings or a new
// actve network added), or as a result of a call to AttemptConnection() or
// Disconnect(). This is needed for the case of
// ConnectionStateType::kConnecting in ComputeStatus().
//
// Note: When OnActiveNetworksChanged() is called shortly after starting a
// disconnect to a ConnectionStateType::kConnecting |tether_network_|, the
// |tether_network_|'s ConnectionStateType may still remain in the
// ConnectionStateType::kConnecting state. This may happen if on the phone,
// hotspot is off but bluetooth tethering is on, and a connection attempt is
// made, but the user does not acknowledge the notification to connect on
// their phone, and subsequently decides to disconnect while
// |tether_network_|'s ConnectionStateType is still
// ConnectionStateType::kConnecting.
FetchVisibleTetherNetwork();
}
void TetherControllerImpl::OnNetworkStateListChanged() {
// Any network change whether caused externally or within this class should
// be reflected to the state of this class (e.g user makes changes to Tether
// network in OS Settings).
FetchVisibleTetherNetwork();
}
void TetherControllerImpl::OnDeviceStateListChanged() {
if (connect_disconnect_status_ !=
ConnectDisconnectStatus::kScanningForEligiblePhone) {
return;
}
cros_network_config_->GetDeviceStateList(
base::BindOnce(&TetherControllerImpl::OnGetDeviceStateList,
weak_ptr_factory_.GetWeakPtr()));
}
void TetherControllerImpl::OnGetDeviceStateList(
std::vector<DeviceStatePropertiesPtr> devices) {
if (connect_disconnect_status_ !=
ConnectDisconnectStatus::kScanningForEligiblePhone) {
return;
}
// There should only be one Tether device in the list.
bool is_tether_device_scanning = false;
for (const auto& device : devices) {
NetworkType type = device->type;
if (type != NetworkType::kTether)
continue;
is_tether_device_scanning = device->scanning;
break;
}
if (!is_tether_device_scanning) {
NotifyAttemptConnectionScanFailed();
SetConnectDisconnectStatus(ConnectDisconnectStatus::kIdle);
}
}
void TetherControllerImpl::FetchVisibleTetherNetwork() {
// Return the connected, connecting, or connectable Tether network.
connector_->GetNetworkStateList(
chromeos::network_config::mojom::NetworkFilter::New(FilterType::kVisible,
NetworkType::kTether,
/*limit=*/0),
base::BindOnce(&TetherControllerImpl::OnVisibleTetherNetworkFetched,
weak_ptr_factory_.GetWeakPtr()));
}
void TetherControllerImpl::OnVisibleTetherNetworkFetched(
std::vector<chromeos::network_config::mojom::NetworkStatePropertiesPtr>
networks) {
chromeos::network_config::mojom::NetworkStatePropertiesPtr
previous_tether_network = std::move(tether_network_);
if (!networks.empty()) {
// The number of tether networks is expected to be at most 1, though some
// tests do use multiple networks.
tether_network_ = std::move(networks[0]);
} else {
tether_network_ = nullptr;
}
// No observeable changes to the tether network specifically. This fetch
// was initiated by a change in a non Tether network type.
if (tether_network_.Equals(previous_tether_network))
return;
// If AttemptConnection() was called when Instant Tethering was disabled.
// The feature must be enabled before scanning can occur.
if (connect_disconnect_status_ ==
ConnectDisconnectStatus::kTurningOnInstantTethering) {
UpdateStatus();
return;
}
// If AttemptConnection() was called when there was no available tether
// connection.
if (connect_disconnect_status_ ==
ConnectDisconnectStatus::kScanningForEligiblePhone &&
!tether_network_.is_null()) {
StartConnect();
return;
}
// If there is no attempt connection in progress, or an attempt connection
// caused OnVisibleTetherNetworkFetched() to be fired. This case also occurs
// in the event that Tethering settings are changed externally from this class
// (e.g user connects via Settings).
SetConnectDisconnectStatus(ConnectDisconnectStatus::kIdle);
}
void TetherControllerImpl::SetConnectDisconnectStatus(
ConnectDisconnectStatus connect_disconnect_status) {
if (connect_disconnect_status_ != connect_disconnect_status)
weak_ptr_factory_.InvalidateWeakPtrs();
connect_disconnect_status_ = connect_disconnect_status;
UpdateStatus();
}
void TetherControllerImpl::UpdateStatus() {
Status status = ComputeStatus();
if (status_ == status)
return;
PA_LOG(INFO) << "TetherController status update: " << status_ << " => "
<< status;
status_ = status;
if (is_attempting_connection_ && status_ == Status::kConnected)
util::LogTetherConnectionResult(util::TetherConnectionResult::kSuccess);
if (status_ != Status::kConnecting)
is_attempting_connection_ = false;
NotifyStatusChanged();
}
TetherController::Status TetherControllerImpl::ComputeStatus() const {
FeatureState feature_state =
multidevice_setup_client_->GetFeatureState(Feature::kInstantTethering);
if (feature_state != FeatureState::kDisabledByUser &&
feature_state != FeatureState::kEnabledByUser) {
// Tethering may be for instance, prohibited by policy or not supported
// by the phone or Chromebook.
return Status::kIneligibleForFeature;
}
if (phone_model_->phone_status_model().has_value()) {
// If the phone status exists, and it indicates that the phone
// does not have reception, the status becomes no kNoReception.
bool does_sim_exist_with_reception =
phone_model_->phone_status_model()->mobile_status() ==
PhoneStatusModel::MobileStatus::kSimWithReception;
if (!does_sim_exist_with_reception)
return Status::kNoReception;
}
if (connect_disconnect_status_ ==
ConnectDisconnectStatus::kTurningOnInstantTethering ||
connect_disconnect_status_ ==
ConnectDisconnectStatus::kScanningForEligiblePhone ||
connect_disconnect_status_ ==
ConnectDisconnectStatus::kConnectingToEligiblePhone) {
return Status::kConnecting;
}
if (feature_state == FeatureState::kDisabledByUser)
return Status::kConnectionUnavailable;
if (tether_network_.is_null())
return Status::kConnectionUnavailable;
ConnectionStateType connection_state = tether_network_->connection_state;
switch (connection_state) {
case ConnectionStateType::kOnline:
[[fallthrough]];
case ConnectionStateType::kConnected:
[[fallthrough]];
case ConnectionStateType::kPortal:
return Status::kConnected;
case ConnectionStateType::kConnecting:
return Status::kConnecting;
case ConnectionStateType::kNotConnected:
return Status::kConnectionAvailable;
}
return Status::kConnectionUnavailable;
}
} // namespace phonehub
} // namespace ash