Index: ios/chrome/browser/ui/preload_controller.mm |
diff --git a/ios/chrome/browser/ui/preload_controller.mm b/ios/chrome/browser/ui/preload_controller.mm |
new file mode 100644 |
index 0000000000000000000000000000000000000000..cc8fcf7b764af814526955f9d1579d549f5ac29d |
--- /dev/null |
+++ b/ios/chrome/browser/ui/preload_controller.mm |
@@ -0,0 +1,407 @@ |
+// 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/ui/preload_controller.h" |
+ |
+#include "base/ios/device_util.h" |
+#include "base/logging.h" |
+#include "base/metrics/field_trial.h" |
+#include "base/metrics/histogram.h" |
+#include "base/strings/sys_string_conversions.h" |
+#include "components/prefs/pref_service.h" |
+#include "ios/chrome/browser/browser_state/chrome_browser_state.h" |
+#include "ios/chrome/browser/pref_names.h" |
+#import "ios/chrome/browser/tabs/tab.h" |
+#include "ios/chrome/browser/ui/preload_controller_delegate.h" |
+#include "ios/chrome/browser/ui/prerender_final_status.h" |
+#import "ios/web/public/web_state/ui/crw_native_content.h" |
+#include "ios/web/public/web_thread.h" |
+#import "ios/web/web_state/ui/crw_web_controller.h" |
+#import "net/base/mac/url_conversions.h" |
+#include "net/url_request/url_fetcher.h" |
+#include "net/url_request/url_fetcher_delegate.h" |
+#include "ui/base/page_transition_types.h" |
+ |
+// ID of the URLFetcher responsible for prefetches. |
+const int kPreloadControllerURLFetcherID = 1; |
+ |
+namespace { |
+// Delay before starting to prerender a URL. |
+const NSTimeInterval kPrerenderDelay = 0.5; |
+ |
+// The finch experiment to turn off prefetching 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 sucessful prerenders. |
+const char kPrerendersPerSessionCountHistogramName[] = |
+ "Prerender.PrerendersPerSessionCount"; |
+ |
+// 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 prefetching is enabled. |
+- (BOOL)isPrefetchingEnabled; |
+ |
+// Returns YES if the |url| is valid for prerendering and prefetching. |
+- (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; |
+ |
+// Starts the scheduled prefetch request. |
+- (void)startPrefetch; |
+ |
+// Cancels the scheduled prefetch request. |
+- (void)cancelPrefetch; |
+ |
+// Completes the current prefetch request. Called by the URLFetcher's delegate |
+// when the URLFetcher has compleeted fetching the |prefetchedURL|. |
+- (void)prefetchDidComplete:(const net::URLFetcher*)source; |
+ |
+@end |
+ |
+// Delegate to handle completion of URLFetcher operations. |
+class PrefetchDelegate : public net::URLFetcherDelegate { |
+ public: |
+ explicit PrefetchDelegate(PreloadController* owner) : owner_(owner) {} |
+ void OnURLFetchComplete(const net::URLFetcher* source) override { |
+ [owner_ prefetchDidComplete:source]; |
+ } |
+ |
+ private: |
+ PreloadController* owner_; // weak |
+}; |
+ |
+@implementation PreloadController |
+ |
+@synthesize prerenderedURL = prerenderedURL_; |
+@synthesize prefetchedURL = prefetchedURL_; |
+@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()); |
+ observerBridge_.reset(new PrefObserverBridge(self)); |
+ prefChangeRegistrar_.Init(browserState_->GetPrefs()); |
+ observerBridge_->ObserveChangesForPreference( |
+ prefs::kNetworkPredictionEnabled, &prefChangeRegistrar_); |
+ observerBridge_->ObserveChangesForPreference( |
+ prefs::kNetworkPredictionWifiOnly, &prefChangeRegistrar_); |
+ if (enabled_ && wifiOnly_) { |
+ connectionTypeObserverBridge_.reset( |
+ new ConnectionTypeObserverBridge(self)); |
+ } |
+ |
+ [[NSNotificationCenter defaultCenter] |
+ addObserver:self |
+ selector:@selector(didReceiveMemoryWarning) |
+ name:UIApplicationDidReceiveMemoryWarningNotification |
+ object:nil]; |
+ } |
+ return self; |
+} |
+ |
+- (void)dealloc { |
+ UMA_HISTOGRAM_COUNTS(kPrerendersPerSessionCountHistogramName, |
+ successfulPrerendersPerSessionCount_); |
+ [[NSNotificationCenter defaultCenter] removeObserver:self]; |
+ [self cancelPrerender]; |
+ [super dealloc]; |
+} |
+ |
+- (void)prerenderURL:(const GURL&)url |
+ referrer:(const web::Referrer&)referrer |
+ transition:(ui::PageTransition)transition |
+ immediately:(BOOL)immediately { |
+ // TODO(rohitrao): 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)prefetchURL:(const GURL&)url transition:(ui::PageTransition)transition { |
+ if (![self isPrefetchingEnabled] || ![self shouldPreloadURL:url]) { |
+ return; |
+ } |
+ |
+ // Ignore this request if the the currently prefetched page matches this URL. |
+ if ([self hasPrefetchedURL:url]) { |
+ return; |
+ } |
+ |
+ // Cancel any in-fight prefetches before starting a new one. |
+ [self cancelPrefetch]; |
+ |
+ DCHECK(url.is_valid()); |
+ if (!url.is_valid()) { |
+ return; |
+ } |
+ |
+ prefetchedURL_ = [self urlToPrefetchURL:url]; |
+ prefetcherDelegate_.reset(new PrefetchDelegate(self)); |
+ prefetcher_ = |
+ net::URLFetcher::Create(kPreloadControllerURLFetcherID, prefetchedURL_, |
+ net::URLFetcher::GET, prefetcherDelegate_.get()); |
+ prefetcher_->SetRequestContext(browserState_->GetRequestContext()); |
+ prefetcher_->Start(); |
+} |
+ |
+- (void)cancelPrerender { |
+ [self cancelPrerenderForReason:PRERENDER_FINAL_STATUS_CANCELLED]; |
+} |
+ |
+- (void)cancelPrerenderForReason:(PrerenderFinalStatus)reason { |
+ [self removeScheduledPrerenderRequests]; |
+ [self destroyPreviewContentsForReason:reason]; |
+} |
+ |
+- (Tab*)releasePrerenderContents { |
+ successfulPrerendersPerSessionCount_++; |
+ UMA_HISTOGRAM_ENUMERATION(kPrerenderFinalStatusHistogramName, |
+ PRERENDER_FINAL_STATUS_USED, |
+ PRERENDER_FINAL_STATUS_MAX); |
+ [self removeScheduledPrerenderRequests]; |
+ prerenderedURL_ = GURL(); |
+ [[tab_ webController] setNativeProvider:nil]; |
+ [tab_ setDelegate:nil]; |
+ return [tab_.release() autorelease]; |
+} |
+ |
+- (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 |
+ |
+// Delegate the call to the original native provider. |
+- (BOOL)hasControllerForURL:(const GURL&)url { |
+ return [[tab_ webController].nativeProvider hasControllerForURL:url]; |
+} |
+ |
+// Override the CRWNativeContentProvider methods to cancel any prerenders that |
+// require native content. |
+- (id<CRWNativeContent>)controllerForURL:(const GURL&)url { |
+ [self schedulePrerenderCancel]; |
+ return nil; |
+} |
+ |
+// Override the CRWNativeContentProvider methods to cancel any prerenders that |
+// require native content. |
+- (id<CRWNativeContent>)controllerForURL:(const GURL&)url |
+ withError:(NSError*)error |
+ isPost:(BOOL)isPost { |
+ [self schedulePrerenderCancel]; |
+ return nil; |
+} |
+ |
+#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)isPrefetchingEnabled { |
+ DCHECK_CURRENTLY_ON(web::WebThread::UI); |
+ return enabled_ && (!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; |
+ } |
+ |
+ Tab* tab = [Tab |
+ newPreloadingTabWithBrowserState:browserState_ |
+ url:prerenderedURL_ |
+ referrer:scheduledReferrer_ |
+ transition:scheduledTransition_ |
+ provider:self |
+ opener:nil |
+ desktopUserAgent:[delegate_ shouldUseDesktopUserAgent] |
+ configuration:^(Tab* tab) { |
+ [tab setIsPrerenderTab:YES]; |
+ [tab setDelegate:self]; |
+ }]; |
+ |
+ // Create and set up the prerender. |
+ tab_.reset([tab retain]); |
+ |
+ // Trigger the page to start loading. |
+ [tab_ view]; |
+} |
+ |
+- (const GURL)urlToPrefetchURL:(const GURL&)url { |
+ GURL::Replacements replacements; |
+ |
+ // Add prefetch indicator to query params. |
+ std::string query = url.query(); |
+ if (!query.empty()) { |
+ query.append("&"); |
+ } |
+ query.append("pf=i"); |
+ replacements.SetQueryStr(query); |
+ |
+ return url.ReplaceComponents(replacements); |
+} |
+ |
+- (BOOL)hasPrefetchedURL:(const GURL&)url { |
+ return prefetchedURL_ == [self urlToPrefetchURL:url]; |
+} |
+ |
+- (void)cancelPrefetch { |
+ prefetcher_.reset(); |
+ prefetchedURL_ = GURL(); |
+} |
+ |
+- (void)prefetchDidComplete:(const net::URLFetcher*)source { |
+ if (source) { |
+ DLOG_IF(WARNING, source->GetResponseCode() != 200) |
+ << "Prefetching URL got response code " << source->GetResponseCode(); |
+ } |
+ prefetcher_.reset(); |
+} |
+ |
+- (void)destroyPreviewContents { |
+ [self destroyPreviewContentsForReason:PRERENDER_FINAL_STATUS_CANCELLED]; |
+} |
+ |
+- (void)destroyPreviewContentsForReason:(PrerenderFinalStatus)reason { |
+ if (!tab_.get()) |
+ return; |
+ |
+ UMA_HISTOGRAM_ENUMERATION(kPrerenderFinalStatusHistogramName, reason, |
+ PRERENDER_FINAL_STATUS_MAX); |
+ [[tab_ webController] setNativeProvider:nil]; |
+ [tab_ setDelegate:nil]; |
+ [tab_ close]; |
+ tab_.reset(); |
+ prerenderedURL_ = GURL(); |
+} |
+ |
+- (void)schedulePrerenderCancel { |
+ // TODO(rohitrao): 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. b/5944421 |
+ [self removeScheduledPrerenderRequests]; |
+ [self performSelector:@selector(cancelPrerender) withObject:nil afterDelay:0]; |
+} |
+ |
+- (void)removeScheduledPrerenderRequests { |
+ [NSObject cancelPreviousPerformRequestsWithTarget:self]; |
+ scheduledURL_ = GURL(); |
+} |
+ |
+#pragma mark - TabDelegate |
+ |
+- (void)discardPrerender { |
+ [self schedulePrerenderCancel]; |
+} |
+ |
+@end |