chromium/chromeos/ash/services/device_sync/cryptauth_device_activity_getter_impl.cc

// Copyright 2019 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/services/device_sync/cryptauth_device_activity_getter_impl.h"

#include <array>
#include <utility>

#include "base/functional/bind.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_functions.h"
#include "base/time/time.h"
#include "chromeos/ash/components/multidevice/logging/logging.h"
#include "chromeos/ash/components/multidevice/software_feature.h"
#include "chromeos/ash/components/multidevice/software_feature_state.h"
#include "chromeos/ash/services/device_sync/async_execution_time_metrics_logger.h"
#include "chromeos/ash/services/device_sync/cryptauth_client.h"
#include "chromeos/ash/services/device_sync/cryptauth_key_bundle.h"
#include "chromeos/ash/services/device_sync/cryptauth_task_metrics_logger.h"
#include "chromeos/ash/services/device_sync/device_sync_type_converters.h"
#include "chromeos/ash/services/device_sync/proto/cryptauth_logging.h"

namespace ash {

namespace device_sync {

namespace {

// TODO(https://crbug.com/933656): Use async execution time metrics to tune
// these timeout values.
constexpr base::TimeDelta kWaitingForGetDevicesActivityStatusResponseTimeout =
    kMaxAsyncExecutionTime;

void RecordGetDevicesActivityStatusMetrics(
    const base::TimeDelta& execution_time,
    CryptAuthApiCallResult result) {
  LogAsyncExecutionTimeMetric(
      "CryptAuth.DeviceSyncV2.DeviceActivityGetter.ExecutionTime."
      "GetDevicesActivityStatus",
      execution_time);
  LogCryptAuthApiCallSuccessMetric(
      "CryptAuth.DeviceSyncV2.DeviceActivityGetter.ApiCallResult."
      "GetDevicesActivityStatus",
      result);
}

}  // namespace

// static
CryptAuthDeviceActivityGetterImpl::Factory*
    CryptAuthDeviceActivityGetterImpl::Factory::test_factory_ = nullptr;

// static
void CryptAuthDeviceActivityGetterImpl::Factory::SetFactoryForTesting(
    Factory* test_factory) {
  test_factory_ = test_factory;
}

// static
std::optional<base::TimeDelta>
CryptAuthDeviceActivityGetterImpl::GetTimeoutForState(State state) {
  switch (state) {
    case State::kWaitingForGetDevicesActivityStatusResponse:
      return kWaitingForGetDevicesActivityStatusResponseTimeout;
    default:
      // Signifies that there should not be a timeout.
      return std::nullopt;
  }
}

CryptAuthDeviceActivityGetterImpl::Factory::~Factory() = default;

std::unique_ptr<CryptAuthDeviceActivityGetter>
CryptAuthDeviceActivityGetterImpl::Factory::Create(
    const std::string& instance_id,
    const std::string& instance_id_token,
    CryptAuthClientFactory* client_factory,
    std::unique_ptr<base::OneShotTimer> timer) {
  if (test_factory_)
    return test_factory_->CreateInstance(instance_id, instance_id_token,
                                         client_factory, std::move(timer));

  return base::WrapUnique(new CryptAuthDeviceActivityGetterImpl(
      instance_id, instance_id_token, client_factory, std::move(timer)));
}

CryptAuthDeviceActivityGetterImpl::CryptAuthDeviceActivityGetterImpl(
    const std::string& instance_id,
    const std::string& instance_id_token,
    CryptAuthClientFactory* client_factory,
    std::unique_ptr<base::OneShotTimer> timer)
    : instance_id_(instance_id),
      instance_id_token_(instance_id_token),
      client_factory_(client_factory),
      timer_(std::move(timer)) {
  DCHECK(client_factory);
}

CryptAuthDeviceActivityGetterImpl::~CryptAuthDeviceActivityGetterImpl() =
    default;

void CryptAuthDeviceActivityGetterImpl::SetState(State state) {
  timer_->Stop();

  PA_LOG(INFO) << "Transitioning from " << state_ << " to " << state;
  state_ = state;
  last_state_change_timestamp_ = base::TimeTicks::Now();

  std::optional<base::TimeDelta> timeout_for_state = GetTimeoutForState(state);
  if (!timeout_for_state)
    return;

  timer_->Start(FROM_HERE, *timeout_for_state,
                base::BindOnce(&CryptAuthDeviceActivityGetterImpl::OnTimeout,
                               base::Unretained(this)));
}

void CryptAuthDeviceActivityGetterImpl::OnAttemptStarted() {
  cryptauthv2::GetDevicesActivityStatusRequest request;
  request.mutable_context()->mutable_client_metadata()->set_retry_count(0);
  request.mutable_context()->mutable_client_metadata()->set_invocation_reason(
      cryptauthv2::ClientMetadata::INVOCATION_REASON_UNSPECIFIED);
  request.mutable_context()->set_group(
      CryptAuthKeyBundle::KeyBundleNameEnumToString(
          CryptAuthKeyBundle::Name::kDeviceSyncBetterTogether));
  request.mutable_context()->set_device_id(instance_id_);
  request.mutable_context()->set_device_id_token(instance_id_token_);

  SetState(State::kWaitingForGetDevicesActivityStatusResponse);

  cryptauth_client_ = client_factory_->CreateInstance();
  cryptauth_client_->GetDevicesActivityStatus(
      request,
      base::BindOnce(
          &CryptAuthDeviceActivityGetterImpl::OnGetDevicesActivityStatusSuccess,
          base::Unretained(this)),
      base::BindOnce(
          &CryptAuthDeviceActivityGetterImpl::OnGetDevicesActivityStatusFailure,
          base::Unretained(this)));
}

void CryptAuthDeviceActivityGetterImpl::OnGetDevicesActivityStatusSuccess(
    const cryptauthv2::GetDevicesActivityStatusResponse& response) {
  DCHECK(state_ == State::kWaitingForGetDevicesActivityStatusResponse);

  RecordGetDevicesActivityStatusMetrics(
      base::TimeTicks::Now() - last_state_change_timestamp_,
      CryptAuthApiCallResult::kSuccess);

  PA_LOG(VERBOSE) << "GetDevicesActivityStatus response:\n" << response;

  DeviceActivityStatusResult device_activity_statuses;

  for (const cryptauthv2::DeviceActivityStatus& device_activity_status :
       response.device_activity_statuses()) {
    device_activity_statuses.emplace_back(mojom::DeviceActivityStatus::New(
        device_activity_status.device_id(),
        base::Time::FromTimeT(device_activity_status.last_activity_time_sec()),
        std::move(device_activity_status.connectivity_status()),
        base::Time::FromTimeT(
            device_activity_status.last_update_time().seconds()) +
            base::Nanoseconds(
                device_activity_status.last_update_time().nanos())));
  }

  cryptauth_client_.reset();
  SetState(State::kFinished);
  FinishAttemptSuccessfully(std::move(device_activity_statuses));
}

void CryptAuthDeviceActivityGetterImpl::OnGetDevicesActivityStatusFailure(
    NetworkRequestError error) {
  DCHECK(state_ == State::kWaitingForGetDevicesActivityStatusResponse);

  RecordGetDevicesActivityStatusMetrics(
      base::TimeTicks::Now() - last_state_change_timestamp_,
      CryptAuthApiCallResultFromNetworkRequestError(error));

  OnAttemptError(error);
}

void CryptAuthDeviceActivityGetterImpl::OnTimeout() {
  DCHECK_EQ(state_, State::kWaitingForGetDevicesActivityStatusResponse);
  PA_LOG(ERROR) << "Timed out in state " << state_ << ".";

  base::TimeDelta execution_time =
      base::TimeTicks::Now() - last_state_change_timestamp_;
  RecordGetDevicesActivityStatusMetrics(execution_time,
                                        CryptAuthApiCallResult::kTimeout);

  OnAttemptError(NetworkRequestError::kUnknown);
}

void CryptAuthDeviceActivityGetterImpl::OnAttemptError(
    NetworkRequestError error) {
  cryptauth_client_.reset();
  SetState(State::kFinished);
  FinishAttemptWithError(error);
}

std::ostream& operator<<(
    std::ostream& stream,
    const CryptAuthDeviceActivityGetterImpl::State& state) {
  switch (state) {
    case CryptAuthDeviceActivityGetterImpl::State::kNotStarted:
      stream << "[DeviceActivityGetter state: Not started]";
      break;
    case CryptAuthDeviceActivityGetterImpl::State::
        kWaitingForGetDevicesActivityStatusResponse:
      stream << "[DeviceActivityGetter state: Waiting for "
             << "GetDevicesActivityStatus response]";
      break;
    case CryptAuthDeviceActivityGetterImpl::State::kFinished:
      stream << "[DeviceActivityGetter state: Finished]";
      break;
  }

  return stream;
}

}  // namespace device_sync

}  // namespace ash