blob: 1e5090d4747e615e2019b428d7249def4d2776be [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"
sdefresned9217bc2016-12-19 13:58:3213#include "base/strings/sys_string_conversions.h"
14#include "components/prefs/pref_service.h"
edchincd32fdf2017-10-25 12:45:4515#import "components/signin/ios/browser/account_consistency_service.h"
sdefresned9217bc2016-12-19 13:58:3216#include "ios/chrome/browser/browser_state/chrome_browser_state.h"
Sylvain Defresnef5b8a8e2017-10-24 02:54:4417#import "ios/chrome/browser/history/history_tab_helper.h"
sdefresned9217bc2016-12-19 13:58:3218#include "ios/chrome/browser/pref_names.h"
Rohit Rao44f204302017-08-10 14:49:5419#include "ios/chrome/browser/prerender/preload_controller_delegate.h"
edchincd32fdf2017-10-25 12:45:4520#import "ios/chrome/browser/signin/account_consistency_service_factory.h"
sdefresne2c600c52017-04-04 16:49:5921#import "ios/chrome/browser/tabs/legacy_tab_helper.h"
sdefresned9217bc2016-12-19 13:58:3222#import "ios/chrome/browser/tabs/tab.h"
sdefresne2c600c52017-04-04 16:49:5923#import "ios/chrome/browser/tabs/tab_helper_util.h"
24#import "ios/chrome/browser/tabs/tab_private.h"
sdefresned9217bc2016-12-19 13:58:3225#include "ios/chrome/browser/ui/prerender_final_status.h"
Gregory Chatzinoff0c2d0a62017-08-25 01:54:3726#import "ios/web/public/navigation_manager.h"
sdefresned9217bc2016-12-19 13:58:3227#import "ios/web/public/web_state/ui/crw_native_content.h"
28#include "ios/web/public/web_thread.h"
29#import "ios/web/web_state/ui/crw_web_controller.h"
30#import "net/base/mac/url_conversions.h"
sdefresned9217bc2016-12-19 13:58:3231#include "ui/base/page_transition_types.h"
32
stkhapugine757b63a2017-04-10 12:31:2733#if !defined(__has_feature) || !__has_feature(objc_arc)
34#error "This file requires ARC support."
35#endif
36
sdefresned9217bc2016-12-19 13:58:3237namespace {
38// Delay before starting to prerender a URL.
39const NSTimeInterval kPrerenderDelay = 0.5;
40
Rohit Rao9a8c39b2017-08-14 17:52:3041// The finch experiment to turn off prerendering as a field trial.
sdefresned9217bc2016-12-19 13:58:3242const char kTabEvictionFieldTrialName[] = "TabEviction";
43// The associated group.
44const char kPrerenderTabEvictionTrialGroup[] = "NoPrerendering";
45// The name of the histogram for recording final status (e.g. used/cancelled)
46// of prerender requests.
47const char kPrerenderFinalStatusHistogramName[] = "Prerender.FinalStatus";
Julien Brianceaub7e590ac2017-08-01 17:30:2248// The name of the histogram for recording the number of successful prerenders.
sdefresned9217bc2016-12-19 13:58:3249const char kPrerendersPerSessionCountHistogramName[] =
50 "Prerender.PrerendersPerSessionCount";
51
52// Is this install selected for this particular experiment.
53bool IsPrerenderTabEvictionExperimentalGroup() {
54 base::FieldTrial* trial =
55 base::FieldTrialList::Find(kTabEvictionFieldTrialName);
56 return trial && trial->group_name() == kPrerenderTabEvictionTrialGroup;
57}
58
59} // namespace
60
edchincd32fdf2017-10-25 12:45:4561@interface PreloadController (PrivateMethods)<ManageAccountsDelegate>
sdefresned9217bc2016-12-19 13:58:3262
63// Returns YES if prerendering is enabled.
64- (BOOL)isPrerenderingEnabled;
65
Rohit Rao9a8c39b2017-08-14 17:52:3066// Returns YES if the |url| is valid for prerendering.
sdefresned9217bc2016-12-19 13:58:3267- (BOOL)shouldPreloadURL:(const GURL&)url;
68
69// Called to start any scheduled prerendering requests.
70- (void)startPrerender;
71
72// Destroys the preview Tab and resets |prerenderURL_| to the empty URL.
73- (void)destroyPreviewContents;
74
75// Schedules the current prerender to be cancelled during the next run of the
76// event loop.
77- (void)schedulePrerenderCancel;
78
79// Removes any scheduled prerender requests and resets |scheduledURL| to the
80// empty URL.
81- (void)removeScheduledPrerenderRequests;
82
sdefresned9217bc2016-12-19 13:58:3283@end
84
sdefresne2c600c52017-04-04 16:49:5985@implementation PreloadController {
86 ios::ChromeBrowserState* browserState_; // Weak.
87
88 // The WebState used for prerendering.
89 std::unique_ptr<web::WebState> webState_;
90
91 // The URL that is prerendered in |webState_|. This can be different from
92 // the value returned by WebState last committed navigation item, for example
93 // in cases where there was a redirect.
94 //
95 // When choosing whether or not to use a prerendered Tab,
96 // BrowserViewController compares the URL being loaded by the omnibox with the
97 // URL of the prerendered Tab. Comparing against the Tab's currently URL
98 // could return false negatives in cases of redirect, hence the need to store
99 // the originally prerendered URL.
100 GURL prerenderedURL_;
101
102 // The URL that is scheduled to be prerendered, its associated transition and
103 // referrer. |scheduledTransition_| and |scheduledReferrer_| are not valid
104 // when |scheduledURL_| is empty.
105 GURL scheduledURL_;
106 ui::PageTransition scheduledTransition_;
107 web::Referrer scheduledReferrer_;
108
sdefresne2c600c52017-04-04 16:49:59109 // Bridge to listen to pref changes.
110 std::unique_ptr<PrefObserverBridge> observerBridge_;
111 // Registrar for pref changes notifications.
112 PrefChangeRegistrar prefChangeRegistrar_;
113 // Observer for the WWAN setting. Contains a valid object only if the
114 // instant setting is set to wifi-only.
115 std::unique_ptr<ConnectionTypeObserverBridge> connectionTypeObserverBridge_;
116
117 // Whether or not the preference is enabled.
118 BOOL enabled_;
119 // Whether or not prerendering is only when on wifi.
120 BOOL wifiOnly_;
121 // Whether or not the current connection is using WWAN.
122 BOOL usingWWAN_;
123
124 // Number of successful prerenders (i.e. the user viewed the prerendered page)
125 // during the lifetime of this controller.
126 int successfulPrerendersPerSessionCount_;
sdefresne2c600c52017-04-04 16:49:59127}
sdefresned9217bc2016-12-19 13:58:32128
129@synthesize prerenderedURL = prerenderedURL_;
sdefresned9217bc2016-12-19 13:58:32130@synthesize delegate = delegate_;
131
132- (instancetype)initWithBrowserState:(ios::ChromeBrowserState*)browserState {
133 DCHECK(browserState);
134 DCHECK_CURRENTLY_ON(web::WebThread::UI);
135 if ((self = [super init])) {
136 browserState_ = browserState;
137 enabled_ =
138 browserState_->GetPrefs()->GetBoolean(prefs::kNetworkPredictionEnabled);
139 wifiOnly_ = browserState_->GetPrefs()->GetBoolean(
140 prefs::kNetworkPredictionWifiOnly);
141 usingWWAN_ = net::NetworkChangeNotifier::IsConnectionCellular(
142 net::NetworkChangeNotifier::GetConnectionType());
143 observerBridge_.reset(new PrefObserverBridge(self));
144 prefChangeRegistrar_.Init(browserState_->GetPrefs());
145 observerBridge_->ObserveChangesForPreference(
146 prefs::kNetworkPredictionEnabled, &prefChangeRegistrar_);
147 observerBridge_->ObserveChangesForPreference(
148 prefs::kNetworkPredictionWifiOnly, &prefChangeRegistrar_);
149 if (enabled_ && wifiOnly_) {
150 connectionTypeObserverBridge_.reset(
151 new ConnectionTypeObserverBridge(self));
152 }
153
154 [[NSNotificationCenter defaultCenter]
155 addObserver:self
156 selector:@selector(didReceiveMemoryWarning)
157 name:UIApplicationDidReceiveMemoryWarningNotification
158 object:nil];
159 }
160 return self;
161}
162
michaeldobc2f42e2017-01-12 19:04:47163- (void)browserStateDestroyed {
164 [self cancelPrerender];
165 connectionTypeObserverBridge_.reset();
166}
167
sdefresned9217bc2016-12-19 13:58:32168- (void)dealloc {
169 UMA_HISTOGRAM_COUNTS(kPrerendersPerSessionCountHistogramName,
170 successfulPrerendersPerSessionCount_);
171 [[NSNotificationCenter defaultCenter] removeObserver:self];
172 [self cancelPrerender];
sdefresned9217bc2016-12-19 13:58:32173}
174
175- (void)prerenderURL:(const GURL&)url
176 referrer:(const web::Referrer&)referrer
177 transition:(ui::PageTransition)transition
178 immediately:(BOOL)immediately {
Rohit Rao44f204302017-08-10 14:49:54179 // TODO(crbug.com/754050): If shouldPrerenderURL returns false, should we
180 // cancel any scheduled prerender requests?
sdefresned9217bc2016-12-19 13:58:32181 if (![self isPrerenderingEnabled] || ![self shouldPreloadURL:url])
182 return;
183
184 // Ignore this request if there is already a scheduled request for the same
185 // URL; or, if there is no scheduled request, but the currently prerendered
186 // page matches this URL.
187 if (url == scheduledURL_ ||
188 (scheduledURL_.is_empty() && url == prerenderedURL_)) {
189 return;
190 }
191
192 [self removeScheduledPrerenderRequests];
193 scheduledURL_ = url;
194 scheduledTransition_ = transition;
195 scheduledReferrer_ = referrer;
196
197 NSTimeInterval delay = immediately ? 0.0 : kPrerenderDelay;
198 [self performSelector:@selector(startPrerender)
199 withObject:nil
200 afterDelay:delay];
201}
202
sdefresned9217bc2016-12-19 13:58:32203- (void)cancelPrerender {
204 [self cancelPrerenderForReason:PRERENDER_FINAL_STATUS_CANCELLED];
205}
206
207- (void)cancelPrerenderForReason:(PrerenderFinalStatus)reason {
208 [self removeScheduledPrerenderRequests];
209 [self destroyPreviewContentsForReason:reason];
210}
211
sdefresne2c600c52017-04-04 16:49:59212- (std::unique_ptr<web::WebState>)releasePrerenderContents {
sdefresned9217bc2016-12-19 13:58:32213 successfulPrerendersPerSessionCount_++;
214 UMA_HISTOGRAM_ENUMERATION(kPrerenderFinalStatusHistogramName,
215 PRERENDER_FINAL_STATUS_USED,
216 PRERENDER_FINAL_STATUS_MAX);
217 [self removeScheduledPrerenderRequests];
218 prerenderedURL_ = GURL();
sdefresne2c600c52017-04-04 16:49:59219
220 if (webState_) {
221 Tab* tab = LegacyTabHelper::GetTabForWebState(webState_.get());
222 [[tab webController] setNativeProvider:nil];
223 [tab setDelegate:nil];
Sylvain Defresnef5b8a8e2017-10-24 02:54:44224
225 HistoryTabHelper::FromWebState(webState_.get())
226 ->SetDelayHistoryServiceNotification(false);
edchincd32fdf2017-10-25 12:45:45227
228 if (AccountConsistencyService* accountConsistencyService =
229 ios::AccountConsistencyServiceFactory::GetForBrowserState(
230 browserState_)) {
231 accountConsistencyService->RemoveWebStateHandler(webState_.get());
232 }
sdefresne2c600c52017-04-04 16:49:59233 }
234
235 return std::move(webState_);
sdefresned9217bc2016-12-19 13:58:32236}
237
238- (void)connectionTypeChanged:(net::NetworkChangeNotifier::ConnectionType)type {
239 DCHECK_CURRENTLY_ON(web::WebThread::UI);
240 usingWWAN_ = net::NetworkChangeNotifier::IsConnectionCellular(type);
241 if (wifiOnly_ && usingWWAN_)
242 [self cancelPrerender];
243}
244
245- (void)onPreferenceChanged:(const std::string&)preferenceName {
246 if (preferenceName == prefs::kNetworkPredictionEnabled ||
247 preferenceName == prefs::kNetworkPredictionWifiOnly) {
248 DCHECK_CURRENTLY_ON(web::WebThread::UI);
249 // The logic is simpler if both preferences changes are handled equally.
250 enabled_ =
251 browserState_->GetPrefs()->GetBoolean(prefs::kNetworkPredictionEnabled);
252 wifiOnly_ = browserState_->GetPrefs()->GetBoolean(
253 prefs::kNetworkPredictionWifiOnly);
254
255 if (wifiOnly_ && enabled_) {
256 if (!connectionTypeObserverBridge_.get()) {
257 usingWWAN_ = net::NetworkChangeNotifier::IsConnectionCellular(
258 net::NetworkChangeNotifier::GetConnectionType());
259 connectionTypeObserverBridge_.reset(
260 new ConnectionTypeObserverBridge(self));
261 }
262 if (usingWWAN_) {
263 [self cancelPrerender];
264 }
265 } else if (enabled_) {
266 connectionTypeObserverBridge_.reset();
267 } else {
268 [self cancelPrerender];
269 connectionTypeObserverBridge_.reset();
270 }
271 }
272}
273
274- (void)didReceiveMemoryWarning {
275 [self cancelPrerenderForReason:PRERENDER_FINAL_STATUS_MEMORY_LIMIT_EXCEEDED];
276}
277
278#pragma mark -
279#pragma mark CRWNativeContentProvider implementation
280
sdefresned9217bc2016-12-19 13:58:32281- (BOOL)hasControllerForURL:(const GURL&)url {
sdefresne2c600c52017-04-04 16:49:59282 if (!webState_)
283 return NO;
rohitraoeeb5293b2017-06-15 14:40:02284
285 return [delegate_ preloadHasNativeControllerForURL:url];
sdefresned9217bc2016-12-19 13:58:32286}
287
288// Override the CRWNativeContentProvider methods to cancel any prerenders that
289// require native content.
olivierrobind43eecb2017-01-27 20:35:26290- (id<CRWNativeContent>)controllerForURL:(const GURL&)url
291 webState:(web::WebState*)webState {
sdefresned9217bc2016-12-19 13:58:32292 [self schedulePrerenderCancel];
293 return nil;
294}
295
296// Override the CRWNativeContentProvider methods to cancel any prerenders that
297// require native content.
298- (id<CRWNativeContent>)controllerForURL:(const GURL&)url
299 withError:(NSError*)error
300 isPost:(BOOL)isPost {
301 [self schedulePrerenderCancel];
302 return nil;
303}
304
305#pragma mark -
306#pragma mark Private Methods
307
308- (BOOL)isPrerenderingEnabled {
309 DCHECK_CURRENTLY_ON(web::WebThread::UI);
310 return !IsPrerenderTabEvictionExperimentalGroup() && enabled_ &&
311 !ios::device_util::IsSingleCoreDevice() &&
312 ios::device_util::RamIsAtLeast512Mb() && (!wifiOnly_ || !usingWWAN_);
313}
314
sdefresned9217bc2016-12-19 13:58:32315- (BOOL)shouldPreloadURL:(const GURL&)url {
316 return url.SchemeIs(url::kHttpScheme) || url.SchemeIs(url::kHttpsScheme);
317}
318
319- (void)startPrerender {
320 // Destroy any existing prerenders before starting a new one.
321 [self destroyPreviewContents];
322 prerenderedURL_ = scheduledURL_;
323 scheduledURL_ = GURL();
324
325 DCHECK(prerenderedURL_.is_valid());
326 if (!prerenderedURL_.is_valid()) {
327 [self destroyPreviewContents];
328 return;
329 }
330
sdefresne2c600c52017-04-04 16:49:59331 web::WebState::CreateParams createParams(browserState_);
332 webState_ = web::WebState::Create(createParams);
333 AttachTabHelpers(webState_.get());
sdefresned9217bc2016-12-19 13:58:32334
sdefresne2c600c52017-04-04 16:49:59335 Tab* tab = LegacyTabHelper::GetTabForWebState(webState_.get());
336 DCHECK(tab);
337
338 [[tab webController] setNativeProvider:self];
sdefresne7d699dd2017-04-05 13:05:23339 webState_->SetWebUsageEnabled(true);
sdefresne2c600c52017-04-04 16:49:59340 [tab setIsPrerenderTab:YES];
341 [tab setDelegate:self];
edchincd32fdf2017-10-25 12:45:45342 if (AccountConsistencyService* accountConsistencyService =
343 ios::AccountConsistencyServiceFactory::GetForBrowserState(
344 browserState_)) {
345 accountConsistencyService->SetWebStateHandler(webState_.get(), self);
346 }
sdefresne2c600c52017-04-04 16:49:59347
Sylvain Defresnef5b8a8e2017-10-24 02:54:44348 HistoryTabHelper::FromWebState(webState_.get())
349 ->SetDelayHistoryServiceNotification(true);
350
sdefresne2c600c52017-04-04 16:49:59351 web::NavigationManager::WebLoadParams loadParams(prerenderedURL_);
352 loadParams.referrer = scheduledReferrer_;
353 loadParams.transition_type = scheduledTransition_;
rohitraoeeb5293b2017-06-15 14:40:02354 if ([delegate_ preloadShouldUseDesktopUserAgent]) {
sdefresne2c600c52017-04-04 16:49:59355 loadParams.user_agent_override_option =
356 web::NavigationManager::UserAgentOverrideOption::DESKTOP;
357 }
sdefresne7d699dd2017-04-05 13:05:23358 webState_->GetNavigationManager()->LoadURLWithParams(loadParams);
sdefresned9217bc2016-12-19 13:58:32359
360 // Trigger the page to start loading.
sdefresne7d699dd2017-04-05 13:05:23361 // TODO(crbug.com/705819): Remove this call.
sdefresne2c600c52017-04-04 16:49:59362 [tab view];
sdefresned9217bc2016-12-19 13:58:32363}
364
sdefresned9217bc2016-12-19 13:58:32365- (void)destroyPreviewContents {
366 [self destroyPreviewContentsForReason:PRERENDER_FINAL_STATUS_CANCELLED];
367}
368
369- (void)destroyPreviewContentsForReason:(PrerenderFinalStatus)reason {
sdefresne2c600c52017-04-04 16:49:59370 if (!webState_)
sdefresned9217bc2016-12-19 13:58:32371 return;
372
373 UMA_HISTOGRAM_ENUMERATION(kPrerenderFinalStatusHistogramName, reason,
374 PRERENDER_FINAL_STATUS_MAX);
sdefresne2c600c52017-04-04 16:49:59375
376 Tab* tab = LegacyTabHelper::GetTabForWebState(webState_.get());
377 [[tab webController] setNativeProvider:nil];
378 webState_.reset();
379
sdefresned9217bc2016-12-19 13:58:32380 prerenderedURL_ = GURL();
381}
382
383- (void)schedulePrerenderCancel {
Rohit Rao44f204302017-08-10 14:49:54384 // TODO(crbug.com/228550): Instead of cancelling the prerender, should we mark
385 // it as failed instead? That way, subsequent prerender requests for the same
386 // URL will not kick off new prerenders.
sdefresned9217bc2016-12-19 13:58:32387 [self removeScheduledPrerenderRequests];
388 [self performSelector:@selector(cancelPrerender) withObject:nil afterDelay:0];
389}
390
391- (void)removeScheduledPrerenderRequests {
392 [NSObject cancelPreviousPerformRequestsWithTarget:self];
393 scheduledURL_ = GURL();
394}
395
396#pragma mark - TabDelegate
397
398- (void)discardPrerender {
399 [self schedulePrerenderCancel];
400}
401
edchincd32fdf2017-10-25 12:45:45402#pragma mark - ManageAccountsDelegate
403
404- (void)onManageAccounts {
405 [self discardPrerender];
406}
407
408- (void)onAddAccount {
409 [self discardPrerender];
410}
411
412- (void)onGoIncognito:(const GURL&)url {
413 [self discardPrerender];
414}
415
sdefresned9217bc2016-12-19 13:58:32416@end