blob: bd49bd0695ced38ac4a5b18185347bc12a4c7e29 [file] [log] [blame]
// 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/session_restoration_browser_agent.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/favicon/ios/web_favicon_driver.h"
#import "components/previous_session_info/previous_session_info.h"
#import "ios/chrome/browser/sessions/session_ios.h"
#import "ios/chrome/browser/sessions/session_ios_factory.h"
#import "ios/chrome/browser/sessions/session_restoration_observer.h"
#import "ios/chrome/browser/sessions/session_service_ios.h"
#import "ios/chrome/browser/sessions/session_window_ios.h"
#import "ios/chrome/browser/shared/model/browser/browser.h"
#import "ios/chrome/browser/shared/model/browser_state/chrome_browser_state.h"
#import "ios/chrome/browser/shared/model/url/chrome_url_constants.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/web_state_list.h"
#import "ios/chrome/browser/web/features.h"
#import "ios/chrome/browser/web/page_placeholder_tab_helper.h"
#import "ios/chrome/browser/web/session_state/web_session_state_tab_helper.h"
#import "ios/chrome/browser/web_state_list/web_state_list_serialization.h"
#import "ios/chrome/browser/web_state_list/web_usage_enabler/web_usage_enabler_browser_agent.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/web_state.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
BROWSER_USER_DATA_KEY_IMPL(SessionRestorationBrowserAgent)
SessionRestorationBrowserAgent::SessionRestorationBrowserAgent(
Browser* browser,
SessionServiceIOS* session_service,
bool enable_pinned_web_states)
: session_service_(session_service),
web_state_list_(browser->GetWebStateList()),
web_enabler_(WebUsageEnablerBrowserAgent::FromBrowser(browser)),
browser_state_(browser->GetBrowserState()),
session_ios_factory_(
[[SessionIOSFactory alloc] initWithWebStateList:web_state_list_]),
enable_pinned_web_states_(enable_pinned_web_states),
all_web_state_observer_(
std::make_unique<AllWebStateObservationForwarder>(web_state_list_,
this)) {
browser->AddObserver(this);
web_state_list_->AddObserver(this);
}
SessionRestorationBrowserAgent::~SessionRestorationBrowserAgent() {
// Disconnect the session factory object as it's not granteed that it will be
// released before it's referenced by the session service.
[session_ios_factory_ disconnect];
}
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);
}
bool SessionRestorationBrowserAgent::RestoreSessionWindow(
SessionWindowIOS* window,
SessionRestorationScope scope) {
// Start the session restoration.
restoring_session_ = true;
for (auto& observer : observers_) {
observer.WillStartSessionRestoration();
}
const int old_count = web_state_list_->count();
const int old_first_non_pinned =
web_state_list_->GetIndexOfFirstNonPinnedWebState();
DCHECK_GE(old_count, 0);
web_state_list_->PerformBatchOperation(
base::BindOnce(^(WebStateList* web_state_list) {
web::WebState::CreateParams create_params(browser_state_);
DeserializeWebStateList(
web_state_list, window, scope, enable_pinned_web_states_,
base::BindRepeating(&web::WebState::CreateWithStorageSession,
create_params));
}));
DCHECK_GE(web_state_list_->count(), old_count);
int restored_count = web_state_list_->count() - old_count;
int restored_pinned_count =
web_state_list_->GetIndexOfFirstNonPinnedWebState() -
old_first_non_pinned;
NSArray<CRWSessionStorage*>* restored_session_storages =
GetRestoredSessionStoragesForScope(scope, window.sessions,
restored_count);
DCHECK_EQ(restored_session_storages.count,
static_cast<NSUInteger>(restored_count));
std::vector<web::WebState*> restored_web_states;
restored_web_states.reserve(restored_count);
std::vector<web::WebState*> web_states_to_remove;
web_states_to_remove.reserve(restored_count);
// Find restored pinned WebStates.
for (int index = old_first_non_pinned;
index < web_state_list_->GetIndexOfFirstNonPinnedWebState(); ++index) {
web::WebState* web_state = web_state_list_->GetWebStateAt(index);
const int session_index = index - old_first_non_pinned;
DCHECK_EQ(restored_session_storages[session_index].uniqueIdentifier,
web_state->GetUniqueIdentifier());
if (restored_session_storages[session_index].itemStorages.count > 0) {
restored_web_states.push_back(web_state);
} else {
web_states_to_remove.push_back(web_state);
}
}
// Find restored non-pinned WebStates.
for (int index = old_count + restored_pinned_count;
index < web_state_list_->count(); ++index) {
web::WebState* web_state = web_state_list_->GetWebStateAt(index);
const int session_index = index - old_count;
DCHECK_EQ(restored_session_storages[session_index].uniqueIdentifier,
web_state->GetUniqueIdentifier());
if (restored_session_storages[session_index].itemStorages.count > 0) {
restored_web_states.push_back(web_state);
} else {
web_states_to_remove.push_back(web_state);
}
}
// Do not count WebState that are going to be removed.
restored_count -= web_states_to_remove.size();
DCHECK_EQ(restored_web_states.size(),
static_cast<unsigned long>(restored_count));
// Iterating backwards to avoid messing up the indexes.
for (int index = restored_count - 1; index >= 0; --index) {
web::WebState* web_state = restored_web_states[index];
const GURL& visible_url = web_state->GetVisibleURL();
if (visible_url != kChromeUINewTabURL) {
PagePlaceholderTabHelper::FromWebState(web_state)
->AddPlaceholderForNextNavigation();
}
if (visible_url.is_valid()) {
favicon::WebFaviconDriver::FromWebState(web_state)->FetchFavicon(
visible_url, /*is_same_document=*/false);
}
}
for (web::WebState* web_state_to_remove : web_states_to_remove) {
const int index = web_state_list_->GetIndexOfWebState(web_state_to_remove);
DCHECK(index != WebStateList::kInvalidIndex);
web_state_list_->CloseWebStateAt(index, WebStateList::CLOSE_NO_FLAGS);
}
// If there was only one tab and it was the new tab page, clobber it.
bool closed_ntp_tab = false;
if (old_count == 1) {
web::WebState* web_state = web_state_list_->GetWebStateAt(0);
// An "unrealized" WebState has no pending load. Checking for realization
// before accessing the NavigationManager prevents accidental realization
// of the WebState.
const bool has_pending_load =
web_state->IsRealized() &&
web_state->GetNavigationManager()->GetPendingItem() != nullptr;
if (!has_pending_load &&
(web_state->GetLastCommittedURL() == kChromeUINewTabURL)) {
web_state_list_->CloseWebStateAt(0, WebStateList::CLOSE_USER_ACTION);
closed_ntp_tab = true;
}
}
for (auto& observer : observers_) {
observer.SessionRestorationFinished(restored_web_states);
}
// Session restoration is complete.
restoring_session_ = false;
// Schedule a session save.
SaveSession(/*immediately*/ false);
return closed_ntp_tab;
}
bool 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];
SessionIOS* session = [session_service_
loadSessionWithSessionID:session_identifier_
directory:browser_state_->GetStatePath()];
SessionWindowIOS* session_window = nil;
if (session) {
DCHECK_EQ(session.sessionWindows.count, 1u);
session_window = session.sessionWindows[0];
}
const bool closed_ntp_tab =
RestoreSessionWindow(session_window, SessionRestorationScope::kAll);
base::UmaHistogramTimes("Session.WebStates.LoadingTimeOnMainThread",
base::TimeTicks::Now() - start_time);
return closed_ntp_tab;
}
bool SessionRestorationBrowserAgent::IsRestoringSession() {
return restoring_session_;
}
void SessionRestorationBrowserAgent::SaveSession(bool immediately) {
DCHECK(session_identifier_.length != 0);
if (!CanSaveSession())
return;
[session_service_ saveSession:session_ios_factory_
sessionID:session_identifier_
directory:browser_state_->GetStatePath()
immediately:immediately];
if (web::UseNativeSessionRestorationCache()) {
for (int i = 0; i < web_state_list_->count(); ++i) {
web::WebState* web_state = web_state_list_->GetWebStateAt(i);
WebSessionStateTabHelper::FromWebState(web_state)
->SaveSessionStateIfStale();
}
}
}
NSArray<CRWSessionStorage*>*
SessionRestorationBrowserAgent::GetRestoredSessionStoragesForScope(
SessionRestorationScope scope,
NSArray<CRWSessionStorage*>* session_storages,
int restored_count) {
NSRange restored_sessions_range;
switch (scope) {
case SessionRestorationScope::kPinnedOnly:
case SessionRestorationScope::kAll:
restored_sessions_range = NSMakeRange(0, restored_count);
break;
case SessionRestorationScope::kRegularOnly:
restored_sessions_range =
NSMakeRange(session_storages.count - restored_count, restored_count);
break;
}
return [session_storages subarrayWithRange:restored_sessions_range];
}
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 state and web state list.
if (!browser_state_ || !web_state_list_) {
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.
if (!web_state_list_->empty() && !web_state_list_->GetActiveWebState()) {
return false;
}
return true;
}
#pragma mark - BrowserObserver
void SessionRestorationBrowserAgent::BrowserDestroyed(Browser* browser) {
DCHECK_EQ(browser->GetWebStateList(), web_state_list_);
// Stop observing web states.
all_web_state_observer_.reset();
// Stop observing web state list.
browser->GetWebStateList()->RemoveObserver(this);
browser->RemoveObserver(this);
}
#pragma mark - WebStateListObserver
void SessionRestorationBrowserAgent::WebStateActivatedAt(
WebStateList* web_state_list,
web::WebState* old_web_state,
web::WebState* new_web_state,
int active_index,
ActiveWebStateChangeReason reason) {
if (new_web_state && new_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::WebStateListChanged(
WebStateList* web_state_list,
const WebStateListChange& change,
const WebStateSelection& selection) {
switch (change.type()) {
case WebStateListChange::Type::kSelectionOnly:
// TODO(crbug.com/1442546): Move the implementation from
// WebStateActivatedAt() to here. Note that here is reachable only when
// `reason` == ActiveWebStateChangeReason::Activated.
break;
case WebStateListChange::Type::kDetach:
// TODO(crbug.com/1442546): Move the implementation from
// WebStateDetachedAt() to here.
break;
case WebStateListChange::Type::kMove: {
const WebStateListChangeMove& move_change =
change.As<WebStateListChangeMove>();
if (move_change.moved_web_state()->IsLoading()) {
return;
}
// 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()) {
return;
}
// 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 (selection.activating ||
insert_change.inserted_web_state()->IsLoading()) {
return;
}
// Persist the session state if the new web state is not loading.
SaveSession(/*immediately=*/false);
break;
}
}
}
void SessionRestorationBrowserAgent::WillDetachWebStateAt(
WebStateList* web_state_list,
web::WebState* web_state,
int index) {
if (web_state_list->active_index() == index)
return;
// Persist the session state if a background tab is detached.
SaveSession(/*immediately=*/false);
}
void SessionRestorationBrowserAgent::WebStateDetachedAt(
WebStateList* web_state_list,
web::WebState* web_state,
int index) {
if (!web_state_list_->empty())
return;
// 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);
}
// WebStateObserver methods
void SessionRestorationBrowserAgent::DidFinishNavigation(
web::WebState* web_state,
web::NavigationContext* navigation_context) {
// Save the session each time a navigation finishes.
SaveSession(/*immediately=*/false);
}