| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2006-2009 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 | |
| 5 #ifdef CHROME_PERSONALIZATION | |
| 6 | |
| 7 #include "chrome/browser/sync/glue/model_associator.h" | |
| 8 | |
| 9 #include <stack> | |
| 10 | |
| 11 #include "base/message_loop.h" | |
| 12 #include "base/task.h" | |
| 13 #include "chrome/browser/bookmarks/bookmark_model.h" | |
| 14 #include "chrome/browser/sync/engine/syncapi.h" | |
| 15 #include "chrome/browser/sync/profile_sync_service.h" | |
| 16 | |
| 17 namespace browser_sync { | |
| 18 | |
| 19 // The sync protocol identifies top-level entities by means of well-known tags, | |
| 20 // which should not be confused with titles. Each tag corresponds to a | |
| 21 // singleton instance of a particular top-level node in a user's share; the | |
| 22 // tags are consistent across users. The tags allow us to locate the specific | |
| 23 // folders whose contents we care about synchronizing, without having to do a | |
| 24 // lookup by name or path. The tags should not be made user-visible. | |
| 25 // For example, the tag "bookmark_bar" represents the permanent node for | |
| 26 // bookmarks bar in Chrome. The tag "other_bookmarks" represents the permanent | |
| 27 // folder Other Bookmarks in Chrome. | |
| 28 // | |
| 29 // It is the responsibility of something upstream (at time of writing, | |
| 30 // the sync server) to create these tagged nodes when initializing sync | |
| 31 // for the first time for a user. Thus, once the backend finishes | |
| 32 // initializing, the ProfileSyncService can rely on the presence of tagged | |
| 33 // nodes. | |
| 34 // | |
| 35 // TODO(ncarter): Pull these tags from an external protocol specification | |
| 36 // rather than hardcoding them here. | |
| 37 static const wchar_t* kOtherBookmarksTag = L"other_bookmarks"; | |
| 38 static const wchar_t* kBookmarkBarTag = L"bookmark_bar"; | |
| 39 | |
| 40 // Bookmark comparer for map of bookmark nodes. | |
| 41 class BookmarkComparer { | |
| 42 public: | |
| 43 // Compares the two given nodes and returns whether node1 should appear | |
| 44 // before node2 in strict weak ordering. | |
| 45 bool operator()(const BookmarkNode* node1, | |
| 46 const BookmarkNode* node2) const { | |
| 47 DCHECK(node1); | |
| 48 DCHECK(node2); | |
| 49 | |
| 50 // Keep folder nodes before non-folder nodes. | |
| 51 if (node1->is_folder() != node2->is_folder()) | |
| 52 return node1->is_folder(); | |
| 53 | |
| 54 int result = node1->GetTitle().compare(node2->GetTitle()); | |
| 55 if (result != 0) | |
| 56 return result < 0; | |
| 57 | |
| 58 result = node1->GetURL().spec().compare(node2->GetURL().spec()); | |
| 59 if (result != 0) | |
| 60 return result < 0; | |
| 61 | |
| 62 return false; | |
| 63 } | |
| 64 }; | |
| 65 | |
| 66 // Provides the following abstraction: given a parent bookmark node, find best | |
| 67 // matching child node for many sync nodes. | |
| 68 class BookmarkNodeFinder { | |
| 69 public: | |
| 70 // Creats an instance with the given parent bookmark node. | |
| 71 explicit BookmarkNodeFinder(const BookmarkNode* parent_node); | |
| 72 | |
| 73 // Finds best matching node for the given sync node. | |
| 74 // Returns the matching node if one exists; NULL otherwise. If a matching | |
| 75 // node is found, it's removed for further matches. | |
| 76 const BookmarkNode* FindBookmarkNode(const sync_api::BaseNode& sync_node); | |
| 77 | |
| 78 private: | |
| 79 typedef std::set<const BookmarkNode*, BookmarkComparer> BookmarkNodesSet; | |
| 80 | |
| 81 const BookmarkNode* parent_node_; | |
| 82 BookmarkNodesSet child_nodes_; | |
| 83 | |
| 84 DISALLOW_COPY_AND_ASSIGN(BookmarkNodeFinder); | |
| 85 }; | |
| 86 | |
| 87 BookmarkNodeFinder::BookmarkNodeFinder(const BookmarkNode* parent_node) | |
| 88 : parent_node_(parent_node) { | |
| 89 for (int i = 0; i < parent_node_->GetChildCount(); ++i) | |
| 90 child_nodes_.insert(parent_node_->GetChild(i)); | |
| 91 } | |
| 92 | |
| 93 const BookmarkNode* BookmarkNodeFinder::FindBookmarkNode( | |
| 94 const sync_api::BaseNode& sync_node) { | |
| 95 // Create a bookmark node from the given sync node. | |
| 96 BookmarkNode temp_node(GURL(sync_node.GetURL())); | |
| 97 temp_node.SetTitle(UTF16ToWide(sync_node.GetTitle())); | |
| 98 if (sync_node.GetIsFolder()) | |
| 99 temp_node.SetType(BookmarkNode::FOLDER); | |
| 100 else | |
| 101 temp_node.SetType(BookmarkNode::URL); | |
| 102 | |
| 103 const BookmarkNode* result = NULL; | |
| 104 BookmarkNodesSet::iterator iter = child_nodes_.find(&temp_node); | |
| 105 if (iter != child_nodes_.end()) { | |
| 106 result = *iter; | |
| 107 // Remove the matched node so we don't match with it again. | |
| 108 child_nodes_.erase(iter); | |
| 109 } | |
| 110 | |
| 111 return result; | |
| 112 } | |
| 113 | |
| 114 ModelAssociator::ModelAssociator(ProfileSyncService* sync_service) | |
| 115 : sync_service_(sync_service), | |
| 116 task_pending_(false) { | |
| 117 DCHECK(sync_service_); | |
| 118 } | |
| 119 | |
| 120 void ModelAssociator::ClearAll() { | |
| 121 id_map_.clear(); | |
| 122 id_map_inverse_.clear(); | |
| 123 dirty_assocations_sync_ids_.clear(); | |
| 124 } | |
| 125 | |
| 126 int64 ModelAssociator::GetSyncIdFromBookmarkId(int64 node_id) const { | |
| 127 BookmarkIdToSyncIdMap::const_iterator iter = id_map_.find(node_id); | |
| 128 return iter == id_map_.end() ? sync_api::kInvalidId : iter->second; | |
| 129 } | |
| 130 | |
| 131 bool ModelAssociator::GetBookmarkIdFromSyncId(int64 sync_id, | |
| 132 int64* node_id) const { | |
| 133 SyncIdToBookmarkIdMap::const_iterator iter = id_map_inverse_.find(sync_id); | |
| 134 if (iter == id_map_inverse_.end()) | |
| 135 return false; | |
| 136 *node_id = iter->second; | |
| 137 return true; | |
| 138 } | |
| 139 | |
| 140 bool ModelAssociator::InitSyncNodeFromBookmarkId( | |
| 141 int64 node_id, | |
| 142 sync_api::BaseNode* sync_node) { | |
| 143 DCHECK(sync_node); | |
| 144 int64 sync_id = GetSyncIdFromBookmarkId(node_id); | |
| 145 if (sync_id == sync_api::kInvalidId) | |
| 146 return false; | |
| 147 if (!sync_node->InitByIdLookup(sync_id)) | |
| 148 return false; | |
| 149 DCHECK(sync_node->GetId() == sync_id); | |
| 150 return true; | |
| 151 } | |
| 152 | |
| 153 const BookmarkNode* ModelAssociator::GetBookmarkNodeFromSyncId(int64 sync_id) { | |
| 154 int64 node_id; | |
| 155 if (!GetBookmarkIdFromSyncId(sync_id, &node_id)) | |
| 156 return false; | |
| 157 BookmarkModel* model = sync_service_->profile()->GetBookmarkModel(); | |
| 158 return model->GetNodeByID(node_id); | |
| 159 } | |
| 160 | |
| 161 void ModelAssociator::AssociateIds(int64 node_id, int64 sync_id) { | |
| 162 DCHECK_NE(sync_id, sync_api::kInvalidId); | |
| 163 DCHECK(id_map_.find(node_id) == id_map_.end()); | |
| 164 DCHECK(id_map_inverse_.find(sync_id) == id_map_inverse_.end()); | |
| 165 id_map_[node_id] = sync_id; | |
| 166 id_map_inverse_[sync_id] = node_id; | |
| 167 dirty_assocations_sync_ids_.insert(sync_id); | |
| 168 PostPersistAssociationsTask(); | |
| 169 } | |
| 170 | |
| 171 void ModelAssociator::DisassociateIds(int64 sync_id) { | |
| 172 SyncIdToBookmarkIdMap::iterator iter = id_map_inverse_.find(sync_id); | |
| 173 if (iter == id_map_inverse_.end()) | |
| 174 return; | |
| 175 id_map_.erase(iter->second); | |
| 176 id_map_inverse_.erase(iter); | |
| 177 dirty_assocations_sync_ids_.erase(sync_id); | |
| 178 } | |
| 179 | |
| 180 bool ModelAssociator::BookmarkModelHasUserCreatedNodes() const { | |
| 181 BookmarkModel* model = sync_service_->profile()->GetBookmarkModel(); | |
| 182 DCHECK(model->IsLoaded()); | |
| 183 return model->GetBookmarkBarNode()->GetChildCount() > 0 || | |
| 184 model->other_node()->GetChildCount() > 0; | |
| 185 } | |
| 186 | |
| 187 bool ModelAssociator::SyncModelHasUserCreatedNodes() { | |
| 188 int64 bookmark_bar_sync_id; | |
| 189 if (!GetSyncIdForTaggedNode(WideToUTF16(kBookmarkBarTag), | |
| 190 &bookmark_bar_sync_id)) { | |
| 191 NOTREACHED(); | |
| 192 return false; | |
| 193 } | |
| 194 int64 other_bookmarks_sync_id; | |
| 195 if (!GetSyncIdForTaggedNode(WideToUTF16(kOtherBookmarksTag), | |
| 196 &other_bookmarks_sync_id)) { | |
| 197 NOTREACHED(); | |
| 198 return false; | |
| 199 } | |
| 200 | |
| 201 sync_api::ReadTransaction trans( | |
| 202 sync_service_->backend()->GetUserShareHandle()); | |
| 203 | |
| 204 sync_api::ReadNode bookmark_bar_node(&trans); | |
| 205 if (!bookmark_bar_node.InitByIdLookup(bookmark_bar_sync_id)) { | |
| 206 NOTREACHED(); | |
| 207 return false; | |
| 208 } | |
| 209 | |
| 210 sync_api::ReadNode other_bookmarks_node(&trans); | |
| 211 if (!other_bookmarks_node.InitByIdLookup(other_bookmarks_sync_id)) { | |
| 212 NOTREACHED(); | |
| 213 return false; | |
| 214 } | |
| 215 | |
| 216 // Sync model has user created nodes if either one of the permanent nodes | |
| 217 // has children. | |
| 218 return bookmark_bar_node.GetFirstChildId() != sync_api::kInvalidId || | |
| 219 other_bookmarks_node.GetFirstChildId() != sync_api::kInvalidId; | |
| 220 } | |
| 221 | |
| 222 bool ModelAssociator::NodesMatch(const BookmarkNode* bookmark, | |
| 223 const sync_api::BaseNode* sync_node) const { | |
| 224 if (bookmark->GetTitle() != UTF16ToWide(sync_node->GetTitle())) | |
| 225 return false; | |
| 226 if (bookmark->is_folder() != sync_node->GetIsFolder()) | |
| 227 return false; | |
| 228 if (bookmark->is_url()) { | |
| 229 if (bookmark->GetURL() != GURL(sync_node->GetURL())) | |
| 230 return false; | |
| 231 } | |
| 232 // Don't compare favicons here, because they are not really | |
| 233 // user-updated and we don't have versioning information -- a site changing | |
| 234 // its favicon shouldn't result in a bookmark mismatch. | |
| 235 return true; | |
| 236 } | |
| 237 | |
| 238 bool ModelAssociator::AssociateTaggedPermanentNode( | |
| 239 const BookmarkNode* permanent_node, | |
| 240 const string16 &tag) { | |
| 241 // Do nothing if |permanent_node| is already initialized and associated. | |
| 242 int64 sync_id = GetSyncIdFromBookmarkId(permanent_node->id()); | |
| 243 if (sync_id != sync_api::kInvalidId) | |
| 244 return true; | |
| 245 if (!GetSyncIdForTaggedNode(tag, &sync_id)) | |
| 246 return false; | |
| 247 | |
| 248 AssociateIds(permanent_node->id(), sync_id); | |
| 249 return true; | |
| 250 } | |
| 251 | |
| 252 bool ModelAssociator::GetSyncIdForTaggedNode(const string16& tag, | |
| 253 int64* sync_id) { | |
| 254 sync_api::ReadTransaction trans( | |
| 255 sync_service_->backend()->GetUserShareHandle()); | |
| 256 sync_api::ReadNode sync_node(&trans); | |
| 257 if (!sync_node.InitByTagLookup(tag.c_str())) | |
| 258 return false; | |
| 259 *sync_id = sync_node.GetId(); | |
| 260 return true; | |
| 261 } | |
| 262 | |
| 263 bool ModelAssociator::AssociateModels() { | |
| 264 // Try to load model associations from persisted associations first. If that | |
| 265 // succeeds, we don't need to run the complex model matching algorithm. | |
| 266 if (LoadAssociations()) | |
| 267 return true; | |
| 268 | |
| 269 ClearAll(); | |
| 270 | |
| 271 // We couldn't load model assocations from persisted assocations. So build | |
| 272 // them. | |
| 273 return BuildAssocations(); | |
| 274 } | |
| 275 | |
| 276 bool ModelAssociator::BuildAssocations() { | |
| 277 // Algorithm description: | |
| 278 // Match up the roots and recursively do the following: | |
| 279 // * For each sync node for the current sync parent node, find the best | |
| 280 // matching bookmark node under the corresponding bookmark parent node. | |
| 281 // If no matching node is found, create a new bookmark node in the same | |
| 282 // position as the corresponding sync node. | |
| 283 // If a matching node is found, update the properties of it from the | |
| 284 // corresponding sync node. | |
| 285 // * When all children sync nodes are done, add the extra children bookmark | |
| 286 // nodes to the sync parent node. | |
| 287 // | |
| 288 // This algorithm will do a good job of merging when folder names are a good | |
| 289 // indicator of the two folders being the same. It will handle reordering and | |
| 290 // new node addition very well (without creating duplicates). | |
| 291 // This algorithm will not do well if the folder name has changes but the | |
| 292 // children under them are all the same. | |
| 293 | |
| 294 BookmarkModel* model = sync_service_->profile()->GetBookmarkModel(); | |
| 295 DCHECK(model->IsLoaded()); | |
| 296 | |
| 297 // To prime our association, we associate the top-level nodes, Bookmark Bar | |
| 298 // and Other Bookmarks. | |
| 299 if (!AssociateTaggedPermanentNode(model->other_node(), | |
| 300 WideToUTF16(kOtherBookmarksTag))) { | |
| 301 NOTREACHED() << "Server did not create top-level nodes. Possibly we " | |
| 302 << "are running against an out-of-date server?"; | |
| 303 return false; | |
| 304 } | |
| 305 if (!AssociateTaggedPermanentNode(model->GetBookmarkBarNode(), | |
| 306 WideToUTF16(kBookmarkBarTag))) { | |
| 307 NOTREACHED() << "Server did not create top-level nodes. Possibly we " | |
| 308 << "are running against an out-of-date server?"; | |
| 309 return false; | |
| 310 } | |
| 311 int64 bookmark_bar_sync_id = GetSyncIdFromBookmarkId( | |
| 312 model->GetBookmarkBarNode()->id()); | |
| 313 DCHECK(bookmark_bar_sync_id != sync_api::kInvalidId); | |
| 314 int64 other_bookmarks_sync_id = GetSyncIdFromBookmarkId( | |
| 315 model->other_node()->id()); | |
| 316 DCHECK(other_bookmarks_sync_id!= sync_api::kInvalidId); | |
| 317 | |
| 318 std::stack<int64> dfs_stack; | |
| 319 dfs_stack.push(other_bookmarks_sync_id); | |
| 320 dfs_stack.push(bookmark_bar_sync_id); | |
| 321 | |
| 322 sync_api::WriteTransaction trans( | |
| 323 sync_service_->backend()->GetUserShareHandle()); | |
| 324 | |
| 325 while (!dfs_stack.empty()) { | |
| 326 int64 sync_parent_id = dfs_stack.top(); | |
| 327 dfs_stack.pop(); | |
| 328 | |
| 329 sync_api::ReadNode sync_parent(&trans); | |
| 330 if (!sync_parent.InitByIdLookup(sync_parent_id)) { | |
| 331 NOTREACHED(); | |
| 332 return false; | |
| 333 } | |
| 334 // Only folder nodes are pushed on to the stack. | |
| 335 DCHECK(sync_parent.GetIsFolder()); | |
| 336 | |
| 337 const BookmarkNode* parent_node = GetBookmarkNodeFromSyncId(sync_parent_id); | |
| 338 DCHECK(parent_node->is_folder()); | |
| 339 | |
| 340 BookmarkNodeFinder node_finder(parent_node); | |
| 341 | |
| 342 int index = 0; | |
| 343 int64 sync_child_id = sync_parent.GetFirstChildId(); | |
| 344 while (sync_child_id != sync_api::kInvalidId) { | |
| 345 sync_api::WriteNode sync_child_node(&trans); | |
| 346 if (!sync_child_node.InitByIdLookup(sync_child_id)) { | |
| 347 NOTREACHED(); | |
| 348 return false; | |
| 349 } | |
| 350 | |
| 351 const BookmarkNode* child_node = NULL; | |
| 352 child_node = node_finder.FindBookmarkNode(sync_child_node); | |
| 353 if (child_node) { | |
| 354 model->Move(child_node, parent_node, index); | |
| 355 // Set the favicon for bookmark node from sync node or vice versa. | |
| 356 if (!sync_service_->SetBookmarkFavicon(&sync_child_node, child_node)) | |
| 357 sync_service_->SetSyncNodeFavicon(child_node, &sync_child_node); | |
| 358 } else { | |
| 359 // Create a new bookmark node for the sync node. | |
| 360 child_node = sync_service_->CreateBookmarkNode(&sync_child_node, | |
| 361 parent_node, | |
| 362 index); | |
| 363 } | |
| 364 AssociateIds(child_node->id(), sync_child_id); | |
| 365 if (sync_child_node.GetIsFolder()) | |
| 366 dfs_stack.push(sync_child_id); | |
| 367 | |
| 368 sync_child_id = sync_child_node.GetSuccessorId(); | |
| 369 ++index; | |
| 370 } | |
| 371 | |
| 372 // At this point all the children nodes of the parent sync node have | |
| 373 // corresponding children in the parent bookmark node and they are all in | |
| 374 // the right positions: from 0 to index - 1. | |
| 375 // So the children starting from index in the parent bookmark node are the | |
| 376 // ones that are not present in the parent sync node. So create them. | |
| 377 for (int i = index; i < parent_node->GetChildCount(); ++i) { | |
| 378 sync_child_id = sync_service_->CreateSyncNode(parent_node, i, &trans); | |
| 379 if (parent_node->GetChild(i)->is_folder()) | |
| 380 dfs_stack.push(sync_child_id); | |
| 381 } | |
| 382 } | |
| 383 return true; | |
| 384 } | |
| 385 | |
| 386 void ModelAssociator::PostPersistAssociationsTask() { | |
| 387 // No need to post a task if a task is already pending. | |
| 388 if (task_pending_) | |
| 389 return; | |
| 390 task_pending_ = true; | |
| 391 MessageLoop::current()->PostTask( | |
| 392 FROM_HERE, | |
| 393 NewRunnableMethod(this, &ModelAssociator::PersistAssociations)); | |
| 394 } | |
| 395 | |
| 396 void ModelAssociator::PersistAssociations() { | |
| 397 DCHECK(task_pending_); | |
| 398 task_pending_ = false; | |
| 399 | |
| 400 // If there are no dirty assocations we have nothing to do. We handle this | |
| 401 // explicity instead of letting the for loop do it to avoid creating a write | |
| 402 // transaction in this case. | |
| 403 if (dirty_assocations_sync_ids_.empty()) { | |
| 404 DCHECK(id_map_.empty()); | |
| 405 DCHECK(id_map_inverse_.empty()); | |
| 406 return; | |
| 407 } | |
| 408 | |
| 409 sync_api::WriteTransaction trans( | |
| 410 sync_service_->backend()->GetUserShareHandle()); | |
| 411 DirtyAssocationsSyncIds::iterator iter; | |
| 412 for (iter = dirty_assocations_sync_ids_.begin(); | |
| 413 iter != dirty_assocations_sync_ids_.end(); | |
| 414 ++iter) { | |
| 415 int64 sync_id = *iter; | |
| 416 sync_api::WriteNode sync_node(&trans); | |
| 417 if (!sync_node.InitByIdLookup(sync_id)) { | |
| 418 sync_service_->SetUnrecoverableError(); | |
| 419 return; | |
| 420 } | |
| 421 int64 node_id; | |
| 422 if (GetBookmarkIdFromSyncId(sync_id, &node_id)) | |
| 423 sync_node.SetExternalId(node_id); | |
| 424 else | |
| 425 NOTREACHED(); | |
| 426 } | |
| 427 dirty_assocations_sync_ids_.clear(); | |
| 428 } | |
| 429 | |
| 430 bool ModelAssociator::LoadAssociations() { | |
| 431 BookmarkModel* model = sync_service_->profile()->GetBookmarkModel(); | |
| 432 DCHECK(model->IsLoaded()); | |
| 433 // If the bookmarks changed externally, our previous assocations may not be | |
| 434 // valid; so return false. | |
| 435 if (model->file_changed()) | |
| 436 return false; | |
| 437 | |
| 438 // Our persisted assocations should be valid. Try to populate id assocation | |
| 439 // maps using persisted assocations. | |
| 440 | |
| 441 int64 other_bookmarks_id; | |
| 442 if (!GetSyncIdForTaggedNode(WideToUTF16(kOtherBookmarksTag), | |
| 443 &other_bookmarks_id)) { | |
| 444 NOTREACHED(); // We should always be able to find the permanent nodes. | |
| 445 return false; | |
| 446 } | |
| 447 int64 bookmark_bar_id; | |
| 448 if (!GetSyncIdForTaggedNode(WideToUTF16(kBookmarkBarTag), &bookmark_bar_id)) { | |
| 449 NOTREACHED(); // We should always be able to find the permanent nodes. | |
| 450 return false; | |
| 451 } | |
| 452 | |
| 453 std::stack<int64> dfs_stack; | |
| 454 dfs_stack.push(other_bookmarks_id); | |
| 455 dfs_stack.push(bookmark_bar_id); | |
| 456 | |
| 457 sync_api::ReadTransaction trans( | |
| 458 sync_service_->backend()->GetUserShareHandle()); | |
| 459 | |
| 460 while (!dfs_stack.empty()) { | |
| 461 int64 parent_id = dfs_stack.top(); | |
| 462 dfs_stack.pop(); | |
| 463 sync_api::ReadNode sync_parent(&trans); | |
| 464 if (!sync_parent.InitByIdLookup(parent_id)) { | |
| 465 NOTREACHED(); | |
| 466 return false; | |
| 467 } | |
| 468 | |
| 469 int64 external_id = sync_parent.GetExternalId(); | |
| 470 if (external_id == 0) | |
| 471 return false; | |
| 472 | |
| 473 const BookmarkNode* node = model->GetNodeByID(external_id); | |
| 474 if (!node) | |
| 475 return false; | |
| 476 | |
| 477 // Don't try to call NodesMatch on permanent nodes like bookmark bar and | |
| 478 // other bookmarks. They are not expected to match. | |
| 479 if (node != model->GetBookmarkBarNode() && | |
| 480 node != model->other_node() && | |
| 481 !NodesMatch(node, &sync_parent)) | |
| 482 return false; | |
| 483 | |
| 484 AssociateIds(external_id, sync_parent.GetId()); | |
| 485 | |
| 486 // Add all children of the current node to the stack. | |
| 487 int64 child_id = sync_parent.GetFirstChildId(); | |
| 488 while (child_id != sync_api::kInvalidId) { | |
| 489 dfs_stack.push(child_id); | |
| 490 sync_api::ReadNode child_node(&trans); | |
| 491 if (!child_node.InitByIdLookup(child_id)) { | |
| 492 NOTREACHED(); | |
| 493 return false; | |
| 494 } | |
| 495 child_id = child_node.GetSuccessorId(); | |
| 496 } | |
| 497 } | |
| 498 DCHECK(dfs_stack.empty()); | |
| 499 return true; | |
| 500 } | |
| 501 | |
| 502 } // namespace browser_sync | |
| 503 | |
| 504 #endif // CHROME_PERSONALIZATION | |
| OLD | NEW |