Index: ios/chrome/browser/native_app_launcher/native_app_navigation_controller.mm |
diff --git a/ios/chrome/browser/native_app_launcher/native_app_navigation_controller.mm b/ios/chrome/browser/native_app_launcher/native_app_navigation_controller.mm |
new file mode 100644 |
index 0000000000000000000000000000000000000000..4515dcc5c0fa477a8dc60d7dd546d39145ec807c |
--- /dev/null |
+++ b/ios/chrome/browser/native_app_launcher/native_app_navigation_controller.mm |
@@ -0,0 +1,247 @@ |
+// 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 "ios/chrome/browser/native_app_launcher/native_app_navigation_controller.h" |
+ |
+#import <StoreKit/StoreKit.h> |
+ |
+#include "base/metrics/user_metrics.h" |
+#include "base/metrics/user_metrics_action.h" |
+#include "components/infobars/core/infobar_manager.h" |
+#import "ios/chrome/browser/installation_notifier.h" |
+#include "ios/chrome/browser/native_app_launcher/native_app_infobar_delegate.h" |
+#import "ios/chrome/browser/open_url_util.h" |
+#import "ios/chrome/browser/tabs/tab.h" |
+#include "ios/public/provider/chrome/browser/chrome_browser_provider.h" |
+#import "ios/public/provider/chrome/browser/native_app_launcher/native_app_metadata.h" |
+#import "ios/public/provider/chrome/browser/native_app_launcher/native_app_types.h" |
+#import "ios/public/provider/chrome/browser/native_app_launcher/native_app_whitelist_manager.h" |
+#import "ios/web/navigation/navigation_manager_impl.h" |
+#include "ios/web/public/navigation_item.h" |
+#import "net/base/mac/url_conversions.h" |
+#include "net/url_request/url_request_context_getter.h" |
+ |
+using base::UserMetricsAction; |
+ |
+@interface NativeAppNavigationController () |
+// Shows a native app infobar by looking at the page's URL and by checking |
+// wheter that infobar should be bypassed or not. |
+- (void)showInfoBarIfNecessary; |
+ |
+// Returns a pointer to the NSMutableSet of |_appsPossiblyBeingInstalled| |
+- (NSMutableSet*)appsPossiblyBeingInstalled; |
+ |
+// Records what type of infobar was opened. |
+- (void)recordInfobarDisplayedOfType:(NativeAppControllerType)type |
+ onLinkNavigation:(BOOL)isLinkNavigation; |
+ |
+// Returns whether the current state is Link Navigation in the sense of Native |
+// App Launcher, i.e. a navigation caused by an explicit user action in the |
+// rectangle of the web content area. |
+- (BOOL)isLinkNavigation; |
+ |
+@end |
+ |
+@implementation NativeAppNavigationController { |
+ // A reference to the URLRequestContextGetter needed to fetch icons. |
+ scoped_refptr<net::URLRequestContextGetter> _requestContextGetter; |
+ // Tab hosting the infobar. |
+ __unsafe_unretained Tab* _tab; // weak |
+ base::scoped_nsprotocol<id<NativeAppMetadata>> _metadata; |
+ // A set of appIds encoded as NSStrings. |
+ base::scoped_nsobject<NSMutableSet> _appsPossiblyBeingInstalled; |
+} |
+ |
+// This prevents incorrect initialization of this object. |
+- (id)init { |
+ NOTREACHED(); |
+ return nil; |
+} |
+ |
+// Designated initializer. Use this instead of -init. |
+- (id)initWithRequestContextGetter:(net::URLRequestContextGetter*)context |
+ tab:(Tab*)tab { |
+ self = [super init]; |
+ if (self) { |
+ DCHECK(context); |
+ _requestContextGetter = context; |
+ // Allows |tab| to be nil for unit testing. |
+ _tab = tab; |
+ _appsPossiblyBeingInstalled.reset([[NSMutableSet alloc] init]); |
+ } |
+ return self; |
+} |
+ |
+- (void)copyStateFrom:(NativeAppNavigationController*)controller { |
+ DCHECK(controller); |
+ _appsPossiblyBeingInstalled.reset([[NSMutableSet alloc] |
+ initWithSet:[controller appsPossiblyBeingInstalled]]); |
+ for (NSString* appIdString in _appsPossiblyBeingInstalled.get()) { |
+ DCHECK([appIdString isKindOfClass:[NSString class]]); |
+ NSURL* appURL = |
+ [ios::GetChromeBrowserProvider()->GetNativeAppWhitelistManager() |
+ schemeForAppId:appIdString]; |
+ [[InstallationNotifier sharedInstance] |
+ registerForInstallationNotifications:self |
+ withSelector:@selector(appDidInstall:) |
+ forScheme:[appURL scheme]]; |
+ } |
+ [self showInfoBarIfNecessary]; |
+} |
+ |
+- (void)dealloc { |
+ [[InstallationNotifier sharedInstance] unregisterForNotifications:self]; |
+ [super dealloc]; |
+} |
+ |
+- (NSMutableSet*)appsPossiblyBeingInstalled { |
+ return _appsPossiblyBeingInstalled; |
+} |
+ |
+- (void)showInfoBarIfNecessary { |
+ // Find a potential matching native app. |
+ GURL pageURL = _tab.webState->GetLastCommittedURL(); |
+ _metadata.reset( |
+ [ios::GetChromeBrowserProvider()->GetNativeAppWhitelistManager() |
+ newNativeAppForURL:pageURL]); |
+ if (!_metadata || [_metadata shouldBypassInfoBars]) |
+ return; |
+ |
+ // Select the infobar type. |
+ NativeAppControllerType type; |
+ if ([_metadata canOpenURL:pageURL]) { // App is installed. |
+ type = [self isLinkNavigation] && ![_metadata shouldAutoOpenLinks] |
+ ? NATIVE_APP_OPEN_POLICY_CONTROLLER |
+ : NATIVE_APP_LAUNCHER_CONTROLLER; |
+ } else { // App is not installed. |
+ // Check if the user already opened the store for this app. |
+ if ([_appsPossiblyBeingInstalled containsObject:[_metadata appId]]) |
+ return; |
+ type = NATIVE_APP_INSTALLER_CONTROLLER; |
+ } |
+ // Inform the metadata that an infobar of |type| will be shown so that metrics |
+ // and ignored behavior can be handled. |
+ [_metadata willBeShownInInfobarOfType:type]; |
+ // Display the proper infobar. |
+ infobars::InfoBarManager* infoBarManager = [_tab infoBarManager]; |
+ NativeAppInfoBarDelegate::Create(infoBarManager, self, pageURL, type); |
+ [self recordInfobarDisplayedOfType:type |
+ onLinkNavigation:[self isLinkNavigation]]; |
+} |
+ |
+- (void)recordInfobarDisplayedOfType:(NativeAppControllerType)type |
+ onLinkNavigation:(BOOL)isLinkNavigation { |
+ switch (type) { |
+ case NATIVE_APP_INSTALLER_CONTROLLER: |
+ base::RecordAction( |
+ isLinkNavigation |
+ ? UserMetricsAction("MobileGALInstallInfoBarLinkNavigation") |
+ : UserMetricsAction("MobileGALInstallInfoBarDirectNavigation")); |
+ break; |
+ case NATIVE_APP_LAUNCHER_CONTROLLER: |
+ base::RecordAction(UserMetricsAction("MobileGALLaunchInfoBar")); |
+ break; |
+ case NATIVE_APP_OPEN_POLICY_CONTROLLER: |
+ base::RecordAction(UserMetricsAction("MobileGALOpenPolicyInfoBar")); |
+ break; |
+ default: |
+ NOTREACHED(); |
+ break; |
+ } |
+} |
+ |
+#pragma mark - |
+#pragma mark NativeAppNavigationControllerProtocol methods |
+ |
+- (NSString*)appId { |
+ return [_metadata appId]; |
+} |
+ |
+- (NSString*)appName { |
+ return [_metadata appName]; |
+} |
+ |
+- (void)fetchSmallIconWithCompletionBlock:(void (^)(UIImage*))block { |
+ [_metadata fetchSmallIconWithContext:_requestContextGetter.get() |
+ completionBlock:block]; |
+} |
+ |
+- (void)openStore { |
+ // Register to get a notification when the app is installed. |
+ [[InstallationNotifier sharedInstance] |
+ registerForInstallationNotifications:self |
+ withSelector:@selector(appDidInstall:) |
+ forScheme:[_metadata anyScheme]]; |
+ NSString* appIdString = [self appId]; |
+ // Defensively early return if native app metadata returns an nil string for |
+ // appId which can cause subsequent -addObject: to throw exceptions. |
+ if (![appIdString length]) |
+ return; |
+ DCHECK(![_appsPossiblyBeingInstalled containsObject:appIdString]); |
+ [_appsPossiblyBeingInstalled addObject:appIdString]; |
+ [_tab openAppStore:appIdString]; |
+} |
+ |
+- (void)launchApp:(const GURL&)URL { |
+ // TODO(crbug.com/353957): Pass the ChromeIdentity to |
+ // -launchURLWithURL:identity: |
+ GURL launchURL([_metadata launchURLWithURL:URL identity:nil]); |
+ if (launchURL.is_valid()) { |
+ OpenUrlWithCompletionHandler(net::NSURLWithGURL(launchURL), nil); |
+ } |
+} |
+ |
+- (void)updateMetadataWithUserAction:(NativeAppActionType)userAction { |
+ [_metadata updateWithUserAction:userAction]; |
+} |
+ |
+#pragma mark - |
+#pragma mark CRWWebControllerObserver methods |
+ |
+- (void)pageLoaded:(CRWWebController*)webController { |
+ if (![_tab isPrerenderTab]) |
+ [self showInfoBarIfNecessary]; |
+} |
+ |
+- (void)webControllerWillClose:(CRWWebController*)webController { |
+ [webController removeObserver:self]; |
+} |
+ |
+- (BOOL)isLinkNavigation { |
+ if (![_tab navigationManager]) |
+ return NO; |
+ web::NavigationItem* userItem = [_tab navigationManager]->GetLastUserItem(); |
+ if (!userItem) |
+ return NO; |
+ ui::PageTransition currentTransition = userItem->GetTransitionType(); |
+ return PageTransitionCoreTypeIs(currentTransition, |
+ ui::PAGE_TRANSITION_LINK) || |
+ PageTransitionCoreTypeIs(currentTransition, |
+ ui::PAGE_TRANSITION_AUTO_BOOKMARK); |
+} |
+ |
+- (void)appDidInstall:(NSNotification*)notification { |
+ [self removeAppFromNotification:notification]; |
+ [self showInfoBarIfNecessary]; |
+} |
+ |
+- (void)removeAppFromNotification:(NSNotification*)notification { |
+ DCHECK([[notification object] isKindOfClass:[InstallationNotifier class]]); |
+ NSString* schemeOfInstalledApp = [notification name]; |
+ __block NSString* appIDToRemove = nil; |
+ [_appsPossiblyBeingInstalled |
+ enumerateObjectsUsingBlock:^(id appID, BOOL* stop) { |
+ NSURL* appURL = |
+ [ios::GetChromeBrowserProvider()->GetNativeAppWhitelistManager() |
+ schemeForAppId:appID]; |
+ if ([[appURL scheme] isEqualToString:schemeOfInstalledApp]) { |
+ appIDToRemove = appID; |
+ *stop = YES; |
+ } |
+ }]; |
+ DCHECK(appIDToRemove); |
+ [_appsPossiblyBeingInstalled removeObject:appIDToRemove]; |
+} |
+ |
+@end |