bookmark_change_processor.cc revision 72a454cd3513ac24fbdd0e0cb9ad70b86a99b801
1// Copyright (c) 2010 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4#include "chrome/browser/sync/glue/bookmark_change_processor.h" 5 6#include <stack> 7#include <vector> 8 9#include "base/string16.h" 10#include "base/string_util.h" 11 12#include "base/utf_string_conversions.h" 13#include "chrome/browser/bookmarks/bookmark_utils.h" 14#include "chrome/browser/browser_thread.h" 15#include "chrome/browser/favicon_service.h" 16#include "chrome/browser/profiles/profile.h" 17#include "chrome/browser/sync/profile_sync_service.h" 18#include "third_party/skia/include/core/SkBitmap.h" 19#include "ui/gfx/codec/png_codec.h" 20 21namespace browser_sync { 22 23BookmarkChangeProcessor::BookmarkChangeProcessor( 24 BookmarkModelAssociator* model_associator, 25 UnrecoverableErrorHandler* error_handler) 26 : ChangeProcessor(error_handler), 27 bookmark_model_(NULL), 28 model_associator_(model_associator) { 29 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 30 DCHECK(model_associator); 31 DCHECK(error_handler); 32} 33 34void BookmarkChangeProcessor::StartImpl(Profile* profile) { 35 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 36 DCHECK(!bookmark_model_); 37 bookmark_model_ = profile->GetBookmarkModel(); 38 DCHECK(bookmark_model_->IsLoaded()); 39 bookmark_model_->AddObserver(this); 40} 41 42void BookmarkChangeProcessor::StopImpl() { 43 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 44 DCHECK(bookmark_model_); 45 bookmark_model_->RemoveObserver(this); 46 bookmark_model_ = NULL; 47 model_associator_ = NULL; 48} 49 50void BookmarkChangeProcessor::UpdateSyncNodeProperties( 51 const BookmarkNode* src, BookmarkModel* model, sync_api::WriteNode* dst) { 52 // Set the properties of the item. 53 dst->SetIsFolder(src->is_folder()); 54 dst->SetTitle(UTF16ToWideHack(src->GetTitle())); 55 if (!src->is_folder()) 56 dst->SetURL(src->GetURL()); 57 SetSyncNodeFavicon(src, model, dst); 58} 59 60// static 61void BookmarkChangeProcessor::EncodeFavicon(const BookmarkNode* src, 62 BookmarkModel* model, 63 std::vector<unsigned char>* dst) { 64 const SkBitmap& favicon = model->GetFavIcon(src); 65 66 dst->clear(); 67 68 // Check for zero-dimension images. This can happen if the favicon is 69 // still being loaded. 70 if (favicon.empty()) 71 return; 72 73 // Re-encode the BookmarkNode's favicon as a PNG, and pass the data to the 74 // sync subsystem. 75 if (!gfx::PNGCodec::EncodeBGRASkBitmap(favicon, false, dst)) 76 return; 77} 78 79void BookmarkChangeProcessor::RemoveOneSyncNode( 80 sync_api::WriteTransaction* trans, const BookmarkNode* node) { 81 sync_api::WriteNode sync_node(trans); 82 if (!model_associator_->InitSyncNodeFromChromeId(node->id(), &sync_node)) { 83 error_handler()->OnUnrecoverableError(FROM_HERE, std::string()); 84 return; 85 } 86 // This node should have no children. 87 DCHECK(sync_node.GetFirstChildId() == sync_api::kInvalidId); 88 // Remove association and delete the sync node. 89 model_associator_->Disassociate(sync_node.GetId()); 90 sync_node.Remove(); 91} 92 93void BookmarkChangeProcessor::RemoveSyncNodeHierarchy( 94 const BookmarkNode* topmost) { 95 sync_api::WriteTransaction trans(share_handle()); 96 97 // Later logic assumes that |topmost| has been unlinked. 98 DCHECK(!topmost->GetParent()); 99 100 // A BookmarkModel deletion event means that |node| and all its children were 101 // deleted. Sync backend expects children to be deleted individually, so we do 102 // a depth-first-search here. At each step, we consider the |index|-th child 103 // of |node|. |index_stack| stores index values for the parent levels. 104 std::stack<int> index_stack; 105 index_stack.push(0); // For the final pop. It's never used. 106 const BookmarkNode* node = topmost; 107 int index = 0; 108 while (node) { 109 // The top of |index_stack| should always be |node|'s index. 110 DCHECK(!node->GetParent() || (node->GetParent()->IndexOfChild(node) == 111 index_stack.top())); 112 if (index == node->GetChildCount()) { 113 // If we've processed all of |node|'s children, delete |node| and move 114 // on to its successor. 115 RemoveOneSyncNode(&trans, node); 116 node = node->GetParent(); 117 index = index_stack.top() + 1; // (top() + 0) was what we removed. 118 index_stack.pop(); 119 } else { 120 // If |node| has an unprocessed child, process it next after pushing the 121 // current state onto the stack. 122 DCHECK_LT(index, node->GetChildCount()); 123 index_stack.push(index); 124 node = node->GetChild(index); 125 index = 0; 126 } 127 } 128 DCHECK(index_stack.empty()); // Nothing should be left on the stack. 129} 130 131void BookmarkChangeProcessor::Loaded(BookmarkModel* model) { 132 NOTREACHED(); 133} 134 135void BookmarkChangeProcessor::BookmarkModelBeingDeleted( 136 BookmarkModel* model) { 137 DCHECK(!running()) << "BookmarkModel deleted while ChangeProcessor running."; 138 bookmark_model_ = NULL; 139} 140 141void BookmarkChangeProcessor::BookmarkNodeAdded(BookmarkModel* model, 142 const BookmarkNode* parent, 143 int index) { 144 DCHECK(running()); 145 DCHECK(share_handle()); 146 147 // Acquire a scoped write lock via a transaction. 148 sync_api::WriteTransaction trans(share_handle()); 149 150 CreateSyncNode(parent, model, index, &trans, model_associator_, 151 error_handler()); 152} 153 154// static 155int64 BookmarkChangeProcessor::CreateSyncNode(const BookmarkNode* parent, 156 BookmarkModel* model, int index, sync_api::WriteTransaction* trans, 157 BookmarkModelAssociator* associator, 158 UnrecoverableErrorHandler* error_handler) { 159 const BookmarkNode* child = parent->GetChild(index); 160 DCHECK(child); 161 162 // Create a WriteNode container to hold the new node. 163 sync_api::WriteNode sync_child(trans); 164 165 // Actually create the node with the appropriate initial position. 166 if (!PlaceSyncNode(CREATE, parent, index, trans, &sync_child, associator, 167 error_handler)) { 168 error_handler->OnUnrecoverableError(FROM_HERE, 169 "Sync node creation failed; recovery unlikely"); 170 return sync_api::kInvalidId; 171 } 172 173 UpdateSyncNodeProperties(child, model, &sync_child); 174 175 // Associate the ID from the sync domain with the bookmark node, so that we 176 // can refer back to this item later. 177 associator->Associate(child, sync_child.GetId()); 178 179 return sync_child.GetId(); 180} 181 182 183void BookmarkChangeProcessor::BookmarkNodeRemoved(BookmarkModel* model, 184 const BookmarkNode* parent, 185 int index, 186 const BookmarkNode* node) { 187 DCHECK(running()); 188 RemoveSyncNodeHierarchy(node); 189} 190 191void BookmarkChangeProcessor::BookmarkNodeChanged(BookmarkModel* model, 192 const BookmarkNode* node) { 193 DCHECK(running()); 194 // We shouldn't see changes to the top-level nodes. 195 if (node == model->GetBookmarkBarNode() || node == model->other_node()) { 196 NOTREACHED() << "Saw update to permanent node!"; 197 return; 198 } 199 200 // Acquire a scoped write lock via a transaction. 201 sync_api::WriteTransaction trans(share_handle()); 202 203 // Lookup the sync node that's associated with |node|. 204 sync_api::WriteNode sync_node(&trans); 205 if (!model_associator_->InitSyncNodeFromChromeId(node->id(), &sync_node)) { 206 error_handler()->OnUnrecoverableError(FROM_HERE, std::string()); 207 return; 208 } 209 210 UpdateSyncNodeProperties(node, model, &sync_node); 211 212 DCHECK_EQ(sync_node.GetIsFolder(), node->is_folder()); 213 DCHECK_EQ(model_associator_->GetChromeNodeFromSyncId( 214 sync_node.GetParentId()), 215 node->GetParent()); 216 // This node's index should be one more than the predecessor's index. 217 DCHECK_EQ(node->GetParent()->IndexOfChild(node), 218 CalculateBookmarkModelInsertionIndex(node->GetParent(), 219 &sync_node)); 220} 221 222 223void BookmarkChangeProcessor::BookmarkNodeMoved(BookmarkModel* model, 224 const BookmarkNode* old_parent, int old_index, 225 const BookmarkNode* new_parent, int new_index) { 226 DCHECK(running()); 227 const BookmarkNode* child = new_parent->GetChild(new_index); 228 // We shouldn't see changes to the top-level nodes. 229 if (child == model->GetBookmarkBarNode() || child == model->other_node()) { 230 NOTREACHED() << "Saw update to permanent node!"; 231 return; 232 } 233 234 // Acquire a scoped write lock via a transaction. 235 sync_api::WriteTransaction trans(share_handle()); 236 237 // Lookup the sync node that's associated with |child|. 238 sync_api::WriteNode sync_node(&trans); 239 if (!model_associator_->InitSyncNodeFromChromeId(child->id(), &sync_node)) { 240 error_handler()->OnUnrecoverableError(FROM_HERE, std::string()); 241 return; 242 } 243 244 if (!PlaceSyncNode(MOVE, new_parent, new_index, &trans, &sync_node, 245 model_associator_, error_handler())) { 246 error_handler()->OnUnrecoverableError(FROM_HERE, std::string()); 247 return; 248 } 249} 250 251void BookmarkChangeProcessor::BookmarkNodeFavIconLoaded(BookmarkModel* model, 252 const BookmarkNode* node) { 253 DCHECK(running()); 254 BookmarkNodeChanged(model, node); 255} 256 257void BookmarkChangeProcessor::BookmarkNodeChildrenReordered( 258 BookmarkModel* model, const BookmarkNode* node) { 259 260 // Acquire a scoped write lock via a transaction. 261 sync_api::WriteTransaction trans(share_handle()); 262 263 // The given node's children got reordered. We need to reorder all the 264 // children of the corresponding sync node. 265 for (int i = 0; i < node->GetChildCount(); ++i) { 266 sync_api::WriteNode sync_child(&trans); 267 if (!model_associator_->InitSyncNodeFromChromeId(node->GetChild(i)->id(), 268 &sync_child)) { 269 error_handler()->OnUnrecoverableError(FROM_HERE, std::string()); 270 return; 271 } 272 DCHECK_EQ(sync_child.GetParentId(), 273 model_associator_->GetSyncIdFromChromeId(node->id())); 274 275 if (!PlaceSyncNode(MOVE, node, i, &trans, &sync_child, 276 model_associator_, error_handler())) { 277 error_handler()->OnUnrecoverableError(FROM_HERE, std::string()); 278 return; 279 } 280 } 281} 282 283// static 284bool BookmarkChangeProcessor::PlaceSyncNode(MoveOrCreate operation, 285 const BookmarkNode* parent, int index, sync_api::WriteTransaction* trans, 286 sync_api::WriteNode* dst, BookmarkModelAssociator* associator, 287 UnrecoverableErrorHandler* error_handler) { 288 sync_api::ReadNode sync_parent(trans); 289 if (!associator->InitSyncNodeFromChromeId(parent->id(), &sync_parent)) { 290 LOG(WARNING) << "Parent lookup failed"; 291 error_handler->OnUnrecoverableError(FROM_HERE, std::string()); 292 return false; 293 } 294 295 bool success = false; 296 if (index == 0) { 297 // Insert into first position. 298 success = (operation == CREATE) ? 299 dst->InitByCreation(syncable::BOOKMARKS, sync_parent, NULL) : 300 dst->SetPosition(sync_parent, NULL); 301 if (success) { 302 DCHECK_EQ(dst->GetParentId(), sync_parent.GetId()); 303 DCHECK_EQ(dst->GetId(), sync_parent.GetFirstChildId()); 304 DCHECK_EQ(dst->GetPredecessorId(), sync_api::kInvalidId); 305 } 306 } else { 307 // Find the bookmark model predecessor, and insert after it. 308 const BookmarkNode* prev = parent->GetChild(index - 1); 309 sync_api::ReadNode sync_prev(trans); 310 if (!associator->InitSyncNodeFromChromeId(prev->id(), &sync_prev)) { 311 LOG(WARNING) << "Predecessor lookup failed"; 312 return false; 313 } 314 success = (operation == CREATE) ? 315 dst->InitByCreation(syncable::BOOKMARKS, sync_parent, &sync_prev) : 316 dst->SetPosition(sync_parent, &sync_prev); 317 if (success) { 318 DCHECK_EQ(dst->GetParentId(), sync_parent.GetId()); 319 DCHECK_EQ(dst->GetPredecessorId(), sync_prev.GetId()); 320 DCHECK_EQ(dst->GetId(), sync_prev.GetSuccessorId()); 321 } 322 } 323 return success; 324} 325 326// Determine the bookmark model index to which a node must be moved so that 327// predecessor of the node (in the bookmark model) matches the predecessor of 328// |source| (in the sync model). 329// As a precondition, this assumes that the predecessor of |source| has been 330// updated and is already in the correct position in the bookmark model. 331int BookmarkChangeProcessor::CalculateBookmarkModelInsertionIndex( 332 const BookmarkNode* parent, 333 const sync_api::BaseNode* child_info) const { 334 DCHECK(parent); 335 DCHECK(child_info); 336 int64 predecessor_id = child_info->GetPredecessorId(); 337 // A return ID of kInvalidId indicates no predecessor. 338 if (predecessor_id == sync_api::kInvalidId) 339 return 0; 340 341 // Otherwise, insert after the predecessor bookmark node. 342 const BookmarkNode* predecessor = 343 model_associator_->GetChromeNodeFromSyncId(predecessor_id); 344 DCHECK(predecessor); 345 DCHECK_EQ(predecessor->GetParent(), parent); 346 return parent->IndexOfChild(predecessor) + 1; 347} 348 349// ApplyModelChanges is called by the sync backend after changes have been made 350// to the sync engine's model. Apply these changes to the browser bookmark 351// model. 352void BookmarkChangeProcessor::ApplyChangesFromSyncModel( 353 const sync_api::BaseTransaction* trans, 354 const sync_api::SyncManager::ChangeRecord* changes, 355 int change_count) { 356 if (!running()) 357 return; 358 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 359 // A note about ordering. Sync backend is responsible for ordering the change 360 // records in the following order: 361 // 362 // 1. Deletions, from leaves up to parents. 363 // 2. Existing items with synced parents & predecessors. 364 // 3. New items with synced parents & predecessors. 365 // 4. Items with parents & predecessors in the list. 366 // 5. Repeat #4 until all items are in the list. 367 // 368 // "Predecessor" here means the previous item within a given folder; an item 369 // in the first position is always said to have a synced predecessor. 370 // For the most part, applying these changes in the order given will yield 371 // the correct result. There is one exception, however: for items that are 372 // moved away from a folder that is being deleted, we will process the delete 373 // before the move. Since deletions in the bookmark model propagate from 374 // parent to child, we must move them to a temporary location. 375 BookmarkModel* model = bookmark_model_; 376 377 // We are going to make changes to the bookmarks model, but don't want to end 378 // up in a feedback loop, so remove ourselves as an observer while applying 379 // changes. 380 model->RemoveObserver(this); 381 382 // A parent to hold nodes temporarily orphaned by parent deletion. It is 383 // lazily created inside the loop. 384 const BookmarkNode* foster_parent = NULL; 385 for (int i = 0; i < change_count; ++i) { 386 const BookmarkNode* dst = 387 model_associator_->GetChromeNodeFromSyncId(changes[i].id); 388 // Ignore changes to the permanent top-level nodes. We only care about 389 // their children. 390 if ((dst == model->GetBookmarkBarNode()) || (dst == model->other_node())) 391 continue; 392 if (changes[i].action == 393 sync_api::SyncManager::ChangeRecord::ACTION_DELETE) { 394 // Deletions should always be at the front of the list. 395 DCHECK(i == 0 || changes[i-1].action == changes[i].action); 396 // Children of a deleted node should not be deleted; they may be 397 // reparented by a later change record. Move them to a temporary place. 398 DCHECK(dst) << "Could not find node to be deleted"; 399 const BookmarkNode* parent = dst->GetParent(); 400 if (dst->GetChildCount()) { 401 if (!foster_parent) { 402 foster_parent = model->AddGroup(model->other_node(), 403 model->other_node()->GetChildCount(), 404 string16()); 405 } 406 for (int i = dst->GetChildCount() - 1; i >= 0; --i) { 407 model->Move(dst->GetChild(i), foster_parent, 408 foster_parent->GetChildCount()); 409 } 410 } 411 DCHECK_EQ(dst->GetChildCount(), 0) << "Node being deleted has children"; 412 model_associator_->Disassociate(changes[i].id); 413 model->Remove(parent, parent->IndexOfChild(dst)); 414 dst = NULL; 415 } else { 416 DCHECK_EQ((changes[i].action == 417 sync_api::SyncManager::ChangeRecord::ACTION_ADD), (dst == NULL)) 418 << "ACTION_ADD should be seen if and only if the node is unknown."; 419 420 sync_api::ReadNode src(trans); 421 if (!src.InitByIdLookup(changes[i].id)) { 422 error_handler()->OnUnrecoverableError(FROM_HERE, 423 "ApplyModelChanges was passed a bad ID"); 424 return; 425 } 426 427 CreateOrUpdateBookmarkNode(&src, model); 428 } 429 } 430 // Clean up the temporary node. 431 if (foster_parent) { 432 // There should be no nodes left under the foster parent. 433 DCHECK_EQ(foster_parent->GetChildCount(), 0); 434 model->Remove(foster_parent->GetParent(), 435 foster_parent->GetParent()->IndexOfChild(foster_parent)); 436 foster_parent = NULL; 437 } 438 439 // We are now ready to hear about bookmarks changes again. 440 model->AddObserver(this); 441} 442 443// Create a bookmark node corresponding to |src| if one is not already 444// associated with |src|. 445const BookmarkNode* BookmarkChangeProcessor::CreateOrUpdateBookmarkNode( 446 sync_api::BaseNode* src, 447 BookmarkModel* model) { 448 const BookmarkNode* parent = 449 model_associator_->GetChromeNodeFromSyncId(src->GetParentId()); 450 if (!parent) { 451 DLOG(WARNING) << "Could not find parent of node being added/updated." 452 << " Node title: " << src->GetTitle() 453 << ", parent id = " << src->GetParentId(); 454 455 return NULL; 456 } 457 int index = CalculateBookmarkModelInsertionIndex(parent, src); 458 const BookmarkNode* dst = model_associator_->GetChromeNodeFromSyncId( 459 src->GetId()); 460 if (!dst) { 461 dst = CreateBookmarkNode(src, parent, model, index); 462 model_associator_->Associate(dst, src->GetId()); 463 } else { 464 // URL and is_folder are not expected to change. 465 // TODO(ncarter): Determine if such changes should be legal or not. 466 DCHECK_EQ(src->GetIsFolder(), dst->is_folder()); 467 468 // Handle reparenting and/or repositioning. 469 model->Move(dst, parent, index); 470 471 if (!src->GetIsFolder()) 472 model->SetURL(dst, src->GetURL()); 473 model->SetTitle(dst, WideToUTF16Hack(src->GetTitle())); 474 475 SetBookmarkFavicon(src, dst, model->profile()); 476 } 477 478 return dst; 479} 480 481// static 482// Creates a bookmark node under the given parent node from the given sync 483// node. Returns the newly created node. 484const BookmarkNode* BookmarkChangeProcessor::CreateBookmarkNode( 485 sync_api::BaseNode* sync_node, 486 const BookmarkNode* parent, 487 BookmarkModel* model, 488 int index) { 489 DCHECK(parent); 490 DCHECK(index >= 0 && index <= parent->GetChildCount()); 491 492 const BookmarkNode* node; 493 if (sync_node->GetIsFolder()) { 494 node = model->AddGroup(parent, index, 495 WideToUTF16Hack(sync_node->GetTitle())); 496 } else { 497 node = model->AddURL(parent, index, 498 WideToUTF16Hack(sync_node->GetTitle()), 499 sync_node->GetURL()); 500 SetBookmarkFavicon(sync_node, node, model->profile()); 501 } 502 return node; 503} 504 505// static 506// Sets the favicon of the given bookmark node from the given sync node. 507bool BookmarkChangeProcessor::SetBookmarkFavicon( 508 sync_api::BaseNode* sync_node, 509 const BookmarkNode* bookmark_node, 510 Profile* profile) { 511 std::vector<unsigned char> icon_bytes_vector; 512 sync_node->GetFaviconBytes(&icon_bytes_vector); 513 if (icon_bytes_vector.empty()) 514 return false; 515 516 ApplyBookmarkFavicon(bookmark_node, profile, icon_bytes_vector); 517 518 return true; 519} 520 521// static 522// Applies the given favicon bytes vector to the given bookmark node. 523void BookmarkChangeProcessor::ApplyBookmarkFavicon( 524 const BookmarkNode* bookmark_node, 525 Profile* profile, 526 const std::vector<unsigned char>& icon_bytes_vector) { 527 // Registering a favicon requires that we provide a source URL, but we 528 // don't know where these came from. Currently we just use the 529 // destination URL, which is not correct, but since the favicon URL 530 // is used as a key in the history's thumbnail DB, this gives us a value 531 // which does not collide with others. 532 GURL fake_icon_url = bookmark_node->GetURL(); 533 534 HistoryService* history = 535 profile->GetHistoryService(Profile::EXPLICIT_ACCESS); 536 FaviconService* favicon_service = 537 profile->GetFaviconService(Profile::EXPLICIT_ACCESS); 538 539 history->AddPage(bookmark_node->GetURL(), history::SOURCE_SYNCED); 540 favicon_service->SetFavicon(bookmark_node->GetURL(), 541 fake_icon_url, 542 icon_bytes_vector); 543} 544 545// static 546void BookmarkChangeProcessor::SetSyncNodeFavicon( 547 const BookmarkNode* bookmark_node, 548 BookmarkModel* model, 549 sync_api::WriteNode* sync_node) { 550 std::vector<unsigned char> favicon_bytes; 551 EncodeFavicon(bookmark_node, model, &favicon_bytes); 552 if (!favicon_bytes.empty()) 553 sync_node->SetFaviconBytes(favicon_bytes); 554} 555 556} // namespace browser_sync 557