blob: 74529e9391d3a98ebe56e15643f8f6b8028c3af8 [file] [log] [blame]
// Copyright 2018 The Chromium Authors
// 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/functional/bind.h"
#include "base/functional/callback.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "components/sync/base/features.h"
#include "components/sync/base/time.h"
#include "components/sync/protocol/session_specifics.pb.h"
#include "components/sync/protocol/sync_enums.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;
// Enumeration of possible results when placeholder tabs are attempted to be
// resynced. Used in UMA metrics. Do not re-order or delete these entries; they
// are used in a UMA histogram. Please edit SyncPlaceholderTabResyncResult in
// enums.xml if a value is added.
// LINT.IfChange(SyncPlaceholderTabResyncResult)
enum PlaceholderTabResyncResultHistogramValue {
PLACEHOLDER_TAB_FOUND = 0,
PLACEHOLDER_TAB_RESYNCED = 1,
PLACEHOLDER_TAB_NOT_SYNCED = 2,
kMaxValue = PLACEHOLDER_TAB_NOT_SYNCED
};
// LINT.ThenChange(/tools/metrics/histograms/metadata/sync/enums.xml:SyncPlaceholderTabResyncResult)
// 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 window_delegates =
synced_window_getter->GetSyncedWindowDelegates();
for (const auto& [window_id, window_delegate] : window_delegates) {
if (window_delegate->IsSessionRestoreInProgress()) {
return true;
}
}
return false;
}
bool IsWindowSyncable(const SyncedWindowDelegate& window_delegate) {
return window_delegate.ShouldSync() && 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_id, window_delegate] :
delegates_getter->GetSyncedWindowDelegates()) {
if (window_delegate->IsTypeNormal() && IsWindowSyncable(*window_delegate)) {
return true;
}
}
return false;
}
sync_pb::SyncEnums_BrowserType BrowserTypeFromWindowDelegate(
const SyncedWindowDelegate& delegate) {
if (delegate.IsTypeNormal()) {
return sync_pb::SyncEnums_BrowserType_TYPE_TABBED;
}
if (delegate.IsTypePopup()) {
return sync_pb::SyncEnums_BrowserType_TYPE_POPUP;
}
// This is a custom tab within an app. These will not be restored on
// startup if not present.
return sync_pb::SyncEnums_BrowserType_TYPE_CUSTOM_TAB;
}
#if BUILDFLAG(IS_ANDROID)
void RecordPlaceholderTabResyncResult(
PlaceholderTabResyncResultHistogramValue result_value) {
base::UmaHistogramEnumeration("Sync.PlaceholderTabResyncResult",
result_value);
}
#endif // BUILDFLAG(IS_ANDROID)
} // 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() = default;
void LocalSessionEventHandlerImpl::OnSessionRestoreComplete() {
std::unique_ptr<WriteBatch> batch = delegate_->CreateLocalSessionWriteBatch();
// The initial state of the tracker may contain tabs that are unmmapped but
// haven't been marked as free yet.
CleanupLocalTabs(batch.get());
AssociateWindows(RELOAD_TABS, batch.get(), /*is_session_restore=*/true);
batch->Commit();
}
sync_pb::SessionTab
LocalSessionEventHandlerImpl::GetTabSpecificsFromDelegateForTest(
SyncedTabDelegate& tab_delegate) const {
return GetTabSpecificsFromDelegate(tab_delegate);
}
void LocalSessionEventHandlerImpl::CleanupLocalTabs(WriteBatch* batch) {
std::set<int> deleted_tab_node_ids =
session_tracker_->CleanupLocalTabs(base::BindRepeating(
&Delegate::IsTabNodeUnsynced, base::Unretained(delegate_)));
for (int tab_node_id : deleted_tab_node_ids) {
batch->Delete(tab_node_id);
}
}
void LocalSessionEventHandlerImpl::AssociateWindows(ReloadTabsOption option,
WriteBatch* batch,
bool is_session_restore) {
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 window_delegates =
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->SetModifiedTime(base::Time::Now());
} else {
DVLOG(1) << "Found no tabbed windows. Reloading "
<< current_session->windows.size()
<< " windows from previous session.";
}
for (auto& [window_id, window_delegate] : window_delegates) {
// Make sure the window is viewable and is not about to be closed. 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 window is null, the browser is about to be deleted, so we
// ignore it. There is no check for having open tabs anymore. This is needed
// to handle a case when the last tab is closed (on Andorid it doesn't mean
// that the window is about to be removed). Instead, there is a check if the
// window is about to be closed. If the window is last for the profile, the
// latest state will be kept.
if (!IsWindowSyncable(*window_delegate)) {
continue;
}
DCHECK_EQ(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 BUILDFLAG(IS_ANDROID)
// Metrics recording will only occur if AssociateWindows is called through
// a session restore, denoted by is_session_restore.
if (synced_tab->IsPlaceholderTab()) {
if (tab && is_session_restore) {
RecordPlaceholderTabResyncResult(PLACEHOLDER_TAB_FOUND);
} else if (!tab) {
// The placeholder tab doesn't have a tracked counterpart. This is
// possible, for example, if the tab was created as a placeholder tab.
bool was_tab_resynced = AssociatePlaceholderTab(
synced_tab->CreatePlaceholderTabSyncedTabDelegate(), batch);
if (was_tab_resynced) {
// If the tab was presumed to have resynced successfully, perform
// another lookup.
tab = session_tracker_->LookupSessionTab(current_session_tag_,
tab_id);
if (tab && is_session_restore) {
RecordPlaceholderTabResyncResult(PLACEHOLDER_TAB_RESYNCED);
}
} else if (is_session_restore) {
RecordPlaceholderTabResyncResult(PLACEHOLDER_TAB_NOT_SYNCED);
}
} else if (is_session_restore) {
// This metric logic path will likely record no tab data as long as
// the RestoreSyncedPlaceholderTabs flag is enabled. If it is
// disabled, this path will record all placeholder tabs that the
// flag-guarded logic would have attempted to target.
RecordPlaceholderTabResyncResult(PLACEHOLDER_TAB_NOT_SYNCED);
}
}
#endif // BUILDFLAG(IS_ANDROID)
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();
synced_session_window->window_type =
BrowserTypeFromWindowDelegate(*window_delegate);
}
}
CleanupLocalTabs(batch);
// 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()) {
// Do nothing else. By not proactively adding the tab to the session, it
// will be removed if necessary during subsequent cleanup.
return;
}
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());
// 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));
}
void LocalSessionEventHandlerImpl::OnLocalTabModified(
SyncedTabDelegate* modified_tab) {
DCHECK(!current_session_tag_.empty());
// Defers updates if session restore is in progress.
if (IsSessionRestoreInProgress(sessions_client_)) {
return;
}
// Don't track empty tabs.
if (modified_tab->GetEntryCount() != 0) {
sessions::SerializedNavigationEntry current;
modified_tab->GetSerializedNavigationAtIndex(
modified_tab->GetCurrentEntryIndex(), &current);
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(), /*is_session_restore=*/false);
batch->Commit();
}
sync_pb::SessionTab LocalSessionEventHandlerImpl::GetTabSpecificsFromDelegate(
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());
if (base::FeatureList::IsEnabled(syncer::kSyncSessionOnVisibilityChanged)) {
specifics.set_last_active_time_unix_epoch_millis(
(tab_delegate.GetLastActiveTime() - base::Time::UnixEpoch())
.InMilliseconds());
}
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 has_child_account = tab_delegate.ProfileHasChildAccount();
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 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 (has_child_account) {
const std::vector<std::unique_ptr<const SerializedNavigationEntry>>*
blocked_navigations = tab_delegate.GetBlockedNavigations();
if (blocked_navigations) {
for (const auto& entry_unique_ptr : *blocked_navigations) {
sync_pb::TabNavigation* navigation = specifics.add_navigation();
SessionNavigationToSyncData(*entry_unique_ptr).Swap(navigation);
}
}
}
if (window_delegate) {
specifics.set_browser_type(BrowserTypeFromWindowDelegate(*window_delegate));
}
return specifics;
}
bool LocalSessionEventHandlerImpl::AssociatePlaceholderTab(
std::unique_ptr<SyncedTabDelegate> snapshot,
WriteBatch* batch) {
// In the event the data read fails or there is no persisted data, a nullptr
// will have been returned and this should early exit.
if (!snapshot) {
return false;
}
const SessionID tab_id = snapshot->GetSessionId();
const SessionID window_id = snapshot->GetWindowId();
// If for some reason the tab ID or the window ID is invalid, skip it.
if (!tab_id.is_valid() || !window_id.is_valid()) {
return false;
}
AssociateTab(snapshot.get(), batch);
return true;
}
} // namespace sync_sessions