1 // Copyright 2012 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.
5 #include "chrome/browser/sync/glue/session_model_associator.h"
11 #include "base/bind.h"
12 #include "base/location.h"
13 #include "base/logging.h"
14 #include "base/strings/utf_string_conversions.h"
15 #include "base/threading/sequenced_worker_pool.h"
16 #include "chrome/browser/chrome_notification_types.h"
17 #include "chrome/browser/favicon/favicon_service_factory.h"
18 #include "chrome/browser/history/history_service.h"
19 #if !defined(OS_ANDROID)
20 #include "chrome/browser/network_time/navigation_time_helper.h"
22 #include "chrome/browser/prefs/pref_service_syncable.h"
23 #include "chrome/browser/profiles/profile.h"
24 #include "chrome/browser/sessions/session_id.h"
25 #include "chrome/browser/sync/glue/device_info.h"
26 #include "chrome/browser/sync/glue/synced_device_tracker.h"
27 #include "chrome/browser/sync/glue/synced_session.h"
28 #include "chrome/browser/sync/glue/synced_tab_delegate.h"
29 #include "chrome/browser/sync/glue/synced_window_delegate.h"
30 #include "chrome/browser/sync/profile_sync_service.h"
31 #include "chrome/common/chrome_switches.h"
32 #include "chrome/common/pref_names.h"
33 #include "chrome/common/url_constants.h"
34 #include "components/sessions/serialized_navigation_entry.h"
35 #include "components/user_prefs/pref_registry_syncable.h"
36 #include "content/public/browser/browser_thread.h"
37 #include "content/public/browser/favicon_status.h"
38 #include "content/public/browser/navigation_entry.h"
39 #include "content/public/browser/notification_details.h"
40 #include "content/public/browser/notification_service.h"
41 #include "content/public/common/url_constants.h"
42 #include "sync/api/sync_error.h"
43 #include "sync/api/time.h"
44 #include "sync/internal_api/public/base/model_type.h"
45 #include "sync/internal_api/public/read_node.h"
46 #include "sync/internal_api/public/read_transaction.h"
47 #include "sync/internal_api/public/write_node.h"
48 #include "sync/internal_api/public/write_transaction.h"
49 #include "sync/protocol/session_specifics.pb.h"
50 #include "sync/syncable/directory.h"
51 #include "sync/syncable/syncable_read_transaction.h"
52 #include "sync/syncable/syncable_write_transaction.h"
54 #include "base/linux_util.h"
59 using content::BrowserThread;
60 using content::NavigationEntry;
61 using prefs::kSyncSessionsGUID;
62 using sessions::SerializedNavigationEntry;
63 using syncer::SESSIONS;
67 std::string SessionTagPrefix() {
68 return std::string("session_sync");
71 // Given a transaction, returns the GUID-based string that should be used for
72 // |current_machine_tag_|.
73 std::string GetMachineTagFromTransaction(
74 syncer::WriteTransaction* trans) {
75 syncer::syncable::Directory* dir = trans->GetWrappedWriteTrans()->directory();
76 std::string machine_tag = SessionTagPrefix();
77 machine_tag.append(dir->cache_guid());
81 // Given a session tag this function returns the client_id(cache_guid).
82 std::string GetClientIdFromSessionTag(const std::string& session_tag) {
83 if (session_tag.find_first_of(SessionTagPrefix()) == std::string::npos) {
84 LOG(ERROR) << "Session tag is malformatted";
88 std::string client_id = session_tag.substr(
89 SessionTagPrefix().length(),
90 session_tag.length());
97 namespace browser_sync {
100 static const char kNoSessionsFolderError[] =
101 "Server did not create the top-level sessions node. We "
102 "might be running against an out-of-date server.";
104 // The maximum number of navigations in each direction we care to sync.
105 static const int kMaxSyncNavigationCount = 6;
107 // Default number of days without activity after which a session is considered
108 // stale and becomes a candidate for garbage collection.
109 static const size_t kDefaultStaleSessionThresholdDays = 14; // 2 weeks.
111 // Maximum number of favicons to sync.
112 // TODO(zea): pull this from the server.
113 static const int kMaxSyncFavicons = 200;
117 SessionModelAssociator::SessionModelAssociator(
118 ProfileSyncService* sync_service,
119 DataTypeErrorHandler* error_handler)
120 : local_tab_pool_(sync_service),
121 local_session_syncid_(syncer::kInvalidId),
122 sync_service_(sync_service),
123 stale_session_threshold_days_(kDefaultStaleSessionThresholdDays),
124 setup_for_test_(false),
125 waiting_for_change_(false),
126 profile_(sync_service->profile()),
127 error_handler_(error_handler),
128 favicon_cache_(profile_,
129 sync_service->current_experiments().favicon_sync_limit),
130 test_weak_factory_(this) {
131 DCHECK(CalledOnValidThread());
132 DCHECK(sync_service_);
136 SessionModelAssociator::SessionModelAssociator(ProfileSyncService* sync_service,
138 : local_tab_pool_(sync_service),
139 local_session_syncid_(syncer::kInvalidId),
140 sync_service_(sync_service),
141 stale_session_threshold_days_(kDefaultStaleSessionThresholdDays),
142 setup_for_test_(setup_for_test),
143 waiting_for_change_(false),
144 profile_(sync_service->profile()),
145 error_handler_(NULL),
146 favicon_cache_(profile_, kMaxSyncFavicons),
147 test_weak_factory_(this) {
148 DCHECK(CalledOnValidThread());
149 DCHECK(sync_service_);
151 DCHECK(setup_for_test);
154 SessionModelAssociator::~SessionModelAssociator() {
155 DCHECK(CalledOnValidThread());
158 bool SessionModelAssociator::SyncModelHasUserCreatedNodes(bool* has_nodes) {
159 DCHECK(CalledOnValidThread());
162 syncer::ReadTransaction trans(FROM_HERE, sync_service_->GetUserShare());
163 syncer::ReadNode root(&trans);
164 if (root.InitByTagLookup(syncer::ModelTypeToRootTag(syncer::SESSIONS)) !=
165 syncer::BaseNode::INIT_OK) {
166 LOG(ERROR) << kNoSessionsFolderError;
169 // The sync model has user created nodes iff the sessions folder has
171 *has_nodes = root.HasChildren();
175 int64 SessionModelAssociator::GetSyncIdFromSessionTag(const std::string& tag) {
176 DCHECK(CalledOnValidThread());
177 syncer::ReadTransaction trans(FROM_HERE, sync_service_->GetUserShare());
178 syncer::ReadNode node(&trans);
179 if (node.InitByClientTagLookup(SESSIONS, tag) != syncer::BaseNode::INIT_OK)
180 return syncer::kInvalidId;
184 bool SessionModelAssociator::AssociateWindows(bool reload_tabs,
185 syncer::SyncError* error) {
186 DCHECK(CalledOnValidThread());
187 std::string local_tag = GetCurrentMachineTag();
188 sync_pb::SessionSpecifics specifics;
189 specifics.set_session_tag(local_tag);
190 sync_pb::SessionHeader* header_s = specifics.mutable_header();
191 SyncedSession* current_session =
192 synced_session_tracker_.GetSession(local_tag);
193 current_session->modified_time = base::Time::Now();
194 header_s->set_client_name(current_session_name_);
195 header_s->set_device_type(DeviceInfo::GetLocalDeviceType());
197 synced_session_tracker_.ResetSessionTracking(local_tag);
198 std::set<SyncedWindowDelegate*> windows =
199 SyncedWindowDelegate::GetSyncedWindowDelegates();
200 for (std::set<SyncedWindowDelegate*>::const_iterator i =
201 windows.begin(); i != windows.end(); ++i) {
202 // Make sure the window has tabs and a viewable window. The viewable window
203 // check is necessary because, for example, when a browser is closed the
204 // destructor is not necessarily run immediately. This means its possible
205 // for us to get a handle to a browser that is about to be removed. If
206 // the tab count is 0 or the window is NULL, the browser is about to be
207 // deleted, so we ignore it.
208 if (ShouldSyncWindow(*i) && (*i)->GetTabCount() && (*i)->HasWindow()) {
209 sync_pb::SessionWindow window_s;
210 SessionID::id_type window_id = (*i)->GetSessionId();
211 DVLOG(1) << "Associating window " << window_id << " with "
212 << (*i)->GetTabCount() << " tabs.";
213 window_s.set_window_id(window_id);
214 // Note: We don't bother to set selected tab index anymore. We still
215 // consume it when receiving foreign sessions, as reading it is free, but
216 // it triggers too many sync cycles with too little value to make setting
218 if ((*i)->IsTypeTabbed()) {
219 window_s.set_browser_type(
220 sync_pb::SessionWindow_BrowserType_TYPE_TABBED);
222 window_s.set_browser_type(
223 sync_pb::SessionWindow_BrowserType_TYPE_POPUP);
226 // Store the order of tabs.
227 bool found_tabs = false;
228 for (int j = 0; j < (*i)->GetTabCount(); ++j) {
229 SessionID::id_type tab_id = (*i)->GetTabIdAt(j);
230 SyncedTabDelegate* synced_tab = (*i)->GetTabAt(j);
232 // GetTabAt can return a null tab; in that case just skip it.
236 if (!synced_tab->HasWebContents()) {
237 // For tabs without WebContents update the |tab_id|, as it could have
238 // changed after a session restore.
239 // Note: We cannot check if a tab is valid if it has no WebContents.
240 // We assume any such tab is valid and leave the contents of
241 // corresponding sync node unchanged.
242 if (synced_tab->GetSyncId() > TabNodePool::kInvalidTabNodeID &&
243 tab_id > TabNodePool::kInvalidTabID) {
244 UpdateTabIdIfNecessary(synced_tab->GetSyncId(), tab_id);
246 window_s.add_tab(tab_id);
252 // It's possible for GetTabAt to return a tab which has no web
253 // contents. We can assume this means the tab already existed but
254 // hasn't changed, so no need to reassociate.
255 if (synced_tab->HasWebContents() &&
256 !AssociateTab(synced_tab, error)) {
257 // Association failed. Either we need to re-associate, or this is an
258 // unrecoverable error.
263 // If the tab is valid, it would have been added to the tracker either
264 // by the above AssociateTab call (at association time), or by the
265 // change processor calling AssociateTab for all modified tabs.
266 // Therefore, we can key whether this window has valid tabs based on
267 // the tab's presence in the tracker.
268 const SessionTab* tab = NULL;
269 if (synced_session_tracker_.LookupSessionTab(local_tag, tab_id, &tab)) {
271 window_s.add_tab(tab_id);
274 // Only add a window if it contains valid tabs.
276 sync_pb::SessionWindow* header_window = header_s->add_window();
277 *header_window = window_s;
279 // Update this window's representation in the synced session tracker.
280 synced_session_tracker_.PutWindowInSession(local_tag, window_id);
281 PopulateSessionWindowFromSpecifics(
285 current_session->windows[window_id],
286 &synced_session_tracker_);
291 local_tab_pool_.DeleteUnassociatedTabNodes();
292 // Free memory for closed windows and tabs.
293 synced_session_tracker_.CleanupSession(local_tag);
295 syncer::WriteTransaction trans(FROM_HERE, sync_service_->GetUserShare());
296 syncer::WriteNode header_node(&trans);
297 if (header_node.InitByIdLookup(local_session_syncid_) !=
298 syncer::BaseNode::INIT_OK) {
300 *error = error_handler_->CreateAndUploadError(
302 "Failed to load local session header node.",
307 header_node.SetSessionSpecifics(specifics);
308 if (waiting_for_change_) QuitLoopForSubtleTesting();
313 bool SessionModelAssociator::ShouldSyncWindow(
314 const SyncedWindowDelegate* window) {
317 return window->IsTypeTabbed() || window->IsTypePopup();
320 bool SessionModelAssociator::AssociateTabs(
321 const std::vector<SyncedTabDelegate*>& tabs,
322 syncer::SyncError* error) {
323 DCHECK(CalledOnValidThread());
324 for (std::vector<SyncedTabDelegate*>::const_iterator i = tabs.begin();
327 if (!AssociateTab(*i, error))
330 if (waiting_for_change_) QuitLoopForSubtleTesting();
334 bool SessionModelAssociator::AssociateTab(SyncedTabDelegate* const tab,
335 syncer::SyncError* error) {
336 DCHECK(CalledOnValidThread());
337 DCHECK(tab->HasWebContents());
338 int tab_node_id(TabNodePool::kInvalidTabNodeID);
339 SessionID::id_type tab_id = tab->GetSessionId();
340 if (tab->IsBeingDestroyed()) {
341 // This tab is closing.
342 TabLinksMap::iterator tab_iter = local_tab_map_.find(tab_id);
343 if (tab_iter == local_tab_map_.end()) {
344 // We aren't tracking this tab (for example, sync setting page).
347 local_tab_pool_.FreeTabNode(tab_iter->second->tab_node_id());
348 local_tab_map_.erase(tab_iter);
352 if (!ShouldSyncTab(*tab))
355 TabLinksMap::iterator local_tab_map_iter = local_tab_map_.find(tab_id);
356 TabLink* tab_link = NULL;
357 if (local_tab_map_iter == local_tab_map_.end()) {
358 tab_node_id = tab->GetSyncId();
359 // if there is an old sync node for the tab, reuse it.
360 if (!local_tab_pool_.IsUnassociatedTabNode(tab_node_id)) {
361 // This is a new tab, get a sync node for it.
362 tab_node_id = local_tab_pool_.GetFreeTabNode();
363 if (tab_node_id == TabNodePool::kInvalidTabNodeID) {
365 *error = error_handler_->CreateAndUploadError(
367 "Received invalid tab node from tab pool.",
372 tab->SetSyncId(tab_node_id);
374 local_tab_pool_.AssociateTabNode(tab_node_id, tab_id);
375 tab_link = new TabLink(tab_node_id, tab);
376 local_tab_map_[tab_id] = make_linked_ptr<TabLink>(tab_link);
378 // This tab is already associated with a sync node, reuse it.
379 // Note: on some platforms the tab object may have changed, so we ensure
380 // the tab link is up to date.
381 tab_link = local_tab_map_iter->second.get();
382 local_tab_map_iter->second->set_tab(tab);
385 DCHECK_NE(tab_link->tab_node_id(), TabNodePool::kInvalidTabNodeID);
387 DVLOG(1) << "Reloading tab " << tab_id << " from window "
388 << tab->GetWindowId();
389 return WriteTabContentsToSyncModel(tab_link, error);
393 GURL SessionModelAssociator::GetCurrentVirtualURL(
394 const SyncedTabDelegate& tab_delegate) {
395 const int current_index = tab_delegate.GetCurrentEntryIndex();
396 const int pending_index = tab_delegate.GetPendingEntryIndex();
397 const NavigationEntry* current_entry =
398 (current_index == pending_index) ?
399 tab_delegate.GetPendingEntry() :
400 tab_delegate.GetEntryAtIndex(current_index);
401 return current_entry->GetVirtualURL();
405 GURL SessionModelAssociator::GetCurrentFaviconURL(
406 const SyncedTabDelegate& tab_delegate) {
407 const int current_index = tab_delegate.GetCurrentEntryIndex();
408 const int pending_index = tab_delegate.GetPendingEntryIndex();
409 const NavigationEntry* current_entry =
410 (current_index == pending_index) ?
411 tab_delegate.GetPendingEntry() :
412 tab_delegate.GetEntryAtIndex(current_index);
413 return (current_entry->GetFavicon().valid ?
414 current_entry->GetFavicon().url :
418 bool SessionModelAssociator::WriteTabContentsToSyncModel(
420 syncer::SyncError* error) {
421 DCHECK(CalledOnValidThread());
422 const SyncedTabDelegate& tab_delegate = *(tab_link->tab());
423 int tab_node_id = tab_link->tab_node_id();
424 GURL old_tab_url = tab_link->url();
425 const GURL new_url = GetCurrentVirtualURL(tab_delegate);
426 DVLOG(1) << "Local tab " << tab_delegate.GetSessionId()
427 << " now has URL " << new_url.spec();
429 SessionTab* session_tab = NULL;
431 syncer::WriteTransaction trans(FROM_HERE, sync_service_->GetUserShare());
432 syncer::WriteNode tab_node(&trans);
433 if (tab_node.InitByClientTagLookup(
435 TabNodePool::TabIdToTag(current_machine_tag_, tab_node_id)) !=
436 syncer::BaseNode::INIT_OK) {
438 *error = error_handler_->CreateAndUploadError(
440 "Failed to look up local tab node",
446 // Load the last stored version of this tab so we can compare changes. If
447 // this is a new tab, session_tab will be a new, blank SessionTab object.
448 sync_pb::SessionSpecifics specifics = tab_node.GetSessionSpecifics();
449 const int s_tab_node_id(specifics.tab_node_id());
450 DCHECK_EQ(tab_node_id, s_tab_node_id);
452 synced_session_tracker_.GetTab(GetCurrentMachineTag(),
453 tab_delegate.GetSessionId(),
454 specifics.tab_node_id());
455 SetSessionTabFromDelegate(tab_delegate, base::Time::Now(), session_tab);
456 sync_pb::SessionTab tab_s = session_tab->ToSyncData();
458 if (new_url == old_tab_url) {
459 // Load the old specifics and copy over the favicon data if needed.
460 // TODO(zea): remove this once favicon sync is enabled as a separate type.
461 tab_s.set_favicon(specifics.tab().favicon());
462 tab_s.set_favicon_source(specifics.tab().favicon_source());
463 tab_s.set_favicon_type(specifics.tab().favicon_type());
465 // Retain the base SessionSpecifics data (tag, tab_node_id, etc.), and just
466 // write the new SessionTabSpecifics.
467 specifics.mutable_tab()->CopyFrom(tab_s);
469 // Write into the actual sync model.
470 tab_node.SetSessionSpecifics(specifics);
473 // Trigger the favicon load if needed. We do this outside the write
474 // transaction to avoid jank.
475 tab_link->set_url(new_url);
476 if (new_url != old_tab_url) {
477 favicon_cache_.OnFaviconVisited(new_url,
478 GetCurrentFaviconURL(tab_delegate));
481 // Update our last modified time.
482 synced_session_tracker_.GetSession(GetCurrentMachineTag())->modified_time =
489 void SessionModelAssociator::SetSessionTabFromDelegate(
490 const SyncedTabDelegate& tab_delegate,
492 SessionTab* session_tab) {
494 session_tab->window_id.set_id(tab_delegate.GetWindowId());
495 session_tab->tab_id.set_id(tab_delegate.GetSessionId());
496 session_tab->tab_visual_index = 0;
497 session_tab->current_navigation_index = tab_delegate.GetCurrentEntryIndex();
498 session_tab->pinned = tab_delegate.IsPinned();
499 session_tab->extension_app_id = tab_delegate.GetExtensionAppId();
500 session_tab->user_agent_override.clear();
501 session_tab->timestamp = mtime;
502 const int current_index = tab_delegate.GetCurrentEntryIndex();
503 const int pending_index = tab_delegate.GetPendingEntryIndex();
504 const int min_index = std::max(0, current_index - kMaxSyncNavigationCount);
505 const int max_index = std::min(current_index + kMaxSyncNavigationCount,
506 tab_delegate.GetEntryCount());
507 bool is_managed = tab_delegate.ProfileIsManaged();
508 session_tab->navigations.clear();
510 #if !defined(OS_ANDROID)
511 // For getting navigation time in network time.
512 NavigationTimeHelper* nav_time_helper =
513 tab_delegate.HasWebContents() ?
514 NavigationTimeHelper::FromWebContents(tab_delegate.GetWebContents()) :
518 for (int i = min_index; i < max_index; ++i) {
519 const NavigationEntry* entry = (i == pending_index) ?
520 tab_delegate.GetPendingEntry() : tab_delegate.GetEntryAtIndex(i);
522 if (!entry->GetVirtualURL().is_valid())
525 scoped_ptr<content::NavigationEntry> network_time_entry(
526 content::NavigationEntry::Create(*entry));
527 #if !defined(OS_ANDROID)
528 if (nav_time_helper) {
529 network_time_entry->SetTimestamp(
530 nav_time_helper->GetNavigationTime(entry));
534 session_tab->navigations.push_back(
535 SerializedNavigationEntry::FromNavigationEntry(i, *network_time_entry));
537 session_tab->navigations.back().set_blocked_state(
538 SerializedNavigationEntry::STATE_ALLOWED);
543 const std::vector<const NavigationEntry*>& blocked_navigations =
544 *tab_delegate.GetBlockedNavigations();
545 int offset = session_tab->navigations.size();
546 for (size_t i = 0; i < blocked_navigations.size(); ++i) {
547 session_tab->navigations.push_back(
548 SerializedNavigationEntry::FromNavigationEntry(
549 i + offset, *blocked_navigations[i]));
550 // Blocked navigations already use network navigation time.
551 session_tab->navigations.back().set_blocked_state(
552 SerializedNavigationEntry::STATE_BLOCKED);
553 // TODO(bauerb): Add categories
556 session_tab->session_storage_persistent_id.clear();
559 void SessionModelAssociator::FaviconsUpdated(
560 const std::set<GURL>& urls) {
561 // TODO(zea): consider a separate container for tabs with outstanding favicon
562 // loads so we don't have to iterate through all tabs comparing urls.
563 for (std::set<GURL>::const_iterator i = urls.begin(); i != urls.end(); ++i) {
564 for (TabLinksMap::iterator tab_iter = local_tab_map_.begin();
565 tab_iter != local_tab_map_.end();
567 if (tab_iter->second->url() == *i)
568 favicon_cache_.OnPageFaviconUpdated(*i);
573 syncer::SyncError SessionModelAssociator::AssociateModels(
574 syncer::SyncMergeResult* local_merge_result,
575 syncer::SyncMergeResult* syncer_merge_result) {
576 DCHECK(CalledOnValidThread());
577 syncer::SyncError error;
579 // Ensure that we disassociated properly, otherwise memory might leak.
580 DCHECK(synced_session_tracker_.Empty());
581 DCHECK_EQ(0U, local_tab_pool_.Capacity());
583 local_session_syncid_ = syncer::kInvalidId;
585 scoped_ptr<DeviceInfo> local_device_info(sync_service_->GetLocalDeviceInfo());
587 #if defined(OS_ANDROID)
588 std::string transaction_tag;
590 // Read any available foreign sessions and load any session data we may have.
591 // If we don't have any local session data in the db, create a header node.
593 syncer::WriteTransaction trans(FROM_HERE, sync_service_->GetUserShare());
595 syncer::ReadNode root(&trans);
596 if (root.InitByTagLookup(syncer::ModelTypeToRootTag(model_type())) !=
597 syncer::BaseNode::INIT_OK) {
598 return error_handler_->CreateAndUploadError(
600 kNoSessionsFolderError,
604 // Make sure we have a machine tag.
605 if (current_machine_tag_.empty())
606 InitializeCurrentMachineTag(&trans);
607 if (local_device_info) {
608 current_session_name_ = local_device_info->client_name();
610 return error_handler_->CreateAndUploadError(
612 "Failed to get device info.",
615 synced_session_tracker_.SetLocalSessionTag(current_machine_tag_);
616 if (!UpdateAssociationsFromSyncModel(root, &trans, &error)) {
617 DCHECK(error.IsSet());
621 if (local_session_syncid_ == syncer::kInvalidId) {
622 // The sync db didn't have a header node for us, we need to create one.
623 syncer::WriteNode write_node(&trans);
624 syncer::WriteNode::InitUniqueByCreationResult result =
625 write_node.InitUniqueByCreation(SESSIONS, root, current_machine_tag_);
626 if (result != syncer::WriteNode::INIT_SUCCESS) {
627 // If we can't look it up, and we can't create it, chances are there's
628 // a pre-existing node that has encryption issues. But, since we can't
629 // load the item, we can't remove it, and error out at this point.
630 return error_handler_->CreateAndUploadError(
632 "Failed to create sessions header sync node.",
636 // Write the initial values to the specifics so that in case of a crash or
637 // error we don't persist a half-written node.
638 write_node.SetTitle(base::UTF8ToWide(current_machine_tag_));
639 sync_pb::SessionSpecifics base_specifics;
640 base_specifics.set_session_tag(current_machine_tag_);
641 sync_pb::SessionHeader* header_s = base_specifics.mutable_header();
642 header_s->set_client_name(current_session_name_);
643 header_s->set_device_type(DeviceInfo::GetLocalDeviceType());
644 write_node.SetSessionSpecifics(base_specifics);
646 local_session_syncid_ = write_node.GetId();
648 #if defined(OS_ANDROID)
649 transaction_tag = GetMachineTagFromTransaction(&trans);
652 #if defined(OS_ANDROID)
653 // We need to delete foreign sessions after giving up our
654 // syncer::WriteTransaction, since DeleteForeignSession(std::string&) uses
655 // its own syncer::WriteTransaction.
656 if (current_machine_tag_.compare(transaction_tag) != 0)
657 DeleteForeignSession(transaction_tag);
660 // Check if anything has changed on the client side.
661 if (!UpdateSyncModelDataFromClient(&error)) {
662 DCHECK(error.IsSet());
666 DVLOG(1) << "Session models associated.";
667 DCHECK(!error.IsSet());
671 syncer::SyncError SessionModelAssociator::DisassociateModels() {
672 DCHECK(CalledOnValidThread());
673 DVLOG(1) << "Disassociating local session " << GetCurrentMachineTag();
674 synced_session_tracker_.Clear();
675 local_tab_map_.clear();
676 local_tab_pool_.Clear();
677 local_session_syncid_ = syncer::kInvalidId;
678 current_machine_tag_ = "";
679 current_session_name_ = "";
681 // There is no local model stored with which to disassociate, just notify
682 // foreign session handlers.
683 content::NotificationService::current()->Notify(
684 chrome::NOTIFICATION_FOREIGN_SESSION_DISABLED,
685 content::Source<Profile>(sync_service_->profile()),
686 content::NotificationService::NoDetails());
687 return syncer::SyncError();
690 void SessionModelAssociator::InitializeCurrentMachineTag(
691 syncer::WriteTransaction* trans) {
692 DCHECK(CalledOnValidThread());
693 DCHECK(current_machine_tag_.empty());
694 std::string persisted_guid;
695 browser_sync::SyncPrefs prefs(profile_->GetPrefs());
696 persisted_guid = prefs.GetSyncSessionsGUID();
697 if (!persisted_guid.empty()) {
698 current_machine_tag_ = persisted_guid;
699 DVLOG(1) << "Restoring persisted session sync guid: "
702 current_machine_tag_ = GetMachineTagFromTransaction(trans);
703 DVLOG(1) << "Creating session sync guid: " << current_machine_tag_;
704 prefs.SetSyncSessionsGUID(current_machine_tag_);
707 local_tab_pool_.SetMachineTag(current_machine_tag_);
710 bool SessionModelAssociator::GetSyncedFaviconForPageURL(
711 const std::string& page_url,
712 scoped_refptr<base::RefCountedMemory>* favicon_png) const {
713 return favicon_cache_.GetSyncedFaviconForPageURL(GURL(page_url), favicon_png);
716 scoped_ptr<browser_sync::DeviceInfo>
717 SessionModelAssociator::GetDeviceInfoForSessionTag(
718 const std::string& session_tag) {
719 std::string client_id = GetClientIdFromSessionTag(session_tag);
720 return sync_service_->GetDeviceInfo(client_id);
723 bool SessionModelAssociator::UpdateAssociationsFromSyncModel(
724 const syncer::ReadNode& root,
725 syncer::WriteTransaction* trans,
726 syncer::SyncError* error) {
727 DCHECK(CalledOnValidThread());
728 DCHECK(local_tab_pool_.Empty());
729 DCHECK_EQ(local_session_syncid_, syncer::kInvalidId);
731 // Iterate through the nodes and associate any foreign sessions.
732 int64 id = root.GetFirstChildId();
733 while (id != syncer::kInvalidId) {
734 syncer::WriteNode sync_node(trans);
735 if (sync_node.InitByIdLookup(id) != syncer::BaseNode::INIT_OK) {
737 *error = error_handler_->CreateAndUploadError(
739 "Failed to load sync node",
744 int64 next_id = sync_node.GetSuccessorId();
746 const sync_pb::SessionSpecifics& specifics =
747 sync_node.GetSessionSpecifics();
748 const base::Time& modification_time = sync_node.GetModificationTime();
749 if (specifics.session_tag().empty() ||
750 (specifics.has_tab() && (!specifics.has_tab_node_id() ||
751 !specifics.tab().has_tab_id()))) {
752 // This is a corrupted node. Just delete it.
753 LOG(WARNING) << "Found invalid session node, deleting.";
754 sync_node.Tombstone();
755 } else if (specifics.session_tag() != GetCurrentMachineTag()) {
756 AssociateForeignSpecifics(specifics, modification_time);
758 // This is previously stored local session information.
759 if (specifics.has_header() &&
760 local_session_syncid_ == syncer::kInvalidId) {
761 // This is our previous header node, reuse it.
762 local_session_syncid_ = id;
763 if (specifics.header().has_client_name()) {
764 current_session_name_ = specifics.header().client_name();
767 if (specifics.has_header() || !specifics.has_tab()) {
768 LOG(WARNING) << "Found invalid session node, deleting.";
769 sync_node.Tombstone();
771 // This is a valid old tab node, add it to the pool so it can be
772 // reused for reassociation.
773 local_tab_pool_.AddTabNode(specifics.tab_node_id());
783 void SessionModelAssociator::AssociateForeignSpecifics(
784 const sync_pb::SessionSpecifics& specifics,
785 const base::Time& modification_time) {
786 DCHECK(CalledOnValidThread());
787 std::string foreign_session_tag = specifics.session_tag();
788 if (foreign_session_tag == GetCurrentMachineTag() && !setup_for_test_)
791 SyncedSession* foreign_session =
792 synced_session_tracker_.GetSession(foreign_session_tag);
793 if (specifics.has_header()) {
794 // Read in the header data for this foreign session.
795 // Header data contains window information and ordered tab id's for each
798 // Load (or create) the SyncedSession object for this client.
799 const sync_pb::SessionHeader& header = specifics.header();
800 PopulateSessionHeaderFromSpecifics(header,
804 // Reset the tab/window tracking for this session (must do this before
805 // we start calling PutWindowInSession and PutTabInWindow so that all
806 // unused tabs/windows get cleared by the CleanupSession(...) call).
807 synced_session_tracker_.ResetSessionTracking(foreign_session_tag);
809 // Process all the windows and their tab information.
810 int num_windows = header.window_size();
811 DVLOG(1) << "Associating " << foreign_session_tag << " with "
812 << num_windows << " windows.";
813 for (int i = 0; i < num_windows; ++i) {
814 const sync_pb::SessionWindow& window_s = header.window(i);
815 SessionID::id_type window_id = window_s.window_id();
816 synced_session_tracker_.PutWindowInSession(foreign_session_tag,
818 PopulateSessionWindowFromSpecifics(foreign_session_tag,
821 foreign_session->windows[window_id],
822 &synced_session_tracker_);
825 // Delete any closed windows and unused tabs as necessary.
826 synced_session_tracker_.CleanupSession(foreign_session_tag);
827 } else if (specifics.has_tab()) {
828 const sync_pb::SessionTab& tab_s = specifics.tab();
829 SessionID::id_type tab_id = tab_s.tab_id();
831 synced_session_tracker_.GetTab(foreign_session_tag,
833 specifics.tab_node_id());
835 // Update SessionTab based on protobuf.
836 tab->SetFromSyncData(tab_s, modification_time);
838 // If a favicon or favicon urls are present, load them into the in-memory
840 LoadForeignTabFavicon(tab_s);
842 // Update the last modified time.
843 if (foreign_session->modified_time < modification_time)
844 foreign_session->modified_time = modification_time;
846 LOG(WARNING) << "Ignoring foreign session node with missing header/tab "
847 << "fields and tag " << foreign_session_tag << ".";
851 bool SessionModelAssociator::DisassociateForeignSession(
852 const std::string& foreign_session_tag) {
853 DCHECK(CalledOnValidThread());
854 if (foreign_session_tag == GetCurrentMachineTag()) {
855 DVLOG(1) << "Local session deleted! Doing nothing until a navigation is "
859 DVLOG(1) << "Disassociating session " << foreign_session_tag;
860 return synced_session_tracker_.DeleteSession(foreign_session_tag);
864 void SessionModelAssociator::PopulateSessionHeaderFromSpecifics(
865 const sync_pb::SessionHeader& header_specifics,
867 SyncedSession* session_header) {
868 if (header_specifics.has_client_name()) {
869 session_header->session_name = header_specifics.client_name();
871 if (header_specifics.has_device_type()) {
872 switch (header_specifics.device_type()) {
873 case sync_pb::SyncEnums_DeviceType_TYPE_WIN:
874 session_header->device_type = SyncedSession::TYPE_WIN;
876 case sync_pb::SyncEnums_DeviceType_TYPE_MAC:
877 session_header->device_type = SyncedSession::TYPE_MACOSX;
879 case sync_pb::SyncEnums_DeviceType_TYPE_LINUX:
880 session_header->device_type = SyncedSession::TYPE_LINUX;
882 case sync_pb::SyncEnums_DeviceType_TYPE_CROS:
883 session_header->device_type = SyncedSession::TYPE_CHROMEOS;
885 case sync_pb::SyncEnums_DeviceType_TYPE_PHONE:
886 session_header->device_type = SyncedSession::TYPE_PHONE;
888 case sync_pb::SyncEnums_DeviceType_TYPE_TABLET:
889 session_header->device_type = SyncedSession::TYPE_TABLET;
891 case sync_pb::SyncEnums_DeviceType_TYPE_OTHER:
892 // Intentionally fall-through
894 session_header->device_type = SyncedSession::TYPE_OTHER;
898 session_header->modified_time = mtime;
902 void SessionModelAssociator::PopulateSessionWindowFromSpecifics(
903 const std::string& session_tag,
904 const sync_pb::SessionWindow& specifics,
906 SessionWindow* session_window,
907 SyncedSessionTracker* tracker) {
908 if (specifics.has_window_id())
909 session_window->window_id.set_id(specifics.window_id());
910 if (specifics.has_selected_tab_index())
911 session_window->selected_tab_index = specifics.selected_tab_index();
912 if (specifics.has_browser_type()) {
913 if (specifics.browser_type() ==
914 sync_pb::SessionWindow_BrowserType_TYPE_TABBED) {
915 session_window->type = 1;
917 session_window->type = 2;
920 session_window->timestamp = mtime;
921 session_window->tabs.resize(specifics.tab_size(), NULL);
922 for (int i = 0; i < specifics.tab_size(); i++) {
923 SessionID::id_type tab_id = specifics.tab(i);
924 tracker->PutTabInWindow(session_tag,
925 session_window->window_id.id(),
931 void SessionModelAssociator::LoadForeignTabFavicon(
932 const sync_pb::SessionTab& tab) {
933 // First go through and iterate over all the navigations, checking if any
934 // have valid favicon urls.
935 for (int i = 0; i < tab.navigation_size(); ++i) {
936 if (!tab.navigation(i).favicon_url().empty()) {
937 const std::string& page_url = tab.navigation(i).virtual_url();
938 const std::string& favicon_url = tab.navigation(i).favicon_url();
939 favicon_cache_.OnReceivedSyncFavicon(GURL(page_url),
942 syncer::TimeToProtoTime(
947 // Then go through and check for any legacy favicon data.
948 if (!tab.has_favicon() || tab.favicon().empty())
950 if (!tab.has_favicon_type() ||
951 tab.favicon_type() != sync_pb::SessionTab::TYPE_WEB_FAVICON) {
952 DVLOG(1) << "Ignoring non-web favicon.";
955 if (tab.navigation_size() == 0)
957 int selected_index = tab.current_navigation_index();
958 selected_index = std::max(
960 std::min(selected_index,
961 static_cast<int>(tab.navigation_size() - 1)));
962 GURL navigation_url(tab.navigation(selected_index).virtual_url());
963 if (!navigation_url.is_valid())
965 GURL favicon_source(tab.favicon_source());
966 if (!favicon_source.is_valid())
969 const std::string& favicon = tab.favicon();
970 DVLOG(1) << "Storing synced favicon for url " << navigation_url.spec()
971 << " with size " << favicon.size() << " bytes.";
972 favicon_cache_.OnReceivedSyncFavicon(navigation_url,
975 syncer::TimeToProtoTime(
979 bool SessionModelAssociator::UpdateSyncModelDataFromClient(
980 syncer::SyncError* error) {
981 DCHECK(CalledOnValidThread());
983 // Associate all open windows and their tabs.
984 return AssociateWindows(true, error);
987 void SessionModelAssociator::AttemptSessionsDataRefresh() const {
988 DVLOG(1) << "Triggering sync refresh for sessions datatype.";
989 const syncer::ModelTypeSet types(syncer::SESSIONS);
990 content::NotificationService::current()->Notify(
991 chrome::NOTIFICATION_SYNC_REFRESH_LOCAL,
992 content::Source<Profile>(profile_),
993 content::Details<const syncer::ModelTypeSet>(&types));
996 bool SessionModelAssociator::GetLocalSession(
997 const SyncedSession* * local_session) {
998 DCHECK(CalledOnValidThread());
999 if (current_machine_tag_.empty())
1001 *local_session = synced_session_tracker_.GetSession(GetCurrentMachineTag());
1005 bool SessionModelAssociator::GetAllForeignSessions(
1006 std::vector<const SyncedSession*>* sessions) {
1007 DCHECK(CalledOnValidThread());
1008 return synced_session_tracker_.LookupAllForeignSessions(sessions);
1011 bool SessionModelAssociator::GetForeignSession(
1012 const std::string& tag,
1013 std::vector<const SessionWindow*>* windows) {
1014 DCHECK(CalledOnValidThread());
1015 return synced_session_tracker_.LookupSessionWindows(tag, windows);
1018 bool SessionModelAssociator::GetForeignTab(
1019 const std::string& tag,
1020 const SessionID::id_type tab_id,
1021 const SessionTab** tab) {
1022 DCHECK(CalledOnValidThread());
1023 const SessionTab* synced_tab = NULL;
1024 bool success = synced_session_tracker_.LookupSessionTab(tag,
1032 void SessionModelAssociator::DeleteStaleSessions() {
1033 DCHECK(CalledOnValidThread());
1034 std::vector<const SyncedSession*> sessions;
1035 if (!GetAllForeignSessions(&sessions))
1036 return; // No foreign sessions.
1038 // Iterate through all the sessions and delete any with age older than
1039 // |stale_session_threshold_days_|.
1040 for (std::vector<const SyncedSession*>::const_iterator iter =
1041 sessions.begin(); iter != sessions.end(); ++iter) {
1042 const SyncedSession* session = *iter;
1043 int session_age_in_days =
1044 (base::Time::Now() - session->modified_time).InDays();
1045 std::string session_tag = session->session_tag;
1046 if (session_age_in_days > 0 && // If false, local clock is not trustworty.
1047 static_cast<size_t>(session_age_in_days) >
1048 stale_session_threshold_days_) {
1049 DVLOG(1) << "Found stale session " << session_tag
1050 << " with age " << session_age_in_days << ", deleting.";
1051 DeleteForeignSession(session_tag);
1056 void SessionModelAssociator::SetStaleSessionThreshold(
1057 size_t stale_session_threshold_days) {
1058 DCHECK(CalledOnValidThread());
1059 if (stale_session_threshold_days_ == 0) {
1060 NOTREACHED() << "Attempted to set invalid stale session threshold.";
1063 stale_session_threshold_days_ = stale_session_threshold_days;
1064 // TODO(zea): maybe make this preference-based? Might be nice to let users be
1065 // able to modify this once and forget about it. At the moment, if we want a
1066 // different threshold we will need to call this everytime we create a new
1067 // model associator and before we AssociateModels (probably from DTC).
1070 void SessionModelAssociator::DeleteForeignSession(const std::string& tag) {
1071 DCHECK(CalledOnValidThread());
1072 if (tag == GetCurrentMachineTag()) {
1073 LOG(ERROR) << "Attempting to delete local session. This is not currently "
1078 if (!DisassociateForeignSession(tag)) {
1079 // We don't have any data for this session, our work here is done!
1083 syncer::WriteTransaction trans(FROM_HERE, sync_service_->GetUserShare());
1084 syncer::ReadNode root(&trans);
1085 if (root.InitByTagLookup(syncer::ModelTypeToRootTag(syncer::SESSIONS)) !=
1086 syncer::BaseNode::INIT_OK) {
1087 LOG(ERROR) << kNoSessionsFolderError;
1090 int64 id = root.GetFirstChildId();
1091 while (id != syncer::kInvalidId) {
1092 syncer::WriteNode sync_node(&trans);
1093 if (sync_node.InitByIdLookup(id) != syncer::BaseNode::INIT_OK) {
1094 LOG(ERROR) << "Failed to fetch sync node for id " << id;
1097 id = sync_node.GetSuccessorId();
1098 const sync_pb::SessionSpecifics& specifics =
1099 sync_node.GetSessionSpecifics();
1100 if (specifics.session_tag() == tag)
1101 sync_node.Tombstone();
1104 content::NotificationService::current()->Notify(
1105 chrome::NOTIFICATION_FOREIGN_SESSION_UPDATED,
1106 content::Source<Profile>(sync_service_->profile()),
1107 content::NotificationService::NoDetails());
1110 bool SessionModelAssociator::IsValidTab(const SyncedTabDelegate& tab) const {
1111 if ((!sync_service_ || tab.profile() != sync_service_->profile()) &&
1115 const SyncedWindowDelegate* window =
1116 SyncedWindowDelegate::FindSyncedWindowDelegateWithId(
1118 if (!window && !setup_for_test_)
1123 bool SessionModelAssociator::TabHasValidEntry(
1124 const SyncedTabDelegate& tab) const {
1125 if (tab.ProfileIsManaged() && tab.GetBlockedNavigations()->size() > 0)
1128 int entry_count = tab.GetEntryCount();
1129 if (entry_count == 0)
1130 return false; // This deliberately ignores a new pending entry.
1132 int pending_index = tab.GetPendingEntryIndex();
1133 bool found_valid_url = false;
1134 for (int i = 0; i < entry_count; ++i) {
1135 const content::NavigationEntry* entry = (i == pending_index) ?
1136 tab.GetPendingEntry() : tab.GetEntryAtIndex(i);
1139 const GURL& virtual_url = entry->GetVirtualURL();
1140 if (virtual_url.is_valid() &&
1141 !virtual_url.SchemeIs(content::kChromeUIScheme) &&
1142 !virtual_url.SchemeIs(chrome::kChromeNativeScheme) &&
1143 !virtual_url.SchemeIsFile()) {
1144 found_valid_url = true;
1147 return found_valid_url;
1150 // If this functionality changes, browser_sync::ShouldSyncSessionTab should be
1151 // modified to match.
1152 bool SessionModelAssociator::ShouldSyncTab(const SyncedTabDelegate& tab) const {
1153 DCHECK(CalledOnValidThread());
1154 if (!IsValidTab(tab))
1156 return TabHasValidEntry(tab);
1159 void SessionModelAssociator::QuitLoopForSubtleTesting() {
1160 if (waiting_for_change_) {
1161 DVLOG(1) << "Quitting base::MessageLoop for test.";
1162 waiting_for_change_ = false;
1163 test_weak_factory_.InvalidateWeakPtrs();
1164 base::MessageLoop::current()->Quit();
1168 FaviconCache* SessionModelAssociator::GetFaviconCache() {
1169 return &favicon_cache_;
1172 void SessionModelAssociator::BlockUntilLocalChangeForTest(
1173 base::TimeDelta timeout) {
1174 if (test_weak_factory_.HasWeakPtrs())
1176 waiting_for_change_ = true;
1177 base::MessageLoop::current()->PostDelayedTask(
1179 base::Bind(&SessionModelAssociator::QuitLoopForSubtleTesting,
1180 test_weak_factory_.GetWeakPtr()),
1184 bool SessionModelAssociator::CryptoReadyIfNecessary() {
1185 // We only access the cryptographer while holding a transaction.
1186 syncer::ReadTransaction trans(FROM_HERE, sync_service_->GetUserShare());
1187 const syncer::ModelTypeSet encrypted_types = trans.GetEncryptedTypes();
1188 return !encrypted_types.Has(SESSIONS) ||
1189 sync_service_->IsCryptographerReady(&trans);
1192 void SessionModelAssociator::UpdateTabIdIfNecessary(
1194 SessionID::id_type new_tab_id) {
1195 DCHECK_NE(tab_node_id, TabNodePool::kInvalidTabNodeID);
1196 SessionID::id_type old_tab_id =
1197 local_tab_pool_.GetTabIdFromTabNodeId(tab_node_id);
1198 if (old_tab_id != new_tab_id) {
1199 // Rewrite tab id if required.
1200 syncer::WriteTransaction trans(FROM_HERE, sync_service_->GetUserShare());
1201 syncer::WriteNode tab_node(&trans);
1202 if (tab_node.InitByClientTagLookup(syncer::SESSIONS,
1203 TabNodePool::TabIdToTag(current_machine_tag_, tab_node_id)) ==
1204 syncer::BaseNode::INIT_OK) {
1205 sync_pb::SessionSpecifics session_specifics =
1206 tab_node.GetSessionSpecifics();
1207 DCHECK(session_specifics.has_tab());
1208 if (session_specifics.has_tab()) {
1209 sync_pb::SessionTab* tab_s = session_specifics.mutable_tab();
1210 tab_s->set_tab_id(new_tab_id);
1211 tab_node.SetSessionSpecifics(session_specifics);
1212 // Update tab node pool with the new association.
1213 local_tab_pool_.ReassociateTabNode(tab_node_id, new_tab_id);
1219 } // namespace browser_sync