blob: d96c044be3422fbb139014c6aea31e1ec1240bcc [file] [log] [blame]
sdefresned9217bc2016-12-19 13:58:321// Copyright 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#import <UIKit/UIKit.h>
6
Rohit Rao44f204302017-08-10 14:49:547#include "ios/chrome/browser/prerender/preload_controller.h"
sdefresned9217bc2016-12-19 13:58:328
9#include "base/ios/device_util.h"
10#include "base/logging.h"
11#include "base/metrics/field_trial.h"
asvitkinef1899e32017-01-27 16:30:2912#include "base/metrics/histogram_macros.h"
Sylvain Defresnef5d2d952017-11-14 11:15:3113#include "base/metrics/user_metrics.h"
14#include "base/metrics/user_metrics_action.h"
sdefresned9217bc2016-12-19 13:58:3215#include "base/strings/sys_string_conversions.h"
16#include "components/prefs/pref_service.h"
edchincd32fdf2017-10-25 12:45:4517#import "components/signin/ios/browser/account_consistency_service.h"
sdefresned9217bc2016-12-19 13:58:3218#include "ios/chrome/browser/browser_state/chrome_browser_state.h"
Sylvain Defresnef5d2d952017-11-14 11:15:3119#include "ios/chrome/browser/chrome_url_constants.h"
20#import "ios/chrome/browser/geolocation/omnibox_geolocation_controller.h"
Sylvain Defresnef5b8a8e2017-10-24 02:54:4421#import "ios/chrome/browser/history/history_tab_helper.h"
sdefresned9217bc2016-12-19 13:58:3222#include "ios/chrome/browser/pref_names.h"
Rohit Rao44f204302017-08-10 14:49:5423#include "ios/chrome/browser/prerender/preload_controller_delegate.h"
edchincd32fdf2017-10-25 12:45:4524#import "ios/chrome/browser/signin/account_consistency_service_factory.h"
sdefresne2c600c52017-04-04 16:49:5925#import "ios/chrome/browser/tabs/legacy_tab_helper.h"
sdefresned9217bc2016-12-19 13:58:3226#import "ios/chrome/browser/tabs/tab.h"
sdefresne2c600c52017-04-04 16:49:5927#import "ios/chrome/browser/tabs/tab_helper_util.h"
28#import "ios/chrome/browser/tabs/tab_private.h"
sdefresned9217bc2016-12-19 13:58:3229#include "ios/chrome/browser/ui/prerender_final_status.h"
Sylvain Defresnef5d2d952017-11-14 11:15:3130#import "ios/web/public/navigation_item.h"
Gregory Chatzinoff0c2d0a62017-08-25 01:54:3731#import "ios/web/public/navigation_manager.h"
sdefresned9217bc2016-12-19 13:58:3232#import "ios/web/public/web_state/ui/crw_native_content.h"
Sylvain Defresnef5d2d952017-11-14 11:15:3133#import "ios/web/public/web_state/web_state.h"
sdefresned9217bc2016-12-19 13:58:3234#include "ios/web/public/web_thread.h"
35#import "ios/web/web_state/ui/crw_web_controller.h"
36#import "net/base/mac/url_conversions.h"
sdefresned9217bc2016-12-19 13:58:3237#include "ui/base/page_transition_types.h"
38
stkhapugine757b63a2017-04-10 12:31:2739#if !defined(__has_feature) || !__has_feature(objc_arc)
40#error "This file requires ARC support."
41#endif
42
sdefresned9217bc2016-12-19 13:58:3243namespace {
44// Delay before starting to prerender a URL.
45const NSTimeInterval kPrerenderDelay = 0.5;
46
Rohit Rao9a8c39b2017-08-14 17:52:3047// The finch experiment to turn off prerendering as a field trial.
sdefresned9217bc2016-12-19 13:58:3248const char kTabEvictionFieldTrialName[] = "TabEviction";
49// The associated group.
50const char kPrerenderTabEvictionTrialGroup[] = "NoPrerendering";
51// The name of the histogram for recording final status (e.g. used/cancelled)
52// of prerender requests.
53const char kPrerenderFinalStatusHistogramName[] = "Prerender.FinalStatus";
Julien Brianceaub7e590ac2017-08-01 17:30:2254// The name of the histogram for recording the number of successful prerenders.
sdefresned9217bc2016-12-19 13:58:3255const char kPrerendersPerSessionCountHistogramName[] =
56 "Prerender.PrerendersPerSessionCount";
57
58// Is this install selected for this particular experiment.
59bool IsPrerenderTabEvictionExperimentalGroup() {
60 base::FieldTrial* trial =
61 base::FieldTrialList::Find(kTabEvictionFieldTrialName);
62 return trial && trial->group_name() == kPrerenderTabEvictionTrialGroup;
63}
64
65} // namespace
66
edchincd32fdf2017-10-25 12:45:4567@interface PreloadController (PrivateMethods)<ManageAccountsDelegate>
sdefresned9217bc2016-12-19 13:58:3268
69// Returns YES if prerendering is enabled.
70- (BOOL)isPrerenderingEnabled;
71
Rohit Rao9a8c39b2017-08-14 17:52:3072// Returns YES if the |url| is valid for prerendering.
sdefresned9217bc2016-12-19 13:58:3273- (BOOL)shouldPreloadURL:(const GURL&)url;
74
75// Called to start any scheduled prerendering requests.
76- (void)startPrerender;
77
78// Destroys the preview Tab and resets |prerenderURL_| to the empty URL.
79- (void)destroyPreviewContents;
80
81// Schedules the current prerender to be cancelled during the next run of the
82// event loop.
83- (void)schedulePrerenderCancel;
84
85// Removes any scheduled prerender requests and resets |scheduledURL| to the
86// empty URL.
87- (void)removeScheduledPrerenderRequests;
88
sdefresned9217bc2016-12-19 13:58:3289@end
90
sdefresne2c600c52017-04-04 16:49:5991@implementation PreloadController {
92 ios::ChromeBrowserState* browserState_; // Weak.
93
94 // The WebState used for prerendering.
95 std::unique_ptr<web::WebState> webState_;
96
Sylvain Defresnef5d2d952017-11-14 11:15:3197 // The WebStateDelegateBridge used to register self as a CRWWebStateDelegate
98 // with the pre-rendered WebState.
99 std::unique_ptr<web::WebStateDelegateBridge> webStateDelegate_;
100
sdefresne2c600c52017-04-04 16:49:59101 // The URL that is prerendered in |webState_|. This can be different from
102 // the value returned by WebState last committed navigation item, for example
103 // in cases where there was a redirect.
104 //
105 // When choosing whether or not to use a prerendered Tab,
106 // BrowserViewController compares the URL being loaded by the omnibox with the
107 // URL of the prerendered Tab. Comparing against the Tab's currently URL
108 // could return false negatives in cases of redirect, hence the need to store
109 // the originally prerendered URL.
110 GURL prerenderedURL_;
111
112 // The URL that is scheduled to be prerendered, its associated transition and
113 // referrer. |scheduledTransition_| and |scheduledReferrer_| are not valid
114 // when |scheduledURL_| is empty.
115 GURL scheduledURL_;
116 ui::PageTransition scheduledTransition_;
117 web::Referrer scheduledReferrer_;
118
sdefresne2c600c52017-04-04 16:49:59119 // Bridge to listen to pref changes.
120 std::unique_ptr<PrefObserverBridge> observerBridge_;
121 // Registrar for pref changes notifications.
122 PrefChangeRegistrar prefChangeRegistrar_;
123 // Observer for the WWAN setting. Contains a valid object only if the
124 // instant setting is set to wifi-only.
125 std::unique_ptr<ConnectionTypeObserverBridge> connectionTypeObserverBridge_;
126
127 // Whether or not the preference is enabled.
128 BOOL enabled_;
129 // Whether or not prerendering is only when on wifi.
130 BOOL wifiOnly_;
131 // Whether or not the current connection is using WWAN.
132 BOOL usingWWAN_;
133
134 // Number of successful prerenders (i.e. the user viewed the prerendered page)
135 // during the lifetime of this controller.
136 int successfulPrerendersPerSessionCount_;
sdefresne2c600c52017-04-04 16:49:59137}
sdefresned9217bc2016-12-19 13:58:32138
139@synthesize prerenderedURL = prerenderedURL_;
sdefresned9217bc2016-12-19 13:58:32140@synthesize delegate = delegate_;
141
142- (instancetype)initWithBrowserState:(ios::ChromeBrowserState*)browserState {
143 DCHECK(browserState);
144 DCHECK_CURRENTLY_ON(web::WebThread::UI);
145 if ((self = [super init])) {
146 browserState_ = browserState;
147 enabled_ =
148 browserState_->GetPrefs()->GetBoolean(prefs::kNetworkPredictionEnabled);
149 wifiOnly_ = browserState_->GetPrefs()->GetBoolean(
150 prefs::kNetworkPredictionWifiOnly);
151 usingWWAN_ = net::NetworkChangeNotifier::IsConnectionCellular(
152 net::NetworkChangeNotifier::GetConnectionType());
Sylvain Defresnef5d2d952017-11-14 11:15:31153 webStateDelegate_.reset(new web::WebStateDelegateBridge(self));
sdefresned9217bc2016-12-19 13:58:32154 observerBridge_.reset(new PrefObserverBridge(self));
155 prefChangeRegistrar_.Init(browserState_->GetPrefs());
156 observerBridge_->ObserveChangesForPreference(
157 prefs::kNetworkPredictionEnabled, &prefChangeRegistrar_);
158 observerBridge_->ObserveChangesForPreference(
159 prefs::kNetworkPredictionWifiOnly, &prefChangeRegistrar_);
160 if (enabled_ && wifiOnly_) {
161 connectionTypeObserverBridge_.reset(
162 new ConnectionTypeObserverBridge(self));
163 }
164
165 [[NSNotificationCenter defaultCenter]
166 addObserver:self
167 selector:@selector(didReceiveMemoryWarning)
168 name:UIApplicationDidReceiveMemoryWarningNotification
169 object:nil];
170 }
171 return self;
172}
173
michaeldobc2f42e2017-01-12 19:04:47174- (void)browserStateDestroyed {
175 [self cancelPrerender];
176 connectionTypeObserverBridge_.reset();
177}
178
sdefresned9217bc2016-12-19 13:58:32179- (void)dealloc {
180 UMA_HISTOGRAM_COUNTS(kPrerendersPerSessionCountHistogramName,
181 successfulPrerendersPerSessionCount_);
182 [[NSNotificationCenter defaultCenter] removeObserver:self];
183 [self cancelPrerender];
sdefresned9217bc2016-12-19 13:58:32184}
185
186- (void)prerenderURL:(const GURL&)url
187 referrer:(const web::Referrer&)referrer
188 transition:(ui::PageTransition)transition
189 immediately:(BOOL)immediately {
Rohit Rao44f204302017-08-10 14:49:54190 // TODO(crbug.com/754050): If shouldPrerenderURL returns false, should we
191 // cancel any scheduled prerender requests?
sdefresned9217bc2016-12-19 13:58:32192 if (![self isPrerenderingEnabled] || ![self shouldPreloadURL:url])
193 return;
194
195 // Ignore this request if there is already a scheduled request for the same
196 // URL; or, if there is no scheduled request, but the currently prerendered
197 // page matches this URL.
198 if (url == scheduledURL_ ||
199 (scheduledURL_.is_empty() && url == prerenderedURL_)) {
200 return;
201 }
202
203 [self removeScheduledPrerenderRequests];
204 scheduledURL_ = url;
205 scheduledTransition_ = transition;
206 scheduledReferrer_ = referrer;
207
208 NSTimeInterval delay = immediately ? 0.0 : kPrerenderDelay;
209 [self performSelector:@selector(startPrerender)
210 withObject:nil
211 afterDelay:delay];
212}
213
sdefresned9217bc2016-12-19 13:58:32214- (void)cancelPrerender {
215 [self cancelPrerenderForReason:PRERENDER_FINAL_STATUS_CANCELLED];
216}
217
218- (void)cancelPrerenderForReason:(PrerenderFinalStatus)reason {
219 [self removeScheduledPrerenderRequests];
220 [self destroyPreviewContentsForReason:reason];
221}
222
Sylvain Defresnef5d2d952017-11-14 11:15:31223- (BOOL)isWebStatePrerendered:(web::WebState*)webState {
224 return webState && webState_.get() == webState;
225}
226
sdefresne2c600c52017-04-04 16:49:59227- (std::unique_ptr<web::WebState>)releasePrerenderContents {
sdefresned9217bc2016-12-19 13:58:32228 successfulPrerendersPerSessionCount_++;
229 UMA_HISTOGRAM_ENUMERATION(kPrerenderFinalStatusHistogramName,
230 PRERENDER_FINAL_STATUS_USED,
231 PRERENDER_FINAL_STATUS_MAX);
232 [self removeScheduledPrerenderRequests];
233 prerenderedURL_ = GURL();
sdefresne2c600c52017-04-04 16:49:59234
Sylvain Defresnef5d2d952017-11-14 11:15:31235 if (!webState_)
236 return nullptr;
Sylvain Defresnef5b8a8e2017-10-24 02:54:44237
Sylvain Defresnef5d2d952017-11-14 11:15:31238 // Move the pre-rendered WebState to a local variable so that it will no
239 // longer be considered as pre-rendering (otherwise tab helpers may early
240 // exist when invoked).
241 std::unique_ptr<web::WebState> webState = std::move(webState_);
242 DCHECK(![self isWebStatePrerendered:webState.get()]);
edchincd32fdf2017-10-25 12:45:45243
Sylvain Defresnef5d2d952017-11-14 11:15:31244 Tab* tab = LegacyTabHelper::GetTabForWebState(webState.get());
245 [[tab webController] setNativeProvider:nil];
246 webState->SetShouldSuppressDialogs(false);
247 webState->SetDelegate(nullptr);
248
249 HistoryTabHelper::FromWebState(webState_.get())
250 ->SetDelayHistoryServiceNotification(false);
251
252 if (AccountConsistencyService* accountConsistencyService =
253 ios::AccountConsistencyServiceFactory::GetForBrowserState(
254 browserState_)) {
255 accountConsistencyService->RemoveWebStateHandler(webState_.get());
256 }
257
258 if ([tab loadFinished]) {
259 // If the page has finished loading, take a snapshot. If the page is
260 // still loading, do nothing, as CRWWebController will automatically take
261 // a snapshot once the load completes.
262 [tab updateSnapshotWithOverlay:YES visibleFrameOnly:YES];
263
264 [[OmniboxGeolocationController sharedInstance] finishPageLoadForTab:tab
265 loadSuccess:YES];
266
267 if (!webState->GetLastCommittedURL().SchemeIs(kChromeUIScheme)) {
268 base::RecordAction(base::UserMetricsAction("MobilePageLoaded"));
edchincd32fdf2017-10-25 12:45:45269 }
sdefresne2c600c52017-04-04 16:49:59270 }
271
Sylvain Defresnef5d2d952017-11-14 11:15:31272 [tab setDelegate:nil];
273
274 return webState;
sdefresned9217bc2016-12-19 13:58:32275}
276
277- (void)connectionTypeChanged:(net::NetworkChangeNotifier::ConnectionType)type {
278 DCHECK_CURRENTLY_ON(web::WebThread::UI);
279 usingWWAN_ = net::NetworkChangeNotifier::IsConnectionCellular(type);
280 if (wifiOnly_ && usingWWAN_)
281 [self cancelPrerender];
282}
283
284- (void)onPreferenceChanged:(const std::string&)preferenceName {
285 if (preferenceName == prefs::kNetworkPredictionEnabled ||
286 preferenceName == prefs::kNetworkPredictionWifiOnly) {
287 DCHECK_CURRENTLY_ON(web::WebThread::UI);
288 // The logic is simpler if both preferences changes are handled equally.
289 enabled_ =
290 browserState_->GetPrefs()->GetBoolean(prefs::kNetworkPredictionEnabled);
291 wifiOnly_ = browserState_->GetPrefs()->GetBoolean(
292 prefs::kNetworkPredictionWifiOnly);
293
294 if (wifiOnly_ && enabled_) {
295 if (!connectionTypeObserverBridge_.get()) {
296 usingWWAN_ = net::NetworkChangeNotifier::IsConnectionCellular(
297 net::NetworkChangeNotifier::GetConnectionType());
298 connectionTypeObserverBridge_.reset(
299 new ConnectionTypeObserverBridge(self));
300 }
301 if (usingWWAN_) {
302 [self cancelPrerender];
303 }
304 } else if (enabled_) {
305 connectionTypeObserverBridge_.reset();
306 } else {
307 [self cancelPrerender];
308 connectionTypeObserverBridge_.reset();
309 }
310 }
311}
312
313- (void)didReceiveMemoryWarning {
314 [self cancelPrerenderForReason:PRERENDER_FINAL_STATUS_MEMORY_LIMIT_EXCEEDED];
315}
316
317#pragma mark -
318#pragma mark CRWNativeContentProvider implementation
319
sdefresned9217bc2016-12-19 13:58:32320- (BOOL)hasControllerForURL:(const GURL&)url {
sdefresne2c600c52017-04-04 16:49:59321 if (!webState_)
322 return NO;
rohitraoeeb5293b2017-06-15 14:40:02323
324 return [delegate_ preloadHasNativeControllerForURL:url];
sdefresned9217bc2016-12-19 13:58:32325}
326
327// Override the CRWNativeContentProvider methods to cancel any prerenders that
328// require native content.
olivierrobind43eecb2017-01-27 20:35:26329- (id<CRWNativeContent>)controllerForURL:(const GURL&)url
330 webState:(web::WebState*)webState {
sdefresned9217bc2016-12-19 13:58:32331 [self schedulePrerenderCancel];
332 return nil;
333}
334
335// Override the CRWNativeContentProvider methods to cancel any prerenders that
336// require native content.
337- (id<CRWNativeContent>)controllerForURL:(const GURL&)url
338 withError:(NSError*)error
339 isPost:(BOOL)isPost {
340 [self schedulePrerenderCancel];
341 return nil;
342}
343
344#pragma mark -
345#pragma mark Private Methods
346
347- (BOOL)isPrerenderingEnabled {
348 DCHECK_CURRENTLY_ON(web::WebThread::UI);
349 return !IsPrerenderTabEvictionExperimentalGroup() && enabled_ &&
350 !ios::device_util::IsSingleCoreDevice() &&
351 ios::device_util::RamIsAtLeast512Mb() && (!wifiOnly_ || !usingWWAN_);
352}
353
sdefresned9217bc2016-12-19 13:58:32354- (BOOL)shouldPreloadURL:(const GURL&)url {
355 return url.SchemeIs(url::kHttpScheme) || url.SchemeIs(url::kHttpsScheme);
356}
357
358- (void)startPrerender {
359 // Destroy any existing prerenders before starting a new one.
360 [self destroyPreviewContents];
361 prerenderedURL_ = scheduledURL_;
362 scheduledURL_ = GURL();
363
364 DCHECK(prerenderedURL_.is_valid());
365 if (!prerenderedURL_.is_valid()) {
366 [self destroyPreviewContents];
367 return;
368 }
369
sdefresne2c600c52017-04-04 16:49:59370 web::WebState::CreateParams createParams(browserState_);
371 webState_ = web::WebState::Create(createParams);
372 AttachTabHelpers(webState_.get());
sdefresned9217bc2016-12-19 13:58:32373
sdefresne2c600c52017-04-04 16:49:59374 Tab* tab = LegacyTabHelper::GetTabForWebState(webState_.get());
375 DCHECK(tab);
376
377 [[tab webController] setNativeProvider:self];
Sylvain Defresnef5d2d952017-11-14 11:15:31378 webState_->SetDelegate(webStateDelegate_.get());
379 webState_->SetShouldSuppressDialogs(true);
380 webState_->SetWebUsageEnabled(true);
sdefresne2c600c52017-04-04 16:49:59381 [tab setDelegate:self];
edchincd32fdf2017-10-25 12:45:45382 if (AccountConsistencyService* accountConsistencyService =
383 ios::AccountConsistencyServiceFactory::GetForBrowserState(
384 browserState_)) {
385 accountConsistencyService->SetWebStateHandler(webState_.get(), self);
386 }
sdefresne2c600c52017-04-04 16:49:59387
Sylvain Defresnef5b8a8e2017-10-24 02:54:44388 HistoryTabHelper::FromWebState(webState_.get())
389 ->SetDelayHistoryServiceNotification(true);
390
sdefresne2c600c52017-04-04 16:49:59391 web::NavigationManager::WebLoadParams loadParams(prerenderedURL_);
392 loadParams.referrer = scheduledReferrer_;
393 loadParams.transition_type = scheduledTransition_;
rohitraoeeb5293b2017-06-15 14:40:02394 if ([delegate_ preloadShouldUseDesktopUserAgent]) {
sdefresne2c600c52017-04-04 16:49:59395 loadParams.user_agent_override_option =
396 web::NavigationManager::UserAgentOverrideOption::DESKTOP;
397 }
sdefresne7d699dd2017-04-05 13:05:23398 webState_->GetNavigationManager()->LoadURLWithParams(loadParams);
sdefresned9217bc2016-12-19 13:58:32399
400 // Trigger the page to start loading.
sdefresne7d699dd2017-04-05 13:05:23401 // TODO(crbug.com/705819): Remove this call.
sdefresne2c600c52017-04-04 16:49:59402 [tab view];
sdefresned9217bc2016-12-19 13:58:32403}
404
sdefresned9217bc2016-12-19 13:58:32405- (void)destroyPreviewContents {
406 [self destroyPreviewContentsForReason:PRERENDER_FINAL_STATUS_CANCELLED];
407}
408
409- (void)destroyPreviewContentsForReason:(PrerenderFinalStatus)reason {
sdefresne2c600c52017-04-04 16:49:59410 if (!webState_)
sdefresned9217bc2016-12-19 13:58:32411 return;
412
413 UMA_HISTOGRAM_ENUMERATION(kPrerenderFinalStatusHistogramName, reason,
414 PRERENDER_FINAL_STATUS_MAX);
sdefresne2c600c52017-04-04 16:49:59415
416 Tab* tab = LegacyTabHelper::GetTabForWebState(webState_.get());
417 [[tab webController] setNativeProvider:nil];
Sylvain Defresnef5d2d952017-11-14 11:15:31418 webState_->SetDelegate(nullptr);
sdefresne2c600c52017-04-04 16:49:59419 webState_.reset();
420
sdefresned9217bc2016-12-19 13:58:32421 prerenderedURL_ = GURL();
422}
423
424- (void)schedulePrerenderCancel {
Rohit Rao44f204302017-08-10 14:49:54425 // TODO(crbug.com/228550): Instead of cancelling the prerender, should we mark
426 // it as failed instead? That way, subsequent prerender requests for the same
427 // URL will not kick off new prerenders.
sdefresned9217bc2016-12-19 13:58:32428 [self removeScheduledPrerenderRequests];
429 [self performSelector:@selector(cancelPrerender) withObject:nil afterDelay:0];
430}
431
432- (void)removeScheduledPrerenderRequests {
433 [NSObject cancelPreviousPerformRequestsWithTarget:self];
434 scheduledURL_ = GURL();
435}
436
Sylvain Defresnef5d2d952017-11-14 11:15:31437#pragma mark - CRWWebStateDelegate
438
439- (void)webState:(web::WebState*)webState
440 didRequestHTTPAuthForProtectionSpace:(NSURLProtectionSpace*)protectionSpace
441 proposedCredential:(NSURLCredential*)proposedCredential
442 completionHandler:(void (^)(NSString* username,
443 NSString* password))handler {
444 DCHECK([self isWebStatePrerendered:webState]);
445 [self schedulePrerenderCancel];
446 if (handler) {
447 handler(nil, nil);
448 }
449}
450
sdefresned9217bc2016-12-19 13:58:32451#pragma mark - TabDelegate
452
453- (void)discardPrerender {
454 [self schedulePrerenderCancel];
455}
456
edchincd32fdf2017-10-25 12:45:45457#pragma mark - ManageAccountsDelegate
458
459- (void)onManageAccounts {
460 [self discardPrerender];
461}
462
463- (void)onAddAccount {
464 [self discardPrerender];
465}
466
467- (void)onGoIncognito:(const GURL&)url {
468 [self discardPrerender];
469}
470
sdefresned9217bc2016-12-19 13:58:32471@end