// Copyright 2021 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/web_session_state_tab_helper.h"
#import "base/apple/foundation_util.h"
#import "base/files/file_path.h"
#import "base/files/file_util.h"
#import "base/logging.h"
#import "base/memory/ptr_util.h"
#import "base/metrics/histogram_macros.h"
#import "base/path_service.h"
#import "base/strings/string_util.h"
#import "base/task/sequenced_task_runner.h"
#import "base/threading/thread_restrictions.h"
#import "build/branding_buildflags.h"
#import "components/strings/grit/components_strings.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/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/shared/model/url/chrome_url_constants.h"
#import "ios/web/public/js_messaging/web_frame.h"
#import "ios/web/public/navigation/navigation_context.h"
#import "ios/web/public/navigation/navigation_manager.h"
#import "ios/web/public/session/serializable_user_data_manager.h"
#import "ios/web/public/ui/crw_web_view_proxy.h"
#import "ios/web/public/ui/crw_web_view_scroll_view_proxy.h"
#import "ios/web/public/web_client.h"
#import "ios/web/public/web_state.h"
#import "ui/base/l10n/l10n_util.h"
namespace {
// Maximum size of session state NSData object in kilobyes.
const int64_t kMaxSessionState = 1024 * 5; // 5MB
} // anonymous namespace
// Observes scroll and zoom events and executes LoggingBlock.
@interface WebSessionStateScrollingObserver
: NSObject <CRWWebViewScrollViewProxyObserver>
- (instancetype)initWithClosure:(base::RepeatingClosure)loggingClosure;
@end
@implementation WebSessionStateScrollingObserver {
base::RepeatingClosure callback_;
}
- (instancetype)initWithClosure:(base::RepeatingClosure)closure {
if ((self = [super init])) {
callback_ = std::move(closure);
}
return self;
}
- (void)webViewScrollViewDidEndDragging:
(CRWWebViewScrollViewProxy*)webViewScrollViewProxy
willDecelerate:(BOOL)decelerate {
callback_.Run();
}
- (void)webViewScrollViewDidEndZooming:
(CRWWebViewScrollViewProxy*)webViewScrollViewProxy
atScale:(CGFloat)scale {
callback_.Run();
}
@end
WebSessionStateTabHelper::WebSessionStateTabHelper(web::WebState* web_state)
: web_state_(web_state) {
web_state_->AddObserver(this);
web_state_->GetPageWorldWebFramesManager()->AddObserver(this);
if (web_state_->IsRealized()) {
CreateScrollingObserver();
}
}
WebSessionStateTabHelper::~WebSessionStateTabHelper() = default;
ChromeBrowserState* WebSessionStateTabHelper::GetBrowserState() {
return ChromeBrowserState::FromBrowserState(web_state_->GetBrowserState());
}
NSData* WebSessionStateTabHelper::FetchSessionFromCache() {
WebSessionStateCache* cache =
WebSessionStateCacheFactory::GetForBrowserState(GetBrowserState());
NSData* data =
[cache sessionStateDataForWebStateID:web_state_->GetUniqueIdentifier()];
return data.length ? data : nil;
}
void WebSessionStateTabHelper::SaveSessionStateIfStale() {
if (!stale_)
return;
SaveSessionState();
}
void WebSessionStateTabHelper::SaveSessionState() {
stale_ = false;
NSData* data = web_state_->SessionStateData();
if (data) {
int64_t size_kb = data.length / 1024;
UMA_HISTOGRAM_COUNTS_100000("Session.WebState.CustomWebViewSerializedSize",
size_kb);
WebSessionStateCache* cache =
WebSessionStateCacheFactory::GetForBrowserState(GetBrowserState());
// To prevent very large session states from using too much space, don't
// persist any `data` larger than 5MB. If this happens, remove the now
// stale session state data.
if (size_kb > kMaxSessionState) {
[cache
removeSessionStateDataForWebStateID:web_state_->GetUniqueIdentifier()
incognito:GetBrowserState()
->IsOffTheRecord()];
return;
}
[cache persistSessionStateData:data
forWebStateID:web_state_->GetUniqueIdentifier()];
}
}
#pragma mark - WebStateObserver
void WebSessionStateTabHelper::WebStateDestroyed(web::WebState* web_state) {
web_state->RemoveObserver(this);
web_state_->GetPageWorldWebFramesManager()->RemoveObserver(this);
if (scroll_observer_) {
[web_state->GetWebViewProxy().scrollViewProxy
removeObserver:scroll_observer_];
scroll_observer_ = nil;
}
if (stale_) {
SaveSessionState();
}
}
void WebSessionStateTabHelper::DidFinishNavigation(
web::WebState* web_state,
web::NavigationContext* navigation_context) {
// Don't record navigations that result in downloads, since these will be
// discarded and there's no simple callback when discarded.
if (navigation_context->IsDownload())
return;
MarkStale();
}
void WebSessionStateTabHelper::WebFrameBecameAvailable(
web::WebFramesManager* web_frames_manager,
web::WebFrame* web_frame) {
if (web_frame->IsMainFrame())
return;
// -WebFrameBecameAvailable is called much more often than navigations, so
// check if either `item_count_` or `last_committed_item_index_` has changed
// before marking a page as stale.
web::NavigationManager* navigation_manager =
web_state_->GetNavigationManager();
if (item_count_ == web_state_->GetNavigationItemCount() &&
last_committed_item_index_ ==
navigation_manager->GetLastCommittedItemIndex()) {
return;
}
MarkStale();
}
void WebSessionStateTabHelper::WebStateRealized(web::WebState* web_state) {
CreateScrollingObserver();
}
#pragma mark - Private
void WebSessionStateTabHelper::CreateScrollingObserver() {
base::RepeatingClosure closure = base::BindRepeating(
&WebSessionStateTabHelper::OnScrollEvent, weak_ptr_factory_.GetWeakPtr());
DCHECK(!scroll_observer_);
scroll_observer_ =
[[WebSessionStateScrollingObserver alloc] initWithClosure:closure];
[web_state_->GetWebViewProxy().scrollViewProxy addObserver:scroll_observer_];
}
void WebSessionStateTabHelper::OnScrollEvent() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
stale_ = true;
}
void WebSessionStateTabHelper::MarkStale() {
web::NavigationManager* navigationManager =
web_state_->GetNavigationManager();
item_count_ = web_state_->GetNavigationItemCount();
last_committed_item_index_ = navigationManager->GetLastCommittedItemIndex();
stale_ = true;
}
WEB_STATE_USER_DATA_KEY_IMPL(WebSessionStateTabHelper)