OLD | NEW |
(Empty) | |
| 1 // Copyright 2015 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 "ios/chrome/app/chrome_app_startup_parameters.h" |
| 6 |
| 7 #include "base/logging.h" |
| 8 #include "base/mac/foundation_util.h" |
| 9 #include "base/mac/scoped_nsobject.h" |
| 10 #include "base/metrics/histogram.h" |
| 11 #include "base/metrics/user_metrics_action.h" |
| 12 #include "base/strings/sys_string_conversions.h" |
| 13 #include "ios/chrome/browser/chrome_url_constants.h" |
| 14 #include "ios/chrome/browser/xcallback_parameters.h" |
| 15 #include "ios/chrome/common/app_group/app_group_constants.h" |
| 16 #include "ios/chrome/common/x_callback_url.h" |
| 17 #import "net/base/mac/url_conversions.h" |
| 18 #include "url/gurl.h" |
| 19 |
| 20 namespace { |
| 21 |
| 22 // Key of the UMA Startup.MobileSessionStartAction histogram. |
| 23 const char kUMAMobileSessionStartActionHistogram[] = |
| 24 "Startup.MobileSessionStartAction"; |
| 25 |
| 26 const char kApplicationGroupCommandDelay[] = |
| 27 "Startup.ApplicationGroupCommandDelay"; |
| 28 |
| 29 // URL Query String parameter to indicate that this openURL: request arrived |
| 30 // here due to a Smart App Banner presentation on a Google.com page. |
| 31 NSString* const kSmartAppBannerKey = @"safarisab"; |
| 32 |
| 33 const CGFloat kAppGroupTriggersVoiceSearchTimeout = 15.0; |
| 34 |
| 35 // Values of the UMA Startup.MobileSessionStartAction histogram. |
| 36 enum MobileSessionStartAction { |
| 37 START_ACTION_OPEN_HTTP = 0, |
| 38 START_ACTION_OPEN_HTTPS, |
| 39 START_ACTION_OPEN_FILE, |
| 40 START_ACTION_XCALLBACK_OPEN, |
| 41 START_ACTION_XCALLBACK_OTHER, |
| 42 START_ACTION_OTHER, |
| 43 START_ACTION_XCALLBACK_APPGROUP_COMMAND, |
| 44 MOBILE_SESSION_START_ACTION_COUNT, |
| 45 }; |
| 46 |
| 47 } // namespace |
| 48 |
| 49 @implementation ChromeAppStartupParameters { |
| 50 base::scoped_nsobject<NSString> _secureSourceApp; |
| 51 base::scoped_nsobject<NSString> _declaredSourceApp; |
| 52 base::scoped_nsobject<NSURL> _completeURL; |
| 53 } |
| 54 |
| 55 - (instancetype)initWithExternalURL:(const GURL&)externalURL |
| 56 xCallbackParameters:(XCallbackParameters*)xCallbackParameters { |
| 57 NOTREACHED(); |
| 58 return nil; |
| 59 } |
| 60 |
| 61 - (instancetype)initWithExternalURL:(const GURL&)externalURL |
| 62 xCallbackParameters:(XCallbackParameters*)xCallbackParameters |
| 63 declaredSourceApp:(NSString*)declaredSourceApp |
| 64 secureSourceApp:(NSString*)secureSourceApp |
| 65 completeURL:(NSURL*)completeURL { |
| 66 self = [super initWithExternalURL:externalURL |
| 67 xCallbackParameters:xCallbackParameters]; |
| 68 if (self) { |
| 69 _declaredSourceApp.reset([declaredSourceApp copy]); |
| 70 _secureSourceApp.reset([secureSourceApp copy]); |
| 71 _completeURL.reset([completeURL retain]); |
| 72 } |
| 73 return self; |
| 74 } |
| 75 |
| 76 + (instancetype)newChromeAppStartupParametersWithURL:(NSURL*)completeURL |
| 77 fromSourceApplication:(NSString*)appId { |
| 78 GURL gurl = net::GURLWithNSURL(completeURL); |
| 79 |
| 80 if (!gurl.is_valid() || gurl.scheme().length() == 0) |
| 81 return nil; |
| 82 |
| 83 // TODO(ios): Temporary fix for b/7174478 |
| 84 if (IsXCallbackURL(gurl)) { |
| 85 NSString* action = [completeURL path]; |
| 86 // Currently only "open" and "extension-command" are supported. |
| 87 // Other actions are being considered (see b/6914153). |
| 88 if ([action |
| 89 isEqualToString: |
| 90 [NSString |
| 91 stringWithFormat: |
| 92 @"/%s", app_group::kChromeAppGroupXCallbackCommand]]) { |
| 93 UMA_HISTOGRAM_ENUMERATION(kUMAMobileSessionStartActionHistogram, |
| 94 START_ACTION_XCALLBACK_APPGROUP_COMMAND, |
| 95 MOBILE_SESSION_START_ACTION_COUNT); |
| 96 return [ChromeAppStartupParameters |
| 97 newExtensionCommandAppStartupParametersFromWithURL:completeURL |
| 98 fromSourceApplication:appId]; |
| 99 } |
| 100 |
| 101 if (![action isEqualToString:@"/open"]) { |
| 102 UMA_HISTOGRAM_ENUMERATION(kUMAMobileSessionStartActionHistogram, |
| 103 START_ACTION_XCALLBACK_OTHER, |
| 104 MOBILE_SESSION_START_ACTION_COUNT); |
| 105 return nil; |
| 106 } |
| 107 |
| 108 UMA_HISTOGRAM_ENUMERATION(kUMAMobileSessionStartActionHistogram, |
| 109 START_ACTION_XCALLBACK_OPEN, |
| 110 MOBILE_SESSION_START_ACTION_COUNT); |
| 111 |
| 112 std::map<std::string, std::string> parameters = |
| 113 ExtractQueryParametersFromXCallbackURL(gurl); |
| 114 GURL url = GURL(parameters["url"]); |
| 115 if (!url.is_valid() || |
| 116 (!url.SchemeIs(url::kHttpScheme) && !url.SchemeIs(url::kHttpsScheme))) { |
| 117 return nil; |
| 118 } |
| 119 |
| 120 base::scoped_nsobject<XCallbackParameters> xcallbackParameters( |
| 121 [[XCallbackParameters alloc] initWithSourceAppId:appId]); |
| 122 |
| 123 return [[ChromeAppStartupParameters alloc] |
| 124 initWithExternalURL:url |
| 125 xCallbackParameters:xcallbackParameters |
| 126 declaredSourceApp:appId |
| 127 secureSourceApp:nil |
| 128 completeURL:completeURL]; |
| 129 |
| 130 } else if (gurl.SchemeIsFile()) { |
| 131 UMA_HISTOGRAM_ENUMERATION(kUMAMobileSessionStartActionHistogram, |
| 132 START_ACTION_OPEN_FILE, |
| 133 MOBILE_SESSION_START_ACTION_COUNT); |
| 134 // |url| is the path to a file received from another application. |
| 135 GURL::Replacements replacements; |
| 136 const std::string host(kChromeUIExternalFileHost); |
| 137 std::string filename = gurl.ExtractFileName(); |
| 138 replacements.SetPathStr(filename); |
| 139 replacements.SetSchemeStr(kChromeUIScheme); |
| 140 replacements.SetHostStr(host); |
| 141 GURL externalURL = gurl.ReplaceComponents(replacements); |
| 142 if (!externalURL.is_valid()) |
| 143 return nil; |
| 144 return [[ChromeAppStartupParameters alloc] initWithExternalURL:externalURL |
| 145 xCallbackParameters:nil |
| 146 declaredSourceApp:appId |
| 147 secureSourceApp:nil |
| 148 completeURL:completeURL]; |
| 149 } else { |
| 150 // Replace the scheme with https or http depending on whether the input |
| 151 // |url| scheme ends with an 's'. |
| 152 BOOL useHttps = gurl.scheme()[gurl.scheme().length() - 1] == 's'; |
| 153 MobileSessionStartAction action = |
| 154 useHttps ? START_ACTION_OPEN_HTTPS : START_ACTION_OPEN_HTTP; |
| 155 UMA_HISTOGRAM_ENUMERATION(kUMAMobileSessionStartActionHistogram, action, |
| 156 MOBILE_SESSION_START_ACTION_COUNT); |
| 157 GURL::Replacements replace_scheme; |
| 158 if (useHttps) |
| 159 replace_scheme.SetSchemeStr(url::kHttpsScheme); |
| 160 else |
| 161 replace_scheme.SetSchemeStr(url::kHttpScheme); |
| 162 GURL externalURL = gurl.ReplaceComponents(replace_scheme); |
| 163 if (!externalURL.is_valid()) |
| 164 return nil; |
| 165 return [[ChromeAppStartupParameters alloc] initWithExternalURL:externalURL |
| 166 xCallbackParameters:nil |
| 167 declaredSourceApp:appId |
| 168 secureSourceApp:nil |
| 169 completeURL:completeURL]; |
| 170 } |
| 171 } |
| 172 |
| 173 + (instancetype)newExtensionCommandAppStartupParametersFromWithURL:(NSURL*)url |
| 174 fromSourceApplication: |
| 175 (NSString*)appId { |
| 176 NSString* appGroup = app_group::ApplicationGroup(); |
| 177 base::scoped_nsobject<NSUserDefaults> sharedDefaults( |
| 178 [[NSUserDefaults alloc] initWithSuiteName:appGroup]); |
| 179 |
| 180 NSString* commandDictionaryPreference = |
| 181 base::SysUTF8ToNSString(app_group::kChromeAppGroupCommandPreference); |
| 182 NSDictionary* commandDictionary = base::mac::ObjCCast<NSDictionary>( |
| 183 [sharedDefaults objectForKey:commandDictionaryPreference]); |
| 184 |
| 185 [sharedDefaults removeObjectForKey:commandDictionaryPreference]; |
| 186 |
| 187 // |sharedDefaults| is used for communication between apps. Synchronize to |
| 188 // avoid synchronization issues (like removing the next order). |
| 189 [sharedDefaults synchronize]; |
| 190 |
| 191 if (!commandDictionary) { |
| 192 return nil; |
| 193 } |
| 194 |
| 195 NSString* commandCallerPreference = |
| 196 base::SysUTF8ToNSString(app_group::kChromeAppGroupCommandAppPreference); |
| 197 NSString* commandCaller = base::mac::ObjCCast<NSString>( |
| 198 [commandDictionary objectForKey:commandCallerPreference]); |
| 199 |
| 200 NSString* commandPreference = base::SysUTF8ToNSString( |
| 201 app_group::kChromeAppGroupCommandCommandPreference); |
| 202 NSString* command = base::mac::ObjCCast<NSString>( |
| 203 [commandDictionary objectForKey:commandPreference]); |
| 204 |
| 205 NSString* commandTimePreference = |
| 206 base::SysUTF8ToNSString(app_group::kChromeAppGroupCommandTimePreference); |
| 207 id commandTime = base::mac::ObjCCast<NSDate>( |
| 208 [commandDictionary objectForKey:commandTimePreference]); |
| 209 |
| 210 NSString* commandParameterPreference = base::SysUTF8ToNSString( |
| 211 app_group::kChromeAppGroupCommandParameterPreference); |
| 212 NSString* commandParameter = base::mac::ObjCCast<NSString>( |
| 213 [commandDictionary objectForKey:commandParameterPreference]); |
| 214 |
| 215 if (!commandCaller || !command || !commandTimePreference) { |
| 216 return nil; |
| 217 } |
| 218 |
| 219 // Check the time of the last request to avoid app from intercepting old |
| 220 // open url request and replay it later. |
| 221 NSTimeInterval delay = [[NSDate date] timeIntervalSinceDate:commandTime]; |
| 222 UMA_HISTOGRAM_COUNTS_100(kApplicationGroupCommandDelay, delay); |
| 223 if (delay > kAppGroupTriggersVoiceSearchTimeout) |
| 224 return nil; |
| 225 return [ChromeAppStartupParameters |
| 226 newAppStartupParametersForCommand:command |
| 227 withParameter:commandParameter |
| 228 withURL:url |
| 229 fromSourceApplication:appId |
| 230 fromSecureSourceApplication:commandCaller]; |
| 231 } |
| 232 |
| 233 + (instancetype)newAppStartupParametersForCommand:(NSString*)command |
| 234 withParameter:(id)parameter |
| 235 withURL:(NSURL*)url |
| 236 fromSourceApplication:(NSString*)appId |
| 237 fromSecureSourceApplication:(NSString*)secureSourceApp { |
| 238 if ([command |
| 239 isEqualToString:base::SysUTF8ToNSString( |
| 240 app_group::kChromeAppGroupVoiceSearchCommand)]) { |
| 241 ChromeAppStartupParameters* params = [[ChromeAppStartupParameters alloc] |
| 242 initWithExternalURL:GURL(kChromeUINewTabURL) |
| 243 xCallbackParameters:nil |
| 244 declaredSourceApp:appId |
| 245 secureSourceApp:secureSourceApp |
| 246 completeURL:url]; |
| 247 [params setLaunchVoiceSearch:YES]; |
| 248 return params; |
| 249 } |
| 250 |
| 251 if ([command isEqualToString:base::SysUTF8ToNSString( |
| 252 app_group::kChromeAppGroupNewTabCommand)]) { |
| 253 return [[ChromeAppStartupParameters alloc] |
| 254 initWithExternalURL:GURL(kChromeUINewTabURL) |
| 255 xCallbackParameters:nil |
| 256 declaredSourceApp:appId |
| 257 secureSourceApp:secureSourceApp |
| 258 completeURL:url]; |
| 259 } |
| 260 if ([command isEqualToString:base::SysUTF8ToNSString( |
| 261 app_group::kChromeAppGroupOpenURLCommand)]) { |
| 262 if (!parameter || ![parameter isKindOfClass:[NSString class]]) |
| 263 return nil; |
| 264 GURL externalURL(base::SysNSStringToUTF8(parameter)); |
| 265 if (!externalURL.is_valid() || !externalURL.SchemeIsHTTPOrHTTPS()) |
| 266 return nil; |
| 267 return |
| 268 [[ChromeAppStartupParameters alloc] initWithExternalURL:externalURL |
| 269 xCallbackParameters:nil |
| 270 declaredSourceApp:appId |
| 271 secureSourceApp:secureSourceApp |
| 272 completeURL:url]; |
| 273 } |
| 274 |
| 275 return nil; |
| 276 } |
| 277 |
| 278 - (MobileSessionCallerApp)callerApp { |
| 279 if ([_secureSourceApp isEqualToString:@"TodayExtension"]) |
| 280 return CALLER_APP_GOOGLE_CHROME_TODAY_EXTENSION; |
| 281 |
| 282 if (![_declaredSourceApp length]) |
| 283 return CALLER_APP_NOT_AVAILABLE; |
| 284 if ([_declaredSourceApp isEqualToString:@"com.google.GoogleMobile"]) |
| 285 return CALLER_APP_GOOGLE_SEARCH; |
| 286 if ([_declaredSourceApp isEqualToString:@"com.google.Gmail"]) |
| 287 return CALLER_APP_GOOGLE_GMAIL; |
| 288 if ([_declaredSourceApp isEqualToString:@"com.google.GooglePlus"]) |
| 289 return CALLER_APP_GOOGLE_PLUS; |
| 290 if ([_declaredSourceApp isEqualToString:@"com.google.Drive"]) |
| 291 return CALLER_APP_GOOGLE_DRIVE; |
| 292 if ([_declaredSourceApp isEqualToString:@"com.google.b612"]) |
| 293 return CALLER_APP_GOOGLE_EARTH; |
| 294 if ([_declaredSourceApp isEqualToString:@"com.google.ios.youtube"]) |
| 295 return CALLER_APP_GOOGLE_YOUTUBE; |
| 296 if ([_declaredSourceApp isEqualToString:@"com.google.Maps"]) |
| 297 return CALLER_APP_GOOGLE_MAPS; |
| 298 if ([_declaredSourceApp hasPrefix:@"com.google."]) |
| 299 return CALLER_APP_GOOGLE_OTHER; |
| 300 if ([_declaredSourceApp isEqualToString:@"com.apple.mobilesafari"]) |
| 301 return CALLER_APP_APPLE_MOBILESAFARI; |
| 302 if ([_declaredSourceApp hasPrefix:@"com.apple."]) |
| 303 return CALLER_APP_APPLE_OTHER; |
| 304 |
| 305 return CALLER_APP_OTHER; |
| 306 } |
| 307 |
| 308 - (first_run::ExternalLaunch)launchSource { |
| 309 if ([self callerApp] != CALLER_APP_APPLE_MOBILESAFARI) { |
| 310 return first_run::LAUNCH_BY_OTHERS; |
| 311 } |
| 312 |
| 313 NSString* query = [_completeURL query]; |
| 314 // Takes care of degenerated case of no QUERY_STRING. |
| 315 if (![query length]) |
| 316 return first_run::LAUNCH_BY_MOBILESAFARI; |
| 317 // Look for |kSmartAppBannerKey| anywhere within the query string. |
| 318 NSRange found = [query rangeOfString:kSmartAppBannerKey]; |
| 319 if (found.location == NSNotFound) |
| 320 return first_run::LAUNCH_BY_MOBILESAFARI; |
| 321 // |kSmartAppBannerKey| can be at the beginning or end of the query |
| 322 // string and may also be optionally followed by a equal sign and a value. |
| 323 // For now, just look for the presence of the key and ignore the value. |
| 324 if (found.location + found.length < [query length]) { |
| 325 // There are characters following the found location. |
| 326 unichar charAfter = |
| 327 [query characterAtIndex:(found.location + found.length)]; |
| 328 if (charAfter != '&' && charAfter != '=') |
| 329 return first_run::LAUNCH_BY_MOBILESAFARI; |
| 330 } |
| 331 if (found.location > 0) { |
| 332 unichar charBefore = [query characterAtIndex:(found.location - 1)]; |
| 333 if (charBefore != '&') |
| 334 return first_run::LAUNCH_BY_MOBILESAFARI; |
| 335 } |
| 336 return first_run::LAUNCH_BY_SMARTAPPBANNER; |
| 337 } |
| 338 |
| 339 @end |
OLD | NEW |