blob: 703728a982e3d14d0d14e8d16675052d4653bf0b [file] [log] [blame]
// Copyright 2022 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/saved_tab_groups/saved_tab_group_sync_bridge.h"
#include <algorithm>
#include <memory>
#include <string>
#include <unordered_map>
#include <utility>
#include "base/check.h"
#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/functional/callback_helpers.h"
#include "base/guid.h"
#include "base/ranges/algorithm.h"
#include "base/strings/utf_string_conversions.h"
#include "components/saved_tab_groups/saved_tab_group.h"
#include "components/saved_tab_groups/saved_tab_group_model.h"
#include "components/saved_tab_groups/saved_tab_group_tab.h"
#include "components/sync/base/model_type.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/model_error.h"
#include "components/sync/model/model_type_change_processor.h"
#include "components/sync/model/model_type_store.h"
#include "components/sync/model/mutable_data_batch.h"
#include "components/sync/protocol/entity_data.h"
#include "components/sync/protocol/saved_tab_group_specifics.pb.h"
namespace {
constexpr base::TimeDelta discard_orphaned_tabs_threshold =
base::Microseconds(base::Time::kMicrosecondsPerDay * 90);
std::unique_ptr<syncer::EntityData> CreateEntityData(
std::unique_ptr<sync_pb::SavedTabGroupSpecifics> specific) {
std::unique_ptr<syncer::EntityData> entity_data =
std::make_unique<syncer::EntityData>();
entity_data->name = specific->guid();
entity_data->specifics.set_allocated_saved_tab_group(specific.release());
return entity_data;
}
} // anonymous namespace
SavedTabGroupSyncBridge::SavedTabGroupSyncBridge(
SavedTabGroupModel* model,
syncer::OnceModelTypeStoreFactory create_store_callback,
std::unique_ptr<syncer::ModelTypeChangeProcessor> change_processor)
: syncer::ModelTypeSyncBridge(std::move(change_processor)), model_(model) {
DCHECK(model_);
std::move(create_store_callback)
.Run(syncer::SAVED_TAB_GROUP,
base::BindOnce(&SavedTabGroupSyncBridge::OnStoreCreated,
weak_ptr_factory_.GetWeakPtr()));
}
SavedTabGroupSyncBridge::~SavedTabGroupSyncBridge() = default;
std::unique_ptr<syncer::MetadataChangeList>
SavedTabGroupSyncBridge::CreateMetadataChangeList() {
return syncer::ModelTypeStore::WriteBatch::CreateMetadataChangeList();
}
absl::optional<syncer::ModelError> SavedTabGroupSyncBridge::MergeSyncData(
std::unique_ptr<syncer::MetadataChangeList> metadata_change_list,
syncer::EntityChangeList entity_changes) {
std::unique_ptr<syncer::ModelTypeStore::WriteBatch> write_batch =
store_->CreateWriteBatch();
std::set<std::string> synced_items;
// Merge sync to local data.
for (const auto& change : entity_changes) {
synced_items.insert(change->storage_key());
AddDataToLocalStorage(std::move(change->data().specifics.saved_tab_group()),
metadata_change_list.get(), write_batch.get(),
/*notify_sync=*/true);
}
ResolveTabsMissingGroups(write_batch.get());
// Update sync with any locally stored data not currently stored in sync.
for (const SavedTabGroup& group : model_->saved_tab_groups()) {
for (const SavedTabGroupTab& tab : group.saved_tabs()) {
if (synced_items.count(tab.saved_tab_guid().AsLowercaseString()))
continue;
SendToSync(tab.ToSpecifics(), metadata_change_list.get());
}
if (synced_items.count(group.saved_guid().AsLowercaseString()))
continue;
SendToSync(group.ToSpecifics(), metadata_change_list.get());
}
write_batch->TakeMetadataChangesFrom(std::move(metadata_change_list));
store_->CommitWriteBatch(
std::move(write_batch),
base::BindOnce(&SavedTabGroupSyncBridge::OnDatabaseSave,
weak_ptr_factory_.GetWeakPtr()));
return {};
}
absl::optional<syncer::ModelError> SavedTabGroupSyncBridge::ApplySyncChanges(
std::unique_ptr<syncer::MetadataChangeList> metadata_change_list,
syncer::EntityChangeList entity_changes) {
std::unique_ptr<syncer::ModelTypeStore::WriteBatch> write_batch =
store_->CreateWriteBatch();
for (const std::unique_ptr<syncer::EntityChange>& change : entity_changes) {
switch (change->type()) {
case syncer::EntityChange::ACTION_DELETE: {
DeleteDataFromLocalStorage(
base::GUID::ParseLowercase(change->storage_key()),
write_batch.get());
break;
}
case syncer::EntityChange::ACTION_ADD:
case syncer::EntityChange::ACTION_UPDATE: {
AddDataToLocalStorage(
std::move(change->data().specifics.saved_tab_group()),
metadata_change_list.get(), write_batch.get(),
/*notify_sync=*/false);
break;
}
}
}
ResolveTabsMissingGroups(write_batch.get());
write_batch->TakeMetadataChangesFrom(std::move(metadata_change_list));
store_->CommitWriteBatch(
std::move(write_batch),
base::BindOnce(&SavedTabGroupSyncBridge::OnDatabaseSave,
weak_ptr_factory_.GetWeakPtr()));
return {};
}
std::string SavedTabGroupSyncBridge::GetStorageKey(
const syncer::EntityData& entity_data) {
return entity_data.specifics.saved_tab_group().guid();
}
std::string SavedTabGroupSyncBridge::GetClientTag(
const syncer::EntityData& entity_data) {
return GetStorageKey(entity_data);
}
void SavedTabGroupSyncBridge::GetData(StorageKeyList storage_keys,
DataCallback callback) {
auto batch = std::make_unique<syncer::MutableDataBatch>();
for (const std::string& guid : storage_keys) {
base::GUID parsed_guid = base::GUID::ParseLowercase(guid);
for (const SavedTabGroup& group : model_->saved_tab_groups()) {
if (group.saved_guid() == parsed_guid) {
AddEntryToBatch(batch.get(), group.ToSpecifics());
break;
}
if (group.ContainsTab(parsed_guid)) {
const SavedTabGroupTab& tab =
group.saved_tabs()[group.GetIndexOfTab(parsed_guid).value()];
AddEntryToBatch(batch.get(), tab.ToSpecifics());
break;
}
}
}
std::move(callback).Run(std::move(batch));
}
void SavedTabGroupSyncBridge::GetAllDataForDebugging(DataCallback callback) {
auto batch = std::make_unique<syncer::MutableDataBatch>();
for (const SavedTabGroup& group : model_->saved_tab_groups()) {
AddEntryToBatch(batch.get(), group.ToSpecifics());
for (const SavedTabGroupTab& tab : group.saved_tabs()) {
AddEntryToBatch(batch.get(), tab.ToSpecifics());
}
}
std::move(callback).Run(std::move(batch));
}
// SavedTabGroupModelObserver
void SavedTabGroupSyncBridge::SavedTabGroupAddedLocally(
const base::GUID& guid) {
std::unique_ptr<syncer::ModelTypeStore::WriteBatch> write_batch =
store_->CreateWriteBatch();
const SavedTabGroup* group = model_->Get(guid);
DCHECK(group);
int index = model_->GetIndexOf(guid).value();
std::unique_ptr<sync_pb::SavedTabGroupSpecifics> group_specific =
group->ToSpecifics();
group_specific->mutable_group()->set_position(index);
UpsertEntitySpecific(std::move(group_specific), write_batch.get());
for (size_t i = 0; i < group->saved_tabs().size(); ++i) {
std::unique_ptr<sync_pb::SavedTabGroupSpecifics> tab_specific =
group->saved_tabs()[i].ToSpecifics();
tab_specific->mutable_tab()->set_position(i);
UpsertEntitySpecific(std::move(tab_specific), write_batch.get());
}
store_->CommitWriteBatch(
std::move(write_batch),
base::BindOnce(&SavedTabGroupSyncBridge::OnDatabaseSave,
weak_ptr_factory_.GetWeakPtr()));
}
void SavedTabGroupSyncBridge::SavedTabGroupRemovedLocally(
const SavedTabGroup* removed_group) {
std::unique_ptr<syncer::ModelTypeStore::WriteBatch> write_batch =
store_->CreateWriteBatch();
RemoveEntitySpecific(removed_group->saved_guid(), write_batch.get());
for (const SavedTabGroupTab& tab : removed_group->saved_tabs())
RemoveEntitySpecific(tab.saved_tab_guid(), write_batch.get());
store_->CommitWriteBatch(
std::move(write_batch),
base::BindOnce(&SavedTabGroupSyncBridge::OnDatabaseSave,
weak_ptr_factory_.GetWeakPtr()));
}
void SavedTabGroupSyncBridge::SavedTabGroupUpdatedLocally(
const base::GUID& group_guid,
const absl::optional<base::GUID>& tab_guid) {
std::unique_ptr<syncer::ModelTypeStore::WriteBatch> write_batch =
store_->CreateWriteBatch();
const SavedTabGroup* group = model_->Get(group_guid);
DCHECK(group);
if (tab_guid.has_value()) {
if (!group->ContainsTab(tab_guid.value())) {
RemoveEntitySpecific(tab_guid.value(), write_batch.get());
} else {
int tab_index = group->GetIndexOfTab(tab_guid.value()).value();
UpsertEntitySpecific(group->saved_tabs()[tab_index].ToSpecifics(),
write_batch.get());
}
} else {
UpsertEntitySpecific(group->ToSpecifics(), write_batch.get());
}
store_->CommitWriteBatch(
std::move(write_batch),
base::BindOnce(&SavedTabGroupSyncBridge::OnDatabaseSave,
weak_ptr_factory_.GetWeakPtr()));
}
void SavedTabGroupSyncBridge::SavedTabGroupReorderedLocally() {
// TODO(dljames): Find a more efficient way to only upsert the data that has
// changed. If a group has changed, update all groups. If a tab has changed,
// update all tabs in its group.
std::unique_ptr<syncer::ModelTypeStore::WriteBatch> write_batch =
store_->CreateWriteBatch();
for (const SavedTabGroup& group : model_->saved_tab_groups()) {
UpsertEntitySpecific(group.ToSpecifics(), write_batch.get());
for (const SavedTabGroupTab& tab : group.saved_tabs())
UpsertEntitySpecific(tab.ToSpecifics(), write_batch.get());
}
store_->CommitWriteBatch(std::move(write_batch), base::DoNothing());
}
void SavedTabGroupSyncBridge::UpsertEntitySpecific(
std::unique_ptr<sync_pb::SavedTabGroupSpecifics> specific,
syncer::ModelTypeStore::WriteBatch* write_batch) {
write_batch->WriteData(specific->guid(), specific->SerializeAsString());
SendToSync(std::move(specific), write_batch->GetMetadataChangeList());
}
void SavedTabGroupSyncBridge::RemoveEntitySpecific(
const base::GUID& guid,
syncer::ModelTypeStore::WriteBatch* write_batch) {
write_batch->DeleteData(guid.AsLowercaseString());
if (!change_processor()->IsTrackingMetadata())
return;
change_processor()->Delete(guid.AsLowercaseString(),
write_batch->GetMetadataChangeList());
}
void SavedTabGroupSyncBridge::AddDataToLocalStorage(
const sync_pb::SavedTabGroupSpecifics& specifics,
syncer::MetadataChangeList* metadata_change_list,
syncer::ModelTypeStore::WriteBatch* write_batch,
bool notify_sync) {
std::string group_id =
specifics.has_tab() ? specifics.tab().group_guid() : specifics.guid();
SavedTabGroup* existing_group =
model_->Get(base::GUID::ParseLowercase(group_id));
// Cases where `specifics` is a group.
if (specifics.has_group()) {
if (existing_group) {
// Resolve the conflict by merging the sync and local data. Once
// finished, write the result to the store and update sync with the new
// merged result if appropriate.
std::unique_ptr<sync_pb::SavedTabGroupSpecifics> merged_entry =
model_->MergeGroup(std::move(specifics));
// Write result to the store.
write_batch->WriteData(merged_entry->guid(),
merged_entry->SerializeAsString());
// Update sync with the new merged result.
if (notify_sync)
SendToSync(std::move(merged_entry), metadata_change_list);
} else {
// We do not have this group. Add the group from sync into local storage.
write_batch->WriteData(specifics.guid(), specifics.SerializeAsString());
model_->AddedFromSync(SavedTabGroup::FromSpecifics(specifics));
}
return;
}
// Cases where `specifics` is a tab.
if (specifics.has_tab()) {
if (existing_group && existing_group->ContainsTab(
base::GUID::ParseLowercase(specifics.guid()))) {
// Resolve the conflict by merging the sync and local data. Once finished,
// write the result to the store and update sync with the new merged
// result if appropriate.
std::unique_ptr<sync_pb::SavedTabGroupSpecifics> merged_entry =
model_->MergeTab(std::move(specifics));
// Write result to the store.
write_batch->WriteData(merged_entry->guid(),
merged_entry->SerializeAsString());
// Update sync with the new merged result.
if (notify_sync)
SendToSync(std::move(merged_entry), metadata_change_list);
return;
}
// We write tabs to local storage regardless of the existence of its group
// in order to recover the tabs in the event the group was not received and
// a crash / restart occurred.
write_batch->WriteData(specifics.guid(), specifics.SerializeAsString());
if (existing_group) {
// We do not have this tab. Add the tab from sync into local storage.
model_->AddTabToGroup(existing_group->saved_guid(),
SavedTabGroupTab::FromSpecifics(specifics),
/*update_tab_positions=*/false);
} else {
// We reach this case if we were unable to find a group for this tab. This
// can happen when sync sends the tab data before the group data. In this
// case, we will store the tabs in case the group comes in later.
// TODO(dljames): Cleanup orphaned tabs after some time if the groups
// never come in.
tabs_missing_groups_.emplace_back(std::move(specifics));
}
}
}
void SavedTabGroupSyncBridge::DeleteDataFromLocalStorage(
const base::GUID& guid,
syncer::ModelTypeStore::WriteBatch* write_batch) {
write_batch->DeleteData(guid.AsLowercaseString());
// Check if the model contains the group guid. If so, remove that group and
// all of its tabs.
if (model_->Contains(guid)) {
model_->Remove(guid);
return;
}
for (const SavedTabGroup& group : model_->saved_tab_groups()) {
if (!group.ContainsTab(guid))
continue;
model_->RemoveTabFromGroup(group.saved_guid(), guid,
/*update_tab_positions=*/false);
return;
}
}
void SavedTabGroupSyncBridge::ResolveTabsMissingGroups(
syncer::ModelTypeStore::WriteBatch* write_batch) {
auto tab_iterator = tabs_missing_groups_.begin();
while (tab_iterator != tabs_missing_groups_.end()) {
SavedTabGroup* group = model_->Get(
base::GUID::ParseLowercase(tab_iterator->tab().group_guid()));
if (!group) {
base::Time last_update_time = base::Time::FromDeltaSinceWindowsEpoch(
base::Microseconds(tab_iterator->update_time_windows_epoch_micros()));
base::Time now = base::Time::Now();
// Discard orphaned tabs that have not been updated for 90 days.
if (now - last_update_time >= discard_orphaned_tabs_threshold) {
RemoveEntitySpecific(base::GUID::ParseLowercase(tab_iterator->guid()),
write_batch);
tab_iterator = tabs_missing_groups_.erase(tab_iterator);
} else {
++tab_iterator;
}
} else {
write_batch->WriteData(tab_iterator->guid(),
tab_iterator->SerializeAsString());
model_->AddTabToGroup(group->saved_guid(),
SavedTabGroupTab::FromSpecifics(*tab_iterator),
/*update_tab_positions=*/false);
tab_iterator = tabs_missing_groups_.erase(tab_iterator);
}
}
}
void SavedTabGroupSyncBridge::AddEntryToBatch(
syncer::MutableDataBatch* batch,
std::unique_ptr<sync_pb::SavedTabGroupSpecifics> specific) {
std::unique_ptr<syncer::EntityData> entity_data =
CreateEntityData(std::move(specific));
// Copy because our key is the name of `entity_data`.
std::string name = entity_data->name;
batch->Put(name, std::move(entity_data));
}
void SavedTabGroupSyncBridge::SendToSync(
std::unique_ptr<sync_pb::SavedTabGroupSpecifics> specific,
syncer::MetadataChangeList* metadata_change_list) {
DCHECK(metadata_change_list);
if (!change_processor()->IsTrackingMetadata())
return;
auto entity_data = CreateEntityData(std::move(specific));
// Copy because our key is the name of `entity_data`.
std::string name = entity_data->name;
change_processor()->Put(name, std::move(entity_data), metadata_change_list);
}
void SavedTabGroupSyncBridge::OnStoreCreated(
const absl::optional<syncer::ModelError>& error,
std::unique_ptr<syncer::ModelTypeStore> store) {
if (error) {
change_processor()->ReportError(*error);
return;
}
store_ = std::move(store);
store_->ReadAllData(base::BindOnce(&SavedTabGroupSyncBridge::OnDatabaseLoad,
weak_ptr_factory_.GetWeakPtr()));
}
void SavedTabGroupSyncBridge::OnDatabaseLoad(
const absl::optional<syncer::ModelError>& error,
std::unique_ptr<syncer::ModelTypeStore::RecordList> entries) {
if (error) {
change_processor()->ReportError(*error);
return;
}
store_->ReadAllMetadata(
base::BindOnce(&SavedTabGroupSyncBridge::OnReadAllMetadata,
weak_ptr_factory_.GetWeakPtr(), std::move(entries)));
}
void SavedTabGroupSyncBridge::OnReadAllMetadata(
std::unique_ptr<syncer::ModelTypeStore::RecordList> entries,
const absl::optional<syncer::ModelError>& error,
std::unique_ptr<syncer::MetadataBatch> metadata_batch) {
if (error) {
change_processor()->ReportError({FROM_HERE, "Failed to read metadata."});
return;
}
change_processor()->ModelReadyToSync(std::move(metadata_batch));
std::vector<sync_pb::SavedTabGroupSpecifics> stored_entries;
stored_entries.reserve(entries->size());
for (const syncer::ModelTypeStore::Record& r : *entries) {
sync_pb::SavedTabGroupSpecifics proto;
if (!proto.ParseFromString(r.value))
continue;
stored_entries.emplace_back(std::move(proto));
}
// Update `model_` with any data stored in local storage except for orphaned
// tabs. Orphaned tabs will be returned and added to `tabs_missing_groups_` in
// case their missing group ever arrives.
tabs_missing_groups_ = model_->LoadStoredEntries(std::move(stored_entries));
observation_.Observe(model_);
}
void SavedTabGroupSyncBridge::OnDatabaseSave(
const absl::optional<syncer::ModelError>& error) {
if (error) {
change_processor()->ReportError({FROM_HERE, "Failed to save metadata."});
return;
}
// TODO(dljames): React to store failures when a save is not successful.
}