// Copyright 2016 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/main_application_delegate.h"
#import <UserNotifications/UserNotifications.h>
#import "base/apple/foundation_util.h"
#import "base/feature_list.h"
#import "base/ios/ios_util.h"
#import "base/metrics/histogram_functions.h"
#import "base/metrics/user_metrics.h"
#import "base/strings/sys_string_conversions.h"
#import "components/download/public/background_service/background_download_service.h"
#import "components/search_engines/prepopulated_engines.h"
#import "components/search_engines/template_url.h"
#import "components/search_engines/template_url_prepopulate_data.h"
#import "components/search_engines/template_url_service.h"
#import "components/send_tab_to_self/features.h"
#import "components/signin/public/identity_manager/identity_manager.h"
#import "ios/chrome/app/application_delegate/app_state.h"
#import "ios/chrome/app/application_delegate/memory_warning_helper.h"
#import "ios/chrome/app/application_delegate/metrics_mediator.h"
#import "ios/chrome/app/application_delegate/startup_information.h"
#import "ios/chrome/app/application_delegate/url_opener.h"
#import "ios/chrome/app/application_delegate/url_opener_params.h"
#import "ios/chrome/app/chrome_overlay_window.h"
#import "ios/chrome/app/main_application_delegate_testing.h"
#import "ios/chrome/app/main_controller.h"
#import "ios/chrome/app/startup/app_launch_metrics.h"
#import "ios/chrome/browser/commerce/model/push_notification/push_notification_feature.h"
#import "ios/chrome/browser/content_notification/model/content_notification_util.h"
#import "ios/chrome/browser/crash_report/model/crash_keys_helper.h"
#import "ios/chrome/browser/download/model/background_service/background_download_service_factory.h"
#import "ios/chrome/browser/keyboard/ui_bundled/menu_builder.h"
#import "ios/chrome/browser/push_notification/model/push_notification_delegate.h"
#import "ios/chrome/browser/push_notification/model/push_notification_util.h"
#import "ios/chrome/browser/search_engines/model/template_url_service_factory.h"
#import "ios/chrome/browser/shared/coordinator/scene/scene_controller.h"
#import "ios/chrome/browser/shared/coordinator/scene/scene_delegate.h"
#import "ios/chrome/browser/shared/coordinator/scene/scene_state.h"
#import "ios/chrome/browser/shared/model/browser/browser.h"
#import "ios/chrome/browser/shared/model/browser/browser_provider.h"
#import "ios/chrome/browser/shared/model/browser/browser_provider_interface.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/signin/model/identity_manager_factory.h"
#import "ios/web/common/uikit_ui_util.h"
#import "ios/web/public/thread/web_task_traits.h"
#import "ios/web/public/thread/web_thread.h"
namespace {
// The time delay after firstSceneWillEnterForeground: before checking for main
// intent signals.
constexpr base::TimeDelta kMainIntentCheckDelay = base::Seconds(1);
} // namespace
@interface MainApplicationDelegate () <AppStateObserver> {
MainController* _mainController;
// Memory helper used to log the number of memory warnings received.
MemoryWarningHelper* _memoryHelper;
// Metrics mediator used to check and update the metrics accordingly to the
// user preferences.
MetricsMediator* _metricsMediator;
// Container for startup information.
id<StartupInformation> _startupInformation;
// The set of "scene sessions" that needs to be discarded. See
// -application:didDiscardSceneSessions: for details.
NSSet<UISceneSession*>* _sceneSessionsToDiscard;
}
// YES if application:didFinishLaunchingWithOptions: was called. Used to
// determine whether or not shutdown should be invoked from
// applicationWillTerminate:.
@property(nonatomic, assign) BOOL didFinishLaunching;
// Delegate that handles delivered push notification workflow.
@property(nonatomic, strong) PushNotificationDelegate* pushNotificationDelegate;
@end
@implementation MainApplicationDelegate
- (instancetype)init {
if ((self = [super init])) {
_memoryHelper = [[MemoryWarningHelper alloc] init];
_mainController = [[MainController alloc] init];
_metricsMediator = [[MetricsMediator alloc] init];
[_mainController setMetricsMediator:_metricsMediator];
_startupInformation = _mainController;
_appState =
[[AppState alloc] initWithStartupInformation:_startupInformation];
_pushNotificationDelegate =
[[PushNotificationDelegate alloc] initWithAppState:_appState];
[_mainController setAppState:_appState];
}
return self;
}
#pragma mark - UIApplicationDelegate methods -
#pragma mark Responding to App State Changes and System Events
// Called by the OS to create the UI for display. The UI will not be displayed,
// even if it is ready, until this function returns.
// The absolute minimum work should be done here, to ensure that the application
// startup is fast, and the UI appears as soon as possible.
- (BOOL)application:(UIApplication*)application
didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
self.didFinishLaunching = YES;
UNUserNotificationCenter* center =
[UNUserNotificationCenter currentNotificationCenter];
center.delegate = _pushNotificationDelegate;
_appState.startupInformation.didFinishLaunchingTime = base::TimeTicks::Now();
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
[defaults
setInteger:[defaults integerForKey:
metrics_mediator::
kAppDidFinishLaunchingConsecutiveCallsKey] +
1
forKey:metrics_mediator::kAppDidFinishLaunchingConsecutiveCallsKey];
[_appState startInitialization];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(sceneWillConnect:)
name:UISceneWillConnectNotification
object:nil];
// UIApplicationWillResignActiveNotification is delivered before the last
// scene has entered the background.
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(lastSceneWillEnterBackground:)
name:UIApplicationWillResignActiveNotification
object:nil];
// UIApplicationDidEnterBackgroundNotification is delivered after the last
// scene has entered the background.
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(lastSceneDidEnterBackground:)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
// UIApplicationWillEnterForegroundNotification will be delivered right
// after the first scene sends UISceneWillEnterForegroundNotification.
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(firstSceneWillEnterForeground:)
name:UIApplicationWillEnterForegroundNotification
object:nil];
return YES;
}
- (void)applicationWillTerminate:(UIApplication*)application {
// Any report captured from this point on should be noted as after terminate.
crash_keys::SetCrashedAfterAppWillTerminate();
// If `self.didFinishLaunching` is NO, that indicates that the app was
// terminated before startup could be run. In this situation, skip running
// shutdown, since the app was never fully started.
if (!self.didFinishLaunching)
return;
if (_appState.initStage <= InitStageSafeMode)
return;
// Instead of adding code here, consider if it could be handled by listening
// for UIApplicationWillterminate.
[_appState applicationWillTerminate:application];
}
- (void)applicationDidReceiveMemoryWarning:(UIApplication*)application {
if (_appState.initStage <= InitStageSafeMode)
return;
[_memoryHelper handleMemoryPressure];
}
- (void)application:(UIApplication*)application
didDiscardSceneSessions:(NSSet<UISceneSession*>*)sceneSessions {
// This method is invoked by iOS to inform the application that the sessions
// for "closed windows" are garbage collected and that any data associated
// with them by the application needs to be deleted.
//
// The documentation says that if the application is not running when the OS
// decides to discard the sessions, then it will call this method the next
// time the application starts up. As seen by crbug.com/1292641, this call
// happens before -[UIApplicationDelegate sceneWillConnect:] which means
// that it can happen before Chrome has properly initialized. In that case,
// record the list of sessions to discard and clean them once Chrome is
// initialized.
if (_appState.initStage <= InitStageBrowserObjectsForBackgroundHandlers) {
_sceneSessionsToDiscard = [sceneSessions copy];
[_appState addObserver:self];
return;
}
[_appState application:application didDiscardSceneSessions:sceneSessions];
}
- (UIInterfaceOrientationMask)application:(UIApplication*)application
supportedInterfaceOrientationsForWindow:(UIWindow*)window {
if (_appState.portraitOnly) {
return UIInterfaceOrientationMaskPortrait;
}
// Apply a no-op mask by default.
return UIInterfaceOrientationMaskAll;
}
- (void)application:(UIApplication*)application
didReceiveRemoteNotification:(NSDictionary*)userInfo
fetchCompletionHandler:
(void (^)(UIBackgroundFetchResult result))completionHandler {
// This method is invoked by iOS to process an incoming remote push
// notification for the application and fetch any additional data.
// According to the documentation, iOS invokes this function whether the
// application is in the foreground or background. In addition, iOS will
// launch the application and place it in background mode to invoke this
// function. However, iOS will not do this if the user has force-quit the
// application. In that case, the user must relaunch the application or must
// restart the device before the system will launch the application and invoke
// this function.
UIBackgroundFetchResult result = [self.pushNotificationDelegate
applicationWillProcessIncomingRemoteNotification:userInfo];
if (completionHandler) {
completionHandler(result);
}
}
- (void)application:(UIApplication*)application
didRegisterForRemoteNotificationsWithDeviceToken:(NSData*)deviceToken {
// In rare cases, for example when a user obtains a new device and restores it
// from a previous backup, iOS invokes the [application
// didRegisterForRemoteNotificationsWithDeviceToken:] function potentially
// before Chrome threads have been initialized. In this case, iOS'
// invocation is ignored and the device is registered for push notifications
// through the normal startup process.
if (!web::WebThread::IsThreadInitialized(web::WebThread::UI)) {
return;
}
// This method is invoked by iOS on the successful registration of the app to
// APNS and retrieval of the device's APNS token.
base::UmaHistogramBoolean("IOS.PushNotification.APNSDeviceRegistration",
true);
web::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce(^{
if ([self isContentNotificationAvailable] ||
base::FeatureList::IsEnabled(
send_tab_to_self::kSendTabToSelfIOSPushNotifications)) {
// TODO(crbug.com/341906612) Remove use of
// browserProviderInterfaceDoNotUse.
Browser* browser =
self.mainController.browserProviderInterfaceDoNotUse
.mainBrowserProvider.browser;
[self.pushNotificationDelegate
applicationDidRegisterWithAPNS:deviceToken
browserState:browser->GetBrowserState()];
// Logs when a Registration succeeded with a loaded BrowserState.
base::UmaHistogramBoolean(
"ContentNotifications.Registration.BrowserStateUnavailable",
false);
} else {
[self.pushNotificationDelegate
applicationDidRegisterWithAPNS:deviceToken
browserState:nil];
}
}));
}
- (void)application:(UIApplication*)application
didFailToRegisterForRemoteNotificationsWithError:(NSError*)error {
// This method is invoked by iOS to inform the application that the attempt to
// obtain the device's APNS token from APNS failed
base::UmaHistogramBoolean("IOS.PushNotification.APNSDeviceRegistration",
false);
}
- (void)application:(UIApplication*)application
handleEventsForBackgroundURLSession:(NSString*)identifier
completionHandler:(void (^)())completionHandler {
if (![identifier
hasPrefix:base::SysUTF8ToNSString(
download::kBackgroundDownloadIdentifierPrefix)]) {
completionHandler();
return;
}
// TODO(crbug.com/325613461) Remove this Browser dependency, ideally by
// refactoring into a dedicated agent.
Browser* browser = _mainController.browserProviderInterfaceDoNotUse
.mainBrowserProvider.browser;
if (!browser) {
// TODO(crbug.com/40240359): We should store the completionHandler and wait
// for mainBrowserProvider creation.
completionHandler();
return;
}
// TODO(crbug.com/325613461): Associate downloads with a specific file path to
// determine which browser state / download service to use here.
download::BackgroundDownloadService* download_service =
BackgroundDownloadServiceFactory::GetForBrowserState(
browser->GetBrowserState());
if (download_service) {
download_service->HandleEventsForBackgroundURLSession(
base::BindOnce(completionHandler));
return;
}
completionHandler();
}
#pragma mark - Scenes lifecycle
- (NSInteger)foregroundSceneCount {
NSInteger foregroundSceneCount = 0;
for (UIScene* scene in UIApplication.sharedApplication.connectedScenes) {
if ((scene.activationState == UISceneActivationStateForegroundInactive) ||
(scene.activationState == UISceneActivationStateForegroundActive)) {
foregroundSceneCount++;
}
}
return foregroundSceneCount;
}
- (void)sceneWillConnect:(NSNotification*)notification {
UIWindowScene* scene =
base::apple::ObjCCastStrict<UIWindowScene>(notification.object);
SceneDelegate* sceneDelegate =
base::apple::ObjCCastStrict<SceneDelegate>(scene.delegate);
// Under some iOS 15 betas, Chrome gets scene connection events for some
// system scene connections. To handle this, early return if the connecting
// scene doesn't have a valid delegate. (See crbug.com/1217461)
if (!sceneDelegate)
return;
// TODO(crbug.com/40679152): This should be called later, or this flow should
// be changed completely.
if (self.foregroundSceneCount == 0) {
[_appState applicationWillEnterForeground:UIApplication.sharedApplication
metricsMediator:_metricsMediator
memoryHelper:_memoryHelper];
}
}
- (void)lastSceneWillEnterBackground:(NSNotification*)notification {
if (_appState.initStage <= InitStageSafeMode)
return;
[_appState willResignActive];
}
- (void)lastSceneDidEnterBackground:(NSNotification*)notification {
// Reset `startupHadExternalIntent` for all scenes in case external intents
// were triggered while the application was in the foreground.
for (SceneState* scene in self.appState.connectedScenes) {
if (scene.startupHadExternalIntent) {
scene.startupHadExternalIntent = NO;
}
}
[_appState applicationDidEnterBackground:UIApplication.sharedApplication
memoryHelper:_memoryHelper];
}
- (void)firstSceneWillEnterForeground:(NSNotification*)notification {
// This method may be invoked really early in the application lifetime
// even before the creation of the main loop. Thus it is not possible
// to use PostTask API here, and we have to use dispatch_async(...).
__weak MainApplicationDelegate* weakSelf = self;
dispatch_after(
dispatch_time(DISPATCH_TIME_NOW, kMainIntentCheckDelay.InNanoseconds()),
dispatch_get_main_queue(), ^{
[weakSelf firstSceneDidEnterForeground];
});
// Register if it's a cold start or when bringing Chrome to foreground with
// Content Push Notifications available.
if (_startupInformation.isColdStart ||
[self isContentNotificationAvailable]) {
[PushNotificationUtil
registerDeviceWithAPNSWithContentNotificationsAvailable:
[self isContentNotificationAvailable]];
}
[_appState applicationWillEnterForeground:UIApplication.sharedApplication
metricsMediator:_metricsMediator
memoryHelper:_memoryHelper];
}
#pragma mark - AppStateObserver methods
- (void)appState:(AppState*)appState
didTransitionFromInitStage:(InitStage)previousInitStage {
DCHECK_EQ(_appState, appState);
// The app transitioned to InitStageBrowserObjectsForBackgroundHandlers
// or past that stage.
if (_appState.initStage >= InitStageBrowserObjectsForBackgroundHandlers) {
DCHECK(_sceneSessionsToDiscard);
[_appState removeObserver:self];
[_appState application:[UIApplication sharedApplication]
didDiscardSceneSessions:_sceneSessionsToDiscard];
_sceneSessionsToDiscard = nil;
}
}
#pragma mark - UIResponder methods
- (void)buildMenuWithBuilder:(id<UIMenuBuilder>)builder {
[super buildMenuWithBuilder:builder];
[MenuBuilder buildMainMenuWithBuilder:builder];
}
#pragma mark - Testing methods
+ (AppState*)sharedAppState {
return base::apple::ObjCCast<MainApplicationDelegate>(
[[UIApplication sharedApplication] delegate])
.appState;
}
+ (MainController*)sharedMainController {
return base::apple::ObjCCast<MainApplicationDelegate>(
[[UIApplication sharedApplication] delegate])
.mainController;
}
- (MainController*)mainController {
return _mainController;
}
#pragma mark - Private
// Returns whether the application was started via an external intent or
// directly (i.e. by tapping on the app button directly).
- (BOOL)appStartupFromExternalIntent {
for (SceneState* scene in self.appState.connectedScenes) {
if (scene.startupHadExternalIntent) {
return YES;
}
}
return NO;
}
// Invoked on the main sequence after -firstSceneWillEnterForeground: is
// called, after a small delay. The delay is there to give time for the
// intents to be received by the application (as they are not guaranteed
// to happen before -firstSceneWillEnterForeground:).
- (void)firstSceneDidEnterForeground {
if ([self appStartupFromExternalIntent]) {
base::RecordAction(base::UserMetricsAction("IOSOpenByViewIntent"));
} else {
base::RecordAction(base::UserMetricsAction("IOSOpenByMainIntent"));
base::UmaHistogramEnumeration(kAppLaunchSource, AppLaunchSource::APP_ICON);
}
}
// `YES` if Content notification is enabled or registered. Called before
// register device With APNS.
- (BOOL)isContentNotificationAvailable {
// TODO(crbug.com/341903881) Do not use
// mainController.browserProviderInterfaceDoNotUse.
Browser* browser = _mainController.browserProviderInterfaceDoNotUse
.mainBrowserProvider.browser;
if (!browser) {
base::UmaHistogramBoolean(
"ContentNotifications.Registration.BrowserStateUnavailable", true);
return NO;
}
ChromeBrowserState* browserState = browser->GetBrowserState();
return IsContentNotificationEnabled(browserState) ||
IsContentNotificationRegistered(browserState);
}
@end