// 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/tabs_closer.h"
#import <optional>
#import "base/check.h"
#import "base/functional/bind.h"
#import "base/functional/callback.h"
#import "base/unguessable_token.h"
#import "base/uuid.h"
#import "components/saved_tab_groups/saved_tab_group.h"
#import "components/saved_tab_groups/tab_group_sync_service.h"
#import "components/sessions/core/session_id.h"
#import "ios/chrome/browser/saved_tab_groups/model/tab_group_sync_service_factory.h"
#import "ios/chrome/browser/sessions/model/session_restoration_service.h"
#import "ios/chrome/browser/sessions/model/session_restoration_service_factory.h"
#import "ios/chrome/browser/shared/model/browser/browser.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.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/tab_group.h"
#import "ios/chrome/browser/shared/model/web_state_list/tab_group_range.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_list_delegate.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_opener.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/web/public/web_state.h"
namespace {
// Information about a tab group.
struct TabGroupInfo {
const TabGroupRange group_range;
const tab_groups::TabGroupId tab_group_id;
const tab_groups::TabGroupVisualData visual_data;
};
// Moves WebStates in range [start; start+count) from `source` to `target`.
void MoveWebStatesInRangeBetweenLists(WebStateList* source,
WebStateList* target,
int start,
int count) {
DCHECK_GE(start, 0);
DCHECK_LT(start, source->count());
DCHECK_LE(start + count, source->count());
// Only one of the WebStateList can contain pinned tabs.
CHECK(target->pinned_tabs_count() == 0 || source->pinned_tabs_count() == 0);
const int old_active_index = source->active_index();
const int old_pinned_count = source->pinned_tabs_count();
const int offset = target->count();
const int end = start + count;
const OrderControllerSourceFromWebStateList order_controller_source(*source);
const OrderController order_controller(order_controller_source);
source->ActivateWebStateAt(order_controller.DetermineNewActiveIndex(
old_active_index, RemovingIndexes({.start = start, .count = count})));
// Store the groups info.
std::vector<TabGroupInfo> groups;
for (const TabGroup* group : source->GetGroups()) {
TabGroupRange range = group->range();
// The group is not in the range of moving items, ignore it.
if (range.range_end() <= start || end <= range.range_begin()) {
continue;
}
// The current implementation does not support partially closing
// a group. So assert that the group is fully contained in the
// range of closed items.
CHECK(start <= range.range_begin() && range.range_end() <= end);
range.Move(offset - start);
groups.push_back(TabGroupInfo{
.group_range = range,
.tab_group_id = group->tab_group_id(),
.visual_data = group->visual_data(),
});
}
for (int n = 0; n < count; ++n) {
const bool is_pinned = start + n < old_pinned_count;
std::unique_ptr<web::WebState> web_state = source->DetachWebStateAt(start);
const WebStateList::InsertionParams params =
WebStateList::InsertionParams::AtIndex(n + offset)
.Pinned(is_pinned)
.Activate(start + n == old_active_index);
const int insertion_index =
target->InsertWebState(std::move(web_state), params);
DCHECK_EQ(n + offset, insertion_index);
}
// Restore the groups info.
for (const auto& group : groups) {
target->CreateGroup(group.group_range.AsSet(), group.visual_data,
group.tab_group_id);
}
}
} // namespace
class TabsCloser::UndoStorage {
public:
explicit UndoStorage(Browser* browser);
UndoStorage(const UndoStorage&) = delete;
UndoStorage& operator=(const UndoStorage&) = delete;
~UndoStorage();
// Returns the number of tabs that have been closed.
int count() const { return temporary_browser_->GetWebStateList()->count(); }
// Closes tabs in range [start; start+count) from `original_browser_` and
// stores state to allow undoing the operation if needed.
void CloseTabs(int start, int count);
// Undoes the close operation performed in `CloseTabs`.
void Undo();
// Confirms the close operation performed in `CloseTabs`, deleting the state.
// This is irreversible and no data can be recovered after this method has
// been called.
void Drop();
private:
// Stores opener-opened information for a WebState.
struct Opener {
int opener_index;
int opener_navigation_index;
};
raw_ptr<Browser> original_browser_{nullptr};
std::unique_ptr<Browser> temporary_browser_;
std::vector<std::optional<Opener>> openers_;
};
TabsCloser::UndoStorage::UndoStorage(Browser* browser)
: original_browser_(browser),
temporary_browser_(Browser::CreateTemporary(browser->GetBrowserState())) {
SessionRestorationServiceFactory::GetForBrowserState(
temporary_browser_->GetBrowserState())
->AttachBackup(original_browser_.get(), temporary_browser_.get());
}
TabsCloser::UndoStorage::~UndoStorage() {
// If there is still a pending undo when the object is destroyed, consider
// that the close operation has been confirmed.
Drop();
// The temporary browser must now be empty.
DCHECK(temporary_browser_->GetWebStateList()->empty());
SessionRestorationService* service =
SessionRestorationServiceFactory::GetForBrowserState(
temporary_browser_->GetBrowserState());
service->Disconnect(temporary_browser_.get());
}
void TabsCloser::UndoStorage::CloseTabs(int start, int count) {
WebStateList* web_state_list = original_browser_->GetWebStateList();
std::map<web::WebState*, int> web_state_map;
for (int index = 0; index < web_state_list->count(); ++index) {
web::WebState* web_state = web_state_list->GetWebStateAt(index);
web_state_map.insert(std::make_pair(web_state, index));
}
for (int index = 0; index < web_state_list->count(); ++index) {
WebStateOpener opener = web_state_list->GetOpenerOfWebStateAt(index);
if (opener.opener) {
DCHECK(base::Contains(web_state_map, opener.opener));
openers_.push_back(Opener{
.opener_index = web_state_map[opener.opener],
.opener_navigation_index = opener.navigation_index,
});
} else {
openers_.push_back(std::nullopt);
}
}
DCHECK_EQ(static_cast<int>(openers_.size()), web_state_list->count());
WebStateList* source = original_browser_->GetWebStateList();
WebStateList* target = temporary_browser_->GetWebStateList();
WebStateList::ScopedBatchOperation lock_source =
source->StartBatchOperation();
WebStateList::ScopedBatchOperation lock_target =
target->StartBatchOperation();
MoveWebStatesInRangeBetweenLists(source, target, start, count);
}
void TabsCloser::UndoStorage::Undo() {
WebStateList* source = temporary_browser_->GetWebStateList();
WebStateList* target = original_browser_->GetWebStateList();
WebStateList::ScopedBatchOperation lock_source =
source->StartBatchOperation();
WebStateList::ScopedBatchOperation lock_target =
target->StartBatchOperation();
MoveWebStatesInRangeBetweenLists(source, target, 0, source->count());
DCHECK_EQ(static_cast<int>(openers_.size()), target->count());
for (int index = 0; index < target->count(); ++index) {
const std::optional<Opener>& opener = openers_[index];
if (opener.has_value()) {
target->SetOpenerOfWebStateAt(
index, WebStateOpener(target->GetWebStateAt(opener->opener_index),
opener->opener_navigation_index));
}
}
openers_.clear();
}
void TabsCloser::UndoStorage::Drop() {
CloseAllWebStates(*temporary_browser_->GetWebStateList(),
WebStateList::CLOSE_USER_ACTION);
openers_.clear();
}
TabsCloser::TabsCloser(Browser* browser, ClosePolicy policy)
: browser_(browser), close_policy_(policy) {
DCHECK(browser_);
DCHECK(!browser_->GetBrowserState()->IsOffTheRecord());
}
TabsCloser::~TabsCloser() = default;
bool TabsCloser::CanCloseTabs() const {
WebStateList* web_state_list = browser_->GetWebStateList();
switch (close_policy_) {
case ClosePolicy::kAllTabs:
return web_state_list->count() != 0;
case ClosePolicy::kRegularTabs:
return web_state_list->regular_tabs_count() != 0;
}
}
int TabsCloser::CloseTabs() {
DCHECK(CanCloseTabs());
WebStateList* web_state_list = browser_->GetWebStateList();
int start, count;
switch (close_policy_) {
case ClosePolicy::kAllTabs:
start = 0;
count = web_state_list->count();
break;
case ClosePolicy::kRegularTabs:
start = web_state_list->pinned_tabs_count();
count = web_state_list->regular_tabs_count();
break;
}
if (IsTabGroupSyncEnabled()) {
tab_groups::TabGroupSyncService* sync_service =
tab_groups::TabGroupSyncServiceFactory::GetForBrowserState(
browser_->GetBrowserState());
CHECK(sync_service);
for (const TabGroup* tab_group : web_state_list->GetGroups()) {
tab_groups::TabGroupId local_id = tab_group->tab_group_id();
std::optional<tab_groups::SavedTabGroup> saved_group =
sync_service->GetGroup(local_id);
if (saved_group) {
local_to_saved_group_ids_.insert(
std::make_pair(local_id, saved_group->saved_guid()));
sync_service->RemoveLocalTabGroupMapping(local_id);
}
}
}
state_ = std::make_unique<UndoStorage>(browser_);
state_->CloseTabs(start, count);
// Force a session save to avoid having to wait for the timeout.
// This is mostly useful when user has a large number of tabs (see
// bug https://crbug.com/1510953 for details).
SessionRestorationServiceFactory::GetForBrowserState(
browser_->GetBrowserState())
->SaveSessions();
return state_->count();
}
bool TabsCloser::CanUndoCloseTabs() const {
return state_ != nullptr;
}
int TabsCloser::UndoCloseTabs() {
DCHECK(CanUndoCloseTabs());
// Invalidate `state_` before performing the "undo" operation.
std::unique_ptr<UndoStorage> state = std::exchange(state_, {});
const int result = state->count();
if (!IsTabGroupSyncEnabled()) {
state->Undo();
return result;
}
tab_groups::TabGroupSyncService* sync_service =
tab_groups::TabGroupSyncServiceFactory::GetForBrowserState(
browser_->GetBrowserState());
CHECK(sync_service);
WebStateList* web_state_list = browser_->GetWebStateList();
auto pauser = sync_service->CreateScopedLocalObserverPauser();
std::set<const TabGroup*> tab_groups_to_delete;
state->Undo();
for (const TabGroup* tab_group : web_state_list->GetGroups()) {
tab_groups::LocalTabGroupID local_id = tab_group->tab_group_id();
auto iterator = local_to_saved_group_ids_.find(local_id);
CHECK(iterator != local_to_saved_group_ids_.end(),
base::NotFatalUntil::M132);
base::Uuid saved_id = iterator->second;
std::optional<tab_groups::SavedTabGroup> saved_group =
sync_service->GetGroup(saved_id);
if (!saved_group) {
// The group has probably been deleted remotely.
tab_groups_to_delete.insert(tab_group);
continue;
}
sync_service->ConnectLocalTabGroup(saved_id, local_id);
}
for (const TabGroup* tab_group : tab_groups_to_delete) {
web_state_list->DeleteGroup(tab_group);
}
local_to_saved_group_ids_.clear();
return result;
}
int TabsCloser::ConfirmDeletion() {
DCHECK(CanUndoCloseTabs());
// Invalidate `state_` before performing the "drop" operation.
local_to_saved_group_ids_.clear();
std::unique_ptr<UndoStorage> state = std::exchange(state_, {});
const int result = state->count();
state->Drop();
return result;
}