// 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/strings/sys_string_conversions.h"
#import "base/task/bind_post_task.h"
#import "base/types/cxx23_to_underlying.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+Testing.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/deferred_initialization_runner.h"
#import "ios/chrome/app/profile/profile_init_stage.h"
#import "ios/chrome/app/profile/profile_state.h"
#import "ios/chrome/browser/browsing_data/model/sessions_storage_util.h"
#import "ios/chrome/browser/crash_report/model/crash_helper.h"
#import "ios/chrome/browser/crash_report/model/crash_keys_helper.h"
#import "ios/chrome/browser/crash_report/model/crash_loop_detection_util.h"
#import "ios/chrome/browser/crash_report/model/features.h"
#import "ios/chrome/browser/device_sharing/model/device_sharing_manager.h"
#import "ios/chrome/browser/enterprise/model/idle/idle_service_factory.h"
#import "ios/chrome/browser/feature_engagement/model/tracker_factory.h"
#import "ios/chrome/browser/metrics/model/web_state_list_metrics_browser_agent.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/profile/profile_ios.h"
#import "ios/chrome/browser/shared/model/profile/profile_manager_ios.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/shared/public/features/features.h"
#import "ios/chrome/browser/signin/model/authentication_service.h"
#import "ios/chrome/browser/signin/model/authentication_service_factory.h"
#import "ios/chrome/browser/signin/model/system_identity_manager.h"
#import "ios/chrome/browser/web_state_list/model/session_metrics.h"
#import "ios/chrome/browser/web_state_list/model/web_usage_enabler/web_usage_enabler_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));
}
// Return the equivalent ProfileInitStage from app InitStage.
ProfileInitStage ProfileInitStageFromAppInitStage(InitStage app_init_stage) {
switch (app_init_stage) {
case InitStageStart:
case InitStageBrowserBasic:
case InitStageSafeMode:
case InitStageVariationsSeed:
NOTREACHED();
case InitStageBrowserObjectsForBackgroundHandlers:
return ProfileInitStage::InitStageProfileLoaded;
case InitStageEnterprise:
return ProfileInitStage::InitStageEnterprise;
case InitStageBrowserObjectsForUI:
return ProfileInitStage::InitStagePrepareUI;
case InitStageNormalUI:
return ProfileInitStage::InitStageUIReady;
case InitStageFirstRun:
return ProfileInitStage::InitStageFirstRun;
case InitStageChoiceScreen:
return ProfileInitStage::InitStageChoiceScreen;
case InitStageFinal:
return ProfileInitStage::InitStageFinal;
}
}
} // 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;
// 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/41211311): Remove this once rdar://22392526 is fixed.
BOOL _applicationInBackground;
// The counter of the number of views which want to block the screen to
// portrait mode for iPhone. This counter should always be 0 for iPad.
NSUInteger _iphonePortraitOnlyCounter;
}
@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];
// Observe the status of VoiceOver for crash logging.
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(voiceOverStatusDidChange:)
name:UIAccessibilityVoiceOverStatusDidChangeNotification
object:nil];
crash_keys::SetVoiceOverRunning(UIAccessibilityIsVoiceOverRunning());
[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;
}
if (_iphonePortraitOnlyCounter > 0) {
return YES;
}
// Return YES if the First Run UI is showing.
return self.initStage > InitStageSafeMode &&
self.initStage <= InitStageFirstRun &&
self.startupInformation.isFirstRun;
}
- (NSArray<id<AppStateAgent>>*)connectedAgents {
return [self.agents copy];
}
#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;
}
for (ChromeBrowserState* browserState :
GetApplicationContext()->GetProfileManager()->GetLoadedProfiles()) {
enterprise_idle::IdleServiceFactory::GetForBrowserState(browserState)
->OnApplicationWillEnterBackground();
}
[MetricsMediator
applicationDidEnterBackground:[memoryHelper
foregroundMemoryWarningCount]];
[self.startupInformation expireFirstUserActionRecorder];
// TODO(crbug.com/325596562): Update this for multiple browser states and for
// per-state cookie storage.
if (self.mainProfile.browserState && !_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.mainProfile.browserState->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/40760092): 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;
for (ChromeBrowserState* chromeBrowserState :
GetApplicationContext()->GetProfileManager()->GetLoadedProfiles()) {
AuthenticationServiceFactory::GetForBrowserState(chromeBrowserState)
->OnApplicationWillEnterForeground();
enterprise_idle::IdleServiceFactory::GetForBrowserState(chromeBrowserState)
->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];
for (ChromeBrowserState* chromeBrowserState :
GetApplicationContext()->GetProfileManager()->GetLoadedProfiles()) {
feature_engagement::Tracker* tracker =
feature_engagement::TrackerFactory::GetForBrowserState(
chromeBrowserState);
// Send the "Chrome Opened" event to the feature_engagement::Tracker on a
// warm start.
tracker->NotifyEvent(feature_engagement::events::kChromeOpened);
[metricsMediator notifyCredentialProviderWasUsed:tracker];
}
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) {
Browser* browser =
sceneState.browserProviderInterface.currentBrowserProvider.browser;
if (browser && WebUsageEnablerBrowserAgent::FromBrowser(browser)) {
WebUsageEnablerBrowserAgent::FromBrowser(browser)->SetWebUsageEnabled(
false);
}
}
}
[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.
std::set<std::string> sessionIDs;
for (UISceneSession* session in sceneSessions) {
sessionIDs.insert(base::SysNSStringToUTF8(session.persistentIdentifier));
}
sessions_storage_util::MarkSessionsForRemoval(std::move(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.
for (ChromeBrowserState* browserState :
GetApplicationContext()->GetProfileManager()->GetLoadedProfiles()) {
SessionMetrics::FromBrowserState(browserState)
->RecordAndClearSessionMetrics(
MetricsToRecordFlags::kActivatedTabCount);
if (browserState->HasOffTheRecordChromeBrowserState()) {
ChromeBrowserState* otrChromeBrowserState =
browserState->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<AppStateObserver>)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/40710078): 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)initializeUIPreSafeMode {
// TODO(crbug.com/40760092): 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;
// TODO(crbug.com/353683675) Improve this logic once ProfileInitStage and
// (app) InitStage are fully decoupled.
if (initStage >= InitStageBrowserObjectsForBackgroundHandlers) {
ProfileInitStage currStage = self.mainProfile.initStage;
ProfileInitStage nextStage = ProfileInitStageFromAppInitStage(initStage);
while (currStage != nextStage) {
// The ProfileInitStage enum has more values than InitStage, so move over
// all stage that have no representation in InitStage to avoid failing
// CHECK in -[ProfileState setInitStage:].
currStage =
static_cast<ProfileInitStage>(base::to_underlying(currStage) + 1);
self.mainProfile.initStage = currStage;
}
}
self.isIncrementingInitStage = NO;
if (self.needsIncrementInitStage) {
self.needsIncrementInitStage = NO;
[self queueTransitionToNextInitStage];
}
}
#pragma mark - IphonePortraitOnlyManager
- (void)incrementIphonePortraitOnlyCounter {
++_iphonePortraitOnlyCounter;
}
- (void)decrementIphonePortraitOnlyCounter {
CHECK_GT(_iphonePortraitOnlyCounter, 0ul);
--_iphonePortraitOnlyCounter;
}
#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 - Voice Over lifecycle
- (void)voiceOverStatusDidChange:(NSNotification*)notification {
crash_keys::SetVoiceOverRunning(UIAccessibilityIsVoiceOverRunning());
}
#pragma mark - AppStateObserver
// TODO(crbug.com/40756629): Move this logic to a specific agent.
- (void)appState:(AppState*)appState
didTransitionFromInitStage:(InitStage)previousInitStage {
if (previousInitStage != InitStageBrowserObjectsForUI) {
return;
}
[self completeUIInitialization];
}
@end