chromium/components/desks_storage/core/desk_sync_bridge.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 "components/desks_storage/core/desk_sync_bridge.h"

#include <optional>
#include <string>

#include "ash/constants/ash_features.h"
#include "ash/public/cpp/desk_template.h"
#include "base/check_op.h"
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/json/json_writer.h"
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "base/trace_event/trace_event.h"
#include "base/uuid.h"
#include "build/chromeos_buildflags.h"
#include "chromeos/ui/base/window_state_type.h"
#include "components/account_id/account_id.h"
#include "components/app_constants/constants.h"
#include "components/app_restore/app_launch_info.h"
#include "components/app_restore/window_info.h"
#include "components/desks_storage/core/desk_model_observer.h"
#include "components/desks_storage/core/desk_storage_metrics_util.h"
#include "components/desks_storage/core/desk_template_conversion.h"
#include "components/desks_storage/core/desk_template_util.h"
#include "components/services/app_service/public/cpp/app_launch_util.h"
#include "components/services/app_service/public/cpp/app_registry_cache.h"
#include "components/services/app_service/public/cpp/app_registry_cache_wrapper.h"
#include "components/services/app_service/public/cpp/app_types.h"
#include "components/sync/base/deletion_origin.h"
#include "components/sync/model/data_type_local_change_processor.h"
#include "components/sync/model/entity_change.h"
#include "components/sync/model/metadata_batch.h"
#include "components/sync/model/metadata_change_list.h"
#include "components/sync/model/mutable_data_batch.h"
#include "components/sync/protocol/workspace_desk_specifics.pb.h"
#include "ui/base/ui_base_types.h"
#include "ui/base/window_open_disposition.h"

#if !BUILDFLAG(IS_CHROMEOS_LACROS)
#include "chromeos/crosapi/cpp/lacros_startup_state.h"  // nogncheck
#endif  // !BUILDFLAG(IS_CHROMEOS_LACROS)

namespace desks_storage {

using BrowserAppTab =
    sync_pb::WorkspaceDeskSpecifics_BrowserAppWindow_BrowserAppTab;
using BrowserAppWindow = sync_pb::WorkspaceDeskSpecifics_BrowserAppWindow;
using ArcApp = sync_pb::WorkspaceDeskSpecifics_ArcApp;
using ArcAppWindowSize = sync_pb::WorkspaceDeskSpecifics_ArcApp_WindowSize;
using ash::DeskTemplate;
using ash::DeskTemplateSource;
using ash::DeskTemplateType;
using SyncDeskType = sync_pb::WorkspaceDeskSpecifics_DeskType;
using WindowState = sync_pb::WorkspaceDeskSpecifics_WindowState;
using WindowBound = sync_pb::WorkspaceDeskSpecifics_WindowBound;
using LaunchContainer = sync_pb::WorkspaceDeskSpecifics_LaunchContainer;
// Use name prefixed with Sync here to avoid name collision with original class
// which isn't defined in a namespace.
using SyncWindowOpenDisposition =
    sync_pb::WorkspaceDeskSpecifics_WindowOpenDisposition;
using ProgressiveWebApp = sync_pb::WorkspaceDeskSpecifics_ProgressiveWebApp;
using ChromeApp = sync_pb::WorkspaceDeskSpecifics_ChromeApp;
using WorkspaceDeskSpecifics_App = sync_pb::WorkspaceDeskSpecifics_App;
using SyncTabGroup = sync_pb::WorkspaceDeskSpecifics_BrowserAppWindow_TabGroup;
using SyncTabGroupColor = sync_pb::WorkspaceDeskSpecifics_TabGroupColor;
using TabGroupColor = tab_groups::TabGroupColorId;

namespace {

using syncer::DataTypeStore;

// The maximum number of templates the chrome sync storage can hold.
constexpr size_t kMaxTemplateCount = 6u;

// The maximum number of bytes a template can be.
// Sync server silently ignores large items. The client-side
// needs to check item size to avoid sending large items.
// This limit follows precedent set by the chrome extension API:
// chrome.storage.sync.QUOTA_BYTES_PER_ITEM.
constexpr size_t kMaxTemplateSize = 8192u;

// Allocate a EntityData and copies `specifics` into it.
std::unique_ptr<syncer::EntityData> CopyToEntityData(
    const sync_pb::WorkspaceDeskSpecifics& specifics) {
  auto entity_data = std::make_unique<syncer::EntityData>();
  *entity_data->specifics.mutable_workspace_desk() = specifics;
  entity_data->name = specifics.uuid();
  entity_data->creation_time = desk_template_conversion::ProtoTimeToTime(
      specifics.created_time_windows_epoch_micros());
  return entity_data;
}

// Parses the content of `record_list` into `*desk_templates`. The output
// parameters are first for binding purposes.
std::optional<syncer::ModelError> ParseDeskTemplatesOnBackendSequence(
    base::flat_map<base::Uuid, std::unique_ptr<DeskTemplate>>* desk_templates,
    std::unique_ptr<DataTypeStore::RecordList> record_list) {
  DCHECK(desk_templates);
  DCHECK(desk_templates->empty());
  DCHECK(record_list);

  for (const syncer::DataTypeStore::Record& r : *record_list) {
    auto specifics = std::make_unique<sync_pb::WorkspaceDeskSpecifics>();
    if (specifics->ParseFromString(r.value)) {
      const base::Uuid uuid =
          base::Uuid::ParseCaseInsensitive(specifics->uuid());
      if (!uuid.is_valid()) {
        return syncer::ModelError(
            FROM_HERE,
            base::StringPrintf("Failed to parse WorkspaceDeskSpecifics uuid %s",
                               specifics->uuid().c_str()));
      }

      std::unique_ptr<ash::DeskTemplate> entry =
          desk_template_conversion::FromSyncProto(*specifics);

      if (!entry)
        continue;
      (*desk_templates)[uuid] = std::move(entry);
    } else {
      return syncer::ModelError(
          FROM_HERE, "Failed to deserialize WorkspaceDeskSpecifics.");
    }
  }

  return std::nullopt;
}

}  // namespace

DeskSyncBridge::DeskSyncBridge(
    std::unique_ptr<syncer::DataTypeLocalChangeProcessor> change_processor,
    syncer::OnceDataTypeStoreFactory create_store_callback,
    const AccountId& account_id)
    : DataTypeSyncBridge(std::move(change_processor)),
      is_ready_(false),
      account_id_(account_id) {
  std::move(create_store_callback)
      .Run(syncer::WORKSPACE_DESK,
           base::BindOnce(&DeskSyncBridge::OnStoreCreated,
                          weak_ptr_factory_.GetWeakPtr()));
}

DeskSyncBridge::~DeskSyncBridge() = default;

std::unique_ptr<syncer::MetadataChangeList>
DeskSyncBridge::CreateMetadataChangeList() {
  return DataTypeStore::WriteBatch::CreateMetadataChangeList();
}

std::optional<syncer::ModelError> DeskSyncBridge::MergeFullSyncData(
    std::unique_ptr<syncer::MetadataChangeList> metadata_change_list,
    syncer::EntityChangeList entity_data) {
  // MergeFullSyncData will be called when Desk Template data type is enabled
  // to start syncing. There could be local desk templates that user has created
  // before enabling sync or during the time when Desk Template sync is
  // disabled. We should merge local and server data. We will send all
  // local-only templates to server and save server templates to local.

  UploadLocalOnlyData(metadata_change_list.get(), entity_data);

  // Apply server changes locally. Currently, if a template exists on both
  // local and server side, the server version will win.
  // TODO(yzd) We will add a template update timestamp and update this logic to
  // be: for templates that exist on both local and server side, we will keep
  // the one with later update timestamp.
  return ApplyIncrementalSyncChanges(std::move(metadata_change_list),
                                     std::move(entity_data));
}

std::optional<syncer::ModelError> DeskSyncBridge::ApplyIncrementalSyncChanges(
    std::unique_ptr<syncer::MetadataChangeList> metadata_change_list,
    syncer::EntityChangeList entity_changes) {
  std::vector<raw_ptr<const DeskTemplate, VectorExperimental>> added_or_updated;
  std::vector<base::Uuid> removed;
  std::unique_ptr<DataTypeStore::WriteBatch> batch = store_->CreateWriteBatch();

  for (const std::unique_ptr<syncer::EntityChange>& change : entity_changes) {
    const base::Uuid uuid =
        base::Uuid::ParseCaseInsensitive(change->storage_key());
    if (!uuid.is_valid()) {
      // Skip invalid storage keys.
      continue;
    }

    switch (change->type()) {
      case syncer::EntityChange::ACTION_DELETE: {
        if (desk_template_entries_.find(uuid) != desk_template_entries_.end()) {
          desk_template_entries_.erase(uuid);
          batch->DeleteData(uuid.AsLowercaseString());
          removed.push_back(uuid);
        }
        break;
      }
      case syncer::EntityChange::ACTION_UPDATE:
      case syncer::EntityChange::ACTION_ADD: {
        const sync_pb::WorkspaceDeskSpecifics& specifics =
            change->data().specifics.workspace_desk();

        std::unique_ptr<DeskTemplate> remote_entry =
            desk_template_conversion::FromSyncProto(specifics);
        if (!remote_entry) {
          // Skip invalid entries.
          continue;
        }

        DCHECK_EQ(uuid, remote_entry->uuid());
        std::string serialized_remote_entry = specifics.SerializeAsString();

        // Add/update the remote_entry to the model.
        desk_template_entries_[uuid] = std::move(remote_entry);
        added_or_updated.push_back(GetUserEntryByUUID(uuid));

        // Write to the store.
        batch->WriteData(uuid.AsLowercaseString(), serialized_remote_entry);
        break;
      }
    }
  }

  batch->TakeMetadataChangesFrom(std::move(metadata_change_list));
  Commit(std::move(batch));

  NotifyRemoteDeskTemplateAddedOrUpdated(added_or_updated);
  NotifyRemoteDeskTemplateDeleted(removed);

  return std::nullopt;
}

std::unique_ptr<syncer::DataBatch> DeskSyncBridge::GetDataForCommit(
    StorageKeyList storage_keys) {
  auto batch = std::make_unique<syncer::MutableDataBatch>();

  for (const std::string& uuid : storage_keys) {
    const DeskTemplate* entry =
        GetUserEntryByUUID(base::Uuid::ParseCaseInsensitive(uuid));
    if (!entry) {
      continue;
    }

    batch->Put(uuid, CopyToEntityData(desk_template_conversion::ToSyncProto(
                         entry, apps::AppRegistryCacheWrapper::Get()
                                    .GetAppRegistryCache(account_id_))));
  }
  return batch;
}

std::unique_ptr<syncer::DataBatch> DeskSyncBridge::GetAllDataForDebugging() {
  auto batch = std::make_unique<syncer::MutableDataBatch>();
  for (const auto& it : desk_template_entries_) {
    batch->Put(it.first.AsLowercaseString(),
               CopyToEntityData(desk_template_conversion::ToSyncProto(
                   it.second.get(),
                   apps::AppRegistryCacheWrapper::Get().GetAppRegistryCache(
                       account_id_))));
  }
  return batch;
}

std::string DeskSyncBridge::GetClientTag(
    const syncer::EntityData& entity_data) {
  return GetStorageKey(entity_data);
}

std::string DeskSyncBridge::GetStorageKey(
    const syncer::EntityData& entity_data) {
  return entity_data.specifics.workspace_desk().uuid();
}

DeskModel::GetAllEntriesResult DeskSyncBridge::GetAllEntries() {
  if (!IsReady()) {
    LOG(WARNING) << "Unable to get all entries: Not Ready";
    return GetAllEntriesResult(
        GetAllEntriesStatus::kFailure,
        std::vector<raw_ptr<const DeskTemplate, VectorExperimental>>());
  }

  std::vector<raw_ptr<const DeskTemplate, VectorExperimental>> entries;

  for (const auto& it : policy_entries_)
    entries.push_back(it.get());

  for (const auto& it : desk_template_entries_) {
    DCHECK_EQ(it.first, it.second->uuid());
    entries.push_back(it.second.get());
  }

  return GetAllEntriesResult(GetAllEntriesStatus::kOk, std::move(entries));
}

DeskModel::GetEntryByUuidResult DeskSyncBridge::GetEntryByUUID(
    const base::Uuid& uuid) {
  if (!IsReady()) {
    LOG(WARNING) << "Unable to get entry by UUID: Not Ready";
    return GetEntryByUuidResult(GetEntryByUuidStatus::kFailure, nullptr);
  }

  if (!uuid.is_valid()) {
    LOG(WARNING) << "Unable to get entry by UUID: Invalid UUID";
    return GetEntryByUuidResult(GetEntryByUuidStatus::kInvalidUuid, nullptr);
  }

  auto it = desk_template_entries_.find(uuid);
  if (it == desk_template_entries_.end()) {
    std::unique_ptr<DeskTemplate> policy_entry =
        GetAdminDeskTemplateByUUID(uuid);

    if (policy_entry) {
      return GetEntryByUuidResult(GetEntryByUuidStatus::kOk,
                                  std::move(policy_entry));
    } else {
      LOG(WARNING) << "Unable to get entry by UUID: Entry not found";
      return GetEntryByUuidResult(GetEntryByUuidStatus::kNotFound, nullptr);
    }
  } else {
    return GetEntryByUuidResult(GetEntryByUuidStatus::kOk,
                                it->second.get()->Clone());
  }
}

void DeskSyncBridge::AddOrUpdateEntry(std::unique_ptr<DeskTemplate> new_entry,
                                      AddOrUpdateEntryCallback callback) {
  if (!IsReady()) {
    // This sync bridge has not finished initializing. Do not save the new entry
    // yet.
    LOG(WARNING) << "Unable to add or update entry: Not Ready";
    std::move(callback).Run(AddOrUpdateEntryStatus::kFailure,
                            std::move(new_entry));
    return;
  }

  if (!new_entry) {
    LOG(WARNING) << "Unable to add or update entry: No new entry";
    std::move(callback).Run(AddOrUpdateEntryStatus::kInvalidArgument,
                            std::move(new_entry));
    return;
  }

  base::Uuid uuid = new_entry->uuid();
  if (!uuid.is_valid()) {
    LOG(WARNING) << "Unable to add or update entry: Invalid UUID";
    std::move(callback).Run(AddOrUpdateEntryStatus::kInvalidArgument,
                            std::move(new_entry));
    return;
  }

  // When a user creates a desk template locally, the desk template has `kUser`
  // as its source. Only user desk templates should be saved to Sync.
  DCHECK_EQ(DeskTemplateSource::kUser, new_entry->source());
  new_entry->set_client_cache_guid(change_processor()->TrackedCacheGuid());
  auto entry = new_entry->Clone();
  entry->set_template_name(
      base::CollapseWhitespace(new_entry->template_name(), true));

  std::unique_ptr<DataTypeStore::WriteBatch> batch = store_->CreateWriteBatch();

  // Check the new entry size and ensure it is below the size limit.
  auto sync_proto = desk_template_conversion::ToSyncProto(
      entry.get(),
      apps::AppRegistryCacheWrapper::Get().GetAppRegistryCache(account_id_));
  RecordSavedDeskTemplateSizeHistogram(new_entry->type(),
                                       sync_proto.ByteSizeLong());
  if (sync_proto.ByteSizeLong() > kMaxTemplateSize) {
    LOG(WARNING) << "Unable to add or update entry: Entry is too large";
    std::move(callback).Run(AddOrUpdateEntryStatus::kEntryTooLarge,
                            std::move(new_entry));
    return;
  }

  // Add/update this entry to the store and model.
  change_processor()->Put(uuid.AsLowercaseString(),
                          CopyToEntityData(sync_proto),
                          batch->GetMetadataChangeList());

  desk_template_entries_[uuid] =
      desk_template_conversion::FromSyncProto(sync_proto);
  const DeskTemplate* result = GetUserEntryByUUID(uuid);

  batch->WriteData(
      uuid.AsLowercaseString(),
      desk_template_conversion::ToSyncProto(
          result,
          apps::AppRegistryCacheWrapper::Get().GetAppRegistryCache(account_id_))
          .SerializeAsString());

  Commit(std::move(batch));

  std::move(callback).Run(AddOrUpdateEntryStatus::kOk, std::move(new_entry));
}

void DeskSyncBridge::DeleteEntry(const base::Uuid& uuid,
                                 DeleteEntryCallback callback) {
  if (!IsReady()) {
    // This sync bridge has not finished initializing.
    // Cannot delete anything.
    LOG(WARNING) << "Unable to delete entry: Not Ready";
    std::move(callback).Run(DeleteEntryStatus::kFailure);
    return;
  }

  if (GetUserEntryByUUID(uuid) == nullptr) {
    // Consider the deletion successful if the entry does not exist.
    std::move(callback).Run(DeleteEntryStatus::kOk);
    return;
  }

  std::unique_ptr<DataTypeStore::WriteBatch> batch = store_->CreateWriteBatch();

  change_processor()->Delete(uuid.AsLowercaseString(),
                             syncer::DeletionOrigin::Unspecified(),
                             batch->GetMetadataChangeList());

  desk_template_entries_.erase(uuid);

  batch->DeleteData(uuid.AsLowercaseString());

  Commit(std::move(batch));

  std::move(callback).Run(DeleteEntryStatus::kOk);
}

void DeskSyncBridge::DeleteAllEntries(DeleteEntryCallback callback) {
  DeleteEntryStatus status = DeleteAllEntriesSync();
  std::move(callback).Run(status);
}

DeskModel::DeleteEntryStatus DeskSyncBridge::DeleteAllEntriesSync() {
  if (!IsReady()) {
    // This sync bridge has not finished initializing.
    // Cannot delete anything.
    LOG(WARNING) << "Unable to delete entries: Not Ready";
    return DeleteEntryStatus::kFailure;
  }

  std::unique_ptr<DataTypeStore::WriteBatch> batch = store_->CreateWriteBatch();

  std::set<base::Uuid> all_uuids = GetAllEntryUuids();

  for (const auto& uuid : all_uuids) {
    change_processor()->Delete(uuid.AsLowercaseString(),
                               syncer::DeletionOrigin::Unspecified(),
                               batch->GetMetadataChangeList());
    batch->DeleteData(uuid.AsLowercaseString());
  }
  desk_template_entries_.clear();
  return DeleteEntryStatus::kOk;
}

size_t DeskSyncBridge::GetEntryCount() const {
  return GetSaveAndRecallDeskEntryCount() + GetDeskTemplateEntryCount();
}

// Return 0 for now since chrome sync does not support save and recall desks.
size_t DeskSyncBridge::GetSaveAndRecallDeskEntryCount() const {
  return 0u;
}

size_t DeskSyncBridge::GetDeskTemplateEntryCount() const {
  size_t template_count = std::count_if(
      desk_template_entries_.begin(), desk_template_entries_.end(),
      [](const std::pair<base::Uuid, std::unique_ptr<ash::DeskTemplate>>&
             entry) {
        return entry.second->type() == ash::DeskTemplateType::kTemplate;
      });
  return template_count + policy_entries_.size();
}

// Chrome sync does not support save and recall desks yet. Return 0 for max
// count.
size_t DeskSyncBridge::GetMaxSaveAndRecallDeskEntryCount() const {
  return 0u;
}

size_t DeskSyncBridge::GetMaxDeskTemplateEntryCount() const {
  return kMaxTemplateCount + policy_entries_.size();
}

std::set<base::Uuid> DeskSyncBridge::GetAllEntryUuids() const {
  std::set<base::Uuid> keys;

  for (const auto& it : policy_entries_)
    keys.emplace(it.get()->uuid());

  for (const auto& it : desk_template_entries_) {
    DCHECK_EQ(it.first, it.second->uuid());
    keys.emplace(it.first);
  }
  return keys;
}

bool DeskSyncBridge::IsReady() const {
  if (is_ready_) {
    DCHECK(store_);
  }
  return is_ready_;
}

bool DeskSyncBridge::IsSyncing() const {
  return change_processor()->IsTrackingMetadata();
}

// TODO(zhumatthew): Once desk sync bridge supports save and recall desk type,
// update this method to search the correct cache for the entry.
ash::DeskTemplate* DeskSyncBridge::FindOtherEntryWithName(
    const std::u16string& name,
    ash::DeskTemplateType type,
    const base::Uuid& uuid) const {
  return desk_template_util::FindOtherEntryWithName(name, uuid,
                                                    desk_template_entries_);
}

const DeskTemplate* DeskSyncBridge::GetUserEntryByUUID(
    const base::Uuid& uuid) const {
  auto it = desk_template_entries_.find(uuid);
  if (it == desk_template_entries_.end())
    return nullptr;
  return it->second.get();
}

void DeskSyncBridge::NotifyDeskModelLoaded() {
  for (DeskModelObserver& observer : observers_) {
    observer.DeskModelLoaded();
  }
}

void DeskSyncBridge::NotifyRemoteDeskTemplateAddedOrUpdated(
    const std::vector<raw_ptr<const DeskTemplate, VectorExperimental>>&
        new_entries) {
  if (new_entries.empty()) {
    return;
  }

  for (DeskModelObserver& observer : observers_) {
    observer.EntriesAddedOrUpdatedRemotely(new_entries);
  }
}

void DeskSyncBridge::NotifyRemoteDeskTemplateDeleted(
    const std::vector<base::Uuid>& uuids) {
  if (uuids.empty()) {
    return;
  }

  for (DeskModelObserver& observer : observers_) {
    observer.EntriesRemovedRemotely(uuids);
  }
}

void DeskSyncBridge::OnStoreCreated(
    const std::optional<syncer::ModelError>& error,
    std::unique_ptr<syncer::DataTypeStore> store) {
  if (error) {
    change_processor()->ReportError(*error);
    return;
  }

  auto stored_desk_templates = std::make_unique<DeskEntries>();
  DeskEntries* stored_desk_templates_copy = stored_desk_templates.get();
  store_ = std::move(store);
  store_->ReadAllDataAndPreprocess(
      base::BindOnce(&ParseDeskTemplatesOnBackendSequence,
                     base::Unretained(stored_desk_templates_copy)),
      base::BindOnce(&DeskSyncBridge::OnReadAllData,
                     weak_ptr_factory_.GetWeakPtr(),
                     std::move(stored_desk_templates)));
}

void DeskSyncBridge::OnReadAllData(
    std::unique_ptr<DeskEntries> stored_desk_templates,
    const std::optional<syncer::ModelError>& error) {
  DCHECK(stored_desk_templates);

  if (error) {
    change_processor()->ReportError(*error);
    return;
  }

  desk_template_entries_ = std::move(*stored_desk_templates);
  store_->ReadAllMetadata(base::BindOnce(&DeskSyncBridge::OnReadAllMetadata,
                                         weak_ptr_factory_.GetWeakPtr()));
}

void DeskSyncBridge::OnReadAllMetadata(
    const std::optional<syncer::ModelError>& error,
    std::unique_ptr<syncer::MetadataBatch> metadata_batch) {
  TRACE_EVENT0("ui", "DeskSyncBridge::OnReadAllMetadata");
  if (error) {
    change_processor()->ReportError(*error);
    return;
  }

  change_processor()->ModelReadyToSync(std::move(metadata_batch));
  is_ready_ = true;
  NotifyDeskModelLoaded();
}

void DeskSyncBridge::OnCommit(const std::optional<syncer::ModelError>& error) {
  if (error) {
    change_processor()->ReportError(*error);
  }
}

void DeskSyncBridge::Commit(std::unique_ptr<DataTypeStore::WriteBatch> batch) {
  store_->CommitWriteBatch(std::move(batch),
                           base::BindOnce(&DeskSyncBridge::OnCommit,
                                          weak_ptr_factory_.GetWeakPtr()));
}

void DeskSyncBridge::UploadLocalOnlyData(
    syncer::MetadataChangeList* metadata_change_list,
    const syncer::EntityChangeList& entity_data) {
  std::set<base::Uuid> local_keys_to_upload;
  for (const auto& it : desk_template_entries_) {
    DCHECK_EQ(DeskTemplateSource::kUser, it.second->source());
    local_keys_to_upload.insert(it.first);
  }

  // Strip `local_keys_to_upload` of any key (UUID) that is already known to the
  // server.
  for (const std::unique_ptr<syncer::EntityChange>& change : entity_data) {
    local_keys_to_upload.erase(
        base::Uuid::ParseCaseInsensitive(change->storage_key()));
  }

  // Upload the local-only templates.
  for (const base::Uuid& uuid : local_keys_to_upload) {
    change_processor()->Put(
        uuid.AsLowercaseString(),
        CopyToEntityData(desk_template_conversion::ToSyncProto(
            desk_template_entries_[uuid].get(),
            apps::AppRegistryCacheWrapper::Get().GetAppRegistryCache(
                account_id_))),
        metadata_change_list);
  }
}

bool DeskSyncBridge::HasUserTemplateWithName(const std::u16string& name) {
  return base::Contains(desk_template_entries_, name,
                        [](const DeskEntries::value_type& entry) {
                          return entry.second->template_name();
                        });
}

bool DeskSyncBridge::HasUuid(const base::Uuid& uuid) const {
  return uuid.is_valid() && base::Contains(desk_template_entries_, uuid);
}

std::string DeskSyncBridge::GetCacheGuid() {
  return change_processor()->TrackedCacheGuid();
}

}  // namespace desks_storage