[go: nahoru, domu]

blob: 50efc85dc2cf23ed334afe3231207fc043c74ac5 [file] [log] [blame]
// 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/application_delegate/app_state.h"
#import <utility>
#import "base/apple/foundation_util.h"
#import "base/critical_closure.h"
#import "base/functional/bind.h"
#import "base/functional/callback.h"
#import "base/ios/crb_protocol_observers.h"
#import "base/ios/ios_util.h"
#import "base/metrics/histogram_functions.h"
#import "base/metrics/histogram_macros.h"
#import "base/notreached.h"
#import "base/task/bind_post_task.h"
#import "components/feature_engagement/public/event_constants.h"
#import "components/feature_engagement/public/tracker.h"
#import "components/metrics/metrics_service.h"
#import "components/previous_session_info/previous_session_info.h"
#import "ios/chrome/app/application_delegate/app_state+private.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/user_activity_handler.h"
#import "ios/chrome/app/deferred_initialization_runner.h"
#import "ios/chrome/browser/browsing_data/sessions_storage_util.h"
#import "ios/chrome/browser/crash_report/crash_helper.h"
#import "ios/chrome/browser/crash_report/crash_keys_helper.h"
#import "ios/chrome/browser/crash_report/crash_loop_detection_util.h"
#import "ios/chrome/browser/crash_report/features.h"
#import "ios/chrome/browser/device_sharing/device_sharing_manager.h"
#import "ios/chrome/browser/feature_engagement/tracker_factory.h"
#import "ios/chrome/browser/shared/coordinator/scene/scene_delegate.h"
#import "ios/chrome/browser/shared/model/application_context/application_context.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/browser_state/chrome_browser_state.h"
#import "ios/chrome/browser/shared/public/commands/application_commands.h"
#import "ios/chrome/browser/shared/public/commands/command_dispatcher.h"
#import "ios/chrome/browser/shared/public/commands/help_commands.h"
#import "ios/chrome/browser/shared/public/commands/open_new_tab_command.h"
#import "ios/chrome/browser/signin/authentication_service.h"
#import "ios/chrome/browser/signin/authentication_service_factory.h"
#import "ios/chrome/browser/signin/system_identity_manager.h"
#import "ios/chrome/browser/ui/content_suggestions/content_suggestions_feature.h"
#import "ios/chrome/browser/web_state_list/session_metrics.h"
#import "ios/chrome/browser/web_state_list/web_state_list_metrics_browser_agent.h"
#import "ios/net/cookies/cookie_store_ios.h"
#import "ios/public/provider/chrome/browser/app_distribution/app_distribution_api.h"
#import "ios/public/provider/chrome/browser/user_feedback/user_feedback_api.h"
#import "ios/web/public/thread/web_task_traits.h"
#import "ios/web/public/thread/web_thread.h"
#import "net/url_request/url_request_context.h"
#import "net/url_request/url_request_context_getter.h"
#import "ui/base/device_form_factor.h"
namespace {
NSString* const kStartupAttemptReset = @"StartupAttemptReset";
// Flushes the CookieStore on the IO thread and invoke `closure` upon
// completion. The sequence where `closure` is invoked is unspecified.
void FlushCookieStoreOnIOThread(
scoped_refptr<net::URLRequestContextGetter> getter,
base::OnceClosure closure) {
DCHECK_CURRENTLY_ON(web::WebThread::IO);
getter->GetURLRequestContext()->cookie_store()->FlushStore(
std::move(closure));
}
} // namespace
#pragma mark - AppStateObserverList
@interface AppStateObserverList : CRBProtocolObservers <AppStateObserver>
@end
@implementation AppStateObserverList
@end
#pragma mark - AppState
@interface AppState () <AppStateObserver>
// Container for observers.
@property(nonatomic, strong) AppStateObserverList* observers;
// YES if cookies are currently being flushed to disk. Declared as a property
// to allow modifying it in a block via a __weak pointer without checking if
// the pointer is nil or not.
@property(nonatomic, assign) BOOL savingCookies;
// This method is the first to be called when user launches the application.
// This performs the minimal amount of browser initialization that is needed by
// safe mode.
// Depending on the background tasks history, the state of the application is
// INITIALIZATION_STAGE_BACKGROUND so this
// step cannot be included in the `startUpBrowserToStage:` method.
- (void)initializeUIPreSafeMode;
// Complete the browser initialization for a regular startup.
- (void)completeUIInitialization;
// Saves the current launch details to user defaults.
- (void)saveLaunchDetailsToDefaults;
// This flag is set when the first scene has activated since the startup, and
// never reset.
@property(nonatomic, assign) BOOL firstSceneHasActivated;
// Redefined as readwrite.
@property(nonatomic, assign) BOOL firstSceneHasInitializedUI;
// The current blocker target if any.
@property(nonatomic, weak, readwrite) id<UIBlockerTarget> uiBlockerTarget;
// The counter of currently shown blocking UIs. Do not use this directly,
// instead use incrementBlockingUICounterForScene: and
// incrementBlockingUICounterForScene or the ScopedUIBlocker.
@property(nonatomic, assign) NSUInteger blockingUICounter;
// Agents attached to this app state.
@property(nonatomic, strong) NSMutableArray<id<AppStateAgent>>* agents;
// A flag that tracks if the init stage is currently being incremented. Used to
// prevent reentrant calls to queueTransitionToNextInitStage originating from
// stage change notifications.
@property(nonatomic, assign) BOOL isIncrementingInitStage;
// A flag that tracks if another increment of init stage needs to happen after
// this one is complete. Will be set if queueTransitionToNextInitStage is called
// while queueTransitionToNextInitStage is already on the call stack.
@property(nonatomic, assign) BOOL needsIncrementInitStage;
@end
@implementation AppState {
// Whether the application is currently in the background.
// This is a workaround for rdar://22392526 where
// -applicationDidEnterBackground: can be called twice.
// TODO(crbug.com/546196): Remove this once rdar://22392526 is fixed.
BOOL _applicationInBackground;
}
@synthesize userInteracted = _userInteracted;
- (instancetype)initWithStartupInformation:
(id<StartupInformation>)startupInformation {
self = [super init];
if (self) {
_observers = [AppStateObserverList
observersWithProtocol:@protocol(AppStateObserver)];
_agents = [[NSMutableArray alloc] init];
_startupInformation = startupInformation;
_appCommandDispatcher = [[CommandDispatcher alloc] init];
// Subscribe to scene connection notifications.
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(sceneWillConnect:)
name:UISceneWillConnectNotification
object:nil];
[self addObserver:self];
}
return self;
}
#pragma mark - Properties implementation
- (void)setUiBlockerTarget:(id<UIBlockerTarget>)uiBlockerTarget {
_uiBlockerTarget = uiBlockerTarget;
for (SceneState* scene in self.connectedScenes) {
// When there's a scene with blocking UI, all other scenes should show the
// overlay.
BOOL shouldPresentOverlay =
(uiBlockerTarget != nil) && (scene != uiBlockerTarget);
scene.presentingModalOverlay = shouldPresentOverlay;
}
}
// Do not use this setter directly, instead use -queueTransitionToInitStage:
// that provides reentry guards.
- (void)setInitStage:(InitStage)newInitStage {
DCHECK(newInitStage >= InitStageStart);
DCHECK(newInitStage <= InitStageFinal);
// As of writing this, it seems reasonable for init stages to be strictly
// incremented by one only: if a stage needs to be skipped, it can just be a
// no-op, but the observers will get a chance to react to it normally. If in
// the future these need to be skipped, or go backwards:
// 1. Check that all observers will support this change
// 2. Keep the previous init stage and modify addObserver: code to send the
// previous init stage instead.
DCHECK(newInitStage == _initStage + 1 ||
(newInitStage == InitStageStart && _initStage == InitStageStart));
// It's probably a programming error to set the same init stage twice, except
// for InitStageStart to kick off the startup.
DCHECK(newInitStage == InitStageStart || _initStage != newInitStage);
InitStage previousInitStage = _initStage;
[self.observers appState:self willTransitionToInitStage:newInitStage];
_initStage = newInitStage;
[self.observers appState:self didTransitionFromInitStage:previousInitStage];
}
- (BOOL)portraitOnly {
if (ui::GetDeviceFormFactor() != ui::DEVICE_FORM_FACTOR_PHONE) {
return NO;
}
// Return YES if the First Run UI is showing.
return self.initStage > InitStageSafeMode &&
self.initStage <= InitStageFirstRun &&
self.startupInformation.isFirstRun;
}
#pragma mark - Public methods.
- (void)applicationDidEnterBackground:(UIApplication*)application
memoryHelper:(MemoryWarningHelper*)memoryHelper {
// Exit the app if backgrounding the app while being in safe mode.
if (self.initStage == InitStageSafeMode) {
exit(0);
}
if (_applicationInBackground) {
return;
}
_applicationInBackground = YES;
crash_keys::SetCurrentlyInBackground(true);
if (self.initStage < InitStageBrowserObjectsForUI) {
// The clean-up done in `-applicationDidEnterBackground:` is only valid for
// the case when the application is started in foreground, so there is
// nothing to clean up as the application was not initialized for
// foreground.
//
// From the stack trace of the crash bug http://crbug.com/437307 , it
// seems that `-applicationDidEnterBackground:` may be called when the app
// is started in background and before the initialization for background
// stage is done. Note that the crash bug could not be reproduced though.
return;
}
[MetricsMediator
applicationDidEnterBackground:[memoryHelper
foregroundMemoryWarningCount]];
[self.startupInformation expireFirstUserActionRecorder];
if (self.mainBrowserState && !_savingCookies) {
// Record that saving the cookies has started to prevent posting multiple
// tasks if the user quickly background, foreground and background the app
// again.
_savingCookies = YES;
// The closure may be called on any sequence, so ensure it is posted back
// on the current one but using base::BindPostTask(). The critical closure
// guarantees that the task will be run before backgrounding.
__weak AppState* weakSelf = self;
base::OnceClosure closure = base::BindPostTask(
base::SequencedTaskRunner::GetCurrentDefault(),
base::MakeCriticalClosure(
"applicationDidEnterBackground:_savingCookies", base::BindOnce(^{
// Accessing a property in a block is safe as this is compiled
// to sending a message which is well defined on nil.
weakSelf.savingCookies = NO;
}),
/*is_immediate=*/true));
// Saving the cookies needs to happen on the IO thread.
web::GetIOThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(
&FlushCookieStoreOnIOThread,
base::WrapRefCounted(self.mainBrowserState->GetRequestContext()),
std::move(closure)));
}
// Mark the startup as clean if it hasn't already been.
[[DeferredInitializationRunner sharedInstance]
runBlockIfNecessary:kStartupAttemptReset];
// Set date/time that the background fetch handler was called in the user
// defaults.
[MetricsMediator logDateInUserDefaults];
// Clear the memory warning flag since the app is now safely in background.
[[PreviousSessionInfo sharedInstance] resetMemoryWarningFlag];
[[PreviousSessionInfo sharedInstance] stopRecordingMemoryFootprint];
GetApplicationContext()->OnAppEnterBackground();
}
- (void)applicationWillEnterForeground:(UIApplication*)application
metricsMediator:(MetricsMediator*)metricsMediator
memoryHelper:(MemoryWarningHelper*)memoryHelper {
// Fully initialize the browser objects for the browser UI if it is not
// already the case. This is especially needed for scene startup.
if (self.initStage < InitStageBrowserObjectsForUI) {
// Invariant: The app has passed InitStageStart.
CHECK(self.initStage != InitStageStart);
// TODO(crbug.com/1197330): This function should only be called once
// during a specific stage, but this requires non-trivial refactoring, so
// for now #initializeUIPreSafeMode will just return early if called more
// than once.
// The application has been launched in background and the initialization
// is not complete.
[self initializeUIPreSafeMode];
return;
}
// Don't go further with foregrounding the app when the app has not passed
// safe mode yet or was initialized from the background.
if (self.initStage <= InitStageSafeMode || !_applicationInBackground)
return;
_applicationInBackground = NO;
if (self.mainBrowserState) {
AuthenticationServiceFactory::GetForBrowserState(self.mainBrowserState)
->OnApplicationWillEnterForeground();
}
crash_keys::SetCurrentlyInBackground(false);
// Update the state of metrics and crash reporting, as the method of
// communication may have changed while the app was in the background.
[metricsMediator updateMetricsStateBasedOnPrefsUserTriggered:NO];
// Send any feedback that might be still on temporary storage.
if (ios::provider::IsUserFeedbackSupported())
ios::provider::UploadAllPendingUserFeedback();
GetApplicationContext()->OnAppEnterForeground();
[MetricsMediator
logLaunchMetricsWithStartupInformation:self.startupInformation
connectedScenes:self.connectedScenes];
[memoryHelper resetForegroundMemoryWarningCount];
if (self.mainBrowserState) {
// Send the "Chrome Opened" event to the feature_engagement::Tracker on a
// warm start.
feature_engagement::TrackerFactory::GetForBrowserState(
self.mainBrowserState)
->NotifyEvent(feature_engagement::events::kChromeOpened);
}
base::RecordAction(base::UserMetricsAction("MobileWillEnterForeground"));
// This will be a no-op if upload already started.
crash_helper::UploadCrashReports();
}
- (void)applicationWillTerminate:(UIApplication*)application {
if (!_applicationInBackground) {
base::UmaHistogramBoolean(
"Stability.IOS.UTE.AppWillTerminateWasCalledInForeground", true);
}
if (_appIsTerminating) {
// Previous handling of this method spun the runloop, resulting in
// recursive calls; this does not appear to happen with the new shutdown
// flow, but this is here to ensure that if it can happen, it gets noticed
// and fixed.
CHECK(false);
}
_appIsTerminating = YES;
[_appCommandDispatcher prepareForShutdown];
// Cancel any in-flight distribution notifications.
ios::provider::CancelAppDistributionNotifications();
// Halt the tabs, so any outstanding requests get cleaned up, without actually
// closing the tabs. Set the BVC to inactive to cancel all the dialogs.
// Don't do this if there are no scenes, since there's no defined interface
// provider (and no tabs).
if (self.initStage >= InitStageBrowserObjectsForUI) {
for (SceneState* sceneState in self.connectedScenes) {
sceneState.browserProviderInterface.currentBrowserProvider
.userInteractionEnabled = NO;
}
}
[self.startupInformation stopChromeMain];
}
- (void)application:(UIApplication*)application
didDiscardSceneSessions:(NSSet<UISceneSession*>*)sceneSessions {
DCHECK_GE(self.initStage, InitStageBrowserObjectsForBackgroundHandlers);
GetApplicationContext()
->GetSystemIdentityManager()
->ApplicationDidDiscardSceneSessions(sceneSessions);
// Usually Chrome uses -[SceneState sceneSessionID] as identifier to properly
// support devices that do not support multi-window (and which use a constant
// identifier). For devices that do not support multi-window the session is
// saved at a constant path, so it is harmless to delete files at a path
// derived from -persistentIdentifier (since there won't be files deleted).
// For devices that do support multi-window, there is data to delete once the
// session is garbage collected.
//
// Thus it is always correct to use -persistentIdentifier here.
NSMutableArray<NSString*>* sessionIDs =
[NSMutableArray arrayWithCapacity:sceneSessions.count];
for (UISceneSession* session in sceneSessions) {
[sessionIDs addObject:session.persistentIdentifier];
}
sessions_storage_util::MarkSessionsForRemoval(sessionIDs);
crash_keys::SetConnectedScenesCount([self connectedScenes].count);
}
- (void)willResignActive {
// Regardless of app state, if the user is able to background the app, reset
// the failed startup count.
crash_util::ResetFailedStartupAttemptCount();
if (self.initStage < InitStageBrowserObjectsForUI) {
// If the application did not pass the foreground initialization stage,
// there is no active tab model to resign.
return;
}
// Set [self.startupInformation isColdStart] to NO in anticipation of the next
// time the app becomes active.
[self.startupInformation setIsColdStart:NO];
// Record session metrics (self.mainBrowserState may be null during tests).
if (self.mainBrowserState) {
SessionMetrics::FromBrowserState(self.mainBrowserState)
->RecordAndClearSessionMetrics(
MetricsToRecordFlags::kActivatedTabCount);
if (self.mainBrowserState->HasOffTheRecordChromeBrowserState()) {
ChromeBrowserState* otrChromeBrowserState =
self.mainBrowserState->GetOffTheRecordChromeBrowserState();
SessionMetrics::FromBrowserState(otrChromeBrowserState)
->RecordAndClearSessionMetrics(MetricsToRecordFlags::kNoMetrics);
}
}
}
- (void)addObserver:(id<AppStateObserver>)observer {
[self.observers addObserver:observer];
if ([observer respondsToSelector:@selector(appState:
didTransitionFromInitStage:)] &&
self.initStage > InitStageStart) {
InitStage previousInitStage = static_cast<InitStage>(self.initStage - 1);
// Trigger an update on the newly added agent.
[observer appState:self didTransitionFromInitStage:previousInitStage];
}
}
- (void)removeObserver:(id<SceneStateObserver>)observer {
[self.observers removeObserver:observer];
}
- (void)addAgent:(id<AppStateAgent>)agent {
DCHECK(agent);
[self.agents addObject:agent];
[agent setAppState:self];
}
- (void)removeAgent:(id<AppStateAgent>)agent {
DCHECK(agent);
DCHECK([self.agents containsObject:agent]);
[self.agents removeObject:agent];
}
- (void)queueTransitionToNextInitStage {
InitStage nextInitStage = static_cast<InitStage>(self.initStage + 1);
DCHECK(nextInitStage <= InitStageFinal);
[self queueTransitionToInitStage:nextInitStage];
}
- (void)startInitialization {
[self queueTransitionToInitStage:InitStageStart];
}
#pragma mark - Multiwindow-related
- (SceneState*)foregroundActiveScene {
for (SceneState* sceneState in self.connectedScenes) {
if (sceneState.activationLevel == SceneActivationLevelForegroundActive) {
return sceneState;
}
}
return nil;
}
- (NSArray<SceneState*>*)connectedScenes {
NSMutableArray* sceneStates = [[NSMutableArray alloc] init];
NSSet* connectedScenes = [UIApplication sharedApplication].connectedScenes;
for (UIWindowScene* scene in connectedScenes) {
if (![scene.delegate isKindOfClass:[SceneDelegate class]]) {
// This might happen in tests.
// TODO(crbug.com/1113097): This shouldn't be needed. (It might also
// be the cause of crbug.com/1142782).
[sceneStates addObject:[[SceneState alloc] initWithAppState:self]];
continue;
}
SceneDelegate* sceneDelegate =
base::apple::ObjCCastStrict<SceneDelegate>(scene.delegate);
[sceneStates addObject:sceneDelegate.sceneState];
}
return sceneStates;
}
- (NSArray<SceneState*>*)foregroundScenes {
return [self.connectedScenes
filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(
SceneState* scene,
NSDictionary* bindings) {
return scene.activationLevel >= SceneActivationLevelForegroundInactive;
}]];
}
- (void)setLastTappedWindow:(UIWindow*)window {
if (_lastTappedWindow == window) {
return;
}
_lastTappedWindow = window;
[self.observers appState:self lastTappedWindowChanged:window];
}
- (void)initializeUIPreSafeMode {
// TODO(crbug.com/1197330): Consider replacing this with a DCHECK once we
// make sure that #initializeUIPreSafeMode is only called once. This should
// be done in a one-line change that is easy to revert.
// Only perform the pre-safemode initialization once.
if (_userInteracted) {
return;
}
_userInteracted = YES;
[self saveLaunchDetailsToDefaults];
// Continue the initialization.
[self queueTransitionToNextInitStage];
}
- (void)completeUIInitialization {
DCHECK([self.startupInformation isColdStart]);
}
#pragma mark - Internal methods.
- (void)saveLaunchDetailsToDefaults {
// Reset the failure count on first launch, increment it on other launches.
if ([[PreviousSessionInfo sharedInstance] isFirstSessionAfterUpgrade])
crash_util::ResetFailedStartupAttemptCount();
else
crash_util::IncrementFailedStartupAttemptCount(false);
// The startup failure count *must* be synchronized now, since the crashes it
// is trying to count are during startup.
// -[PreviousSessionInfo beginRecordingCurrentSession] calls `synchronize` on
// the user defaults, so leverage that to prevent calling it twice.
// Start recording info about this session.
[[PreviousSessionInfo sharedInstance] beginRecordingCurrentSession];
}
- (void)queueTransitionToInitStage:(InitStage)initStage {
if (self.isIncrementingInitStage) {
// It is an error to queue more than one transition at once.
DCHECK(!self.needsIncrementInitStage);
// Set a flag to increment after the observers are notified of the current
// change.
self.needsIncrementInitStage = YES;
return;
}
self.isIncrementingInitStage = YES;
self.initStage = initStage;
self.isIncrementingInitStage = NO;
if (self.needsIncrementInitStage) {
self.needsIncrementInitStage = NO;
[self queueTransitionToNextInitStage];
}
}
#pragma mark - UIBlockerManager
- (void)incrementBlockingUICounterForTarget:(id<UIBlockerTarget>)target {
DCHECK(self.uiBlockerTarget == nil || target == self.uiBlockerTarget)
<< "Another scene is already showing a blocking UI!";
self.blockingUICounter++;
if (!self.uiBlockerTarget) {
self.uiBlockerTarget = target;
}
}
- (void)decrementBlockingUICounterForTarget:(id<UIBlockerTarget>)target {
DCHECK(self.blockingUICounter > 0 && self.uiBlockerTarget == target);
self.blockingUICounter--;
if (self.blockingUICounter == 0) {
self.uiBlockerTarget = nil;
}
}
- (id<UIBlockerTarget>)currentUIBlocker {
return self.uiBlockerTarget;
}
#pragma mark - SceneStateObserver
- (void)sceneStateDidEnableUI:(SceneState*)sceneState {
if (self.firstSceneHasInitializedUI) {
return;
}
self.firstSceneHasInitializedUI = YES;
[self.observers appState:self firstSceneHasInitializedUI:sceneState];
}
- (void)sceneState:(SceneState*)sceneState
transitionedToActivationLevel:(SceneActivationLevel)level {
if (level >= SceneActivationLevelForegroundActive) {
sceneState.presentingModalOverlay =
(self.uiBlockerTarget != nil) && (self.uiBlockerTarget != sceneState);
[self.observers appState:self sceneDidBecomeActive:sceneState];
}
crash_keys::SetForegroundScenesCount([self foregroundScenes].count);
}
#pragma mark - Scenes lifecycle
- (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;
SceneState* sceneState = sceneDelegate.sceneState;
DCHECK(sceneState);
[self.observers appState:self sceneConnected:sceneState];
crash_keys::SetConnectedScenesCount([self connectedScenes].count);
}
#pragma mark - AppStateObserver
// TODO(crbug.com/1191489): Move this logic to a specific agent.
- (void)appState:(AppState*)appState
didTransitionFromInitStage:(InitStage)previousInitStage {
if (previousInitStage != InitStageBrowserObjectsForUI) {
return;
}
[self completeUIInitialization];
}
@end