blob: aeafd78661e77ff528a2ac727d25163825254829 [file] [log] [blame]
// 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/metrics/histogram_macros.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"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
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 {
NSString* _secureSourceApp;
NSString* _declaredSourceApp;
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 = [declaredSourceApp copy];
_secureSourceApp = [secureSourceApp copy];
_completeURL = completeURL;
}
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;
}
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();
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::kChromeAppGroupFocusOmniboxCommand)]) {
ChromeAppStartupParameters* params = [[ChromeAppStartupParameters alloc]
initWithExternalURL:GURL(kChromeUINewTabURL)
xCallbackParameters:nil
declaredSourceApp:appId
secureSourceApp:secureSourceApp
completeURL:url];
[params setLaunchFocusOmnibox:YES];
return params;
}
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];
}
if ([command
isEqualToString:base::SysUTF8ToNSString(
app_group::kChromeAppGroupQRScannerCommand)]) {
ChromeAppStartupParameters* params = [[ChromeAppStartupParameters alloc]
initWithExternalURL:GURL(kChromeUINewTabURL)
xCallbackParameters:nil
declaredSourceApp:appId
secureSourceApp:secureSourceApp
completeURL:url];
[params setLaunchQRScanner:YES];
return params;
}
if ([command isEqualToString:
base::SysUTF8ToNSString(
app_group::kChromeAppGroupIncognitoSearchCommand)]) {
ChromeAppStartupParameters* params = [[ChromeAppStartupParameters alloc]
initWithExternalURL:GURL(kChromeUINewTabURL)
xCallbackParameters:nil
declaredSourceApp:appId
secureSourceApp:secureSourceApp
completeURL:url];
[params setLaunchInIncognito:YES];
[params setLaunchFocusOmnibox:YES];
return params;
}
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