Index: ios/chrome/app/chrome_app_startup_parameters.mm |
diff --git a/ios/chrome/app/chrome_app_startup_parameters.mm b/ios/chrome/app/chrome_app_startup_parameters.mm |
new file mode 100644 |
index 0000000000000000000000000000000000000000..b00dc5ca2f13bc550cad7c2453e737cc4e8aecc5 |
--- /dev/null |
+++ b/ios/chrome/app/chrome_app_startup_parameters.mm |
@@ -0,0 +1,339 @@ |
+// Copyright 2015 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/app/chrome_app_startup_parameters.h" |
+ |
+#include "base/logging.h" |
+#include "base/mac/foundation_util.h" |
+#include "base/mac/scoped_nsobject.h" |
+#include "base/metrics/histogram.h" |
+#include "base/metrics/user_metrics_action.h" |
+#include "base/strings/sys_string_conversions.h" |
+#include "ios/chrome/browser/chrome_url_constants.h" |
+#include "ios/chrome/browser/xcallback_parameters.h" |
+#include "ios/chrome/common/app_group/app_group_constants.h" |
+#include "ios/chrome/common/x_callback_url.h" |
+#import "net/base/mac/url_conversions.h" |
+#include "url/gurl.h" |
+ |
+namespace { |
+ |
+// Key of the UMA Startup.MobileSessionStartAction histogram. |
+const char kUMAMobileSessionStartActionHistogram[] = |
+ "Startup.MobileSessionStartAction"; |
+ |
+const char kApplicationGroupCommandDelay[] = |
+ "Startup.ApplicationGroupCommandDelay"; |
+ |
+// URL Query String parameter to indicate that this openURL: request arrived |
+// here due to a Smart App Banner presentation on a Google.com page. |
+NSString* const kSmartAppBannerKey = @"safarisab"; |
+ |
+const CGFloat kAppGroupTriggersVoiceSearchTimeout = 15.0; |
+ |
+// Values of the UMA Startup.MobileSessionStartAction histogram. |
+enum MobileSessionStartAction { |
+ START_ACTION_OPEN_HTTP = 0, |
+ START_ACTION_OPEN_HTTPS, |
+ START_ACTION_OPEN_FILE, |
+ START_ACTION_XCALLBACK_OPEN, |
+ START_ACTION_XCALLBACK_OTHER, |
+ START_ACTION_OTHER, |
+ START_ACTION_XCALLBACK_APPGROUP_COMMAND, |
+ MOBILE_SESSION_START_ACTION_COUNT, |
+}; |
+ |
+} // namespace |
+ |
+@implementation ChromeAppStartupParameters { |
+ base::scoped_nsobject<NSString> _secureSourceApp; |
+ base::scoped_nsobject<NSString> _declaredSourceApp; |
+ base::scoped_nsobject<NSURL> _completeURL; |
+} |
+ |
+- (instancetype)initWithExternalURL:(const GURL&)externalURL |
+ xCallbackParameters:(XCallbackParameters*)xCallbackParameters { |
+ NOTREACHED(); |
+ return nil; |
+} |
+ |
+- (instancetype)initWithExternalURL:(const GURL&)externalURL |
+ xCallbackParameters:(XCallbackParameters*)xCallbackParameters |
+ declaredSourceApp:(NSString*)declaredSourceApp |
+ secureSourceApp:(NSString*)secureSourceApp |
+ completeURL:(NSURL*)completeURL { |
+ self = [super initWithExternalURL:externalURL |
+ xCallbackParameters:xCallbackParameters]; |
+ if (self) { |
+ _declaredSourceApp.reset([declaredSourceApp copy]); |
+ _secureSourceApp.reset([secureSourceApp copy]); |
+ _completeURL.reset([completeURL retain]); |
+ } |
+ return self; |
+} |
+ |
++ (instancetype)newChromeAppStartupParametersWithURL:(NSURL*)completeURL |
+ fromSourceApplication:(NSString*)appId { |
+ GURL gurl = net::GURLWithNSURL(completeURL); |
+ |
+ if (!gurl.is_valid() || gurl.scheme().length() == 0) |
+ return nil; |
+ |
+ // TODO(ios): Temporary fix for b/7174478 |
+ if (IsXCallbackURL(gurl)) { |
+ NSString* action = [completeURL path]; |
+ // Currently only "open" and "extension-command" are supported. |
+ // Other actions are being considered (see b/6914153). |
+ if ([action |
+ isEqualToString: |
+ [NSString |
+ stringWithFormat: |
+ @"/%s", app_group::kChromeAppGroupXCallbackCommand]]) { |
+ UMA_HISTOGRAM_ENUMERATION(kUMAMobileSessionStartActionHistogram, |
+ START_ACTION_XCALLBACK_APPGROUP_COMMAND, |
+ MOBILE_SESSION_START_ACTION_COUNT); |
+ return [ChromeAppStartupParameters |
+ newExtensionCommandAppStartupParametersFromWithURL:completeURL |
+ fromSourceApplication:appId]; |
+ } |
+ |
+ if (![action isEqualToString:@"/open"]) { |
+ UMA_HISTOGRAM_ENUMERATION(kUMAMobileSessionStartActionHistogram, |
+ START_ACTION_XCALLBACK_OTHER, |
+ MOBILE_SESSION_START_ACTION_COUNT); |
+ return nil; |
+ } |
+ |
+ UMA_HISTOGRAM_ENUMERATION(kUMAMobileSessionStartActionHistogram, |
+ START_ACTION_XCALLBACK_OPEN, |
+ MOBILE_SESSION_START_ACTION_COUNT); |
+ |
+ std::map<std::string, std::string> parameters = |
+ ExtractQueryParametersFromXCallbackURL(gurl); |
+ GURL url = GURL(parameters["url"]); |
+ if (!url.is_valid() || |
+ (!url.SchemeIs(url::kHttpScheme) && !url.SchemeIs(url::kHttpsScheme))) { |
+ return nil; |
+ } |
+ |
+ base::scoped_nsobject<XCallbackParameters> xcallbackParameters( |
+ [[XCallbackParameters alloc] initWithSourceAppId:appId]); |
+ |
+ return [[ChromeAppStartupParameters alloc] |
+ initWithExternalURL:url |
+ xCallbackParameters:xcallbackParameters |
+ declaredSourceApp:appId |
+ secureSourceApp:nil |
+ completeURL:completeURL]; |
+ |
+ } else if (gurl.SchemeIsFile()) { |
+ UMA_HISTOGRAM_ENUMERATION(kUMAMobileSessionStartActionHistogram, |
+ START_ACTION_OPEN_FILE, |
+ MOBILE_SESSION_START_ACTION_COUNT); |
+ // |url| is the path to a file received from another application. |
+ GURL::Replacements replacements; |
+ const std::string host(kChromeUIExternalFileHost); |
+ std::string filename = gurl.ExtractFileName(); |
+ replacements.SetPathStr(filename); |
+ replacements.SetSchemeStr(kChromeUIScheme); |
+ replacements.SetHostStr(host); |
+ GURL externalURL = gurl.ReplaceComponents(replacements); |
+ if (!externalURL.is_valid()) |
+ return nil; |
+ return [[ChromeAppStartupParameters alloc] initWithExternalURL:externalURL |
+ xCallbackParameters:nil |
+ declaredSourceApp:appId |
+ secureSourceApp:nil |
+ completeURL:completeURL]; |
+ } else { |
+ // Replace the scheme with https or http depending on whether the input |
+ // |url| scheme ends with an 's'. |
+ BOOL useHttps = gurl.scheme()[gurl.scheme().length() - 1] == 's'; |
+ MobileSessionStartAction action = |
+ useHttps ? START_ACTION_OPEN_HTTPS : START_ACTION_OPEN_HTTP; |
+ UMA_HISTOGRAM_ENUMERATION(kUMAMobileSessionStartActionHistogram, action, |
+ MOBILE_SESSION_START_ACTION_COUNT); |
+ GURL::Replacements replace_scheme; |
+ if (useHttps) |
+ replace_scheme.SetSchemeStr(url::kHttpsScheme); |
+ else |
+ replace_scheme.SetSchemeStr(url::kHttpScheme); |
+ GURL externalURL = gurl.ReplaceComponents(replace_scheme); |
+ if (!externalURL.is_valid()) |
+ return nil; |
+ return [[ChromeAppStartupParameters alloc] initWithExternalURL:externalURL |
+ xCallbackParameters:nil |
+ declaredSourceApp:appId |
+ secureSourceApp:nil |
+ completeURL:completeURL]; |
+ } |
+} |
+ |
++ (instancetype)newExtensionCommandAppStartupParametersFromWithURL:(NSURL*)url |
+ fromSourceApplication: |
+ (NSString*)appId { |
+ NSString* appGroup = app_group::ApplicationGroup(); |
+ base::scoped_nsobject<NSUserDefaults> sharedDefaults( |
+ [[NSUserDefaults alloc] initWithSuiteName:appGroup]); |
+ |
+ NSString* commandDictionaryPreference = |
+ base::SysUTF8ToNSString(app_group::kChromeAppGroupCommandPreference); |
+ NSDictionary* commandDictionary = base::mac::ObjCCast<NSDictionary>( |
+ [sharedDefaults objectForKey:commandDictionaryPreference]); |
+ |
+ [sharedDefaults removeObjectForKey:commandDictionaryPreference]; |
+ |
+ // |sharedDefaults| is used for communication between apps. Synchronize to |
+ // avoid synchronization issues (like removing the next order). |
+ [sharedDefaults synchronize]; |
+ |
+ if (!commandDictionary) { |
+ return nil; |
+ } |
+ |
+ NSString* commandCallerPreference = |
+ base::SysUTF8ToNSString(app_group::kChromeAppGroupCommandAppPreference); |
+ NSString* commandCaller = base::mac::ObjCCast<NSString>( |
+ [commandDictionary objectForKey:commandCallerPreference]); |
+ |
+ NSString* commandPreference = base::SysUTF8ToNSString( |
+ app_group::kChromeAppGroupCommandCommandPreference); |
+ NSString* command = base::mac::ObjCCast<NSString>( |
+ [commandDictionary objectForKey:commandPreference]); |
+ |
+ NSString* commandTimePreference = |
+ base::SysUTF8ToNSString(app_group::kChromeAppGroupCommandTimePreference); |
+ id commandTime = base::mac::ObjCCast<NSDate>( |
+ [commandDictionary objectForKey:commandTimePreference]); |
+ |
+ NSString* commandParameterPreference = base::SysUTF8ToNSString( |
+ app_group::kChromeAppGroupCommandParameterPreference); |
+ NSString* commandParameter = base::mac::ObjCCast<NSString>( |
+ [commandDictionary objectForKey:commandParameterPreference]); |
+ |
+ if (!commandCaller || !command || !commandTimePreference) { |
+ return nil; |
+ } |
+ |
+ // Check the time of the last request to avoid app from intercepting old |
+ // open url request and replay it later. |
+ NSTimeInterval delay = [[NSDate date] timeIntervalSinceDate:commandTime]; |
+ UMA_HISTOGRAM_COUNTS_100(kApplicationGroupCommandDelay, delay); |
+ if (delay > kAppGroupTriggersVoiceSearchTimeout) |
+ return nil; |
+ return [ChromeAppStartupParameters |
+ newAppStartupParametersForCommand:command |
+ withParameter:commandParameter |
+ withURL:url |
+ fromSourceApplication:appId |
+ fromSecureSourceApplication:commandCaller]; |
+} |
+ |
++ (instancetype)newAppStartupParametersForCommand:(NSString*)command |
+ withParameter:(id)parameter |
+ withURL:(NSURL*)url |
+ fromSourceApplication:(NSString*)appId |
+ fromSecureSourceApplication:(NSString*)secureSourceApp { |
+ if ([command |
+ isEqualToString:base::SysUTF8ToNSString( |
+ app_group::kChromeAppGroupVoiceSearchCommand)]) { |
+ ChromeAppStartupParameters* params = [[ChromeAppStartupParameters alloc] |
+ initWithExternalURL:GURL(kChromeUINewTabURL) |
+ xCallbackParameters:nil |
+ declaredSourceApp:appId |
+ secureSourceApp:secureSourceApp |
+ completeURL:url]; |
+ [params setLaunchVoiceSearch:YES]; |
+ return params; |
+ } |
+ |
+ if ([command isEqualToString:base::SysUTF8ToNSString( |
+ app_group::kChromeAppGroupNewTabCommand)]) { |
+ return [[ChromeAppStartupParameters alloc] |
+ initWithExternalURL:GURL(kChromeUINewTabURL) |
+ xCallbackParameters:nil |
+ declaredSourceApp:appId |
+ secureSourceApp:secureSourceApp |
+ completeURL:url]; |
+ } |
+ if ([command isEqualToString:base::SysUTF8ToNSString( |
+ app_group::kChromeAppGroupOpenURLCommand)]) { |
+ if (!parameter || ![parameter isKindOfClass:[NSString class]]) |
+ return nil; |
+ GURL externalURL(base::SysNSStringToUTF8(parameter)); |
+ if (!externalURL.is_valid() || !externalURL.SchemeIsHTTPOrHTTPS()) |
+ return nil; |
+ return |
+ [[ChromeAppStartupParameters alloc] initWithExternalURL:externalURL |
+ xCallbackParameters:nil |
+ declaredSourceApp:appId |
+ secureSourceApp:secureSourceApp |
+ completeURL:url]; |
+ } |
+ |
+ return nil; |
+} |
+ |
+- (MobileSessionCallerApp)callerApp { |
+ if ([_secureSourceApp isEqualToString:@"TodayExtension"]) |
+ return CALLER_APP_GOOGLE_CHROME_TODAY_EXTENSION; |
+ |
+ if (![_declaredSourceApp length]) |
+ return CALLER_APP_NOT_AVAILABLE; |
+ if ([_declaredSourceApp isEqualToString:@"com.google.GoogleMobile"]) |
+ return CALLER_APP_GOOGLE_SEARCH; |
+ if ([_declaredSourceApp isEqualToString:@"com.google.Gmail"]) |
+ return CALLER_APP_GOOGLE_GMAIL; |
+ if ([_declaredSourceApp isEqualToString:@"com.google.GooglePlus"]) |
+ return CALLER_APP_GOOGLE_PLUS; |
+ if ([_declaredSourceApp isEqualToString:@"com.google.Drive"]) |
+ return CALLER_APP_GOOGLE_DRIVE; |
+ if ([_declaredSourceApp isEqualToString:@"com.google.b612"]) |
+ return CALLER_APP_GOOGLE_EARTH; |
+ if ([_declaredSourceApp isEqualToString:@"com.google.ios.youtube"]) |
+ return CALLER_APP_GOOGLE_YOUTUBE; |
+ if ([_declaredSourceApp isEqualToString:@"com.google.Maps"]) |
+ return CALLER_APP_GOOGLE_MAPS; |
+ if ([_declaredSourceApp hasPrefix:@"com.google."]) |
+ return CALLER_APP_GOOGLE_OTHER; |
+ if ([_declaredSourceApp isEqualToString:@"com.apple.mobilesafari"]) |
+ return CALLER_APP_APPLE_MOBILESAFARI; |
+ if ([_declaredSourceApp hasPrefix:@"com.apple."]) |
+ return CALLER_APP_APPLE_OTHER; |
+ |
+ return CALLER_APP_OTHER; |
+} |
+ |
+- (first_run::ExternalLaunch)launchSource { |
+ if ([self callerApp] != CALLER_APP_APPLE_MOBILESAFARI) { |
+ return first_run::LAUNCH_BY_OTHERS; |
+ } |
+ |
+ NSString* query = [_completeURL query]; |
+ // Takes care of degenerated case of no QUERY_STRING. |
+ if (![query length]) |
+ return first_run::LAUNCH_BY_MOBILESAFARI; |
+ // Look for |kSmartAppBannerKey| anywhere within the query string. |
+ NSRange found = [query rangeOfString:kSmartAppBannerKey]; |
+ if (found.location == NSNotFound) |
+ return first_run::LAUNCH_BY_MOBILESAFARI; |
+ // |kSmartAppBannerKey| can be at the beginning or end of the query |
+ // string and may also be optionally followed by a equal sign and a value. |
+ // For now, just look for the presence of the key and ignore the value. |
+ if (found.location + found.length < [query length]) { |
+ // There are characters following the found location. |
+ unichar charAfter = |
+ [query characterAtIndex:(found.location + found.length)]; |
+ if (charAfter != '&' && charAfter != '=') |
+ return first_run::LAUNCH_BY_MOBILESAFARI; |
+ } |
+ if (found.location > 0) { |
+ unichar charBefore = [query characterAtIndex:(found.location - 1)]; |
+ if (charBefore != '&') |
+ return first_run::LAUNCH_BY_MOBILESAFARI; |
+ } |
+ return first_run::LAUNCH_BY_SMARTAPPBANNER; |
+} |
+ |
+@end |