// 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_feature_status_setter_impl.h"
#include <utility>
#include "base/functional/bind.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_functions.h"
#include "chromeos/ash/components/multidevice/logging/logging.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_feature_type.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/proto/cryptauth_common.pb.h"
namespace ash {
namespace device_sync {
namespace {
// TODO(https://crbug.com/933656): Use async execution time metric to tune this.
constexpr base::TimeDelta kWaitingForBatchSetFeatureStatusesResponseTimeout =
kMaxAsyncExecutionTime;
void RecordBatchSetFeatureStatusesMetrics(const base::TimeDelta& execution_time,
CryptAuthApiCallResult result) {
LogAsyncExecutionTimeMetric(
"CryptAuth.DeviceSyncV2.FeatureStatusSetter.ExecutionTime."
"SetFeatureStatuses",
execution_time);
LogCryptAuthApiCallSuccessMetric(
"CryptAuth.DeviceSyncV2.FeatureStatusSetter.ApiCallResult."
"SetFeatureStatuses",
result);
}
} // namespace
// static
CryptAuthFeatureStatusSetterImpl::Factory*
CryptAuthFeatureStatusSetterImpl::Factory::test_factory_ = nullptr;
// static
std::unique_ptr<CryptAuthFeatureStatusSetter>
CryptAuthFeatureStatusSetterImpl::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 CryptAuthFeatureStatusSetterImpl(
instance_id, instance_id_token, client_factory, std::move(timer)));
}
// static
void CryptAuthFeatureStatusSetterImpl::Factory::SetFactoryForTesting(
Factory* test_factory) {
test_factory_ = test_factory;
}
CryptAuthFeatureStatusSetterImpl::Factory::~Factory() = default;
CryptAuthFeatureStatusSetterImpl::CryptAuthFeatureStatusSetterImpl(
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)) {}
CryptAuthFeatureStatusSetterImpl::~CryptAuthFeatureStatusSetterImpl() = default;
// static
std::optional<base::TimeDelta>
CryptAuthFeatureStatusSetterImpl::GetTimeoutForState(State state) {
switch (state) {
case State::kWaitingForBatchSetFeatureStatusesResponse:
return kWaitingForBatchSetFeatureStatusesResponseTimeout;
default:
// Signifies that there should not be a timeout.
return std::nullopt;
}
}
CryptAuthFeatureStatusSetterImpl::Request::Request(
const std::string& device_id,
multidevice::SoftwareFeature feature,
FeatureStatusChange status_change,
base::OnceClosure success_callback,
base::OnceCallback<void(NetworkRequestError)> error_callback)
: device_id(device_id),
feature(feature),
status_change(status_change),
success_callback(std::move(success_callback)),
error_callback(std::move(error_callback)) {}
CryptAuthFeatureStatusSetterImpl::Request::Request(Request&& request)
: device_id(request.device_id),
feature(request.feature),
status_change(request.status_change),
success_callback(std::move(request.success_callback)),
error_callback(std::move(request.error_callback)) {}
CryptAuthFeatureStatusSetterImpl::Request::~Request() = default;
void CryptAuthFeatureStatusSetterImpl::SetFeatureStatus(
const std::string& device_id,
multidevice::SoftwareFeature feature,
FeatureStatusChange status_change,
base::OnceClosure success_callback,
base::OnceCallback<void(NetworkRequestError)> error_callback) {
pending_requests_.emplace(device_id, feature, status_change,
std::move(success_callback),
std::move(error_callback));
if (state_ == State::kIdle)
ProcessRequestQueue();
}
void CryptAuthFeatureStatusSetterImpl::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(&CryptAuthFeatureStatusSetterImpl::OnTimeout,
base::Unretained(this)));
}
void CryptAuthFeatureStatusSetterImpl::OnTimeout() {
DCHECK_EQ(state_, State::kWaitingForBatchSetFeatureStatusesResponse);
base::TimeDelta execution_time =
base::TimeTicks::Now() - last_state_change_timestamp_;
RecordBatchSetFeatureStatusesMetrics(execution_time,
CryptAuthApiCallResult::kTimeout);
PA_LOG(ERROR) << "Timed out in state " << state_ << ".";
// TODO(https://crbug.com/1011358): Use more specific error codes.
FinishAttempt(NetworkRequestError::kUnknown);
}
void CryptAuthFeatureStatusSetterImpl::ProcessRequestQueue() {
if (pending_requests_.empty())
return;
cryptauthv2::BatchSetFeatureStatusesRequest request;
request.mutable_context()->set_group(
CryptAuthKeyBundle::KeyBundleNameEnumToString(
CryptAuthKeyBundle::Name::kDeviceSyncBetterTogether));
request.mutable_context()->mutable_client_metadata()->set_invocation_reason(
cryptauthv2::ClientMetadata::FEATURE_TOGGLED);
request.mutable_context()->set_device_id(instance_id_);
request.mutable_context()->set_device_id_token(instance_id_token_);
cryptauthv2::DeviceFeatureStatus* device_feature_status =
request.add_device_feature_statuses();
device_feature_status->set_device_id(pending_requests_.front().device_id);
cryptauthv2::DeviceFeatureStatus::FeatureStatus* feature_status =
device_feature_status->add_feature_statuses();
feature_status->set_feature_type(
CryptAuthFeatureTypeToString(CryptAuthFeatureTypeFromSoftwareFeature(
pending_requests_.front().feature)));
std::string status;
switch (pending_requests_.front().status_change) {
case FeatureStatusChange::kEnableExclusively:
status = "exclusively enable";
feature_status->set_enabled(true);
feature_status->set_enable_exclusively(true);
break;
case FeatureStatusChange::kEnableNonExclusively:
status = "enable";
feature_status->set_enabled(true);
feature_status->set_enable_exclusively(false);
break;
case FeatureStatusChange::kDisable:
status = "disable";
feature_status->set_enabled(false);
feature_status->set_enable_exclusively(false);
break;
}
PA_LOG(INFO) << "Attempting to " << status << " feature "
<< pending_requests_.front().feature;
SetState(State::kWaitingForBatchSetFeatureStatusesResponse);
cryptauth_client_ = client_factory_->CreateInstance();
cryptauth_client_->BatchSetFeatureStatuses(
request,
base::BindOnce(
&CryptAuthFeatureStatusSetterImpl::OnBatchSetFeatureStatusesSuccess,
base::Unretained(this)),
base::BindOnce(
&CryptAuthFeatureStatusSetterImpl::OnBatchSetFeatureStatusesFailure,
base::Unretained(this)));
}
void CryptAuthFeatureStatusSetterImpl::OnBatchSetFeatureStatusesSuccess(
const cryptauthv2::BatchSetFeatureStatusesResponse& response) {
DCHECK_EQ(State::kWaitingForBatchSetFeatureStatusesResponse, state_);
PA_LOG(VERBOSE) << "SetFeatureStatus attempt succeeded.";
RecordBatchSetFeatureStatusesMetrics(
base::TimeTicks::Now() - last_state_change_timestamp_,
CryptAuthApiCallResult::kSuccess);
FinishAttempt(std::nullopt /* error */);
}
void CryptAuthFeatureStatusSetterImpl::OnBatchSetFeatureStatusesFailure(
NetworkRequestError error) {
DCHECK_EQ(State::kWaitingForBatchSetFeatureStatusesResponse, state_);
PA_LOG(ERROR) << "BatchSetFeatureStatuses call failed with error " << error
<< ".";
RecordBatchSetFeatureStatusesMetrics(
base::TimeTicks::Now() - last_state_change_timestamp_,
CryptAuthApiCallResultFromNetworkRequestError(error));
FinishAttempt(error);
}
void CryptAuthFeatureStatusSetterImpl::FinishAttempt(
std::optional<NetworkRequestError> error) {
cryptauth_client_.reset();
SetState(State::kIdle);
DCHECK(!pending_requests_.empty());
Request current_request = std::move(pending_requests_.front());
pending_requests_.pop();
if (error) {
std::move(current_request.error_callback).Run(*error);
} else {
std::move(current_request.success_callback).Run();
}
ProcessRequestQueue();
}
std::ostream& operator<<(std::ostream& stream,
const CryptAuthFeatureStatusSetterImpl::State& state) {
switch (state) {
case CryptAuthFeatureStatusSetterImpl::State::kIdle:
stream << "[CryptAuthFeatureStatusSetter state: Idle]";
break;
case CryptAuthFeatureStatusSetterImpl::State::
kWaitingForBatchSetFeatureStatusesResponse:
stream << "[CryptAuthFeatureStatusSetter state: Waiting for "
<< "BatchSetFeatureStatuses response]";
break;
}
return stream;
}
} // namespace device_sync
} // namespace ash