chromium/ios/chrome/app/startup/chrome_app_startup_parameters.mm

// 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/app/startup/app_launch_metrics.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 {

// Key of the UMA Startup.MobileSessionStartAction histogram.
const char kUMAMobileSessionStartActionHistogram[] =
    "Startup.MobileSessionStartAction";

const char kApplicationGroupCommandDelay[] =
    "Startup.ApplicationGroupCommandDelay";

// UMA histogram key for IOS.ExternalAction.
const char kExternalActionHistogram[] = "IOS.ExternalAction";

// 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_constants.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;

// Values of the UMA Startup.MobileSessionStartAction histogram.
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
// LINT.IfChange
enum MobileSessionStartAction {
  // Logged when an application passes an http URL to Chrome using the custom
  // registered scheme (f.e. googlechrome).
  START_ACTION_OPEN_HTTP = 0,
  // Logged when an application passes an https URL to Chrome using the custom
  // registered scheme (f.e. googlechromes).
  START_ACTION_OPEN_HTTPS = 1,
  START_ACTION_OPEN_FILE = 2,
  START_ACTION_XCALLBACK_OPEN = 3,
  START_ACTION_XCALLBACK_OTHER = 4,
  START_ACTION_OTHER = 5,
  START_ACTION_XCALLBACK_APPGROUP_COMMAND = 6,
  // Logged when any application passes an http URL to Chrome using the standard
  // "http" scheme. This can happen when Chrome is set as the default browser
  // on iOS 14+ as http openURL calls will be directed to Chrome by the system
  // from all other apps.
  START_ACTION_OPEN_HTTP_FROM_OS = 7,
  // Logged when any application passes an https URL to Chrome using the
  // standard "https" scheme. This can happen when Chrome is set as the default
  // browser on iOS 14+ as http openURL calls will be directed to Chrome by the
  // system from all other apps.
  START_ACTION_OPEN_HTTPS_FROM_OS = 8,
  START_ACTION_WIDGET_KIT_COMMAND = 9,
  // Logged when Chrome is opened via the external action scheme.
  START_EXTERNAL_ACTION = 10,
  MOBILE_SESSION_START_ACTION_COUNT
};
// LINT.ThenChange(/tools/metrics/histograms/metadata/ios/enums.xml)

// Values of the UMA iOS.SearchExtension.Action histogram.
// LINT.IfChange
enum SearchExtensionAction {
  ACTION_NO_ACTION,
  ACTION_NEW_SEARCH,
  ACTION_NEW_INCOGNITO_SEARCH,
  ACTION_NEW_VOICE_SEARCH,
  ACTION_NEW_QR_CODE_SEARCH,
  ACTION_OPEN_URL,
  ACTION_SEARCH_TEXT,
  ACTION_SEARCH_IMAGE,
  ACTION_LENS,
  SEARCH_EXTENSION_ACTION_COUNT,
};
// LINT.ThenChange(/tools/metrics/histograms/metadata/ios/enums.xml)

// Values of the UMA IOS.WidgetKit.Action histogram.
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
// LINT.IfChange
enum class WidgetKitExtensionAction {
  ACTION_DINO_WIDGET_GAME = 0,
  ACTION_SEARCH_WIDGET_SEARCH = 1,
  ACTION_QUICK_ACTIONS_SEARCH = 2,
  ACTION_QUICK_ACTIONS_INCOGNITO = 3,
  ACTION_QUICK_ACTIONS_VOICE_SEARCH = 4,
  ACTION_QUICK_ACTIONS_QR_READER = 5,
  ACTION_LOCKSCREEN_LAUNCHER_SEARCH = 6,
  ACTION_LOCKSCREEN_LAUNCHER_INCOGNITO = 7,
  ACTION_LOCKSCREEN_LAUNCHER_VOICE_SEARCH = 8,
  ACTION_LOCKSCREEN_LAUNCHER_GAME = 9,
  ACTION_QUICK_ACTIONS_LENS = 10,
  ACTION_SHORTCUTS_SEARCH = 11,
  ACTION_SHORTCUTS_OPEN = 12,
  ACTION_SEARCH_PASSWORDS_WIDGET_SEARCH_PASSWORDS = 13,
  kMaxValue = ACTION_SEARCH_PASSWORDS_WIDGET_SEARCH_PASSWORDS,
};
// LINT.ThenChange(/tools/metrics/histograms/metadata/ios/enums.xml)

// Values of the UMA IOS.ExternalAction histogram.
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
// LINT.IfChange
enum class IOSExternalAction {
  // Logged when Chrome is passed an invalid action.
  ACTION_INVALID = 0,
  // Logged when Chrome is passed a "OpenNTP" action.
  ACTION_OPEN_NTP = 1,
  // Logged when Chrome is passed a "DefaultBrowserSettings" action.
  ACTION_DEFAULT_BROWSER_SETTINGS = 2,
  // Logged when Chrome is passed a "DefaultBrowserSettings" action, but instead
  // will show the NTP, since Chrome is already set as default browser.
  ACTION_SKIPPED_DEFAULT_BROWSER_SETTINGS_FOR_NTP = 3,
  kMaxValue = ACTION_SKIPPED_DEFAULT_BROWSER_SETTINGS_FOR_NTP,
};
// LINT.ThenChange(/tools/metrics/histograms/metadata/ios/enums.xml)

// Histogram helper to log the UMA IOS.WidgetKit.Action histogram.
void LogWidgetKitAction(WidgetKitExtensionAction action) {
  UmaHistogramEnumeration("IOS.WidgetKit.Action", 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 {
  self = [super initWithExternalURL:externalURL
                        completeURL:net::GURLWithNSURL(completeURL)
                    applicationMode:mode];
  if (self) {
    _declaredSourceApp = [declaredSourceApp copy];
    _secureSourceApp = [secureSourceApp copy];
  }
  return self;
}

+ (instancetype)startupParametersWithURL:(NSURL*)completeURL
                       sourceApplication:(NSString*)appID {
  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];
      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];
    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 "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
          startupParametersForExtensionCommandWithURL:completeURL
                                    sourceApplication: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(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];
    // 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];
  } 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];
  } 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:ApplicationModeForTabOpening::UNDETERMINED];
    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 {
  return [[ChromeAppStartupParameters alloc]
      initWithExternalURL:externalURL
        declaredSourceApp:appID
          secureSourceApp:nil
              completeURL:completeURL
          applicationMode:ApplicationModeForTabOpening::UNDETERMINED];
}

// 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 {
  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)];
  } 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)];
    } else {
      action = IOSExternalAction::ACTION_DEFAULT_BROWSER_SETTINGS;
      params = [self startupParametersForExternalActionWithAppID:appID
                                                     completeURL:completeURL
                                                     externalURL:GURL()];
      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 {
  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];
}

+ (instancetype)startupParametersForCommand:(NSString*)command
                           withExternalText:(NSString*)externalText
                               externalData:(NSData*)externalData
                                      index:(NSNumber*)index
                                        URL:(NSURL*)URL
                          sourceApplication:(NSString*)appID
                    secureSourceApplication:(NSString*)secureAppID {
  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];
    [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];
    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];
    [params setPostOpeningAction:FOCUS_OMNIBOX];
    action = ACTION_NEW_SEARCH;
  }

  if ([command isEqualToString:base::SysUTF8ToNSString(
                                   app_group::kChromeAppGroupOpenURLCommand)]) {
    if (!externalText || ![externalText isKindOfClass:[NSString class]])
      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];
    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];

    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];

    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];
    [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];
    [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];
    [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];
    [params setPostOpeningAction:SEARCH_PASSWORDS];
    action = ACTION_NO_ACTION;
  }

  if ([secureAppID
          isEqualToString:app_group::kOpenCommandSourceSearchExtension]) {
    UMA_HISTOGRAM_ENUMERATION("IOS.SearchExtension.Action", 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_IN_MIGRATION();
        break;
    }
  }
  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_IN_MIGRATION();
        break;
    }
  }
  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_IN_MIGRATION();
        break;
    }
  }
  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