chromium/chromeos/ash/services/device_sync/device_sync_impl.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/services/device_sync/device_sync_impl.h"

#include <optional>

#include "ash/constants/ash_features.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/ranges/algorithm.h"
#include "base/time/default_clock.h"
#include "base/timer/timer.h"
#include "base/unguessable_token.h"
#include "chromeos/ash/components/multidevice/logging/logging.h"
#include "chromeos/ash/components/multidevice/secure_message_delegate_impl.h"
#include "chromeos/ash/services/device_sync/attestation_certificates_syncer_impl.h"
#include "chromeos/ash/services/device_sync/cryptauth_client_impl.h"
#include "chromeos/ash/services/device_sync/cryptauth_device_activity_getter_impl.h"
#include "chromeos/ash/services/device_sync/cryptauth_device_manager_impl.h"
#include "chromeos/ash/services/device_sync/cryptauth_device_notifier_impl.h"
#include "chromeos/ash/services/device_sync/cryptauth_device_registry_impl.h"
#include "chromeos/ash/services/device_sync/cryptauth_enroller_factory_impl.h"
#include "chromeos/ash/services/device_sync/cryptauth_enrollment_manager_impl.h"
#include "chromeos/ash/services/device_sync/cryptauth_feature_status_setter_impl.h"
#include "chromeos/ash/services/device_sync/cryptauth_feature_type.h"
#include "chromeos/ash/services/device_sync/cryptauth_gcm_manager_impl.h"
#include "chromeos/ash/services/device_sync/cryptauth_key_registry_impl.h"
#include "chromeos/ash/services/device_sync/cryptauth_metadata_syncer_impl.h"
#include "chromeos/ash/services/device_sync/cryptauth_scheduler_impl.h"
#include "chromeos/ash/services/device_sync/cryptauth_task_metrics_logger.h"
#include "chromeos/ash/services/device_sync/cryptauth_v2_device_manager_impl.h"
#include "chromeos/ash/services/device_sync/cryptauth_v2_enrollment_manager_impl.h"
#include "chromeos/ash/services/device_sync/device_sync_type_converters.h"
#include "chromeos/ash/services/device_sync/group_private_key_and_better_together_metadata_status.h"
#include "chromeos/ash/services/device_sync/proto/cryptauth_api.pb.h"
#include "chromeos/ash/services/device_sync/proto/device_classifier_util.h"
#include "chromeos/ash/services/device_sync/public/cpp/gcm_device_info_provider.h"
#include "chromeos/ash/services/device_sync/remote_device_provider_impl.h"
#include "chromeos/ash/services/device_sync/software_feature_manager_impl.h"
#include "chromeos/ash/services/device_sync/synced_bluetooth_address_tracker_impl.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/signin/public/base/consent_level.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"

namespace ash {

namespace device_sync {

namespace {

constexpr base::TimeDelta kSetFeatureEnabledTimeout = base::Seconds(5);

// Timeout value for asynchronous operation.
// TODO(https://crbug.com/933656): Use async execution time metrics to tune
// this timeout value.
constexpr base::TimeDelta kWaitingForClientAppMetadataTimeout =
    base::Seconds(60);

constexpr base::TimeDelta kBaseRetryDelay = base::Seconds(5);

// This enum is tied directly to a UMA enum defined in
// //tools/metrics/histograms/enums.xml, and should always reflect it (do not
// change one without changing the other). Entries should be never modified
// or deleted. Only additions possible.
enum class DeviceSyncRequestFailureReason {
  kRequestSucceededButUnexpectedResult = 0,
  kServiceNotYetInitialized = 1,
  kOffline = 2,
  kEndpointNotFound = 3,
  kAuthenticationError = 4,
  kBadRequest = 5,
  kResponseMalformed = 6,
  kInternalServerError = 7,
  kUnknownNetworkError = 8,
  kUnknown = 9,
  kMaxValue = kUnknown
};

// This enum is tied directly to a UMA enum defined in
// //tools/metrics/histograms/enums.xml, and should always reflect it (do not
// change one without changing the other). Entries should be never modified
// or deleted. Only additions possible.
enum class ForceCryptAuthOperationResult {
  kSuccess = 0,
  kServiceNotReady = 1,
  kMaxValue = kServiceNotReady
};

// This enum is tied directly to a UMA enum defined in
// //tools/metrics/histograms/enums.xml, and should always reflect it (do not
// change one without changing the other). Entries should be never modified
// or deleted. Only additions possible.
enum class DeviceSyncSetSoftwareFeature {
  kUnknown = 0,
  kBetterTogetherSuite = 1,
  kSmartLock = 2,
  kInstantTethering = 3,
  kMessages = 4,
  kUnexpectedClientFeature = 5,
  kMaxValue = kUnexpectedClientFeature
};

DeviceSyncRequestFailureReason GetDeviceSyncRequestFailureReason(
    mojom::NetworkRequestResult failure_reason) {
  switch (failure_reason) {
    case mojom::NetworkRequestResult::kRequestSucceededButUnexpectedResult:
      return DeviceSyncRequestFailureReason::
          kRequestSucceededButUnexpectedResult;
    case mojom::NetworkRequestResult::kServiceNotYetInitialized:
      return DeviceSyncRequestFailureReason::kServiceNotYetInitialized;
    case mojom::NetworkRequestResult::kOffline:
      return DeviceSyncRequestFailureReason::kOffline;
    case mojom::NetworkRequestResult::kEndpointNotFound:
      return DeviceSyncRequestFailureReason::kEndpointNotFound;
    case mojom::NetworkRequestResult::kAuthenticationError:
      return DeviceSyncRequestFailureReason::kAuthenticationError;
    case mojom::NetworkRequestResult::kBadRequest:
      return DeviceSyncRequestFailureReason::kBadRequest;
    case mojom::NetworkRequestResult::kResponseMalformed:
      return DeviceSyncRequestFailureReason::kResponseMalformed;
    case mojom::NetworkRequestResult::kInternalServerError:
      return DeviceSyncRequestFailureReason::kInternalServerError;
    case mojom::NetworkRequestResult::kUnknown:
      return DeviceSyncRequestFailureReason::kUnknownNetworkError;
    default:
      return DeviceSyncRequestFailureReason::kUnknown;
  }
  NOTREACHED_IN_MIGRATION();
}

// The exponential back off is: base * 2^(num_failures - 1)
base::TimeDelta GetRetryDelay(size_t num_failures) {
  DCHECK_GT(num_failures, 0u);
  return kBaseRetryDelay * (1 << (num_failures - 1));
}

void RecordGcmRegistrationMetrics(const base::TimeDelta& execution_time,
                                  bool success) {
  base::UmaHistogramCustomTimes(
      "CryptAuth.DeviceSyncService.GcmRegistration.ExecutionTime",
      execution_time, base::Seconds(1) /* min */, base::Minutes(10) /* max */,
      100 /* buckets */);

  base::UmaHistogramBoolean(
      "CryptAuth.DeviceSyncService.GcmRegistration.Success", success);
}

void RecordClientAppMetadataFetchMetrics(const base::TimeDelta& execution_time,
                                         CryptAuthAsyncTaskResult result) {
  base::UmaHistogramCustomTimes(
      "CryptAuth.DeviceSyncService.ClientAppMetadataFetch.ExecutionTime",
      execution_time, base::Seconds(1) /* min */,
      kWaitingForClientAppMetadataTimeout /* max */, 100 /* buckets */);

  LogCryptAuthAsyncTaskSuccessMetric(
      "CryptAuth.DeviceSyncService.ClientAppMetadataFetch.AsyncTaskResult",
      result);
}

void RecordInitializationMetrics(const base::TimeDelta& execution_time) {
  base::UmaHistogramCustomTimes(
      "CryptAuth.DeviceSyncService.Initialization.ExecutionTime",
      execution_time, base::Seconds(1) /* min */,
      kWaitingForClientAppMetadataTimeout /* max */, 100 /* buckets */);
}

void RecordSetSoftwareFeatureStateResult(bool success) {
  UMA_HISTOGRAM_BOOLEAN(
      "MultiDevice.DeviceSyncService.SetSoftwareFeatureState.Result", success);
}

void RecordSetSoftwareFeatureStateResultFailureReason(
    DeviceSyncRequestFailureReason failure_reason) {
  UMA_HISTOGRAM_ENUMERATION(
      "MultiDevice.DeviceSyncService.SetSoftwareFeatureState.Result."
      "FailureReason",
      failure_reason);
}

DeviceSyncSetSoftwareFeature GetDeviceSyncSoftwareFeature(
    multidevice::SoftwareFeature software_feature) {
  switch (software_feature) {
    case multidevice::SoftwareFeature::kBetterTogetherHost:
      return DeviceSyncSetSoftwareFeature::kBetterTogetherSuite;
    case multidevice::SoftwareFeature::kSmartLockHost:
      return DeviceSyncSetSoftwareFeature::kSmartLock;
    case multidevice::SoftwareFeature::kInstantTetheringHost:
      return DeviceSyncSetSoftwareFeature::kInstantTethering;
    case multidevice::SoftwareFeature::kMessagesForWebHost:
      return DeviceSyncSetSoftwareFeature::kMessages;
    default:
      NOTREACHED_IN_MIGRATION();
      return DeviceSyncSetSoftwareFeature::kUnexpectedClientFeature;
  }
}

void RecordSetSoftwareFailedFeature(bool enabled,
                                    multidevice::SoftwareFeature feature) {
  if (enabled) {
    UMA_HISTOGRAM_ENUMERATION(
        "MultiDevice.DeviceSyncService.SetSoftwareFeatureState.Enable."
        "FailedFeature",
        GetDeviceSyncSoftwareFeature(feature));
  } else {
    UMA_HISTOGRAM_ENUMERATION(
        "MultiDevice.DeviceSyncService.SetSoftwareFeatureState.Disable."
        "FailedFeature",
        GetDeviceSyncSoftwareFeature(feature));
  }
}

void RecordFindEligibleDevicesResult(bool success) {
  UMA_HISTOGRAM_BOOLEAN(
      "MultiDevice.DeviceSyncService.FindEligibleDevices.Result", success);
}

void RecordFindEligibleDevicesResultFailureReason(
    DeviceSyncRequestFailureReason failure_reason) {
  UMA_HISTOGRAM_ENUMERATION(
      "MultiDevice.DeviceSyncService.FindEligibleDevices.Result."
      "FailureReason",
      failure_reason);
}

void RecordForceEnrollmentNowResult(ForceCryptAuthOperationResult result) {
  UMA_HISTOGRAM_ENUMERATION(
      "MultiDevice.DeviceSyncService.ForceEnrollmentNow.Result", result);
}

void RecordForceSyncNowResult(ForceCryptAuthOperationResult result) {
  UMA_HISTOGRAM_ENUMERATION("MultiDevice.DeviceSyncService.ForceSyncNow.Result",
                            result);
}

}  // namespace

// static
DeviceSyncImpl::Factory* DeviceSyncImpl::Factory::custom_factory_instance_ =
    nullptr;

// static
std::unique_ptr<DeviceSyncBase> DeviceSyncImpl::Factory::Create(
    signin::IdentityManager* identity_manager,
    instance_id::InstanceIDDriver* instance_id_driver,
    PrefService* profile_prefs,
    const GcmDeviceInfoProvider* gcm_device_info_provider,
    ClientAppMetadataProvider* client_app_metadata_provider,
    scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
    std::unique_ptr<base::OneShotTimer> timer,
    AttestationCertificatesSyncer::GetAttestationCertificatesFunction
        get_attestation_certificates_function) {
  if (custom_factory_instance_) {
    return custom_factory_instance_->CreateInstance(
        identity_manager, instance_id_driver, profile_prefs,
        gcm_device_info_provider, client_app_metadata_provider,
        std::move(url_loader_factory), std::move(timer),
        get_attestation_certificates_function);
  }

  return base::WrapUnique(new DeviceSyncImpl(
      identity_manager, instance_id_driver, profile_prefs,
      gcm_device_info_provider, client_app_metadata_provider,
      std::move(url_loader_factory), base::DefaultClock::GetInstance(),
      std::move(timer), get_attestation_certificates_function));
}

// static
void DeviceSyncImpl::Factory::SetCustomFactory(Factory* custom_factory) {
  custom_factory_instance_ = custom_factory;
}

// static
bool DeviceSyncImpl::Factory::IsCustomFactorySet() {
  return custom_factory_instance_ != nullptr;
}

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

DeviceSyncImpl::PendingSetSoftwareFeatureRequest::
    PendingSetSoftwareFeatureRequest(
        const std::string& device_public_key,
        multidevice::SoftwareFeature software_feature,
        bool enabled,
        RemoteDeviceProvider* remote_device_provider,
        SetSoftwareFeatureStateCallback callback)
    : device_public_key_(device_public_key),
      software_feature_(software_feature),
      enabled_(enabled),
      remote_device_provider_(remote_device_provider),
      callback_(std::move(callback)) {}

DeviceSyncImpl::PendingSetSoftwareFeatureRequest::
    ~PendingSetSoftwareFeatureRequest() = default;

bool DeviceSyncImpl::PendingSetSoftwareFeatureRequest::IsFulfilled() const {
  const auto& synced_devices = remote_device_provider_->GetSyncedDevices();
  const auto devices_it =
      base::ranges::find(synced_devices, device_public_key_,
                         &multidevice::RemoteDevice::public_key);

  // If the device to edit no longer exists, the request is not fulfilled.
  if (devices_it == synced_devices.end())
    return false;

  const auto features_map_it =
      devices_it->software_features.find(software_feature_);

  // If the device does not contain an entry for |software_feature_|, the
  // request is not fulfilled.
  if (features_map_it == devices_it->software_features.end())
    return false;

  if (enabled_)
    return features_map_it->second ==
           multidevice::SoftwareFeatureState::kEnabled;

  return features_map_it->second ==
         multidevice::SoftwareFeatureState::kSupported;
}

void DeviceSyncImpl::PendingSetSoftwareFeatureRequest::InvokeCallback(
    mojom::NetworkRequestResult result) {
  // Callback should only be invoked once.
  DCHECK(callback_);
  std::move(callback_).Run(result);
}

DeviceSyncImpl::PendingSetFeatureStatusRequest::PendingSetFeatureStatusRequest(
    const std::string& device_instance_id,
    multidevice::SoftwareFeature software_feature,
    FeatureStatusChange status_change,
    RemoteDeviceProvider* remote_device_provider,
    SetFeatureStatusCallback callback)
    : device_instance_id_(device_instance_id),
      software_feature_(software_feature),
      status_change_(status_change),
      remote_device_provider_(remote_device_provider),
      callback_(std::move(callback)) {
  DCHECK(!device_instance_id.empty());
}

DeviceSyncImpl::PendingSetFeatureStatusRequest::
    ~PendingSetFeatureStatusRequest() = default;

bool DeviceSyncImpl::PendingSetFeatureStatusRequest::IsFulfilled() const {
  // True if the device from the request is included in the synced-devices list.
  bool is_requested_device_in_list = false;

  // True if the feature from the request is enabled on the device from the
  // request.
  bool is_feature_enabled_for_requested_device = false;

  // True if the feature from the request is enabled on any synced device other
  // than the device from the request.
  bool is_feature_enabled_for_any_other_device = false;

  for (const multidevice::RemoteDevice& remote_device :
       remote_device_provider_->GetSyncedDevices()) {
    const auto it = remote_device.software_features.find(software_feature_);
    bool is_feature_set_for_device =
        it != remote_device.software_features.end();
    bool is_feature_enabled_for_device =
        is_feature_set_for_device &&
        it->second == multidevice::SoftwareFeatureState::kEnabled;

    if (device_instance_id_ == remote_device.instance_id) {
      DCHECK(!is_requested_device_in_list);
      is_requested_device_in_list = true;

      // If the requested device does not contain an entry for
      // |software_feature_|, the request is not fulfilled.
      if (!is_feature_set_for_device)
        return false;

      is_feature_enabled_for_requested_device = is_feature_enabled_for_device;
    } else {
      is_feature_enabled_for_any_other_device =
          is_feature_enabled_for_any_other_device ||
          is_feature_enabled_for_device;
    }
  }

  // If the requested device no longer exists, the request is not fulfilled.
  if (!is_requested_device_in_list)
    return false;

  switch (status_change_) {
    case FeatureStatusChange::kEnableExclusively:
      return is_feature_enabled_for_requested_device &&
             !is_feature_enabled_for_any_other_device;
    case FeatureStatusChange::kEnableNonExclusively:
      return is_feature_enabled_for_requested_device;
    case FeatureStatusChange::kDisable:
      return !is_feature_enabled_for_requested_device;
  }
}

void DeviceSyncImpl::PendingSetFeatureStatusRequest::InvokeCallback(
    mojom::NetworkRequestResult result) {
  // Callback should only be invoked once.
  DCHECK(callback_);
  std::move(callback_).Run(result);
}

DeviceSyncImpl::DeviceSyncImpl(
    signin::IdentityManager* identity_manager,
    instance_id::InstanceIDDriver* instance_id_driver,
    PrefService* profile_prefs,
    const GcmDeviceInfoProvider* gcm_device_info_provider,
    ClientAppMetadataProvider* client_app_metadata_provider,
    scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
    base::Clock* clock,
    std::unique_ptr<base::OneShotTimer> timer,
    AttestationCertificatesSyncer::GetAttestationCertificatesFunction
        get_attestation_certificates_function)
    : DeviceSyncBase(),
      identity_manager_(identity_manager),
      instance_id_driver_(instance_id_driver),
      profile_prefs_(profile_prefs),
      gcm_device_info_provider_(gcm_device_info_provider),
      client_app_metadata_provider_(client_app_metadata_provider),
      url_loader_factory_(std::move(url_loader_factory)),
      clock_(clock),
      timer_(std::move(timer)),
      get_attestation_certificates_function_(
          get_attestation_certificates_function) {
  DCHECK(profile_prefs_);
  PA_LOG(VERBOSE) << "DeviceSyncImpl: Initializing.";
  initialization_start_timestamp_ = base::TimeTicks::Now();
  RunNextInitializationStep();
}

DeviceSyncImpl::~DeviceSyncImpl() {
  if (cryptauth_enrollment_manager_)
    cryptauth_enrollment_manager_->RemoveObserver(this);

  if (remote_device_provider_)
    remote_device_provider_->RemoveObserver(this);

  if (identity_manager_)
    identity_manager_->RemoveObserver(this);  // No-op if we aren't observing.
}

void DeviceSyncImpl::ForceEnrollmentNow(ForceEnrollmentNowCallback callback) {
  if (status_ != InitializationStatus::kReady) {
    PA_LOG(WARNING) << "DeviceSyncImpl::ForceEnrollmentNow() invoked before "
                    << "initialization was complete. Cannot force enrollment.";
    std::move(callback).Run(false /* success */);
    RecordForceEnrollmentNowResult(
        ForceCryptAuthOperationResult::kServiceNotReady /* result */);
    return;
  }

  cryptauth_enrollment_manager_->ForceEnrollmentNow(
      cryptauth::INVOCATION_REASON_MANUAL, std::nullopt /* session_id */);
  std::move(callback).Run(true /* success */);
  RecordForceEnrollmentNowResult(
      ForceCryptAuthOperationResult::kSuccess /* result */);
}

void DeviceSyncImpl::ForceSyncNow(ForceSyncNowCallback callback) {
  if (status_ != InitializationStatus::kReady) {
    PA_LOG(WARNING) << "DeviceSyncImpl::ForceSyncNow() invoked before "
                    << "initialization was complete. Cannot force sync.";
    std::move(callback).Run(false /* success */);
    RecordForceSyncNowResult(
        ForceCryptAuthOperationResult::kServiceNotReady /* result */);
    return;
  }

  if (features::ShouldUseV1DeviceSync()) {
    cryptauth_device_manager_->ForceSyncNow(
        cryptauth::INVOCATION_REASON_MANUAL);
  }

  if (features::ShouldUseV2DeviceSync()) {
    cryptauth_v2_device_manager_->ForceDeviceSyncNow(
        cryptauthv2::ClientMetadata::MANUAL, std::nullopt /* session_id */);
  }

  std::move(callback).Run(true /* success */);
  RecordForceSyncNowResult(
      ForceCryptAuthOperationResult::kSuccess /* result */);
}

void DeviceSyncImpl::GetGroupPrivateKeyStatus(
    GetGroupPrivateKeyStatusCallback callback) {
  DCHECK(features::ShouldUseV2DeviceSync);

  if (status_ != InitializationStatus::kReady) {
    PA_LOG(WARNING) << "DeviceSyncImpl::GetGroupPrivateKeyStatus() invoked "
                       "before initialization was complete. Cannot return "
                       "group private key status.";
    std::move(callback).Run(
        GroupPrivateKeyStatus::
            kStatusUnavailableBecauseDeviceSyncIsNotInitialized);
    return;
  }

  std::move(callback).Run(
      cryptauth_v2_device_manager_->GetDeviceSyncerGroupPrivateKeyStatus());
}

void DeviceSyncImpl::GetBetterTogetherMetadataStatus(
    GetBetterTogetherMetadataStatusCallback callback) {
  DCHECK(features::ShouldUseV2DeviceSync);

  if (status_ != InitializationStatus::kReady) {
    PA_LOG(WARNING)
        << "DeviceSyncImpl::GetBetterTogetherMetadataStatus() invoked "
           "before initialization was complete. Cannot return "
           "better together metadata status.";
    std::move(callback).Run(
        BetterTogetherMetadataStatus::
            kStatusUnavailableBecauseDeviceSyncIsNotInitialized);
    return;
  }

  std::move(callback).Run(cryptauth_v2_device_manager_
                              ->GetDeviceSyncerBetterTogetherMetadataStatus());
}

void DeviceSyncImpl::GetLocalDeviceMetadata(
    GetLocalDeviceMetadataCallback callback) {
  if (status_ != InitializationStatus::kReady) {
    PA_LOG(WARNING) << "DeviceSyncImpl::GetLocalDeviceMetadata() invoked "
                    << "before initialization was complete. Cannot return "
                    << "local device metadata.";
    std::move(callback).Run(std::nullopt);
    return;
  }

  std::string public_key = cryptauth_enrollment_manager_->GetUserPublicKey();
  DCHECK(!public_key.empty());
  std::move(callback).Run(GetSyncedDeviceWithPublicKey(public_key));
}

void DeviceSyncImpl::GetSyncedDevices(GetSyncedDevicesCallback callback) {
  if (status_ != InitializationStatus::kReady) {
    PA_LOG(WARNING) << "DeviceSyncImpl::GetSyncedDevices() invoked before "
                    << "initialization was complete. Cannot return devices.";
    std::move(callback).Run(std::nullopt);
    return;
  }

  std::move(callback).Run(remote_device_provider_->GetSyncedDevices());
}

void DeviceSyncImpl::SetSoftwareFeatureState(
    const std::string& device_public_key,
    multidevice::SoftwareFeature software_feature,
    bool enabled,
    bool is_exclusive,
    SetSoftwareFeatureStateCallback callback) {
  DCHECK(features::ShouldUseV1DeviceSync());

  if (status_ != InitializationStatus::kReady) {
    PA_LOG(WARNING) << "DeviceSyncImpl::SetSoftwareFeatureState() invoked "
                    << "before initialization was complete. Cannot set state.";
    std::move(callback).Run(
        mojom::NetworkRequestResult::kServiceNotYetInitialized);

    RecordSetSoftwareFeatureStateResult(false /* success */);
    RecordSetSoftwareFeatureStateResultFailureReason(
        DeviceSyncRequestFailureReason::kServiceNotYetInitialized);
    RecordSetSoftwareFailedFeature(enabled, software_feature);
    return;
  }

  auto request_id = base::UnguessableToken::Create();
  id_to_pending_set_software_feature_request_map_.emplace(
      request_id, std::make_unique<PendingSetSoftwareFeatureRequest>(
                      device_public_key, software_feature, enabled,
                      remote_device_provider_.get(), std::move(callback)));
  StartSetSoftwareFeatureTimer();

  software_feature_manager_->SetSoftwareFeatureState(
      device_public_key, software_feature, enabled,
      base::BindOnce(&DeviceSyncImpl::OnSetSoftwareFeatureStateSuccess,
                     weak_ptr_factory_.GetWeakPtr()),
      base::BindOnce(&DeviceSyncImpl::OnSetSoftwareFeatureStateError,
                     weak_ptr_factory_.GetWeakPtr(), request_id),
      is_exclusive);
}

void DeviceSyncImpl::SetFeatureStatus(const std::string& device_instance_id,
                                      multidevice::SoftwareFeature feature,
                                      FeatureStatusChange status_change,
                                      SetFeatureStatusCallback callback) {
  DCHECK(features::ShouldUseV2DeviceSync);
  DCHECK(!device_instance_id.empty());

  if (status_ != InitializationStatus::kReady) {
    PA_LOG(WARNING) << "DeviceSyncImpl::SetFeatureStatus() invoked before "
                    << "initialization was complete. Cannot enable/disable "
                    << "feature.";
    std::move(callback).Run(
        mojom::NetworkRequestResult::kServiceNotYetInitialized);
    return;
  }

  auto request_id = base::UnguessableToken::Create();
  id_to_pending_set_feature_status_request_map_.emplace(
      request_id, std::make_unique<PendingSetFeatureStatusRequest>(
                      device_instance_id, feature, status_change,
                      remote_device_provider_.get(), std::move(callback)));

  // Before v1 DeviceSync is disabled, we need to use the
  // CryptAuthFeatureStatusSetter indirectly via the SoftwareFeatureManager to
  // ensure an ordering of SetSoftwareFeatureState() and SetFeatureStatus()
  // calls. These two functions have similar effects on the CryptAuth backend,
  // so the order of the calls matters. For example, say that, during setup, we
  // select a device without an Instance ID to be the multi-device host, then we
  // change our mind and select a device with an Instance ID. These calls to
  // SetSoftwareFeatureState() and SetFeatureStatus(), respectively, need to be
  // ordered so that the device with the Instance ID will always be set as the
  // multi-device host. When v1 DeviceSync is disabled,
  // SetSoftwareFeatureState() will not longer be called, and the queue
  // maintained by the FeatureStatusSetter will be sufficient.
  if (features::ShouldUseV1DeviceSync()) {
    software_feature_manager_->SetFeatureStatus(
        device_instance_id, feature, status_change,
        base::BindOnce(&DeviceSyncImpl::OnSetFeatureStatusSuccess,
                       weak_ptr_factory_.GetWeakPtr()),
        base::BindOnce(&DeviceSyncImpl::OnSetFeatureStatusError,
                       weak_ptr_factory_.GetWeakPtr(), request_id));
  } else {
    feature_status_setter_->SetFeatureStatus(
        device_instance_id, feature, status_change,
        base::BindOnce(&DeviceSyncImpl::OnSetFeatureStatusSuccess,
                       weak_ptr_factory_.GetWeakPtr()),
        base::BindOnce(&DeviceSyncImpl::OnSetFeatureStatusError,
                       weak_ptr_factory_.GetWeakPtr(), request_id));
  }
}

void DeviceSyncImpl::FindEligibleDevices(
    multidevice::SoftwareFeature software_feature,
    FindEligibleDevicesCallback callback) {
  DCHECK(features::ShouldUseV1DeviceSync());

  if (status_ != InitializationStatus::kReady) {
    PA_LOG(WARNING) << "DeviceSyncImpl::FindEligibleDevices() invoked before "
                    << "initialization was complete. Cannot find devices.";
    std::move(callback).Run(
        mojom::NetworkRequestResult::kServiceNotYetInitialized,
        nullptr /* response */);
    return;
  }

  auto split_callback = base::SplitOnceCallback(std::move(callback));
  software_feature_manager_->FindEligibleDevices(
      software_feature,
      base::BindOnce(&DeviceSyncImpl::OnFindEligibleDevicesSuccess,
                     weak_ptr_factory_.GetWeakPtr(),
                     std::move(split_callback.first)),
      base::BindOnce(&DeviceSyncImpl::OnFindEligibleDevicesError,
                     weak_ptr_factory_.GetWeakPtr(),
                     std::move(split_callback.second)));
}

void DeviceSyncImpl::NotifyDevices(
    const std::vector<std::string>& device_instance_ids,
    cryptauthv2::TargetService target_service,
    multidevice::SoftwareFeature feature,
    NotifyDevicesCallback callback) {
  DCHECK(features::ShouldUseV2DeviceSync);

  if (status_ != InitializationStatus::kReady) {
    PA_LOG(WARNING) << "DeviceSyncImpl::NotifyDevices() invoked before "
                    << "initialization was complete. Cannot notify devices.";
    std::move(callback).Run(
        mojom::NetworkRequestResult::kServiceNotYetInitialized);
    return;
  }

  auto request_id = base::UnguessableToken::Create();
  pending_notify_devices_callbacks_.emplace(request_id, std::move(callback));

  device_notifier_->NotifyDevices(
      device_instance_ids, target_service,
      CryptAuthFeatureTypeFromSoftwareFeature(feature),
      base::BindOnce(&DeviceSyncImpl::OnNotifyDevicesSuccess,
                     weak_ptr_factory_.GetWeakPtr(), request_id),
      base::BindOnce(&DeviceSyncImpl::OnNotifyDevicesError,
                     weak_ptr_factory_.GetWeakPtr(), request_id));
}

void DeviceSyncImpl::GetDevicesActivityStatus(
    GetDevicesActivityStatusCallback callback) {
  if (status_ != InitializationStatus::kReady) {
    PA_LOG(WARNING)
        << "DeviceSyncImpl::GetDevicesActivityStatus() invoked before "
        << "initialization was complete. Cannot get activity statuses.";
    std::move(callback).Run(
        mojom::NetworkRequestResult::kServiceNotYetInitialized,
        std::nullopt /* device_activity_statuses */);
    return;
  }

  auto request_id = base::UnguessableToken::Create();
  get_devices_activity_status_callbacks_.emplace(request_id,
                                                 std::move(callback));

  cryptauth_device_activity_getter_ =
      CryptAuthDeviceActivityGetterImpl::Factory::Create(
          client_app_metadata_->instance_id(),
          client_app_metadata_->instance_id_token(),
          cryptauth_client_factory_.get());

  cryptauth_device_activity_getter_->GetDevicesActivityStatus(
      base::BindOnce(&DeviceSyncImpl::OnGetDevicesActivityStatusFinished,
                     weak_ptr_factory_.GetWeakPtr(), request_id),
      base::BindOnce(&DeviceSyncImpl::OnGetDevicesActivityStatusError,
                     weak_ptr_factory_.GetWeakPtr(), request_id));
}

void DeviceSyncImpl::GetDebugInfo(GetDebugInfoCallback callback) {
  if (status_ != InitializationStatus::kReady) {
    PA_LOG(WARNING) << "DeviceSyncImpl::GetDebugInfo() invoked before "
                    << "initialization was complete. Cannot provide info.";
    std::move(callback).Run(nullptr);
    return;
  }

  if (features::ShouldUseV2DeviceSync()) {
    std::move(callback).Run(mojom::DebugInfo::New(
        cryptauth_enrollment_manager_->GetLastEnrollmentTime(),
        cryptauth_enrollment_manager_->GetTimeToNextAttempt(),
        cryptauth_enrollment_manager_->IsRecoveringFromFailure(),
        cryptauth_enrollment_manager_->IsEnrollmentInProgress(),
        cryptauth_v2_device_manager_->GetLastDeviceSyncTime().value_or(
            base::Time()),
        cryptauth_v2_device_manager_->GetTimeToNextAttempt().value_or(
            base::TimeDelta::Max()),
        cryptauth_v2_device_manager_->IsRecoveringFromFailure(),
        cryptauth_v2_device_manager_->IsDeviceSyncInProgress()));
  } else {
    std::move(callback).Run(mojom::DebugInfo::New(
        cryptauth_enrollment_manager_->GetLastEnrollmentTime(),
        cryptauth_enrollment_manager_->GetTimeToNextAttempt(),
        cryptauth_enrollment_manager_->IsRecoveringFromFailure(),
        cryptauth_enrollment_manager_->IsEnrollmentInProgress(),
        cryptauth_device_manager_->GetLastSyncTime(),
        cryptauth_device_manager_->GetTimeToNextAttempt(),
        cryptauth_device_manager_->IsRecoveringFromFailure(),
        cryptauth_device_manager_->IsSyncInProgress()));
  }
}

void DeviceSyncImpl::OnEnrollmentFinished(bool success) {
  PA_LOG(VERBOSE) << "DeviceSyncImpl: Enrollment finished; success = "
                  << success;

  if (!success)
    return;

  if (status_ == InitializationStatus::kWaitingForEnrollment)
    RunNextInitializationStep();

  NotifyOnEnrollmentFinished();
}

void DeviceSyncImpl::OnSyncDeviceListChanged() {
  PA_LOG(VERBOSE) << "DeviceSyncImpl: Synced devices changed; notifying "
                  << "observers.";
  NotifyOnNewDevicesSynced();

  // Iterate through pending SetSoftwareFeature() requests. If any of them have
  // been fulfilled, invoke their callbacks.
  for (auto it = id_to_pending_set_software_feature_request_map_.begin();
       it != id_to_pending_set_software_feature_request_map_.end();) {
    if (!it->second->IsFulfilled()) {
      ++it;
      continue;
    }

    PA_LOG(VERBOSE)
        << "DeviceSyncImpl::OnSyncDeviceListChanged(): Feature state "
        << "updated via device sync; notifying success callbacks.";
    it->second->InvokeCallback(mojom::NetworkRequestResult::kSuccess);
    it = id_to_pending_set_software_feature_request_map_.erase(it);
  }

  // Iterate through pending SetFeatureStatus() requests. If any of them have
  // been fulfilled, invoke their callbacks.
  for (auto it = id_to_pending_set_feature_status_request_map_.begin();
       it != id_to_pending_set_feature_status_request_map_.end();) {
    if (!it->second->IsFulfilled()) {
      ++it;
      continue;
    }

    PA_LOG(VERBOSE) << "DeviceSyncImpl::OnSyncDeviceListChanged(): Feature "
                    << "status updated via device sync; notifying success "
                    << "callbacks.";
    it->second->InvokeCallback(mojom::NetworkRequestResult::kSuccess);
    it = id_to_pending_set_feature_status_request_map_.erase(it);
  }
}

void DeviceSyncImpl::Shutdown() {
  cryptauth_device_activity_getter_.reset();
  software_feature_manager_.reset();
  feature_status_setter_.reset();
  device_notifier_.reset();
  remote_device_provider_.reset();
  cryptauth_device_manager_.reset();
  cryptauth_enrollment_manager_.reset();
  cryptauth_v2_device_manager_.reset();
  cryptauth_device_registry_.reset();
  cryptauth_scheduler_.reset();
  cryptauth_key_registry_.reset();
  cryptauth_client_factory_.reset();
  cryptauth_gcm_manager_.reset();

  identity_manager_ = nullptr;
  instance_id_driver_ = nullptr;
  profile_prefs_ = nullptr;
  gcm_device_info_provider_ = nullptr;
  client_app_metadata_provider_ = nullptr;
  url_loader_factory_ = nullptr;
  clock_ = nullptr;
}

void DeviceSyncImpl::OnPrimaryAccountChanged(
    const signin::PrimaryAccountChangeEvent& event) {
  PA_LOG(VERBOSE) << "DeviceSyncImpl: OnPrimaryAccountChanged";
  switch (event.GetEventTypeFor(signin::ConsentLevel::kSignin)) {
    case signin::PrimaryAccountChangeEvent::Type::kSet:
      identity_manager_->RemoveObserver(this);
      ProcessPrimaryAccountInfo(event.GetCurrentState().primary_account);
      break;
    case signin::PrimaryAccountChangeEvent::Type::kCleared:
      [[fallthrough]];
    case signin::PrimaryAccountChangeEvent::Type::kNone:
      // Ignored
      break;
  }
}

void DeviceSyncImpl::RunNextInitializationStep() {
  switch (status_) {
    case InitializationStatus::kNotStarted:
      FetchAccountInfo();
      break;
    case InitializationStatus::kFetchingAccountInfo:
      RegisterWithGcm();
      break;
    case InitializationStatus::kWaitingForGcmRegistration:
      if (base::FeatureList::IsEnabled(features::kCryptAuthV2Enrollment)) {
        FetchClientAppMetadata();
      } else {
        WaitForValidEnrollment();
      }
      break;
    case InitializationStatus::kWaitingForClientAppMetadata:
      WaitForValidEnrollment();
      break;
    case InitializationStatus::kWaitingForEnrollment:
      CompleteInitializationAfterSuccessfulEnrollment();
      break;
    case InitializationStatus::kReady:
      NOTREACHED_IN_MIGRATION();
      break;
  }
}

void DeviceSyncImpl::FetchAccountInfo() {
  status_ = InitializationStatus::kFetchingAccountInfo;

  // "Unconsented" because this feature is not tied to browser sync consent.
  CoreAccountInfo primary_account =
      identity_manager_->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin);
  if (primary_account.account_id.empty()) {
    // Primary profile not loaded yet. This happens when adding a new account.
    PA_LOG(VERBOSE) << "DeviceSyncImpl: Waiting for primary account info";
    identity_manager_->AddObserver(this);
  } else {
    // Profile is ready immediately. This occurs during normal login and during
    // the browser crash-and-restore flow.
    ProcessPrimaryAccountInfo(primary_account);
  }
}

void DeviceSyncImpl::ProcessPrimaryAccountInfo(
    const CoreAccountInfo& primary_account_info) {
  DCHECK_EQ(status_, InitializationStatus::kFetchingAccountInfo);
  if (primary_account_info.account_id.empty()) {
    PA_LOG(ERROR)
        << "No primary account information available; cannot proceed.";

    // TODO(jamescook): This early exit was originally added to work around
    // browser_tests failures. Those don't happen any more. However, I am
    // uncertain how primary account ids work for non-GAIA logins like Active
    // Directory, and I can't figure out how to test them, so I'm leaving
    // this here.
    return;
  }

  primary_account_info_ = primary_account_info;
  PA_LOG(VERBOSE) << "DeviceSyncImpl: Profile initialized.";

  RunNextInitializationStep();
}

void DeviceSyncImpl::RegisterWithGcm() {
  status_ = InitializationStatus::kWaitingForGcmRegistration;

  if (!cryptauth_gcm_manager_) {
    // Initialize |cryptauth_gcm_manager_| and have it start listening for GCM
    // tickles.
    cryptauth_gcm_manager_ = CryptAuthGCMManagerImpl::Factory::Create(
        instance_id_driver_, profile_prefs_);
    cryptauth_gcm_manager_->StartListening();
  }

  // The device previously completed GCM registration, and that registration
  // id is not deprecated.
  const std::string& registration_id =
      cryptauth_gcm_manager_->GetRegistrationId();
  if (!registration_id.empty() &&
      !CryptAuthGCMManager::IsRegistrationIdDeprecated(registration_id)) {
    RunNextInitializationStep();
    return;
  }

  // The registration result is sent to observers via OnGCMRegistrationResult().
  cryptauth_gcm_manager_->AddObserver(this);

  DCHECK(cryptauth_gcm_manager_->IsListening());
  PA_LOG(VERBOSE) << "DeviceSyncImpl: Registering with GCM.";
  gcm_registration_start_timestamp_ = base::TimeTicks::Now();
  cryptauth_gcm_manager_->RegisterWithGCM();
}

void DeviceSyncImpl::OnGCMRegistrationResult(bool success) {
  if (status_ != InitializationStatus::kWaitingForGcmRegistration)
    return;

  cryptauth_gcm_manager_->RemoveObserver(this);

  bool was_successful =
      success && !cryptauth_gcm_manager_->GetRegistrationId().empty();
  RecordGcmRegistrationMetrics(
      base::TimeTicks::Now() - gcm_registration_start_timestamp_,
      was_successful);

  if (!was_successful) {
    ++num_gcm_registration_failures_;
    PA_LOG(ERROR) << "DeviceSyncImpl: GCM registration failed. Num failures: "
                  << num_gcm_registration_failures_;
    timer_->Start(FROM_HERE, GetRetryDelay(num_gcm_registration_failures_),
                  base::BindOnce(&DeviceSyncImpl::RegisterWithGcm,
                                 base::Unretained(this)));
    return;
  }

  PA_LOG(VERBOSE) << "DeviceSyncImpl: GCM registration succeeded.";
  RunNextInitializationStep();
}

void DeviceSyncImpl::FetchClientAppMetadata() {
  DCHECK(base::FeatureList::IsEnabled(features::kCryptAuthV2Enrollment));
  status_ = InitializationStatus::kWaitingForClientAppMetadata;

  timer_->Start(FROM_HERE, kWaitingForClientAppMetadataTimeout,
                base::BindOnce(&DeviceSyncImpl::OnClientAppMetadataFetchTimeout,
                               weak_ptr_factory_.GetWeakPtr()));

  // NOTE(crbug.com/1006280): It is very important that the GCM manager is
  // listening before fetching ClientAppMetadata. Otherwise, the user's Instance
  // ID, which is assumed to be stable, might be deleted.
  DCHECK(cryptauth_gcm_manager_->IsListening());
  PA_LOG(VERBOSE) << "DeviceSyncImpl: Fetching ClientAppMetadata.";
  client_app_metadata_fetch_start_timestamp_ = base::TimeTicks::Now();
  client_app_metadata_provider_->GetClientAppMetadata(
      cryptauth_gcm_manager_->GetRegistrationId(),
      base::BindOnce(&DeviceSyncImpl::OnClientAppMetadataFetched,
                     weak_ptr_factory_.GetWeakPtr()));
}

void DeviceSyncImpl::OnClientAppMetadataFetchTimeout() {
  DCHECK_EQ(status_, InitializationStatus::kWaitingForClientAppMetadata);
  weak_ptr_factory_.InvalidateWeakPtrs();
  PA_LOG(VERBOSE) << "DeviceSyncImpl: ClientAppMetadata fetch timed out.";
  RecordClientAppMetadataFetchMetrics(
      base::TimeTicks::Now() - client_app_metadata_fetch_start_timestamp_,
      CryptAuthAsyncTaskResult::kTimeout);

  OnClientAppMetadataFetchFailure();
}

void DeviceSyncImpl::OnClientAppMetadataFetched(
    const std::optional<cryptauthv2::ClientAppMetadata>& client_app_metadata) {
  DCHECK_EQ(status_, InitializationStatus::kWaitingForClientAppMetadata);
  timer_->Stop();

  bool success = client_app_metadata.has_value();
  CryptAuthAsyncTaskResult result = success ? CryptAuthAsyncTaskResult::kSuccess
                                            : CryptAuthAsyncTaskResult::kError;
  RecordClientAppMetadataFetchMetrics(
      base::TimeTicks::Now() - client_app_metadata_fetch_start_timestamp_,
      result);

  if (!success) {
    OnClientAppMetadataFetchFailure();
    return;
  }

  client_app_metadata_ = client_app_metadata;
  RunNextInitializationStep();
}

void DeviceSyncImpl::OnClientAppMetadataFetchFailure() {
  DCHECK_EQ(status_, InitializationStatus::kWaitingForClientAppMetadata);
  ++num_client_app_metadata_fetch_failures_;
  PA_LOG(ERROR)
      << "DeviceSyncImpl: ClientAppMetadata fetch failed. Num failures: "
      << num_client_app_metadata_fetch_failures_;
  timer_->Start(FROM_HERE,
                GetRetryDelay(num_client_app_metadata_fetch_failures_),
                base::BindOnce(&DeviceSyncImpl::FetchClientAppMetadata,
                               base::Unretained(this)));
}

void DeviceSyncImpl::WaitForValidEnrollment() {
  status_ = InitializationStatus::kWaitingForEnrollment;
  InitializeCryptAuthManagementObjects();

  // If enrollment has not yet completed successfully, initialization cannot
  // continue. Once enrollment has finished, OnEnrollmentFinished() is invoked,
  // which finishes the initialization flow.
  if (!cryptauth_enrollment_manager_->IsEnrollmentValid()) {
    PA_LOG(VERBOSE) << "DeviceSyncImpl: Waiting for enrollment to complete.";
    return;
  }

  RunNextInitializationStep();
}

void DeviceSyncImpl::InitializeCryptAuthManagementObjects() {
  DCHECK_EQ(status_, InitializationStatus::kWaitingForEnrollment);

  cryptauth_client_factory_ = std::make_unique<CryptAuthClientFactoryImpl>(
      identity_manager_, url_loader_factory_,
      device_classifier_util::GetDeviceClassifier());

  // Initialize |cryptauth_enrollment_manager_| and start observing, then call
  // Start() immediately to schedule enrollment.
  if (base::FeatureList::IsEnabled(features::kCryptAuthV2Enrollment)) {
    cryptauth_key_registry_ =
        CryptAuthKeyRegistryImpl::Factory::Create(profile_prefs_);

    cryptauth_scheduler_ =
        CryptAuthSchedulerImpl::Factory::Create(profile_prefs_);

    cryptauth_enrollment_manager_ =
        CryptAuthV2EnrollmentManagerImpl::Factory::Create(
            *client_app_metadata_, cryptauth_key_registry_.get(),
            cryptauth_client_factory_.get(), cryptauth_gcm_manager_.get(),
            cryptauth_scheduler_.get(), profile_prefs_, clock_);
  } else {
    cryptauth_enrollment_manager_ =
        CryptAuthEnrollmentManagerImpl::Factory::Create(
            clock_,
            std::make_unique<CryptAuthEnrollerFactoryImpl>(
                cryptauth_client_factory_.get()),
            multidevice::SecureMessageDelegateImpl::Factory::Create(),
            gcm_device_info_provider_->GetGcmDeviceInfo(),
            cryptauth_gcm_manager_.get(), profile_prefs_);
  }

  // Initialize v1 and v2 CryptAuth device managers (depending on feature
  // flags). Start() is not called yet since the device has not completed
  // enrollment.
  if (features::ShouldUseV1DeviceSync()) {
    cryptauth_device_manager_ = CryptAuthDeviceManagerImpl::Factory::Create(
        clock_, cryptauth_client_factory_.get(), cryptauth_gcm_manager_.get(),
        profile_prefs_);
  }
  if (features::ShouldUseV2DeviceSync()) {
    cryptauth_device_registry_ =
        CryptAuthDeviceRegistryImpl::Factory::Create(profile_prefs_);

    cryptauth_v2_device_manager_ =
        CryptAuthV2DeviceManagerImpl::Factory::Create(
            *client_app_metadata_, cryptauth_device_registry_.get(),
            cryptauth_key_registry_.get(), cryptauth_client_factory_.get(),
            cryptauth_gcm_manager_.get(), cryptauth_scheduler_.get(),
            profile_prefs_, get_attestation_certificates_function_);
  }

  cryptauth_enrollment_manager_->AddObserver(this);
  cryptauth_enrollment_manager_->Start();
}

void DeviceSyncImpl::CompleteInitializationAfterSuccessfulEnrollment() {
  DCHECK_EQ(status_, InitializationStatus::kWaitingForEnrollment);
  DCHECK(cryptauth_enrollment_manager_->IsEnrollmentValid());

  // Now that enrollment has completed, the current device has been registered
  // with the CryptAuth back-end and can begin monitoring synced devices.
  if (features::ShouldUseV1DeviceSync()) {
    cryptauth_device_manager_->Start();
  }
  if (features::ShouldUseV2DeviceSync()) {
    cryptauth_v2_device_manager_->Start();
  }

  remote_device_provider_ = RemoteDeviceProviderImpl::Factory::Create(
      cryptauth_device_manager_.get(), cryptauth_v2_device_manager_.get(),
      primary_account_info_.email,
      cryptauth_enrollment_manager_->GetUserPrivateKey());
  remote_device_provider_->AddObserver(this);

  if (features::ShouldUseV2DeviceSync()) {
    feature_status_setter_ = CryptAuthFeatureStatusSetterImpl::Factory::Create(
        client_app_metadata_->instance_id(),
        client_app_metadata_->instance_id_token(),
        cryptauth_client_factory_.get());

    device_notifier_ = CryptAuthDeviceNotifierImpl::Factory::Create(
        client_app_metadata_->instance_id(),
        client_app_metadata_->instance_id_token(),
        cryptauth_client_factory_.get());
  }

  if (features::ShouldUseV1DeviceSync()) {
    software_feature_manager_ = SoftwareFeatureManagerImpl::Factory::Create(
        cryptauth_client_factory_.get(), feature_status_setter_.get());
  }

  status_ = InitializationStatus::kReady;
  PA_LOG(VERBOSE) << "DeviceSyncImpl: CryptAuth Enrollment is valid; service "
                  << "fully initialized.";
  RecordInitializationMetrics(base::TimeTicks::Now() -
                              initialization_start_timestamp_);
}

std::optional<multidevice::RemoteDevice>
DeviceSyncImpl::GetSyncedDeviceWithPublicKey(
    const std::string& public_key) const {
  DCHECK_EQ(status_, InitializationStatus::kReady)
      << "DeviceSyncImpl::GetSyncedDeviceWithPublicKey() called before ready.";

  const auto& synced_devices = remote_device_provider_->GetSyncedDevices();
  const auto it = base::ranges::find(synced_devices, public_key,
                                     &multidevice::RemoteDevice::public_key);

  if (it == synced_devices.end())
    return std::nullopt;

  return *it;
}

void DeviceSyncImpl::OnSetSoftwareFeatureStateSuccess() {
  DCHECK(features::ShouldUseV1DeviceSync());

  PA_LOG(VERBOSE) << "DeviceSyncImpl::OnSetSoftwareFeatureStateSuccess(): "
                  << "Successfully completed SetSoftwareFeatureState() call; "
                  << "requesting force sync.";

  cryptauth_device_manager_->ForceSyncNow(
      cryptauth::INVOCATION_REASON_FEATURE_TOGGLED);

  if (features::ShouldUseV2DeviceSync()) {
    cryptauth_v2_device_manager_->ForceDeviceSyncNow(
        cryptauthv2::ClientMetadata::FEATURE_TOGGLED,
        std::nullopt /* session_id */);
  }

  RecordSetSoftwareFeatureStateResult(true /* success */);
}

void DeviceSyncImpl::OnSetSoftwareFeatureStateError(
    const base::UnguessableToken& request_id,
    NetworkRequestError error) {
  DCHECK(features::ShouldUseV1DeviceSync());

  auto it = id_to_pending_set_software_feature_request_map_.find(request_id);
  if (it == id_to_pending_set_software_feature_request_map_.end()) {
    PA_LOG(ERROR) << "DeviceSyncImpl::OnSetSoftwareFeatureStateError(): "
                  << "Could not find request entry with ID " << request_id;
    NOTREACHED_IN_MIGRATION();
    return;
  }

  RecordSetSoftwareFeatureStateResult(false /* success */);
  RecordSetSoftwareFeatureStateResultFailureReason(
      GetDeviceSyncRequestFailureReason(
          mojo::ConvertTo<mojom::NetworkRequestResult>(error)));
  RecordSetSoftwareFailedFeature(it->second->enabled(),
                                 it->second->software_feature());

  it->second->InvokeCallback(
      mojo::ConvertTo<mojom::NetworkRequestResult>(error));
  id_to_pending_set_software_feature_request_map_.erase(it);
}

void DeviceSyncImpl::OnSetFeatureStatusSuccess() {
  DCHECK(features::ShouldUseV2DeviceSync());

  PA_LOG(VERBOSE) << "DeviceSyncImpl::OnSetFeatureStatusSuccess(): "
                  << "Successfully completed SetFeatureStatus() call; "
                  << "requesting force sync.";

  if (features::ShouldUseV1DeviceSync()) {
    cryptauth_device_manager_->ForceSyncNow(
        cryptauth::INVOCATION_REASON_FEATURE_TOGGLED);
  }

  cryptauth_v2_device_manager_->ForceDeviceSyncNow(
      cryptauthv2::ClientMetadata::FEATURE_TOGGLED,
      std::nullopt /* session_id */);
}

void DeviceSyncImpl::OnSetFeatureStatusError(
    const base::UnguessableToken& request_id,
    NetworkRequestError error) {
  DCHECK(features::ShouldUseV2DeviceSync());

  auto it = id_to_pending_set_feature_status_request_map_.find(request_id);
  if (it == id_to_pending_set_feature_status_request_map_.end()) {
    PA_LOG(ERROR) << "DeviceSyncImpl::OnSetFeatureStatusError(): "
                  << "Could not find request entry with ID " << request_id;
    NOTREACHED_IN_MIGRATION();
    return;
  }

  it->second->InvokeCallback(
      mojo::ConvertTo<mojom::NetworkRequestResult>(error));
  id_to_pending_set_feature_status_request_map_.erase(it);
}

void DeviceSyncImpl::OnFindEligibleDevicesSuccess(
    base::OnceCallback<void(mojom::NetworkRequestResult,
                            mojom::FindEligibleDevicesResponsePtr)> callback,
    const std::vector<cryptauth::ExternalDeviceInfo>& eligible_device_infos,
    const std::vector<cryptauth::IneligibleDevice>& ineligible_devices) {
  DCHECK(features::ShouldUseV1DeviceSync());

  std::vector<multidevice::RemoteDevice> eligible_remote_devices;
  for (const auto& eligible_device_info : eligible_device_infos) {
    auto possible_device =
        GetSyncedDeviceWithPublicKey(eligible_device_info.public_key());
    if (possible_device) {
      eligible_remote_devices.emplace_back(*possible_device);
    } else {
      PA_LOG(ERROR) << "Could not find eligible device with public key \""
                    << eligible_device_info.public_key() << "\".";
    }
  }

  std::vector<multidevice::RemoteDevice> ineligible_remote_devices;
  for (const auto& ineligible_device : ineligible_devices) {
    auto possible_device =
        GetSyncedDeviceWithPublicKey(ineligible_device.device().public_key());
    if (possible_device) {
      ineligible_remote_devices.emplace_back(*possible_device);
    } else {
      PA_LOG(ERROR) << "Could not find ineligible device with public key \""
                    << ineligible_device.device().public_key() << "\".";
    }
  }

  std::move(callback).Run(
      mojom::NetworkRequestResult::kSuccess,
      mojom::FindEligibleDevicesResponse::New(eligible_remote_devices,
                                              ineligible_remote_devices));

  RecordFindEligibleDevicesResult(true /* success */);
}

void DeviceSyncImpl::OnFindEligibleDevicesError(
    base::OnceCallback<void(mojom::NetworkRequestResult,
                            mojom::FindEligibleDevicesResponsePtr)> callback,
    NetworkRequestError error) {
  DCHECK(features::ShouldUseV1DeviceSync());

  std::move(callback).Run(mojo::ConvertTo<mojom::NetworkRequestResult>(error),
                          nullptr /* response */);

  RecordFindEligibleDevicesResult(false /* success */);
  RecordFindEligibleDevicesResultFailureReason(
      GetDeviceSyncRequestFailureReason(
          mojo::ConvertTo<mojom::NetworkRequestResult>(error)));
}

void DeviceSyncImpl::OnNotifyDevicesSuccess(
    const base::UnguessableToken& request_id) {
  DCHECK(features::ShouldUseV2DeviceSync());

  auto it = pending_notify_devices_callbacks_.find(request_id);
  if (it == pending_notify_devices_callbacks_.end()) {
    PA_LOG(ERROR) << "DeviceSyncImpl::OnNotifyDevicesSuccess(): "
                  << "Could not find request entry with ID " << request_id;
    NOTREACHED_IN_MIGRATION();
    return;
  }

  std::move(it->second).Run(mojom::NetworkRequestResult::kSuccess);
  pending_notify_devices_callbacks_.erase(it);
}

void DeviceSyncImpl::OnNotifyDevicesError(
    const base::UnguessableToken& request_id,
    NetworkRequestError error) {
  DCHECK(features::ShouldUseV2DeviceSync());

  auto it = pending_notify_devices_callbacks_.find(request_id);
  if (it == pending_notify_devices_callbacks_.end()) {
    PA_LOG(ERROR) << "DeviceSyncImpl::OnNotifyDevicesError(): "
                  << "Could not find request entry with ID " << request_id;
    NOTREACHED_IN_MIGRATION();
    return;
  }

  std::move(it->second)
      .Run(mojo::ConvertTo<mojom::NetworkRequestResult>(error));
  pending_notify_devices_callbacks_.erase(it);
}

void DeviceSyncImpl::OnGetDevicesActivityStatusFinished(
    const base::UnguessableToken& request_id,
    CryptAuthDeviceActivityGetter::DeviceActivityStatusResult
        device_activity_status_result) {
  DCHECK(features::ShouldUseV2DeviceSync());

  auto iter = get_devices_activity_status_callbacks_.find(request_id);
  DCHECK(iter != get_devices_activity_status_callbacks_.end());
  std::move(iter->second)
      .Run(mojom::NetworkRequestResult::kSuccess,
           std::make_optional(std::move(device_activity_status_result)));
  get_devices_activity_status_callbacks_.erase(iter);
}

void DeviceSyncImpl::OnGetDevicesActivityStatusError(
    const base::UnguessableToken& request_id,
    NetworkRequestError error) {
  DCHECK(features::ShouldUseV2DeviceSync());

  auto iter = get_devices_activity_status_callbacks_.find(request_id);
  DCHECK(iter != get_devices_activity_status_callbacks_.end());
  std::move(iter->second)
      .Run(mojo::ConvertTo<mojom::NetworkRequestResult>(error), std::nullopt);
  get_devices_activity_status_callbacks_.erase(iter);
}

void DeviceSyncImpl::StartSetSoftwareFeatureTimer() {
  timer_->Start(FROM_HERE, kSetFeatureEnabledTimeout,
                base::BindOnce(&DeviceSyncImpl::OnSetSoftwareFeatureTimerFired,
                               base::Unretained(this)));
}

void DeviceSyncImpl::OnSetSoftwareFeatureTimerFired() {
  if (id_to_pending_set_software_feature_request_map_.empty())
    return;

  PA_LOG(WARNING) << "DeviceSyncImpl::OnSetSoftwareFeatureTimerFired(): "
                  << "Timed out waiting for device feature states to update. "
                  << "Invoking failure callbacks.";

  // Any pending requests that are still present have timed out, so invoke
  // their callbacks and remove them from the map.
  auto it = id_to_pending_set_software_feature_request_map_.begin();
  while (it != id_to_pending_set_software_feature_request_map_.end()) {
    RecordSetSoftwareFeatureStateResult(false /* success */);
    RecordSetSoftwareFeatureStateResultFailureReason(
        DeviceSyncRequestFailureReason::kRequestSucceededButUnexpectedResult);
    RecordSetSoftwareFailedFeature(it->second->enabled(),
                                   it->second->software_feature());

    it->second->InvokeCallback(
        mojom::NetworkRequestResult::kRequestSucceededButUnexpectedResult);
    it = id_to_pending_set_software_feature_request_map_.erase(it);
  }
}

}  // namespace device_sync

}  // namespace ash