blob: 5d4f52f03a2e460a2c9761660dab144a3643f09e [file] [log] [blame]
// Copyright 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import <UIKit/UIKit.h>
#include "ios/chrome/browser/prerender/preload_controller.h"
#include "base/ios/device_util.h"
#include "base/logging.h"
#include "base/metrics/field_trial.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/sys_string_conversions.h"
#import "components/prefs/ios/pref_observer_bridge.h"
#include "components/prefs/pref_service.h"
#import "components/signin/ios/browser/account_consistency_service.h"
#import "ios/chrome/browser/app_launcher/app_launcher_tab_helper.h"
#include "ios/chrome/browser/browser_state/chrome_browser_state.h"
#include "ios/chrome/browser/crash_report/crash_report_helper.h"
#import "ios/chrome/browser/geolocation/omnibox_geolocation_controller.h"
#import "ios/chrome/browser/history/history_tab_helper.h"
#import "ios/chrome/browser/itunes_urls/itunes_urls_handler_tab_helper.h"
#include "ios/chrome/browser/pref_names.h"
#include "ios/chrome/browser/prerender/preload_controller_delegate.h"
#import "ios/chrome/browser/signin/account_consistency_service_factory.h"
#import "ios/chrome/browser/tabs/tab_helper_util.h"
#import "ios/web/public/deprecated/crw_native_content.h"
#import "ios/web/public/deprecated/crw_native_content_holder.h"
#import "ios/web/public/deprecated/crw_web_controller_util.h"
#import "ios/web/public/navigation/navigation_item.h"
#import "ios/web/public/navigation/navigation_manager.h"
#import "ios/web/public/navigation/web_state_policy_decider_bridge.h"
#include "ios/web/public/thread/web_thread.h"
#import "ios/web/public/web_state.h"
#include "ios/web/public/web_state/web_state_observer_bridge.h"
#import "ios/web/web_state/ui/crw_web_controller.h"
#import "net/base/mac/url_conversions.h"
#include "ui/base/page_transition_types.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
using web::WebStatePolicyDecider;
namespace {
// PrerenderFinalStatus values are used in the "Prerender.FinalStatus" histogram
// and new values needs to be kept in sync with histogram.xml.
enum PrerenderFinalStatus {
PRERENDER_FINAL_STATUS_USED = 0,
PRERENDER_FINAL_STATUS_MEMORY_LIMIT_EXCEEDED = 12,
PRERENDER_FINAL_STATUS_CANCELLED = 32,
PRERENDER_FINAL_STATUS_MAX = 52,
};
// Delay before starting to prerender a URL.
const NSTimeInterval kPrerenderDelay = 0.5;
// The finch experiment to turn off prerendering as a field trial.
const char kTabEvictionFieldTrialName[] = "TabEviction";
// The associated group.
const char kPrerenderTabEvictionTrialGroup[] = "NoPrerendering";
// The name of the histogram for recording final status (e.g. used/cancelled)
// of prerender requests.
const char kPrerenderFinalStatusHistogramName[] = "Prerender.FinalStatus";
// The name of the histogram for recording the number of successful prerenders.
const char kPrerendersPerSessionCountHistogramName[] =
"Prerender.PrerendersPerSessionCount";
// The name of the histogram for recording time until a successful prerender.
const char kPrerenderStartToReleaseContentsTime[] =
"Prerender.PrerenderStartToReleaseContentsTime";
// Is this install selected for this particular experiment.
bool IsPrerenderTabEvictionExperimentalGroup() {
base::FieldTrial* trial =
base::FieldTrialList::Find(kTabEvictionFieldTrialName);
return trial && trial->group_name() == kPrerenderTabEvictionTrialGroup;
}
} // namespace
@interface PreloadController (PrivateMethods)
// Returns YES if prerendering is enabled.
- (BOOL)isPrerenderingEnabled;
// Returns YES if the |url| is valid for prerendering.
- (BOOL)shouldPreloadURL:(const GURL&)url;
// Called to start any scheduled prerendering requests.
- (void)startPrerender;
// Destroys the preview Tab and resets |prerenderURL_| to the empty URL.
- (void)destroyPreviewContents;
// Schedules the current prerender to be cancelled during the next run of the
// event loop.
- (void)schedulePrerenderCancel;
// Removes any scheduled prerender requests and resets |scheduledURL| to the
// empty URL.
- (void)removeScheduledPrerenderRequests;
// Records metric on a successful prerender.
- (void)recordReleaseMetrics;
@end
@interface PreloadController ()<CRWWebStateObserver,
CRWWebStatePolicyDecider,
ManageAccountsDelegate,
PrefObserverDelegate>
@end
@implementation PreloadController {
ios::ChromeBrowserState* browserState_; // Weak.
// The WebState used for prerendering.
std::unique_ptr<web::WebState> webState_;
// The WebStateDelegateBridge used to register self as a CRWWebStateDelegate
// with the pre-rendered WebState.
std::unique_ptr<web::WebStateDelegateBridge> webStateDelegate_;
// The WebStateObserverBridge used to register self as a WebStateObserver
// with the pre-rendered WebState.
std::unique_ptr<web::WebStateObserverBridge> webStateObserver_;
// The URL that is prerendered in |webState_|. This can be different from
// the value returned by WebState last committed navigation item, for example
// in cases where there was a redirect.
//
// When choosing whether or not to use a prerendered Tab,
// BrowserViewController compares the URL being loaded by the omnibox with the
// URL of the prerendered Tab. Comparing against the Tab's currently URL
// could return false negatives in cases of redirect, hence the need to store
// the originally prerendered URL.
GURL prerenderedURL_;
// The URL that is scheduled to be prerendered, its associated transition and
// referrer. |scheduledTransition_| and |scheduledReferrer_| are not valid
// when |scheduledURL_| is empty.
GURL scheduledURL_;
ui::PageTransition scheduledTransition_;
web::Referrer scheduledReferrer_;
// Bridge to listen to pref changes.
std::unique_ptr<PrefObserverBridge> observerBridge_;
// Registrar for pref changes notifications.
PrefChangeRegistrar prefChangeRegistrar_;
// Observer for the WWAN setting. Contains a valid object only if the
// instant setting is set to wifi-only.
std::unique_ptr<ConnectionTypeObserverBridge> connectionTypeObserverBridge_;
// Whether or not the preference is enabled.
BOOL enabled_;
// Whether or not prerendering is only when on wifi.
BOOL wifiOnly_;
// Whether or not the current connection is using WWAN.
BOOL usingWWAN_;
// Number of successful prerenders (i.e. the user viewed the prerendered page)
// during the lifetime of this controller.
int successfulPrerendersPerSessionCount_;
// Tracks the last time of the last attempt to load a |prerenderedURL_|. Used
// for UMA reporting of load durations.
base::TimeTicks startTime_;
// Bridge to provide navigation policies for |webState_|.
std::unique_ptr<web::WebStatePolicyDeciderBridge> policyDeciderBridge_;
}
@synthesize prerenderedURL = prerenderedURL_;
@synthesize delegate = delegate_;
- (instancetype)initWithBrowserState:(ios::ChromeBrowserState*)browserState {
DCHECK(browserState);
DCHECK_CURRENTLY_ON(web::WebThread::UI);
if ((self = [super init])) {
browserState_ = browserState;
enabled_ =
browserState_->GetPrefs()->GetBoolean(prefs::kNetworkPredictionEnabled);
wifiOnly_ = browserState_->GetPrefs()->GetBoolean(
prefs::kNetworkPredictionWifiOnly);
usingWWAN_ = net::NetworkChangeNotifier::IsConnectionCellular(
net::NetworkChangeNotifier::GetConnectionType());
webStateDelegate_ = std::make_unique<web::WebStateDelegateBridge>(self);
webStateObserver_ = std::make_unique<web::WebStateObserverBridge>(self);
observerBridge_ = std::make_unique<PrefObserverBridge>(self);
prefChangeRegistrar_.Init(browserState_->GetPrefs());
observerBridge_->ObserveChangesForPreference(
prefs::kNetworkPredictionEnabled, &prefChangeRegistrar_);
observerBridge_->ObserveChangesForPreference(
prefs::kNetworkPredictionWifiOnly, &prefChangeRegistrar_);
if (enabled_ && wifiOnly_) {
connectionTypeObserverBridge_ =
std::make_unique<ConnectionTypeObserverBridge>(self);
}
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(didReceiveMemoryWarning)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
}
return self;
}
- (void)browserStateDestroyed {
[self cancelPrerender];
connectionTypeObserverBridge_.reset();
}
- (void)dealloc {
UMA_HISTOGRAM_COUNTS_1M(kPrerendersPerSessionCountHistogramName,
successfulPrerendersPerSessionCount_);
[self cancelPrerender];
}
- (void)prerenderURL:(const GURL&)url
referrer:(const web::Referrer&)referrer
transition:(ui::PageTransition)transition
immediately:(BOOL)immediately {
// TODO(crbug.com/754050): If shouldPrerenderURL returns false, should we
// cancel any scheduled prerender requests?
if (![self isPrerenderingEnabled] || ![self shouldPreloadURL:url])
return;
// Ignore this request if there is already a scheduled request for the same
// URL; or, if there is no scheduled request, but the currently prerendered
// page matches this URL.
if (url == scheduledURL_ ||
(scheduledURL_.is_empty() && url == prerenderedURL_)) {
return;
}
[self removeScheduledPrerenderRequests];
scheduledURL_ = url;
scheduledTransition_ = transition;
scheduledReferrer_ = referrer;
NSTimeInterval delay = immediately ? 0.0 : kPrerenderDelay;
[self performSelector:@selector(startPrerender)
withObject:nil
afterDelay:delay];
}
- (void)cancelPrerender {
[self cancelPrerenderForReason:PRERENDER_FINAL_STATUS_CANCELLED];
}
- (void)cancelPrerenderForReason:(PrerenderFinalStatus)reason {
[self removeScheduledPrerenderRequests];
[self destroyPreviewContentsForReason:reason];
}
- (BOOL)isWebStatePrerendered:(web::WebState*)webState {
return webState && webState_.get() == webState;
}
- (std::unique_ptr<web::WebState>)releasePrerenderContents {
successfulPrerendersPerSessionCount_++;
[self recordReleaseMetrics];
[self removeScheduledPrerenderRequests];
prerenderedURL_ = GURL();
startTime_ = base::TimeTicks();
if (!webState_)
return nullptr;
// Move the pre-rendered WebState to a local variable so that it will no
// longer be considered as pre-rendering (otherwise tab helpers may early
// exist when invoked).
std::unique_ptr<web::WebState> webState = std::move(webState_);
DCHECK(![self isWebStatePrerendered:webState.get()]);
web_deprecated::SetNativeProvider(webState.get(), nil);
webState->RemoveObserver(webStateObserver_.get());
breakpad::StopMonitoringURLsForWebState(webState.get());
webState->SetDelegate(nullptr);
policyDeciderBridge_.reset();
HistoryTabHelper::FromWebState(webState.get())
->SetDelayHistoryServiceNotification(false);
if (AccountConsistencyService* accountConsistencyService =
ios::AccountConsistencyServiceFactory::GetForBrowserState(
browserState_)) {
accountConsistencyService->RemoveWebStateHandler(webState.get());
}
if (!webState->IsLoading()) {
[[OmniboxGeolocationController sharedInstance]
finishPageLoadForWebState:webState.get()
loadSuccess:YES];
}
return webState;
}
- (void)connectionTypeChanged:(net::NetworkChangeNotifier::ConnectionType)type {
DCHECK_CURRENTLY_ON(web::WebThread::UI);
usingWWAN_ = net::NetworkChangeNotifier::IsConnectionCellular(type);
if (wifiOnly_ && usingWWAN_)
[self cancelPrerender];
}
- (void)onPreferenceChanged:(const std::string&)preferenceName {
if (preferenceName == prefs::kNetworkPredictionEnabled ||
preferenceName == prefs::kNetworkPredictionWifiOnly) {
DCHECK_CURRENTLY_ON(web::WebThread::UI);
// The logic is simpler if both preferences changes are handled equally.
enabled_ =
browserState_->GetPrefs()->GetBoolean(prefs::kNetworkPredictionEnabled);
wifiOnly_ = browserState_->GetPrefs()->GetBoolean(
prefs::kNetworkPredictionWifiOnly);
if (wifiOnly_ && enabled_) {
if (!connectionTypeObserverBridge_.get()) {
usingWWAN_ = net::NetworkChangeNotifier::IsConnectionCellular(
net::NetworkChangeNotifier::GetConnectionType());
connectionTypeObserverBridge_.reset(
new ConnectionTypeObserverBridge(self));
}
if (usingWWAN_) {
[self cancelPrerender];
}
} else if (enabled_) {
connectionTypeObserverBridge_.reset();
} else {
[self cancelPrerender];
connectionTypeObserverBridge_.reset();
}
}
}
- (void)didReceiveMemoryWarning {
[self cancelPrerenderForReason:PRERENDER_FINAL_STATUS_MEMORY_LIMIT_EXCEEDED];
}
#pragma mark -
#pragma mark CRWNativeContentProvider implementation
- (BOOL)hasControllerForURL:(const GURL&)url {
if (!webState_)
return NO;
return [delegate_ preloadHasNativeControllerForURL:url];
}
// Override the CRWNativeContentProvider methods to cancel any prerenders that
// require native content.
- (id<CRWNativeContent>)controllerForURL:(const GURL&)url
webState:(web::WebState*)webState {
[self schedulePrerenderCancel];
return nil;
}
- (UIEdgeInsets)nativeContentInsetForWebState:(web::WebState*)webState {
// |-controllerForURL:webState:| short-circuits the native controller
// presentation flow, so the insets are never used.
return UIEdgeInsetsZero;
}
#pragma mark -
#pragma mark Private Methods
- (BOOL)isPrerenderingEnabled {
DCHECK_CURRENTLY_ON(web::WebThread::UI);
return !IsPrerenderTabEvictionExperimentalGroup() && enabled_ &&
!ios::device_util::IsSingleCoreDevice() &&
ios::device_util::RamIsAtLeast512Mb() && (!wifiOnly_ || !usingWWAN_);
}
- (BOOL)shouldPreloadURL:(const GURL&)url {
return url.SchemeIs(url::kHttpScheme) || url.SchemeIs(url::kHttpsScheme);
}
- (void)startPrerender {
// Destroy any existing prerenders before starting a new one.
[self destroyPreviewContents];
prerenderedURL_ = scheduledURL_;
scheduledURL_ = GURL();
DCHECK(prerenderedURL_.is_valid());
if (!prerenderedURL_.is_valid()) {
[self destroyPreviewContents];
return;
}
web::WebState::CreateParams createParams(browserState_);
webState_ = web::WebState::Create(createParams);
// Add the preload controller as a policyDecider before other tab helpers, so
// that it can block the navigation if needed before other policy deciders
// execute thier side effects (eg. AppLauncherTabHelper launching app).
policyDeciderBridge_ =
std::make_unique<web::WebStatePolicyDeciderBridge>(webState_.get(), self);
AttachTabHelpers(webState_.get(), /*for_prerender=*/true);
web_deprecated::SetNativeProvider(webState_.get(), nil);
webState_->SetDelegate(webStateDelegate_.get());
webState_->AddObserver(webStateObserver_.get());
breakpad::MonitorURLsForWebState(webState_.get());
webState_->SetWebUsageEnabled(true);
if (AccountConsistencyService* accountConsistencyService =
ios::AccountConsistencyServiceFactory::GetForBrowserState(
browserState_)) {
accountConsistencyService->SetWebStateHandler(webState_.get(), self);
}
HistoryTabHelper::FromWebState(webState_.get())
->SetDelayHistoryServiceNotification(true);
web::NavigationManager::WebLoadParams loadParams(prerenderedURL_);
loadParams.referrer = scheduledReferrer_;
loadParams.transition_type = scheduledTransition_;
if ([delegate_ preloadShouldUseDesktopUserAgent]) {
loadParams.user_agent_override_option =
web::NavigationManager::UserAgentOverrideOption::DESKTOP;
}
webState_->SetKeepRenderProcessAlive(true);
webState_->GetNavigationManager()->LoadURLWithParams(loadParams);
// LoadIfNecessary is needed because the view is not created (but needed) when
// loading the page. TODO(crbug.com/705819): Remove this call.
webState_->GetNavigationManager()->LoadIfNecessary();
startTime_ = base::TimeTicks::Now();
}
- (void)destroyPreviewContents {
[self destroyPreviewContentsForReason:PRERENDER_FINAL_STATUS_CANCELLED];
}
- (void)destroyPreviewContentsForReason:(PrerenderFinalStatus)reason {
if (!webState_)
return;
UMA_HISTOGRAM_ENUMERATION(kPrerenderFinalStatusHistogramName, reason,
PRERENDER_FINAL_STATUS_MAX);
web_deprecated::SetNativeProvider(webState_.get(), nil);
webState_->RemoveObserver(webStateObserver_.get());
breakpad::StopMonitoringURLsForWebState(webState_.get());
webState_->SetDelegate(nullptr);
webState_.reset();
prerenderedURL_ = GURL();
startTime_ = base::TimeTicks();
}
- (void)schedulePrerenderCancel {
// TODO(crbug.com/228550): Instead of cancelling the prerender, should we mark
// it as failed instead? That way, subsequent prerender requests for the same
// URL will not kick off new prerenders.
[self removeScheduledPrerenderRequests];
[self performSelector:@selector(cancelPrerender) withObject:nil afterDelay:0];
}
- (void)removeScheduledPrerenderRequests {
[NSObject cancelPreviousPerformRequestsWithTarget:self];
scheduledURL_ = GURL();
}
#pragma mark - CRWWebStateDelegate
- (web::WebState*)webState:(web::WebState*)webState
createNewWebStateForURL:(const GURL&)URL
openerURL:(const GURL&)openerURL
initiatedByUser:(BOOL)initiatedByUser {
DCHECK([self isWebStatePrerendered:webState]);
[self schedulePrerenderCancel];
return nil;
}
- (web::JavaScriptDialogPresenter*)javaScriptDialogPresenterForWebState:
(web::WebState*)webState {
DCHECK([self isWebStatePrerendered:webState]);
[self schedulePrerenderCancel];
return nullptr;
}
- (void)webState:(web::WebState*)webState
didRequestHTTPAuthForProtectionSpace:(NSURLProtectionSpace*)protectionSpace
proposedCredential:(NSURLCredential*)proposedCredential
completionHandler:(void (^)(NSString* username,
NSString* password))handler {
DCHECK([self isWebStatePrerendered:webState]);
[self schedulePrerenderCancel];
if (handler) {
handler(nil, nil);
}
}
- (void)recordReleaseMetrics {
UMA_HISTOGRAM_ENUMERATION(kPrerenderFinalStatusHistogramName,
PRERENDER_FINAL_STATUS_USED,
PRERENDER_FINAL_STATUS_MAX);
DCHECK_NE(base::TimeTicks(), startTime_);
UMA_HISTOGRAM_TIMES(kPrerenderStartToReleaseContentsTime,
base::TimeTicks::Now() - startTime_);
}
#pragma mark - CRWWebStateObserver
- (void)webState:(web::WebState*)webState
didLoadPageWithSuccess:(BOOL)loadSuccess {
DCHECK_EQ(webState, webState_.get());
// Cancel prerendering if response is "application/octet-stream". It can be a
// video file which should not be played from preload tab. See issue at
// http://crbug.com/436813 for more details.
const std::string& mimeType = webState->GetContentsMimeType();
if (mimeType == "application/octet-stream")
[self schedulePrerenderCancel];
}
#pragma mark - ManageAccountsDelegate
- (void)onManageAccounts {
[self schedulePrerenderCancel];
}
- (void)onAddAccount {
[self schedulePrerenderCancel];
}
- (void)onGoIncognito:(const GURL&)url {
[self schedulePrerenderCancel];
}
#pragma mark - CRWWebStatePolicyDecider
- (BOOL)shouldAllowRequest:(NSURLRequest*)request
requestInfo:(const WebStatePolicyDecider::RequestInfo&)info {
GURL requestURL = net::GURLWithNSURL(request.URL);
// Don't allow preloading for requests that are handled by opening another
// application or by presenting a native UI.
if (AppLauncherTabHelper::IsAppUrl(requestURL) ||
ITunesUrlsHandlerTabHelper::CanHandleUrl(requestURL)) {
[self schedulePrerenderCancel];
return NO;
}
return YES;
}
@end