// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/browsing_data/model/tabs_closure_util.h"
#import "ios/chrome/browser/shared/model/web_state_list/removing_indexes.h"
#import "ios/chrome/browser/shared/model/web_state_list/tab_group.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
#import "ios/web/public/navigation/navigation_item.h"
#import "ios/web/public/navigation/navigation_manager.h"
#import "ios/web/public/web_state.h"
namespace {
// Returns true if the `recent_navigation_time` is between [`begin_time`,
// `end_time`[. False, otherwise.
bool ShouldCloseTab(base::Time recent_navigation_time,
base::Time begin_time,
base::Time end_time) {
CHECK_LE(begin_time, end_time);
return recent_navigation_time >= begin_time &&
recent_navigation_time < end_time;
}
// Returns the last navigation timestamp of `web_state`. For an unrealized
// WebState, uses the information in `cached_tabs_to_close`.
base::Time GetWebStateLastNavigationTime(
web::WebState* web_state,
const tabs_closure_util::WebStateIDToTime& cached_tabs_to_close) {
web::WebStateID const web_state_id = web_state->GetUniqueIdentifier();
if (web_state->IsRealized()) {
// While the tabs' information was loaded from disk and this method was
// invoked (i.e. when tabs' closure was requested), a new tab could have
// been created or a new navigation could have happened. In either case,
// the tabs are realized, and as such, we can get the last navigation time
// directly from the WebState, avoiding using stale data.
web::NavigationItem* navigation_item =
web_state->GetNavigationManager()->GetLastCommittedItem();
return navigation_item ? navigation_item->GetTimestamp()
: web_state->GetLastActiveTime();
}
if (auto iter = cached_tabs_to_close.find(web_state_id);
iter != cached_tabs_to_close.end()) {
// If the tab is unrealized, avoid realizing it, and use the cached
// information which is still accurate.
return iter->second;
}
// BYOT and other unrealized WebStates that have no navigation can be
// created after loading the session from disk. Because they have no
// navigation, or they would be realized, we will use last_active_time
// (which is set to creation_time) as a fallback.
return web_state->GetLastActiveTime();
}
} // namespace
namespace tabs_closure_util {
base::Time GetLastCommittedTimestampFromStorage(
web::proto::WebStateStorage storage) {
const auto& navigation_storage = storage.navigation();
const int index = navigation_storage.last_committed_item_index();
if (index < 0 || navigation_storage.items_size() <= index) {
// Invalid last committed item index, return last active time as fallback.
return web::TimeFromProto(storage.metadata().last_active_time());
}
const auto& item_storage = navigation_storage.items(index);
return web::TimeFromProto(item_storage.timestamp());
}
WebStateIDToTime GetTabsInfoForCache(
const WebStateIDToTime& tabs_to_last_navigation_time,
base::Time begin_time,
base::Time end_time) {
CHECK_LE(begin_time, end_time);
WebStateIDToTime tabs_to_close;
for (auto const& tab_to_time : tabs_to_last_navigation_time) {
if (ShouldCloseTab(tab_to_time.second, begin_time, end_time)) {
tabs_to_close.insert({tab_to_time.first, tab_to_time.second});
}
}
return tabs_to_close;
}
std::set<web::WebStateID> GetTabsToClose(
WebStateList* web_state_list,
base::Time begin_time,
base::Time end_time,
const WebStateIDToTime& cached_tabs_to_close) {
CHECK(web_state_list);
std::set<web::WebStateID> webstates_to_close;
// Exclude web states that are pinned.
for (int index = web_state_list->pinned_tabs_count();
index < web_state_list->count(); ++index) {
web::WebState* web_state = web_state_list->GetWebStateAt(index);
base::Time last_navigation_time =
GetWebStateLastNavigationTime(web_state, cached_tabs_to_close);
if (ShouldCloseTab(last_navigation_time, begin_time, end_time)) {
webstates_to_close.insert(web_state->GetUniqueIdentifier());
}
}
return webstates_to_close;
}
std::map<tab_groups::TabGroupId, std::set<int>> GetTabGroupsWithTabsToClose(
WebStateList* web_state_list,
base::Time begin_time,
base::Time end_time,
const WebStateIDToTime& cached_tabs_to_close) {
CHECK(web_state_list);
std::map<tab_groups::TabGroupId, std::set<int>> groups_with_tabs_to_close;
for (const TabGroup* tab_group : web_state_list->GetGroups()) {
std::set<int> webstate_indexes;
for (int index : tab_group->range()) {
base::Time last_navigation_time = GetWebStateLastNavigationTime(
web_state_list->GetWebStateAt(index), cached_tabs_to_close);
if (ShouldCloseTab(last_navigation_time, begin_time, end_time)) {
webstate_indexes.insert(index);
}
}
if (!webstate_indexes.empty()) {
groups_with_tabs_to_close.insert(
{tab_group->tab_group_id(), webstate_indexes});
}
}
return groups_with_tabs_to_close;
}
void CloseTabs(WebStateList* web_state_list,
base::Time begin_time,
base::Time end_time,
const WebStateIDToTime& cached_tabs_to_close) {
CHECK(web_state_list);
std::set<web::WebStateID> web_state_ids_to_close = GetTabsToClose(
web_state_list, begin_time, end_time, cached_tabs_to_close);
if (web_state_ids_to_close.empty()) {
return;
}
std::vector<int> indices_to_close;
// Exclude web states that are pinned.
for (int index = web_state_list->pinned_tabs_count();
index < web_state_list->count(); ++index) {
web::WebState* web_state = web_state_list->GetWebStateAt(index);
if (web_state_ids_to_close.contains(web_state->GetUniqueIdentifier())) {
indices_to_close.push_back(index);
}
if (indices_to_close.size() == web_state_ids_to_close.size()) {
break;
}
}
auto lock = web_state_list->StartBatchOperation();
web_state_list->CloseWebStatesAtIndices(
WebStateList::CLOSE_NO_FLAGS,
RemovingIndexes(std::move(indices_to_close)));
}
} // namespace tabs_closure_util