// Copyright 2019 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_restoration_browser_agent.h"
#import <vector>
#import "base/apple/foundation_util.h"
#import "base/ios/ios_util.h"
#import "base/memory/ptr_util.h"
#import "base/metrics/histogram_functions.h"
#import "base/strings/sys_string_conversions.h"
#import "base/time/time.h"
#import "components/previous_session_info/previous_session_info.h"
#import "ios/chrome/browser/sessions/model/session_constants.h"
#import "ios/chrome/browser/sessions/model/session_restoration_observer.h"
#import "ios/chrome/browser/sessions/model/session_service_ios.h"
#import "ios/chrome/browser/sessions/model/session_tab_group.h"
#import "ios/chrome/browser/sessions/model/session_window_ios.h"
#import "ios/chrome/browser/sessions/model/session_window_ios_factory.h"
#import "ios/chrome/browser/sessions/model/web_session_state_cache.h"
#import "ios/chrome/browser/sessions/model/web_session_state_cache_factory.h"
#import "ios/chrome/browser/sessions/model/web_session_state_tab_helper.h"
#import "ios/chrome/browser/sessions/model/web_state_list_serialization.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/all_web_state_observation_forwarder.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/removing_indexes.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/web/public/navigation/navigation_item.h"
#import "ios/web/public/navigation/navigation_manager.h"
#import "ios/web/public/session/crw_session_storage.h"
#import "ios/web/public/session/crw_session_user_data.h"
#import "ios/web/public/web_state.h"
namespace {
// A concrete implementation of OrderControllerSource that query data
// from a SessionWindowIOS.
class OrderControllerSourceFromSessionWindowIOS final
: public OrderControllerSource {
public:
// Constructor taking the `session_window` used to return the data.
explicit OrderControllerSourceFromSessionWindowIOS(
SessionWindowIOS* session_window);
// 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:
SessionWindowIOS* session_window_;
};
OrderControllerSourceFromSessionWindowIOS::
OrderControllerSourceFromSessionWindowIOS(SessionWindowIOS* session_window)
: session_window_(session_window) {}
int OrderControllerSourceFromSessionWindowIOS::GetCount() const {
return static_cast<int>(session_window_.sessions.count);
}
int OrderControllerSourceFromSessionWindowIOS::GetPinnedCount() const {
int pinned_count = 0;
for (CRWSessionStorage* session in session_window_.sessions) {
CRWSessionUserData* user_data = session.userData;
NSNumber* pinned_obj = base::apple::ObjCCast<NSNumber>(
[user_data objectForKey:kLegacyWebStateListPinnedStateKey]);
// All pinned items are at the beginning of the list, so stop as
// soon as the first unpinned tab is found.
if (!pinned_obj || ![pinned_obj boolValue]) {
break;
}
++pinned_count;
}
return pinned_count;
}
int OrderControllerSourceFromSessionWindowIOS::GetOpenerOfItemAt(
int index) const {
DCHECK_GE(index, 0);
DCHECK_LT(index, GetCount());
CRWSessionUserData* user_data = session_window_.sessions[index].userData;
NSNumber* opener_index_obj = base::apple::ObjCCast<NSNumber>(
[user_data objectForKey:kLegacyWebStateListOpenerIndexKey]);
if (!opener_index_obj) {
return WebStateList::kInvalidIndex;
}
return [opener_index_obj intValue];
}
bool OrderControllerSourceFromSessionWindowIOS::IsOpenerOfItemAt(
int index,
int opener_index,
bool check_navigation_index) const {
DCHECK_GE(index, 0);
DCHECK_LT(index, GetCount());
// `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);
CRWSessionUserData* user_data = session_window_.sessions[index].userData;
NSNumber* opener_index_obj = base::apple::ObjCCast<NSNumber>(
[user_data objectForKey:kLegacyWebStateListOpenerIndexKey]);
if (!opener_index_obj || [opener_index_obj intValue] != opener_index) {
return false;
}
return true;
}
TabGroupRange OrderControllerSourceFromSessionWindowIOS::GetGroupRangeOfItemAt(
int index) const {
for (SessionTabGroup* group in session_window_.tabGroups) {
const TabGroupRange group_range(group.rangeStart, group.rangeCount);
if (group_range.contains(index)) {
return group_range;
}
}
return TabGroupRange::InvalidRange();
}
std::set<int>
OrderControllerSourceFromSessionWindowIOS::GetCollapsedGroupIndexes() const {
std::set<int> collapsed_indexes;
for (SessionTabGroup* group in session_window_.tabGroups) {
if (group.collapsedState) {
const TabGroupRange group_range(group.rangeStart, group.rangeCount);
collapsed_indexes.insert(group_range.begin(), group_range.end());
}
}
return collapsed_indexes;
}
// Determines the new active index.
NSUInteger GetActiveIndex(SessionWindowIOS* session_window,
const RemovingIndexes& removing_indexes) {
int active_index = session_window.selectedIndex != NSNotFound
? static_cast<int>(session_window.selectedIndex)
: WebStateList::kInvalidIndex;
const OrderControllerSourceFromSessionWindowIOS source(session_window);
const OrderController order_controller(source);
// Update the `active_index` using the shared logic and the knowledge
// of the removed items.
active_index = removing_indexes.IndexAfterRemoval(
order_controller.DetermineNewActiveIndex(active_index, removing_indexes));
return active_index != WebStateList::kInvalidIndex
? static_cast<NSUInteger>(active_index)
: NSNotFound;
}
// Updates opener_index for `session` according to `removing_indexes`.
void UpdateOpenerIndex(CRWSessionUserData* user_data,
const RemovingIndexes& removing_indexes) {
NSNumber* opener_index_obj = base::apple::ObjCCast<NSNumber>(
[user_data objectForKey:kLegacyWebStateListOpenerIndexKey]);
if (!opener_index_obj) {
return;
}
const int opener_index =
removing_indexes.IndexAfterRemoval([opener_index_obj intValue]);
if (opener_index == WebStateList::kInvalidIndex) {
[user_data removeObjectForKey:kLegacyWebStateListOpenerIndexKey];
[user_data removeObjectForKey:kLegacyWebStateListOpenerNavigationIndexKey];
} else {
[user_data setObject:@(opener_index)
forKey:kLegacyWebStateListOpenerIndexKey];
}
}
// Filters out session items that are considered invalid: either because they
// are empty (no navigation), or duplicates.
SessionWindowIOS* FilterInvalidTabs(SessionWindowIOS* session_window) {
DCHECK_LE(session_window.sessions.count, static_cast<NSUInteger>(INT_MAX));
const int sessions_count = static_cast<int>(session_window.sessions.count);
std::vector<int> items_to_drop;
std::set<web::WebStateID> seen_identifiers;
// Count the number of dropped tabs because they are duplicates, for
// reporting.
int duplicate_count = 0;
for (int index = 0; index < sessions_count; ++index) {
CRWSessionStorage* session = session_window.sessions[index];
if (session.itemStorages.count == 0) {
// Filter out session items that would be empty after restoration.
items_to_drop.push_back(index);
} else {
// Filter out session items that are duplicate (after something went bad
// somewhere).
if (seen_identifiers.contains(session.uniqueIdentifier)) {
items_to_drop.push_back(index);
duplicate_count++;
}
seen_identifiers.insert(session.uniqueIdentifier);
}
}
base::UmaHistogramCounts100("Tabs.DroppedDuplicatesCountOnSessionRestore",
duplicate_count);
// Nothing to do.
if (items_to_drop.empty()) {
return session_window;
}
// Compute the new value of selectedIndex before updating the opener-opened
// relationship, as OrderController take into account the closed WebStates.
const RemovingIndexes removing_indexes(std::move(items_to_drop));
const NSUInteger selected_index =
GetActiveIndex(session_window, removing_indexes);
// Create the new list of sessions, updating the opener-opened relationship
// to take into account the dropped CRWSessionStorage items.
NSMutableArray<CRWSessionStorage*>* sessions = [[NSMutableArray alloc] init];
for (int index = 0; index < sessions_count; ++index) {
if (removing_indexes.Contains(index)) {
continue;
}
CRWSessionStorage* session = session_window.sessions[index];
UpdateOpenerIndex(session.userData, removing_indexes);
[sessions addObject:session];
}
// Create the new list of tab groups, updating the `rangeStart` and
// `rangeCount` properties.
NSMutableArray<SessionTabGroup*>* groups = [[NSMutableArray alloc] init];
for (SessionTabGroup* group in session_window.tabGroups) {
const TabGroupRange initial_range(group.rangeStart, group.rangeCount);
const TabGroupRange final_range =
removing_indexes.RangeAfterRemoval(initial_range);
if (final_range.valid()) {
group.rangeStart = final_range.range_begin();
group.rangeCount = final_range.count();
[groups addObject:group];
}
}
return [[SessionWindowIOS alloc] initWithSessions:sessions
tabGroups:groups
selectedIndex:selected_index];
}
// Creates a WebState with `params` and `session_storage`.
std::unique_ptr<web::WebState> CreateWebState(
const web::WebState::CreateParams& params,
CRWSessionStorage* session_storage) {
__weak WebSessionStateCache* weak_cache =
WebSessionStateCacheFactory::GetForBrowserState(
ChromeBrowserState::FromBrowserState(params.browser_state.get()));
const web::WebStateID web_state_id = session_storage.uniqueIdentifier;
return web::WebState::CreateWithStorageSession(
params, session_storage, base::BindOnce(^{
return [weak_cache sessionStateDataForWebStateID:web_state_id];
}));
}
} // namespace
BROWSER_USER_DATA_KEY_IMPL(SessionRestorationBrowserAgent)
SessionRestorationBrowserAgent::SessionRestorationBrowserAgent(
Browser* browser,
SessionServiceIOS* session_service,
bool enable_pinned_web_states,
bool enable_tab_groups)
: session_service_(session_service),
browser_(browser),
session_window_ios_factory_([[SessionWindowIOSFactory alloc]
initWithWebStateList:browser_->GetWebStateList()]),
enable_pinned_web_states_(enable_pinned_web_states),
enable_tab_groups_(enable_tab_groups),
all_web_state_observer_(std::make_unique<AllWebStateObservationForwarder>(
browser_->GetWebStateList(),
this)) {
browser_->AddObserver(this);
browser_->GetWebStateList()->AddObserver(this);
}
SessionRestorationBrowserAgent::~SessionRestorationBrowserAgent() {
// Disconnect the session factory object as it's not garanteed that it will
// be released before it's referenced by the session service.
[session_window_ios_factory_ disconnect];
// If the object is destroyed before the Browser, unregister it from the
// ObserverList explicitly.
if (browser_) {
BrowserDestroyed(browser_);
}
}
void SessionRestorationBrowserAgent::SetSessionID(
NSString* session_identifier) {
DCHECK(session_identifier.length != 0);
session_identifier_ = session_identifier;
}
NSString* SessionRestorationBrowserAgent::GetSessionID() const {
DCHECK(session_identifier_.length != 0)
<< "SetSessionID must be called before GetSessionID";
return session_identifier_;
}
void SessionRestorationBrowserAgent::AddObserver(
SessionRestorationObserver* observer) {
observers_.AddObserver(observer);
}
void SessionRestorationBrowserAgent::RemoveObserver(
SessionRestorationObserver* observer) {
observers_.RemoveObserver(observer);
}
void SessionRestorationBrowserAgent::RestoreSessionWindow(
SessionWindowIOS* window) {
// Start the session restoration.
restoring_session_ = true;
for (auto& observer : observers_) {
observer.WillStartSessionRestoration(browser_);
}
// Restore the tabs (except the invalid ones).
const std::vector<web::WebState*> restored_web_states =
DeserializeWebStateList(
browser_->GetWebStateList(), FilterInvalidTabs(window),
enable_pinned_web_states_, enable_tab_groups_,
base::BindRepeating(
&CreateWebState,
web::WebState::CreateParams(browser_->GetBrowserState())));
for (auto& observer : observers_) {
observer.SessionRestorationFinished(browser_, restored_web_states);
}
// Session restoration is complete.
restoring_session_ = false;
// Schedule a session save.
SaveSession(/*immediately*/ false);
}
void SessionRestorationBrowserAgent::RestoreSession() {
DCHECK(session_identifier_.length != 0);
const base::TimeTicks start_time = base::TimeTicks::Now();
PreviousSessionInfo* session_info = [PreviousSessionInfo sharedInstance];
base::ScopedClosureRunner scoped_restore =
[session_info startSessionRestoration];
SessionWindowIOS* session_window = [session_service_
loadSessionWithSessionID:session_identifier_
directory:browser_->GetBrowserState()->GetStatePath()];
RestoreSessionWindow(session_window);
base::UmaHistogramTimes(kSessionHistogramLoadingTime,
base::TimeTicks::Now() - start_time);
}
bool SessionRestorationBrowserAgent::IsRestoringSession() {
return restoring_session_;
}
void SessionRestorationBrowserAgent::SaveSession(bool immediately) {
DCHECK(session_identifier_.length != 0);
if (!CanSaveSession())
return;
WebStateList* const web_state_list = browser_->GetWebStateList();
if (web_state_list->IsBatchInProgress()) {
save_after_batch_ = true;
save_immediately_ = save_immediately_ || immediately;
return;
}
[session_service_ saveSession:session_window_ios_factory_
sessionID:session_identifier_
directory:browser_->GetBrowserState()->GetStatePath()
immediately:immediately];
for (int i = 0; i < web_state_list->count(); ++i) {
web::WebState* web_state = web_state_list->GetWebStateAt(i);
if (WebSessionStateTabHelper* tab_helper =
WebSessionStateTabHelper::FromWebState(web_state)) {
tab_helper->SaveSessionStateIfStale();
}
}
}
bool SessionRestorationBrowserAgent::CanSaveSession() {
// Do not schedule a save while a session restoration is in progress.
if (restoring_session_) {
return false;
}
// A session requires an active Browser.
if (!browser_) {
return false;
}
// Sessions where there's no active tab shouldn't be saved, unless the web
// state list is empty. This is a transitional state.
WebStateList* const web_state_list = browser_->GetWebStateList();
if (!web_state_list->empty() && !web_state_list->GetActiveWebState()) {
return false;
}
return true;
}
#pragma mark - BrowserObserver
void SessionRestorationBrowserAgent::BrowserDestroyed(Browser* browser) {
DCHECK_EQ(browser, browser_);
// Stop observing web states.
all_web_state_observer_.reset();
// Stop observing web state list.
browser_->GetWebStateList()->RemoveObserver(this);
browser_->RemoveObserver(this);
browser_ = nullptr;
}
#pragma mark - WebStateListObserver
void SessionRestorationBrowserAgent::WebStateListWillChange(
WebStateList* web_state_list,
const WebStateListChangeDetach& detach_change,
const WebStateListStatus& status) {
DCHECK_EQ(browser_->GetWebStateList(), web_state_list);
if (web_state_list->active_index() == detach_change.detached_from_index()) {
return;
}
// Persist the session state if a background tab is detached.
SaveSession(/*immediately=*/false);
}
void SessionRestorationBrowserAgent::WebStateListDidChange(
WebStateList* web_state_list,
const WebStateListChange& change,
const WebStateListStatus& status) {
DCHECK_EQ(browser_->GetWebStateList(), web_state_list);
switch (change.type()) {
case WebStateListChange::Type::kStatusOnly:
// The activation is handled after this switch statement.
break;
case WebStateListChange::Type::kDetach: {
if (!web_state_list->empty()) {
break;
}
// Persist the session state after CloseAllWebStates. SaveSession will
// discard calls when the web_state_list is not empty and the active
// WebState is null, which is the order CloseAllWebStates uses.
SaveSession(/*immediately=*/false);
break;
}
case WebStateListChange::Type::kMove: {
const WebStateListChangeMove& move_change =
change.As<WebStateListChangeMove>();
if (move_change.moved_web_state()->IsLoading()) {
break;
}
// Persist the session state if the new web state is not loading.
SaveSession(/*immediately=*/false);
break;
}
case WebStateListChange::Type::kReplace: {
const WebStateListChangeReplace& replace_change =
change.As<WebStateListChangeReplace>();
if (replace_change.inserted_web_state()->IsLoading()) {
break;
}
// Persist the session state if the new web state is not loading.
SaveSession(/*immediately=*/false);
break;
}
case WebStateListChange::Type::kInsert: {
const WebStateListChangeInsert& insert_change =
change.As<WebStateListChangeInsert>();
if (status.active_web_state_change() ||
insert_change.inserted_web_state()->IsLoading()) {
break;
}
// Persist the session state if the new web state is not loading.
SaveSession(/*immediately=*/false);
break;
}
case WebStateListChange::Type::kGroupCreate:
// Persist the session state.
SaveSession(/*immediately=*/false);
break;
case WebStateListChange::Type::kGroupVisualDataUpdate:
// Persist the session state.
SaveSession(/*immediately=*/false);
break;
case WebStateListChange::Type::kGroupMove:
// Persist the session state.
SaveSession(/*immediately=*/false);
break;
case WebStateListChange::Type::kGroupDelete:
// Persist the session state.
SaveSession(/*immediately=*/false);
break;
}
if (status.active_web_state_change()) {
if (status.new_active_web_state &&
status.new_active_web_state->IsLoading()) {
return;
}
// Persist the session state if the new web state is not loading (or if
// the last tab was closed).
SaveSession(/*immediately=*/false);
}
}
void SessionRestorationBrowserAgent::WillBeginBatchOperation(
WebStateList* web_state_list) {
save_after_batch_ = false;
save_immediately_ = false;
}
void SessionRestorationBrowserAgent::BatchOperationEnded(
WebStateList* web_state_list) {
if (save_after_batch_) {
SaveSession(save_immediately_);
save_after_batch_ = false;
save_immediately_ = false;
}
}
#pragma mark - WebStateObserver
void SessionRestorationBrowserAgent::DidFinishNavigation(
web::WebState* web_state,
web::NavigationContext* navigation_context) {
// Save the session each time a navigation finishes.
SaveSession(/*immediately=*/false);
}