// 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/sessions/model/session_loading.h"
#import <vector>
#import "base/check.h"
#import "base/check_op.h"
#import "base/memory/raw_ref.h"
#import "base/metrics/histogram_functions.h"
#import "base/strings/stringprintf.h"
#import "ios/chrome/browser/sessions/model/session_constants.h"
#import "ios/chrome/browser/sessions/model/session_internal_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.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"
namespace ios::sessions {
namespace {
// A concrete implementation of OrderControllerSource that query data
// from an ios::proto::WebStateListStorage.
class OrderControllerSourceFromWebStateListStorage final
: public OrderControllerSource {
public:
// Constructor taking the `session_storage` used to return the data.
explicit OrderControllerSourceFromWebStateListStorage(
const ios::proto::WebStateListStorage& session_metadata);
// OrderControllerSource implementation.
int GetCount() const final;
int GetPinnedCount() const final;
int GetOpenerOfItemAt(int index) const final;
bool IsOpenerOfItemAt(int index,
int opener_index,
bool check_navigation_index) const final;
TabGroupRange GetGroupRangeOfItemAt(int index) const final;
std::set<int> GetCollapsedGroupIndexes() const final;
private:
raw_ref<const ios::proto::WebStateListStorage> session_metadata_;
};
OrderControllerSourceFromWebStateListStorage::
OrderControllerSourceFromWebStateListStorage(
const ios::proto::WebStateListStorage& session_metadata)
: session_metadata_(session_metadata) {}
int OrderControllerSourceFromWebStateListStorage::GetCount() const {
return session_metadata_->items_size();
}
int OrderControllerSourceFromWebStateListStorage::GetPinnedCount() const {
return session_metadata_->pinned_item_count();
}
int OrderControllerSourceFromWebStateListStorage::GetOpenerOfItemAt(
int index) const {
DCHECK_GE(index, 0);
DCHECK_LT(index, session_metadata_->items_size());
const auto& item_storage = session_metadata_->items(index);
if (!item_storage.has_opener()) {
return WebStateList::kInvalidIndex;
}
return item_storage.opener().index();
}
bool OrderControllerSourceFromWebStateListStorage::IsOpenerOfItemAt(
int index,
int opener_index,
bool check_navigation_index) const {
DCHECK_GE(index, 0);
DCHECK_LT(index, session_metadata_->items_size());
// `check_navigation_index` is only used for `DetermineInsertionIndex()`
// which should not be used, so we can assert that the parameter is false.
DCHECK(!check_navigation_index);
const auto& item_storage = session_metadata_->items(index);
if (!item_storage.has_opener()) {
return false;
}
return item_storage.opener().index() == opener_index;
}
TabGroupRange
OrderControllerSourceFromWebStateListStorage::GetGroupRangeOfItemAt(
int index) const {
for (auto& group_storage : session_metadata_->groups()) {
const ios::proto::RangeIndex range_index = group_storage.range();
const TabGroupRange group_range(range_index.start(), range_index.count());
if (group_range.contains(index)) {
return group_range;
}
}
return TabGroupRange::InvalidRange();
}
std::set<int>
OrderControllerSourceFromWebStateListStorage::GetCollapsedGroupIndexes() const {
std::set<int> collapsed_indexes;
for (auto& group_storage : session_metadata_->groups()) {
if (group_storage.collapsed()) {
const ios::proto::RangeIndex range_index = group_storage.range();
const TabGroupRange group_range(range_index.start(), range_index.count());
collapsed_indexes.insert(group_range.begin(), group_range.end());
}
}
return collapsed_indexes;
}
// Returns an ios::proto::WebStateListStorage representing an empty session.
ios::proto::WebStateListStorage EmptyWebStateListStorage() {
ios::proto::WebStateListStorage web_state_list_storage;
web_state_list_storage.set_active_index(-1);
return web_state_list_storage;
}
} // namespace
base::FilePath WebStateDirectory(const base::FilePath& directory,
web::WebStateID identifier) {
return directory.Append(base::StringPrintf("%08x", identifier.identifier()));
}
ios::proto::WebStateListStorage FilterItems(
ios::proto::WebStateListStorage storage,
const RemovingIndexes& removing_indexes) {
// If there is no items to remove, return the input unmodified.
if (removing_indexes.count() == 0) {
return storage;
}
// Compute the new active index, before removing items from `storage`.
ios::proto::WebStateListStorage result;
{
const OrderControllerSourceFromWebStateListStorage source(storage);
const OrderController order_controller(source);
result.set_active_index(removing_indexes.IndexAfterRemoval(
order_controller.DetermineNewActiveIndex(storage.active_index(),
removing_indexes)));
}
const int items_size = storage.items_size();
int pinned_item_count = storage.pinned_item_count();
for (int index = 0; index < items_size; ++index) {
if (removing_indexes.Contains(index)) {
if (index < storage.pinned_item_count()) {
DCHECK_GE(pinned_item_count, 1);
--pinned_item_count;
}
continue;
}
// Add a new item, copying its value from the old item.
ios::proto::WebStateListItemStorage* item_storage = result.add_items();
item_storage->Swap(storage.mutable_items(index));
// Fix the opener index (to take into account the closed items) or clear
// the opener information if the opener has been closed.
if (item_storage->has_opener()) {
const int opener_index = item_storage->opener().index();
if (removing_indexes.Contains(opener_index)) {
item_storage->clear_opener();
} else {
item_storage->mutable_opener()->set_index(
removing_indexes.IndexAfterRemoval(opener_index));
}
}
}
DCHECK_GE(pinned_item_count, 0);
DCHECK_LE(pinned_item_count, result.items_size());
result.set_pinned_item_count(pinned_item_count);
// Create the new list of tab groups, updating the range `start` and
// range `count` properties.
for (auto& initial_group_storage : *storage.mutable_groups()) {
const ios::proto::RangeIndex initial_range_index =
initial_group_storage.range();
const TabGroupRange initial_range(initial_range_index.start(),
initial_range_index.count());
const TabGroupRange final_range =
removing_indexes.RangeAfterRemoval(initial_range);
if (final_range.valid()) {
// Add a new group, copying its value from the old group.
ios::proto::TabGroupStorage* group_storage = result.add_groups();
group_storage->Swap(&initial_group_storage);
ios::proto::RangeIndex* range_index = group_storage->mutable_range();
range_index->set_start(final_range.range_begin());
range_index->set_count(final_range.count());
}
}
return result;
}
ios::proto::WebStateListStorage LoadSessionStorage(
const base::FilePath& directory) {
const base::FilePath session_metadata_file =
directory.Append(kSessionMetadataFilename);
// If the session metadata cannot be loaded, then the session is absent or
// has been corrupted; return an empty session.
ios::proto::WebStateListStorage session;
if (!ParseProto(session_metadata_file, session)) {
return EmptyWebStateListStorage();
}
// Used to filter items (either duplicate or WebState with no navigations).
std::vector<int> items_to_drop;
// Count the number of dropped tabs because they are duplicates, for
// reporting.
std::set<web::WebStateID> seen_identifiers;
int duplicate_count = 0;
const int items_size = session.items_size();
for (int index = 0; index < items_size; ++index) {
// If the item identifier is invalid, then the session has been corrupted;
// return an empty session.
const auto& item = session.items(index);
if (!web::WebStateID::IsValidValue(item.identifier())) {
return EmptyWebStateListStorage();
}
const auto web_state_id =
web::WebStateID::FromSerializedValue(item.identifier());
const base::FilePath web_state_dir =
WebStateDirectory(directory, web_state_id);
const base::FilePath web_state_storage_file =
web_state_dir.Append(kWebStateStorageFilename);
// If the item storage does not exist, then the session has been
// corrupted; return an empty session.
if (!FileExists(web_state_storage_file)) {
return EmptyWebStateListStorage();
}
// If the item would be empty, drop it before restoration.
if (!item.metadata().navigation_item_count()) {
items_to_drop.push_back(index);
continue;
}
// If the item is a duplicate, drop it before restoration.
if (seen_identifiers.contains(web_state_id)) {
items_to_drop.push_back(index);
duplicate_count++;
continue;
}
seen_identifiers.insert(web_state_id);
}
base::UmaHistogramCounts100("Tabs.DroppedDuplicatesCountOnSessionRestore",
duplicate_count);
return FilterItems(std::move(session),
RemovingIndexes(std::move(items_to_drop)));
}
} // namespace ios::sessions