blob: d9dccad7e56fc54d10c3fca180a83b1cc2242d25 [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
Hans Wennborg3a2fb61f2020-05-11 15:51:169#include "base/check_op.h"
sdefresned9217bc2016-12-19 13:58:3210#include "base/ios/device_util.h"
sdefresned9217bc2016-12-19 13:58:3211#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"
Moe Ahmadi7431d0b2018-04-13 17:32:2014#import "components/prefs/ios/pref_observer_bridge.h"
sdefresned9217bc2016-12-19 13:58:3215#include "components/prefs/pref_service.h"
edchincd32fdf2017-10-25 12:45:4516#import "components/signin/ios/browser/account_consistency_service.h"
mrefaat3ad5e412018-08-02 20:11:3417#import "ios/chrome/browser/app_launcher/app_launcher_tab_helper.h"
sdefresned9217bc2016-12-19 13:58:3218#include "ios/chrome/browser/browser_state/chrome_browser_state.h"
mrefaat4aec47b2019-03-29 18:44:2119#include "ios/chrome/browser/crash_report/crash_report_helper.h"
Sylvain Defresnef5d2d952017-11-14 11:15:3120#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"
mrefaat3ad5e412018-08-02 20:11:3422#import "ios/chrome/browser/itunes_urls/itunes_urls_handler_tab_helper.h"
sdefresned9217bc2016-12-19 13:58:3223#include "ios/chrome/browser/pref_names.h"
Rohit Rao44f204302017-08-10 14:49:5424#include "ios/chrome/browser/prerender/preload_controller_delegate.h"
Stepan Khapugin193af312020-10-01 16:18:4025#import "ios/chrome/browser/prerender/prerender_pref.h"
edchincd32fdf2017-10-25 12:45:4526#import "ios/chrome/browser/signin/account_consistency_service_factory.h"
sdefresne2c600c52017-04-04 16:49:5927#import "ios/chrome/browser/tabs/tab_helper_util.h"
Eugene Butde5ddcdc2019-07-23 00:23:4228#import "ios/web/public/navigation/navigation_item.h"
29#import "ios/web/public/navigation/navigation_manager.h"
30#import "ios/web/public/navigation/web_state_policy_decider_bridge.h"
Yi Sua98c08fb2019-06-13 09:03:5631#include "ios/web/public/thread/web_thread.h"
Kurt Horimoto271707392019-09-21 00:28:0832#import "ios/web/public/ui/java_script_dialog_presenter.h"
Justin Cohen6836cfc52019-10-02 13:28:5833#include "ios/web/public/web_client.h"
Eugene Butd3564aaea2019-08-13 15:50:4534#import "ios/web/public/web_state.h"
Eugene But45bb9962019-09-16 17:34:2135#import "ios/web/public/web_state_observer_bridge.h"
sdefresned9217bc2016-12-19 13:58:3236#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
mrefaat3ad5e412018-08-02 20:11:3443using web::WebStatePolicyDecider;
44
Kurt Horimotoeb0081e2019-09-26 18:53:3545// Protocol used to cancel a scheduled preload request.
46@protocol PreloadCancelling <NSObject>
Kurt Horimoto271707392019-09-21 00:28:0847
48// Schedules the current prerender to be cancelled during the next run of the
49// event loop.
50- (void)schedulePrerenderCancel;
51
Kurt Horimoto271707392019-09-21 00:28:0852@end
53
54namespace {
55
56// PrerenderFinalStatus values are used in the "Prerender.FinalStatus" histogram
57// and new values needs to be kept in sync with histogram.xml.
58enum PrerenderFinalStatus {
59 PRERENDER_FINAL_STATUS_USED = 0,
60 PRERENDER_FINAL_STATUS_MEMORY_LIMIT_EXCEEDED = 12,
61 PRERENDER_FINAL_STATUS_CANCELLED = 32,
62 PRERENDER_FINAL_STATUS_MAX = 52,
63};
64
65// Delay before starting to prerender a URL.
66const NSTimeInterval kPrerenderDelay = 0.5;
67
68// The finch experiment to turn off prerendering as a field trial.
69const char kTabEvictionFieldTrialName[] = "TabEviction";
70// The associated group.
71const char kPrerenderTabEvictionTrialGroup[] = "NoPrerendering";
72// The name of the histogram for recording final status (e.g. used/cancelled)
73// of prerender requests.
74const char kPrerenderFinalStatusHistogramName[] = "Prerender.FinalStatus";
75// The name of the histogram for recording the number of successful prerenders.
76const char kPrerendersPerSessionCountHistogramName[] =
77 "Prerender.PrerendersPerSessionCount";
78// The name of the histogram for recording time until a successful prerender.
Gauthier Ambard6d3ed3472020-05-11 09:11:4879const char kPrerenderPrerenderTimeSaved[] = "Prerender.PrerenderTimeSaved";
80// Histogram to record that the load was complete when the prerender was used.
81// Not recorded if the pre-render isn't used.
82const char kPrerenderLoadComplete[] = "Prerender.PrerenderLoadComplete";
Kurt Horimoto271707392019-09-21 00:28:0883
84// Is this install selected for this particular experiment.
85bool IsPrerenderTabEvictionExperimentalGroup() {
86 base::FieldTrial* trial =
87 base::FieldTrialList::Find(kTabEvictionFieldTrialName);
88 return trial && trial->group_name() == kPrerenderTabEvictionTrialGroup;
89}
90
Kurt Horimotoeb0081e2019-09-26 18:53:3591// Returns whether |url| can be prerendered.
92bool CanPrerenderURL(const GURL& url) {
93 // Prerendering is only enabled for http and https URLs.
94 return url.is_valid() &&
95 (url.SchemeIs(url::kHttpScheme) || url.SchemeIs(url::kHttpsScheme));
96}
97
98// Object used to schedule prerenders.
99class PrerenderRequest {
100 public:
101 PrerenderRequest() {}
102 PrerenderRequest(const GURL& url,
103 ui::PageTransition transition,
104 const web::Referrer& referrer)
105 : url_(url), transition_(transition), referrer_(referrer) {}
106
107 const GURL& url() const { return url_; }
108 ui::PageTransition transition() const { return transition_; }
109 const web::Referrer referrer() const { return referrer_; }
110
111 private:
112 const GURL url_;
113 const ui::PageTransition transition_ = ui::PAGE_TRANSITION_LINK;
114 const web::Referrer referrer_;
115};
116
Kurt Horimoto271707392019-09-21 00:28:08117// A no-op JavaScriptDialogPresenter that cancels prerendering when the
118// prerendered page attempts to show dialogs.
119class PreloadJavaScriptDialogPresenter : public web::JavaScriptDialogPresenter {
120 public:
Kurt Horimotoeb0081e2019-09-26 18:53:35121 explicit PreloadJavaScriptDialogPresenter(
122 id<PreloadCancelling> cancel_handler)
123 : cancel_handler_(cancel_handler) {
124 DCHECK(cancel_handler_);
Kurt Horimoto271707392019-09-21 00:28:08125 }
126
127 // web::JavaScriptDialogPresenter:
128 void RunJavaScriptDialog(web::WebState* web_state,
129 const GURL& origin_url,
130 web::JavaScriptDialogType dialog_type,
131 NSString* message_text,
132 NSString* default_prompt_text,
133 web::DialogClosedCallback callback) override {
134 std::move(callback).Run(NO, nil);
Kurt Horimotoeb0081e2019-09-26 18:53:35135 [cancel_handler_ schedulePrerenderCancel];
Kurt Horimoto271707392019-09-21 00:28:08136 }
137
138 void CancelDialogs(web::WebState* web_state) override {}
139
140 private:
Kurt Horimotoeb0081e2019-09-26 18:53:35141 __weak id<PreloadCancelling> cancel_handler_ = nil;
Kurt Horimoto271707392019-09-21 00:28:08142};
Justin Cohen340b4992020-10-09 20:28:27143
144// Maximum time to let a cancelled webState attempt to finish restore.
145static const size_t kMaximumCancelledWebStateDelay = 2;
146
147// Used to enable the workaround for a WebKit crash, see crbug.com/1032928.
148const base::Feature kPreloadDelayWebStateReset{
149 "PreloadDelayWebStateReset", base::FEATURE_DISABLED_BY_DEFAULT};
150
Kurt Horimoto271707392019-09-21 00:28:08151} // namespace
152
Kurt Horimotoeb0081e2019-09-26 18:53:35153@interface PreloadController () <CRConnectionTypeObserverBridge,
154 CRWWebStateDelegate,
155 CRWWebStateObserver,
156 CRWWebStatePolicyDecider,
157 ManageAccountsDelegate,
158 PrefObserverDelegate,
159 PreloadCancelling> {
160 std::unique_ptr<web::WebStateDelegateBridge> _webStateDelegate;
161 std::unique_ptr<web::WebStateObserverBridge> _webStateObserver;
162 std::unique_ptr<PrefObserverBridge> _observerBridge;
163 std::unique_ptr<ConnectionTypeObserverBridge> _connectionTypeObserver;
164 std::unique_ptr<web::WebStatePolicyDeciderBridge> _policyDeciderBridge;
sdefresned9217bc2016-12-19 13:58:32165
Kurt Horimotoeb0081e2019-09-26 18:53:35166 // The WebState used for prerendering.
167 std::unique_ptr<web::WebState> _webState;
168
169 // The scheduled request.
170 std::unique_ptr<PrerenderRequest> _scheduledRequest;
171
172 // Registrar for pref changes notifications.
173 PrefChangeRegistrar _prefChangeRegistrar;
174
175 // The dialog presenter.
176 std::unique_ptr<web::JavaScriptDialogPresenter> _dialogPresenter;
177}
178
179// The ChromeBrowserState passed on initialization.
Sylvain Defresne8f0cda2b2020-01-24 11:50:00180@property(nonatomic) ChromeBrowserState* browserState;
Kurt Horimotoeb0081e2019-09-26 18:53:35181
182// Redefine property as readwrite. The URL that is prerendered in |_webState|.
183// This can be different from the value returned by WebState last committed
184// navigation item, for example in cases where there was a redirect.
185//
186// When choosing whether or not to use a prerendered Tab,
187// BrowserViewController compares the URL being loaded by the omnibox with the
188// URL of the prerendered Tab. Comparing against the Tab's currently URL
189// could return false negatives in cases of redirect, hence the need to store
190// the originally prerendered URL.
191@property(nonatomic, readwrite, assign) GURL prerenderedURL;
192
193// The URL in the currently scheduled prerender request, or an empty one if
194// there is no prerender scheduled.
195@property(nonatomic, readonly) const GURL& scheduledURL;
196
Stepan Khapugin193af312020-10-01 16:18:40197// Network prediction settings.
198@property(nonatomic)
199 prerender_prefs::NetworkPredictionSetting networkPredictionSetting;
Kurt Horimotoeb0081e2019-09-26 18:53:35200
201// Whether or not the current connection is using WWAN.
Stepan Khapugin193af312020-10-01 16:18:40202@property(nonatomic, assign) BOOL isOnCellularNetwork;
Kurt Horimotoeb0081e2019-09-26 18:53:35203
204// Number of successful prerenders (i.e. the user viewed the prerendered page)
205// during the lifetime of this controller.
206@property(nonatomic) NSUInteger successfulPrerendersPerSessionCount;
207
208// Tracks the time of the last attempt to load a prerender URL. Used for UMA
209// reporting of load durations.
210@property(nonatomic) base::TimeTicks startTime;
211
Gauthier Ambard6d3ed3472020-05-11 09:11:48212// Whether the load was completed or not.
213@property(nonatomic, assign) BOOL loadCompleted;
214// The time between the start of the load and the completion (only valid if the
215// load completed).
216@property(nonatomic) base::TimeDelta completionTime;
217
Kurt Horimotoeb0081e2019-09-26 18:53:35218// Called to start any scheduled prerendering requests.
219- (void)startPrerender;
220
221// Destroys the preview Tab and resets |prerenderURL_| to the empty URL.
222- (void)destroyPreviewContents;
223
224// Removes any scheduled prerender requests and resets |scheduledURL| to the
225// empty URL.
226- (void)removeScheduledPrerenderRequests;
227
228// Records metric on a successful prerender.
229- (void)recordReleaseMetrics;
230
231@end
232
233@implementation PreloadController
Kurt Horimoto38bfb152019-09-25 21:05:41234
Sylvain Defresne8f0cda2b2020-01-24 11:50:00235- (instancetype)initWithBrowserState:(ChromeBrowserState*)browserState {
sdefresned9217bc2016-12-19 13:58:32236 DCHECK(browserState);
237 DCHECK_CURRENTLY_ON(web::WebThread::UI);
238 if ((self = [super init])) {
Kurt Horimotoeb0081e2019-09-26 18:53:35239 _browserState = browserState;
Stepan Khapugin193af312020-10-01 16:18:40240 _networkPredictionSetting =
241 static_cast<prerender_prefs::NetworkPredictionSetting>(
242 _browserState->GetPrefs()->GetInteger(
243 prefs::kNetworkPredictionSetting));
244 _isOnCellularNetwork = net::NetworkChangeNotifier::IsConnectionCellular(
sdefresned9217bc2016-12-19 13:58:32245 net::NetworkChangeNotifier::GetConnectionType());
Kurt Horimotoeb0081e2019-09-26 18:53:35246 _webStateDelegate = std::make_unique<web::WebStateDelegateBridge>(self);
247 _webStateObserver = std::make_unique<web::WebStateObserverBridge>(self);
248 _observerBridge = std::make_unique<PrefObserverBridge>(self);
249 _prefChangeRegistrar.Init(_browserState->GetPrefs());
250 _observerBridge->ObserveChangesForPreference(
Stepan Khapugin193af312020-10-01 16:18:40251 prefs::kNetworkPredictionSetting, &_prefChangeRegistrar);
Kurt Horimotoeb0081e2019-09-26 18:53:35252 _dialogPresenter = std::make_unique<PreloadJavaScriptDialogPresenter>(self);
Stepan Khapugin193af312020-10-01 16:18:40253 if (_networkPredictionSetting ==
254 prerender_prefs::NetworkPredictionSetting::kEnabledWifiOnly) {
Kurt Horimotoeb0081e2019-09-26 18:53:35255 _connectionTypeObserver =
Sylvain Defresne949367262018-03-02 22:33:16256 std::make_unique<ConnectionTypeObserverBridge>(self);
sdefresned9217bc2016-12-19 13:58:32257 }
258
259 [[NSNotificationCenter defaultCenter]
260 addObserver:self
261 selector:@selector(didReceiveMemoryWarning)
262 name:UIApplicationDidReceiveMemoryWarningNotification
263 object:nil];
264 }
265 return self;
266}
267
sdefresned9217bc2016-12-19 13:58:32268- (void)dealloc {
Steven Holte95922222018-09-14 20:06:23269 UMA_HISTOGRAM_COUNTS_1M(kPrerendersPerSessionCountHistogramName,
Kurt Horimotoeb0081e2019-09-26 18:53:35270 self.successfulPrerendersPerSessionCount);
sdefresned9217bc2016-12-19 13:58:32271 [self cancelPrerender];
sdefresned9217bc2016-12-19 13:58:32272}
273
Kurt Horimotoeb0081e2019-09-26 18:53:35274#pragma mark - Accessors
275
276- (const GURL&)scheduledURL {
277 return _scheduledRequest ? _scheduledRequest->url() : GURL::EmptyGURL();
278}
279
280- (BOOL)isEnabled {
281 DCHECK_CURRENTLY_ON(web::WebThread::UI);
Stepan Khapugin193af312020-10-01 16:18:40282
283 if (IsPrerenderTabEvictionExperimentalGroup() ||
284 ios::device_util::IsSingleCoreDevice() ||
285 !ios::device_util::RamIsAtLeast512Mb() ||
286 net::NetworkChangeNotifier::IsOffline()) {
287 return false;
288 }
289
290 switch (self.networkPredictionSetting) {
291 case prerender_prefs::NetworkPredictionSetting::kEnabledWifiOnly: {
292 return !self.isOnCellularNetwork;
293 }
294
295 case prerender_prefs::NetworkPredictionSetting::kEnabledWifiAndCellular: {
296 return true;
297 }
298
299 case prerender_prefs::NetworkPredictionSetting::kDisabled: {
300 return false;
301 }
302 }
Kurt Horimotoeb0081e2019-09-26 18:53:35303}
304
Gauthier Ambard6d3ed3472020-05-11 09:11:48305- (void)setLoadCompleted:(BOOL)loadCompleted {
306 if (_loadCompleted == loadCompleted)
307 return;
308
309 _loadCompleted = loadCompleted;
310
311 if (loadCompleted) {
312 self.completionTime = base::TimeTicks::Now() - self.startTime;
313 } else {
314 self.completionTime = base::TimeDelta();
315 }
316}
317
Kurt Horimotoeb0081e2019-09-26 18:53:35318#pragma mark - Public
319
320- (void)browserStateDestroyed {
321 [self cancelPrerender];
322 _connectionTypeObserver.reset();
323}
324
sdefresned9217bc2016-12-19 13:58:32325- (void)prerenderURL:(const GURL&)url
326 referrer:(const web::Referrer&)referrer
327 transition:(ui::PageTransition)transition
328 immediately:(BOOL)immediately {
Kurt Horimotoeb0081e2019-09-26 18:53:35329 // TODO(crbug.com/754050): If CanPrerenderURL() returns false, should we
Rohit Rao44f204302017-08-10 14:49:54330 // cancel any scheduled prerender requests?
Kurt Horimotoeb0081e2019-09-26 18:53:35331 if (!self.enabled || !CanPrerenderURL(url))
sdefresned9217bc2016-12-19 13:58:32332 return;
333
334 // Ignore this request if there is already a scheduled request for the same
335 // URL; or, if there is no scheduled request, but the currently prerendered
336 // page matches this URL.
Kurt Horimotoeb0081e2019-09-26 18:53:35337 if (url == self.scheduledURL ||
338 (self.scheduledURL.is_empty() && url == self.prerenderedURL)) {
sdefresned9217bc2016-12-19 13:58:32339 return;
340 }
341
342 [self removeScheduledPrerenderRequests];
Kurt Horimotoeb0081e2019-09-26 18:53:35343 _scheduledRequest =
344 std::make_unique<PrerenderRequest>(url, transition, referrer);
sdefresned9217bc2016-12-19 13:58:32345
346 NSTimeInterval delay = immediately ? 0.0 : kPrerenderDelay;
347 [self performSelector:@selector(startPrerender)
348 withObject:nil
349 afterDelay:delay];
350}
351
sdefresned9217bc2016-12-19 13:58:32352- (void)cancelPrerender {
353 [self cancelPrerenderForReason:PRERENDER_FINAL_STATUS_CANCELLED];
354}
355
356- (void)cancelPrerenderForReason:(PrerenderFinalStatus)reason {
357 [self removeScheduledPrerenderRequests];
358 [self destroyPreviewContentsForReason:reason];
359}
360
Sylvain Defresnef5d2d952017-11-14 11:15:31361- (BOOL)isWebStatePrerendered:(web::WebState*)webState {
Kurt Horimotoeb0081e2019-09-26 18:53:35362 return webState && _webState.get() == webState;
Sylvain Defresnef5d2d952017-11-14 11:15:31363}
364
Justin Cohenceff1a212019-10-01 20:43:43365- (std::unique_ptr<web::WebState>)releasePrerenderContents {
Justin Cohen6836cfc52019-10-02 13:28:58366 if (!_webState ||
367 _webState->GetNavigationManager()->IsRestoreSessionInProgress())
Kurt Horimotoeb0081e2019-09-26 18:53:35368 return nullptr;
369
370 self.successfulPrerendersPerSessionCount++;
Justin Cohen118fee6c2018-12-06 19:36:59371 [self recordReleaseMetrics];
sdefresned9217bc2016-12-19 13:58:32372 [self removeScheduledPrerenderRequests];
Kurt Horimotoeb0081e2019-09-26 18:53:35373 self.prerenderedURL = GURL();
374 self.startTime = base::TimeTicks();
Gauthier Ambard6d3ed3472020-05-11 09:11:48375 self.loadCompleted = NO;
Sylvain Defresnef5b8a8e2017-10-24 02:54:44376
Sylvain Defresnef5d2d952017-11-14 11:15:31377 // Move the pre-rendered WebState to a local variable so that it will no
378 // longer be considered as pre-rendering (otherwise tab helpers may early
379 // exist when invoked).
Kurt Horimotoeb0081e2019-09-26 18:53:35380 std::unique_ptr<web::WebState> webState = std::move(_webState);
Sylvain Defresnef5d2d952017-11-14 11:15:31381 DCHECK(![self isWebStatePrerendered:webState.get()]);
edchincd32fdf2017-10-25 12:45:45382
Kurt Horimotoeb0081e2019-09-26 18:53:35383 webState->RemoveObserver(_webStateObserver.get());
Olivier Robin984283a2020-06-22 11:08:50384 breakpad::StopMonitoringURLsForPreloadWebState(webState.get());
Sylvain Defresnef5d2d952017-11-14 11:15:31385 webState->SetDelegate(nullptr);
Kurt Horimotoeb0081e2019-09-26 18:53:35386 _policyDeciderBridge.reset();
Gauthier Ambard5144bfbd2017-11-15 10:07:52387 HistoryTabHelper::FromWebState(webState.get())
Sylvain Defresnef5d2d952017-11-14 11:15:31388 ->SetDelayHistoryServiceNotification(false);
389
390 if (AccountConsistencyService* accountConsistencyService =
391 ios::AccountConsistencyServiceFactory::GetForBrowserState(
Kurt Horimotoeb0081e2019-09-26 18:53:35392 self.browserState)) {
Gauthier Ambard5144bfbd2017-11-15 10:07:52393 accountConsistencyService->RemoveWebStateHandler(webState.get());
Sylvain Defresnef5d2d952017-11-14 11:15:31394 }
395
mrefaata4bdb95b2018-12-04 18:36:10396 if (!webState->IsLoading()) {
mrefaatc977e372019-02-04 22:19:18397 [[OmniboxGeolocationController sharedInstance]
398 finishPageLoadForWebState:webState.get()
399 loadSuccess:YES];
sdefresne2c600c52017-04-04 16:49:59400 }
401
Sylvain Defresnef5d2d952017-11-14 11:15:31402 return webState;
sdefresned9217bc2016-12-19 13:58:32403}
404
Kurt Horimotoeb0081e2019-09-26 18:53:35405#pragma mark - CRConnectionTypeObserverBridge
406
sdefresned9217bc2016-12-19 13:58:32407- (void)connectionTypeChanged:(net::NetworkChangeNotifier::ConnectionType)type {
408 DCHECK_CURRENTLY_ON(web::WebThread::UI);
Stepan Khapugin193af312020-10-01 16:18:40409 self.isOnCellularNetwork =
410 net::NetworkChangeNotifier::IsConnectionCellular(type);
411 if (self.networkPredictionSetting ==
412 prerender_prefs::NetworkPredictionSetting::kEnabledWifiOnly &&
413 self.isOnCellularNetwork) {
sdefresned9217bc2016-12-19 13:58:32414 [self cancelPrerender];
Stepan Khapugin193af312020-10-01 16:18:40415 }
sdefresned9217bc2016-12-19 13:58:32416}
417
Sylvain Defresnef5d2d952017-11-14 11:15:31418#pragma mark - CRWWebStateDelegate
419
Eugene But6c41f5e2018-11-09 00:39:32420- (web::WebState*)webState:(web::WebState*)webState
421 createNewWebStateForURL:(const GURL&)URL
422 openerURL:(const GURL&)openerURL
423 initiatedByUser:(BOOL)initiatedByUser {
424 DCHECK([self isWebStatePrerendered:webState]);
425 [self schedulePrerenderCancel];
426 return nil;
427}
428
429- (web::JavaScriptDialogPresenter*)javaScriptDialogPresenterForWebState:
430 (web::WebState*)webState {
431 DCHECK([self isWebStatePrerendered:webState]);
Kurt Horimotoeb0081e2019-09-26 18:53:35432 return _dialogPresenter.get();
Eugene But6c41f5e2018-11-09 00:39:32433}
434
Sylvain Defresnef5d2d952017-11-14 11:15:31435- (void)webState:(web::WebState*)webState
436 didRequestHTTPAuthForProtectionSpace:(NSURLProtectionSpace*)protectionSpace
437 proposedCredential:(NSURLCredential*)proposedCredential
438 completionHandler:(void (^)(NSString* username,
439 NSString* password))handler {
440 DCHECK([self isWebStatePrerendered:webState]);
441 [self schedulePrerenderCancel];
442 if (handler) {
443 handler(nil, nil);
444 }
445}
446
Gauthier Ambard99f408192020-08-03 12:56:46447- (UIView*)webViewContainerForWebState:(web::WebState*)webState {
448 return [self.delegate webViewContainer];
449}
450
Sylvain Defresne949367262018-03-02 22:33:16451#pragma mark - CRWWebStateObserver
sdefresned9217bc2016-12-19 13:58:32452
Sylvain Defresne949367262018-03-02 22:33:16453- (void)webState:(web::WebState*)webState
Gauthier Ambard2b604d82019-11-05 13:23:15454 didFinishNavigation:(web::NavigationContext*)navigation {
455 DCHECK_EQ(webState, _webState.get());
456 if ([self shouldCancelPreloadForMimeType:webState->GetContentsMimeType()])
457 [self schedulePrerenderCancel];
458}
459
460- (void)webState:(web::WebState*)webState
Sylvain Defresne949367262018-03-02 22:33:16461 didLoadPageWithSuccess:(BOOL)loadSuccess {
Kurt Horimotoeb0081e2019-09-26 18:53:35462 DCHECK_EQ(webState, _webState.get());
Gauthier Ambard2b604d82019-11-05 13:23:15463 // The load should have been cancelled when the navigation finishes, but this
464 // makes sure that we didn't miss one.
Gauthier Ambard6d3ed3472020-05-11 09:11:48465 if ([self shouldCancelPreloadForMimeType:webState->GetContentsMimeType()]) {
Sylvain Defresne949367262018-03-02 22:33:16466 [self schedulePrerenderCancel];
Gauthier Ambard6d3ed3472020-05-11 09:11:48467 } else if (loadSuccess) {
468 self.loadCompleted = YES;
469 }
Sylvain Defresne949367262018-03-02 22:33:16470}
471
mrefaat3ad5e412018-08-02 20:11:34472#pragma mark - CRWWebStatePolicyDecider
473
Mike Dougherty15f6db52020-04-09 19:13:07474- (WebStatePolicyDecider::PolicyDecision)
475 shouldAllowRequest:(NSURLRequest*)request
476 requestInfo:(const WebStatePolicyDecider::RequestInfo&)info {
mrefaat3ad5e412018-08-02 20:11:34477 GURL requestURL = net::GURLWithNSURL(request.URL);
478 // Don't allow preloading for requests that are handled by opening another
479 // application or by presenting a native UI.
480 if (AppLauncherTabHelper::IsAppUrl(requestURL) ||
481 ITunesUrlsHandlerTabHelper::CanHandleUrl(requestURL)) {
482 [self schedulePrerenderCancel];
Mike Dougherty15f6db52020-04-09 19:13:07483 return WebStatePolicyDecider::PolicyDecision::Cancel();
mrefaat3ad5e412018-08-02 20:11:34484 }
Mike Dougherty15f6db52020-04-09 19:13:07485 return WebStatePolicyDecider::PolicyDecision::Allow();
mrefaat3ad5e412018-08-02 20:11:34486}
Kurt Horimotoeb0081e2019-09-26 18:53:35487
488#pragma mark - ManageAccountsDelegate
489
490- (void)onManageAccounts {
491 [self schedulePrerenderCancel];
492}
493
Nohemi Fernandez753c2f62020-09-09 15:18:41494- (void)onShowConsistencyPromo {
495 [self schedulePrerenderCancel];
496}
497
Kurt Horimotoeb0081e2019-09-26 18:53:35498- (void)onAddAccount {
499 [self schedulePrerenderCancel];
500}
501
502- (void)onGoIncognito:(const GURL&)url {
503 [self schedulePrerenderCancel];
504}
505
506#pragma mark - PrefObserverDelegate
507
508- (void)onPreferenceChanged:(const std::string&)preferenceName {
Stepan Khapugin193af312020-10-01 16:18:40509 if (preferenceName == prefs::kNetworkPredictionSetting) {
Kurt Horimotoeb0081e2019-09-26 18:53:35510 DCHECK_CURRENTLY_ON(web::WebThread::UI);
511 // The logic is simpler if both preferences changes are handled equally.
Stepan Khapugin193af312020-10-01 16:18:40512 self.networkPredictionSetting =
513 static_cast<prerender_prefs::NetworkPredictionSetting>(
514 self.browserState->GetPrefs()->GetInteger(
515 prefs::kNetworkPredictionSetting));
Kurt Horimotoeb0081e2019-09-26 18:53:35516
Stepan Khapugin193af312020-10-01 16:18:40517 switch (self.networkPredictionSetting) {
518 case prerender_prefs::NetworkPredictionSetting::kEnabledWifiOnly: {
519 if (!_connectionTypeObserver.get()) {
520 self.isOnCellularNetwork =
521 net::NetworkChangeNotifier::IsConnectionCellular(
522 net::NetworkChangeNotifier::GetConnectionType());
523 _connectionTypeObserver =
524 std::make_unique<ConnectionTypeObserverBridge>(self);
525 }
526 if (self.isOnCellularNetwork) {
527 [self cancelPrerender];
528 }
529 break;
Kurt Horimotoeb0081e2019-09-26 18:53:35530 }
Stepan Khapugin193af312020-10-01 16:18:40531
532 case prerender_prefs::NetworkPredictionSetting::kEnabledWifiAndCellular: {
533 _connectionTypeObserver.reset();
534 break;
535 }
536
537 case prerender_prefs::NetworkPredictionSetting::kDisabled: {
Kurt Horimotoeb0081e2019-09-26 18:53:35538 [self cancelPrerender];
Stepan Khapugin193af312020-10-01 16:18:40539 _connectionTypeObserver.reset();
540 break;
Kurt Horimotoeb0081e2019-09-26 18:53:35541 }
Kurt Horimotoeb0081e2019-09-26 18:53:35542 }
543 }
544}
545
546#pragma mark - PreloadCancelling
547
548- (void)schedulePrerenderCancel {
549 // TODO(crbug.com/228550): Instead of cancelling the prerender, should we mark
550 // it as failed instead? That way, subsequent prerender requests for the same
551 // URL will not kick off new prerenders.
552 [self removeScheduledPrerenderRequests];
553 [self performSelector:@selector(cancelPrerender) withObject:nil afterDelay:0];
554}
555
556#pragma mark - Cancellation Helpers
557
Gauthier Ambard2b604d82019-11-05 13:23:15558- (BOOL)shouldCancelPreloadForMimeType:(std::string)mimeType {
559 // Cancel prerendering if response is "application/octet-stream". It can be a
560 // video file which should not be played from preload tab. See issue at
561 // http://crbug.com/436813 for more details.
562 // On iOS 13, PDF are getting focused when loaded, preventing the user from
563 // typing in the omnibox. See crbug.com/1017352.
564 return mimeType == "application/octet-stream" ||
565 mimeType == "application/pdf";
566}
567
Kurt Horimotoeb0081e2019-09-26 18:53:35568- (void)removeScheduledPrerenderRequests {
569 [NSObject cancelPreviousPerformRequestsWithTarget:self];
570 _scheduledRequest = nullptr;
571}
572
573#pragma mark - Prerender Helpers
574
575- (void)startPrerender {
576 // Destroy any existing prerenders before starting a new one.
577 [self destroyPreviewContents];
578 self.prerenderedURL = self.scheduledURL;
579 std::unique_ptr<PrerenderRequest> request = std::move(_scheduledRequest);
580
Justin Cohen6836cfc52019-10-02 13:28:58581 web::WebState* webStateToReplace = [self.delegate webStateToReplace];
582 if (!self.prerenderedURL.is_valid() || !webStateToReplace) {
Kurt Horimotoeb0081e2019-09-26 18:53:35583 [self destroyPreviewContents];
584 return;
585 }
586
587 web::WebState::CreateParams createParams(self.browserState);
Yi Suee7be512019-12-09 13:25:10588 _webState = web::WebState::CreateWithStorageSession(
589 createParams, webStateToReplace->BuildSessionStorage());
Justin Cohen6836cfc52019-10-02 13:28:58590
Kurt Horimotoeb0081e2019-09-26 18:53:35591 // Add the preload controller as a policyDecider before other tab helpers, so
592 // that it can block the navigation if needed before other policy deciders
593 // execute thier side effects (eg. AppLauncherTabHelper launching app).
594 _policyDeciderBridge =
595 std::make_unique<web::WebStatePolicyDeciderBridge>(_webState.get(), self);
596 AttachTabHelpers(_webState.get(), /*for_prerender=*/true);
597
Kurt Horimotoeb0081e2019-09-26 18:53:35598 _webState->SetDelegate(_webStateDelegate.get());
599 _webState->AddObserver(_webStateObserver.get());
Olivier Robin984283a2020-06-22 11:08:50600 breakpad::MonitorURLsForPreloadWebState(_webState.get());
Kurt Horimotoeb0081e2019-09-26 18:53:35601 _webState->SetWebUsageEnabled(true);
602
603 if (AccountConsistencyService* accountConsistencyService =
604 ios::AccountConsistencyServiceFactory::GetForBrowserState(
605 self.browserState)) {
606 accountConsistencyService->SetWebStateHandler(_webState.get(), self);
607 }
608
609 HistoryTabHelper::FromWebState(_webState.get())
610 ->SetDelayHistoryServiceNotification(true);
611
612 web::NavigationManager::WebLoadParams loadParams(self.prerenderedURL);
613 loadParams.referrer = request->referrer();
614 loadParams.transition_type = request->transition();
Kurt Horimotoeb0081e2019-09-26 18:53:35615 _webState->SetKeepRenderProcessAlive(true);
616 _webState->GetNavigationManager()->LoadURLWithParams(loadParams);
617
618 // LoadIfNecessary is needed because the view is not created (but needed) when
619 // loading the page. TODO(crbug.com/705819): Remove this call.
620 _webState->GetNavigationManager()->LoadIfNecessary();
621
622 self.startTime = base::TimeTicks::Now();
Gauthier Ambard6d3ed3472020-05-11 09:11:48623 self.loadCompleted = NO;
Kurt Horimotoeb0081e2019-09-26 18:53:35624}
625
626#pragma mark - Teardown Helpers
627
628- (void)destroyPreviewContents {
629 [self destroyPreviewContentsForReason:PRERENDER_FINAL_STATUS_CANCELLED];
630}
631
632- (void)destroyPreviewContentsForReason:(PrerenderFinalStatus)reason {
633 if (!_webState)
634 return;
635
636 UMA_HISTOGRAM_ENUMERATION(kPrerenderFinalStatusHistogramName, reason,
637 PRERENDER_FINAL_STATUS_MAX);
638
Kurt Horimotoeb0081e2019-09-26 18:53:35639 _webState->RemoveObserver(_webStateObserver.get());
Olivier Robin984283a2020-06-22 11:08:50640 breakpad::StopMonitoringURLsForPreloadWebState(_webState.get());
Kurt Horimotoeb0081e2019-09-26 18:53:35641 _webState->SetDelegate(nullptr);
Kurt Horimotoeb0081e2019-09-26 18:53:35642
Justin Cohen340b4992020-10-09 20:28:27643 // Preload appears to trigger an edge-case crash in WebKit when a restore is
644 // triggered and cancelled before it can complete. This isn't specific to
645 // preload, but is very easy to trigger in preload. As a speculative fix, if
646 // a preload is in restore, don't destroy it until after restore is complete.
647 // This logic should really belong in WebState itself, so any attempt to
648 // destroy a WebState during restore will trigger this logic. Even better,
649 // this edge case crash should be fixed in WebKit:
650 // https://bugs.webkit.org/show_bug.cgi?id=217440.
651 // The crash in WebKit appears to be related to IPC throttling. Session
652 // restore can create a large number of IPC calls, which can then be
653 // throttled. It seems if the WKWebView is destroyed with this backlog of
654 // IPC calls, sometimes WebKit crashes.
655 // See crbug.com/1032928 for an explanation for how to trigger this crash.
656 // Note the timer should only be called if for some reason session restoration
657 // fails to complete -- thus preventing a WebState leak.
658 static bool delayPreloadDestroyWebState =
659 base::FeatureList::IsEnabled(kPreloadDelayWebStateReset);
660 if (delayPreloadDestroyWebState &&
661 _webState->GetNavigationManager()->IsRestoreSessionInProgress()) {
662 __block std::unique_ptr<web::WebState> webState = std::move(_webState);
663 __block std::unique_ptr<base::OneShotTimer> resetTimer(
664 new base::OneShotTimer());
665 auto reset_block = ^{
666 if (webState) {
667 webState.reset();
668 }
669
670 if (resetTimer) {
671 resetTimer->Stop();
672 resetTimer.reset();
673 }
674 };
675 resetTimer->Start(
676 FROM_HERE, base::TimeDelta::FromSeconds(kMaximumCancelledWebStateDelay),
677 base::BindOnce(reset_block));
678 webState->GetNavigationManager()->AddRestoreCompletionCallback(
679 base::BindOnce(^{
680 dispatch_async(dispatch_get_main_queue(), reset_block);
681 }));
682 } else {
683 _webState.reset();
684 }
Kurt Horimotoeb0081e2019-09-26 18:53:35685 self.prerenderedURL = GURL();
686 self.startTime = base::TimeTicks();
Gauthier Ambard6d3ed3472020-05-11 09:11:48687 self.loadCompleted = NO;
Kurt Horimotoeb0081e2019-09-26 18:53:35688}
689
690#pragma mark - Notification Helpers
691
692- (void)didReceiveMemoryWarning {
693 [self cancelPrerenderForReason:PRERENDER_FINAL_STATUS_MEMORY_LIMIT_EXCEEDED];
694}
695
696#pragma mark - Metrics Helpers
697
698- (void)recordReleaseMetrics {
699 UMA_HISTOGRAM_ENUMERATION(kPrerenderFinalStatusHistogramName,
700 PRERENDER_FINAL_STATUS_USED,
701 PRERENDER_FINAL_STATUS_MAX);
702
Gauthier Ambard6d3ed3472020-05-11 09:11:48703 UMA_HISTOGRAM_BOOLEAN(kPrerenderLoadComplete, self.loadCompleted);
704
705 if (self.loadCompleted) {
706 DCHECK_NE(base::TimeDelta(), self.completionTime);
707 UMA_HISTOGRAM_TIMES(kPrerenderPrerenderTimeSaved, self.completionTime);
708 } else {
709 DCHECK_NE(base::TimeTicks(), self.startTime);
710 UMA_HISTOGRAM_TIMES(kPrerenderPrerenderTimeSaved,
711 base::TimeTicks::Now() - self.startTime);
712 }
Kurt Horimotoeb0081e2019-09-26 18:53:35713}
714
sdefresned9217bc2016-12-19 13:58:32715@end