blob: eb65258d151d0c7a58a1eef8d3f994b6bce6e31c [file] [log] [blame]
// Copyright 2012 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_controller.h"
#import <memory>
#import "base/apple/bundle_locations.h"
#import "base/apple/foundation_util.h"
#import "base/barrier_closure.h"
#import "base/check_op.h"
#import "base/feature_list.h"
#import "base/functional/callback.h"
#import "base/functional/concurrent_closures.h"
#import "base/ios/ios_util.h"
#import "base/memory/raw_ptr.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/path_service.h"
#import "base/strings/sys_string_conversions.h"
#import "base/task/bind_post_task.h"
#import "base/task/sequenced_task_runner.h"
#import "components/component_updater/component_updater_service.h"
#import "components/component_updater/installer_policies/autofill_states_component_installer.h"
#import "components/component_updater/installer_policies/on_device_head_suggest_component_installer.h"
#import "components/component_updater/installer_policies/optimization_hints_component_installer.h"
#import "components/component_updater/installer_policies/plus_address_blocklist_component_installer.h"
#import "components/component_updater/installer_policies/safety_tips_component_installer.h"
#import "components/content_settings/core/browser/host_content_settings_map.h"
#import "components/metrics/metrics_pref_names.h"
#import "components/metrics/metrics_service.h"
#import "components/password_manager/core/common/password_manager_features.h"
#import "components/password_manager/core/common/passwords_directory_util_ios.h"
#import "components/prefs/ios/pref_observer_bridge.h"
#import "components/prefs/pref_change_registrar.h"
#import "components/prefs/scoped_user_pref_update.h"
#import "components/previous_session_info/previous_session_info.h"
#import "components/sync/service/sync_service.h"
#import "components/web_resource/web_resource_pref_names.h"
#import "ios/chrome/app/app_metrics_app_state_agent.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/background_refresh/background_refresh_app_agent.h"
#import "ios/chrome/app/background_refresh/test_refresher.h"
#import "ios/chrome/app/blocking_scene_commands.h"
#import "ios/chrome/app/change_profile_animator.h"
#import "ios/chrome/app/change_profile_commands.h"
#import "ios/chrome/app/deferred_initialization_runner.h"
#import "ios/chrome/app/deferred_initialization_task_names.h"
#import "ios/chrome/app/enterprise_app_agent.h"
#import "ios/chrome/app/fast_app_terminate_buildflags.h"
#import "ios/chrome/app/launch_screen_view_controller.h"
#import "ios/chrome/app/memory_monitor.h"
#import "ios/chrome/app/profile/profile_controller.h"
#import "ios/chrome/app/profile/profile_state.h"
#import "ios/chrome/app/profile/profile_state_observer.h"
#import "ios/chrome/app/safe_mode_app_state_agent.h"
#import "ios/chrome/app/startup/chrome_app_startup_parameters.h"
#import "ios/chrome/app/startup/chrome_main_starter.h"
#import "ios/chrome/app/startup/client_registration.h"
#import "ios/chrome/app/startup/ios_chrome_main.h"
#import "ios/chrome/app/startup/ios_enable_sandbox_dump_buildflags.h"
#import "ios/chrome/app/startup/provider_registration.h"
#import "ios/chrome/app/startup/register_experimental_settings.h"
#import "ios/chrome/app/startup/setup_debugging.h"
#import "ios/chrome/app/startup_tasks.h"
#import "ios/chrome/app/tests_hook.h"
#import "ios/chrome/app/variations_app_state_agent.h"
#import "ios/chrome/browser/accessibility/model/window_accessibility_change_notifier_app_agent.h"
#import "ios/chrome/browser/appearance/ui_bundled/appearance_customization.h"
#import "ios/chrome/browser/banner_promo/model/default_browser_banner_promo_app_agent.h"
#import "ios/chrome/browser/browsing_data/model/sessions_storage_util.h"
#import "ios/chrome/browser/content_settings/model/host_content_settings_map_factory.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/crash_report_helper.h"
#import "ios/chrome/browser/credential_provider/model/credential_provider_buildflags.h"
#import "ios/chrome/browser/default_browser/model/utils.h"
#import "ios/chrome/browser/discover_feed/model/discover_feed_app_agent.h"
#import "ios/chrome/browser/download/model/download_directory_util.h"
#import "ios/chrome/browser/first_run/model/first_run.h"
#import "ios/chrome/browser/first_run/ui_bundled/first_run_util.h"
#import "ios/chrome/browser/main/ui_bundled/browser_view_wrangler.h"
#import "ios/chrome/browser/memory/model/memory_debugger_manager.h"
#import "ios/chrome/browser/metrics/model/first_user_action_recorder.h"
#import "ios/chrome/browser/metrics/model/incognito_usage_app_state_agent.h"
#import "ios/chrome/browser/metrics/model/tab_usage_recorder_browser_agent.h"
#import "ios/chrome/browser/metrics/model/window_configuration_recorder.h"
#import "ios/chrome/browser/ntp/ui_bundled/new_tab_page_feature.h"
#import "ios/chrome/browser/omaha/model/omaha_service.h"
#import "ios/chrome/browser/passwords/model/password_manager_util_ios.h"
#import "ios/chrome/browser/saved_tab_groups/model/tab_group_sync_service_factory.h"
#import "ios/chrome/browser/screenshot/model/screenshot_metrics_recorder.h"
#import "ios/chrome/browser/search_engines/model/search_engines_util.h"
#import "ios/chrome/browser/sessions/model/session_restoration_service.h"
#import "ios/chrome/browser/sessions/model/session_restoration_service_factory.h"
#import "ios/chrome/browser/sessions/model/session_util.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/application_context/application_context.h"
#import "ios/chrome/browser/shared/model/browser/browser.h"
#import "ios/chrome/browser/shared/model/browser/browser_list.h"
#import "ios/chrome/browser/shared/model/browser/browser_list_factory.h"
#import "ios/chrome/browser/shared/model/browser/browser_provider.h"
#import "ios/chrome/browser/shared/model/paths/paths.h"
#import "ios/chrome/browser/shared/model/prefs/pref_names.h"
#import "ios/chrome/browser/shared/model/profile/profile_attributes_ios.h"
#import "ios/chrome/browser/shared/model/profile/profile_attributes_storage_ios.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/model/web_state_list/web_state_list.h"
#import "ios/chrome/browser/shared/public/commands/browser_coordinator_commands.h"
#import "ios/chrome/browser/shared/public/commands/command_dispatcher.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/shared/public/features/system_flags.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/signin/model/account_profile_mapper.h"
#import "ios/chrome/browser/signin/model/system_identity_manager.h"
#import "ios/chrome/browser/sync/model/sync_service_factory.h"
#import "ios/chrome/browser/ui/device_orientation/scoped_force_portrait_orientation.h"
#import "ios/chrome/browser/url_loading/model/url_loading_params.h"
#import "ios/chrome/browser/web/model/choose_file/choose_file_file_utils.h"
#import "ios/chrome/browser/web_state_list/model/web_usage_enabler/web_usage_enabler_browser_agent.h"
#import "ios/chrome/browser/webui/ui_bundled/chrome_web_ui_ios_controller_factory.h"
#import "ios/chrome/common/app_group/app_group_constants.h"
#import "ios/chrome/common/app_group/app_group_field_trial_version.h"
#import "ios/chrome/common/app_group/app_group_utils.h"
#import "ios/components/cookie_util/cookie_util.h"
#import "ios/net/cookies/cookie_store_ios.h"
#import "ios/net/empty_nsurlcache.h"
#import "ios/public/provider/chrome/browser/app_distribution/app_distribution_api.h"
#import "ios/public/provider/chrome/browser/memory_experimenter/memory_experimenter_api.h"
#import "ios/public/provider/chrome/browser/overrides/overrides_api.h"
#import "ios/public/provider/chrome/browser/raccoon/raccoon_api.h"
#import "ios/public/provider/chrome/browser/ui_utils/ui_utils_api.h"
#import "ios/public/provider/chrome/browser/user_feedback/user_feedback_api.h"
#import "ios/web/public/webui/web_ui_ios_controller_factory.h"
#import "net/base/apple/url_conversions.h"
#import "rlz/buildflags/buildflags.h"
#import "services/network/public/cpp/shared_url_loader_factory.h"
#import "ui/base/device_form_factor.h"
#if BUILDFLAG(IOS_CREDENTIAL_PROVIDER_ENABLED)
#import "ios/chrome/app/credential_provider_migrator_app_agent.h"
#endif
#if BUILDFLAG(IOS_ENABLE_SANDBOX_DUMP)
#import "ios/chrome/app/dump_documents_statistics.h"
#endif
#if BUILDFLAG(ENABLE_RLZ)
#import "components/rlz/rlz_tracker.h" // nogncheck
#import "ios/chrome/browser/rlz/rlz_tracker_delegate_impl.h" // nogncheck
#endif
@interface MainController (ForUnloadProfileMarkedForDeletion)
- (void)unloadProfileMarkedForDeletion:(std::string_view)profileName
completion:(ProfileDeletedCallback)completion;
@end
namespace {
#if BUILDFLAG(FAST_APP_TERMINATE_ENABLED)
// Skip chromeMain.reset() on shutdown, see crbug.com/1328891 for details.
BASE_FEATURE(kFastApplicationWillTerminate,
"FastApplicationWillTerminate",
base::FEATURE_DISABLED_BY_DEFAULT);
#endif // BUILDFLAG(FAST_APP_TERMINATE_ENABLED)
// Constants for deferring memory debugging tools startup.
NSString* const kMemoryDebuggingToolsStartup = @"MemoryDebuggingToolsStartup";
// Constants for deferring saving field trial values
NSString* const kSaveFieldTrialValues = @"SaveFieldTrialValues";
// Constants for refreshing the WidgetKit after five minutes
NSString* const kWidgetKitRefreshFiveMinutes = @"WidgetKitRefreshFiveMinutes";
// Constants for deferred check if it is necessary to send pings to
// Chrome distribution related services.
NSString* const kSendInstallPingIfNecessary = @"SendInstallPingIfNecessary";
// Constants for deferred deletion of leftover user downloaded files.
NSString* const kDeleteDownloads = @"DeleteDownloads";
// Constants for deferred deletion of leftover user chosen files for upload.
NSString* const kDeleteChooseFile = @"DeleteChooseFile";
// Constants for deferred deletion of leftover temporary passwords files.
NSString* const kDeleteTempPasswords = @"DeleteTempPasswords";
// Constants for deferred UMA logging of existing Siri User shortcuts.
NSString* const kLogSiriShortcuts = @"LogSiriShortcuts";
// Constants for deferred sending of queued feedback.
NSString* const kSendQueuedFeedback = @"SendQueuedFeedback";
// Constants for deferring the upload of crash reports.
NSString* const kUploadCrashReports = @"UploadCrashReports";
// Constants for deferring the enterprise managed device check.
NSString* const kEnterpriseManagedDeviceCheck = @"EnterpriseManagedDeviceCheck";
// Constants for deferred deletion of leftover session state files.
NSString* const kPurgeWebSessionStates = @"PurgeWebSessionStates";
// Constant for deffered memory experimentation.
NSString* const kMemoryExperimentation = @"BeginMemoryExperimentation";
// Adapted from chrome/browser/ui/browser_init.cc.
void RegisterComponentsForUpdate() {
component_updater::ComponentUpdateService* cus =
GetApplicationContext()->GetComponentUpdateService();
DCHECK(cus);
RegisterOnDeviceHeadSuggestComponent(
cus, GetApplicationContext()->GetApplicationLocale());
RegisterSafetyTipsComponent(cus);
RegisterAutofillStatesComponent(cus,
GetApplicationContext()->GetLocalState());
RegisterOptimizationHintsComponent(cus);
RegisterPlusAddressBlocklistComponent(cus);
}
// The delay before beginning memory experimentation.
constexpr base::TimeDelta kMemoryExperimentationDelay = base::Minutes(1);
// Schedules memory experimentation.
void BeginMemoryExperimentationAfterDelay() {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
kMemoryExperimentationDelay.InNanoseconds()),
dispatch_get_main_queue(), ^{
ios::provider::BeginMemoryExperimentation();
});
}
// Inserts `session_ids` into the set of discarded sessions for `attrs`.
void InsertDiscardedSessions(const std::set<std::string>& session_ids,
ProfileAttributesIOS& attrs) {
auto discarded_sessions = attrs.GetDiscardedSessions();
discarded_sessions.insert(session_ids.begin(), session_ids.end());
attrs.SetDiscardedSessions(discarded_sessions);
}
// Mark all `sessions` as discarded sessions for all profiles.
void MarkSessionsAsDiscardedForAllProfiles(NSSet<UISceneSession*>* sessions) {
ProfileAttributesStorageIOS* storage = GetApplicationContext()
->GetProfileManager()
->GetProfileAttributesStorage();
// Prior to M-133, the list of sessions to discard was stored in a plist.
// If the file still exists, then copy the session identifiers, and then
// delete the file.
std::set<std::string> sessionIDs =
sessions_storage_util::GetDiscardedSessions();
// 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.
for (UISceneSession* session in sessions) {
sessionIDs.insert(base::SysNSStringToUTF8(session.persistentIdentifier));
}
storage->IterateOverProfileAttributes(
base::BindRepeating(&InsertDiscardedSessions, sessionIDs));
sessions_storage_util::ResetDiscardedSessions();
}
// It was found that -application:didDiscardSceneSessions: may be called with
// UISceneSession* corresponding to SceneState* that are still connected. It
// caused flakyness of EarlGrey tests (see https://crbug.com/390108895). The
// behaviour has only been confirmed for EarlGrey tests. Record an histogram
// counting how many Scenes are discarded while still connected to detect if
// the issue also reproduce in production (if it were to reproduce, it would
// cause unexplained tab losses).
//
// See https://crbug.com/392575873 for details.
void RecordDiscardSceneStillConnected(NSSet<UISceneSession*>* scene_sessions,
NSArray<SceneState*>* connected_scenes) {
// iPhone do not use -persistentIdentifier to identify the session data
// for a SceneState, so they will never delete data. Only record metric
// for iPad since even if the issue reproduce on iPhone, it won't have
// any impact.
if (ui::GetDeviceFormFactor() != ui::DEVICE_FORM_FACTOR_TABLET) {
return;
}
NSUInteger count_discarded_scene_still_connected = 0;
NSMutableSet<NSString*>* connected_identifiers = [[NSMutableSet alloc] init];
for (SceneState* scene_state in connected_scenes) {
[connected_identifiers addObject:scene_state.sceneSessionID];
}
for (UISceneSession* scene_session in scene_sessions) {
NSString* persistent_identifier = scene_session.persistentIdentifier;
if ([connected_identifiers containsObject:persistent_identifier]) {
++count_discarded_scene_still_connected;
}
}
base::UmaHistogramExactLinear(
"IOS.Sessions.DiscardedScenesStillConnectedCount",
count_discarded_scene_still_connected, 100);
}
// Helper used to call -unloadProfileMarkedForDeletion:completion: from
// a callback.
void UnloadProfileMarkedForDeletion(MainController* controller,
std::string_view profile_name,
ProfileDeletedCallback completion) {
[controller unloadProfileMarkedForDeletion:profile_name
completion:std::move(completion)];
}
// Helper used to implement a continuation that invoke `done_closure`.
void DeleteProfileContinuation(base::OnceClosure done_closure,
SceneState* scene_state,
base::OnceClosure next_closure) {
std::move(done_closure).Run();
std::move(next_closure).Run();
}
} // namespace
@interface MainController () <AppStateObserver,
BlockingSceneCommands,
ChangeProfileCommands,
PrefObserverDelegate,
ProfileStateObserver>
// Handles collecting metrics on user triggered screenshots
@property(nonatomic, strong)
ScreenshotMetricsRecorder* screenshotMetricsRecorder;
// Pings distribution services.
- (void)pingDistributionServices;
// Sends any feedback that happens to still be on local storage.
- (void)sendQueuedFeedback;
// Called whenever an orientation change is received.
- (void)orientationDidChange:(NSNotification*)notification;
// Register to receive orientation change notification to update crash keys.
- (void)registerForOrientationChangeNotifications;
// Asynchronously creates the pref observers.
- (void)schedulePrefObserverInitialization;
// Asynchronously schedules pings to distribution services.
- (void)scheduleAppDistributionPings;
// Asynchronously schedule the init of the memoryDebuggerManager.
- (void)scheduleMemoryDebuggingTools;
// Asynchronously kick off regular free memory checks.
- (void)startFreeMemoryMonitoring;
// Asynchronously schedules the reset of the failed startup attempt counter.
- (void)scheduleStartupAttemptReset;
// Asynchronously schedules the upload of crash reports.
- (void)scheduleCrashReportUpload;
// Schedules various tasks to be performed after the application becomes active.
- (void)scheduleLowPriorityStartupTasks;
// Schedules the deletion of user downloaded files that might be leftover
// from the last time Chrome was run.
- (void)scheduleDeleteTempDownloadsDirectory;
// Schedules the deletion of file user chosed to upload that might be leftover
// from the last time Chrome was run.
- (void)scheduleDeleteTempChooseFileDirectory;
// Schedule the deletion of the temporary passwords files that might
// be left over from incomplete export operations.
- (void)scheduleDeleteTempPasswordsDirectory;
// Schedule the start of memory experimentation.
- (void)scheduleMemoryExperimentation;
// Crashes the application if requested.
- (void)crashIfRequested;
// Initializes the application to the minimum initialization needed in all
// cases.
- (void)startUpBrowserBasicInitialization;
// Initializes the browser objects for the background handlers, perform any
// background initilisation that are required, and then transition to the
// next stage.
- (void)startUpBrowserBackgroundInitialization;
@end
@implementation MainController {
// The object that drives the Chrome startup/shutdown logic.
std::unique_ptr<IOSChromeMain> _chromeMain;
// True if the current session began from a cold start. False if the app has
// entered the background at least once since start up.
BOOL _isColdStart;
// True if the launch metrics have already been recorded.
BOOL _launchMetricsRecorded;
// YES if the user has ever interacted with the application. May be NO if the
// application has been woken up by the system for background work.
BOOL _userInteracted;
// Whether the application is currently in the background. Workaround for
// rdar://22392526 where -applicationDidEnterBackground: can be called twice.
// TODO(crbug.com/41211311): remove when rdar:22392526 is fixed
BOOL _applicationInBackground;
// YES if any Profile had initialized the UI for its first Scene.
BOOL _firstWindowCreated;
// An object to record metrics related to the user's first action.
std::unique_ptr<FirstUserActionRecorder> _firstUserActionRecorder;
// Bridge to listen to pref changes.
std::unique_ptr<PrefObserverBridge> _localStatePrefObserverBridge;
// Registrar for pref changes notifications to the local state.
PrefChangeRegistrar _localStatePrefChangeRegistrar;
// The class in charge of showing/hiding the memory debugger when the
// appropriate pref changes.
MemoryDebuggerManager* _memoryDebuggerManager;
// Variable backing metricsMediator property.
__weak MetricsMediator* _metricsMediator;
// Holds the ProfileController for all loaded profiles.
std::map<std::string, ProfileController*, std::less<>> _profileControllers;
WindowConfigurationRecorder* _windowConfigurationRecorder;
// Handler for the startup tasks, deferred or not.
StartupTasks* _startupTasks;
// The set of "scene sessions" that needs to be discarded. See
// -application:didDiscardSceneSessions: for details.
NSMutableSet<UISceneSession*>* _sceneSessionsToDiscard;
// Used to force the device orientation in portrait mode on iPhone.
std::unique_ptr<ScopedForcePortraitOrientation> _scopedForceOrientation;
// The highest ProfileInitStage reached by any ProfileState. This value
// can only be increased, never decreased. It gates application-level
// initialisation that should only happen once at least one Profile has
// reached a significant stage (e.g. loaded the session and allowed the
// user to interact with the application, ...).
ProfileInitStage _highestProfileInitStageReached;
}
// Defined by public protocols.
// - StartupInformation
@synthesize isColdStart = _isColdStart;
@synthesize appLaunchTime = _appLaunchTime;
@synthesize isFirstRun = _isFirstRun;
@synthesize isTerminating = _isTerminating;
@synthesize didFinishLaunchingTime = _didFinishLaunchingTime;
@synthesize firstSceneConnectionTime = _firstSceneConnectionTime;
#pragma mark - Application lifecycle
- (instancetype)init {
if ((self = [super init])) {
_isFirstRun = ShouldPresentFirstRunExperience();
_startupTasks = [[StartupTasks alloc] init];
}
return self;
}
- (void)dealloc {
[NSObject cancelPreviousPerformRequestsWithTarget:self];
}
- (void)startUpBrowserBasicInitialization {
_appLaunchTime = IOSChromeMain::StartTime();
_isColdStart = YES;
UMA_HISTOGRAM_BOOLEAN("IOS.Process.ActivePrewarm",
base::ios::IsApplicationPreWarmed());
[SetupDebugging setUpDebuggingOptions];
// Register all providers before calling any Chromium code.
[ProviderRegistration registerProviders];
// Start dispatching for blocking UI commands.
[self.appState.appCommandDispatcher
startDispatchingToTarget:self
forProtocol:@protocol(BlockingSceneCommands)];
// Start dispatching for profile change commands.
[self.appState.appCommandDispatcher
startDispatchingToTarget:self
forProtocol:@protocol(ChangeProfileCommands)];
}
- (void)startUpBrowserBackgroundInitialization {
NSBundle* baseBundle = base::apple::OuterBundle();
base::apple::SetBaseBundleID(
base::SysNSStringToUTF8([baseBundle bundleIdentifier]).c_str());
// Register default values for experimental settings (Application Preferences)
// and set the "Version" key in the UserDefaults.
[RegisterExperimentalSettings
registerExperimentalSettingsWithUserDefaults:[NSUserDefaults
standardUserDefaults]
bundle:base::apple::
FrameworkBundle()];
// Register all clients before calling any web code.
[ClientRegistration registerClients];
_chromeMain = [ChromeMainStarter startChromeMain];
// Register the ChangeProfileCommands handler with AccountProfileMapper.
GetApplicationContext()
->GetAccountProfileMapper()
->SetChangeProfileCommandsHandler(HandlerForProtocol(
self.appState.appCommandDispatcher, ChangeProfileCommands));
// Start recording field trial info.
[[PreviousSessionInfo sharedInstance] beginRecordingFieldTrials];
// Give tests a chance to prepare for testing.
tests_hook::SetUpTestsIfPresent();
// Initialize the provider UI global state.
ios::provider::InitializeUI();
// If the user has interacted with the app, then start (or continue) watching
// for crashes. Otherwise, do not watch for crashes.
//
// Depending on the client's ExtendedVariationsSafeMode experiment group (see
// MaybeExtendVariationsSafeMode() in variations_field_trial_creator.cc for
// more info), the signal to start watching for crashes may have occurred
// earlier.
//
// TODO(b/184937096): Remove the below call to OnAppEnterForeground() if this
// call is moved earlier for all clients. It is is being kept here for the
// time being for the control group of the extended Variations Safe Mode
// experiment.
//
// TODO(crbug.com/40190949): Stop watching for a crash if this is a background
// fetch.
if (_userInteracted) {
GetApplicationContext()->GetMetricsService()->OnAppEnterForeground();
}
web::WebUIIOSControllerFactory::RegisterFactory(
ChromeWebUIIOSControllerFactory::GetInstance());
[NSURLCache setSharedURLCache:[EmptyNSURLCache emptyNSURLCache]];
if (_sceneSessionsToDiscard) {
[self application:[UIApplication sharedApplication]
didDiscardSceneSessions:std::exchange(_sceneSessionsToDiscard, nil)];
}
[self.appState queueTransitionToNextInitStage];
}
// This initialization must happen before any windows are created.
- (void)startUpBeforeFirstWindowCreated {
// TODO(crbug.com/40190949): Determine whether Chrome needs to resume
// watching for crashes.
self.appState.postCrashAction = [self postCrashAction];
base::UmaHistogramEnumeration("Stability.IOS.PostCrashAction",
self.appState.postCrashAction);
GetApplicationContext()->OnAppEnterForeground();
// Although this duplicates some metrics_service startup logic also in
// IOSChromeMain(), this call does additional work, checking for wifi-only
// and setting up the required support structures.
[_metricsMediator updateMetricsStateBasedOnPrefsUserTriggered:NO];
// Crash the app during startup if requested but only after we have enabled
// uploading crash reports.
[self crashIfRequested];
if (experimental_flags::MustClearApplicationGroupSandbox()) {
// Clear the Application group sandbox if requested. This operation take
// some time and will access the file system synchronously as the rest of
// the startup sequence requires it to be completed before continuing.
app_group::ClearAppGroupSandbox();
}
RegisterComponentsForUpdate();
[[PreviousSessionInfo sharedInstance] resetConnectedSceneSessionIDs];
_windowConfigurationRecorder = [[WindowConfigurationRecorder alloc] init];
}
// This initialization must only happen once there's at least one Chrome window
// open.
- (void)startUpAfterFirstWindowCreated {
// "Low priority" tasks
[_startupTasks registerForApplicationWillResignActiveNotification];
[self registerForOrientationChangeNotifications];
CustomizeUIAppearance();
// Schedule the prefs observer init first to ensure kMetricsReportingEnabled
// is synced before starting uploads.
[self schedulePrefObserverInitialization];
[self scheduleCrashReportUpload];
ios::provider::InstallOverrides();
[self scheduleLowPriorityStartupTasks];
// Now that everything is properly set up, run the tests.
tests_hook::RunTestsIfPresent();
self.screenshotMetricsRecorder = [[ScreenshotMetricsRecorder alloc] init];
[self.screenshotMetricsRecorder startRecordingMetrics];
}
- (PostCrashAction)postCrashAction {
if (self.appState.resumingFromSafeMode) {
return PostCrashAction::kShowSafeMode;
}
if (GetApplicationContext()->WasLastShutdownClean()) {
return PostCrashAction::kRestoreTabsCleanShutdown;
}
if (crash_util::GetFailedStartupAttemptCount() >= 2) {
return PostCrashAction::kShowNTPWithReturnToTab;
}
return PostCrashAction::kRestoreTabsUncleanShutdown;
}
#pragma mark - AppLifetimeObserver
- (void)applicationWillResignActive:(UIApplication*)application {
if (_appState.initStage < AppInitStage::kSafeMode) {
return;
}
// Reset the failed startup count as the user was able to background the app.
crash_util::ResetFailedStartupAttemptCount();
// Nothing to do if no profile has been fully yet loaded.
if (_highestProfileInitStageReached < ProfileInitStage::kPrepareUI) {
return;
}
// -applicationWillResignActive: is called by the OS when the application
// loses the focus (either because the last window is backgrounded or when
// the user switch to another application while in split screen mode on an
// iPad). Next time a windows become active, it should be considered as a
// "cold start" since the application was still running without the focus
// as opposed to a "warm start" which happens when the application was not
// running and did the whole startup sequence before activating the window.
_isColdStart = NO;
// Forward the event to all ProfileControllers.
for (const auto& pair : _profileControllers) {
ProfileController* controller = pair.second;
[controller applicationWillResignActive:application];
}
}
- (void)applicationWillTerminate:(UIApplication*)application {
// Avoid re-entrancy, and then mark the app as terminating.
CHECK(!_isTerminating);
_isTerminating = YES;
if (!_applicationInBackground) {
base::UmaHistogramBoolean(
"Stability.IOS.UTE.AppWillTerminateWasCalledInForeground", true);
}
[_appState.appCommandDispatcher prepareForShutdown];
// Cancel any in-flight distribution notification.
ios::provider::CancelAppDistributionNotifications();
// Forward the event to all ProfileControllers.
for (const auto& pair : _profileControllers) {
ProfileController* controller = pair.second;
[controller applicationWillTerminate:application];
}
[self stopChromeMain];
}
- (void)applicationDidEnterBackground:(UIApplication*)application
memoryHelper:(MemoryWarningHelper*)memoryHelper {
// Exit the application if backgrounding while in safe mode.
if (_appState.initStage == AppInitStage::kSafeMode) {
exit(0);
}
if (_applicationInBackground) {
return;
}
_applicationInBackground = YES;
crash_keys::SetCurrentlyInBackground(true);
// Reset `-startupHadExternalIntent` for all scenes in case external intents
// were triggered while the application was in the foreground.
for (SceneState* scene in _appState.connectedScenes) {
scene.startupHadExternalIntent = NO;
}
// The remainder of the cleanup is only valid if at least one of the profile
// has been initialized, so return early if this is not the case.
if (_highestProfileInitStageReached < ProfileInitStage::kPrepareUI) {
return;
}
[self expireFirstUserActionRecorder];
// Forward the event to all ProfileControllers.
for (const auto& pair : _profileControllers) {
ProfileController* controller = pair.second;
[controller applicationDidEnterBackground:application
memoryHelper:memoryHelper];
}
// Mark the startup as clean if it hasn't already been.
[_appState.deferredRunner runBlockNamed:kStartupResetAttemptCount];
// Update metrics.
[MetricsMediator logDateInUserDefaults];
[MetricsMediator
applicationDidEnterBackground:[memoryHelper
foregroundMemoryWarningCount]];
// Clear the memory warning flag since the application is in the background.
PreviousSessionInfo* sessionInfo = [PreviousSessionInfo sharedInstance];
[sessionInfo resetMemoryWarningFlag];
[sessionInfo stopRecordingMemoryFootprint];
GetApplicationContext()->OnAppEnterBackground();
}
- (void)applicationWillEnterForeground:(UIApplication*)application
memoryHelper:(MemoryWarningHelper*)memoryHelper {
// Invariant: the application has passed AppInitStage::kStart.
CHECK_GT(_appState.initStage, AppInitStage::kStart);
// Fully initialize the browser objects for the browser UI if it is not
// already the case. This is especially needed for scene startup.
if (_highestProfileInitStageReached < ProfileInitStage::kPrepareUI) {
// 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 (_appState.initStage <= AppInitStage::kSafeMode ||
!_applicationInBackground) {
return;
}
_applicationInBackground = NO;
crash_keys::SetCurrentlyInBackground(false);
GetApplicationContext()->OnAppEnterForeground();
// Update the state of metrics and crash reporting as the method of
// communication may have changed while the app was in the backgroumd.
[_metricsMediator updateMetricsStateBasedOnPrefsUserTriggered:NO];
[MetricsMediator
logLaunchMetricsWithStartupInformation:self
connectedScenes:_appState.connectedScenes];
// Send any feedback that might still be on temporary storage.
if (ios::provider::IsUserFeedbackSupported()) {
ios::provider::UploadAllPendingUserFeedback();
}
// Forward the event to all ProfileControllers.
for (const auto& pair : _profileControllers) {
ProfileController* controller = pair.second;
[controller applicationWillEnterForeground:application
memoryHelper:memoryHelper];
}
base::RecordAction(base::UserMetricsAction("MobileWillEnterForeground"));
// This will be a no-op if upload already started.
crash_helper::UploadCrashReports();
}
- (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.
ApplicationContext* applicationContext = GetApplicationContext();
if (!applicationContext) {
if (!_sceneSessionsToDiscard) {
_sceneSessionsToDiscard = [sceneSessions mutableCopy];
} else {
[_sceneSessionsToDiscard unionSet:sceneSessions];
}
return;
}
DCHECK_GE(_appState.initStage,
AppInitStage::kBrowserObjectsForBackgroundHandlers);
applicationContext->GetSystemIdentityManager()
->ApplicationDidDiscardSceneSessions(sceneSessions);
MarkSessionsAsDiscardedForAllProfiles(sceneSessions);
RecordDiscardSceneStillConnected(sceneSessions, _appState.connectedScenes);
crash_keys::SetConnectedScenesCount(_appState.connectedScenes.count);
}
#pragma mark Early launch
// 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 {
if (_userInteracted) {
return;
}
_userInteracted = YES;
[self saveLaunchDetailsToDefaults];
[_appState queueTransitionToNextInitStage];
}
// Saves the current launch details to user defaults.
- (void)saveLaunchDetailsToDefaults {
PreviousSessionInfo* sessionInfo = [PreviousSessionInfo sharedInstance];
// Reset the failure count on first launch, increment it on other launches.
if ([sessionInfo 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.
[sessionInfo beginRecordingCurrentSession];
}
#pragma mark - AppStateObserver
- (void)appState:(AppState*)appState sceneConnected:(SceneState*)sceneState {
if (appState.initStage < AppInitStage::kFinal) {
return;
}
ApplicationContext* applicationContext = GetApplicationContext();
ProfileManagerIOS* manager = applicationContext->GetProfileManager();
ProfileAttributesStorageIOS* storage = manager->GetProfileAttributesStorage();
PrefService* localState = applicationContext->GetLocalState();
[self attachProfileToScene:sceneState
profileManager:manager
attributesStorage:storage
localState:localState];
}
- (void)appState:(AppState*)appState
didTransitionFromInitStage:(AppInitStage)previousInitStage {
switch (appState.initStage) {
case AppInitStage::kStart:
[appState queueTransitionToNextInitStage];
break;
case AppInitStage::kBrowserBasic:
[self startUpBrowserBasicInitialization];
break;
case AppInitStage::kSafeMode:
[self addPostSafeModeAgents];
break;
case AppInitStage::kVariationsSeed:
break;
case AppInitStage::kBrowserObjectsForBackgroundHandlers:
[self startUpBrowserBackgroundInitialization];
break;
case AppInitStage::kEnterprise:
break;
case AppInitStage::kFinal:
[self attachProfilesToAllConnectedScenes];
break;
}
}
- (void)addPostSafeModeAgents {
[self.appState addAgent:[[EnterpriseAppAgent alloc] init]];
[self.appState addAgent:[[IncognitoUsageAppStateAgent alloc] init]];
#if BUILDFLAG(IOS_CREDENTIAL_PROVIDER_ENABLED)
[self.appState addAgent:[[CredentialProviderMigratorAppAgent alloc] init]];
#endif
[self.appState addAgent:[[DefaultBrowserBannerPromoAppAgent alloc] init]];
}
#pragma mark - ProfileStateObserver
- (void)profileState:(ProfileState*)profileState
willTransitionToInitStage:(ProfileInitStage)nextInitStage
fromInitStage:(ProfileInitStage)fromInitStage {
if (nextInitStage > _highestProfileInitStageReached) {
switch (nextInitStage) {
case ProfileInitStage::kStart:
NOTREACHED();
case ProfileInitStage::kLoadProfile:
case ProfileInitStage::kMigrateStorage:
case ProfileInitStage::kPurgeDiscardedSessionsData:
case ProfileInitStage::kProfileLoaded:
case ProfileInitStage::kPrepareUI:
// Nothing to do.
break;
case ProfileInitStage::kUIReady:
[self startUpBeforeFirstWindowCreated];
break;
case ProfileInitStage::kFirstRun:
case ProfileInitStage::kChoiceScreen:
case ProfileInitStage::kNormalUI:
case ProfileInitStage::kFinal:
// Nothing to do.
break;
}
if (fromInitStage == ProfileInitStage::kFirstRun) {
// Clear -isFirstRun once the first profile is done presenting the FRE
// (it should not be presented if e.g. the user sign-in with a managed
// profile on their first run of the app after completing the FRE on
// the personal profile).
_isFirstRun = NO;
}
}
}
- (void)profileState:(ProfileState*)profileState
didTransitionToInitStage:(ProfileInitStage)nextInitStage
fromInitStage:(ProfileInitStage)fromInitStage {
if (nextInitStage > _highestProfileInitStageReached) {
_highestProfileInitStageReached = nextInitStage;
switch (nextInitStage) {
case ProfileInitStage::kStart:
NOTREACHED();
case ProfileInitStage::kLoadProfile:
case ProfileInitStage::kMigrateStorage:
case ProfileInitStage::kPurgeDiscardedSessionsData:
// Nothing to do.
break;
case ProfileInitStage::kProfileLoaded:
[self scheduleRLZInitWithProfile:profileState.profile];
break;
case ProfileInitStage::kPrepareUI:
case ProfileInitStage::kUIReady:
// Nothing to do.
break;
case ProfileInitStage::kFirstRun:
// Stop forcing the orientation at the application level (if it was) as
// the ProfileAgent are now responsible for forcing the orientation.
_scopedForceOrientation.reset();
break;
case ProfileInitStage::kChoiceScreen:
case ProfileInitStage::kNormalUI:
case ProfileInitStage::kFinal:
// Nothing to do.
break;
}
}
// This should happen for all ProfileStage as it is responsible for
// removing self from the observers and for recording the lauch metrics
// which should wait until all SceneStates have been mapped to Profiles.
if (nextInitStage == ProfileInitStage::kFinal) {
[profileState removeObserver:self];
[self recordLaunchMetrics];
}
}
// Called when the first scene becomes active.
- (void)profileState:(ProfileState*)appState
firstSceneHasInitializedUI:(SceneState*)sceneState {
if (_firstWindowCreated) {
return;
}
_firstWindowCreated = YES;
[self startUpAfterFirstWindowCreated];
}
#pragma mark - Property implementation.
- (void)setAppState:(AppState*)appState {
DCHECK(!_appState);
_appState = appState;
[appState addObserver:self];
// If this is the first run, force the portrait orientation on iPhone at
// the application level (until at least one ProfileController reaches
// the kFirstRun stage).
//
// This is because the FRE is designed to only be displayed in portrait
// orientation on iPhone but the FRE happen as part of the profile init
// and waiting until then to force the orientation introduces unpleasant
// animation.
//
// There may be some unpleasant animation if other screen want to force
// the orientation (such as the search engine choice screen) as it may
// not be possible to determine here whether they will be run (e.g. if
// the depend on the state of a profile).
if (_isFirstRun) {
_scopedForceOrientation = ForcePortraitOrientationOnIphone(_appState);
}
// Create app state agents.
[appState addAgent:[[AppMetricsAppStateAgent alloc] init]];
[appState addAgent:[[SafeModeAppAgent alloc] init]];
[appState addAgent:[[VariationsAppStateAgent alloc] init]];
BackgroundRefreshAppAgent* refreshAgent =
[[BackgroundRefreshAppAgent alloc] init];
refreshAgent.startupInformation = self;
[_appState addAgent:refreshAgent];
// Register background refresh providers.
[refreshAgent addAppRefreshProvider:[[TestRefresher alloc]
initWithAppState:self.appState]];
// TODO(crbug.com/355142171): Remove the DiscoverFeedAppAgent.
[appState addAgent:[[DiscoverFeedAppAgent alloc] init]];
// Create the window accessibility agent only when multiple windows are
// possible.
if (base::ios::IsMultipleScenesSupported()) {
[appState
addAgent:[[WindowAccessibilityChangeNotifierAppAgent alloc] init]];
}
}
// TODO(crbug.com/341906612): Get rid of this method/property completely.
- (id<BrowserProviderInterface>)browserProviderInterfaceDoNotUse {
if (self.appState.foregroundActiveScene) {
return self.appState.foregroundActiveScene.browserProviderInterface;
}
NSArray<SceneState*>* connectedScenes = self.appState.connectedScenes;
return connectedScenes.count == 0
? nil
: connectedScenes[0].browserProviderInterface;
}
- (BOOL)isFirstLaunchAfterUpgrade {
return [[PreviousSessionInfo sharedInstance] isFirstSessionAfterUpgrade];
}
#pragma mark - StartupInformation implementation.
- (FirstUserActionRecorder*)firstUserActionRecorder {
return _firstUserActionRecorder.get();
}
- (void)resetFirstUserActionRecorder {
_firstUserActionRecorder.reset();
}
- (void)expireFirstUserActionRecorderAfterDelay:(NSTimeInterval)delay {
[self performSelector:@selector(expireFirstUserActionRecorder)
withObject:nil
afterDelay:delay];
}
- (void)activateFirstUserActionRecorderWithBackgroundTime:
(NSTimeInterval)backgroundTime {
base::TimeDelta delta = base::Seconds(backgroundTime);
_firstUserActionRecorder.reset(new FirstUserActionRecorder(delta));
}
- (void)stopChromeMain {
// _localStatePrefChangeRegistrar is observing the local state PrefService,
// which is owned indirectly by _chromeMain (through the ApplicationContext).
// Unregister the observer before the ApplicationContext is destroyed.
_localStatePrefChangeRegistrar.RemoveAll();
// Inform all the ProfileControllers that they will be destroyed in order
// to allow them to perform all required cleanup before the application
// terminates.
for (const auto& pair : _profileControllers) {
ProfileController* controller = pair.second;
[controller shutdown];
}
_profileControllers.clear();
// Cancel any pending deferred startup tasks (the application is shutting
// down, so there is no point in running them).
[_appState.deferredRunner cancelAllBlocks];
#if BUILDFLAG(FAST_APP_TERMINATE_ENABLED)
// _chromeMain.reset() is a blocking call that regularly causes
// applicationWillTerminate to fail after a 5s delay. Experiment with skipping
// this shutdown call. See: crbug.com/1328891
if (base::FeatureList::IsEnabled(kFastApplicationWillTerminate)) {
// Expected number of time the `closure` defined below needs to
// be called before it signal the semaphore. This corresponds to the
// number of services that needs to be waited for.
uint32_t expectedCount = 0;
// MetricsService doesn't depend on a profile.
metrics::MetricsService* metrics =
GetApplicationContext()->GetMetricsService();
if (metrics) {
expectedCount += 1;
}
const std::vector<ProfileIOS*> loadedProfiles =
GetApplicationContext()->GetProfileManager()->GetLoadedProfiles();
for (ProfileIOS* profile : loadedProfiles) {
expectedCount += 1;
if (profile->HasOffTheRecordProfile()) {
expectedCount += 1;
}
}
// `dispatch_semaphore_signal` is called only once when `closure` is called
// `expectedCount` times.
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
base::RepeatingClosure closure =
base::BarrierClosure(expectedCount, base::BindOnce(^{
dispatch_semaphore_signal(semaphore);
}));
for (ProfileIOS* profile : loadedProfiles) {
SessionRestorationServiceFactory::GetForProfile(profile)
->InvokeClosureWhenBackgroundProcessingDone(closure);
if (profile->HasOffTheRecordProfile()) {
ProfileIOS* otrBrowserState = profile->GetOffTheRecordProfile();
SessionRestorationServiceFactory::GetForProfile(otrBrowserState)
->InvokeClosureWhenBackgroundProcessingDone(closure);
}
}
if (metrics) {
metrics->Stop();
// MetricsService::Stop() depends on a committed local state, and does
// so asynchronously. To avoid losing metrics, this minimum wait is
// required. This will introduce a wait that will likely be the source
// of a number of watchdog kills, but it should still be fewer than the
// number of kills `_chromeMain.reset()` is responsible for.
GetApplicationContext()->GetLocalState()->CommitPendingWrite({}, closure);
}
dispatch_time_t dispatchTime =
dispatch_time(DISPATCH_TIME_NOW, 4 * NSEC_PER_SEC);
dispatch_semaphore_wait(semaphore, dispatchTime);
return;
}
#endif // BUILDFLAG(FAST_APP_TERMINATE_ENABLED)
#if BUILDFLAG(ENABLE_RLZ)
rlz::RLZTracker::CleanupRlz();
#endif
_chromeMain.reset();
}
#pragma mark - Startup tasks
- (void)sendQueuedFeedback {
if (ios::provider::IsUserFeedbackSupported()) {
[_appState.deferredRunner
enqueueBlockNamed:kSendQueuedFeedback
block:^{
ios::provider::UploadAllPendingUserFeedback();
}];
}
}
- (void)orientationDidChange:(NSNotification*)notification {
crash_keys::SetCurrentOrientation(GetInterfaceOrientation(),
[[UIDevice currentDevice] orientation]);
}
- (void)registerForOrientationChangeNotifications {
// Register device orientation. UI orientation will be registered by
// each window BVC. These two events may be triggered independently.
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(orientationDidChange:)
name:UIDeviceOrientationDidChangeNotification
object:nil];
}
- (void)schedulePrefObserverInitialization {
__weak MainController* weakSelf = self;
[_appState.deferredRunner enqueueBlockNamed:kStartupInitPrefObservers
block:^{
[weakSelf initializePrefObservers];
}];
}
- (void)initializePrefObservers {
// Track changes to local state prefs.
PrefService* localState = GetApplicationContext()->GetLocalState();
_localStatePrefChangeRegistrar.Init(localState);
_localStatePrefObserverBridge = std::make_unique<PrefObserverBridge>(self);
_localStatePrefObserverBridge->ObserveChangesForPreference(
metrics::prefs::kMetricsReportingEnabled,
&_localStatePrefChangeRegistrar);
// Calls the onPreferenceChanged function in case there was a change to the
// observed preferences before the observer bridge was set up. However, if the
// metrics reporting pref is still unset (has default value), then do not
// call. This likely means that the user is still on the welcome screen during
// the first run experience (FRE), and calling onPreferenceChanged here would
// clear the provisional client ID (in
// MetricsMediator::updateMetricsPrefsOnPermissionChange). The provisional
// client ID is crucial for field trial assignment consistency between the
// first session and follow-up sessions, and is promoted to be the real client
// ID if the user enables metrics reporting in the FRE. Otherwise, it is
// discarded, as would happen here if onPreferenceChanged was called while the
// user was still on the welcome screen and did yet enable/disable metrics
// reporting.
if (!localState->FindPreference(metrics::prefs::kMetricsReportingEnabled)
->IsDefaultValue()) {
[self onPreferenceChanged:metrics::prefs::kMetricsReportingEnabled];
}
}
- (void)scheduleAppDistributionPings {
__weak MainController* weakSelf = self;
[_appState.deferredRunner enqueueBlockNamed:kSendInstallPingIfNecessary
block:^{
[weakSelf pingDistributionServices];
}];
}
- (void)scheduleStartupAttemptReset {
[_appState.deferredRunner
enqueueBlockNamed:kStartupResetAttemptCount
block:^{
crash_util::ResetFailedStartupAttemptCount();
}];
}
- (void)scheduleCrashReportUpload {
[_appState.deferredRunner enqueueBlockNamed:kUploadCrashReports
block:^{
crash_helper::UploadCrashReports();
}];
}
- (void)scheduleMemoryDebuggingTools {
if (experimental_flags::IsMemoryDebuggingEnabled()) {
__weak MainController* weakSelf = self;
[_appState.deferredRunner
enqueueBlockNamed:kMemoryDebuggingToolsStartup
block:^{
[weakSelf initializedMemoryDebuggingTools];
}];
}
}
- (void)initializedMemoryDebuggingTools {
DCHECK(!_memoryDebuggerManager);
DCHECK(experimental_flags::IsMemoryDebuggingEnabled());
_memoryDebuggerManager = [[MemoryDebuggerManager alloc]
initWithView:self.appState.foregroundActiveScene.window
prefs:GetApplicationContext()->GetLocalState()];
}
// Schedule a call to `scheduleSaveFieldTrialValuesForExternals` for deferred
// execution. Externals can be extensions or 1st party apps.
- (void)scheduleSaveFieldTrialValuesForExternals {
__weak __typeof(self) weakSelf = self;
[_appState.deferredRunner
enqueueBlockNamed:kSaveFieldTrialValues
block:^{
[weakSelf saveFieldTrialValuesForExtensions];
[weakSelf saveFieldTrialValuesForGroupApp];
}];
}
// Some experiments value may be useful for first-party applications, so save
// the value in the shared application group.
- (void)saveFieldTrialValuesForGroupApp {
NSUserDefaults* sharedDefaults = app_group::GetCommonGroupUserDefaults();
NSNumber* supportsShowDefaultBrowserPromo = @YES;
NSMutableDictionary* capabilities = [[NSMutableDictionary alloc] init];
[capabilities setObject:supportsShowDefaultBrowserPromo
forKey:app_group::kChromeShowDefaultBrowserPromoCapability];
if (base::FeatureList::IsEnabled(kYoutubeIncognito) &&
base::FeatureList::IsEnabled(kChromeStartupParametersAsync)) {
[capabilities
setObject:@[ app_group::kYoutubeBundleID ]
forKey:app_group::kChromeSupportOpenLinksParametersFromCapability];
} else {
[capabilities
removeObjectForKey:app_group::
kChromeSupportOpenLinksParametersFromCapability];
}
[sharedDefaults setObject:capabilities
forKey:app_group::kChromeCapabilitiesPreference];
}
// Some extensions need the value of field trials but can't get them because the
// field trial infrastructure isn't in extensions. Save the necessary values to
// NSUserDefaults here.
- (void)saveFieldTrialValuesForExtensions {
NSUserDefaults* sharedDefaults = app_group::GetGroupUserDefaults();
// Add other field trial values here if they are needed by extensions.
// The general format is
// {
// name: {
// value: NSNumber bool,
// version: NSNumber int,
// }
// }
NSDictionary* fieldTrialValues = @{
kWidgetKitRefreshFiveMinutes : @{
kFieldTrialValueKey : @([[NSUserDefaults standardUserDefaults]
boolForKey:kWidgetKitRefreshFiveMinutes]),
kFieldTrialVersionKey : @1,
},
};
[sharedDefaults setObject:fieldTrialValues
forKey:app_group::kChromeExtensionFieldTrialPreference];
}
// Schedules a call to `logIfEnterpriseManagedDevice` for deferred
// execution.
- (void)scheduleEnterpriseManagedDeviceCheck {
__weak MainController* weakSelf = self;
[_appState.deferredRunner
enqueueBlockNamed:kEnterpriseManagedDeviceCheck
block:^{
[weakSelf logIfEnterpriseManagedDevice];
}];
}
- (void)logIfEnterpriseManagedDevice {
NSString* managedKey = @"com.apple.configuration.managed";
BOOL isManagedDevice = [[NSUserDefaults standardUserDefaults]
dictionaryForKey:managedKey] != nil;
base::UmaHistogramBoolean("EnterpriseCheck.IsManaged2", isManagedDevice);
}
- (void)startFreeMemoryMonitoring {
// No need for a post-task or a deferred initialisation as the memory
// monitoring already happens on a background sequence.
StartFreeMemoryMonitor();
}
- (void)scheduleLowPriorityStartupTasks {
[_startupTasks initializeOmaha];
// Deferred tasks.
[self scheduleMemoryDebuggingTools];
[self sendQueuedFeedback];
[self scheduleDeleteTempDownloadsDirectory];
[self scheduleDeleteTempPasswordsDirectory];
[self scheduleDeleteTempChooseFileDirectory];
[self scheduleLogSiriShortcuts];
[self scheduleStartupAttemptReset];
[self startFreeMemoryMonitoring];
[self scheduleAppDistributionPings];
[self scheduleSaveFieldTrialValuesForExternals];
[self scheduleEnterpriseManagedDeviceCheck];
[self scheduleMemoryExperimentation];
#if BUILDFLAG(IOS_ENABLE_SANDBOX_DUMP)
[self scheduleDumpDocumentsStatistics];
#endif // BUILDFLAG(IOS_ENABLE_SANDBOX_DUMP)
}
- (void)scheduleDeleteTempDownloadsDirectory {
[_appState.deferredRunner enqueueBlockNamed:kDeleteDownloads
block:^{
DeleteTempDownloadsDirectory();
}];
}
- (void)scheduleDeleteTempChooseFileDirectory {
[_appState.deferredRunner enqueueBlockNamed:kDeleteChooseFile
block:^{
DeleteTempChooseFileDirectory();
}];
}
- (void)scheduleDeleteTempPasswordsDirectory {
[_appState.deferredRunner
enqueueBlockNamed:kDeleteTempPasswords
block:^{
password_manager::DeletePasswordsDirectory();
}];
}
- (void)scheduleMemoryExperimentation {
[_appState.deferredRunner
enqueueBlockNamed:kMemoryExperimentation
block:^{
BeginMemoryExperimentationAfterDelay();
}];
}
- (void)scheduleLogSiriShortcuts {
__weak StartupTasks* startupTasks = _startupTasks;
[_appState.deferredRunner enqueueBlockNamed:kLogSiriShortcuts
block:^{
[startupTasks logSiriShortcuts];
}];
}
#if BUILDFLAG(IOS_ENABLE_SANDBOX_DUMP)
- (void)scheduleDumpDocumentsStatistics {
if ([[NSUserDefaults standardUserDefaults]
boolForKey:@"EnableDumpSandboxFileStatistics"]) {
// Reset the pref to prevent dumping statistics on every launch.
[[NSUserDefaults standardUserDefaults]
setBool:NO
forKey:@"EnableDumpSandboxFileStatistics"];
documents_statistics::DumpSandboxFileStatistics();
}
}
#endif // BUILDFLAG(IOS_ENABLE_SANDBOX_DUMP)
- (void)expireFirstUserActionRecorder {
// Clear out any scheduled calls to this method. For example, the app may have
// been backgrounded before the `kFirstUserActionTimeout` expired.
[NSObject cancelPreviousPerformRequestsWithTarget:self
selector:@selector
(expireFirstUserActionRecorder)
object:nil];
if (_firstUserActionRecorder) {
_firstUserActionRecorder->Expire();
_firstUserActionRecorder.reset();
}
}
- (void)crashIfRequested {
if (experimental_flags::IsStartupCrashEnabled()) {
// Flush out the value cached for crash_helper::SetEnabled().
[[NSUserDefaults standardUserDefaults] synchronize];
int* x = NULL;
*x = 0;
}
}
#pragma mark - Preferences Management
- (void)onPreferenceChanged:(const std::string&)preferenceName {
// Turn on or off metrics & crash reporting when either preference changes.
if (preferenceName == metrics::prefs::kMetricsReportingEnabled) {
[_metricsMediator updateMetricsStateBasedOnPrefsUserTriggered:YES];
}
}
#pragma mark - Helper methods.
- (void)pingDistributionServices {
const base::Time installDate =
base::Time::FromTimeT(GetApplicationContext()->GetLocalState()->GetInt64(
metrics::prefs::kInstallDate));
auto URLLoaderFactory = GetApplicationContext()->GetSharedURLLoaderFactory();
const bool isFirstRun = FirstRun::IsChromeFirstRun();
ios::provider::ScheduleAppDistributionNotifications(URLLoaderFactory,
isFirstRun);
ios::provider::InitializeFirebase(installDate, isFirstRun);
}
// Schedules the initialization of the RLZ library with a give profile. This
// method must only be called once. If this is the first run of the app, it
// will record the installation event.
- (void)scheduleRLZInitWithProfile:(ProfileIOS*)profile {
#if BUILDFLAG(ENABLE_RLZ)
DCHECK(profile);
PrefService* prefs = profile->GetPrefs();
// Negative delay means to send ping immediately after first recorded search.
const int pingDelay = prefs->GetInteger(FirstRun::GetPingDelayPrefName());
rlz::RLZTracker::SetRlzDelegate(std::make_unique<RLZTrackerDelegateImpl>());
rlz::RLZTracker::InitRlzDelayed(
FirstRun::IsChromeFirstRun(), pingDelay < 0,
base::Milliseconds(abs(pingDelay)),
RLZTrackerDelegateImpl::IsGoogleDefaultSearch(profile),
RLZTrackerDelegateImpl::IsGoogleHomepage(profile),
RLZTrackerDelegateImpl::IsGoogleInStartpages(profile));
#endif
}
// Records launch metrics when the application and all initial profiles have
// been fully initialised.
- (void)recordLaunchMetrics {
// Only record the metrics once, not after dynamically loading a new profile
// late (e.g. switching profile for a scene or opening a scene with another
// profile).
if (_launchMetricsRecorded) {
return;
}
// Check that all profiles have been fully initialized before recording the
// metrics (since before that, the tabs may have not been loaded yet and thus
// the metrics can't be properly recorded).
NSArray<SceneState*>* connectedScenes = self.appState.connectedScenes;
for (SceneState* sceneState in connectedScenes) {
if (sceneState.profileState.initStage < ProfileInitStage::kFinal) {
return;
}
}
// As all profiles have been fully initialised, the number of tabs and of
// connected scenes is now correct and can be reported.
[MetricsMediator logLaunchMetricsWithStartupInformation:self
connectedScenes:connectedScenes];
// Avoid reporting the metrics again.
_launchMetricsRecorded = YES;
}
#pragma mark - BlockingSceneCommands
- (void)activateBlockingScene:(UIScene*)requestingScene {
id<UIBlockerTarget> uiBlocker = self.appState.currentUIBlocker;
if (!uiBlocker) {
return;
}
[uiBlocker bringBlockerToFront:requestingScene];
}
#pragma mark - ChangeProfileCommands
- (void)changeProfile:(std::string_view)profileName
forScene:(SceneState*)sceneState
continuation:(ChangeProfileContinuation)continuation {
CHECK(AreSeparateProfilesForManagedAccountsEnabled());
CHECK_EQ(self.appState.initStage, AppInitStage::kFinal);
CHECK(sceneState);
CHECK([self.appState.connectedScenes containsObject:sceneState]);
ProfileManagerIOS* manager = GetApplicationContext()->GetProfileManager();
CHECK(manager->HasProfileWithName(profileName));
// Get the SceneDelegate from the SceneState.
UIWindowScene* scene = sceneState.scene;
SceneDelegate* sceneDelegate =
base::apple::ObjCCast<SceneDelegate>(scene.delegate);
CHECK(sceneDelegate);
UIWindow* window = sceneDelegate.window;
UIViewController* rootViewController = window.rootViewController;
ChangeProfileAnimator* animator =
[[ChangeProfileAnimator alloc] initWithViewController:rootViewController];
ProfileAttributesStorageIOS* storage = manager->GetProfileAttributesStorage();
const std::string sceneIdentifier =
base::SysNSStringToUTF8(sceneState.sceneSessionID);
// If the SceneState is not associated with the correct profile, then
// perform the necessary work to switch the profile used for the scene.
if (profileName != storage->GetProfileNameForSceneID(sceneIdentifier)) {
// The UI has to be destroyed, start animating.
[animator startAnimation];
// Set the mapping between profile and scene.
storage->SetProfileNameForSceneID(sceneIdentifier, profileName);
// Pretend the scene has been disconnected, then reconnect it.
const SceneActivationLevel savedLevel = sceneState.activationLevel;
const WindowActivityOrigin savedOrigin = sceneState.currentOrigin;
UISceneConnectionOptions* savedConnectionOptions =
sceneState.connectionOptions;
// Install a new root view controller before destroying the UI (since it
// does not support dismissing the root view controller after the Browser
// has been destroyed).
// TODO(crbug.com/376667510): SceneDelegate should manage the view
// controller and this should be unnecessary (in fact, it should be possible
// to install a temporary view controller to perform an animation).
LaunchScreenViewController* launchScreen =
[[LaunchScreenViewController alloc] init];
[sceneState setRootViewController:launchScreen makeKeyAndVisible:YES];
[sceneDelegate sceneDidDisconnect:scene]; // destroy the old SceneState
sceneState = sceneDelegate.sceneState; // recreate a new SceneState
sceneState.currentOrigin = savedOrigin;
sceneState.connectionOptions = savedConnectionOptions;
sceneState.activationLevel = SceneActivationLevelBackground;
sceneState.scene = scene;
// Reconnect the scene. This will attach a profile automatically based
// on the information stored in the ProfileAttributesStorageIOS.
[self appState:self.appState sceneConnected:sceneState];
DCHECK(sceneState.profileState);
while (sceneState.activationLevel < savedLevel) {
sceneState.activationLevel = static_cast<SceneActivationLevel>(
base::to_underlying(sceneState.activationLevel) + 1);
}
}
// Wait for the profile to complete its initialisation.
[animator waitForSceneState:sceneState
toInitReachStage:ProfileInitStage::kUIReady
continuation:std::move(continuation)];
}
- (void)deleteProfile:(std::string_view)profileName
completion:(ProfileDeletedCallback)completion {
CHECK(AreSeparateProfilesForManagedAccountsEnabled());
CHECK_EQ(self.appState.initStage, AppInitStage::kFinal);
ProfileManagerIOS* manager = GetApplicationContext()->GetProfileManager();
CHECK(manager->CanDeleteProfileWithName(profileName));
const std::string& personalProfile =
manager->GetProfileAttributesStorage()->GetPersonalProfileName();
DCHECK_GT(personalProfile.size(), 0u);
manager->MarkProfileForDeletion(profileName);
auto iter = _profileControllers.find(profileName);
NSArray<SceneState*>* scenes = nil;
if (iter != _profileControllers.end()) {
ProfileController* controller = iter->second;
scenes = controller.state.connectedScenes;
}
if (scenes.count == 0) {
// Either the Profile is not loaded or there is no scene connected, so
// there is no need to switch any scene to another Profile. Invoke the
// next step directly without waiting.
[self unloadProfileMarkedForDeletion:profileName
completion:std::move(completion)];
return;
}
__weak MainController* weakSelf = self;
base::RepeatingClosure closure = BarrierClosure(
scenes.count, base::BindOnce(&UnloadProfileMarkedForDeletion, weakSelf,
profileName, std::move(completion)));
for (SceneState* scene in scenes) {
[self changeProfile:personalProfile
forScene:scene
continuation:base::BindOnce(&DeleteProfileContinuation, closure)];
}
}
#pragma mark - Private
// Removes `profileName` in profile controller if needed and unload
// `profileName` which should have been marked for deletion.
- (void)unloadProfileMarkedForDeletion:(std::string_view)profileName
completion:(ProfileDeletedCallback)completion {
ProfileManagerIOS* manager = GetApplicationContext()->GetProfileManager();
CHECK(manager->IsProfileMarkedForDeletion(profileName));
if (auto iter = _profileControllers.find(profileName);
iter != _profileControllers.end()) {
ProfileController* controller = iter->second;
NSArray<SceneState*>* scenes = controller.state.connectedScenes;
DCHECK_EQ(scenes.count, 0u);
// Call -shutdown before deleting the object.
[controller shutdown];
_profileControllers.erase(iter);
}
manager->UnloadProfile(profileName);
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(completion), true));
}
// Attach a Profile to all connected scenes.
- (void)attachProfilesToAllConnectedScenes {
ApplicationContext* applicationContext = GetApplicationContext();
ProfileManagerIOS* manager = applicationContext->GetProfileManager();
ProfileAttributesStorageIOS* storage = manager->GetProfileAttributesStorage();
PrefService* localState = applicationContext->GetLocalState();
for (SceneState* sceneState in self.appState.connectedScenes) {
[self attachProfileToScene:sceneState
profileManager:manager
attributesStorage:storage
localState:localState];
}
}
// Attach a profile to `sceneState`.
- (void)attachProfileToScene:(SceneState*)sceneState
profileManager:(ProfileManagerIOS*)manager
attributesStorage:(ProfileAttributesStorageIOS*)storage
localState:(PrefService*)localState {
const std::string sceneID =
base::SysNSStringToUTF8(sceneState.sceneSessionID);
// Determine which profile to use. The logic is to take the first valid
// profile (i.e. the value is set and the profile is known) amongst the
// following value: the profile configured for the scene, the last used
// profile, the personal profile, or as a last resort a new profile.
enum class ProfileChoice {
kProfileForScene,
kLastUsedProfile,
kPersonalProfile,
kNewProfile,
};
static constexpr ProfileChoice kProfileChoices[] = {
ProfileChoice::kProfileForScene,
ProfileChoice::kLastUsedProfile,
ProfileChoice::kPersonalProfile,
ProfileChoice::kNewProfile,
};
std::string profileName;
bool changedProfileNameForScene = false;
for (ProfileChoice choice : kProfileChoices) {
switch (choice) {
case ProfileChoice::kProfileForScene:
profileName = storage->GetProfileNameForSceneID(sceneID);
changedProfileNameForScene = false;
break;
case ProfileChoice::kLastUsedProfile:
profileName = localState->GetString(prefs::kLastUsedProfile);
changedProfileNameForScene = true;
break;
case ProfileChoice::kPersonalProfile:
profileName = storage->GetPersonalProfileName();
changedProfileNameForScene = true;
break;
case ProfileChoice::kNewProfile:
profileName = manager->ReserveNewProfileName();
changedProfileNameForScene = true;
break;
}
// Pick the first valid profile name found.
if (storage->HasProfileWithName(profileName)) {
break;
}
}
// A valid profile name must have been picked (in the last resort a
// new profile name must have been generated).
CHECK(storage->HasProfileWithName(profileName));
// If the mapping has changed, store the mapping between the SceneID
// and the profile in the ProfileAttributesStorageIOS so that it is
// accessible the next time the window is open.
if (changedProfileNameForScene) {
storage->SetProfileNameForSceneID(sceneID, profileName);
}
// Update kLastUsedProfile, to ensure that new window will use the same
// profile (this also make sure that opening a second window will not
// create a profile for users upgrading from M-132 or ealier where the
// kLastUsedProfile was sometimes not correctly updated).
localState->SetString(prefs::kLastUsedProfile, profileName);
auto iterator = _profileControllers.find(profileName);
if (iterator == _profileControllers.end()) {
ProfileController* controller =
[[ProfileController alloc] initWithAppState:self.appState
metricsMediator:_metricsMediator];
[controller.state addObserver:self];
auto insertion_result =
_profileControllers.insert(std::make_pair(profileName, controller));
DCHECK(insertion_result.second);
iterator = insertion_result.first;
// Start loading the profile.
[controller loadProfileNamed:profileName usingManager:manager];
}
DCHECK(iterator != _profileControllers.end());
ProfileState* state = iterator->second.state;
DCHECK(state != nil);
// Attach the SceneState to the ProfileState.
[sceneState.controller setProfileState:state];
}
@end