blob: b1c5185ae04653c6535e78a81c186ffb1a7a3a30 [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/sync_bookmarks/bookmark_model_type_processor.h"
#include <utility>
#include "base/callback.h"
#include "base/strings/utf_string_conversions.h"
#include "base/threading/sequenced_task_runner_handle.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "components/bookmarks/browser/bookmark_node.h"
#include "components/bookmarks/browser/bookmark_utils.h"
#include "components/sync/base/data_type_histogram.h"
#include "components/sync/base/model_type.h"
#include "components/sync/base/time.h"
#include "components/sync/engine/commit_queue.h"
#include "components/sync/engine/cycle/status_counters.h"
#include "components/sync/engine/model_type_processor_proxy.h"
#include "components/sync/model/data_type_activation_request.h"
#include "components/sync/protocol/bookmark_model_metadata.pb.h"
#include "components/sync/protocol/proto_value_conversions.h"
#include "components/sync_bookmarks/bookmark_local_changes_builder.h"
#include "components/sync_bookmarks/bookmark_model_merger.h"
#include "components/sync_bookmarks/bookmark_model_observer_impl.h"
#include "components/sync_bookmarks/bookmark_remote_updates_handler.h"
#include "components/sync_bookmarks/bookmark_specifics_conversions.h"
#include "components/undo/bookmark_undo_utils.h"
namespace sync_bookmarks {
namespace {
class ScopedRemoteUpdateBookmarks {
public:
// |bookmark_model|, |bookmark_undo_service| and |observer| must not be null
// and must outlive this object.
ScopedRemoteUpdateBookmarks(bookmarks::BookmarkModel* bookmark_model,
BookmarkUndoService* bookmark_undo_service,
bookmarks::BookmarkModelObserver* observer)
: bookmark_model_(bookmark_model),
suspend_undo_(bookmark_undo_service),
observer_(observer) {
// Notify UI intensive observers of BookmarkModel that we are about to make
// potentially significant changes to it, so the updates may be batched. For
// example, on Mac, the bookmarks bar displays animations when bookmark
// items are added or deleted.
DCHECK(bookmark_model_);
bookmark_model_->BeginExtensiveChanges();
// Shouldn't be notified upon changes due to sync.
bookmark_model_->RemoveObserver(observer_);
}
~ScopedRemoteUpdateBookmarks() {
// Notify UI intensive observers of BookmarkModel that all updates have been
// applied, and that they may now be consumed. This prevents issues like the
// one described in https://crbug.com/281562, where old and new items on the
// bookmarks bar would overlap.
bookmark_model_->EndExtensiveChanges();
bookmark_model_->AddObserver(observer_);
}
private:
bookmarks::BookmarkModel* const bookmark_model_;
// Changes made to the bookmark model due to sync should not be undoable.
ScopedSuspendBookmarkUndo suspend_undo_;
bookmarks::BookmarkModelObserver* const observer_;
DISALLOW_COPY_AND_ASSIGN(ScopedRemoteUpdateBookmarks);
};
std::string ComputeServerDefinedUniqueTagForDebugging(
const bookmarks::BookmarkNode* node,
bookmarks::BookmarkModel* model) {
if (node == model->bookmark_bar_node()) {
return "bookmark_bar";
}
if (node == model->other_node()) {
return "other_bookmarks";
}
if (node == model->mobile_node()) {
return "synced_bookmarks";
}
return "";
}
} // namespace
BookmarkModelTypeProcessor::BookmarkModelTypeProcessor(
BookmarkUndoService* bookmark_undo_service)
: bookmark_undo_service_(bookmark_undo_service), weak_ptr_factory_(this) {}
BookmarkModelTypeProcessor::~BookmarkModelTypeProcessor() {
if (bookmark_model_ && bookmark_model_observer_) {
bookmark_model_->RemoveObserver(bookmark_model_observer_.get());
}
}
void BookmarkModelTypeProcessor::ConnectSync(
std::unique_ptr<syncer::CommitQueue> worker) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!worker_);
DCHECK(bookmark_model_);
worker_ = std::move(worker);
// |bookmark_tracker_| is instantiated only after initial sync is done.
if (bookmark_tracker_) {
NudgeForCommitIfNeeded();
}
}
void BookmarkModelTypeProcessor::DisconnectSync() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(worker_);
DVLOG(1) << "Disconnecting sync for Bookmarks";
worker_.reset();
}
void BookmarkModelTypeProcessor::GetLocalChanges(
size_t max_entries,
GetLocalChangesCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
BookmarkLocalChangesBuilder builder(bookmark_tracker_.get(), bookmark_model_);
std::vector<syncer::CommitRequestData> local_changes =
builder.BuildCommitRequests(max_entries);
std::move(callback).Run(std::move(local_changes));
}
void BookmarkModelTypeProcessor::OnCommitCompleted(
const sync_pb::ModelTypeState& type_state,
const syncer::CommitResponseDataList& response_list) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
for (const syncer::CommitResponseData& response : response_list) {
// In order to save space, |response.id_in_request| is written when it's
// different from |response.id|. If it's empty, then there was no id change
// during the commit, and |response.id| carries both the old and new ids.
const std::string& old_sync_id =
response.id_in_request.empty() ? response.id : response.id_in_request;
bookmark_tracker_->UpdateUponCommitResponse(old_sync_id, response.id,
response.sequence_number,
response.response_version);
}
bookmark_tracker_->set_model_type_state(
std::make_unique<sync_pb::ModelTypeState>(type_state));
schedule_save_closure_.Run();
}
void BookmarkModelTypeProcessor::OnUpdateReceived(
const sync_pb::ModelTypeState& model_type_state,
const syncer::UpdateResponseDataList& updates) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!model_type_state.cache_guid().empty());
DCHECK_EQ(model_type_state.cache_guid(), cache_guid_);
DCHECK(model_type_state.initial_sync_done());
if (!bookmark_tracker_) {
StartTrackingMetadata(
std::vector<NodeMetadataPair>(),
std::make_unique<sync_pb::ModelTypeState>(model_type_state));
{
ScopedRemoteUpdateBookmarks update_bookmarks(
bookmark_model_, bookmark_undo_service_,
bookmark_model_observer_.get());
BookmarkModelMerger(&updates, bookmark_model_, favicon_service_,
bookmark_tracker_.get())
.Merge();
}
schedule_save_closure_.Run();
NudgeForCommitIfNeeded();
return;
}
ScopedRemoteUpdateBookmarks update_bookmarks(
bookmark_model_, bookmark_undo_service_, bookmark_model_observer_.get());
BookmarkRemoteUpdatesHandler updates_handler(
bookmark_model_, favicon_service_, bookmark_tracker_.get());
const bool got_new_encryption_requirements =
bookmark_tracker_->model_type_state().encryption_key_name() !=
model_type_state.encryption_key_name();
updates_handler.Process(updates, got_new_encryption_requirements);
bookmark_tracker_->set_model_type_state(
std::make_unique<sync_pb::ModelTypeState>(model_type_state));
// Schedule save just in case one is needed.
schedule_save_closure_.Run();
}
const SyncedBookmarkTracker* BookmarkModelTypeProcessor::GetTrackerForTest()
const {
return bookmark_tracker_.get();
}
std::string BookmarkModelTypeProcessor::EncodeSyncMetadata() const {
std::string metadata_str;
if (bookmark_tracker_) {
sync_pb::BookmarkModelMetadata model_metadata =
bookmark_tracker_->BuildBookmarkModelMetadata();
model_metadata.SerializeToString(&metadata_str);
}
return metadata_str;
}
void BookmarkModelTypeProcessor::ModelReadyToSync(
const std::string& metadata_str,
const base::RepeatingClosure& schedule_save_closure,
bookmarks::BookmarkModel* model) {
DCHECK(model);
DCHECK(!bookmark_model_);
DCHECK(!bookmark_tracker_);
DCHECK(!bookmark_model_observer_);
bookmark_model_ = model;
schedule_save_closure_ = schedule_save_closure;
sync_pb::BookmarkModelMetadata model_metadata;
model_metadata.ParseFromString(metadata_str);
if (model_metadata.model_type_state().initial_sync_done() &&
SyncedBookmarkTracker::BookmarkModelMatchesMetadata(model,
model_metadata)) {
std::vector<NodeMetadataPair> nodes_metadata;
for (sync_pb::BookmarkMetadata& bookmark_metadata :
*model_metadata.mutable_bookmarks_metadata()) {
// TODO(crbug.com/516866): Replace with a more efficient way to retrieve
// all nodes and store in a map keyed by id instead of doing a lookup for
// every id.
const bookmarks::BookmarkNode* node = nullptr;
if (!bookmark_metadata.metadata().is_deleted()) {
node = GetBookmarkNodeByID(bookmark_model_, bookmark_metadata.id());
DCHECK(node);
}
auto metadata = std::make_unique<sync_pb::EntityMetadata>();
metadata->Swap(bookmark_metadata.mutable_metadata());
nodes_metadata.emplace_back(node, std::move(metadata));
}
auto model_type_state = std::make_unique<sync_pb::ModelTypeState>();
model_type_state->Swap(model_metadata.mutable_model_type_state());
StartTrackingMetadata(std::move(nodes_metadata),
std::move(model_type_state));
} else if (!model_metadata.model_type_state().initial_sync_done() &&
!model_metadata.bookmarks_metadata().empty()) {
DLOG(ERROR)
<< "Persisted Metadata not empty while initial sync is not done.";
}
ConnectIfReady();
}
void BookmarkModelTypeProcessor::SetFaviconService(
favicon::FaviconService* favicon_service) {
DCHECK(favicon_service);
favicon_service_ = favicon_service;
}
size_t BookmarkModelTypeProcessor::EstimateMemoryUsage() const {
using base::trace_event::EstimateMemoryUsage;
size_t memory_usage = 0;
memory_usage += bookmark_tracker_->EstimateMemoryUsage();
memory_usage += EstimateMemoryUsage(cache_guid_);
return memory_usage;
}
base::WeakPtr<syncer::ModelTypeControllerDelegate>
BookmarkModelTypeProcessor::GetWeakPtr() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return weak_ptr_factory_.GetWeakPtr();
}
void BookmarkModelTypeProcessor::OnSyncStarting(
const syncer::DataTypeActivationRequest& request,
StartCallback start_callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(start_callback);
// |favicon_service_| should have been set by now.
DCHECK(favicon_service_);
DVLOG(1) << "Sync is starting for Bookmarks";
cache_guid_ = request.cache_guid;
start_callback_ = std::move(start_callback);
DCHECK(!cache_guid_.empty());
ConnectIfReady();
}
void BookmarkModelTypeProcessor::ConnectIfReady() {
// Return if the model isn't ready.
if (!bookmark_model_) {
return;
}
// Return if Sync didn't start yet.
if (!start_callback_) {
return;
}
DCHECK(!cache_guid_.empty());
if (bookmark_tracker_ &&
bookmark_tracker_->model_type_state().cache_guid() != cache_guid_) {
// TODO(crbug.com/820049): Properly handle a mismatch between the loaded
// cache guid stored in |bookmark_tracker_.model_type_state_| at
// DecodeSyncMetadata() and the one received from sync at OnSyncStarting()
// stored in |cache_guid_|.
return;
}
auto activation_context =
std::make_unique<syncer::DataTypeActivationResponse>();
if (bookmark_tracker_) {
activation_context->model_type_state =
bookmark_tracker_->model_type_state();
} else {
sync_pb::ModelTypeState model_type_state;
model_type_state.mutable_progress_marker()->set_data_type_id(
GetSpecificsFieldNumberFromModelType(syncer::BOOKMARKS));
model_type_state.set_cache_guid(cache_guid_);
activation_context->model_type_state = model_type_state;
}
activation_context->type_processor =
std::make_unique<syncer::ModelTypeProcessorProxy>(
weak_ptr_factory_.GetWeakPtr(),
base::SequencedTaskRunnerHandle::Get());
std::move(start_callback_).Run(std::move(activation_context));
}
void BookmarkModelTypeProcessor::OnSyncStopping(
syncer::SyncStopMetadataFate metadata_fate) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Disabling sync for a type shouldn't happen before the model is loaded
// because OnSyncStopping() is not allowed to be called before
// OnSyncStarting() has completed..
DCHECK(bookmark_model_);
DCHECK(!start_callback_);
cache_guid_.clear();
worker_.reset();
switch (metadata_fate) {
case syncer::KEEP_METADATA: {
break;
}
case syncer::CLEAR_METADATA: {
// Stop observing local changes. We'll start observing local changes again
// when Sync is (re)started in StartTrackingMetadata().
if (bookmark_tracker_) {
DCHECK(bookmark_model_observer_);
bookmark_model_->RemoveObserver(bookmark_model_observer_.get());
bookmark_model_observer_.reset();
bookmark_tracker_.reset();
}
schedule_save_closure_.Run();
break;
}
}
// Do not let any delayed callbacks to be called.
weak_ptr_factory_.InvalidateWeakPtrs();
}
void BookmarkModelTypeProcessor::NudgeForCommitIfNeeded() {
DCHECK(bookmark_tracker_);
// Don't bother sending anything if there's no one to send to.
if (!worker_) {
return;
}
// Nudge worker if there are any entities with local changes.
if (bookmark_tracker_->HasLocalChanges()) {
worker_->NudgeForCommit();
}
}
void BookmarkModelTypeProcessor::StartTrackingMetadata(
std::vector<NodeMetadataPair> nodes_metadata,
std::unique_ptr<sync_pb::ModelTypeState> model_type_state) {
bookmark_tracker_ = std::make_unique<SyncedBookmarkTracker>(
std::move(nodes_metadata), std::move(model_type_state));
bookmark_model_observer_ = std::make_unique<BookmarkModelObserverImpl>(
base::BindRepeating(&BookmarkModelTypeProcessor::NudgeForCommitIfNeeded,
base::Unretained(this)),
bookmark_tracker_.get());
bookmark_model_->AddObserver(bookmark_model_observer_.get());
}
void BookmarkModelTypeProcessor::GetAllNodesForDebugging(
AllNodesCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto all_nodes = std::make_unique<base::ListValue>();
// Create a permanent folder since sync server no longer create root folders,
// and USS won't migrate root folders from directory, we create root folders.
auto root_node = std::make_unique<base::DictionaryValue>();
// Function isTypeRootNode in sync_node_browser.js use PARENT_ID and
// UNIQUE_SERVER_TAG to check if the node is root node. isChildOf in
// sync_node_browser.js uses modelType to check if root node is parent of real
// data node. NON_UNIQUE_NAME will be the name of node to display.
root_node->SetString("ID", "BOOKMARKS_ROOT");
root_node->SetString("PARENT_ID", "r");
root_node->SetString("UNIQUE_SERVER_TAG", "Bookmarks");
root_node->SetBoolean("IS_DIR", true);
root_node->SetString("modelType", "Bookmarks");
root_node->SetString("NON_UNIQUE_NAME", "Bookmarks");
all_nodes->Append(std::move(root_node));
const bookmarks::BookmarkNode* model_root_node = bookmark_model_->root_node();
for (int i = 0; i < model_root_node->child_count(); ++i) {
const bookmarks::BookmarkNode* model_permanent_node =
model_root_node->GetChild(i);
AppendNodeAndChildrenForDebugging(model_permanent_node, i, all_nodes.get());
}
std::move(callback).Run(syncer::BOOKMARKS, std::move(all_nodes));
}
void BookmarkModelTypeProcessor::AppendNodeAndChildrenForDebugging(
const bookmarks::BookmarkNode* node,
int index,
base::ListValue* all_nodes) const {
const SyncedBookmarkTracker::Entity* entity =
bookmark_tracker_->GetEntityForBookmarkNode(node);
// Include only tracked nodes. Newly added nodes are tracked even before being
// sent to the server. Managed bookmarks (that are installed by a policy)
// aren't syncable and hence not tracked. In addition, Mobile Bookmarks folder
// is only synced after it's been created on the server.
if (!entity) {
return;
}
const sync_pb::EntityMetadata* metadata = entity->metadata();
// Copy data to an EntityData object to reuse its conversion
// ToDictionaryValue() methods.
syncer::EntityData data;
data.id = metadata->server_id();
data.creation_time = node->date_added();
data.modification_time =
syncer::ProtoTimeToTime(metadata->modification_time());
data.non_unique_name = base::UTF16ToUTF8(node->GetTitle());
data.is_folder = node->is_folder();
data.unique_position = metadata->unique_position();
data.specifics = CreateSpecificsFromBookmarkNode(
node, bookmark_model_, /*force_favicon_load=*/false);
if (node->is_permanent_node()) {
data.server_defined_unique_tag =
ComputeServerDefinedUniqueTagForDebugging(node, bookmark_model_);
// Set the parent to empty string to indicate it's parent of the root node
// for bookmarks. The code in sync_node_browser.js links nodes with the
// "modelType" when they are lacking a parent id.
data.parent_id = "";
} else {
const bookmarks::BookmarkNode* parent = node->parent();
const SyncedBookmarkTracker::Entity* parent_entity =
bookmark_tracker_->GetEntityForBookmarkNode(parent);
DCHECK(parent_entity);
data.parent_id = parent_entity->metadata()->server_id();
}
std::unique_ptr<base::DictionaryValue> data_dictionary =
data.ToDictionaryValue();
// TODO(https://crbug.com/516866): Prepending the ID with an "s" is consistent
// with the implementation in ClientTagBasedModelTypeProcessor. Double check
// if this is actually needed and update both implementations if makes sense.
// Set ID value as in legacy directory-based implementation, "s" means server.
data_dictionary->SetString("ID", "s" + metadata->server_id());
if (node->is_permanent_node()) {
// Hardcode the parent of permanent nodes.
data_dictionary->SetString("PARENT_ID", "BOOKMARKS_ROOT");
data_dictionary->SetString("UNIQUE_SERVER_TAG",
data.server_defined_unique_tag);
} else {
data_dictionary->SetString("PARENT_ID", "s" + data.parent_id);
}
data_dictionary->SetInteger("LOCAL_EXTERNAL_ID", node->id());
data_dictionary->SetInteger("positionIndex", index);
data_dictionary->Set("metadata", syncer::EntityMetadataToValue(*metadata));
data_dictionary->SetString("modelType", "Bookmarks");
all_nodes->Append(std::move(data_dictionary));
for (int i = 0; i < node->child_count(); ++i) {
AppendNodeAndChildrenForDebugging(node->GetChild(i), i, all_nodes);
}
}
void BookmarkModelTypeProcessor::GetStatusCountersForDebugging(
StatusCountersCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
syncer::StatusCounters counters;
if (bookmark_tracker_) {
counters.num_entries =
bookmark_tracker_->TrackedBookmarksCountForDebugging();
counters.num_entries_and_tombstones =
counters.num_entries +
bookmark_tracker_->TrackedUncommittedTombstonesCountForDebugging();
}
std::move(callback).Run(syncer::BOOKMARKS, counters);
}
void BookmarkModelTypeProcessor::RecordMemoryUsageAndCountsHistograms() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
SyncRecordModelTypeMemoryHistogram(syncer::BOOKMARKS, EstimateMemoryUsage());
SyncRecordModelTypeCountHistogram(
syncer::BOOKMARKS,
bookmark_tracker_->TrackedBookmarksCountForDebugging());
}
} // namespace sync_bookmarks