chromium/chromeos/ash/components/tether/wifi_hotspot_connector.cc

// Copyright 2017 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/tether/wifi_hotspot_connector.h"

#include <memory>

#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/metrics/histogram_macros.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/default_clock.h"
#include "base/types/expected.h"
#include "base/uuid.h"
#include "chromeos/ash/components/multidevice/logging/logging.h"
#include "chromeos/ash/components/network/device_state.h"
#include "chromeos/ash/components/network/network_connect.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 "chromeos/ash/components/network/network_state_handler.h"
#include "chromeos/ash/components/network/network_type_pattern.h"
#include "chromeos/ash/components/network/shill_property_util.h"
#include "chromeos/ash/components/network/technology_state_controller.h"
#include "third_party/cros_system_api/dbus/shill/dbus-constants.h"

namespace ash::tether {

WifiHotspotConnector::WifiHotspotConnector(NetworkHandler* network_handler,
                                           NetworkConnect* network_connect)
    : network_connect_(network_connect),
      network_handler_(network_handler),
      timer_(std::make_unique<base::OneShotTimer>()),
      clock_(base::DefaultClock::GetInstance()),
      task_runner_(base::SingleThreadTaskRunner::GetCurrentDefault()) {
  network_state_handler_observer_.Observe(
      network_handler->network_state_handler());
}

WifiHotspotConnector::~WifiHotspotConnector() {
  // If a connection attempt is active when this class is destroyed, the attempt
  // has no time to finish successfully, so it is considered a failure.
  if (!wifi_network_guid_.empty()) {
    CompleteActiveConnectionAttempt(
        WifiHotspotConnectionError::kWifiHotspotConnectorClassDestroyed);
  }
}

void WifiHotspotConnector::ConnectToWifiHotspot(
    const std::string& ssid,
    const std::string& password,
    const std::string& tether_network_guid,
    WifiConnectionCallback callback) {
  DCHECK(!ssid.empty());
  // Note: |password| can be empty in some cases.

  if (callback_) {
    DCHECK(timer_->IsRunning());

    // If another connection attempt was underway but had not yet completed,
    // disassociate that network from the Tether network and call the callback,
    // passing an empty string to signal that the connection did not complete
    // successfully.
    bool successful_disassociation =
        network_handler_->network_state_handler()
            ->DisassociateTetherNetworkStateFromWifiNetwork(
                tether_network_guid_);
    if (successful_disassociation) {
      PA_LOG(VERBOSE) << "Wi-Fi network (ID \"" << wifi_network_guid_ << "\") "
                      << "successfully disassociated from Tether network (ID "
                      << "\"" << tether_network_guid_ << "\").";
    } else {
      PA_LOG(ERROR) << "Wi-Fi network (ID \"" << wifi_network_guid_ << "\") "
                    << "failed to disassociate from Tether network ID (\""
                    << tether_network_guid_ << "\").";
    }

    CompleteActiveConnectionAttempt(
        WifiHotspotConnectionError::kCancelledForNewerConnectionAttempt);
  }

  ssid_ = ssid;
  password_ = password;
  tether_network_guid_ = tether_network_guid;
  wifi_network_guid_ = base::Uuid::GenerateRandomV4().AsLowercaseString();
  callback_ = std::move(callback);
  timer_->Start(FROM_HERE, base::Seconds(kConnectionTimeoutSeconds),
                base::BindOnce(&WifiHotspotConnector::OnConnectionTimeout,
                               weak_ptr_factory_.GetWeakPtr()));
  connection_attempt_start_time_ = clock_->Now();

  // If Wi-Fi is enabled, continue with creating the configuration of the
  // hotspot. Otherwise, request that Wi-Fi be enabled and wait; see
  // UpdateWaitingForWifi.
  if (network_handler_->network_state_handler()->IsTechnologyEnabled(
          NetworkTypePattern::WiFi())) {
    // Ensure that a possible previous pending callback to UpdateWaitingForWifi
    // won't result in a second call to CreateWifiConfiguration().
    is_waiting_for_wifi_to_enable_ = false;

    CreateWifiConfiguration();
  } else if (!is_waiting_for_wifi_to_enable_) {
    is_waiting_for_wifi_to_enable_ = true;

    // Once Wi-Fi is enabled, UpdateWaitingForWifi will be called.
    network_handler_->technology_state_controller()->SetTechnologiesEnabled(
        NetworkTypePattern::WiFi(), true /*enabled */,
        base::BindRepeating(&WifiHotspotConnector::OnEnableWifiError,
                            weak_ptr_factory_.GetWeakPtr()));
  }
}

void WifiHotspotConnector::RequestWifiScan() {
  network_handler_->network_state_handler()->RequestScan(
      NetworkTypePattern::WiFi());
}

void WifiHotspotConnector::OnEnableWifiError(const std::string& error_name) {
  is_waiting_for_wifi_to_enable_ = false;
  PA_LOG(ERROR) << "Failed to enable Wi-Fi: " << error_name;
  CompleteActiveConnectionAttempt(
      WifiHotspotConnectionError::kWifiFailedToEnabled);
}

void WifiHotspotConnector::DeviceListChanged() {
  UpdateWaitingForWifi();
}

void WifiHotspotConnector::NetworkPropertiesUpdated(
    const NetworkState* network) {
  if (network->guid() != wifi_network_guid_) {
    // If a different network has been connected, return early and wait for the
    // network with ID |wifi_network_guid_| is updated.
    return;
  }

  if (!has_requested_wifi_scan_) {
    has_requested_wifi_scan_ = true;
    RequestWifiScan();
  }

  // We use "visible" to determine if the network can be connected to, rather
  // than "connectable". In the aftermath of b/302621170, it was discovered
  // "connectable" only refers to the service having a SSID and password set.
  // As the service is configured earlier, that check will always pass, even
  // if the service wasn't discovered by a scan. "Visible", by contrast, is
  // used by the UI to determine if the service should be shown. If it's shown,
  // we know it has been discovered by a fresh scan.
  if (network->visible() && !has_initiated_connection_to_current_network_) {
    // Set |has_initiated_connection_to_current_network_| to true to ensure that
    // this code path is only run once per connection attempt.
    has_initiated_connection_to_current_network_ = true;

    InitiateConnectionToCurrentNetwork();
  }
}

void WifiHotspotConnector::DevicePropertiesUpdated(const DeviceState* device) {
  if (device->Matches(NetworkTypePattern::WiFi())) {
    UpdateWaitingForWifi();
  }
}

void WifiHotspotConnector::OnShuttingDown() {
  network_state_handler_observer_.Reset();
}

void WifiHotspotConnector::UpdateWaitingForWifi() {
  if (!is_waiting_for_wifi_to_enable_ ||
      !network_handler_->network_state_handler()->IsTechnologyEnabled(
          NetworkTypePattern::WiFi())) {
    return;
  }

  is_waiting_for_wifi_to_enable_ = false;

  if (ssid_.empty()) {
    return;
  }

  CreateWifiConfiguration();
}

void WifiHotspotConnector::InitiateConnectionToCurrentNetwork() {
  if (wifi_network_guid_.empty()) {
    PA_LOG(WARNING) << "InitiateConnectionToCurrentNetwork() was called, but "
                    << "the connection was canceled before it could be "
                    << "initiated.";
    return;
  }

  // If the network is now connectable, associate it with a Tether network
  // ASAP so that the correct icon will be displayed in the tray while the
  // network is connecting.
  // NOTE: AssociateTetherNetworkStateWithWifiNetwork() is idempotent, so
  // calling it on each retry is safe.
  // Because this method may be called by `NetworkPropertiesUpdated` (a method
  // on `NetworkStateHandlerObserver`), associate the network in a new task to
  // ensure that NetworkStateHandler is not modified while it is notifying
  // observers. See https://crbug.com/800370.
  task_runner_->PostTask(
      FROM_HERE, base::BindOnce(&WifiHotspotConnector::AssociateNetworks,
                                weak_ptr_factory_.GetWeakPtr(),
                                wifi_network_guid_, tether_network_guid_));

  // Initiate a connection to the network.
  const NetworkState* network_state =
      network_handler_->network_state_handler()->GetNetworkStateFromGuid(
          wifi_network_guid_);

  if (!network_state) {
    PA_LOG(ERROR) << "Network state for " << wifi_network_guid_
                  << " was null. Failing.";
    CompleteActiveConnectionAttempt(
        WifiHotspotConnectionError::kNetworkStateWasNull);
  }

  PA_LOG(INFO) << "Current connection attempt is #"
               << current_connection_attempt_count_
               << ". Attempting to connect...";

  ++current_connection_attempt_count_;

  network_handler_->network_connection_handler()->ConnectToNetwork(
      network_state->path(),
      base::BindOnce(&WifiHotspotConnector::OnWifiConnectionSucceeded,
                     weak_ptr_factory_.GetWeakPtr()),
      base::BindOnce(&WifiHotspotConnector::OnWifiConnectionFailed,
                     weak_ptr_factory_.GetWeakPtr()),
      /*check_error_state=*/false, ConnectCallbackMode::ON_COMPLETED);
}

// TODO(b/318534727): Record the number of attempts before completion in a
// metric.
void WifiHotspotConnector::CompleteActiveConnectionAttempt(
    std::optional<WifiHotspotConnectionError> error) {
  if (wifi_network_guid_.empty()) {
    PA_LOG(ERROR) << "CompleteActiveConnectionAttempt"
                  << "was called, but no connection attempt is in progress.";
    if (error.has_value()) {
      PA_LOG(ERROR) << "CompleteActiveConnectionAttempt error: "
                    << error.value();
    } else {
      PA_LOG(ERROR) << "CompleteActiveConnectionAttempt had no error";
    }

    return;
  }

  PA_LOG(VERBOSE) << "Completing active connection attempt to network with ID: "
                  << wifi_network_guid_;

  // If the connection attempt has failed (e.g., due to cancellation or
  // timeout) and ConnectToNetworkId() has already been called, the in-progress
  // connection attempt should be stopped; there is no cancellation mechanism,
  // so DisconnectNetwork() is called here instead. Without this, it would
  // be possible for the connection to complete after the Tether component had
  // already shut down. See crbug.com/761569.
  if (error.has_value() && has_initiated_connection_to_current_network_) {
    const NetworkState* network_state =
        network_handler_->network_state_handler()->GetNetworkStateFromGuid(
            wifi_network_guid_);
    if (!network_state) {
      PA_LOG(ERROR) << "Network state for " << wifi_network_guid_
                    << " was null. Unable to disconnect.";
    } else {
      // TODO(b/313488946): Handle errors during disconnection on failure.
      network_handler_->network_connection_handler()->DisconnectNetwork(
          network_state->path(), /*success_callback=*/base::DoNothing(),
          /*error_callback=*/base::DoNothingAs<void(const std::string&)>());
    }
  }

  std::string wifi_network_guid_copy_for_callback_ = wifi_network_guid_;

  ssid_.clear();
  password_.clear();
  wifi_network_guid_.clear();
  has_initiated_connection_to_current_network_ = false;
  is_waiting_for_wifi_to_enable_ = false;
  has_requested_wifi_scan_ = false;
  current_connection_attempt_count_ = 0;

  timer_->Stop();

  if (error.has_value()) {
    std::move(callback_).Run(base::unexpected(error.value()));
    return;
  }

  // UMA_HISTOGRAM_MEDIUM_TIMES is used because UMA_HISTOGRAM_TIMES has a max
  // of 10 seconds.
  DCHECK(!connection_attempt_start_time_.is_null());
  UMA_HISTOGRAM_MEDIUM_TIMES(
      "InstantTethering.Performance.ConnectToHotspotDuration",
      clock_->Now() - connection_attempt_start_time_);
  connection_attempt_start_time_ = base::Time();

  std::move(callback_).Run(base::ok(wifi_network_guid_copy_for_callback_));
}

void WifiHotspotConnector::CreateWifiConfiguration() {
  base::Value::Dict properties = CreateWifiPropertyDictionary(ssid_, password_);

  // This newly configured network will eventually be passed as an argument to
  // NetworkPropertiesUpdated().
  network_connect_->CreateConfiguration(std::move(properties),
                                        /* shared */ false);
}

base::Value::Dict WifiHotspotConnector::CreateWifiPropertyDictionary(
    const std::string& ssid,
    const std::string& password) {
  PA_LOG(VERBOSE) << "Creating network configuration. " << "SSID: " << ssid
                  << ", " << "Password: " << password << ", "
                  << "Wi-Fi network GUID: " << wifi_network_guid_;

  base::Value::Dict properties;

  shill_property_util::SetSSID(ssid, &properties);
  properties.Set(shill::kGuidProperty, wifi_network_guid_);
  properties.Set(shill::kAutoConnectProperty, false);
  properties.Set(shill::kTypeProperty, shill::kTypeWifi);
  properties.Set(shill::kSaveCredentialsProperty, true);

  if (password.empty()) {
    properties.Set(shill::kSecurityClassProperty, shill::kSecurityClassNone);
  } else {
    properties.Set(shill::kSecurityClassProperty, shill::kSecurityClassPsk);
    properties.Set(shill::kPassphraseProperty, password);
  }

  return properties;
}

void WifiHotspotConnector::OnConnectionTimeout() {
  CompleteActiveConnectionAttempt(WifiHotspotConnectionError::kTimeout);
}

void WifiHotspotConnector::OnWifiConnectionSucceeded() {
  // The network has connected, so complete the connection attempt. Because
  // this is a NetworkStateHandlerObserver callback, complete the attempt in
  // a new task to ensure that NetworkStateHandler is not modified while it is
  // notifying observers. See https://crbug.com/800370.
  PA_LOG(INFO) << "Successfully connected to Wifi Network";
  CompleteActiveConnectionAttempt(/*error=*/std::nullopt);
}

void WifiHotspotConnector::OnWifiConnectionFailed(
    const std::string& error_name) {
  PA_LOG(ERROR) << "Failed to connect to Wifi Network. Error: [" << error_name
                << "]";
  if (current_connection_attempt_count_ <= kMaxWifiConnectionAttempts) {
    PA_LOG(INFO) << "Current connection attempt is #"
                 << current_connection_attempt_count_ << ". Maximum is "
                 << kMaxWifiConnectionAttempts << ". Retrying Connection...";
    InitiateConnectionToCurrentNetwork();
    return;
  }

  // The network connect has failed, so complete the connection attempt. Because
  // this is a NetworkStateHandlerObserver callback, complete the attempt in
  // a new task to ensure that NetworkStateHandler is not modified while it is
  // notifying observers. See https://crbug.com/800370.
  PA_LOG(ERROR) << "Hit maximum allowed connection attempts. Failing.";
  CompleteActiveConnectionAttempt(
      WifiHotspotConnectionError::kNetworkConnectionHandlerFailed);
}

void WifiHotspotConnector::AssociateNetworks(std::string wifi_network_guid,
                                             std::string tether_network_guid) {
  if (!wifi_network_guid_.empty() && wifi_network_guid != wifi_network_guid_) {
    PA_LOG(INFO) << "Skipping association of [" << tether_network_guid
                 << "] with [" << wifi_network_guid
                 << "], as a newer connection attempt was scheduled before an "
                    "association could be made.";
    return;
  }

  bool successful_association =
      network_handler_->network_state_handler()
          ->AssociateTetherNetworkStateWithWifiNetwork(tether_network_guid,
                                                       wifi_network_guid);
  if (successful_association) {
    PA_LOG(VERBOSE) << "Wi-Fi network (ID \"" << wifi_network_guid << "\") "
                    << "successfully associated with Tether network (ID \""
                    << tether_network_guid << "\"). Starting connection "
                    << "attempt.";
  } else {
    PA_LOG(ERROR) << "Wi-Fi network (ID \"" << wifi_network_guid << "\") "
                  << "failed to associate with Tether network (ID \""
                  << tether_network_guid << "\"). Starting connection "
                  << "attempt.";
  }
}

void WifiHotspotConnector::SetTestDoubles(
    std::unique_ptr<base::OneShotTimer> test_timer,
    base::Clock* test_clock,
    scoped_refptr<base::TaskRunner> test_task_runner) {
  timer_ = std::move(test_timer);
  clock_ = test_clock;
  task_runner_ = test_task_runner;
}

std::ostream& operator<<(
    std::ostream& stream,
    const WifiHotspotConnector::WifiHotspotConnectionError error) {
  switch (error) {
    case WifiHotspotConnector::WifiHotspotConnectionError::kTimeout:
      stream << "[timeout]";
      break;
    case WifiHotspotConnector::WifiHotspotConnectionError::
        kCancelledForNewerConnectionAttempt:
      stream << "[cancelled for newer connection attempt]";
      break;
    case WifiHotspotConnector::WifiHotspotConnectionError::
        kWifiHotspotConnectorClassDestroyed:
      stream << "[WifiHotspotConnector destroyed]";
      break;
    case WifiHotspotConnector::WifiHotspotConnectionError::kNetworkStateWasNull:
      stream << "[network state was null]";
      break;
    case WifiHotspotConnector::WifiHotspotConnectionError::
        kNetworkConnectionHandlerFailed:
      stream << "[network connection handler failed to connect]";
      break;
    case WifiHotspotConnector::WifiHotspotConnectionError::kWifiFailedToEnabled:
      stream << "[wifi failed to enabled]";
      break;
  }

  return stream;
}

}  // namespace ash::tether