blob: bd7624c7280f012e2634c14be71bba25ba450127 [file]
// Copyright 2026 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/skills/internal/skills_sync_bridge.h"
#include <memory>
#include <utility>
#include "base/notimplemented.h"
#include "base/uuid.h"
#include "components/skills/proto/skill_local_data.pb.h"
#include "components/skills/public/skill.h"
#include "components/skills/public/skills_service.h"
#include "components/sync/base/deletion_origin.h"
#include "components/sync/model/data_type_local_change_processor.h"
#include "components/sync/model/in_memory_metadata_change_list.h"
#include "components/sync/model/metadata_batch.h"
#include "components/sync/model/metadata_change_list.h"
#include "components/sync/model/model_error.h"
#include "components/sync/model/mutable_data_batch.h"
#include "components/sync/protocol/entity_data.h"
#include "components/sync/protocol/entity_specifics.pb.h"
#include "components/sync/protocol/skill_specifics.pb.h"
namespace skills {
namespace {
constexpr int kSchemaVersion = 1;
base::Time FromWindowsEpochMicros(int64_t windows_epoch_micros) {
return base::Time::FromDeltaSinceWindowsEpoch(
base::Microseconds(windows_epoch_micros));
}
int64_t ToWindowsEpochMicros(base::Time time) {
return time.ToDeltaSinceWindowsEpoch().InMicroseconds();
}
// Converts a `skill` to specifics proto. `base_specifics` is used to preserve
// unknown fields when committing to the server.
sync_pb::SkillSpecifics SkillToSpecifics(
const Skill& skill,
sync_pb::SkillSpecifics base_specifics) {
sync_pb::SkillSpecifics specifics = std::move(base_specifics);
specifics.set_guid(skill.id);
specifics.set_source_skill_id(skill.source_skill_id);
specifics.set_name(skill.name);
specifics.set_icon(skill.icon);
specifics.mutable_simple_skill()->set_prompt(skill.prompt);
specifics.mutable_simple_skill()->set_description(skill.description);
specifics.set_creation_time_windows_epoch_micros(
ToWindowsEpochMicros(skill.creation_time));
specifics.set_last_update_time_windows_epoch_micros(
ToWindowsEpochMicros(skill.last_update_time));
specifics.set_schema_version(kSchemaVersion);
specifics.set_skill_source(skill.source);
return specifics;
}
std::unique_ptr<Skill> SpecificsToSkill(
const sync_pb::SkillSpecifics& specifics) {
CHECK(specifics.has_simple_skill());
auto skill = std::make_unique<Skill>(
/*id=*/specifics.guid(),
/*name=*/specifics.name(),
/*icon=*/specifics.icon(),
/*prompt=*/specifics.simple_skill().prompt(),
/*description=*/specifics.simple_skill().description());
if (!specifics.source_skill_id().empty()) {
skill->source_skill_id = specifics.source_skill_id();
}
skill->creation_time =
FromWindowsEpochMicros(specifics.creation_time_windows_epoch_micros());
skill->last_update_time =
FromWindowsEpochMicros(specifics.last_update_time_windows_epoch_micros());
skill->source = specifics.skill_source();
return skill;
}
syncer::EntityData SpecificsToEntityData(
const sync_pb::SkillSpecifics& specifics) {
syncer::EntityData entity_data;
*entity_data.specifics.mutable_skill() = specifics;
entity_data.name = specifics.guid();
return entity_data;
}
void StoreSkill(const Skill& skill,
syncer::DataTypeStore::WriteBatch& write_batch) {
proto::SkillLocalData local_data;
// Do not store unknown fields to avoid duplicating data as it's already
// stored in the sync metadata.
*local_data.mutable_specifics() =
SkillToSpecifics(skill, /*base_specifics=*/sync_pb::SkillSpecifics());
write_batch.WriteData(skill.id, local_data.SerializeAsString());
}
} // namespace
SkillsSyncBridge::SkillsSyncBridge(
std::unique_ptr<syncer::DataTypeLocalChangeProcessor> change_processor,
syncer::OnceDataTypeStoreFactory create_store_callback,
SkillsService& skills_service)
: syncer::DataTypeSyncBridge(std::move(change_processor)),
skills_service_(skills_service) {
std::move(create_store_callback)
.Run(syncer::SKILL, base::BindOnce(&SkillsSyncBridge::OnStoreCreated,
weak_ptr_factory_.GetWeakPtr()));
}
SkillsSyncBridge::~SkillsSyncBridge() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}
std::unique_ptr<syncer::MetadataChangeList>
SkillsSyncBridge::CreateMetadataChangeList() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return std::make_unique<syncer::InMemoryMetadataChangeList>();
}
std::optional<syncer::ModelError> SkillsSyncBridge::MergeFullSyncData(
std::unique_ptr<syncer::MetadataChangeList> metadata_change_list,
syncer::EntityChangeList entity_changes) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// This data type does not store local-only data so the initial merge is the
// same as an incremental update.
std::optional<syncer::ModelError> error = ApplyIncrementalSyncChanges(
std::move(metadata_change_list), std::move(entity_changes));
skills_service_->SyncStatusChanged();
return error;
}
std::optional<syncer::ModelError> SkillsSyncBridge::ApplyIncrementalSyncChanges(
std::unique_ptr<syncer::MetadataChangeList> metadata_change_list,
syncer::EntityChangeList entity_changes) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
std::unique_ptr<syncer::DataTypeStore::WriteBatch> write_batch =
store_->CreateWriteBatch();
write_batch->TakeMetadataChangesFrom(std::move(metadata_change_list));
for (const std::unique_ptr<syncer::EntityChange>& entity_change :
entity_changes) {
switch (entity_change->type()) {
case syncer::EntityChange::ACTION_ADD:
case syncer::EntityChange::ACTION_UPDATE: {
const sync_pb::SkillSpecifics& skill_specifics =
entity_change->data().specifics.skill();
if (!skill_specifics.has_simple_skill()) {
// This is a new type of skill, ignore it in the bridge but its
// metadata should still be stored.
continue;
}
const Skill* skill = skills_service_->AddOrUpdateSkillFromSync(
skill_specifics.guid(), skill_specifics.source_skill_id(),
skill_specifics.name(), skill_specifics.icon(),
skill_specifics.simple_skill().prompt(),
skill_specifics.simple_skill().description(),
FromWindowsEpochMicros(
skill_specifics.creation_time_windows_epoch_micros()),
FromWindowsEpochMicros(
skill_specifics.last_update_time_windows_epoch_micros()),
skill_specifics.skill_source());
CHECK(skill);
StoreSkill(*skill, *write_batch);
break;
}
case syncer::EntityChange::ACTION_DELETE: {
const std::string& skill_id = entity_change->storage_key();
skills_service_->DeleteSkill(skill_id,
SkillsService::UpdateSource::kSync);
write_batch->DeleteData(skill_id);
break;
}
}
}
store_->CommitWriteBatch(std::move(write_batch),
base::BindOnce(&SkillsSyncBridge::OnDatabaseSave,
weak_ptr_factory_.GetWeakPtr()));
return {};
}
std::unique_ptr<syncer::DataBatch> SkillsSyncBridge::GetDataForCommit(
StorageKeyList storage_keys) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto batch = std::make_unique<syncer::MutableDataBatch>();
for (const std::string& storage_key : storage_keys) {
const Skill* skill = skills_service_->GetSkillById(storage_key);
if (!skill) {
// Skill was deleted locally.
continue;
}
batch->Put(storage_key,
std::make_unique<syncer::EntityData>(
SpecificsToEntityData(SkillToSpecifics(
*skill, GetPossiblyTrimmedSpecifics(storage_key)))));
}
return batch;
}
std::unique_ptr<syncer::DataBatch> SkillsSyncBridge::GetAllDataForDebugging() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto batch = std::make_unique<syncer::MutableDataBatch>();
for (const std::unique_ptr<Skill>& skill : skills_service_->GetSkills()) {
batch->Put(
skill->id,
std::make_unique<syncer::EntityData>(SpecificsToEntityData(
SkillToSpecifics(*skill,
/*base_specifics=*/sync_pb::SkillSpecifics()))));
}
return batch;
}
std::string SkillsSyncBridge::GetClientTag(
const syncer::EntityData& entity_data) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return entity_data.specifics.skill().guid();
}
std::string SkillsSyncBridge::GetStorageKey(
const syncer::EntityData& entity_data) const {
return GetClientTag(entity_data);
}
void SkillsSyncBridge::ApplyDisableSyncChanges(
std::unique_ptr<syncer::MetadataChangeList> delete_metadata_change_list) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
std::vector<std::string> skills_to_delete;
skills_to_delete.reserve(skills_service_->GetSkills().size());
for (const std::unique_ptr<Skill>& skill : skills_service_->GetSkills()) {
skills_to_delete.push_back(skill->id);
}
for (const std::string& skill_id : skills_to_delete) {
skills_service_->DeleteSkill(skill_id, SkillsService::UpdateSource::kSync);
}
// All skills must be deleted from the service.
CHECK(skills_service_->GetSkills().empty());
// Do not use `delete_metadata_change_list` as all data and metadata should be
// deleted.
store_->DeleteAllDataAndMetadata(base::BindOnce(
&SkillsSyncBridge::OnDatabaseSave, weak_ptr_factory_.GetWeakPtr()));
skills_service_->SyncStatusChanged();
}
sync_pb::EntitySpecifics
SkillsSyncBridge::TrimAllSupportedFieldsFromRemoteSpecifics(
const sync_pb::EntitySpecifics& entity_specifics) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// LINT.IfChange(TrimAllSupportedFieldsFromRemoteSpecifics)
sync_pb::SkillSpecifics trimmed_specifics = entity_specifics.skill();
trimmed_specifics.clear_guid();
trimmed_specifics.clear_name();
trimmed_specifics.clear_icon();
trimmed_specifics.clear_creation_time_windows_epoch_micros();
trimmed_specifics.clear_last_update_time_windows_epoch_micros();
trimmed_specifics.clear_schema_version();
trimmed_specifics.clear_skill_source();
trimmed_specifics.clear_source_skill_id();
if (trimmed_specifics.has_simple_skill()) {
trimmed_specifics.mutable_simple_skill()->clear_prompt();
trimmed_specifics.mutable_simple_skill()->clear_description();
if (trimmed_specifics.simple_skill().ByteSizeLong() == 0) {
trimmed_specifics.clear_simple_skill();
}
}
// LINT.ThenChange(//components/sync/protocol/skill_specifics.proto:SkillSpecifics)
sync_pb::EntitySpecifics trimmed_entity_specifics;
if (trimmed_specifics.ByteSizeLong() > 0) {
*trimmed_entity_specifics.mutable_skill() = std::move(trimmed_specifics);
}
return trimmed_entity_specifics;
}
bool SkillsSyncBridge::IsEntityDataValid(
const syncer::EntityData& entity_data) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (GetClientTag(entity_data).empty()) {
return false;
}
// Do not validate the presence of `simple_skill` for forward compatibility
// with the newer version which may contain other skill types. The bridge will
// not propagate those changes to the server but it would be stored in sync
// metadata to detect unknown fields and potentially redownload the data.
return true;
}
void SkillsSyncBridge::OnSkillUpdated(
std::string_view skill_id,
SkillsService::UpdateSource update_source) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(store_);
// TODO(crbug.com/471795213): consider using CHECK for tracking metadata.
if (update_source == SkillsService::UpdateSource::kSync) {
// This change was made by the remote sync service and it should not be sent
// back to the sync service.
return;
}
std::unique_ptr<syncer::DataTypeStore::WriteBatch> batch =
store_->CreateWriteBatch();
const Skill* skill = skills_service_->GetSkillById(skill_id);
if (!skill) {
// Skill was deleted locally.
std::string skill_id_str(skill_id.data());
batch->DeleteData(skill_id_str);
change_processor()->Delete(skill_id_str,
syncer::DeletionOrigin::Unspecified(),
batch->GetMetadataChangeList());
} else {
// Skill was created or updated locally.
StoreSkill(*skill, *batch);
change_processor()->Put(
skill->id,
std::make_unique<syncer::EntityData>(SpecificsToEntityData(
SkillToSpecifics(*skill, GetPossiblyTrimmedSpecifics(skill->id)))),
batch->GetMetadataChangeList());
}
store_->CommitWriteBatch(std::move(batch),
base::BindOnce(&SkillsSyncBridge::OnDatabaseSave,
weak_ptr_factory_.GetWeakPtr()));
}
void SkillsSyncBridge::OnStoreCreated(
const std::optional<syncer::ModelError>& error,
std::unique_ptr<syncer::DataTypeStore> store) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (error) {
change_processor()->ReportError(*error);
return;
}
store_ = std::move(store);
store_->ReadAllDataAndMetadata(
base::BindOnce(&SkillsSyncBridge::OnReadAllDataAndMetadata,
weak_ptr_factory_.GetWeakPtr()));
}
void SkillsSyncBridge::OnReadAllDataAndMetadata(
const std::optional<syncer::ModelError>& error,
std::unique_ptr<syncer::DataTypeStore::RecordList> entries,
std::unique_ptr<syncer::MetadataBatch> metadata_batch) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (error) {
change_processor()->ReportError(*error);
return;
}
std::vector<std::unique_ptr<Skill>> loaded_skills;
for (const syncer::DataTypeStore::Record& record : *entries) {
proto::SkillLocalData local_data;
if (!local_data.ParseFromString(record.value)) {
continue;
}
if (!IsEntityDataValid(SpecificsToEntityData(local_data.specifics()))) {
continue;
}
if (!local_data.specifics().has_simple_skill()) {
// Verify this explicitly as `IsEntityDataValid()` does not check for it.
continue;
}
if (record.id != local_data.specifics().guid()) {
continue;
}
loaded_skills.push_back(SpecificsToSkill(local_data.specifics()));
}
skills_service_->LoadInitialSkills(std::move(loaded_skills));
skills_service_observation_.Observe(&skills_service_.get());
change_processor()->ModelReadyToSync(std::move(metadata_batch));
if (change_processor()->IsTrackingMetadata()) {
skills_service_->SyncStatusChanged();
}
}
void SkillsSyncBridge::OnDatabaseSave(
const std::optional<syncer::ModelError>& error) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (error) {
change_processor()->ReportError(*error);
}
}
const sync_pb::SkillSpecifics& SkillsSyncBridge::GetPossiblyTrimmedSpecifics(
const std::string& storage_key) const {
// TODO(crbug.com/471795213): verify that metadata is being tracked.
if (!change_processor()->IsTrackingMetadata()) {
return sync_pb::SkillSpecifics::default_instance();
}
return change_processor()
->GetPossiblyTrimmedRemoteSpecifics(storage_key)
.skill();
}
} // namespace skills