| // Copyright 2015 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "ios/chrome/app/startup/chrome_app_startup_parameters.h" |
| |
| #import "base/apple/bundle_locations.h" |
| #import "base/apple/foundation_util.h" |
| #import "base/metrics/histogram_functions.h" |
| #import "base/metrics/histogram_macros.h" |
| #import "base/metrics/user_metrics.h" |
| #import "base/metrics/user_metrics_action.h" |
| #import "base/strings/stringprintf.h" |
| #import "base/strings/sys_string_conversions.h" |
| #import "components/password_manager/core/browser/manage_passwords_referrer.h" |
| #import "ios/chrome/browser/default_browser/model/utils.h" |
| #import "ios/chrome/browser/shared/model/url/chrome_url_constants.h" |
| #import "ios/chrome/browser/shared/public/features/features.h" |
| #import "ios/chrome/common/app_group/app_group_constants.h" |
| #import "ios/chrome/common/x_callback_url.h" |
| #import "ios/components/webui/web_ui_url_constants.h" |
| #import "net/base/apple/url_conversions.h" |
| #import "net/base/url_util.h" |
| #import "url/gurl.h" |
| |
| using base::UmaHistogramEnumeration; |
| |
| namespace { |
| |
| const char kApplicationGroupCommandDelay[] = |
| "Startup.ApplicationGroupCommandDelay"; |
| |
| // Host string used to detect an "external action" scheme URL. |
| NSString* const kExternalActionURLHost = @"ChromeExternalAction"; |
| |
| // Action path string for launching the default browser settings using external |
| // actions. |
| NSString* const kExternalActionDefaultBrowserSettings = |
| @"DefaultBrowserSettings"; |
| |
| // Action path string for Opening an NTP using external actions. |
| NSString* const kExternalActionOpenNTP = @"OpenNTP"; |
| |
| // 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"; |
| |
| // TODO(crbug.com/40725595): When swift is supported move WidgetKit constants to |
| // a file where they can be shared with the extension. Currently these are also |
| // declared as URLs in ios/c/widget_kit_extension/widget_urls.swift. |
| // |
| // Scheme used by the widget extension actions. It's important that this scheme |
| // is never defined as Custom URL Scheme for Chrome so only the widgets can use |
| // the actions on it. |
| NSString* const kWidgetKitSchemeChrome = @"chromewidgetkit"; |
| // Host used to identify Search (small) widget. |
| NSString* const kWidgetKitHostSearchWidget = @"search-widget"; |
| // Host used to identify Quick Actions (medium) widget. |
| NSString* const kWidgetKitHostQuickActionsWidget = @"quick-actions-widget"; |
| // Host used to identify Dino Game (small) widget. |
| NSString* const kWidgetKitHostDinoGameWidget = @"dino-game-widget"; |
| // Host used to identify the Lockscreen Launcher widget. |
| NSString* const kWidgetKitHostLockscreenLauncherWidget = |
| @"lockscreen-launcher-widget"; |
| // Host used to identify the Chrome Shortcuts widget. |
| NSString* const kWidgetKitHostShortcutsWidget = @"shortcuts-widget"; |
| // Host used to identify the Search Passwords widget. |
| NSString* const kWidgetKitHostSearchPasswordsWidget = |
| @"search-passwords-widget"; |
| // Path for search action. |
| NSString* const kWidgetKitActionSearch = @"/search"; |
| // Path for incognito action. |
| NSString* const kWidgetKitActionIncognito = @"/incognito"; |
| // Path for Voice Search action. |
| NSString* const kWidgetKitActionVoiceSearch = @"/voicesearch"; |
| // Path for QR Reader action. |
| NSString* const kWidgetKitActionQRReader = @"/qrreader"; |
| // Path for Lens action. |
| NSString* const kWidgetKitActionLens = @"/lens"; |
| // Path for Game action. |
| NSString* const kWidgetKitActionGame = @"/game"; |
| // Path for open URL action. |
| NSString* const kWidgetKitActionOpenURL = @"/open"; |
| // Path for search passwords action. |
| NSString* const kWidgetKitActionSearchPasswords = @"/search-passwords"; |
| |
| const CGFloat kAppGroupTriggersVoiceSearchTimeout = 15.0; |
| |
| // Histogram helper to log the UMA IOS.WidgetKit.Action histogram. |
| void LogWidgetKitAction(WidgetKitExtensionAction action) { |
| UmaHistogramEnumeration(kWidgetKitActionHistogram, action); |
| } |
| |
| bool CallerAppIsFirstParty(MobileSessionCallerApp callerApp) { |
| switch (callerApp) { |
| case CALLER_APP_GOOGLE_SEARCH: |
| case CALLER_APP_GOOGLE_GMAIL: |
| case CALLER_APP_GOOGLE_PLUS: |
| case CALLER_APP_GOOGLE_DRIVE: |
| case CALLER_APP_GOOGLE_EARTH: |
| case CALLER_APP_GOOGLE_OTHER: |
| case CALLER_APP_GOOGLE_YOUTUBE: |
| case CALLER_APP_GOOGLE_MAPS: |
| case CALLER_APP_GOOGLE_CHROME_TODAY_EXTENSION: |
| case CALLER_APP_GOOGLE_CHROME_SEARCH_EXTENSION: |
| case CALLER_APP_GOOGLE_CHROME_CONTENT_EXTENSION: |
| case CALLER_APP_GOOGLE_CHROME_SHARE_EXTENSION: |
| case CALLER_APP_GOOGLE_CHROME_OPEN_EXTENSION: |
| case CALLER_APP_GOOGLE_CHROME: |
| return true; |
| case CALLER_APP_OTHER: |
| case CALLER_APP_APPLE_MOBILESAFARI: |
| case CALLER_APP_APPLE_OTHER: |
| case CALLER_APP_THIRD_PARTY: |
| case CALLER_APP_NOT_AVAILABLE: |
| case MOBILE_SESSION_CALLER_APP_COUNT: |
| return false; |
| } |
| } |
| |
| TabOpeningPostOpeningAction XCallbackPoaToPostOpeningAction( |
| const std::string& poa_param) { |
| if (poa_param == "default-browser-settings") { |
| return SHOW_DEFAULT_BROWSER_SETTINGS; |
| } |
| return NO_ACTION; |
| } |
| |
| } // namespace |
| |
| @implementation ChromeAppStartupParameters { |
| NSString* _secureSourceApp; |
| NSString* _declaredSourceApp; |
| } |
| |
| - (instancetype)initWithExternalURL:(const GURL&)externalURL |
| declaredSourceApp:(NSString*)declaredSourceApp |
| secureSourceApp:(NSString*)secureSourceApp |
| completeURL:(NSURL*)completeURL |
| applicationMode:(ApplicationModeForTabOpening)mode |
| forceApplicationMode:(BOOL)forceApplicationMode { |
| self = [super initWithExternalURL:externalURL |
| completeURL:net::GURLWithNSURL(completeURL) |
| sourceAppID:declaredSourceApp |
| applicationMode:mode |
| forceApplicationMode:forceApplicationMode]; |
| if (self) { |
| _declaredSourceApp = [declaredSourceApp copy]; |
| _secureSourceApp = [secureSourceApp copy]; |
| } |
| return self; |
| } |
| |
| + (instancetype)startupParametersWithURL:(NSURL*)completeURL |
| sourceApplication:(NSString*)appID |
| applicationMode:(ApplicationModeForTabOpening)mode |
| forceApplicationMode:(BOOL)forceApplicationMode { |
| GURL parsedURL = net::GURLWithNSURL(completeURL); |
| |
| if (!parsedURL.is_valid() || parsedURL.scheme().length() == 0) { |
| return nil; |
| } |
| |
| // Log browser started indirectly for default browser promo experiment stats. |
| LogBrowserIndirectlylaunched(); |
| |
| if ([completeURL.scheme isEqualToString:kWidgetKitSchemeChrome]) { |
| UMA_HISTOGRAM_ENUMERATION(kUMAMobileSessionStartActionHistogram, |
| START_ACTION_WIDGET_KIT_COMMAND, |
| MOBILE_SESSION_START_ACTION_COUNT); |
| |
| base::UmaHistogramEnumeration(kAppLaunchSource, AppLaunchSource::WIDGET); |
| |
| const char* command = ""; |
| NSString* sourceWidget = completeURL.host; |
| NSString* externalText = nil; |
| |
| if ([completeURL.path isEqualToString:kWidgetKitActionSearch]) { |
| command = app_group::kChromeAppGroupFocusOmniboxCommand; |
| } else if ([completeURL.path isEqualToString:kWidgetKitActionIncognito]) { |
| command = app_group::kChromeAppGroupIncognitoSearchCommand; |
| } else if ([completeURL.path isEqualToString:kWidgetKitActionVoiceSearch]) { |
| command = app_group::kChromeAppGroupVoiceSearchCommand; |
| } else if ([completeURL.path isEqualToString:kWidgetKitActionQRReader]) { |
| command = app_group::kChromeAppGroupQRScannerCommand; |
| } else if ([completeURL.path isEqualToString:kWidgetKitActionLens]) { |
| command = app_group::kChromeAppGroupLensCommand; |
| } else if ([completeURL.path isEqual:kWidgetKitActionOpenURL]) { |
| command = app_group::kChromeAppGroupOpenURLCommand; |
| std::string URLQueryParam; |
| if (net::GetValueForKeyInQuery(net::GURLWithNSURL(completeURL), "url", |
| &URLQueryParam)) { |
| externalText = base::SysUTF8ToNSString(URLQueryParam); |
| } |
| } else if ([completeURL.path isEqual:kWidgetKitActionSearchPasswords]) { |
| command = app_group::kChromeAppGroupSearchPasswordsCommand; |
| } else if ([completeURL.path isEqualToString:kWidgetKitActionGame]) { |
| if ([sourceWidget isEqualToString:kWidgetKitHostDinoGameWidget]) { |
| LogWidgetKitAction(WidgetKitExtensionAction::ACTION_DINO_WIDGET_GAME); |
| } else if ([sourceWidget |
| isEqualToString:kWidgetKitHostLockscreenLauncherWidget]) { |
| LogWidgetKitAction( |
| WidgetKitExtensionAction::ACTION_LOCKSCREEN_LAUNCHER_GAME); |
| } |
| |
| GURL URL( |
| base::StringPrintf("%s://%s", kChromeUIScheme, kChromeUIDinoHost)); |
| ChromeAppStartupParameters* appStartupParameters = |
| [[ChromeAppStartupParameters alloc] |
| initWithExternalURL:URL |
| declaredSourceApp:appID |
| secureSourceApp:sourceWidget |
| completeURL:completeURL |
| applicationMode:ApplicationModeForTabOpening::NORMAL |
| forceApplicationMode:forceApplicationMode]; |
| appStartupParameters.openedViaWidgetScheme = YES; |
| return appStartupParameters; |
| } |
| |
| NSString* commandString = base::SysUTF8ToNSString(command); |
| ChromeAppStartupParameters* appStartupParameters = |
| [self startupParametersForCommand:commandString |
| withExternalText:externalText |
| externalData:nil |
| index:0 |
| URL:nil |
| sourceApplication:appID |
| secureSourceApplication:sourceWidget |
| forceApplicationMode:forceApplicationMode]; |
| appStartupParameters.openedViaWidgetScheme = YES; |
| return appStartupParameters; |
| |
| } else if (IsXCallbackURL(parsedURL)) { |
| base::UmaHistogramEnumeration(kAppLaunchSource, |
| AppLaunchSource::X_CALLBACK); |
| // TODO(crbug.com/41004788): Temporary fix. |
| NSString* action = [completeURL path]; |
| // Currently only "open" and "app-group-command" are supported. |
| 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 |
| startupParametersForExtensionCommandWithURL:completeURL |
| sourceApplication:appID |
| forceApplicationMode:forceApplicationMode]; |
| } |
| |
| 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(parsedURL); |
| GURL URLQueryParam = GURL(parameters["url"]); |
| if (!URLQueryParam.is_valid() || |
| (!URLQueryParam.SchemeIs(url::kHttpScheme) && |
| !URLQueryParam.SchemeIs(url::kHttpsScheme))) { |
| return nil; |
| } |
| TabOpeningPostOpeningAction postOpeningAction = |
| XCallbackPoaToPostOpeningAction(parameters["poa"]); |
| |
| ChromeAppStartupParameters* startupParameters = |
| [[ChromeAppStartupParameters alloc] |
| initWithExternalURL:URLQueryParam |
| declaredSourceApp:appID |
| secureSourceApp:nil |
| completeURL:completeURL |
| applicationMode:ApplicationModeForTabOpening::UNDETERMINED |
| forceApplicationMode:forceApplicationMode]; |
| // postOpeningAction can only be NO_ACTION or SHOW_DEFAULT_BROWSER_SETTINGS |
| // (these are the only values returned by `XCallbackPoaToPostOpeningAction`) |
| // so this assignment should not DCHECK, no matter what the URL is. |
| startupParameters.postOpeningAction = postOpeningAction; |
| return startupParameters; |
| } else if ([self isChromeExternalActionURL:completeURL]) { |
| base::RecordAction( |
| base::UserMetricsAction("MobileExternalActionURLOpened")); |
| UMA_HISTOGRAM_ENUMERATION(kUMAMobileSessionStartActionHistogram, |
| START_EXTERNAL_ACTION, |
| MOBILE_SESSION_START_ACTION_COUNT); |
| base::UmaHistogramEnumeration(kAppLaunchSource, |
| AppLaunchSource::EXTERNAL_ACTION); |
| |
| return |
| [self startupParametersForExternalActionWithAppID:appID |
| completeURL:completeURL |
| forceApplicationMode:forceApplicationMode]; |
| } else if (parsedURL.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 = parsedURL.ExtractFileName(); |
| replacements.SetPathStr(filename); |
| replacements.SetSchemeStr(kChromeUIScheme); |
| replacements.SetHostStr(host); |
| GURL externalURL = parsedURL.ReplaceComponents(replacements); |
| if (!externalURL.is_valid()) { |
| return nil; |
| } |
| return [[ChromeAppStartupParameters alloc] |
| initWithExternalURL:externalURL |
| declaredSourceApp:appID |
| secureSourceApp:nil |
| completeURL:completeURL |
| applicationMode:ApplicationModeForTabOpening::NORMAL |
| forceApplicationMode:forceApplicationMode]; |
| } else { |
| GURL externalURL = parsedURL; |
| BOOL openedViaSpecificScheme = NO; |
| MobileSessionStartAction action = START_ACTION_OTHER; |
| if (parsedURL.SchemeIs(url::kHttpScheme)) { |
| action = START_ACTION_OPEN_HTTP_FROM_OS; |
| base::RecordAction( |
| base::UserMetricsAction("MobileDefaultBrowserViewIntent")); |
| } else if (parsedURL.SchemeIs(url::kHttpsScheme)) { |
| action = START_ACTION_OPEN_HTTPS_FROM_OS; |
| base::RecordAction( |
| base::UserMetricsAction("MobileDefaultBrowserViewIntent")); |
| } else { |
| // Replace the scheme with https or http depending on whether the input |
| // `url` scheme ends with an 's'. |
| BOOL useHttps = |
| parsedURL.scheme()[parsedURL.scheme().length() - 1] == 's'; |
| action = useHttps ? START_ACTION_OPEN_HTTPS : START_ACTION_OPEN_HTTP; |
| base::UmaHistogramEnumeration(kAppLaunchSource, |
| AppLaunchSource::LINK_OPENED_FROM_APP); |
| base::RecordAction(base::UserMetricsAction("MobileFirstPartyViewIntent")); |
| |
| GURL::Replacements replaceScheme; |
| if (useHttps) { |
| replaceScheme.SetSchemeStr(url::kHttpsScheme); |
| } else { |
| replaceScheme.SetSchemeStr(url::kHttpScheme); |
| } |
| externalURL = parsedURL.ReplaceComponents(replaceScheme); |
| openedViaSpecificScheme = YES; |
| } |
| UMA_HISTOGRAM_ENUMERATION(kUMAMobileSessionStartActionHistogram, action, |
| MOBILE_SESSION_START_ACTION_COUNT); |
| |
| if (action == START_ACTION_OPEN_HTTP_FROM_OS || |
| action == START_ACTION_OPEN_HTTPS_FROM_OS) { |
| base::UmaHistogramEnumeration(kAppLaunchSource, |
| AppLaunchSource::LINK_OPENED_FROM_OS); |
| LogOpenHTTPURLFromExternalURL(); |
| } |
| |
| if (!externalURL.is_valid()) { |
| return nil; |
| } |
| ChromeAppStartupParameters* params = [[ChromeAppStartupParameters alloc] |
| initWithExternalURL:externalURL |
| declaredSourceApp:appID |
| secureSourceApp:nil |
| completeURL:completeURL |
| applicationMode:mode |
| forceApplicationMode:forceApplicationMode]; |
| params.openedWithURL = YES; |
| params.openedViaFirstPartyScheme = |
| openedViaSpecificScheme && CallerAppIsFirstParty(params.callerApp); |
| return params; |
| } |
| } |
| |
| // Returns true if the URL passed is an external action URL, defined by having |
| // the `kExternalActionURLHost` as host. |
| + (BOOL)isChromeExternalActionURL:(NSURL*)URL { |
| return [URL.host isEqualToString:kExternalActionURLHost]; |
| } |
| |
| // Returns a new `ChromeAppStartupParameters` for a given `appID`, `completeURL` |
| // and `externalURL`. |
| + (ChromeAppStartupParameters*) |
| startupParametersForExternalActionWithAppID:(NSString*)appID |
| completeURL:(NSURL*)completeURL |
| externalURL:(const GURL&)externalURL |
| forceApplicationMode:(BOOL)forceApplicationMode { |
| return [[ChromeAppStartupParameters alloc] |
| initWithExternalURL:externalURL |
| declaredSourceApp:appID |
| secureSourceApp:nil |
| completeURL:completeURL |
| applicationMode:ApplicationModeForTabOpening::UNDETERMINED |
| forceApplicationMode:forceApplicationMode]; |
| } |
| |
| // Returns the correct startup parameters for a given external action passed as |
| // path to the external action "scheme". Returns nil (no-op) if the action is |
| // not recognized. |
| + (instancetype)startupParametersForExternalActionWithAppID:(NSString*)appID |
| completeURL:(NSURL*)completeURL |
| forceApplicationMode: |
| (BOOL)forceApplicationMode { |
| ChromeAppStartupParameters* params; |
| IOSExternalAction action; |
| NSString* path; |
| |
| // Separate the path into its components and ensure there is only one |
| // component after the first "/". |
| NSArray<NSString*>* pathComponents = completeURL.pathComponents; |
| if ([pathComponents count] == 2 && [pathComponents[0] isEqualToString:@"/"]) { |
| path = pathComponents[1]; |
| } |
| |
| if ([path isEqualToString:kExternalActionOpenNTP]) { |
| base::RecordAction( |
| base::UserMetricsAction("MobileExternalActionURLOpenedWithOpenNTP")); |
| action = IOSExternalAction::ACTION_OPEN_NTP; |
| params = [self |
| startupParametersForExternalActionWithAppID:appID |
| completeURL:completeURL |
| externalURL:GURL(kChromeUINewTabURL) |
| forceApplicationMode:forceApplicationMode]; |
| } else if ([path isEqualToString:kExternalActionDefaultBrowserSettings]) { |
| base::RecordAction(base::UserMetricsAction( |
| "MobileExternalActionURLOpenedWithDefaultBrowserSettings")); |
| |
| // If Chrome is already set as default browser, just open the NTP. |
| if (IsChromeLikelyDefaultBrowser()) { |
| action = |
| IOSExternalAction::ACTION_SKIPPED_DEFAULT_BROWSER_SETTINGS_FOR_NTP; |
| params = [self |
| startupParametersForExternalActionWithAppID:appID |
| completeURL:completeURL |
| externalURL:GURL(kChromeUINewTabURL) |
| forceApplicationMode:forceApplicationMode]; |
| } else { |
| action = IOSExternalAction::ACTION_DEFAULT_BROWSER_SETTINGS; |
| params = [self |
| startupParametersForExternalActionWithAppID:appID |
| completeURL:completeURL |
| externalURL:GURL() |
| forceApplicationMode:forceApplicationMode]; |
| params.postOpeningAction = EXTERNAL_ACTION_SHOW_BROWSER_SETTINGS; |
| } |
| } else { |
| action = IOSExternalAction::ACTION_INVALID; |
| params = nil; |
| } |
| |
| base::UmaHistogramEnumeration(kExternalActionHistogram, action); |
| params.openedViaFirstPartyScheme = CallerAppIsFirstParty(params.callerApp); |
| return params; |
| } |
| |
| + (instancetype)startupParametersForExtensionCommandWithURL:(NSURL*)URL |
| sourceApplication:(NSString*)appID |
| forceApplicationMode: |
| (BOOL)forceApplicationMode { |
| NSUserDefaults* sharedDefaults = app_group::GetGroupUserDefaults(); |
| |
| NSString* commandDictionaryPreference = |
| base::SysUTF8ToNSString(app_group::kChromeAppGroupCommandPreference); |
| NSDictionary* commandDictionary = base::apple::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::apple::ObjCCast<NSString>( |
| [commandDictionary objectForKey:commandCallerPreference]); |
| |
| NSString* commandPreference = base::SysUTF8ToNSString( |
| app_group::kChromeAppGroupCommandCommandPreference); |
| NSString* command = base::apple::ObjCCast<NSString>( |
| [commandDictionary objectForKey:commandPreference]); |
| |
| NSString* commandTimePreference = |
| base::SysUTF8ToNSString(app_group::kChromeAppGroupCommandTimePreference); |
| id commandTime = base::apple::ObjCCast<NSDate>( |
| [commandDictionary objectForKey:commandTimePreference]); |
| |
| NSString* commandTextPreference = |
| base::SysUTF8ToNSString(app_group::kChromeAppGroupCommandTextPreference); |
| NSString* externalText = base::apple::ObjCCast<NSString>( |
| [commandDictionary objectForKey:commandTextPreference]); |
| |
| NSString* commandDataPreference = |
| base::SysUTF8ToNSString(app_group::kChromeAppGroupCommandDataPreference); |
| NSData* externalData = base::apple::ObjCCast<NSData>( |
| [commandDictionary objectForKey:commandDataPreference]); |
| |
| NSString* commandIndexPreference = |
| base::SysUTF8ToNSString(app_group::kChromeAppGroupCommandIndexPreference); |
| NSNumber* index = base::apple::ObjCCast<NSNumber>( |
| [commandDictionary objectForKey:commandIndexPreference]); |
| |
| 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 |
| startupParametersForCommand:command |
| withExternalText:externalText |
| externalData:externalData |
| index:index |
| URL:URL |
| sourceApplication:appID |
| secureSourceApplication:commandCaller |
| forceApplicationMode:forceApplicationMode]; |
| } |
| |
| + (instancetype)startupParametersForCommand:(NSString*)command |
| withExternalText:(NSString*)externalText |
| externalData:(NSData*)externalData |
| index:(NSNumber*)index |
| URL:(NSURL*)URL |
| sourceApplication:(NSString*)appID |
| secureSourceApplication:(NSString*)secureAppID |
| forceApplicationMode:(BOOL)forceApplicationMode { |
| SearchExtensionAction action = ACTION_NO_ACTION; |
| ChromeAppStartupParameters* params = nil; |
| |
| if ([command |
| isEqualToString:base::SysUTF8ToNSString( |
| app_group::kChromeAppGroupVoiceSearchCommand)]) { |
| params = [[ChromeAppStartupParameters alloc] |
| initWithExternalURL:GURL(kChromeUINewTabURL) |
| declaredSourceApp:appID |
| secureSourceApp:secureAppID |
| completeURL:URL |
| applicationMode:ApplicationModeForTabOpening::NORMAL |
| forceApplicationMode:forceApplicationMode]; |
| [params setPostOpeningAction:START_VOICE_SEARCH]; |
| action = ACTION_NEW_VOICE_SEARCH; |
| } |
| |
| if ([command isEqualToString:base::SysUTF8ToNSString( |
| app_group::kChromeAppGroupNewTabCommand)]) { |
| params = [[ChromeAppStartupParameters alloc] |
| initWithExternalURL:GURL(kChromeUINewTabURL) |
| declaredSourceApp:appID |
| secureSourceApp:secureAppID |
| completeURL:URL |
| applicationMode:ApplicationModeForTabOpening::NORMAL |
| forceApplicationMode:forceApplicationMode]; |
| action = ACTION_NO_ACTION; |
| } |
| |
| if ([command |
| isEqualToString:base::SysUTF8ToNSString( |
| app_group::kChromeAppGroupFocusOmniboxCommand)]) { |
| params = [[ChromeAppStartupParameters alloc] |
| initWithExternalURL:GURL(kChromeUINewTabURL) |
| declaredSourceApp:appID |
| secureSourceApp:secureAppID |
| completeURL:URL |
| applicationMode:ApplicationModeForTabOpening::NORMAL |
| forceApplicationMode:forceApplicationMode]; |
| [params setPostOpeningAction:FOCUS_OMNIBOX]; |
| action = ACTION_NEW_SEARCH; |
| } |
| |
| if ([command isEqualToString:base::SysUTF8ToNSString( |
| app_group::kChromeAppGroupOpenURLCommand)]) { |
| if (!externalText) { |
| return nil; |
| } |
| GURL externalGURL(base::SysNSStringToUTF8(externalText)); |
| if (!externalGURL.is_valid() || !externalGURL.SchemeIsHTTPOrHTTPS()) { |
| return nil; |
| } |
| params = [[ChromeAppStartupParameters alloc] |
| initWithExternalURL:externalGURL |
| declaredSourceApp:appID |
| secureSourceApp:secureAppID |
| completeURL:URL |
| applicationMode:ApplicationModeForTabOpening::UNDETERMINED |
| forceApplicationMode:forceApplicationMode]; |
| action = ACTION_OPEN_URL; |
| } |
| |
| if ([command |
| isEqualToString:app_group::kChromeAppGroupOpenURLInIcognitoCommand]) { |
| if (!externalText) { |
| return nil; |
| } |
| GURL externalGURL(base::SysNSStringToUTF8(externalText)); |
| if (!externalGURL.is_valid() || !externalGURL.SchemeIsHTTPOrHTTPS()) { |
| return nil; |
| } |
| params = [[ChromeAppStartupParameters alloc] |
| initWithExternalURL:externalGURL |
| declaredSourceApp:appID |
| secureSourceApp:secureAppID |
| completeURL:URL |
| applicationMode:ApplicationModeForTabOpening::INCOGNITO |
| forceApplicationMode:forceApplicationMode]; |
| action = ACTION_OPEN_URL; |
| } |
| |
| if ([command |
| isEqualToString:base::SysUTF8ToNSString( |
| app_group::kChromeAppGroupSearchTextCommand)]) { |
| if (!externalText) { |
| return nil; |
| } |
| |
| params = [[ChromeAppStartupParameters alloc] |
| initWithExternalURL:GURL(kChromeUINewTabURL) |
| declaredSourceApp:appID |
| secureSourceApp:secureAppID |
| completeURL:URL |
| applicationMode:ApplicationModeForTabOpening::UNDETERMINED |
| forceApplicationMode:forceApplicationMode]; |
| |
| params.textQuery = externalText; |
| |
| action = ACTION_SEARCH_TEXT; |
| } |
| |
| if ([command isEqualToString:app_group:: |
| kChromeAppGroupIncognitoSearchTextCommand]) { |
| if (!externalText) { |
| return nil; |
| } |
| |
| params = [[ChromeAppStartupParameters alloc] |
| initWithExternalURL:GURL(kChromeUINewTabURL) |
| declaredSourceApp:appID |
| secureSourceApp:secureAppID |
| completeURL:URL |
| applicationMode:ApplicationModeForTabOpening::INCOGNITO |
| forceApplicationMode:forceApplicationMode]; |
| |
| params.textQuery = externalText; |
| |
| action = ACTION_SEARCH_TEXT; |
| } |
| |
| if ([command |
| isEqualToString:base::SysUTF8ToNSString( |
| app_group::kChromeAppGroupSearchImageCommand)]) { |
| if (!externalData) { |
| return nil; |
| } |
| |
| params = [[ChromeAppStartupParameters alloc] |
| initWithExternalURL:GURL(kChromeUINewTabURL) |
| declaredSourceApp:appID |
| secureSourceApp:secureAppID |
| completeURL:URL |
| applicationMode:ApplicationModeForTabOpening::UNDETERMINED |
| forceApplicationMode:forceApplicationMode]; |
| |
| params.imageSearchData = externalData; |
| |
| action = ACTION_SEARCH_IMAGE; |
| } |
| |
| if ([command isEqualToString: |
| app_group::kChromeAppGroupIncognitoSearchImageCommand]) { |
| if (!externalData) { |
| return nil; |
| } |
| |
| params = [[ChromeAppStartupParameters alloc] |
| initWithExternalURL:GURL(kChromeUINewTabURL) |
| declaredSourceApp:appID |
| secureSourceApp:secureAppID |
| completeURL:URL |
| applicationMode:ApplicationModeForTabOpening::INCOGNITO |
| forceApplicationMode:forceApplicationMode]; |
| |
| params.imageSearchData = externalData; |
| |
| action = ACTION_SEARCH_IMAGE; |
| } |
| |
| if ([command |
| isEqualToString:base::SysUTF8ToNSString( |
| app_group::kChromeAppGroupQRScannerCommand)]) { |
| params = [[ChromeAppStartupParameters alloc] |
| initWithExternalURL:GURL(kChromeUINewTabURL) |
| declaredSourceApp:appID |
| secureSourceApp:secureAppID |
| completeURL:URL |
| applicationMode:ApplicationModeForTabOpening::NORMAL |
| forceApplicationMode:forceApplicationMode]; |
| [params setPostOpeningAction:START_QR_CODE_SCANNER]; |
| |
| action = ACTION_NEW_QR_CODE_SEARCH; |
| } |
| |
| if ([command isEqualToString:base::SysUTF8ToNSString( |
| app_group::kChromeAppGroupLensCommand)]) { |
| params = [[ChromeAppStartupParameters alloc] |
| initWithExternalURL:GURL() |
| declaredSourceApp:appID |
| secureSourceApp:secureAppID |
| completeURL:URL |
| applicationMode:ApplicationModeForTabOpening::NORMAL |
| forceApplicationMode:forceApplicationMode]; |
| [params setPostOpeningAction:START_LENS_FROM_HOME_SCREEN_WIDGET]; |
| action = ACTION_LENS; |
| } |
| |
| if ([command isEqualToString: |
| base::SysUTF8ToNSString( |
| app_group::kChromeAppGroupIncognitoSearchCommand)]) { |
| params = [[ChromeAppStartupParameters alloc] |
| initWithExternalURL:GURL(kChromeUINewTabURL) |
| declaredSourceApp:appID |
| secureSourceApp:secureAppID |
| completeURL:URL |
| applicationMode:ApplicationModeForTabOpening::INCOGNITO |
| forceApplicationMode:forceApplicationMode]; |
| [params setPostOpeningAction:FOCUS_OMNIBOX]; |
| action = ACTION_NEW_INCOGNITO_SEARCH; |
| } |
| |
| if ([command isEqualToString: |
| base::SysUTF8ToNSString( |
| app_group::kChromeAppGroupSearchPasswordsCommand)]) { |
| params = [[ChromeAppStartupParameters alloc] |
| initWithExternalURL:GURL() |
| declaredSourceApp:appID |
| secureSourceApp:secureAppID |
| completeURL:URL |
| applicationMode:ApplicationModeForTabOpening::NORMAL |
| forceApplicationMode:forceApplicationMode]; |
| [params setPostOpeningAction:SEARCH_PASSWORDS]; |
| action = ACTION_NO_ACTION; |
| } |
| |
| if ([secureAppID |
| isEqualToString:app_group::kOpenCommandSourceSearchExtension]) { |
| UMA_HISTOGRAM_ENUMERATION(kSearchExtensionActionHistogram, action, |
| SEARCH_EXTENSION_ACTION_COUNT); |
| } |
| if ([secureAppID |
| isEqualToString:app_group::kOpenCommandSourceContentExtension] && |
| index) { |
| UMA_HISTOGRAM_COUNTS_100("IOS.ContentExtension.Index", |
| [index integerValue]); |
| } |
| if ([secureAppID isEqualToString:kWidgetKitHostSearchWidget]) { |
| LogWidgetKitAction(WidgetKitExtensionAction::ACTION_SEARCH_WIDGET_SEARCH); |
| } |
| if ([secureAppID isEqualToString:kWidgetKitHostQuickActionsWidget]) { |
| switch (action) { |
| case ACTION_NEW_VOICE_SEARCH: |
| LogWidgetKitAction( |
| WidgetKitExtensionAction::ACTION_QUICK_ACTIONS_VOICE_SEARCH); |
| break; |
| |
| case ACTION_NEW_SEARCH: |
| LogWidgetKitAction( |
| WidgetKitExtensionAction::ACTION_QUICK_ACTIONS_SEARCH); |
| break; |
| |
| case ACTION_NEW_QR_CODE_SEARCH: |
| LogWidgetKitAction( |
| WidgetKitExtensionAction::ACTION_QUICK_ACTIONS_QR_READER); |
| break; |
| |
| case ACTION_LENS: |
| LogWidgetKitAction(WidgetKitExtensionAction::ACTION_QUICK_ACTIONS_LENS); |
| break; |
| |
| case ACTION_NEW_INCOGNITO_SEARCH: |
| LogWidgetKitAction( |
| WidgetKitExtensionAction::ACTION_QUICK_ACTIONS_INCOGNITO); |
| break; |
| |
| default: |
| NOTREACHED(); |
| } |
| } |
| if ([secureAppID isEqualToString:kWidgetKitHostLockscreenLauncherWidget]) { |
| switch (action) { |
| case ACTION_NEW_SEARCH: |
| LogWidgetKitAction( |
| WidgetKitExtensionAction::ACTION_LOCKSCREEN_LAUNCHER_SEARCH); |
| break; |
| |
| case ACTION_NEW_INCOGNITO_SEARCH: |
| LogWidgetKitAction( |
| WidgetKitExtensionAction::ACTION_LOCKSCREEN_LAUNCHER_INCOGNITO); |
| break; |
| |
| case ACTION_NEW_VOICE_SEARCH: |
| LogWidgetKitAction( |
| WidgetKitExtensionAction::ACTION_LOCKSCREEN_LAUNCHER_VOICE_SEARCH); |
| break; |
| |
| default: |
| NOTREACHED(); |
| } |
| } |
| if ([secureAppID isEqualToString:kWidgetKitHostShortcutsWidget]) { |
| switch (action) { |
| case ACTION_NEW_SEARCH: |
| LogWidgetKitAction(WidgetKitExtensionAction::ACTION_SHORTCUTS_SEARCH); |
| break; |
| case ACTION_OPEN_URL: |
| LogWidgetKitAction(WidgetKitExtensionAction::ACTION_SHORTCUTS_OPEN); |
| break; |
| default: |
| NOTREACHED(); |
| } |
| } |
| if ([secureAppID isEqualToString:kWidgetKitHostSearchPasswordsWidget]) { |
| LogWidgetKitAction(WidgetKitExtensionAction:: |
| ACTION_SEARCH_PASSWORDS_WIDGET_SEARCH_PASSWORDS); |
| UMA_HISTOGRAM_ENUMERATION( |
| "PasswordManager.ManagePasswordsReferrer", |
| password_manager::ManagePasswordsReferrer::kSearchPasswordsWidget); |
| base::RecordAction(base::UserMetricsAction( |
| "MobileSearchPasswordsWidgetOpenPasswordManager")); |
| } |
| return params; |
| } |
| |
| - (MobileSessionCallerApp)callerApp { |
| if ([_secureSourceApp |
| isEqualToString:app_group::kOpenCommandSourceTodayExtension]) { |
| return CALLER_APP_GOOGLE_CHROME_TODAY_EXTENSION; |
| } |
| if ([_secureSourceApp |
| isEqualToString:app_group::kOpenCommandSourceSearchExtension]) { |
| return CALLER_APP_GOOGLE_CHROME_SEARCH_EXTENSION; |
| } |
| if ([_secureSourceApp |
| isEqualToString:app_group::kOpenCommandSourceContentExtension]) { |
| return CALLER_APP_GOOGLE_CHROME_CONTENT_EXTENSION; |
| } |
| if ([_secureSourceApp |
| isEqualToString:app_group::kOpenCommandSourceShareExtension]) { |
| return CALLER_APP_GOOGLE_CHROME_SHARE_EXTENSION; |
| } |
| if ([_secureSourceApp |
| isEqualToString:app_group::kOpenCommandSourceOpenExtension]) { |
| return CALLER_APP_GOOGLE_CHROME_OPEN_EXTENSION; |
| } |
| |
| if (![_declaredSourceApp length]) { |
| if (self.completeURL.SchemeIs(url::kHttpScheme) || |
| self.completeURL.SchemeIs(url::kHttpsScheme)) { |
| // If Chrome is opened via the system default browser mechanism, the |
| // action should be differentiated from the case where the source is |
| // unknown. |
| return CALLER_APP_THIRD_PARTY; |
| } |
| return CALLER_APP_NOT_AVAILABLE; |
| } |
| |
| if ([_declaredSourceApp |
| isEqualToString:[base::apple::FrameworkBundle() bundleIdentifier]]) { |
| return CALLER_APP_GOOGLE_CHROME; |
| } |
| 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 = base::SysUTF8ToNSString(self.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 |