// Copyright (c) 2011 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 "chrome/browser/sync/glue/bookmark_change_processor.h"
#include <stack>
#include <vector>
#include "base/string16.h"
#include "base/string_util.h"
#include "base/utf_string_conversions.h"
#include "chrome/browser/bookmarks/bookmark_utils.h"
#include "chrome/browser/favicon_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/sync/profile_sync_service.h"
#include "content/browser/browser_thread.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/gfx/codec/png_codec.h"
namespace browser_sync {
BookmarkChangeProcessor::BookmarkChangeProcessor(
BookmarkModelAssociator* model_associator,
UnrecoverableErrorHandler* error_handler)
: ChangeProcessor(error_handler),
bookmark_model_(NULL),
model_associator_(model_associator) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DCHECK(model_associator);
DCHECK(error_handler);
}
void BookmarkChangeProcessor::StartImpl(Profile* profile) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DCHECK(!bookmark_model_);
bookmark_model_ = profile->GetBookmarkModel();
DCHECK(bookmark_model_->IsLoaded());
bookmark_model_->AddObserver(this);
}
void BookmarkChangeProcessor::StopImpl() {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DCHECK(bookmark_model_);
bookmark_model_->RemoveObserver(this);
bookmark_model_ = NULL;
model_associator_ = NULL;
}
void BookmarkChangeProcessor::UpdateSyncNodeProperties(
const BookmarkNode* src, BookmarkModel* model, sync_api::WriteNode* dst) {
// Set the properties of the item.
dst->SetIsFolder(src->is_folder());
dst->SetTitle(UTF16ToWideHack(src->GetTitle()));
if (!src->is_folder())
dst->SetURL(src->GetURL());
SetSyncNodeFavicon(src, model, dst);
}
// static
void BookmarkChangeProcessor::EncodeFavicon(const BookmarkNode* src,
BookmarkModel* model,
std::vector<unsigned char>* dst) {
const SkBitmap& favicon = model->GetFavicon(src);
dst->clear();
// Check for zero-dimension images. This can happen if the favicon is
// still being loaded.
if (favicon.empty())
return;
// Re-encode the BookmarkNode's favicon as a PNG, and pass the data to the
// sync subsystem.
if (!gfx::PNGCodec::EncodeBGRASkBitmap(favicon, false, dst))
return;
}
void BookmarkChangeProcessor::RemoveOneSyncNode(
sync_api::WriteTransaction* trans, const BookmarkNode* node) {
sync_api::WriteNode sync_node(trans);
if (!model_associator_->InitSyncNodeFromChromeId(node->id(), &sync_node)) {
error_handler()->OnUnrecoverableError(FROM_HERE, std::string());
return;
}
// This node should have no children.
DCHECK(sync_node.GetFirstChildId() == sync_api::kInvalidId);
// Remove association and delete the sync node.
model_associator_->Disassociate(sync_node.GetId());
sync_node.Remove();
}
void BookmarkChangeProcessor::RemoveSyncNodeHierarchy(
const BookmarkNode* topmost) {
sync_api::WriteTransaction trans(share_handle());
// Later logic assumes that |topmost| has been unlinked.
DCHECK(!topmost->parent());
// A BookmarkModel deletion event means that |node| and all its children were
// deleted. Sync backend expects children to be deleted individually, so we do
// a depth-first-search here. At each step, we consider the |index|-th child
// of |node|. |index_stack| stores index values for the parent levels.
std::stack<int> index_stack;
index_stack.push(0); // For the final pop. It's never used.
const BookmarkNode* node = topmost;
int index = 0;
while (node) {
// The top of |index_stack| should always be |node|'s index.
DCHECK(!node->parent() || (node->parent()->GetIndexOf(node) ==
index_stack.top()));
if (index == node->child_count()) {
// If we've processed all of |node|'s children, delete |node| and move
// on to its successor.
RemoveOneSyncNode(&trans, node);
node = node->parent();
index = index_stack.top() + 1; // (top() + 0) was what we removed.
index_stack.pop();
} else {
// If |node| has an unprocessed child, process it next after pushing the
// current state onto the stack.
DCHECK_LT(index, node->child_count());
index_stack.push(index);
node = node->GetChild(index);
index = 0;
}
}
DCHECK(index_stack.empty()); // Nothing should be left on the stack.
}
void BookmarkChangeProcessor::Loaded(BookmarkModel* model) {
NOTREACHED();
}
void BookmarkChangeProcessor::BookmarkModelBeingDeleted(
BookmarkModel* model) {
DCHECK(!running()) << "BookmarkModel deleted while ChangeProcessor running.";
bookmark_model_ = NULL;
}
void BookmarkChangeProcessor::BookmarkNodeAdded(BookmarkModel* model,
const BookmarkNode* parent,
int index) {
DCHECK(running());
DCHECK(share_handle());
// Acquire a scoped write lock via a transaction.
sync_api::WriteTransaction trans(share_handle());
CreateSyncNode(parent, model, index, &trans, model_associator_,
error_handler());
}
// static
int64 BookmarkChangeProcessor::CreateSyncNode(const BookmarkNode* parent,
BookmarkModel* model, int index, sync_api::WriteTransaction* trans,
BookmarkModelAssociator* associator,
UnrecoverableErrorHandler* error_handler) {
const BookmarkNode* child = parent->GetChild(index);
DCHECK(child);
// Create a WriteNode container to hold the new node.
sync_api::WriteNode sync_child(trans);
// Actually create the node with the appropriate initial position.
if (!PlaceSyncNode(CREATE, parent, index, trans, &sync_child, associator)) {
error_handler->OnUnrecoverableError(FROM_HERE,
"Sync node creation failed; recovery unlikely");
return sync_api::kInvalidId;
}
UpdateSyncNodeProperties(child, model, &sync_child);
// Associate the ID from the sync domain with the bookmark node, so that we
// can refer back to this item later.
associator->Associate(child, sync_child.GetId());
return sync_child.GetId();
}
void BookmarkChangeProcessor::BookmarkNodeRemoved(BookmarkModel* model,
const BookmarkNode* parent,
int index,
const BookmarkNode* node) {
DCHECK(running());
RemoveSyncNodeHierarchy(node);
}
void BookmarkChangeProcessor::BookmarkNodeChanged(BookmarkModel* model,
const BookmarkNode* node) {
DCHECK(running());
// We shouldn't see changes to the top-level nodes.
if (node == model->GetBookmarkBarNode() || node == model->other_node()) {
NOTREACHED() << "Saw update to permanent node!";
return;
}
// Acquire a scoped write lock via a transaction.
sync_api::WriteTransaction trans(share_handle());
// Lookup the sync node that's associated with |node|.
sync_api::WriteNode sync_node(&trans);
if (!model_associator_->InitSyncNodeFromChromeId(node->id(), &sync_node)) {
error_handler()->OnUnrecoverableError(FROM_HERE, std::string());
return;
}
UpdateSyncNodeProperties(node, model, &sync_node);
DCHECK_EQ(sync_node.GetIsFolder(), node->is_folder());
DCHECK_EQ(model_associator_->GetChromeNodeFromSyncId(
sync_node.GetParentId()),
node->parent());
// This node's index should be one more than the predecessor's index.
DCHECK_EQ(node->parent()->GetIndexOf(node),
CalculateBookmarkModelInsertionIndex(node->parent(),
&sync_node));
}
void BookmarkChangeProcessor::BookmarkNodeMoved(BookmarkModel* model,
const BookmarkNode* old_parent, int old_index,
const BookmarkNode* new_parent, int new_index) {
DCHECK(running());
const BookmarkNode* child = new_parent->GetChild(new_index);
// We shouldn't see changes to the top-level nodes.
if (child == model->GetBookmarkBarNode() || child == model->other_node()) {
NOTREACHED() << "Saw update to permanent node!";
return;
}
// Acquire a scoped write lock via a transaction.
sync_api::WriteTransaction trans(share_handle());
// Lookup the sync node that's associated with |child|.
sync_api::WriteNode sync_node(&trans);
if (!model_associator_->InitSyncNodeFromChromeId(child->id(), &sync_node)) {
error_handler()->OnUnrecoverableError(FROM_HERE, std::string());
return;
}
if (!PlaceSyncNode(MOVE, new_parent, new_index, &trans, &sync_node,
model_associator_)) {
error_handler()->OnUnrecoverableError(FROM_HERE, std::string());
return;
}
}
void BookmarkChangeProcessor::BookmarkNodeFaviconLoaded(BookmarkModel* model,
const BookmarkNode* node) {
DCHECK(running());
BookmarkNodeChanged(model, node);
}
void BookmarkChangeProcessor::BookmarkNodeChildrenReordered(
BookmarkModel* model, const BookmarkNode* node) {
// Acquire a scoped write lock via a transaction.
sync_api::WriteTransaction trans(share_handle());
// The given node's children got reordered. We need to reorder all the
// children of the corresponding sync node.
for (int i = 0; i < node->child_count(); ++i) {
sync_api::WriteNode sync_child(&trans);
if (!model_associator_->InitSyncNodeFromChromeId(node->GetChild(i)->id(),
&sync_child)) {
error_handler()->OnUnrecoverableError(FROM_HERE, std::string());
return;
}
DCHECK_EQ(sync_child.GetParentId(),
model_associator_->GetSyncIdFromChromeId(node->id()));
if (!PlaceSyncNode(MOVE, node, i, &trans, &sync_child,
model_associator_)) {
error_handler()->OnUnrecoverableError(FROM_HERE, std::string());
return;
}
}
}
// static
bool BookmarkChangeProcessor::PlaceSyncNode(MoveOrCreate operation,
const BookmarkNode* parent, int index, sync_api::WriteTransaction* trans,
sync_api::WriteNode* dst, BookmarkModelAssociator* associator) {
sync_api::ReadNode sync_parent(trans);
if (!associator->InitSyncNodeFromChromeId(parent->id(), &sync_parent)) {
LOG(WARNING) << "Parent lookup failed";
return false;
}
bool success = false;
if (index == 0) {
// Insert into first position.
success = (operation == CREATE) ?
dst->InitByCreation(syncable::BOOKMARKS, sync_parent, NULL) :
dst->SetPosition(sync_parent, NULL);
if (success) {
DCHECK_EQ(dst->GetParentId(), sync_parent.GetId());
DCHECK_EQ(dst->GetId(), sync_parent.GetFirstChildId());
DCHECK_EQ(dst->GetPredecessorId(), sync_api::kInvalidId);
}
} else {
// Find the bookmark model predecessor, and insert after it.
const BookmarkNode* prev = parent->GetChild(index - 1);
sync_api::ReadNode sync_prev(trans);
if (!associator->InitSyncNodeFromChromeId(prev->id(), &sync_prev)) {
LOG(WARNING) << "Predecessor lookup failed";
return false;
}
success = (operation == CREATE) ?
dst->InitByCreation(syncable::BOOKMARKS, sync_parent, &sync_prev) :
dst->SetPosition(sync_parent, &sync_prev);
if (success) {
DCHECK_EQ(dst->GetParentId(), sync_parent.GetId());
DCHECK_EQ(dst->GetPredecessorId(), sync_prev.GetId());
DCHECK_EQ(dst->GetId(), sync_prev.GetSuccessorId());
}
}
return success;
}
// Determine the bookmark model index to which a node must be moved so that
// predecessor of the node (in the bookmark model) matches the predecessor of
// |source| (in the sync model).
// As a precondition, this assumes that the predecessor of |source| has been
// updated and is already in the correct position in the bookmark model.
int BookmarkChangeProcessor::CalculateBookmarkModelInsertionIndex(
const BookmarkNode* parent,
const sync_api::BaseNode* child_info) const {
DCHECK(parent);
DCHECK(child_info);
int64 predecessor_id = child_info->GetPredecessorId();
// A return ID of kInvalidId indicates no predecessor.
if (predecessor_id == sync_api::kInvalidId)
return 0;
// Otherwise, insert after the predecessor bookmark node.
const BookmarkNode* predecessor =
model_associator_->GetChromeNodeFromSyncId(predecessor_id);
DCHECK(predecessor);
DCHECK_EQ(predecessor->parent(), parent);
return parent->GetIndexOf(predecessor) + 1;
}
// ApplyModelChanges is called by the sync backend after changes have been made
// to the sync engine's model. Apply these changes to the browser bookmark
// model.
void BookmarkChangeProcessor::ApplyChangesFromSyncModel(
const sync_api::BaseTransaction* trans,
const sync_api::SyncManager::ChangeRecord* changes,
int change_count) {
if (!running())
return;
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
// A note about ordering. Sync backend is responsible for ordering the change
// records in the following order:
//
// 1. Deletions, from leaves up to parents.
// 2. Existing items with synced parents & predecessors.
// 3. New items with synced parents & predecessors.
// 4. Items with parents & predecessors in the list.
// 5. Repeat #4 until all items are in the list.
//
// "Predecessor" here means the previous item within a given folder; an item
// in the first position is always said to have a synced predecessor.
// For the most part, applying these changes in the order given will yield
// the correct result. There is one exception, however: for items that are
// moved away from a folder that is being deleted, we will process the delete
// before the move. Since deletions in the bookmark model propagate from
// parent to child, we must move them to a temporary location.
BookmarkModel* model = bookmark_model_;
// We are going to make changes to the bookmarks model, but don't want to end
// up in a feedback loop, so remove ourselves as an observer while applying
// changes.
model->RemoveObserver(this);
// A parent to hold nodes temporarily orphaned by parent deletion. It is
// lazily created inside the loop.
const BookmarkNode* foster_parent = NULL;
for (int i = 0; i < change_count; ++i) {
const BookmarkNode* dst =
model_associator_->GetChromeNodeFromSyncId(changes[i].id);
// Ignore changes to the permanent top-level nodes. We only care about
// their children.
if ((dst == model->GetBookmarkBarNode()) || (dst == model->other_node()))
continue;
if (changes[i].action ==
sync_api::SyncManager::ChangeRecord::ACTION_DELETE) {
// Deletions should always be at the front of the list.
DCHECK(i == 0 || changes[i-1].action == changes[i].action);
// Children of a deleted node should not be deleted; they may be
// reparented by a later change record. Move them to a temporary place.
DCHECK(dst) << "Could not find node to be deleted";
if (!dst) // Can't do anything if we can't find the chrome node.
continue;
const BookmarkNode* parent = dst->parent();
if (dst->child_count()) {
if (!foster_parent) {
foster_parent = model->AddFolder(model->other_node(),
model->other_node()->child_count(),
string16());
}
for (int i = dst->child_count() - 1; i >= 0; --i) {
model->Move(dst->GetChild(i), foster_parent,
foster_parent->child_count());
}
}
DCHECK_EQ(dst->child_count(), 0) << "Node being deleted has children";
model_associator_->Disassociate(changes[i].id);
int index = parent->GetIndexOf(dst);
if (index > -1)
model->Remove(parent, index);
dst = NULL;
} else {
DCHECK_EQ((changes[i].action ==
sync_api::SyncManager::ChangeRecord::ACTION_ADD), (dst == NULL))
<< "ACTION_ADD should be seen if and only if the node is unknown.";
sync_api::ReadNode src(trans);
if (!src.InitByIdLookup(changes[i].id)) {
error_handler()->OnUnrecoverableError(FROM_HERE,
"ApplyModelChanges was passed a bad ID");
return;
}
CreateOrUpdateBookmarkNode(&src, model);
}
}
// Clean up the temporary node.
if (foster_parent) {
// There should be no nodes left under the foster parent.
DCHECK_EQ(foster_parent->child_count(), 0);
model->Remove(foster_parent->parent(),
foster_parent->parent()->GetIndexOf(foster_parent));
foster_parent = NULL;
}
// We are now ready to hear about bookmarks changes again.
model->AddObserver(this);
}
// Create a bookmark node corresponding to |src| if one is not already
// associated with |src|.
const BookmarkNode* BookmarkChangeProcessor::CreateOrUpdateBookmarkNode(
sync_api::BaseNode* src,
BookmarkModel* model) {
const BookmarkNode* parent =
model_associator_->GetChromeNodeFromSyncId(src->GetParentId());
if (!parent) {
DLOG(WARNING) << "Could not find parent of node being added/updated."
<< " Node title: " << src->GetTitle()
<< ", parent id = " << src->GetParentId();
return NULL;
}
int index = CalculateBookmarkModelInsertionIndex(parent, src);
const BookmarkNode* dst = model_associator_->GetChromeNodeFromSyncId(
src->GetId());
if (!dst) {
dst = CreateBookmarkNode(src, parent, model, index);
model_associator_->Associate(dst, src->GetId());
} else {
// URL and is_folder are not expected to change.
// TODO(ncarter): Determine if such changes should be legal or not.
DCHECK_EQ(src->GetIsFolder(), dst->is_folder());
// Handle reparenting and/or repositioning.
model->Move(dst, parent, index);
if (!src->GetIsFolder())
model->SetURL(dst, src->GetURL());
model->SetTitle(dst, WideToUTF16Hack(src->GetTitle()));
SetBookmarkFavicon(src, dst, model);
}
return dst;
}
// static
// Creates a bookmark node under the given parent node from the given sync
// node. Returns the newly created node.
const BookmarkNode* BookmarkChangeProcessor::CreateBookmarkNode(
sync_api::BaseNode* sync_node,
const BookmarkNode* parent,
BookmarkModel* model,
int index) {
DCHECK(parent);
DCHECK(index >= 0 && index <= parent->child_count());
const BookmarkNode* node;
if (sync_node->GetIsFolder()) {
node = model->AddFolder(parent, index,
WideToUTF16Hack(sync_node->GetTitle()));
} else {
node = model->AddURL(parent, index,
WideToUTF16Hack(sync_node->GetTitle()),
sync_node->GetURL());
SetBookmarkFavicon(sync_node, node, model);
}
return node;
}
// static
// Sets the favicon of the given bookmark node from the given sync node.
bool BookmarkChangeProcessor::SetBookmarkFavicon(
sync_api::BaseNode* sync_node,
const BookmarkNode* bookmark_node,
BookmarkModel* bookmark_model) {
std::vector<unsigned char> icon_bytes_vector;
sync_node->GetFaviconBytes(&icon_bytes_vector);
if (icon_bytes_vector.empty())
return false;
ApplyBookmarkFavicon(bookmark_node, bookmark_model->profile(),
icon_bytes_vector);
return true;
}
// static
// Applies the given favicon bytes vector to the given bookmark node.
void BookmarkChangeProcessor::ApplyBookmarkFavicon(
const BookmarkNode* bookmark_node,
Profile* profile,
const std::vector<unsigned char>& icon_bytes_vector) {
// Registering a favicon requires that we provide a source URL, but we
// don't know where these came from. Currently we just use the
// destination URL, which is not correct, but since the favicon URL
// is used as a key in the history's thumbnail DB, this gives us a value
// which does not collide with others.
GURL fake_icon_url = bookmark_node->GetURL();
HistoryService* history =
profile->GetHistoryService(Profile::EXPLICIT_ACCESS);
FaviconService* favicon_service =
profile->GetFaviconService(Profile::EXPLICIT_ACCESS);
history->AddPageNoVisitForBookmark(bookmark_node->GetURL());
favicon_service->SetFavicon(bookmark_node->GetURL(),
fake_icon_url,
icon_bytes_vector,
history::FAVICON);
}
// static
void BookmarkChangeProcessor::SetSyncNodeFavicon(
const BookmarkNode* bookmark_node,
BookmarkModel* model,
sync_api::WriteNode* sync_node) {
std::vector<unsigned char> favicon_bytes;
EncodeFavicon(bookmark_node, model, &favicon_bytes);
if (!favicon_bytes.empty())
sync_node->SetFaviconBytes(favicon_bytes);
}
} // namespace browser_sync