OLD | NEW |
(Empty) | |
| 1 // 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 |
| 7 #include "ios/chrome/browser/ui/preload_controller.h" |
| 8 |
| 9 #include "base/ios/device_util.h" |
| 10 #include "base/logging.h" |
| 11 #include "base/metrics/field_trial.h" |
| 12 #include "base/metrics/histogram.h" |
| 13 #include "base/strings/sys_string_conversions.h" |
| 14 #include "components/prefs/pref_service.h" |
| 15 #include "ios/chrome/browser/browser_state/chrome_browser_state.h" |
| 16 #include "ios/chrome/browser/pref_names.h" |
| 17 #import "ios/chrome/browser/tabs/tab.h" |
| 18 #include "ios/chrome/browser/ui/preload_controller_delegate.h" |
| 19 #include "ios/chrome/browser/ui/prerender_final_status.h" |
| 20 #import "ios/web/public/web_state/ui/crw_native_content.h" |
| 21 #include "ios/web/public/web_thread.h" |
| 22 #import "ios/web/web_state/ui/crw_web_controller.h" |
| 23 #import "net/base/mac/url_conversions.h" |
| 24 #include "net/url_request/url_fetcher.h" |
| 25 #include "net/url_request/url_fetcher_delegate.h" |
| 26 #include "ui/base/page_transition_types.h" |
| 27 |
| 28 // ID of the URLFetcher responsible for prefetches. |
| 29 const int kPreloadControllerURLFetcherID = 1; |
| 30 |
| 31 namespace { |
| 32 // Delay before starting to prerender a URL. |
| 33 const NSTimeInterval kPrerenderDelay = 0.5; |
| 34 |
| 35 // The finch experiment to turn off prefetching as a field trial. |
| 36 const char kTabEvictionFieldTrialName[] = "TabEviction"; |
| 37 // The associated group. |
| 38 const char kPrerenderTabEvictionTrialGroup[] = "NoPrerendering"; |
| 39 // The name of the histogram for recording final status (e.g. used/cancelled) |
| 40 // of prerender requests. |
| 41 const char kPrerenderFinalStatusHistogramName[] = "Prerender.FinalStatus"; |
| 42 // The name of the histogram for recording the number of sucessful prerenders. |
| 43 const char kPrerendersPerSessionCountHistogramName[] = |
| 44 "Prerender.PrerendersPerSessionCount"; |
| 45 |
| 46 // Is this install selected for this particular experiment. |
| 47 bool IsPrerenderTabEvictionExperimentalGroup() { |
| 48 base::FieldTrial* trial = |
| 49 base::FieldTrialList::Find(kTabEvictionFieldTrialName); |
| 50 return trial && trial->group_name() == kPrerenderTabEvictionTrialGroup; |
| 51 } |
| 52 |
| 53 } // namespace |
| 54 |
| 55 @interface PreloadController (PrivateMethods) |
| 56 |
| 57 // Returns YES if prerendering is enabled. |
| 58 - (BOOL)isPrerenderingEnabled; |
| 59 |
| 60 // Returns YES if prefetching is enabled. |
| 61 - (BOOL)isPrefetchingEnabled; |
| 62 |
| 63 // Returns YES if the |url| is valid for prerendering and prefetching. |
| 64 - (BOOL)shouldPreloadURL:(const GURL&)url; |
| 65 |
| 66 // Called to start any scheduled prerendering requests. |
| 67 - (void)startPrerender; |
| 68 |
| 69 // Destroys the preview Tab and resets |prerenderURL_| to the empty URL. |
| 70 - (void)destroyPreviewContents; |
| 71 |
| 72 // Schedules the current prerender to be cancelled during the next run of the |
| 73 // event loop. |
| 74 - (void)schedulePrerenderCancel; |
| 75 |
| 76 // Removes any scheduled prerender requests and resets |scheduledURL| to the |
| 77 // empty URL. |
| 78 - (void)removeScheduledPrerenderRequests; |
| 79 |
| 80 // Starts the scheduled prefetch request. |
| 81 - (void)startPrefetch; |
| 82 |
| 83 // Cancels the scheduled prefetch request. |
| 84 - (void)cancelPrefetch; |
| 85 |
| 86 // Completes the current prefetch request. Called by the URLFetcher's delegate |
| 87 // when the URLFetcher has compleeted fetching the |prefetchedURL|. |
| 88 - (void)prefetchDidComplete:(const net::URLFetcher*)source; |
| 89 |
| 90 @end |
| 91 |
| 92 // Delegate to handle completion of URLFetcher operations. |
| 93 class PrefetchDelegate : public net::URLFetcherDelegate { |
| 94 public: |
| 95 explicit PrefetchDelegate(PreloadController* owner) : owner_(owner) {} |
| 96 void OnURLFetchComplete(const net::URLFetcher* source) override { |
| 97 [owner_ prefetchDidComplete:source]; |
| 98 } |
| 99 |
| 100 private: |
| 101 PreloadController* owner_; // weak |
| 102 }; |
| 103 |
| 104 @implementation PreloadController |
| 105 |
| 106 @synthesize prerenderedURL = prerenderedURL_; |
| 107 @synthesize prefetchedURL = prefetchedURL_; |
| 108 @synthesize delegate = delegate_; |
| 109 |
| 110 - (instancetype)initWithBrowserState:(ios::ChromeBrowserState*)browserState { |
| 111 DCHECK(browserState); |
| 112 DCHECK_CURRENTLY_ON(web::WebThread::UI); |
| 113 if ((self = [super init])) { |
| 114 browserState_ = browserState; |
| 115 enabled_ = |
| 116 browserState_->GetPrefs()->GetBoolean(prefs::kNetworkPredictionEnabled); |
| 117 wifiOnly_ = browserState_->GetPrefs()->GetBoolean( |
| 118 prefs::kNetworkPredictionWifiOnly); |
| 119 usingWWAN_ = net::NetworkChangeNotifier::IsConnectionCellular( |
| 120 net::NetworkChangeNotifier::GetConnectionType()); |
| 121 observerBridge_.reset(new PrefObserverBridge(self)); |
| 122 prefChangeRegistrar_.Init(browserState_->GetPrefs()); |
| 123 observerBridge_->ObserveChangesForPreference( |
| 124 prefs::kNetworkPredictionEnabled, &prefChangeRegistrar_); |
| 125 observerBridge_->ObserveChangesForPreference( |
| 126 prefs::kNetworkPredictionWifiOnly, &prefChangeRegistrar_); |
| 127 if (enabled_ && wifiOnly_) { |
| 128 connectionTypeObserverBridge_.reset( |
| 129 new ConnectionTypeObserverBridge(self)); |
| 130 } |
| 131 |
| 132 [[NSNotificationCenter defaultCenter] |
| 133 addObserver:self |
| 134 selector:@selector(didReceiveMemoryWarning) |
| 135 name:UIApplicationDidReceiveMemoryWarningNotification |
| 136 object:nil]; |
| 137 } |
| 138 return self; |
| 139 } |
| 140 |
| 141 - (void)dealloc { |
| 142 UMA_HISTOGRAM_COUNTS(kPrerendersPerSessionCountHistogramName, |
| 143 successfulPrerendersPerSessionCount_); |
| 144 [[NSNotificationCenter defaultCenter] removeObserver:self]; |
| 145 [self cancelPrerender]; |
| 146 [super dealloc]; |
| 147 } |
| 148 |
| 149 - (void)prerenderURL:(const GURL&)url |
| 150 referrer:(const web::Referrer&)referrer |
| 151 transition:(ui::PageTransition)transition |
| 152 immediately:(BOOL)immediately { |
| 153 // TODO(rohitrao): If shouldPrerenderURL returns false, should we cancel any |
| 154 // scheduled prerender requests? |
| 155 if (![self isPrerenderingEnabled] || ![self shouldPreloadURL:url]) |
| 156 return; |
| 157 |
| 158 // Ignore this request if there is already a scheduled request for the same |
| 159 // URL; or, if there is no scheduled request, but the currently prerendered |
| 160 // page matches this URL. |
| 161 if (url == scheduledURL_ || |
| 162 (scheduledURL_.is_empty() && url == prerenderedURL_)) { |
| 163 return; |
| 164 } |
| 165 |
| 166 [self removeScheduledPrerenderRequests]; |
| 167 scheduledURL_ = url; |
| 168 scheduledTransition_ = transition; |
| 169 scheduledReferrer_ = referrer; |
| 170 |
| 171 NSTimeInterval delay = immediately ? 0.0 : kPrerenderDelay; |
| 172 [self performSelector:@selector(startPrerender) |
| 173 withObject:nil |
| 174 afterDelay:delay]; |
| 175 } |
| 176 |
| 177 - (void)prefetchURL:(const GURL&)url transition:(ui::PageTransition)transition { |
| 178 if (![self isPrefetchingEnabled] || ![self shouldPreloadURL:url]) { |
| 179 return; |
| 180 } |
| 181 |
| 182 // Ignore this request if the the currently prefetched page matches this URL. |
| 183 if ([self hasPrefetchedURL:url]) { |
| 184 return; |
| 185 } |
| 186 |
| 187 // Cancel any in-fight prefetches before starting a new one. |
| 188 [self cancelPrefetch]; |
| 189 |
| 190 DCHECK(url.is_valid()); |
| 191 if (!url.is_valid()) { |
| 192 return; |
| 193 } |
| 194 |
| 195 prefetchedURL_ = [self urlToPrefetchURL:url]; |
| 196 prefetcherDelegate_.reset(new PrefetchDelegate(self)); |
| 197 prefetcher_ = |
| 198 net::URLFetcher::Create(kPreloadControllerURLFetcherID, prefetchedURL_, |
| 199 net::URLFetcher::GET, prefetcherDelegate_.get()); |
| 200 prefetcher_->SetRequestContext(browserState_->GetRequestContext()); |
| 201 prefetcher_->Start(); |
| 202 } |
| 203 |
| 204 - (void)cancelPrerender { |
| 205 [self cancelPrerenderForReason:PRERENDER_FINAL_STATUS_CANCELLED]; |
| 206 } |
| 207 |
| 208 - (void)cancelPrerenderForReason:(PrerenderFinalStatus)reason { |
| 209 [self removeScheduledPrerenderRequests]; |
| 210 [self destroyPreviewContentsForReason:reason]; |
| 211 } |
| 212 |
| 213 - (Tab*)releasePrerenderContents { |
| 214 successfulPrerendersPerSessionCount_++; |
| 215 UMA_HISTOGRAM_ENUMERATION(kPrerenderFinalStatusHistogramName, |
| 216 PRERENDER_FINAL_STATUS_USED, |
| 217 PRERENDER_FINAL_STATUS_MAX); |
| 218 [self removeScheduledPrerenderRequests]; |
| 219 prerenderedURL_ = GURL(); |
| 220 [[tab_ webController] setNativeProvider:nil]; |
| 221 [tab_ setDelegate:nil]; |
| 222 return [tab_.release() autorelease]; |
| 223 } |
| 224 |
| 225 - (void)connectionTypeChanged:(net::NetworkChangeNotifier::ConnectionType)type { |
| 226 DCHECK_CURRENTLY_ON(web::WebThread::UI); |
| 227 usingWWAN_ = net::NetworkChangeNotifier::IsConnectionCellular(type); |
| 228 if (wifiOnly_ && usingWWAN_) |
| 229 [self cancelPrerender]; |
| 230 } |
| 231 |
| 232 - (void)onPreferenceChanged:(const std::string&)preferenceName { |
| 233 if (preferenceName == prefs::kNetworkPredictionEnabled || |
| 234 preferenceName == prefs::kNetworkPredictionWifiOnly) { |
| 235 DCHECK_CURRENTLY_ON(web::WebThread::UI); |
| 236 // The logic is simpler if both preferences changes are handled equally. |
| 237 enabled_ = |
| 238 browserState_->GetPrefs()->GetBoolean(prefs::kNetworkPredictionEnabled); |
| 239 wifiOnly_ = browserState_->GetPrefs()->GetBoolean( |
| 240 prefs::kNetworkPredictionWifiOnly); |
| 241 |
| 242 if (wifiOnly_ && enabled_) { |
| 243 if (!connectionTypeObserverBridge_.get()) { |
| 244 usingWWAN_ = net::NetworkChangeNotifier::IsConnectionCellular( |
| 245 net::NetworkChangeNotifier::GetConnectionType()); |
| 246 connectionTypeObserverBridge_.reset( |
| 247 new ConnectionTypeObserverBridge(self)); |
| 248 } |
| 249 if (usingWWAN_) { |
| 250 [self cancelPrerender]; |
| 251 } |
| 252 } else if (enabled_) { |
| 253 connectionTypeObserverBridge_.reset(); |
| 254 } else { |
| 255 [self cancelPrerender]; |
| 256 connectionTypeObserverBridge_.reset(); |
| 257 } |
| 258 } |
| 259 } |
| 260 |
| 261 - (void)didReceiveMemoryWarning { |
| 262 [self cancelPrerenderForReason:PRERENDER_FINAL_STATUS_MEMORY_LIMIT_EXCEEDED]; |
| 263 } |
| 264 |
| 265 #pragma mark - |
| 266 #pragma mark CRWNativeContentProvider implementation |
| 267 |
| 268 // Delegate the call to the original native provider. |
| 269 - (BOOL)hasControllerForURL:(const GURL&)url { |
| 270 return [[tab_ webController].nativeProvider hasControllerForURL:url]; |
| 271 } |
| 272 |
| 273 // Override the CRWNativeContentProvider methods to cancel any prerenders that |
| 274 // require native content. |
| 275 - (id<CRWNativeContent>)controllerForURL:(const GURL&)url { |
| 276 [self schedulePrerenderCancel]; |
| 277 return nil; |
| 278 } |
| 279 |
| 280 // Override the CRWNativeContentProvider methods to cancel any prerenders that |
| 281 // require native content. |
| 282 - (id<CRWNativeContent>)controllerForURL:(const GURL&)url |
| 283 withError:(NSError*)error |
| 284 isPost:(BOOL)isPost { |
| 285 [self schedulePrerenderCancel]; |
| 286 return nil; |
| 287 } |
| 288 |
| 289 #pragma mark - |
| 290 #pragma mark Private Methods |
| 291 |
| 292 - (BOOL)isPrerenderingEnabled { |
| 293 DCHECK_CURRENTLY_ON(web::WebThread::UI); |
| 294 return !IsPrerenderTabEvictionExperimentalGroup() && enabled_ && |
| 295 !ios::device_util::IsSingleCoreDevice() && |
| 296 ios::device_util::RamIsAtLeast512Mb() && (!wifiOnly_ || !usingWWAN_); |
| 297 } |
| 298 |
| 299 - (BOOL)isPrefetchingEnabled { |
| 300 DCHECK_CURRENTLY_ON(web::WebThread::UI); |
| 301 return enabled_ && (!wifiOnly_ || !usingWWAN_); |
| 302 } |
| 303 |
| 304 - (BOOL)shouldPreloadURL:(const GURL&)url { |
| 305 return url.SchemeIs(url::kHttpScheme) || url.SchemeIs(url::kHttpsScheme); |
| 306 } |
| 307 |
| 308 - (void)startPrerender { |
| 309 // Destroy any existing prerenders before starting a new one. |
| 310 [self destroyPreviewContents]; |
| 311 prerenderedURL_ = scheduledURL_; |
| 312 scheduledURL_ = GURL(); |
| 313 |
| 314 DCHECK(prerenderedURL_.is_valid()); |
| 315 if (!prerenderedURL_.is_valid()) { |
| 316 [self destroyPreviewContents]; |
| 317 return; |
| 318 } |
| 319 |
| 320 Tab* tab = [Tab |
| 321 newPreloadingTabWithBrowserState:browserState_ |
| 322 url:prerenderedURL_ |
| 323 referrer:scheduledReferrer_ |
| 324 transition:scheduledTransition_ |
| 325 provider:self |
| 326 opener:nil |
| 327 desktopUserAgent:[delegate_ shouldUseDesktopUserAgent] |
| 328 configuration:^(Tab* tab) { |
| 329 [tab setIsPrerenderTab:YES]; |
| 330 [tab setDelegate:self]; |
| 331 }]; |
| 332 |
| 333 // Create and set up the prerender. |
| 334 tab_.reset([tab retain]); |
| 335 |
| 336 // Trigger the page to start loading. |
| 337 [tab_ view]; |
| 338 } |
| 339 |
| 340 - (const GURL)urlToPrefetchURL:(const GURL&)url { |
| 341 GURL::Replacements replacements; |
| 342 |
| 343 // Add prefetch indicator to query params. |
| 344 std::string query = url.query(); |
| 345 if (!query.empty()) { |
| 346 query.append("&"); |
| 347 } |
| 348 query.append("pf=i"); |
| 349 replacements.SetQueryStr(query); |
| 350 |
| 351 return url.ReplaceComponents(replacements); |
| 352 } |
| 353 |
| 354 - (BOOL)hasPrefetchedURL:(const GURL&)url { |
| 355 return prefetchedURL_ == [self urlToPrefetchURL:url]; |
| 356 } |
| 357 |
| 358 - (void)cancelPrefetch { |
| 359 prefetcher_.reset(); |
| 360 prefetchedURL_ = GURL(); |
| 361 } |
| 362 |
| 363 - (void)prefetchDidComplete:(const net::URLFetcher*)source { |
| 364 if (source) { |
| 365 DLOG_IF(WARNING, source->GetResponseCode() != 200) |
| 366 << "Prefetching URL got response code " << source->GetResponseCode(); |
| 367 } |
| 368 prefetcher_.reset(); |
| 369 } |
| 370 |
| 371 - (void)destroyPreviewContents { |
| 372 [self destroyPreviewContentsForReason:PRERENDER_FINAL_STATUS_CANCELLED]; |
| 373 } |
| 374 |
| 375 - (void)destroyPreviewContentsForReason:(PrerenderFinalStatus)reason { |
| 376 if (!tab_.get()) |
| 377 return; |
| 378 |
| 379 UMA_HISTOGRAM_ENUMERATION(kPrerenderFinalStatusHistogramName, reason, |
| 380 PRERENDER_FINAL_STATUS_MAX); |
| 381 [[tab_ webController] setNativeProvider:nil]; |
| 382 [tab_ setDelegate:nil]; |
| 383 [tab_ close]; |
| 384 tab_.reset(); |
| 385 prerenderedURL_ = GURL(); |
| 386 } |
| 387 |
| 388 - (void)schedulePrerenderCancel { |
| 389 // TODO(rohitrao): Instead of cancelling the prerender, should we mark it as |
| 390 // failed instead? That way, subsequent prerender requests for the same URL |
| 391 // will not kick off new prerenders. b/5944421 |
| 392 [self removeScheduledPrerenderRequests]; |
| 393 [self performSelector:@selector(cancelPrerender) withObject:nil afterDelay:0]; |
| 394 } |
| 395 |
| 396 - (void)removeScheduledPrerenderRequests { |
| 397 [NSObject cancelPreviousPerformRequestsWithTarget:self]; |
| 398 scheduledURL_ = GURL(); |
| 399 } |
| 400 |
| 401 #pragma mark - TabDelegate |
| 402 |
| 403 - (void)discardPrerender { |
| 404 [self schedulePrerenderCancel]; |
| 405 } |
| 406 |
| 407 @end |
OLD | NEW |