// Copyright 2023 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/tabs/model/inactive_tabs/utils.h"
#import "base/metrics/histogram_functions.h"
#import "base/ranges/algorithm.h"
#import "ios/chrome/browser/ntp/model/new_tab_page_util.h"
#import "ios/chrome/browser/shared/model/browser/browser.h"
#import "ios/chrome/browser/shared/model/web_state_list/browser_util.h"
#import "ios/chrome/browser/shared/model/web_state_list/order_controller.h"
#import "ios/chrome/browser/shared/model/web_state_list/order_controller_source_from_web_state_list.h"
#import "ios/chrome/browser/shared/model/web_state_list/removing_indexes.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_opener.h"
#import "ios/chrome/browser/tabs/model/inactive_tabs/features.h"
#import "ios/web/public/web_state.h"
namespace {
// Returns true if the given web state last is inactive determined by the given
// threshold.
bool IsInactive(base::TimeDelta threshold, web::WebState* web_state) {
const base::TimeDelta time_since_last_activation =
base::Time::Now() - web_state->GetLastActiveTime();
if (threshold > base::Days(1)) {
// Note: Even though a week is 7 days, the threshold value is returned with
// one extra day in all cases (> instead of >= operator) as it matches the
// user expectations in the following case:
//
// The user opens a tab every Monday. Last Monday it was opened at
// 10:05am. The tab should not immediately be considered inactive at
// 10:06am today.
//
// The padding is here to encompass a flexibility of a day.
return time_since_last_activation.InDays() > threshold.InDays();
} else {
// This is the demo mode. Compare the times with no one-day padding.
// TODO(crbug.com/40890696): Remove this once the experimental flag is
// removed.
return time_since_last_activation > threshold;
}
}
// Policy used by `MoveTabsAccordingToPolicy(...)`.
struct MovePolicy {
enum Policy {
kAll,
kActiveOnly,
kInactiveOnly,
};
// Returns a policy requesting to moving all tabs.
static MovePolicy All() { return MovePolicy{.policy = kAll}; }
// Returns a policy requesting to move all active tabs with `threshold`.
static MovePolicy ActiveOnly(base::TimeDelta threshold) {
return MovePolicy{.policy = kActiveOnly, .threshold = threshold};
}
// Returns a policy requesting to move all inactive tabs with `threshold`.
static MovePolicy InactiveOnly(base::TimeDelta threshold) {
return MovePolicy{.policy = kInactiveOnly, .threshold = threshold};
}
// The policy controlling which tabs to move.
const Policy policy;
// The threshold used to decide whether a tab is inactive or not.
const base::TimeDelta threshold;
};
// Moves tabs from `source_browser` to `target_browser` after removing the
// duplicates (if any, as determined by their unique identifiers) following
// `move_policy`. The histogram `histogram` is used to record the number of
// duplicates found.
void MoveTabsAccordingToPolicy(Browser* source_browser,
Browser* target_browser,
MovePolicy move_policy,
const char* histogram) {
WebStateList* const source_list = source_browser->GetWebStateList();
WebStateList* const target_list = target_browser->GetWebStateList();
const int source_count = source_list->count();
const int target_count = target_list->count();
std::vector<web::WebStateID> target_ids;
for (int index = 0; index < target_count; ++index) {
web::WebState* web_state = target_list->GetWebStateAt(index);
target_ids.push_back(web_state->GetUniqueIdentifier());
}
// Sort the vector of identifiers in order to use binary_search(...).
std::sort(target_ids.begin(), target_ids.end());
std::vector<int> indexes_closing;
std::vector<int> indexes_moving;
std::vector<int> indexes_moving_or_closing;
for (int index = 0; index < source_count; ++index) {
web::WebState* web_state = source_list->GetWebStateAt(index);
const web::WebStateID web_state_id = web_state->GetUniqueIdentifier();
if (base::ranges::binary_search(target_ids, web_state_id)) {
indexes_closing.push_back(index);
indexes_moving_or_closing.push_back(index);
continue;
}
if (move_policy.policy == MovePolicy::kAll) {
indexes_moving.push_back(index);
indexes_moving_or_closing.push_back(index);
continue;
}
// Don't consider tabs presenting the NTP, pinned tabs or tabs in a group as
// inactive.
if (move_policy.policy == MovePolicy::kInactiveOnly) {
if (IsVisibleURLNewTabPage(web_state)) {
continue;
}
if (index < source_list->pinned_tabs_count()) {
continue;
}
if (source_list->GetGroupOfWebStateAt(index)) {
continue;
}
}
const bool is_inactive = IsInactive(move_policy.threshold, web_state);
if (is_inactive == (move_policy.policy == MovePolicy::kInactiveOnly)) {
indexes_moving.push_back(index);
indexes_moving_or_closing.push_back(index);
continue;
}
}
// Record the number of duplicates found.
base::UmaHistogramCounts100(histogram, indexes_closing.size());
// If there are no WebState to move or close, then there is nothing to do.
if (indexes_moving_or_closing.empty()) {
return;
}
// Start a batch operation on the two WebStateList at the same time.
const auto source_lock = source_list->StartBatchOperation();
const auto target_lock = target_list->StartBatchOperation();
// Determine and activate the new active WebState before performing the
// close and move operations. This will prevents over-realisation.
OrderControllerSourceFromWebStateList order_controller_source(*source_list);
OrderController order_controller(order_controller_source);
const int new_active_index = order_controller.DetermineNewActiveIndex(
source_list->active_index(),
RemovingIndexes(std::move(indexes_moving_or_closing)));
source_list->ActivateWebStateAt(new_active_index);
// Determine the index at which tabs are inserted in `target_list`. When
// moving inactive tabs, they are inserted at the end of the destination
// but when moving active ones, they are moved after the pinned tabs.
const int insertion_index = move_policy.policy == MovePolicy::kInactiveOnly
? target_count
: target_list->pinned_tabs_count();
// If the target list has no active WebState, mark the first tab moved out
// of the source list as the active one during the insertion.
int index_to_activate = WebStateList::kInvalidIndex;
if (!target_list->GetActiveWebState()) {
if (!indexes_moving.empty()) {
index_to_activate = indexes_moving.front();
}
}
// Perform the close and move operations by iterating backwards in the
// WebStateList (this avoid having to update the indexes).
for (int iter = 0; iter < source_count; ++iter) {
const int index = source_count - iter - 1;
if (base::ranges::binary_search(indexes_closing, index)) {
source_list->CloseWebStateAt(index, WebStateList::CLOSE_NO_FLAGS);
continue;
}
if (base::ranges::binary_search(indexes_moving, index)) {
// Using `AtIndex` allows to insert all the moved tabs with a desired
// location and `Activate` allows to activate the tab if needed (e.g. the
// first moved tab from source when target has no active WebState).
const WebStateList::InsertionParams params =
WebStateList::InsertionParams::AtIndex(insertion_index)
.Activate(index == index_to_activate);
MoveTabFromBrowserToBrowser(source_browser, index, target_browser,
params);
continue;
}
}
}
} // namespace
void MoveTabsFromActiveToInactive(Browser* active_browser,
Browser* inactive_browser) {
CHECK(IsInactiveTabsEnabled());
CHECK_NE(active_browser, inactive_browser);
MoveTabsAccordingToPolicy(
active_browser, inactive_browser,
MovePolicy::InactiveOnly(InactiveTabsTimeThreshold()),
"Tabs.DroppedDuplicatesCountOnMigrateActiveToInactive");
}
void MoveTabsFromInactiveToActive(Browser* inactive_browser,
Browser* active_browser) {
CHECK(IsInactiveTabsEnabled());
CHECK_NE(active_browser, inactive_browser);
MoveTabsAccordingToPolicy(
inactive_browser, active_browser,
MovePolicy::ActiveOnly(InactiveTabsTimeThreshold()),
"Tabs.DroppedDuplicatesCountOnMigrateInactiveToActive");
}
void RestoreAllInactiveTabs(Browser* inactive_browser,
Browser* active_browser) {
CHECK(!IsInactiveTabsEnabled());
CHECK_NE(active_browser, inactive_browser);
// Record the number of tabs restored from the inactive browser after Inactive
// Tabs has been disabled.
base::UmaHistogramCounts100("Tabs.RestoredFromInactiveCount",
inactive_browser->GetWebStateList()->count());
MoveTabsAccordingToPolicy(inactive_browser, active_browser, MovePolicy::All(),
"Tabs.DroppedDuplicatesCountOnRestoreAllInactive");
}