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