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

#include <memory>
#include <sstream>

#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/metrics/histogram_functions.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/time.h"
#include "chromeos/ash/components/network/device_state.h"
#include "chromeos/ash/components/network/network_device_handler.h"
#include "chromeos/ash/components/network/network_state_handler.h"
#include "chromeos/ash/components/network/network_type_pattern.h"
#include "components/device_event_log/device_event_log.h"
#include "third_party/cros_system_api/dbus/shill/dbus-constants.h"

namespace ash {

namespace {

// Delay for first uninhibit retry attempt. Delay doubles for every
// subsequent attempt.
constexpr base::TimeDelta kUninhibitRetryDelay = base::Seconds(2);

// Timeout waiting for Cellular device scanning state to change
// to true and back to false.
constexpr base::TimeDelta kScanningChangeTimeout = base::Seconds(120);

}  // namespace

CellularInhibitor::InhibitRequest::InhibitRequest(
    InhibitReason inhibit_reason,
    InhibitCallback inhibit_callback)
    : inhibit_reason(inhibit_reason),
      inhibit_callback(std::move(inhibit_callback)) {}

CellularInhibitor::InhibitRequest::~InhibitRequest() = default;

CellularInhibitor::InhibitLock::InhibitLock(base::OnceClosure unlock_callback)
    : unlock_callback_(std::move(unlock_callback)) {}

CellularInhibitor::InhibitLock::~InhibitLock() {
  std::move(unlock_callback_).Run();
}

// static
const base::TimeDelta CellularInhibitor::kInhibitPropertyChangeTimeout =
    base::Seconds(5);

// static
void CellularInhibitor::RecordInhibitOperationResult(
    InhibitOperationResult result) {
  base::UmaHistogramEnumeration("Network.Cellular.InhibitResult", result);
}

CellularInhibitor::CellularInhibitor() = default;

CellularInhibitor::~CellularInhibitor() = default;

void CellularInhibitor::Init(NetworkStateHandler* network_state_handler,
                             NetworkDeviceHandler* network_device_handler) {
  network_state_handler_ = network_state_handler;
  network_device_handler_ = network_device_handler;

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

void CellularInhibitor::InhibitCellularScanning(InhibitReason reason,
                                                InhibitCallback callback) {
  inhibit_requests_.push(
      std::make_unique<InhibitRequest>(reason, std::move(callback)));
  ProcessRequests();
}

std::optional<CellularInhibitor::InhibitReason>
CellularInhibitor::GetInhibitReason() const {
  if (state_ == State::kIdle)
    return std::nullopt;

  return inhibit_requests_.front()->inhibit_reason;
}

void CellularInhibitor::AddObserver(Observer* observer) {
  observer_list_.AddObserver(observer);
}

void CellularInhibitor::RemoveObserver(Observer* observer) {
  observer_list_.RemoveObserver(observer);
}

bool CellularInhibitor::HasObserver(Observer* observer) const {
  return observer_list_.HasObserver(observer);
}

void CellularInhibitor::NotifyInhibitStateChanged() {
  for (auto& observer : observer_list_)
    observer.OnInhibitStateChanged();
}

void CellularInhibitor::DeviceListChanged() {
  CheckScanningIfNeeded();
}

void CellularInhibitor::DevicePropertiesUpdated(const DeviceState* device) {
  CheckScanningIfNeeded();
  CheckInhibitPropertyIfNeeded();
}

const DeviceState* CellularInhibitor::GetCellularDevice() const {
  return network_state_handler_->GetDeviceStateByType(
      NetworkTypePattern::Cellular());
}

void CellularInhibitor::TransitionToState(State state) {
  State old_state = state_;
  state_ = state;

  bool was_inhibited = old_state != State::kIdle;
  bool is_inhibited = state_ != State::kIdle;

  std::stringstream ss;
  ss << "CellularInhibitor state: " << old_state << " => " << state_;

  if (!is_inhibited) {
    DCHECK(!GetInhibitReason());
    NET_LOG(EVENT) << ss.str();
  } else {
    NET_LOG(EVENT) << ss.str() << ", reason: " << *GetInhibitReason();
  }

  if (was_inhibited != is_inhibited)
    NotifyInhibitStateChanged();
}

void CellularInhibitor::ProcessRequests() {
  if (inhibit_requests_.empty())
    return;

  // Another inhibit request is already underway; wait until it has completed
  // before starting a new request.
  if (state_ != State::kIdle)
    return;

  uninhibit_attempts_so_far_ = 0;
  TransitionToState(State::kInhibiting);
  SetInhibitProperty();
}

void CellularInhibitor::OnInhibit(
    bool success,
    std::optional<CellularInhibitor::InhibitOperationResult> result) {
  DCHECK(state_ == State::kWaitForInhibit || state_ == State::kInhibiting);

  if (success) {
    TransitionToState(State::kInhibited);
    std::unique_ptr<InhibitLock> lock = std::make_unique<InhibitLock>(
        base::BindOnce(&CellularInhibitor::AttemptUninhibit,
                       weak_ptr_factory_.GetWeakPtr()));
    std::move(inhibit_requests_.front()->inhibit_callback).Run(std::move(lock));
    return;
  }

  DCHECK(result.has_value());
  RecordInhibitOperationResult(*result);
  std::move(inhibit_requests_.front()->inhibit_callback).Run(nullptr);
  PopRequestAndProcessNext();
}

void CellularInhibitor::AttemptUninhibit() {
  DCHECK(state_ == State::kInhibited);
  TransitionToState(State::kUninhibiting);
  SetInhibitProperty();
}

void CellularInhibitor::OnUninhibit(bool success) {
  DCHECK(state_ == State::kWaitForUninhibit || state_ == State::kUninhibiting);

  if (!success) {
    base::TimeDelta retry_delay =
        kUninhibitRetryDelay * (1 << uninhibit_attempts_so_far_);
    NET_LOG(DEBUG) << "Uninhibit Failed. Retrying in " << retry_delay;
    TransitionToState(State::kInhibited);
    uninhibit_attempts_so_far_++;

    base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
        FROM_HERE,
        base::BindOnce(&CellularInhibitor::AttemptUninhibit,
                       weak_ptr_factory_.GetWeakPtr()),
        retry_delay);
    return;
  }

  scanning_change_timer_.Start(
      FROM_HERE, kScanningChangeTimeout,
      base::BindOnce(&CellularInhibitor::OnScanningChangeTimeout,
                     weak_ptr_factory_.GetWeakPtr()));
  TransitionToState(State::kWaitingForScanningToStop);
  CheckForScanningStopped();
}

void CellularInhibitor::CheckScanningIfNeeded() {
  if (state_ != State::kWaitingForScanningToStop)
    return;
  else
    CheckForScanningStopped();
}

void CellularInhibitor::CheckForScanningStopped() {
  DCHECK(state_ == State::kWaitingForScanningToStop);

  if (!HasScanningStopped())
    return;

  RecordInhibitOperationResult(InhibitOperationResult::kSuccess);
  PopRequestAndProcessNext();
}

bool CellularInhibitor::HasScanningStopped() {
  const DeviceState* cellular_device = GetCellularDevice();
  if (!cellular_device)
    return false;
  return !cellular_device->scanning();
}

void CellularInhibitor::OnScanningChangeTimeout() {
  NET_LOG(ERROR)
      << "Timeout waiting for cellular device scanning state to change.";
  // Assume that inhibit has completed and continue processing other requests.
  RecordInhibitOperationResult(InhibitOperationResult::kUninhibitTimeout);
  PopRequestAndProcessNext();
}

void CellularInhibitor::PopRequestAndProcessNext() {
  scanning_change_timer_.Stop();
  inhibit_requests_.pop();
  TransitionToState(State::kIdle);
  ProcessRequests();
}

void CellularInhibitor::SetInhibitProperty() {
  DCHECK(state_ == State::kInhibiting || state_ == State::kUninhibiting);
  const DeviceState* cellular_device = GetCellularDevice();
  if (!cellular_device) {
    ReturnSetInhibitPropertyResult(/*success=*/false,
                                   InhibitOperationResult::kSetInhibitNoDevice);
    return;
  }

  bool new_inhibit_value = state_ == State::kInhibiting;

  // If the new value is already set, return early.
  if (cellular_device->inhibited() == new_inhibit_value) {
    ReturnSetInhibitPropertyResult(/*success=*/true, /*result=*/std::nullopt);
    return;
  }

  network_device_handler_->SetDeviceProperty(
      cellular_device->path(), shill::kInhibitedProperty,
      base::Value(new_inhibit_value),
      base::BindOnce(&CellularInhibitor::OnSetPropertySuccess,
                     weak_ptr_factory_.GetWeakPtr()),
      base::BindOnce(&CellularInhibitor::OnSetPropertyError,
                     weak_ptr_factory_.GetWeakPtr(),
                     /*attempted_inhibit=*/new_inhibit_value));
}

void CellularInhibitor::OnSetPropertySuccess() {
  if (state_ == State::kInhibiting) {
    TransitionToState(State::kWaitForInhibit);
  } else if (state_ == State::kUninhibiting) {
    TransitionToState(State::kWaitForUninhibit);
  }
  set_inhibit_timer_.Start(
      FROM_HERE, kInhibitPropertyChangeTimeout,
      base::BindOnce(&CellularInhibitor::OnInhibitPropertyChangeTimeout,
                     weak_ptr_factory_.GetWeakPtr()));
  CheckInhibitPropertyIfNeeded();
}

void CellularInhibitor::OnSetPropertyError(bool attempted_inhibit,
                                           const std::string& error_name) {
  NET_LOG(ERROR) << (attempted_inhibit ? "Inhibit" : "Uninhibit")
                 << "CellularScanning() failed: " << error_name;
  ReturnSetInhibitPropertyResult(/*success=*/false,
                                 InhibitOperationResult::kSetInhibitFailed);
}

void CellularInhibitor::ReturnSetInhibitPropertyResult(
    bool success,
    std::optional<CellularInhibitor::InhibitOperationResult> result) {
  set_inhibit_timer_.Stop();
  if (state_ == State::kInhibiting || state_ == State::kWaitForInhibit) {
    OnInhibit(success, result);
    return;
  }
  if (state_ == State::kUninhibiting || state_ == State::kWaitForUninhibit) {
    OnUninhibit(success);
  }
}

void CellularInhibitor::CheckInhibitPropertyIfNeeded() {
  if (state_ != State::kWaitForInhibit && state_ != State::kWaitForUninhibit)
    return;

  const DeviceState* cellular_device = GetCellularDevice();
  if (!cellular_device)
    return;

  if (state_ == State::kWaitForInhibit && !cellular_device->inhibited())
    return;

  if (state_ == State::kWaitForUninhibit && cellular_device->inhibited())
    return;

  ReturnSetInhibitPropertyResult(/*success=*/true, /*result=*/std::nullopt);
}

void CellularInhibitor::OnInhibitPropertyChangeTimeout() {
  NET_LOG(EVENT) << "Timeout waiting for inhibit property change state "
                 << state_;
  ReturnSetInhibitPropertyResult(/*success=*/false,
                                 InhibitOperationResult::kSetInhibitTimeout);
}

std::ostream& operator<<(std::ostream& stream,
                         const CellularInhibitor::State& state) {
  switch (state) {
    case CellularInhibitor::State::kIdle:
      stream << "[Idle]";
      break;
    case CellularInhibitor::State::kInhibiting:
      stream << "[Inhibiting]";
      break;
    case CellularInhibitor::State::kWaitForInhibit:
      stream << "[Waiting for Inhibit property set]";
      break;
    case CellularInhibitor::State::kInhibited:
      stream << "[Inhibited]";
      break;
    case CellularInhibitor::State::kUninhibiting:
      stream << "[Uninhibiting]";
      break;
    case CellularInhibitor::State::kWaitForUninhibit:
      stream << "[Waiting for Inhibit property clear]";
      break;
    case CellularInhibitor::State::kWaitingForScanningToStop:
      stream << "[Waiting for scanning to stop]";
      break;
  }
  return stream;
}

std::ostream& operator<<(
    std::ostream& stream,
    const CellularInhibitor::InhibitReason& inhibit_reason) {
  switch (inhibit_reason) {
    case CellularInhibitor::InhibitReason::kInstallingProfile:
      stream << "[Installing profile]";
      break;
    case CellularInhibitor::InhibitReason::kRenamingProfile:
      stream << "[Renaming profile]";
      break;
    case CellularInhibitor::InhibitReason::kRemovingProfile:
      stream << "[Removing profile]";
      break;
    case CellularInhibitor::InhibitReason::kConnectingToProfile:
      stream << "[Connecting to profile]";
      break;
    case CellularInhibitor::InhibitReason::kRefreshingProfileList:
      stream << "[Refreshing profile list]";
      break;
    case CellularInhibitor::InhibitReason::kResettingEuiccMemory:
      stream << "[Resetting EUICC memory]";
      break;
    case CellularInhibitor::InhibitReason::kDisablingProfile:
      stream << "[Disabling profile]";
      break;
    case CellularInhibitor::InhibitReason::kRequestingAvailableProfiles:
      stream << "[Requesting available profiles]";
      break;
  }
  return stream;
}

}  // namespace ash