|  | // Copyright 2018 The Chromium Authors. All rights reserved. | 
|  | // Use of this source code is governed by a BSD-style license that can be | 
|  | // found in the LICENSE file. | 
|  |  | 
|  | #include "components/sync_sessions/local_session_event_handler_impl.h" | 
|  |  | 
|  | #include <algorithm> | 
|  | #include <map> | 
|  | #include <utility> | 
|  | #include <vector> | 
|  |  | 
|  | #include "base/metrics/histogram_macros.h" | 
|  | #include "base/strings/stringprintf.h" | 
|  | #include "components/sync/model/sync_change.h" | 
|  | #include "components/sync/protocol/sync.pb.h" | 
|  | #include "components/sync_sessions/sync_sessions_client.h" | 
|  | #include "components/sync_sessions/synced_session_tracker.h" | 
|  | #include "components/sync_sessions/synced_tab_delegate.h" | 
|  | #include "components/sync_sessions/synced_window_delegate.h" | 
|  | #include "components/sync_sessions/synced_window_delegates_getter.h" | 
|  |  | 
|  | namespace sync_sessions { | 
|  | namespace { | 
|  |  | 
|  | using sessions::SerializedNavigationEntry; | 
|  |  | 
|  | // The maximum number of navigations in each direction we care to sync. | 
|  | const int kMaxSyncNavigationCount = 6; | 
|  |  | 
|  | bool IsSessionRestoreInProgress(SyncSessionsClient* sessions_client) { | 
|  | DCHECK(sessions_client); | 
|  | SyncedWindowDelegatesGetter* synced_window_getter = | 
|  | sessions_client->GetSyncedWindowDelegatesGetter(); | 
|  | SyncedWindowDelegatesGetter::SyncedWindowDelegateMap windows = | 
|  | synced_window_getter->GetSyncedWindowDelegates(); | 
|  | for (const auto& window_iter_pair : windows) { | 
|  | if (window_iter_pair.second->IsSessionRestoreInProgress()) { | 
|  | return true; | 
|  | } | 
|  | } | 
|  | return false; | 
|  | } | 
|  |  | 
|  | bool IsWindowSyncable(const SyncedWindowDelegate& window_delegate) { | 
|  | return window_delegate.ShouldSync() && window_delegate.GetTabCount() && | 
|  | window_delegate.HasWindow(); | 
|  | } | 
|  |  | 
|  | // On Android, it's possible to not have any tabbed windows when only custom | 
|  | // tabs are currently open. This means that there is tab data that will be | 
|  | // restored later, but we cannot access it. | 
|  | bool ScanForTabbedWindow(SyncedWindowDelegatesGetter* delegates_getter) { | 
|  | for (const auto& window_iter_pair : | 
|  | delegates_getter->GetSyncedWindowDelegates()) { | 
|  | const SyncedWindowDelegate* window_delegate = window_iter_pair.second; | 
|  | if (window_delegate->IsTypeTabbed() && IsWindowSyncable(*window_delegate)) { | 
|  | return true; | 
|  | } | 
|  | } | 
|  | return false; | 
|  | } | 
|  |  | 
|  | }  // namespace | 
|  |  | 
|  | LocalSessionEventHandlerImpl::WriteBatch::WriteBatch() = default; | 
|  |  | 
|  | LocalSessionEventHandlerImpl::WriteBatch::~WriteBatch() = default; | 
|  |  | 
|  | LocalSessionEventHandlerImpl::Delegate::~Delegate() = default; | 
|  |  | 
|  | LocalSessionEventHandlerImpl::LocalSessionEventHandlerImpl( | 
|  | Delegate* delegate, | 
|  | SyncSessionsClient* sessions_client, | 
|  | SyncedSessionTracker* session_tracker) | 
|  | : delegate_(delegate), | 
|  | sessions_client_(sessions_client), | 
|  | session_tracker_(session_tracker) { | 
|  | DCHECK(delegate); | 
|  | DCHECK(sessions_client); | 
|  | DCHECK(session_tracker); | 
|  |  | 
|  | current_session_tag_ = session_tracker_->GetLocalSessionTag(); | 
|  | DCHECK(!current_session_tag_.empty()); | 
|  |  | 
|  | if (!IsSessionRestoreInProgress(sessions_client)) { | 
|  | OnSessionRestoreComplete(); | 
|  | } | 
|  | } | 
|  |  | 
|  | LocalSessionEventHandlerImpl::~LocalSessionEventHandlerImpl() {} | 
|  |  | 
|  | void LocalSessionEventHandlerImpl::OnSessionRestoreComplete() { | 
|  | std::unique_ptr<WriteBatch> batch = delegate_->CreateLocalSessionWriteBatch(); | 
|  | AssociateWindows(RELOAD_TABS, batch.get()); | 
|  | batch->Commit(); | 
|  | } | 
|  |  | 
|  | sync_pb::SessionTab | 
|  | LocalSessionEventHandlerImpl::GetTabSpecificsFromDelegateForTest( | 
|  | const SyncedTabDelegate& tab_delegate) const { | 
|  | return GetTabSpecificsFromDelegate(tab_delegate); | 
|  | } | 
|  |  | 
|  | void LocalSessionEventHandlerImpl::AssociateWindows(ReloadTabsOption option, | 
|  | WriteBatch* batch) { | 
|  | DCHECK(!IsSessionRestoreInProgress(sessions_client_)); | 
|  |  | 
|  | const bool has_tabbed_window = | 
|  | ScanForTabbedWindow(sessions_client_->GetSyncedWindowDelegatesGetter()); | 
|  |  | 
|  | // Note that |current_session| is a pointer owned by |session_tracker_|. | 
|  | // |session_tracker_| will continue to update |current_session| under | 
|  | // the hood so care must be taken accessing it. In particular, invoking | 
|  | // ResetSessionTracking(..) will invalidate all the tab data within | 
|  | // the session, hence why copies of the SyncedSession must be made ahead of | 
|  | // time. | 
|  | SyncedSession* current_session = | 
|  | session_tracker_->GetSession(current_session_tag_); | 
|  |  | 
|  | SyncedWindowDelegatesGetter::SyncedWindowDelegateMap windows = | 
|  | sessions_client_->GetSyncedWindowDelegatesGetter() | 
|  | ->GetSyncedWindowDelegates(); | 
|  |  | 
|  | // Without native data, we need be careful not to obliterate any old | 
|  | // information, while at the same time handling updated tab ids. See | 
|  | // https://crbug.com/639009 for more info. | 
|  | if (has_tabbed_window) { | 
|  | // Just reset the session tracking. No need to worry about the previous | 
|  | // session; the current tabbed windows are now the source of truth. | 
|  | session_tracker_->ResetSessionTracking(current_session_tag_); | 
|  | current_session->modified_time = base::Time::Now(); | 
|  | } else { | 
|  | DVLOG(1) << "Found no tabbed windows. Reloading " | 
|  | << current_session->windows.size() | 
|  | << " windows from previous session."; | 
|  | } | 
|  |  | 
|  | for (auto& window_iter_pair : windows) { | 
|  | const SyncedWindowDelegate* window_delegate = window_iter_pair.second; | 
|  | if (option == RELOAD_TABS) { | 
|  | UMA_HISTOGRAM_COUNTS_1M("Sync.SessionTabs", | 
|  | window_delegate->GetTabCount()); | 
|  | } | 
|  |  | 
|  | // Make sure the window has tabs and a viewable window. The viewable | 
|  | // window check is necessary because, for example, when a browser is | 
|  | // closed the destructor is not necessarily run immediately. This means | 
|  | // its possible for us to get a handle to a browser that is about to be | 
|  | // removed. If the tab count is 0 or the window is null, the browser is | 
|  | // about to be deleted, so we ignore it. | 
|  | if (!IsWindowSyncable(*window_delegate)) { | 
|  | continue; | 
|  | } | 
|  |  | 
|  | SessionID window_id = window_delegate->GetSessionId(); | 
|  | DVLOG(1) << "Associating window " << window_id.id() << " with " | 
|  | << window_delegate->GetTabCount() << " tabs."; | 
|  |  | 
|  | bool found_tabs = false; | 
|  | for (int j = 0; j < window_delegate->GetTabCount(); ++j) { | 
|  | SessionID tab_id = window_delegate->GetTabIdAt(j); | 
|  | SyncedTabDelegate* synced_tab = window_delegate->GetTabAt(j); | 
|  |  | 
|  | // IsWindowSyncable(), via ShouldSync(), guarantees that tabs are not | 
|  | // null. | 
|  | DCHECK(synced_tab); | 
|  |  | 
|  | // If for some reason the tab ID is invalid, skip it. | 
|  | if (!tab_id.is_valid()) { | 
|  | continue; | 
|  | } | 
|  |  | 
|  | // Placeholder tabs are those without WebContents, either because they | 
|  | // were never loaded into memory or they were evicted from memory | 
|  | // (typically only on Android devices). They only have a window ID and a | 
|  | // tab ID,  and we can use the latter to properly reassociate the tab with | 
|  | // the entity that was backing it. The window ID could have changed, but | 
|  | // noone really cares, because the window/tab hierarchy is constructed | 
|  | // from the header entity (which has up-to-date IDs). Hence, in order to | 
|  | // avoid unnecessary traffic, we avoid updating the entity. | 
|  | if (!synced_tab->IsPlaceholderTab() && RELOAD_TABS == option) { | 
|  | AssociateTab(synced_tab, batch); | 
|  | } | 
|  |  | 
|  | // If the tab was syncable, it would have been added to the tracker either | 
|  | // by the above AssociateTab call or by the OnLocalTabModified method | 
|  | // invoking AssociateTab directly. Therefore, we can key whether this | 
|  | // window has valid tabs based on the tab's presence in the tracker. | 
|  | const sessions::SessionTab* tab = | 
|  | session_tracker_->LookupSessionTab(current_session_tag_, tab_id); | 
|  | if (tab) { | 
|  | found_tabs = true; | 
|  |  | 
|  | // Update this window's representation in the synced session tracker. | 
|  | // This is a no-op if called multiple times. | 
|  | session_tracker_->PutWindowInSession(current_session_tag_, window_id); | 
|  |  | 
|  | // Put the tab in the window (must happen after the window is added | 
|  | // to the session). | 
|  | session_tracker_->PutTabInWindow(current_session_tag_, window_id, | 
|  | tab_id); | 
|  | } | 
|  | } | 
|  | if (found_tabs) { | 
|  | SyncedSessionWindow* synced_session_window = | 
|  | current_session->windows[window_id].get(); | 
|  | if (window_delegate->IsTypeTabbed()) { | 
|  | synced_session_window->window_type = | 
|  | sync_pb::SessionWindow_BrowserType_TYPE_TABBED; | 
|  | } else if (window_delegate->IsTypePopup()) { | 
|  | synced_session_window->window_type = | 
|  | sync_pb::SessionWindow_BrowserType_TYPE_POPUP; | 
|  | } else { | 
|  | // This is a custom tab within an app. These will not be restored on | 
|  | // startup if not present. | 
|  | synced_session_window->window_type = | 
|  | sync_pb::SessionWindow_BrowserType_TYPE_CUSTOM_TAB; | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | std::set<int> deleted_tab_node_ids; | 
|  | session_tracker_->CleanupLocalTabs(&deleted_tab_node_ids); | 
|  | for (int tab_node_id : deleted_tab_node_ids) { | 
|  | batch->Delete(tab_node_id); | 
|  | } | 
|  |  | 
|  | // Always update the header.  Sync takes care of dropping this update | 
|  | // if the entity specifics are identical (i.e windows, client name did | 
|  | // not change). | 
|  | auto specifics = std::make_unique<sync_pb::SessionSpecifics>(); | 
|  | specifics->set_session_tag(current_session_tag_); | 
|  | current_session->ToSessionHeaderProto().Swap(specifics->mutable_header()); | 
|  | batch->Put(std::move(specifics)); | 
|  | } | 
|  |  | 
|  | void LocalSessionEventHandlerImpl::AssociateTab( | 
|  | SyncedTabDelegate* const tab_delegate, | 
|  | WriteBatch* batch) { | 
|  | DCHECK(!tab_delegate->IsPlaceholderTab()); | 
|  |  | 
|  | if (tab_delegate->IsBeingDestroyed()) { | 
|  | task_tracker_.CleanTabTasks(tab_delegate->GetSessionId()); | 
|  | // Do nothing else. By not proactively adding the tab to the session, it | 
|  | // will be removed if necessary during subsequent cleanup. | 
|  | return; | 
|  | } | 
|  |  | 
|  | // Ensure the task tracker has up to date task ids for this tab. | 
|  | UpdateTaskTracker(tab_delegate); | 
|  |  | 
|  | if (!tab_delegate->ShouldSync(sessions_client_)) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | SessionID tab_id = tab_delegate->GetSessionId(); | 
|  | int tab_node_id = | 
|  | session_tracker_->LookupTabNodeFromTabId(current_session_tag_, tab_id); | 
|  |  | 
|  | if (tab_node_id == TabNodePool::kInvalidTabNodeID) { | 
|  | // Allocate a new (or reused) sync node for this tab. | 
|  | tab_node_id = session_tracker_->AssociateLocalTabWithFreeTabNode(tab_id); | 
|  | DCHECK_NE(TabNodePool::kInvalidTabNodeID, tab_node_id) | 
|  | << "https://crbug.com/639009"; | 
|  | } | 
|  |  | 
|  | DVLOG(1) << "Syncing tab " << tab_id << " from window " | 
|  | << tab_delegate->GetWindowId() << " using tab node " << tab_node_id; | 
|  |  | 
|  | // Get the previously synced url. | 
|  | sessions::SessionTab* session_tab = | 
|  | session_tracker_->GetTab(current_session_tag_, tab_id); | 
|  | int old_index = session_tab->normalized_navigation_index(); | 
|  | GURL old_url; | 
|  | if (session_tab->navigations.size() > static_cast<size_t>(old_index)) | 
|  | old_url = session_tab->navigations[old_index].virtual_url(); | 
|  |  | 
|  | // Produce the specifics. | 
|  | auto specifics = std::make_unique<sync_pb::SessionSpecifics>(); | 
|  | specifics->set_session_tag(current_session_tag_); | 
|  | specifics->set_tab_node_id(tab_node_id); | 
|  | GetTabSpecificsFromDelegate(*tab_delegate).Swap(specifics->mutable_tab()); | 
|  | WriteTasksIntoSpecifics(specifics->mutable_tab()); | 
|  |  | 
|  | // Update the tracker's session representation. Timestamp will be overwriten, | 
|  | // so we set a null time first to prevent the update from being ignored, if | 
|  | // the local clock is skewed. | 
|  | session_tab->timestamp = base::Time(); | 
|  | UpdateTrackerWithSpecifics(*specifics, base::Time::Now(), session_tracker_); | 
|  | DCHECK(!session_tab->timestamp.is_null()); | 
|  |  | 
|  | // Write to the sync model itself. | 
|  | batch->Put(std::move(specifics)); | 
|  |  | 
|  | int current_index = tab_delegate->GetCurrentEntryIndex(); | 
|  | const GURL new_url = tab_delegate->GetVirtualURLAtIndex(current_index); | 
|  | if (new_url != old_url) { | 
|  | delegate_->OnFaviconVisited( | 
|  | new_url, tab_delegate->GetFaviconURLAtIndex(current_index)); | 
|  | } | 
|  | } | 
|  |  | 
|  | void LocalSessionEventHandlerImpl::UpdateTaskTracker( | 
|  | SyncedTabDelegate* const tab_delegate) { | 
|  | TabTasks* tab_tasks = task_tracker_.GetTabTasks( | 
|  | tab_delegate->GetSessionId(), tab_delegate->GetSourceTabID()); | 
|  |  | 
|  | // Iterate through all navigations in the tab to ensure they all have a task | 
|  | // id set (it's possible some haven't been seen before, such as when a tab | 
|  | // is restored). | 
|  | for (int i = 0; i < tab_delegate->GetEntryCount(); ++i) { | 
|  | sessions::SerializedNavigationEntry serialized_entry; | 
|  | tab_delegate->GetSerializedNavigationAtIndex(i, &serialized_entry); | 
|  |  | 
|  | int nav_id = serialized_entry.unique_id(); | 
|  | int64_t global_id = serialized_entry.timestamp().ToInternalValue(); | 
|  | tab_tasks->UpdateWithNavigation( | 
|  | nav_id, tab_delegate->GetTransitionAtIndex(i), global_id); | 
|  | } | 
|  | } | 
|  |  | 
|  | void LocalSessionEventHandlerImpl::WriteTasksIntoSpecifics( | 
|  | sync_pb::SessionTab* tab_specifics) { | 
|  | TabTasks* tab_tasks = task_tracker_.GetTabTasks( | 
|  | SessionID::FromSerializedValue(tab_specifics->tab_id()), | 
|  | /*parent_tab_id=*/SessionID::InvalidValue()); | 
|  | for (int i = 0; i < tab_specifics->navigation_size(); i++) { | 
|  | // Excluding blocked navigations, which are appended at tail. | 
|  | if (tab_specifics->navigation(i).blocked_state() == | 
|  | sync_pb::TabNavigation::STATE_BLOCKED) { | 
|  | break; | 
|  | } | 
|  |  | 
|  | std::vector<int64_t> task_ids = tab_tasks->GetTaskIdsForNavigation( | 
|  | tab_specifics->navigation(i).unique_id()); | 
|  | if (task_ids.empty()) { | 
|  | continue; | 
|  | } | 
|  |  | 
|  | tab_specifics->mutable_navigation(i)->set_task_id(task_ids.back()); | 
|  | // Pop the task id of navigation self. | 
|  | task_ids.pop_back(); | 
|  | tab_specifics->mutable_navigation(i)->clear_ancestor_task_id(); | 
|  | for (auto ancestor_task_id : task_ids) { | 
|  | tab_specifics->mutable_navigation(i)->add_ancestor_task_id( | 
|  | ancestor_task_id); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | void LocalSessionEventHandlerImpl::OnLocalTabModified( | 
|  | SyncedTabDelegate* modified_tab) { | 
|  | DCHECK(!current_session_tag_.empty()); | 
|  |  | 
|  | // Defers updates if session restore is in progress. | 
|  | if (IsSessionRestoreInProgress(sessions_client_)) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | sessions::SerializedNavigationEntry current; | 
|  | modified_tab->GetSerializedNavigationAtIndex( | 
|  | modified_tab->GetCurrentEntryIndex(), ¤t); | 
|  | delegate_->TrackLocalNavigationId(current.timestamp(), current.unique_id()); | 
|  |  | 
|  | std::unique_ptr<WriteBatch> batch = delegate_->CreateLocalSessionWriteBatch(); | 
|  | AssociateTab(modified_tab, batch.get()); | 
|  | // Note, we always associate windows because it's possible a tab became | 
|  | // "interesting" by going to a valid URL, in which case it needs to be added | 
|  | // to the window's tab information. Similarly, if a tab became | 
|  | // "uninteresting", we remove it from the window's tab information. | 
|  | AssociateWindows(DONT_RELOAD_TABS, batch.get()); | 
|  | batch->Commit(); | 
|  | } | 
|  |  | 
|  | void LocalSessionEventHandlerImpl::OnFaviconsChanged( | 
|  | const std::set<GURL>& page_urls, | 
|  | const GURL& /* icon_url */) { | 
|  | for (const GURL& page_url : page_urls) { | 
|  | if (page_url.is_valid()) { | 
|  | delegate_->OnPageFaviconUpdated(page_url); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | sync_pb::SessionTab LocalSessionEventHandlerImpl::GetTabSpecificsFromDelegate( | 
|  | const SyncedTabDelegate& tab_delegate) const { | 
|  | sync_pb::SessionTab specifics; | 
|  | specifics.set_window_id(tab_delegate.GetWindowId().id()); | 
|  | specifics.set_tab_id(tab_delegate.GetSessionId().id()); | 
|  | specifics.set_tab_visual_index(0); | 
|  | // Use -1 to indicate that the index hasn't been set properly yet. | 
|  | specifics.set_current_navigation_index(-1); | 
|  | const SyncedWindowDelegate* window_delegate = | 
|  | sessions_client_->GetSyncedWindowDelegatesGetter()->FindById( | 
|  | tab_delegate.GetWindowId()); | 
|  | specifics.set_pinned( | 
|  | window_delegate ? window_delegate->IsTabPinned(&tab_delegate) : false); | 
|  | specifics.set_extension_app_id(tab_delegate.GetExtensionAppId()); | 
|  | const int current_index = tab_delegate.GetCurrentEntryIndex(); | 
|  | const int min_index = std::max(0, current_index - kMaxSyncNavigationCount); | 
|  | const int max_index = std::min(current_index + kMaxSyncNavigationCount, | 
|  | tab_delegate.GetEntryCount()); | 
|  | bool is_supervised = tab_delegate.ProfileIsSupervised(); | 
|  |  | 
|  | for (int i = min_index; i < max_index; ++i) { | 
|  | if (!tab_delegate.GetVirtualURLAtIndex(i).is_valid()) { | 
|  | continue; | 
|  | } | 
|  | sessions::SerializedNavigationEntry serialized_entry; | 
|  | tab_delegate.GetSerializedNavigationAtIndex(i, &serialized_entry); | 
|  |  | 
|  | // Set current_navigation_index to the index in navigations. | 
|  | if (i == current_index) | 
|  | specifics.set_current_navigation_index(specifics.navigation_size()); | 
|  |  | 
|  | sync_pb::TabNavigation* navigation = specifics.add_navigation(); | 
|  | SessionNavigationToSyncData(serialized_entry).Swap(navigation); | 
|  |  | 
|  | if (is_supervised) { | 
|  | navigation->set_blocked_state( | 
|  | sync_pb::TabNavigation_BlockedState_STATE_ALLOWED); | 
|  | } | 
|  | } | 
|  |  | 
|  | // If the current navigation is invalid, set the index to the end of the | 
|  | // navigation array. | 
|  | if (specifics.current_navigation_index() < 0) { | 
|  | specifics.set_current_navigation_index(specifics.navigation_size() - 1); | 
|  | } | 
|  |  | 
|  | if (is_supervised) { | 
|  | const std::vector<std::unique_ptr<const SerializedNavigationEntry>>& | 
|  | blocked_navigations = *tab_delegate.GetBlockedNavigations(); | 
|  | for (size_t i = 0; i < blocked_navigations.size(); ++i) { | 
|  | sync_pb::TabNavigation* navigation = specifics.add_navigation(); | 
|  | SessionNavigationToSyncData(*blocked_navigations[i]).Swap(navigation); | 
|  | navigation->set_blocked_state( | 
|  | sync_pb::TabNavigation_BlockedState_STATE_BLOCKED); | 
|  | // TODO(bauerb): Add categories | 
|  | } | 
|  | } | 
|  |  | 
|  | return specifics; | 
|  | } | 
|  |  | 
|  | }  // namespace sync_sessions |