blob: df0c6c7e971d788d3d1e547a83a68084a1fa3cca [file] [log] [blame]
// Copyright 2025 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/contextual_tasks/internal/contextual_task_sync_bridge.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "components/contextual_tasks/internal/conversions.h"
#include "components/sync/base/deletion_origin.h"
#include "components/sync/model/data_batch.h"
#include "components/sync/model/in_memory_metadata_change_list.h"
#include "components/sync/model/mutable_data_batch.h"
#include "components/sync/protocol/contextual_task_specifics.pb.h"
namespace contextual_tasks {
namespace {
sync_pb::AiThreadSpecifics::ThreadType ToProtoThreadType(
ThreadType thread_type) {
switch (thread_type) {
case ThreadType::kUnknown:
return sync_pb::AiThreadSpecifics::UNKNOWN;
case ThreadType::kAiMode:
return sync_pb::AiThreadSpecifics::AI_MODE;
}
}
std::string StorageKeyFromUuid(const base::Uuid& uuid) {
return uuid.AsLowercaseString();
}
// Create new EntityData object to contain specifics for writing changes.
std::unique_ptr<syncer::EntityData> CreateEntityDataFromSpecifics(
const sync_pb::ContextualTaskSpecifics& specifics) {
auto entity_data = std::make_unique<syncer::EntityData>();
*entity_data->specifics.mutable_contextual_task() = specifics;
entity_data->name = specifics.guid();
return entity_data;
}
// Extracts the task ID from a ContextualTaskEntity proto. A single contextual
// task can be represented by multiple entities (e.g., one for the main task
// and others for associated URL resources). This function ensures we can group
// all related entities by a common task ID.
std::string GetTaskIdFromContextualTaskEntity(
const proto::ContextualTaskEntity& entity) {
if (entity.specifics().has_contextual_task()) {
return entity.specifics().guid();
}
if (entity.specifics().has_url_resource()) {
return entity.specifics().url_resource().task_guid();
}
return std::string();
}
// Reconstructs a `ContextualTask` from its constituent entities. This
// involves processing a list of `ContextualTaskEntity` protos, each
// MUST be a part of the task (e.g., the main task details, an associated
// URL), and assembling them into a single, coherent `ContextualTask` object.
std::optional<ContextualTask> BuildTaskFromEntities(
const std::string& task_id_str,
const std::vector<proto::ContextualTaskEntity>& task_entities) {
ContextualTask task(base::Uuid::ParseCaseInsensitive(task_id_str));
bool has_contextual_task_specifics = false;
for (const auto& entity : task_entities) {
const auto& specifics = entity.specifics();
if (specifics.has_contextual_task()) {
const auto& task_proto = specifics.contextual_task();
task.SetTitle(task_proto.title());
if (task_proto.has_thread_id()) {
task.AddThread(Thread(ToThreadType(task_proto.thread_type()),
task_proto.thread_id(), "", ""));
}
has_contextual_task_specifics = true;
} else if (specifics.has_url_resource()) {
const auto& url_resource_proto = specifics.url_resource();
task.AddUrlResource(
UrlResource(base::Uuid::ParseCaseInsensitive(specifics.guid()),
GURL(url_resource_proto.url())));
}
}
if (has_contextual_task_specifics) {
return task;
} else {
return std::nullopt;
}
}
void ApplyEntityProtoToTrimmedSpecifics(
const proto::ContextualTaskEntity& entity,
sync_pb::ContextualTaskSpecifics* mutable_base_specifics) {
mutable_base_specifics->set_guid(entity.specifics().guid());
if (entity.specifics().has_contextual_task()) {
sync_pb::ContextualTask* contextual_task =
mutable_base_specifics->mutable_contextual_task();
contextual_task->set_title(entity.specifics().contextual_task().title());
contextual_task->set_thread_id(
entity.specifics().contextual_task().thread_id());
contextual_task->set_thread_type(
entity.specifics().contextual_task().thread_type());
} else {
sync_pb::UrlResource* url_resource =
mutable_base_specifics->mutable_url_resource();
url_resource->set_task_guid(entity.specifics().url_resource().task_guid());
url_resource->set_url(entity.specifics().url_resource().url());
}
}
proto::ContextualTaskEntity SpecificsToEntityProto(
const sync_pb::ContextualTaskSpecifics& specifics) {
proto::ContextualTaskEntity entity;
entity.set_allocated_specifics(
new sync_pb::ContextualTaskSpecifics(specifics));
return entity;
}
proto::ContextualTaskEntity ContextualTaskToEntityProto(
const ContextualTask& contextual_task) {
CHECK(!contextual_task.IsEphemeral());
proto::ContextualTaskEntity entity;
sync_pb::ContextualTaskSpecifics* specifics = entity.mutable_specifics();
specifics->set_guid(StorageKeyFromUuid(contextual_task.GetTaskId()));
sync_pb::ContextualTask* task = specifics->mutable_contextual_task();
task->set_title(contextual_task.GetTitle());
if (contextual_task.GetThread()) {
task->set_thread_id(contextual_task.GetThread()->server_id);
task->set_thread_type(ToProtoThreadType(contextual_task.GetThread()->type));
}
return entity;
}
proto::ContextualTaskEntity UrlResourceToEntityProto(
const base::Uuid& task_id,
const UrlResource& url_resource) {
proto::ContextualTaskEntity entity;
sync_pb::ContextualTaskSpecifics* specifics = entity.mutable_specifics();
specifics->set_guid(StorageKeyFromUuid(url_resource.url_id));
sync_pb::UrlResource* resource = specifics->mutable_url_resource();
resource->set_task_guid(StorageKeyFromUuid(task_id));
resource->set_url(url_resource.url.spec());
return entity;
}
std::unique_ptr<syncer::EntityData> CreateEntityData(
const sync_pb::ContextualTaskSpecifics& specific) {
std::unique_ptr<syncer::EntityData> entity_data =
std::make_unique<syncer::EntityData>();
entity_data->name = specific.guid();
entity_data->specifics.mutable_contextual_task()->CopyFrom(specific);
return entity_data;
}
} // namespace
ContextualTaskSyncBridge::ContextualTaskSyncBridge(
std::unique_ptr<syncer::DataTypeLocalChangeProcessor> change_processor,
syncer::OnceDataTypeStoreFactory store_factory)
: syncer::DataTypeSyncBridge(std::move(change_processor)) {
std::move(store_factory)
.Run(syncer::CONTEXTUAL_TASK,
base::BindOnce(&ContextualTaskSyncBridge::OnDataTypeStoreCreated,
weak_ptr_factory_.GetWeakPtr()));
}
ContextualTaskSyncBridge::~ContextualTaskSyncBridge() = default;
std::unique_ptr<syncer::MetadataChangeList>
ContextualTaskSyncBridge::CreateMetadataChangeList() {
return std::make_unique<syncer::InMemoryMetadataChangeList>();
}
std::optional<syncer::ModelError> ContextualTaskSyncBridge::MergeFullSyncData(
std::unique_ptr<syncer::MetadataChangeList> metadata_change_list,
syncer::EntityChangeList entity_change_list) {
return ApplyIncrementalSyncChanges(std::move(metadata_change_list),
std::move(entity_change_list));
}
std::optional<syncer::ModelError>
ContextualTaskSyncBridge::ApplyIncrementalSyncChanges(
std::unique_ptr<syncer::MetadataChangeList> metadata_change_list,
syncer::EntityChangeList entity_changes) {
std::unique_ptr<syncer::DataTypeStore::WriteBatch> batch =
data_type_store_->CreateWriteBatch();
std::vector<std::string> added_or_updated_guids;
std::vector<base::Uuid> removed;
for (const std::unique_ptr<syncer::EntityChange>& change : entity_changes) {
const sync_pb::EntitySpecifics& entity_specifics = change->data().specifics;
switch (change->type()) {
case syncer::EntityChange::ACTION_ADD:
case syncer::EntityChange::ACTION_UPDATE: {
CHECK(entity_specifics.has_contextual_task());
const proto::ContextualTaskEntity& entity =
SpecificsToEntityProto(entity_specifics.contextual_task());
bool updated = false;
if (change->type() == syncer::EntityChange::ACTION_ADD) {
updated = AddEntityToMap(entity);
} else {
updated = UpdateEntityInMap(entity);
}
if (updated) {
added_or_updated_guids.emplace_back(change->storage_key());
}
batch->WriteData(change->storage_key(), entity.SerializeAsString());
break;
}
case syncer::EntityChange::ACTION_DELETE:
if (DeleteEntityFromMap(change->storage_key())) {
removed.emplace_back(
base::Uuid::ParseCaseInsensitive(change->storage_key()));
}
batch->DeleteData(change->storage_key());
break;
}
}
batch->TakeMetadataChangesFrom(std::move(metadata_change_list));
data_type_store_->CommitWriteBatch(
std::move(batch),
base::BindOnce(&ContextualTaskSyncBridge::OnDataTypeStoreCommit,
weak_ptr_factory_.GetWeakPtr()));
std::vector<ContextualTask> added_or_updated_task;
for (const std::string& guid : added_or_updated_guids) {
std::optional<ContextualTask> task = GetTaskById(guid);
if (task) {
added_or_updated_task.emplace_back(task.value());
}
}
for (auto& observer : observers_) {
observer.OnTaskAddedOrUpdatedRemotely(added_or_updated_task);
observer.OnTaskRemovedRemotely(removed);
}
return std::nullopt;
}
std::unique_ptr<syncer::DataBatch> ContextualTaskSyncBridge::GetDataForCommit(
StorageKeyList storage_keys) {
auto batch = std::make_unique<syncer::MutableDataBatch>();
for (const auto& key : storage_keys) {
std::optional<proto::ContextualTaskEntity> entity = GetEntityProto(key);
if (entity) {
auto entity_data = std::make_unique<syncer::EntityData>();
entity_data->specifics =
change_processor()->GetPossiblyTrimmedRemoteSpecifics(key);
ApplyEntityProtoToTrimmedSpecifics(
entity.value(), entity_data->specifics.mutable_contextual_task());
batch->Put(key, std::move(entity_data));
}
}
return batch;
}
std::unique_ptr<syncer::DataBatch>
ContextualTaskSyncBridge::GetAllDataForDebugging() {
auto batch = std::make_unique<syncer::MutableDataBatch>();
for (const auto& [task_id, task_entities] : task_id_to_entities_map_) {
for (const auto& entity : task_entities) {
batch->Put(entity.specifics().guid(),
CreateEntityDataFromSpecifics(entity.specifics()));
}
}
return batch;
}
std::string ContextualTaskSyncBridge::GetClientTag(
const syncer::EntityData& entity_data) const {
return GetStorageKey(entity_data);
}
std::string ContextualTaskSyncBridge::GetStorageKey(
const syncer::EntityData& entity_data) const {
return entity_data.specifics.contextual_task().guid();
}
void ContextualTaskSyncBridge::ApplyDisableSyncChanges(
std::unique_ptr<syncer::MetadataChangeList> delete_metadata_change_list) {
std::vector<base::Uuid> uuids;
for (const auto& [task_id, entity] : task_id_to_entities_map_) {
uuids.push_back(base::Uuid::ParseCaseInsensitive(task_id));
}
task_id_to_entities_map_.clear();
data_type_store_->DeleteAllDataAndMetadata(base::DoNothing());
weak_ptr_factory_.InvalidateWeakPtrs();
for (auto& observer : observers_) {
observer.OnTaskRemovedRemotely(uuids);
}
}
bool ContextualTaskSyncBridge::IsEntityDataValid(
const syncer::EntityData& entity_data) const {
const sync_pb::ContextualTaskSpecifics& specifics =
entity_data.specifics.contextual_task();
return specifics.has_contextual_task() || specifics.has_url_resource();
}
sync_pb::EntitySpecifics
ContextualTaskSyncBridge::TrimAllSupportedFieldsFromRemoteSpecifics(
const sync_pb::EntitySpecifics& entity_specifics) const {
sync_pb::ContextualTaskSpecifics trimmed_specifics =
entity_specifics.contextual_task();
trimmed_specifics.clear_guid();
trimmed_specifics.clear_version();
if (trimmed_specifics.has_contextual_task()) {
sync_pb::ContextualTask* task = trimmed_specifics.mutable_contextual_task();
task->clear_title();
task->clear_thread_id();
task->clear_thread_type();
}
if (trimmed_specifics.has_url_resource()) {
sync_pb::UrlResource* url_resource =
trimmed_specifics.mutable_url_resource();
url_resource->clear_task_guid();
url_resource->clear_url();
}
sync_pb::EntitySpecifics trimmed_entity_specifics;
*trimmed_entity_specifics.mutable_contextual_task() =
std::move(trimmed_specifics);
return trimmed_entity_specifics;
}
std::vector<ContextualTask> ContextualTaskSyncBridge::GetTasks() const {
std::vector<ContextualTask> tasks;
for (auto& [task_id, task_entities] : task_id_to_entities_map_) {
std::optional<ContextualTask> task =
BuildTaskFromEntities(task_id, task_entities);
if (task) {
tasks.emplace_back(task.value());
}
}
return tasks;
}
std::optional<ContextualTask> ContextualTaskSyncBridge::GetTaskById(
const std::string& task_guid) const {
auto it = task_id_to_entities_map_.find(task_guid);
if (it != task_id_to_entities_map_.end()) {
return BuildTaskFromEntities(task_guid, it->second);
}
return std::nullopt;
}
void ContextualTaskSyncBridge::OnTaskAddedLocally(
const ContextualTask& contextual_task) {
if (contextual_task.IsEphemeral()) {
return;
}
proto::ContextualTaskEntity entity_proto =
ContextualTaskToEntityProto(contextual_task);
DCHECK(task_id_to_entities_map_.find(entity_proto.specifics().guid()) ==
task_id_to_entities_map_.end());
AddEntityToMap(entity_proto);
UpsertEntityToSync(entity_proto);
}
void ContextualTaskSyncBridge::OnTaskRemovedLocally(const base::Uuid& task_id) {
std::string task_id_str = StorageKeyFromUuid(task_id);
auto it = task_id_to_entities_map_.find(task_id_str);
if (it != task_id_to_entities_map_.end()) {
// The vector contains the task entity itself plus all URL resources.
std::vector<std::string> guids_to_remove;
guids_to_remove.reserve(it->second.size());
for (const auto& entity : it->second) {
guids_to_remove.push_back(entity.specifics().guid());
}
task_id_to_entities_map_.erase(it);
RemoveEntitiesFromSync(guids_to_remove);
}
}
void ContextualTaskSyncBridge::OnTaskUpdatedLocally(
const ContextualTask& contextual_task) {
if (contextual_task.IsEphemeral()) {
return;
}
proto::ContextualTaskEntity entity_proto =
ContextualTaskToEntityProto(contextual_task);
UpdateEntityInMap(entity_proto);
UpsertEntityToSync(entity_proto);
}
void ContextualTaskSyncBridge::OnUrlAddedToTaskLocally(
const base::Uuid& task_id,
const UrlResource& url_resource) {
proto::ContextualTaskEntity entity_proto =
UrlResourceToEntityProto(task_id, url_resource);
AddEntityToMap(entity_proto);
UpsertEntityToSync(entity_proto);
}
void ContextualTaskSyncBridge::OnUrlRemovedFromTaskLocally(
const base::Uuid& url_id) {
std::string storage_key = StorageKeyFromUuid(url_id);
DeleteEntityFromMap(storage_key);
RemoveEntitiesFromSync({storage_key});
}
void ContextualTaskSyncBridge::AddObserver(Observer* observer) {
observers_.AddObserver(observer);
}
void ContextualTaskSyncBridge::RemoveObserver(Observer* observer) {
observers_.RemoveObserver(observer);
}
void ContextualTaskSyncBridge::OnDataTypeStoreCreated(
const std::optional<syncer::ModelError>& error,
std::unique_ptr<syncer::DataTypeStore> store) {
if (error) {
change_processor()->ReportError(*error);
return;
}
data_type_store_ = std::move(store);
data_type_store_->ReadAllData(
base::BindOnce(&ContextualTaskSyncBridge::OnReadAllData,
weak_ptr_factory_.GetWeakPtr()));
}
void ContextualTaskSyncBridge::OnReadAllData(
const std::optional<syncer::ModelError>& error,
std::unique_ptr<syncer::DataTypeStore::RecordList> entries) {
if (error) {
change_processor()->ReportError(*error);
return;
}
for (const auto& record : *entries) {
proto::ContextualTaskEntity entity;
if (!entity.ParseFromString(record.value)) {
change_processor()->ReportError(*error);
return;
}
AddEntityToMap(entity);
}
for (auto& observer : observers_) {
observer.OnContextualTaskDataStoreLoaded();
}
data_type_store_->ReadAllMetadata(
base::BindOnce(&ContextualTaskSyncBridge::OnReadAllMetadata,
weak_ptr_factory_.GetWeakPtr()));
}
bool ContextualTaskSyncBridge::AddEntityToMap(
const proto::ContextualTaskEntity& contextual_task_entity) {
std::string task_id =
GetTaskIdFromContextualTaskEntity(contextual_task_entity);
if (task_id.empty()) {
return false;
}
auto it = task_id_to_entities_map_.find(task_id);
if (it != task_id_to_entities_map_.end()) {
it->second.emplace_back(contextual_task_entity);
} else {
task_id_to_entities_map_.emplace(
task_id,
std::vector<proto::ContextualTaskEntity>{contextual_task_entity});
}
return true;
}
bool ContextualTaskSyncBridge::UpdateEntityInMap(
const proto::ContextualTaskEntity& contextual_task_entity) {
std::string task_id =
GetTaskIdFromContextualTaskEntity(contextual_task_entity);
if (task_id.empty()) {
return false;
}
auto it = task_id_to_entities_map_.find(task_id);
if (it == task_id_to_entities_map_.end()) {
return false;
}
for (proto::ContextualTaskEntity& entity : it->second) {
if (entity.specifics().guid() ==
contextual_task_entity.specifics().guid()) {
entity = contextual_task_entity;
return true;
}
}
return false;
}
bool ContextualTaskSyncBridge::DeleteEntityFromMap(const std::string& guid) {
for (auto& [task_id, task_entities] : task_id_to_entities_map_) {
for (auto it = task_entities.begin(); it != task_entities.end(); ++it) {
if (it->specifics().guid() == guid) {
task_entities.erase(it);
return true;
}
}
}
return false;
}
std::optional<proto::ContextualTaskEntity>
ContextualTaskSyncBridge::GetEntityProto(const std::string& guid) {
for (const auto& [task_id, task_entities] : task_id_to_entities_map_) {
for (const auto& task : task_entities) {
if (task.specifics().guid() == guid) {
return task;
}
}
}
return std::nullopt;
}
void ContextualTaskSyncBridge::OnReadAllMetadata(
const std::optional<syncer::ModelError>& error,
std::unique_ptr<syncer::MetadataBatch> metadata_batch) {
if (error) {
change_processor()->ReportError(*error);
return;
}
change_processor()->ModelReadyToSync(std::move(metadata_batch));
}
void ContextualTaskSyncBridge::OnDataTypeStoreCommit(
const std::optional<syncer::ModelError>& error) {
if (error) {
change_processor()->ReportError(*error);
}
}
void ContextualTaskSyncBridge::UpsertEntityToSync(
const proto::ContextualTaskEntity& data) {
std::unique_ptr<syncer::DataTypeStore::WriteBatch> batch =
data_type_store_->CreateWriteBatch();
batch->WriteData(data.specifics().guid(), data.SerializeAsString());
if (change_processor()->IsTrackingMetadata()) {
auto entity_data = CreateEntityData(data.specifics());
// Copy because our key is the name of `entity_data`.
std::string name = entity_data->name;
change_processor()->Put(name, std::move(entity_data),
batch->GetMetadataChangeList());
}
data_type_store_->CommitWriteBatch(
std::move(batch),
base::BindOnce(&ContextualTaskSyncBridge::OnDataTypeStoreCommit,
weak_ptr_factory_.GetWeakPtr()));
}
void ContextualTaskSyncBridge::RemoveEntitiesFromSync(
const std::vector<std::string>& storage_keys) {
std::unique_ptr<syncer::DataTypeStore::WriteBatch> batch =
data_type_store_->CreateWriteBatch();
for (const std::string& storage_key : storage_keys) {
batch->DeleteData(storage_key);
if (change_processor()->IsTrackingMetadata()) {
change_processor()->Delete(storage_key,
syncer::DeletionOrigin::Unspecified(),
batch->GetMetadataChangeList());
}
}
data_type_store_->CommitWriteBatch(
std::move(batch),
base::BindOnce(&ContextualTaskSyncBridge::OnDataTypeStoreCommit,
weak_ptr_factory_.GetWeakPtr()));
}
} // namespace contextual_tasks