| // Copyright 2019 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/browser/ui/main/scene_controller.h" |
| |
| #import <MaterialComponents/MaterialSnackbar.h> |
| |
| #import "base/feature_list.h" |
| #import "base/functional/callback_helpers.h" |
| #import "base/i18n/message_formatter.h" |
| #import "base/ios/ios_util.h" |
| #import "base/logging.h" |
| #import "base/metrics/histogram_functions.h" |
| #import "base/metrics/histogram_macros.h" |
| #import "base/metrics/user_metrics.h" |
| #import "base/metrics/user_metrics_action.h" |
| #import "base/strings/sys_string_conversions.h" |
| #import "base/time/time.h" |
| #import "components/breadcrumbs/core/breadcrumb_manager_keyed_service.h" |
| #import "components/breadcrumbs/core/breadcrumb_persistent_storage_manager.h" |
| #import "components/breadcrumbs/core/features.h" |
| #import "components/infobars/core/infobar_manager.h" |
| #import "components/prefs/pref_service.h" |
| #import "components/previous_session_info/previous_session_info.h" |
| #import "components/signin/public/base/signin_metrics.h" |
| #import "components/signin/public/base/signin_pref_names.h" |
| #import "components/signin/public/identity_manager/identity_manager.h" |
| #import "components/url_formatter/url_formatter.h" |
| #import "components/version_info/version_info.h" |
| #import "components/web_resource/web_resource_pref_names.h" |
| #import "ios/chrome/app/application_delegate/app_state.h" |
| #import "ios/chrome/app/application_delegate/startup_information.h" |
| #import "ios/chrome/app/application_delegate/url_opener.h" |
| #import "ios/chrome/app/application_delegate/url_opener_params.h" |
| #import "ios/chrome/app/application_delegate/user_activity_handler.h" |
| #import "ios/chrome/app/application_mode.h" |
| #import "ios/chrome/app/chrome_overlay_window.h" |
| #import "ios/chrome/app/deferred_initialization_runner.h" |
| #import "ios/chrome/app/tests_hook.h" |
| #import "ios/chrome/browser/application_context/application_context.h" |
| #import "ios/chrome/browser/browser_state/chrome_browser_state.h" |
| #import "ios/chrome/browser/browsing_data/browsing_data_remove_mask.h" |
| #import "ios/chrome/browser/browsing_data/browsing_data_remover.h" |
| #import "ios/chrome/browser/browsing_data/browsing_data_remover_factory.h" |
| #import "ios/chrome/browser/crash_report/breadcrumbs/breadcrumb_manager_browser_agent.h" |
| #import "ios/chrome/browser/crash_report/breadcrumbs/breadcrumb_manager_keyed_service_factory.h" |
| #import "ios/chrome/browser/crash_report/crash_keys_helper.h" |
| #import "ios/chrome/browser/crash_report/crash_loop_detection_util.h" |
| #import "ios/chrome/browser/crash_report/crash_report_helper.h" |
| #import "ios/chrome/browser/crash_report/crash_restore_helper.h" |
| #import "ios/chrome/browser/default_browser/promo_source.h" |
| #import "ios/chrome/browser/first_run/first_run.h" |
| #import "ios/chrome/browser/geolocation/geolocation_logger.h" |
| #import "ios/chrome/browser/infobars/infobar_manager_impl.h" |
| #import "ios/chrome/browser/mailto_handler/mailto_handler_service.h" |
| #import "ios/chrome/browser/mailto_handler/mailto_handler_service_factory.h" |
| #import "ios/chrome/browser/main/browser.h" |
| #import "ios/chrome/browser/main/browser_list.h" |
| #import "ios/chrome/browser/main/browser_list_factory.h" |
| #import "ios/chrome/browser/main/browser_util.h" |
| #import "ios/chrome/browser/ntp/features.h" |
| #import "ios/chrome/browser/ntp/new_tab_page_tab_helper.h" |
| #import "ios/chrome/browser/policy/cloud/user_policy_signin_service_factory.h" |
| #import "ios/chrome/browser/policy/policy_util.h" |
| #import "ios/chrome/browser/policy/policy_watcher_browser_agent.h" |
| #import "ios/chrome/browser/policy/policy_watcher_browser_agent_observer_bridge.h" |
| #import "ios/chrome/browser/prefs/pref_names.h" |
| #import "ios/chrome/browser/promos_manager/features.h" |
| #import "ios/chrome/browser/screenshot/screenshot_delegate.h" |
| #import "ios/chrome/browser/sessions/session_saving_scene_agent.h" |
| #import "ios/chrome/browser/signin/authentication_service.h" |
| #import "ios/chrome/browser/signin/authentication_service_factory.h" |
| #import "ios/chrome/browser/signin/capabilities_types.h" |
| #import "ios/chrome/browser/signin/chrome_account_manager_service.h" |
| #import "ios/chrome/browser/signin/chrome_account_manager_service_factory.h" |
| #import "ios/chrome/browser/signin/constants.h" |
| #import "ios/chrome/browser/signin/identity_manager_factory.h" |
| #import "ios/chrome/browser/signin/system_identity_manager.h" |
| #import "ios/chrome/browser/snapshots/snapshot_tab_helper.h" |
| #import "ios/chrome/browser/ui/app_store_rating/app_store_rating_scene_agent.h" |
| #import "ios/chrome/browser/ui/app_store_rating/features.h" |
| #import "ios/chrome/browser/ui/appearance/appearance_customization.h" |
| #import "ios/chrome/browser/ui/authentication/signed_in_accounts/signed_in_accounts_view_controller.h" |
| #import "ios/chrome/browser/ui/authentication/signin/signin_coordinator.h" |
| #import "ios/chrome/browser/ui/authentication/signin/signin_utils.h" |
| #import "ios/chrome/browser/ui/authentication/signin_notification_infobar_delegate.h" |
| #import "ios/chrome/browser/ui/browser_view/browser_view_controller.h" |
| #import "ios/chrome/browser/ui/commands/application_commands.h" |
| #import "ios/chrome/browser/ui/commands/browser_commands.h" |
| #import "ios/chrome/browser/ui/commands/browsing_data_commands.h" |
| #import "ios/chrome/browser/ui/commands/command_dispatcher.h" |
| #import "ios/chrome/browser/ui/commands/lens_commands.h" |
| #import "ios/chrome/browser/ui/commands/omnibox_commands.h" |
| #import "ios/chrome/browser/ui/commands/open_new_tab_command.h" |
| #import "ios/chrome/browser/ui/commands/policy_change_commands.h" |
| #import "ios/chrome/browser/ui/commands/qr_scanner_commands.h" |
| #import "ios/chrome/browser/ui/commands/show_signin_command.h" |
| #import "ios/chrome/browser/ui/commands/snackbar_commands.h" |
| #import "ios/chrome/browser/ui/default_promo/default_browser_promo_non_modal_scheduler.h" |
| #import "ios/chrome/browser/ui/default_promo/default_browser_utils.h" |
| #import "ios/chrome/browser/ui/first_run/orientation_limiting_navigation_controller.h" |
| #import "ios/chrome/browser/ui/history/history_coordinator.h" |
| #import "ios/chrome/browser/ui/incognito_interstitial/incognito_interstitial_coordinator.h" |
| #import "ios/chrome/browser/ui/incognito_interstitial/incognito_interstitial_coordinator_delegate.h" |
| #import "ios/chrome/browser/ui/incognito_reauth/incognito_reauth_scene_agent.h" |
| #import "ios/chrome/browser/ui/lens/lens_entrypoint.h" |
| #import "ios/chrome/browser/ui/main/browser_interface_provider.h" |
| #import "ios/chrome/browser/ui/main/browser_view_wrangler.h" |
| #import "ios/chrome/browser/ui/main/default_browser_scene_agent.h" |
| #import "ios/chrome/browser/ui/main/incognito_blocker_scene_agent.h" |
| #import "ios/chrome/browser/ui/main/layout_guide_scene_agent.h" |
| #import "ios/chrome/browser/ui/main/scene_ui_provider.h" |
| #import "ios/chrome/browser/ui/main/ui_blocker_scene_agent.h" |
| #import "ios/chrome/browser/ui/ntp/new_tab_page_feature.h" |
| #import "ios/chrome/browser/ui/policy/signin_policy_scene_agent.h" |
| #import "ios/chrome/browser/ui/policy/user_policy_scene_agent.h" |
| #import "ios/chrome/browser/ui/policy/user_policy_util.h" |
| #import "ios/chrome/browser/ui/promos_manager/promos_manager_scene_agent.h" |
| #import "ios/chrome/browser/ui/scoped_ui_blocker/scoped_ui_blocker.h" |
| #import "ios/chrome/browser/ui/settings/settings_navigation_controller.h" |
| #import "ios/chrome/browser/ui/start_surface/start_surface_features.h" |
| #import "ios/chrome/browser/ui/start_surface/start_surface_recent_tab_browser_agent.h" |
| #import "ios/chrome/browser/ui/start_surface/start_surface_scene_agent.h" |
| #import "ios/chrome/browser/ui/start_surface/start_surface_util.h" |
| #import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_grid_coordinator.h" |
| #import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_grid_coordinator_delegate.h" |
| #import "ios/chrome/browser/ui/thumb_strip/thumb_strip_feature.h" |
| #import "ios/chrome/browser/ui/ui_feature_flags.h" |
| #import "ios/chrome/browser/ui/util/top_view_controller.h" |
| #import "ios/chrome/browser/ui/util/uikit_ui_util.h" |
| #import "ios/chrome/browser/ui/whats_new/promo/whats_new_scene_agent.h" |
| #import "ios/chrome/browser/ui/whats_new/whats_new_util.h" |
| #import "ios/chrome/browser/url/chrome_url_constants.h" |
| #import "ios/chrome/browser/url/url_util.h" |
| #import "ios/chrome/browser/url_loading/scene_url_loading_service.h" |
| #import "ios/chrome/browser/url_loading/url_loading_browser_agent.h" |
| #import "ios/chrome/browser/url_loading/url_loading_params.h" |
| #import "ios/chrome/browser/web_state_list/session_metrics.h" |
| #import "ios/chrome/browser/web_state_list/tab_insertion_browser_agent.h" |
| #import "ios/chrome/browser/web_state_list/web_state_list.h" |
| #import "ios/chrome/browser/web_state_list/web_state_list_observer_bridge.h" |
| #import "ios/chrome/browser/web_state_list/web_state_opener.h" |
| #import "ios/chrome/browser/window_activities/window_activity_helpers.h" |
| #import "ios/chrome/common/ui/reauthentication/reauthentication_module.h" |
| #import "ios/chrome/grit/ios_strings.h" |
| #import "ios/public/provider/chrome/browser/ui_utils/ui_utils_api.h" |
| #import "ios/public/provider/chrome/browser/user_feedback/user_feedback_data.h" |
| #import "ios/web/public/navigation/navigation_item.h" |
| #import "ios/web/public/navigation/navigation_manager.h" |
| #import "ios/web/public/thread/web_task_traits.h" |
| #import "ios/web/public/thread/web_thread.h" |
| #import "ios/web/public/web_state.h" |
| #import "net/base/mac/url_conversions.h" |
| #import "ui/base/l10n/l10n_util.h" |
| |
| #if !defined(__has_feature) || !__has_feature(objc_arc) |
| #error "This file requires ARC support." |
| #endif |
| |
| namespace { |
| |
| // Feature to control whether Search Intents (Widgets, Application |
| // Shortcuts menu) forcibly open a new tab, rather than reusing an |
| // existing NTP. See http://crbug.com/1363375 for details. |
| BASE_FEATURE(kForceNewTabForIntentSearch, |
| "ForceNewTabForIntentSearch", |
| base::FEATURE_DISABLED_BY_DEFAULT); |
| |
| // A rough estimate of the expected duration of a view controller transition |
| // animation. It's used to temporarily disable mutally exclusive chrome |
| // commands that trigger a view controller presentation. |
| const int64_t kExpectedTransitionDurationInNanoSeconds = 0.2 * NSEC_PER_SEC; |
| |
| // Used to update the current BVC mode if a new tab is added while the tab |
| // switcher view is being dismissed. This is different than ApplicationMode in |
| // that it can be set to `NONE` when not in use. |
| enum class TabSwitcherDismissalMode { NONE, NORMAL, INCOGNITO }; |
| |
| // Key of the UMA IOS.MultiWindow.OpenInNewWindow histogram. |
| const char kMultiWindowOpenInNewWindowHistogram[] = |
| "IOS.MultiWindow.OpenInNewWindow"; |
| |
| // TODO(crbug.com/1244632): Use the Authentication Service sign-in status API |
| // instead of this when available. |
| bool IsSigninForcedByPolicy() { |
| BrowserSigninMode policy_mode = static_cast<BrowserSigninMode>( |
| GetApplicationContext()->GetLocalState()->GetInteger( |
| prefs::kBrowserSigninPolicy)); |
| return policy_mode == BrowserSigninMode::kForced; |
| } |
| |
| // Internally the NTP URL is about://newtab/. However, with |
| // `url::kAboutScheme`, there's no host value, only a path. Use this value for |
| // matching the NTP. |
| const char kAboutNewTabPath[] = "//newtab/"; |
| |
| bool IsNTPURL(const GURL& url) { |
| // `url` can be chrome://newtab/ or about://newtab/ depending on where `url` |
| // comes from (the VisibleURL chrome:// from a navigation item or the actual |
| // webView url about://). If the url is about://newtab/, there is no origin |
| // to match, so instead check the scheme and the path. |
| return url.DeprecatedGetOriginAsURL() == kChromeUINewTabURL || |
| (url.SchemeIs(url::kAboutScheme) && url.path() == kAboutNewTabPath); |
| } |
| |
| void InjectNTP(Browser* browser) { |
| // Don't inject an NTP for an empty web state list. |
| if (!browser->GetWebStateList()->count()) |
| return; |
| |
| // Don't inject an NTP on an NTP. |
| web::WebState* webState = browser->GetWebStateList()->GetActiveWebState(); |
| if (IsNTPURL(webState->GetVisibleURL())) |
| return; |
| |
| // Queue up start surface with active tab. |
| StartSurfaceRecentTabBrowserAgent* browser_agent = |
| StartSurfaceRecentTabBrowserAgent::FromBrowser(browser); |
| // This may be nil for an incognito browser. |
| if (browser_agent) |
| browser_agent->SaveMostRecentTab(); |
| |
| // Inject a live NTP. |
| web::WebState::CreateParams create_params(browser->GetBrowserState()); |
| std::unique_ptr<web::WebState> web_state = |
| web::WebState::Create(create_params); |
| std::vector<std::unique_ptr<web::NavigationItem>> items; |
| std::unique_ptr<web::NavigationItem> item(web::NavigationItem::Create()); |
| item->SetURL(GURL(kChromeUINewTabURL)); |
| items.push_back(std::move(item)); |
| web_state->GetNavigationManager()->Restore(0, std::move(items)); |
| NewTabPageTabHelper::CreateForWebState(web_state.get()); |
| NewTabPageTabHelper::FromWebState(web_state.get())->SetShowStartSurface(true); |
| int index = browser->GetWebStateList()->count(); |
| browser->GetWebStateList()->InsertWebState(index, std::move(web_state), |
| WebStateList::INSERT_ACTIVATE, |
| WebStateOpener()); |
| } |
| |
| } // namespace |
| |
| @interface SceneController () <AppStateObserver, |
| PolicyWatcherBrowserAgentObserving, |
| SettingsNavigationControllerDelegate, |
| SceneUIProvider, |
| SceneURLLoadingServiceDelegate, |
| TabGridCoordinatorDelegate, |
| WebStateListObserving, |
| IncognitoInterstitialCoordinatorDelegate> { |
| std::unique_ptr<WebStateListObserverBridge> _webStateListForwardingObserver; |
| std::unique_ptr<PolicyWatcherBrowserAgentObserverBridge> |
| _policyWatcherObserverBridge; |
| } |
| |
| // Navigation View controller for the settings. |
| @property(nonatomic, strong) |
| SettingsNavigationController* settingsNavigationController; |
| |
| // The scene level component for url loading. Is passed down to |
| // browser state level UrlLoadingService instances. |
| @property(nonatomic, assign) SceneUrlLoadingService* sceneURLLoadingService; |
| |
| // Coordinator for displaying history. |
| @property(nonatomic, strong) HistoryCoordinator* historyCoordinator; |
| |
| // Coordinates the creation of PDF screenshots with the window's content. |
| @property(nonatomic, strong) ScreenshotDelegate* screenshotDelegate; |
| |
| // The tab switcher command and the voice search commands can be sent by views |
| // that reside in a different UIWindow leading to the fact that the exclusive |
| // touch property will be ineffective and a command for processing both |
| // commands may be sent in the same run of the runloop leading to |
| // inconsistencies. Those two boolean indicate if one of those commands have |
| // been processed in the last 200ms in order to only allow processing one at |
| // a time. |
| // TODO(crbug.com/560296): Provide a general solution for handling mutually |
| // exclusive chrome commands sent at nearly the same time. |
| @property(nonatomic, assign) BOOL isProcessingTabSwitcherCommand; |
| @property(nonatomic, assign) BOOL isProcessingVoiceSearchCommand; |
| |
| // If not NONE, the current BVC should be switched to this BVC on completion |
| // of tab switcher dismissal. |
| @property(nonatomic, assign) |
| TabSwitcherDismissalMode modeToDisplayOnTabSwitcherDismissal; |
| |
| // A property to track whether the QR Scanner should be started upon tab |
| // switcher dismissal. It can only be YES if the QR Scanner experiment is |
| // enabled. |
| @property(nonatomic, readwrite) |
| TabOpeningPostOpeningAction NTPActionAfterTabSwitcherDismissal; |
| |
| // The main coordinator, lazily created the first time it is accessed. Manages |
| // the main view controller. This property should not be accessed before the |
| // browser has started up to the FOREGROUND stage. |
| @property(nonatomic, strong) TabGridCoordinator* mainCoordinator; |
| |
| // YES while activating a new browser (often leading to dismissing the tab |
| // switcher. |
| @property(nonatomic, assign) BOOL activatingBrowser; |
| |
| // Wrangler to handle BVC and tab model creation, access, and related logic. |
| // Implements features exposed from this object through the |
| // BrowserViewInformation protocol. |
| @property(nonatomic, strong) BrowserViewWrangler* browserViewWrangler; |
| // The coordinator used to control sign-in UI flows. Lazily created the first |
| // time it is accessed. Use -[startSigninCoordinatorWithCompletion:] to start |
| // the coordinator. |
| @property(nonatomic, strong) SigninCoordinator* signinCoordinator; |
| |
| // YES if the process of dismissing the sign-in prompt is from an external |
| // trigger and is currently ongoing. An external trigger isn't done from the |
| // signin prompt itself (i.e., tapping a button in the sign-in prompt that |
| // dismisses the prompt). For example, the -dismissModalDialogswithCompletion |
| // command is considered as an external trigger because it comes from something |
| // outside the sign-in prompt UI. |
| @property(nonatomic, assign) BOOL dismissingSigninPromptFromExternalTrigger; |
| |
| // The coordinator used to present the Incognito interstitial on Incognito |
| // third-party intents. Created in |
| // `showIncognitoInterstitialWithUrlLoadParams:dismissOmnibox:completion:` |
| // and destroyed in |
| // `closePresentedViews`. |
| @property(nonatomic, strong) |
| IncognitoInterstitialCoordinator* incognitoInterstitialCoordinator; |
| |
| @end |
| |
| @implementation SceneController |
| |
| @synthesize startupParameters = _startupParameters; |
| @synthesize startupParametersAreBeingHandled = |
| _startupParametersAreBeingHandled; |
| |
| - (instancetype)initWithSceneState:(SceneState*)sceneState { |
| self = [super init]; |
| if (self) { |
| _sceneState = sceneState; |
| [_sceneState addObserver:self]; |
| [_sceneState.appState addObserver:self]; |
| |
| _sceneURLLoadingService = new SceneUrlLoadingService(); |
| _sceneURLLoadingService->SetDelegate(self); |
| |
| _webStateListForwardingObserver = |
| std::make_unique<WebStateListObserverBridge>(self); |
| |
| _policyWatcherObserverBridge = |
| std::make_unique<PolicyWatcherBrowserAgentObserverBridge>(self); |
| |
| // Add agents. |
| [_sceneState addAgent:[[UIBlockerSceneAgent alloc] init]]; |
| [_sceneState addAgent:[[IncognitoBlockerSceneAgent alloc] init]]; |
| [_sceneState |
| addAgent:[[IncognitoReauthSceneAgent alloc] |
| initWithReauthModule:[[ReauthenticationModule alloc] |
| init]]]; |
| [_sceneState addAgent:[[StartSurfaceSceneAgent alloc] init]]; |
| [_sceneState addAgent:[[SessionSavingSceneAgent alloc] init]]; |
| [_sceneState addAgent:[[LayoutGuideSceneAgent alloc] init]]; |
| } |
| return self; |
| } |
| |
| #pragma mark - Setters and getters |
| |
| - (id<BrowsingDataCommands>)browsingDataCommandsHandler { |
| return HandlerForProtocol(self.sceneState.appState.appCommandDispatcher, |
| BrowsingDataCommands); |
| } |
| |
| - (TabGridCoordinator*)mainCoordinator { |
| if (!_mainCoordinator) { |
| // Lazily create the main coordinator. |
| TabGridCoordinator* tabGridCoordinator = [[TabGridCoordinator alloc] |
| initWithWindow:self.sceneState.window |
| applicationCommandEndpoint:self |
| browsingDataCommandEndpoint:self.browsingDataCommandsHandler |
| regularBrowser:self.mainInterface.browser |
| incognitoBrowser:self.incognitoInterface.browser]; |
| tabGridCoordinator.delegate = self; |
| _mainCoordinator = tabGridCoordinator; |
| tabGridCoordinator.regularThumbStripSupporting = self.mainInterface.bvc; |
| tabGridCoordinator.incognitoThumbStripSupporting = |
| self.incognitoInterface.bvc; |
| } |
| return _mainCoordinator; |
| } |
| |
| - (id<BrowserInterface>)mainInterface { |
| return self.browserViewWrangler.mainInterface; |
| } |
| |
| - (id<BrowserInterface>)currentInterface { |
| return self.browserViewWrangler.currentInterface; |
| } |
| |
| - (id<BrowserInterface>)incognitoInterface { |
| return self.browserViewWrangler.incognitoInterface; |
| } |
| |
| - (id<BrowserInterfaceProvider>)interfaceProvider { |
| return self.browserViewWrangler; |
| } |
| |
| - (void)setStartupParameters:(AppStartupParameters*)parameters { |
| _startupParameters = parameters; |
| self.startupParametersAreBeingHandled = NO; |
| BOOL shouldShowPromo = |
| self.sceneState.appState.shouldShowDefaultBrowserPromo && |
| (parameters.postOpeningAction == NO_ACTION); |
| self.sceneState.appState.shouldShowDefaultBrowserPromo = shouldShowPromo; |
| |
| if (parameters.openedViaFirstPartyScheme) { |
| DefaultBrowserSceneAgent* sceneAgent = |
| [DefaultBrowserSceneAgent agentFromScene:self.sceneState]; |
| [sceneAgent.nonModalScheduler logUserEnteredAppViaFirstPartyScheme]; |
| } |
| } |
| |
| - (BOOL)isPresentingSigninView { |
| return self.signinCoordinator != nil; |
| } |
| |
| - (BOOL)isTabGridVisible { |
| return self.mainCoordinator.isTabGridActive; |
| } |
| |
| #pragma mark - SceneStateObserver |
| |
| - (void)sceneState:(SceneState*)sceneState |
| transitionedToActivationLevel:(SceneActivationLevel)level { |
| AppState* appState = self.sceneState.appState; |
| [self transitionToSceneActivationLevel:level appInitStage:appState.initStage]; |
| } |
| |
| - (void)handleExternalIntents { |
| if (![self canHandleIntents]) { |
| return; |
| } |
| // Handle URL opening from |
| // `UIWindowSceneDelegate scene:willConnectToSession:options:`. |
| for (UIOpenURLContext* context in self.sceneState.connectionOptions |
| .URLContexts) { |
| URLOpenerParams* params = |
| [[URLOpenerParams alloc] initWithUIOpenURLContext:context]; |
| [self |
| openTabFromLaunchWithParams:params |
| startupInformation:self.sceneState.appState.startupInformation |
| appState:self.sceneState.appState]; |
| } |
| if (self.sceneState.connectionOptions.shortcutItem) { |
| [UserActivityHandler |
| performActionForShortcutItem:self.sceneState.connectionOptions |
| .shortcutItem |
| completionHandler:nil |
| tabOpener:self |
| connectionInformation:self |
| startupInformation:self.sceneState.appState.startupInformation |
| interfaceProvider:self.interfaceProvider |
| initStage:self.sceneState.appState.initStage]; |
| } |
| |
| // See if this scene launched as part of a multiwindow URL opening. |
| // If so, load that URL (this also creates a new tab to load the URL |
| // in). No other UI will show in this case. |
| NSUserActivity* activityWithCompletion; |
| for (NSUserActivity* activity in self.sceneState.connectionOptions |
| .userActivities) { |
| if (ActivityIsURLLoad(activity)) { |
| UrlLoadParams params = LoadParamsFromActivity(activity); |
| ApplicationMode mode = params.in_incognito ? ApplicationMode::INCOGNITO |
| : ApplicationMode::NORMAL; |
| [self openOrReuseTabInMode:mode |
| withUrlLoadParams:params |
| tabOpenedCompletion:nil]; |
| } else if (ActivityIsTabMove(activity)) { |
| [self handleTabMoveActivity:activity]; |
| } else if (!activityWithCompletion) { |
| // Completion involves user interaction. |
| // Only one can be triggered. |
| activityWithCompletion = activity; |
| } |
| } |
| if (activityWithCompletion) { |
| // This function is called when the scene is activated (or unblocked). |
| // Consider the scene as still not active at this point as the handling |
| // of startup parameters is not yet done (and will be later in this |
| // function). |
| [UserActivityHandler |
| continueUserActivity:activityWithCompletion |
| applicationIsActive:NO |
| tabOpener:self |
| connectionInformation:self |
| startupInformation:self.sceneState.appState.startupInformation |
| browserState:self.currentInterface.browserState |
| initStage:self.sceneState.appState.initStage]; |
| } |
| self.sceneState.connectionOptions = nil; |
| |
| if (self.startupParameters) { |
| if ([self isIncognitoForced]) { |
| [self.startupParameters |
| setUnexpectedMode:self.startupParameters.applicationMode == |
| ApplicationModeForTabOpening::NORMAL]; |
| // When only incognito mode is available. |
| [self.startupParameters |
| setApplicationMode:ApplicationModeForTabOpening::INCOGNITO]; |
| } else if ([self isIncognitoDisabled]) { |
| [self.startupParameters |
| setUnexpectedMode:self.startupParameters.applicationMode == |
| ApplicationModeForTabOpening::INCOGNITO]; |
| // When incognito mode is disabled. |
| [self.startupParameters |
| setApplicationMode:ApplicationModeForTabOpening::NORMAL]; |
| } |
| |
| [UserActivityHandler |
| handleStartupParametersWithTabOpener:self |
| connectionInformation:self |
| startupInformation:self.sceneState.appState |
| .startupInformation |
| browserState:self.currentInterface.browserState |
| initStage:self.sceneState.appState |
| .initStage]; |
| |
| // Show a toast if the browser is opened in an unexpected mode. |
| if (self.startupParameters.isUnexpectedMode) { |
| [self showToastWhenOpenExternalIntentInUnexpectedMode]; |
| } |
| } |
| } |
| |
| // Handles a tab move activity as part of an intent when launching a |
| // scene. This should only ever be an intent generated by Chrome. |
| - (void)handleTabMoveActivity:(NSUserActivity*)activity { |
| DCHECK(ActivityIsTabMove(activity)); |
| BOOL incognito = GetIncognitoFromTabMoveActivity(activity); |
| NSString* tabID = GetTabIDFromActivity(activity); |
| |
| id<BrowserInterface> interface = self.interfaceProvider.currentInterface; |
| |
| // It's expected that the current interface matches `incognito`. |
| DCHECK(interface.incognito == incognito); |
| |
| // Move the tab to the current interface's browser. |
| MoveTabToBrowser(tabID, interface.browser, /*destination_tab_index=*/0); |
| } |
| |
| - (void)recordWindowCreationForSceneState:(SceneState*)sceneState { |
| // Don't record window creation for single-window environments |
| if (!base::ios::IsMultipleScenesSupported()) |
| return; |
| |
| // Don't record restored window creation. |
| if (sceneState.currentOrigin == WindowActivityRestoredOrigin) |
| return; |
| |
| // If there's only one connected scene, and it isn't being restored, this |
| // must be the initial app launch with scenes, so don't record the window |
| // creation. |
| if (sceneState.appState.connectedScenes.count <= 1) |
| return; |
| |
| base::UmaHistogramEnumeration(kMultiWindowOpenInNewWindowHistogram, |
| sceneState.currentOrigin); |
| } |
| |
| - (BOOL)presentSignInAccountsViewControllerIfNecessary { |
| ChromeBrowserState* browserState = self.currentInterface.browserState; |
| DCHECK(browserState); |
| if ([SignedInAccountsViewController |
| shouldBePresentedForBrowserState:browserState]) { |
| [self presentSignedInAccountsViewControllerForBrowserState:browserState]; |
| return YES; |
| } |
| return NO; |
| } |
| |
| - (void)sceneState:(SceneState*)sceneState |
| hasPendingURLs:(NSSet<UIOpenURLContext*>*)URLContexts { |
| DCHECK(URLContexts); |
| // It is necessary to reset the URLContextsToOpen after opening them. |
| // Handle the opening asynchronously to avoid interfering with potential |
| // other observers. |
| dispatch_async(dispatch_get_main_queue(), ^{ |
| [self openURLContexts:sceneState.URLContextsToOpen]; |
| self.sceneState.URLContextsToOpen = nil; |
| }); |
| } |
| |
| - (void)performActionForShortcutItem:(UIApplicationShortcutItem*)shortcutItem |
| completionHandler: |
| (void (^)(BOOL succeeded))completionHandler { |
| if (self.sceneState.appState.initStage <= InitStageNormalUI || |
| !self.currentInterface.browserState) { |
| // Don't handle the intent if the browser UI objects aren't yet initialized. |
| // This is the case when the app is in safe mode or may be the case when the |
| // app is going through an odd sequence of lifecyle events (shouldn't happen |
| // but happens somehow), see crbug.com/1211006 for more details. |
| return; |
| } |
| |
| self.sceneState.startupHadExternalIntent = YES; |
| |
| // Perform the action in incognito when only incognito mode is available. |
| if ([self isIncognitoForced]) { |
| [self.startupParameters |
| setApplicationMode:ApplicationModeForTabOpening::INCOGNITO]; |
| } |
| |
| [UserActivityHandler |
| performActionForShortcutItem:shortcutItem |
| completionHandler:completionHandler |
| tabOpener:self |
| connectionInformation:self |
| startupInformation:self.sceneState.appState.startupInformation |
| interfaceProvider:self.interfaceProvider |
| initStage:self.sceneState.appState.initStage]; |
| } |
| |
| - (void)sceneState:(SceneState*)sceneState |
| receivedUserActivity:(NSUserActivity*)userActivity { |
| if (!userActivity) { |
| return; |
| } |
| |
| if (self.sceneState.appState.initStage <= InitStageNormalUI || |
| !self.currentInterface.browserState) { |
| // Don't handle the intent if the browser UI objects aren't yet initialized. |
| // This is the case when the app is in safe mode or may be the case when the |
| // app is going through an odd sequence of lifecyle events (shouldn't happen |
| // but happens somehow), see crbug.com/1211006 for more details. |
| return; |
| } |
| |
| BOOL sceneIsActive = [self canHandleIntents]; |
| self.sceneState.startupHadExternalIntent = YES; |
| |
| PrefService* prefs = self.currentInterface.browserState->GetPrefs(); |
| if (IsIncognitoPolicyApplied(prefs) && |
| ![UserActivityHandler canProceedWithUserActivity:userActivity |
| prefService:prefs]) { |
| // If users request opening url in a unavailable mode, don't open the url |
| // but show a toast. |
| [self showToastWhenOpenExternalIntentInUnexpectedMode]; |
| } else { |
| [UserActivityHandler |
| continueUserActivity:userActivity |
| applicationIsActive:sceneIsActive |
| tabOpener:self |
| connectionInformation:self |
| startupInformation:self.sceneState.appState.startupInformation |
| browserState:self.currentInterface.browserState |
| initStage:self.sceneState.appState.initStage]; |
| } |
| |
| if (sceneIsActive) { |
| // It is necessary to reset the pendingUserActivity after handling it. |
| // Handle the reset asynchronously to avoid interfering with other |
| // observers. |
| dispatch_async(dispatch_get_main_queue(), ^{ |
| self.sceneState.pendingUserActivity = nil; |
| }); |
| } |
| } |
| |
| - (void)sceneStateDidHideModalOverlay:(SceneState*)sceneState { |
| [self handleExternalIntents]; |
| } |
| |
| #pragma mark - AppStateObserver |
| |
| - (void)appState:(AppState*)appState |
| didTransitionFromInitStage:(InitStage)previousInitStage { |
| [self transitionToSceneActivationLevel:self.sceneState.activationLevel |
| appInitStage:appState.initStage]; |
| } |
| |
| #pragma mark - private |
| |
| // Shows the Incognito interstitial on top of `activeViewController`. |
| // Assumes the Incognito interstitial coordinator is currently not instantiated. |
| // Runs `completion` once the Incognito interstitial is presented. |
| - (void)showIncognitoInterstitialWithUrlLoadParams: |
| (const UrlLoadParams&)urlLoadParams { |
| DCHECK(self.incognitoInterstitialCoordinator == nil); |
| self.incognitoInterstitialCoordinator = |
| [[IncognitoInterstitialCoordinator alloc] |
| initWithBaseViewController:self.activeViewController |
| browser:self.currentInterface.browser]; |
| self.incognitoInterstitialCoordinator.delegate = self; |
| self.incognitoInterstitialCoordinator.tabOpener = self; |
| self.incognitoInterstitialCoordinator.urlLoadParams = urlLoadParams; |
| [self.incognitoInterstitialCoordinator start]; |
| } |
| |
| // A sink for appState:didTransitionFromInitStage: and |
| // sceneState:transitionedToActivationLevel: events. Discussion: the scene |
| // controller cares both about the app and the scene init stages. This method is |
| // called from both observer callbacks and allows to handle all the transitions |
| // in one place. |
| - (void)transitionToSceneActivationLevel:(SceneActivationLevel)level |
| appInitStage:(InitStage)appInitStage { |
| if (appInitStage < InitStageNormalUI) { |
| // Nothing per-scene should happen before the app completes the global |
| // setup, like executing Safe mode, or creating the main BrowserState. |
| return; |
| } |
| |
| BOOL initializingUIInColdStart = |
| level > SceneActivationLevelBackground && !self.sceneState.UIEnabled; |
| if (initializingUIInColdStart) { |
| [self initializeUI]; |
| // Add the scene to the list of connected scene, to restore in case of |
| // crashes. |
| [[PreviousSessionInfo sharedInstance] |
| addSceneSessionID:self.sceneState.sceneSessionID]; |
| } |
| |
| // When the scene transitions to inactive (such as when it's being shown in |
| // the OS app-switcher), update the title for display on iPadOS. |
| if (level == SceneActivationLevelForegroundInactive) { |
| self.sceneState.scene.title = [self displayTitleForAppSwitcher]; |
| } |
| |
| if (level == SceneActivationLevelForegroundActive && |
| appInitStage == InitStageFinal) { |
| [self tryPresentSigninModalUI]; |
| |
| [self handleExternalIntents]; |
| |
| if (!initializingUIInColdStart && self.mainCoordinator.isTabGridActive && |
| [self shouldOpenNTPTabOnActivationOfBrowser:self.currentInterface |
| .browser]) { |
| DCHECK(!self.activatingBrowser); |
| [self beginActivatingBrowser:self.mainInterface.browser |
| dismissTabSwitcher:YES |
| focusOmnibox:NO]; |
| |
| OpenNewTabCommand* command = [OpenNewTabCommand commandWithIncognito:NO]; |
| command.userInitiated = NO; |
| Browser* browser = self.currentInterface.browser; |
| id<ApplicationCommands> applicationHandler = HandlerForProtocol( |
| browser->GetCommandDispatcher(), ApplicationCommands); |
| [applicationHandler openURLInNewTab:command]; |
| [self finishActivatingBrowserDismissingTabSwitcher:YES]; |
| } |
| } |
| |
| [self recordWindowCreationForSceneState:self.sceneState]; |
| |
| if (self.sceneState.UIEnabled && level == SceneActivationLevelUnattached) { |
| if (base::ios::IsMultipleScenesSupported()) { |
| // If Multiple scenes are not supported, the session shouldn't be |
| // removed as it can be used for normal restoration. |
| [[PreviousSessionInfo sharedInstance] |
| removeSceneSessionID:self.sceneState.sceneSessionID]; |
| } |
| [self teardownUI]; |
| } |
| } |
| |
| // Displays either the sign-in upgrade promo if it is eligible or the list |
| // of signed-in accounts if the user has recently updated their accounts. |
| - (void)tryPresentSigninModalUI { |
| if ([self presentSignInAccountsViewControllerIfNecessary]) { |
| // The user is already signed-in, so do not display the sign-in promo. |
| return; |
| } |
| |
| // If the sign-in promo is not eligible, return immediately. |
| if (![self shouldPresentSigninUpgradePromo]) { |
| return; |
| } |
| |
| ChromeAccountManagerService* accountManagerService = |
| ChromeAccountManagerServiceFactory::GetForBrowserState( |
| self.mainInterface.browser->GetBrowserState()); |
| // The sign-in promo should not be presented if there is no identities. |
| id<SystemIdentity> defaultIdentity = |
| accountManagerService->GetDefaultIdentity(); |
| DCHECK(defaultIdentity); |
| |
| using CapabilityResult = SystemIdentityManager::CapabilityResult; |
| SystemIdentityManager* system_identity_manager = |
| GetApplicationContext()->GetSystemIdentityManager(); |
| |
| // Asynchronously checks whether the default identity can display extended |
| // sync promos and displays the sign-in promo if possible. |
| __weak SceneController* weakSelf = self; |
| base::Time fetch_start = base::Time::Now(); |
| system_identity_manager->CanOfferExtendedSyncPromos( |
| defaultIdentity, base::BindOnce(^(CapabilityResult result) { |
| base::TimeDelta fetch_duration = (base::Time::Now() - fetch_start); |
| base::UmaHistogramTimes( |
| "Signin.AccountCapabilities.GetFromSystemLibraryDuration." |
| "SigninUpgradePromo", |
| fetch_duration); |
| if (fetch_duration > signin::GetWaitThresholdForCapabilities() || |
| result != CapabilityResult::kTrue) { |
| return; |
| } |
| [weakSelf presentSigninUpgradePromo]; |
| })); |
| } |
| |
| - (void)initializeUI { |
| if (self.sceneState.UIEnabled) { |
| return; |
| } |
| |
| [self startUpChromeUI]; |
| self.sceneState.UIEnabled = YES; |
| } |
| |
| // Returns YES if restore prompt can be shown. |
| // The restore prompt shouldn't appear if its appearance may be in conflict |
| // with the expected behavior by the user. |
| // The following cases will not show restore prompt: |
| // 1- New tab / Navigation startup parameters are specified. |
| // 2- Load URL User activity is queud. |
| // 3- Move tab user activity is queued. |
| // 4- Only incognito mode is available. |
| // In these cases if a restore prompt was shown, it may be dismissed immediately |
| // and the user will not have a chance to restore the session. |
| - (BOOL)shouldShowRestorePrompt { |
| BOOL shouldShow = !self.startupParameters && ![self isIncognitoForced]; |
| if (shouldShow) { |
| for (NSUserActivity* activity in self.sceneState.connectionOptions |
| .userActivities) { |
| if (ActivityIsTabMove(activity) || ActivityIsURLLoad(activity)) { |
| shouldShow = NO; |
| break; |
| } |
| } |
| } |
| return shouldShow; |
| } |
| |
| // Starts up a single chrome window and its UI. |
| - (void)startUpChromeUI { |
| DCHECK(!self.browserViewWrangler); |
| DCHECK(self.sceneURLLoadingService); |
| DCHECK(self.sceneState.appState.mainBrowserState); |
| |
| self.browserViewWrangler = [[BrowserViewWrangler alloc] |
| initWithBrowserState:self.sceneState.appState.mainBrowserState |
| sceneState:self.sceneState |
| applicationCommandEndpoint:self |
| browsingDataCommandEndpoint:self.browsingDataCommandsHandler]; |
| |
| // Ensure the main browser is created. |
| Browser* mainBrowser = [self.browserViewWrangler createMainBrowser]; |
| CommandDispatcher* mainCommandDispatcher = |
| mainBrowser->GetCommandDispatcher(); |
| |
| // Add scene agents that require CommandDispatcher. |
| DefaultBrowserSceneAgent* defaultBrowserAgent = |
| [[DefaultBrowserSceneAgent alloc] |
| initWithCommandDispatcher:mainCommandDispatcher]; |
| defaultBrowserAgent.nonModalScheduler.browser = mainBrowser; |
| [self.sceneState addAgent:defaultBrowserAgent]; |
| if (defaultBrowserAgent.nonModalScheduler) { |
| [self.sceneState addObserver:defaultBrowserAgent.nonModalScheduler]; |
| } |
| |
| // Create and start the BVC. |
| [self.browserViewWrangler createMainCoordinatorAndInterface]; |
| |
| id<ApplicationCommands> applicationCommandsHandler = |
| HandlerForProtocol(mainCommandDispatcher, ApplicationCommands); |
| id<PolicyChangeCommands> policyChangeCommandsHandler = |
| HandlerForProtocol(mainCommandDispatcher, PolicyChangeCommands); |
| |
| [self.sceneState |
| addAgent:[[SigninPolicySceneAgent alloc] |
| initWithSceneUIProvider:self |
| applicationCommandsHandler:applicationCommandsHandler |
| policyChangeCommandsHandler:policyChangeCommandsHandler]]; |
| |
| ChromeBrowserState* browserState = self.sceneState.appState.mainBrowserState; |
| PrefService* prefService = browserState->GetPrefs(); |
| AuthenticationService* authService = |
| AuthenticationServiceFactory::GetForBrowserState(browserState); |
| |
| if (IsUserPolicyNotificationNeeded(authService, prefService)) { |
| policy::UserPolicySigninService* userPolicyService = |
| policy::UserPolicySigninServiceFactory::GetForBrowserState( |
| browserState); |
| [self.sceneState |
| addAgent:[[UserPolicySceneAgent alloc] |
| initWithSceneUIProvider:self |
| authService:authService |
| applicationCommandsHandler:applicationCommandsHandler |
| prefService:prefService |
| mainBrowser:mainBrowser |
| policyService:userPolicyService]]; |
| } |
| |
| // Now that the main browser's command dispatcher is created and the newly |
| // started UI coordinators have registered with it, inject it into the |
| // PolicyWatcherBrowserAgent so it can start monitoring UI-impacting policy |
| // changes. |
| PolicyWatcherBrowserAgent* policyWatcherAgent = |
| PolicyWatcherBrowserAgent::FromBrowser(self.mainInterface.browser); |
| policyWatcherAgent->AddObserver(_policyWatcherObserverBridge.get()); |
| policyWatcherAgent->Initialize(policyChangeCommandsHandler); |
| |
| self.screenshotDelegate = [[ScreenshotDelegate alloc] |
| initWithBrowserInterfaceProvider:self.browserViewWrangler]; |
| [self.sceneState.scene.screenshotService setDelegate:self.screenshotDelegate]; |
| |
| // Only create the restoration helper if the session with the current session |
| // id was backed up successfully. |
| if (self.sceneState.appState.sessionRestorationRequired && |
| !self.sceneState.appState.startupInformation.isFirstRun) { |
| if ([CrashRestoreHelper |
| isBackedUpSessionID:self.sceneState.sceneSessionID |
| browserState:mainBrowser->GetBrowserState()]) { |
| self.sceneState.appState.startupInformation.restoreHelper = |
| [[CrashRestoreHelper alloc] initWithBrowser:mainBrowser]; |
| } |
| } |
| |
| // If the application crashed, clear incognito state. |
| if (self.sceneState.appState.postCrashAction == |
| PostCrashAction::kStashTabsAndShowNTP) |
| [self clearIOSSpecificIncognitoData]; |
| |
| [self createInitialUI:[self initialUIMode]]; |
| |
| if ([self shouldShowRestorePrompt]) { |
| [self.sceneState.appState.startupInformation |
| .restoreHelper showRestorePrompt]; |
| self.sceneState.appState.startupInformation.restoreHelper = nil; |
| } |
| |
| // Make sure the geolocation controller is created to observe permission |
| // events. |
| [GeolocationLogger sharedInstance]; |
| |
| if (IsFullscreenPromosManagerEnabled()) |
| [self.sceneState |
| addAgent:[[PromosManagerSceneAgent alloc] |
| initWithCommandDispatcher:mainCommandDispatcher]]; |
| if (IsAppStoreRatingEnabled()) { |
| [self.sceneState |
| addAgent:[[AppStoreRatingSceneAgent alloc] |
| initWithPromosManager:GetApplicationContext() |
| ->GetPromosManager()]]; |
| } |
| |
| if (IsWhatsNewEnabled()) { |
| [self.sceneState |
| addAgent:[[WhatsNewSceneAgent alloc] |
| initWithPromosManager:GetApplicationContext() |
| ->GetPromosManager()]]; |
| } |
| } |
| |
| // Determines the mode (normal or incognito) the initial UI should be in. |
| - (ApplicationMode)initialUIMode { |
| // When only incognito mode is available. |
| if ([self isIncognitoForced]) { |
| return ApplicationMode::INCOGNITO; |
| } |
| |
| // When only incognito mode is disabled. |
| if ([self isIncognitoDisabled]) { |
| return ApplicationMode::NORMAL; |
| } |
| |
| // Check if the UI is being created from an intent; if it is, open in the |
| // correct mode for that activity. Because all activities must be in the same |
| // mode, as soon as any activity reports being in incognito, switch to that |
| // mode. |
| for (NSUserActivity* activity in self.sceneState.connectionOptions |
| .userActivities) { |
| if (ActivityIsTabMove(activity)) { |
| return GetIncognitoFromTabMoveActivity(activity) |
| ? ApplicationMode::INCOGNITO |
| : ApplicationMode::NORMAL; |
| } |
| } |
| |
| // If the app crashed, always launch in normal mode. |
| if (self.sceneState.appState.postCrashAction == |
| PostCrashAction::kStashTabsAndShowNTP) { |
| return ApplicationMode::NORMAL; |
| } |
| |
| // Launch in the mode that matches the state of the scene when the application |
| // was terminated. If the scene was showing the incognito UI, but there are |
| // no incognito tabs open (e.g. the tab switcher was active and user closed |
| // the last tab), then instead show the regular UI. |
| |
| if (self.sceneState.incognitoContentVisible && |
| !self.interfaceProvider.incognitoInterface.browser->GetWebStateList() |
| ->empty()) { |
| return ApplicationMode::INCOGNITO; |
| } |
| |
| // In all other cases, default to normal mode. |
| return ApplicationMode::NORMAL; |
| } |
| |
| // Creates and displays the initial UI in `launchMode`, performing other |
| // setup and configuration as needed. |
| - (void)createInitialUI:(ApplicationMode)launchMode { |
| DCHECK(self.sceneState.appState.mainBrowserState); |
| |
| // Set the Scene application URL loader on the URL loading browser interface |
| // for the regular and incognito interfaces. This will lazily instantiate the |
| // incognito interface if it isn't already created. |
| UrlLoadingBrowserAgent::FromBrowser(self.mainInterface.browser) |
| ->SetSceneService(self.sceneURLLoadingService); |
| UrlLoadingBrowserAgent::FromBrowser(self.incognitoInterface.browser) |
| ->SetSceneService(self.sceneURLLoadingService); |
| // Observe the web state lists for both browsers. |
| self.mainInterface.browser->GetWebStateList()->AddObserver( |
| _webStateListForwardingObserver.get()); |
| self.incognitoInterface.browser->GetWebStateList()->AddObserver( |
| _webStateListForwardingObserver.get()); |
| |
| // Enables UI initializations to query the keyWindow's size. |
| [self.sceneState.window makeKeyAndVisible]; |
| |
| // Lazy init of mainCoordinator. |
| [self.mainCoordinator start]; |
| |
| [self.mainCoordinator setActivePage:[self activePage]]; |
| |
| if (!self.sceneState.appState.startupInformation.isFirstRun) { |
| [self reconcileEulaAsAccepted]; |
| } |
| |
| Browser* browser = (launchMode == ApplicationMode::INCOGNITO) |
| ? self.incognitoInterface.browser |
| : self.mainInterface.browser; |
| |
| // Inject a NTP before setting the interface, which will trigger a load of |
| // the current webState. |
| if (self.sceneState.appState.postCrashAction == |
| PostCrashAction::kShowNTPWithReturnToTab) { |
| DCHECK(base::FeatureList::IsEnabled(kRemoveCrashInfobar)); |
| InjectNTP(browser); |
| } |
| |
| if (launchMode == ApplicationMode::INCOGNITO) { |
| [self setCurrentInterfaceForMode:ApplicationMode::INCOGNITO]; |
| } else { |
| [self setCurrentInterfaceForMode:ApplicationMode::NORMAL]; |
| } |
| |
| // Figure out what UI to show initially. |
| |
| if (self.mainCoordinator.isTabGridActive) { |
| DCHECK(!self.activatingBrowser); |
| [self beginActivatingBrowser:self.mainInterface.browser |
| dismissTabSwitcher:YES |
| focusOmnibox:NO]; |
| [self finishActivatingBrowserDismissingTabSwitcher:YES]; |
| } |
| |
| // If this web state list should have an NTP created when it activates, then |
| // create that tab. |
| if ([self shouldOpenNTPTabOnActivationOfBrowser:browser]) { |
| OpenNewTabCommand* command = [OpenNewTabCommand |
| commandWithIncognito:self.currentInterface.incognito]; |
| command.userInitiated = NO; |
| Browser* currentBrowser = self.currentInterface.browser; |
| id<ApplicationCommands> applicationHandler = HandlerForProtocol( |
| currentBrowser->GetCommandDispatcher(), ApplicationCommands); |
| [applicationHandler openURLInNewTab:command]; |
| } |
| [self maybeShowDefaultBrowserPromo:self.mainInterface.browser]; |
| } |
| |
| // `YES` if Chrome is not the default browser, the app did not crash recently, |
| // the user never saw the promo UI and is in the correct experiment groups. |
| - (BOOL)potentiallyInterestedUser { |
| // If skipping first run, not in Safe Mode, no post opening action and the |
| // launch is not after a crash, consider showing the default browser promo. |
| TabOpeningPostOpeningAction postOpeningAction = |
| self.NTPActionAfterTabSwitcherDismissal; |
| if (self.startupParameters) { |
| postOpeningAction = self.startupParameters.postOpeningAction; |
| } |
| return postOpeningAction == NO_ACTION && |
| GetApplicationContext()->WasLastShutdownClean() && |
| !IsChromeLikelyDefaultBrowser() && |
| !HasUserOpenedSettingsFromFirstRunPromo(); |
| } |
| |
| - (void)maybeShowDefaultBrowserPromo:(Browser*)browser { |
| ChromeBrowserState* browserState = self.sceneState.appState.mainBrowserState; |
| PrefService* prefService = browserState->GetPrefs(); |
| AuthenticationService* authService = |
| AuthenticationServiceFactory::GetForBrowserState(browserState); |
| |
| if (self.sceneState.appState.startupInformation.isFirstRun || |
| IsUserPolicyNotificationNeeded(authService, prefService) || |
| ![self potentiallyInterestedUser]) { |
| // Don't show the default browser promo when either (1) the user is going |
| // through the First Run screens, (2) the user MUST see the User Policy |
| // notification, OR (3) it was determined that the user isn't potentially |
| // interested in that promo. |
| // |
| // Showing the User Policy notification has priority over showing the |
| // default browser promo. Both dialogs are competing for the same time slot |
| // which is after the browser startup and the browser UI is initialized. |
| return; |
| } |
| // Show the Default Browser promo UI if the user's past behavior fits |
| // the categorization of potentially interested users or if the user is |
| // signed in. Do not show if it is determined that Chrome is already the |
| // default browser (checked in the if enclosing this comment) or if the user |
| // has already seen the promo UI. If the user was in the experiment group |
| // that showed the Remind Me Later button and tapped on it, then show the |
| // promo again if now is the right time. |
| |
| BOOL isSignedIn = [self isSignedIn]; |
| |
| // Tailored promos take priority over general promo. |
| BOOL isMadeForIOSPromoEligible = |
| IsLikelyInterestedDefaultBrowserUser(DefaultPromoTypeMadeForIOS); |
| BOOL isAllTabsPromoEligible = |
| IsLikelyInterestedDefaultBrowserUser(DefaultPromoTypeAllTabs) && |
| isSignedIn; |
| BOOL isStaySafePromoEligible = |
| IsLikelyInterestedDefaultBrowserUser(DefaultPromoTypeStaySafe); |
| |
| BOOL isTailoredPromoEligibleUser = |
| !HasUserInteractedWithTailoredFullscreenPromoBefore() && |
| (isMadeForIOSPromoEligible || isAllTabsPromoEligible || |
| isStaySafePromoEligible); |
| if (isTailoredPromoEligibleUser && !UserInPromoCooldown()) { |
| self.sceneState.appState.shouldShowDefaultBrowserPromo = YES; |
| self.sceneState.appState.defaultBrowserPromoTypeToShow = |
| MostRecentInterestDefaultPromoType(!isSignedIn); |
| DCHECK(self.sceneState.appState.defaultBrowserPromoTypeToShow != |
| DefaultPromoTypeGeneral); |
| return; |
| } |
| |
| BOOL isGeneralPromoEligibleUser = |
| !HasUserInteractedWithFullscreenPromoBefore() && |
| (IsLikelyInterestedDefaultBrowserUser(DefaultPromoTypeGeneral) || |
| isSignedIn) && |
| !UserInPromoCooldown(); |
| if (isGeneralPromoEligibleUser || |
| ShouldShowRemindMeLaterDefaultBrowserFullscreenPromo()) { |
| self.sceneState.appState.shouldShowDefaultBrowserPromo = YES; |
| self.sceneState.appState.defaultBrowserPromoTypeToShow = |
| DefaultPromoTypeGeneral; |
| } |
| } |
| |
| // This method completely destroys all of the UI. It should be called when the |
| // scene is disconnected. |
| - (void)teardownUI { |
| if (!self.sceneState.UIEnabled) { |
| return; // Nothing to do. |
| } |
| |
| // The UI should be stopped before the models they observe are stopped. |
| // SigninCoordinator teardown is performed by the `signinCompletion` on |
| // termination of async events, do not add additional teardown here. |
| [self.signinCoordinator |
| interruptWithAction:SigninCoordinatorInterruptActionNoDismiss |
| completion:nil]; |
| |
| [self.historyCoordinator stop]; |
| self.historyCoordinator = nil; |
| |
| // Force close the settings if open. This gives Settings the opportunity to |
| // unregister observers and destroy C++ objects before the application is |
| // shut down without depending on non-deterministic call to -dealloc. |
| [self settingsWasDismissed]; |
| |
| [_mainCoordinator stop]; |
| _mainCoordinator = nil; |
| |
| // Stop observing web state list changes before tearing down the web state |
| // lists. |
| self.mainInterface.browser->GetWebStateList()->RemoveObserver( |
| _webStateListForwardingObserver.get()); |
| self.incognitoInterface.browser->GetWebStateList()->RemoveObserver( |
| _webStateListForwardingObserver.get()); |
| |
| PolicyWatcherBrowserAgent::FromBrowser(self.mainInterface.browser) |
| ->RemoveObserver(_policyWatcherObserverBridge.get()); |
| |
| // TODO(crbug.com/1229306): Consider moving this at the beginning of |
| // teardownUI to indicate that the UI is about to be torn down and that the |
| // dependencies depending on the browser UI models has to be cleaned up |
| // agent). |
| self.sceneState.UIEnabled = NO; |
| |
| [[SessionSavingSceneAgent agentFromScene:self.sceneState] |
| saveSessionsIfNeeded]; |
| [self.browserViewWrangler shutdown]; |
| self.browserViewWrangler = nil; |
| |
| [self.sceneState.appState removeObserver:self]; |
| } |
| |
| // Formats string for display on iPadOS application switcher with the |
| // domain of the foreground tab and the tab count. Assumes the scene is |
| // visible. Will return nil if there are no tabs. |
| - (NSString*)displayTitleForAppSwitcher { |
| DCHECK(self.currentInterface.browser); |
| web::WebState* webState = |
| self.currentInterface.browser->GetWebStateList()->GetActiveWebState(); |
| if (!webState) |
| return nil; |
| |
| // At this point there is at least one tab. |
| int numberOfTabs = self.currentInterface.browser->GetWebStateList()->count(); |
| DCHECK(numberOfTabs > 0); |
| GURL url = webState->GetVisibleURL(); |
| std::u16string urlText = url_formatter::FormatUrl( |
| url, |
| url_formatter::kFormatUrlOmitDefaults | |
| url_formatter::kFormatUrlOmitTrivialSubdomains | |
| url_formatter::kFormatUrlOmitHTTPS | |
| url_formatter::kFormatUrlTrimAfterHost, |
| base::UnescapeRule::SPACES, nullptr, nullptr, nullptr); |
| std::u16string pattern = |
| l10n_util::GetStringUTF16(IDS_IOS_APP_SWITCHER_SCENE_TITLE); |
| std::u16string formattedTitle = |
| base::i18n::MessageFormatter::FormatWithNamedArgs( |
| pattern, "domain", urlText, "count", numberOfTabs - 1); |
| return base::SysUTF16ToNSString(formattedTitle); |
| } |
| |
| // If users request to open tab or search and Chrome is not opened in the mode |
| // they expected, show a toast to clarify that the expected mode is not |
| // available. |
| - (void)showToastWhenOpenExternalIntentInUnexpectedMode { |
| id<SnackbarCommands> handler = HandlerForProtocol( |
| self.mainInterface.browser->GetCommandDispatcher(), SnackbarCommands); |
| BOOL inIncognitoMode = [self isIncognitoForced]; |
| |
| UrlLoadParams params = UrlLoadParams::InNewTab(GURL(kChromeUIManagementURL)); |
| params.web_params.transition_type = ui::PAGE_TRANSITION_TYPED; |
| ProceduralBlock moreAction = ^{ |
| [self dismissModalsAndMaybeOpenSelectedTabInMode: |
| inIncognitoMode ? ApplicationModeForTabOpening::INCOGNITO |
| : ApplicationModeForTabOpening::NORMAL |
| withUrlLoadParams:params |
| dismissOmnibox:YES |
| completion:nil]; |
| }; |
| |
| MDCSnackbarMessageAction* action = [[MDCSnackbarMessageAction alloc] init]; |
| action.handler = moreAction; |
| action.title = l10n_util::GetNSString(IDS_IOS_NAVIGATION_BAR_MORE_BUTTON); |
| action.accessibilityIdentifier = |
| l10n_util::GetNSString(IDS_IOS_NAVIGATION_BAR_MORE_BUTTON); |
| |
| NSString* text = |
| inIncognitoMode |
| ? l10n_util::GetNSString(IDS_IOS_SNACKBAR_MESSAGE_ICOGNITO_FORCED) |
| : l10n_util::GetNSString(IDS_IOS_SNACKBAR_MESSAGE_ICOGNITO_DISABLED); |
| |
| MDCSnackbarMessage* message = [MDCSnackbarMessage messageWithText:text]; |
| message.action = action; |
| |
| [handler showSnackbarMessage:message |
| withHapticType:UINotificationFeedbackTypeError]; |
| } |
| |
| - (BOOL)isIncognitoDisabled { |
| return IsIncognitoModeDisabled( |
| self.mainInterface.browser->GetBrowserState()->GetPrefs()); |
| } |
| |
| - (BOOL)isIncognitoForced { |
| return IsIncognitoModeForced( |
| self.incognitoInterface.browser->GetBrowserState()->GetPrefs()); |
| } |
| |
| // Sets a LocalState pref marking the TOS EULA as accepted. |
| // If this function is called, the EULA flag is not set but the FRE was not |
| // displayed. |
| // This can only happen if the EULA flag has not been set correctly on a |
| // previous session. |
| - (void)reconcileEulaAsAccepted { |
| static dispatch_once_t once_token = 0; |
| dispatch_once(&once_token, ^{ |
| PrefService* prefs = GetApplicationContext()->GetLocalState(); |
| if (!FirstRun::IsChromeFirstRun() && |
| !prefs->GetBoolean(prefs::kEulaAccepted)) { |
| prefs->SetBoolean(prefs::kEulaAccepted, true); |
| prefs->CommitPendingWrite(); |
| base::UmaHistogramBoolean("IOS.ReconcileEULAPref", true); |
| } |
| }); |
| } |
| |
| // Returns YES if the sign-in upgrade promo should be presented. |
| - (BOOL)shouldPresentSigninUpgradePromo { |
| if (self.sceneState.appState.initStage <= InitStageFirstRun) { |
| return NO; |
| } |
| |
| if (!signin::ShouldPresentUserSigninUpgrade( |
| self.sceneState.appState.mainBrowserState, |
| version_info::GetVersion())) { |
| return NO; |
| } |
| // Don't show the promo if there is a blocking task in process. |
| if (self.sceneState.appState.currentUIBlocker) |
| return NO; |
| // Don't show the promo in Incognito mode. |
| if (self.currentInterface == self.incognitoInterface) |
| return NO; |
| // Don't show promos if the app was launched from a URL. |
| if (self.startupParameters) |
| return NO; |
| // Don't show the promo if the window is not active. |
| if (self.sceneState.activationLevel < SceneActivationLevelForegroundActive) |
| return NO; |
| // Don't show the promo if the tab grid is active. |
| if (self.mainCoordinator.isTabGridActive) |
| return NO; |
| // Don't show the promo if already presented. |
| if (self.sceneState.appState.signinUpgradePromoPresentedOnce) |
| return NO; |
| return YES; |
| } |
| |
| // Presents the sign-in upgrade promo. |
| - (void)presentSigninUpgradePromo { |
| // It is possible during a slow asynchronous call that the user changes their |
| // state so as to no longer be eligible for sign-in promos. Return early in |
| // this case. |
| if (![self shouldPresentSigninUpgradePromo]) { |
| return; |
| } |
| self.sceneState.appState.signinUpgradePromoPresentedOnce = YES; |
| DCHECK(!self.signinCoordinator) |
| << "self.signinCoordinator: " |
| << base::SysNSStringToUTF8([self.signinCoordinator description]); |
| Browser* browser = self.mainInterface.browser; |
| self.signinCoordinator = [SigninCoordinator |
| upgradeSigninPromoCoordinatorWithBaseViewController:self.mainInterface |
| .viewController |
| browser:browser]; |
| [self startSigninCoordinatorWithCompletion:nil]; |
| } |
| |
| - (BOOL)canHandleIntents { |
| if (self.sceneState.activationLevel < SceneActivationLevelForegroundActive) { |
| return NO; |
| } |
| |
| if (self.sceneState.appState.initStage <= InitStageFirstRun) { |
| return NO; |
| } |
| |
| if (self.sceneState.presentingModalOverlay) { |
| return NO; |
| } |
| |
| if (IsSigninForcedByPolicy()) { |
| if (self.signinCoordinator) { |
| // Return NO because intents cannot be handled when using |
| // `self.signinCoordinator` for the forced sign-in prompt. |
| return NO; |
| } |
| if (![self isSignedIn]) { |
| // Return NO if the forced sign-in policy is enabled while the browser is |
| // signed out because intent can only be processed when the browser is |
| // signed-in in that case. This condition may be reached at startup before |
| // `self.signinCoordinator` is set to show the forced sign-in prompt. |
| return NO; |
| } |
| } |
| |
| return YES; |
| } |
| |
| - (BOOL)isSignedIn { |
| AuthenticationService* authenticationService = |
| AuthenticationServiceFactory::GetForBrowserState( |
| self.sceneState.appState.mainBrowserState); |
| DCHECK(authenticationService); |
| DCHECK(authenticationService->initialized()); |
| |
| return authenticationService->HasPrimaryIdentity( |
| signin::ConsentLevel::kSignin); |
| } |
| |
| #pragma mark - ApplicationCommands |
| |
| - (void)dismissModalDialogsWithCompletion:(ProceduralBlock)completion { |
| [self dismissModalDialogsWithCompletion:completion dismissOmnibox:YES]; |
| } |
| |
| - (void)dismissModalDialogs { |
| [self dismissModalDialogsWithCompletion:nil dismissOmnibox:YES]; |
| } |
| |
| - (void)showHistory { |
| self.historyCoordinator = [[HistoryCoordinator alloc] |
| initWithBaseViewController:self.currentInterface.viewController |
| browser:self.mainInterface.browser]; |
| self.historyCoordinator.loadStrategy = |
| self.currentInterface.incognito ? UrlLoadStrategy::ALWAYS_IN_INCOGNITO |
| : UrlLoadStrategy::NORMAL; |
| [self.historyCoordinator start]; |
| } |
| |
| // Opens an url from a link in the settings UI. |
| - (void)closeSettingsUIAndOpenURL:(OpenNewTabCommand*)command { |
| DCHECK([command fromChrome]); |
| UrlLoadParams params = UrlLoadParams::InNewTab([command URL]); |
| params.web_params.transition_type = ui::PAGE_TRANSITION_TYPED; |
| ProceduralBlock completion = ^{ |
| ApplicationModeForTabOpening mode = |
| [self isIncognitoForced] ? ApplicationModeForTabOpening::INCOGNITO |
| : ApplicationModeForTabOpening::NORMAL; |
| [self dismissModalsAndMaybeOpenSelectedTabInMode:mode |
| withUrlLoadParams:params |
| dismissOmnibox:YES |
| completion:nil]; |
| }; |
| [self closePresentedViews:YES completion:completion]; |
| } |
| |
| - (void)closeSettingsUI { |
| [self closePresentedViews:YES completion:nullptr]; |
| } |
| |
| - (void)prepareTabSwitcher { |
| web::WebState* currentWebState = |
| self.currentInterface.browser->GetWebStateList()->GetActiveWebState(); |
| if (currentWebState) { |
| SnapshotTabHelper::FromWebState(currentWebState) |
| ->UpdateSnapshotWithCallback(nil); |
| } |
| [self.mainCoordinator prepareToShowTabGrid]; |
| } |
| |
| - (void)displayRegularTabSwitcherInGridLayout { |
| [self displayTabSwitcherForcingRegularTabs:YES]; |
| } |
| |
| - (void)displayTabSwitcherInGridLayout { |
| [self displayTabSwitcherForcingRegularTabs:NO]; |
| } |
| |
| - (void)displayTabSwitcherForcingRegularTabs:(BOOL)forcing { |
| DCHECK(!self.mainCoordinator.isTabGridActive); |
| if (!self.isProcessingVoiceSearchCommand) { |
| [self.currentInterface.bvc userEnteredTabSwitcher]; |
| |
| if (forcing && self.currentInterface.incognito) { |
| [self setCurrentInterfaceForMode:ApplicationMode::NORMAL]; |
| } |
| |
| [self showTabSwitcher]; |
| self.isProcessingTabSwitcherCommand = YES; |
| dispatch_after(dispatch_time(DISPATCH_TIME_NOW, |
| kExpectedTransitionDurationInNanoSeconds), |
| dispatch_get_main_queue(), ^{ |
| self.isProcessingTabSwitcherCommand = NO; |
| }); |
| } |
| } |
| |
| // TODO(crbug.com/779791) : Remove showing settings from MainController. |
| - (void)showAutofillSettingsFromViewController: |
| (UIViewController*)baseViewController { |
| DCHECK(!self.signinCoordinator) |
| << "self.signinCoordinator: " |
| << base::SysNSStringToUTF8([self.signinCoordinator description]); |
| if (self.settingsNavigationController) |
| return; |
| |
| Browser* browser = self.mainInterface.browser; |
| self.settingsNavigationController = |
| [SettingsNavigationController autofillProfileControllerForBrowser:browser |
| delegate:self]; |
| [baseViewController presentViewController:self.settingsNavigationController |
| animated:YES |
| completion:nil]; |
| } |
| |
| - (void)showReportAnIssueFromViewController: |
| (UIViewController*)baseViewController |
| sender:(UserFeedbackSender)sender { |
| [self showReportAnIssueFromViewController:baseViewController |
| sender:sender |
| specificProductData:nil]; |
| } |
| |
| - (void) |
| showReportAnIssueFromViewController:(UIViewController*)baseViewController |
| sender:(UserFeedbackSender)sender |
| specificProductData:(NSDictionary<NSString*, NSString*>*) |
| specificProductData { |
| DCHECK(baseViewController); |
| // This dispatch is necessary to give enough time for the tools menu to |
| // disappear before taking a screenshot. |
| __weak SceneController* weakSelf = self; |
| dispatch_async(dispatch_get_main_queue(), ^{ |
| [weakSelf presentReportAnIssueViewController:baseViewController |
| sender:sender |
| specificProductData:specificProductData]; |
| }); |
| } |
| |
| - (void)presentReportAnIssueViewController:(UIViewController*)baseViewController |
| sender:(UserFeedbackSender)sender |
| specificProductData:(NSDictionary<NSString*, NSString*>*) |
| specificProductData { |
| DCHECK(!self.signinCoordinator) |
| << "self.signinCoordinator: " |
| << base::SysNSStringToUTF8([self.signinCoordinator description]); |
| if (self.settingsNavigationController) |
| return; |
| |
| UserFeedbackData* data = |
| [self createUserFeedbackDataForSender:sender |
| specificProductData:specificProductData]; |
| |
| Browser* browser = self.mainInterface.browser; |
| id<ApplicationCommands> handler = |
| HandlerForProtocol(browser->GetCommandDispatcher(), ApplicationCommands); |
| self.settingsNavigationController = |
| [SettingsNavigationController userFeedbackControllerForBrowser:browser |
| delegate:self |
| userFeedbackData:data |
| handler:handler]; |
| [baseViewController presentViewController:self.settingsNavigationController |
| animated:YES |
| completion:nil]; |
| } |
| |
| - (UserFeedbackData*)createUserFeedbackDataForSender:(UserFeedbackSender)sender |
| specificProductData: |
| (NSDictionary<NSString*, NSString*>*) |
| specificProductData { |
| UserFeedbackData* data = [[UserFeedbackData alloc] init]; |
| data.origin = sender; |
| data.currentPageIsIncognito = self.currentInterface.incognito; |
| |
| CGFloat scale = 0.0; |
| if (!self.mainCoordinator.isTabGridActive) { |
| web::WebState* webState = |
| self.currentInterface.browser->GetWebStateList()->GetActiveWebState(); |
| if (webState) { |
| // Record URL of browser tab that is currently showing |
| GURL url = webState->GetVisibleURL(); |
| std::u16string urlText = url_formatter::FormatUrl(url); |
| data.currentPageDisplayURL = base::SysUTF16ToNSString(urlText); |
| } |
| } else { |
| // For screenshots of the tab switcher we need to use a scale of 1.0 to |
| // avoid spending too much time since the tab switcher can have lots of |
| // subviews. |
| scale = 1.0; |
| } |
| |
| UIView* lastView = self.mainCoordinator.activeViewController.view; |
| DCHECK(lastView); |
| data.currentPageScreenshot = CaptureView(lastView, scale); |
| |
| ChromeBrowserState* browserState = self.currentInterface.browserState; |
| if (browserState->IsOffTheRecord()) { |
| data.currentPageIsIncognito = YES; |
| } else { |
| data.currentPageIsIncognito = NO; |
| signin::IdentityManager* identity_manager = |
| IdentityManagerFactory::GetForBrowserState(browserState); |
| std::string username = |
| identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSync) |
| .email; |
| if (!username.empty()) |
| data.currentPageSyncedUserName = base::SysUTF8ToNSString(username); |
| } |
| |
| data.productSpecificData = specificProductData; |
| return data; |
| } |
| |
| - (void)openURLInNewTab:(OpenNewTabCommand*)command { |
| if (command.inIncognito) { |
| IncognitoReauthSceneAgent* reauthAgent = |
| [IncognitoReauthSceneAgent agentFromScene:self.sceneState]; |
| if (reauthAgent.authenticationRequired) { |
| __weak SceneController* weakSelf = self; |
| [reauthAgent |
| authenticateIncognitoContentWithCompletionBlock:^(BOOL success) { |
| if (success) { |
| [weakSelf openURLInNewTab:command]; |
| } |
| }]; |
| return; |
| } |
| } |
| |
| UrlLoadParams params = |
| UrlLoadParams::InNewTab(command.URL, command.virtualURL); |
| params.SetInBackground(command.inBackground); |
| params.web_params.referrer = command.referrer; |
| params.in_incognito = command.inIncognito; |
| params.append_to = command.appendTo; |
| params.origin_point = command.originPoint; |
| params.from_chrome = command.fromChrome; |
| params.user_initiated = command.userInitiated; |
| params.should_focus_omnibox = command.shouldFocusOmnibox; |
| params.inherit_opener = !command.inBackground; |
| self.sceneURLLoadingService->LoadUrlInNewTab(params); |
| } |
| |
| // TODO(crbug.com/779791) : Do not pass `baseViewController` through dispatcher. |
| - (void)showSignin:(ShowSigninCommand*)command |
| baseViewController:(UIViewController*)baseViewController { |
| DCHECK(!self.signinCoordinator) |
| << "self.signinCoordinator: " |
| << base::SysNSStringToUTF8([self.signinCoordinator description]); |
| Browser* mainBrowser = self.mainInterface.browser; |
| |
| switch (command.operation) { |
| case AuthenticationOperationReauthenticate: |
| self.signinCoordinator = [SigninCoordinator |
| reAuthenticationCoordinatorWithBaseViewController:baseViewController |
| browser:mainBrowser |
| accessPoint:command.accessPoint |
| promoAction:command |
| .promoAction]; |
| break; |
| case AuthenticationOperationSigninAndSync: |
| self.signinCoordinator = [SigninCoordinator |
| userSigninCoordinatorWithBaseViewController:baseViewController |
| browser:mainBrowser |
| identity:command.identity |
| accessPoint:command.accessPoint |
| promoAction:command.promoAction]; |
| break; |
| case AuthenticationOperationSigninOnly: |
| self.signinCoordinator = [SigninCoordinator |
| consistencyPromoSigninCoordinatorWithBaseViewController: |
| baseViewController |
| browser:mainBrowser |
| accessPoint: |
| command.accessPoint]; |
| break; |
| case AuthenticationOperationAddAccount: |
| self.signinCoordinator = [SigninCoordinator |
| addAccountCoordinatorWithBaseViewController:baseViewController |
| browser:mainBrowser |
| accessPoint:command.accessPoint]; |
| break; |
| case AuthenticationOperationForcedSigninAndSync: |
| self.signinCoordinator = [SigninCoordinator |
| forcedSigninCoordinatorWithBaseViewController:baseViewController |
| browser:mainBrowser]; |
| break; |
| } |
| [self startSigninCoordinatorWithCompletion:command.callback]; |
| } |
| |
| - (void) |
| showTrustedVaultReauthForFetchKeysFromViewController: |
| (UIViewController*)viewController |
| trigger: |
| (syncer:: |
| TrustedVaultUserActionTriggerForUMA) |
| trigger { |
| [self |
| showTrustedVaultDialogFromViewController:viewController |
| intent: |
| SigninTrustedVaultDialogIntentFetchKeys |
| trigger:trigger]; |
| } |
| |
| - (void) |
| showTrustedVaultReauthForDegradedRecoverabilityFromViewController: |
| (UIViewController*)viewController |
| trigger: |
| (syncer:: |
| TrustedVaultUserActionTriggerForUMA) |
| trigger { |
| [self |
| showTrustedVaultDialogFromViewController:viewController |
| intent: |
| SigninTrustedVaultDialogIntentDegradedRecoverability |
| trigger:trigger]; |
| } |
| |
| - (void)showWebSigninPromoFromViewController: |
| (UIViewController*)baseViewController |
| URL:(const GURL&)url { |
| // Do not display the web sign-in promo if there is any UI on the screen. |
| if (self.signinCoordinator || self.settingsNavigationController) |
| return; |
| if (self.sceneState.appState.initStage == InitStageFirstRun) { |
| // This case is possible when using force FRE flag and opening chrome |
| // with accounts.google.com in the background. |
| // crbug.com/1293305. |
| return; |
| } |
| self.signinCoordinator = [SigninCoordinator |
| consistencyPromoSigninCoordinatorWithBaseViewController:baseViewController |
| browser:self.mainInterface |
| .browser |
| accessPoint: |
| signin_metrics::AccessPoint:: |
| ACCESS_POINT_WEB_SIGNIN]; |
| if (!self.signinCoordinator) |
| return; |
| __weak SceneController* weakSelf = self; |
| |
| // Copy the URL so it can be safely captured in the block. |
| GURL copiedURL = url; |
| |
| [self startSigninCoordinatorWithCompletion:^(BOOL success) { |
| // If the sign-in is not successful or the scene controller is shut down do |
| // not load the continuation URL. |
| if (!success || !weakSelf) { |
| return; |
| } |
| UrlLoadingBrowserAgent::FromBrowser(weakSelf.mainInterface.browser) |
| ->Load(UrlLoadParams::InCurrentTab(copiedURL)); |
| }]; |
| } |
| |
| - (void)showSigninAccountNotificationFromViewController: |
| (UIViewController*)baseViewController { |
| web::WebState* webState = |
| self.mainInterface.browser->GetWebStateList()->GetActiveWebState(); |
| DCHECK(webState); |
| infobars::InfoBarManager* infoBarManager = |
| InfoBarManagerImpl::FromWebState(webState); |
| DCHECK(infoBarManager); |
| SigninNotificationInfoBarDelegate::Create( |
| infoBarManager, self.mainInterface.browser->GetBrowserState(), self, |
| baseViewController); |
| } |
| |
| - (void)setIncognitoContentVisible:(BOOL)incognitoContentVisible { |
| self.sceneState.incognitoContentVisible = incognitoContentVisible; |
| } |
| |
| - (void)startVoiceSearch { |
| if (!self.isProcessingTabSwitcherCommand) { |
| [self startVoiceSearchInCurrentBVC]; |
| self.isProcessingVoiceSearchCommand = YES; |
| dispatch_after(dispatch_time(DISPATCH_TIME_NOW, |
| kExpectedTransitionDurationInNanoSeconds), |
| dispatch_get_main_queue(), ^{ |
| self.isProcessingVoiceSearchCommand = NO; |
| }); |
| } |
| } |
| |
| - (void)showSettingsFromViewController:(UIViewController*)baseViewController { |
| if (!baseViewController) { |
| baseViewController = self.currentInterface.viewController; |
| } |
| |
| DCHECK(!self.signinCoordinator) |
| << "self.signinCoordinator: " |
| << base::SysNSStringToUTF8([self.signinCoordinator description]); |
| if (self.settingsNavigationController) |
| return; |
| [[DeferredInitializationRunner sharedInstance] |
| runBlockIfNecessary:kPrefObserverInit]; |
| |
| Browser* browser = self.mainInterface.browser; |
| |
| self.settingsNavigationController = |
| [SettingsNavigationController mainSettingsControllerForBrowser:browser |
| delegate:self]; |
| [baseViewController presentViewController:self.settingsNavigationController |
| animated:YES |
| completion:nil]; |
| } |
| |
| - (void)openNewWindowWithActivity:(NSUserActivity*)userActivity { |
| if (!base::ios::IsMultipleScenesSupported()) |
| return; // silent no-op. |
| |
| UISceneActivationRequestOptions* options = |
| [[UISceneActivationRequestOptions alloc] init]; |
| options.requestingScene = self.sceneState.scene; |
| |
| if (self.mainInterface) { |
| PrefService* prefs = self.mainInterface.browserState->GetPrefs(); |
| if (IsIncognitoModeForced(prefs)) { |
| userActivity = AdaptUserActivityToIncognito(userActivity, true); |
| } else if (IsIncognitoModeDisabled(prefs)) { |
| userActivity = AdaptUserActivityToIncognito(userActivity, false); |
| } |
| |
| [UIApplication.sharedApplication |
| requestSceneSessionActivation:nil /* make a new scene */ |
| userActivity:userActivity |
| options:options |
| errorHandler:nil]; |
| } |
| } |
| |
| #pragma mark - ApplicationSettingsCommands |
| |
| // TODO(crbug.com/779791) : Remove show settings from MainController. |
| - (void)showAccountsSettingsFromViewController: |
| (UIViewController*)baseViewController { |
| DCHECK(!self.signinCoordinator) |
| << "self.signinCoordinator: " |
| << base::SysNSStringToUTF8([self.signinCoordinator description]); |
| if (!baseViewController) { |
| DCHECK_EQ(self.currentInterface.viewController, |
| self.mainCoordinator.activeViewController); |
| baseViewController = self.currentInterface.viewController; |
| } |
| |
| if (self.currentInterface.incognito) { |
| NOTREACHED(); |
| return; |
| } |
| if (self.settingsNavigationController) { |
| [self.settingsNavigationController |
| showAccountsSettingsFromViewController:baseViewController]; |
| return; |
| } |
| |
| Browser* browser = self.mainInterface.browser; |
| self.settingsNavigationController = |
| [SettingsNavigationController accountsControllerForBrowser:browser |
| delegate:self]; |
| [baseViewController presentViewController:self.settingsNavigationController |
| animated:YES |
| completion:nil]; |
| } |
| |
| // TODO(crbug.com/779791) : Remove Google services settings from MainController. |
| - (void)showGoogleServicesSettingsFromViewController: |
| (UIViewController*)baseViewController { |
| DCHECK(!self.signinCoordinator) |
| << "self.signinCoordinator: " |
| << base::SysNSStringToUTF8([self.signinCoordinator description]); |
| if (!baseViewController) { |
| DCHECK_EQ(self.currentInterface.viewController, |
| self.mainCoordinator.activeViewController); |
| baseViewController = self.currentInterface.viewController; |
| } |
| |
| if (self.settingsNavigationController) { |
| // Navigate to the Google services settings if the settings dialog is |
| // already opened. |
| [self.settingsNavigationController |
| showGoogleServicesSettingsFromViewController:baseViewController]; |
| return; |
| } |
| |
| Browser* browser = self.mainInterface.browser; |
| self.settingsNavigationController = |
| [SettingsNavigationController googleServicesControllerForBrowser:browser |
| delegate:self]; |
| |
| [baseViewController presentViewController:self.settingsNavigationController |
| animated:YES |
| completion:nil]; |
| } |
| |
| // TODO(crbug.com/779791) : Remove show settings commands from MainController. |
| - (void)showSyncSettingsFromViewController: |
| (UIViewController*)baseViewController { |
| DCHECK(!self.signinCoordinator) |
| << "self.signinCoordinator: " |
| << base::SysNSStringToUTF8([self.signinCoordinator description]); |
| if (self.settingsNavigationController) { |
| [self.settingsNavigationController |
| showSyncSettingsFromViewController:baseViewController]; |
| return; |
| } |
| |
| Browser* browser = self.mainInterface.browser; |
| self.settingsNavigationController = |
| [SettingsNavigationController syncSettingsControllerForBrowser:browser |
| delegate:self]; |
| [baseViewController presentViewController:self.settingsNavigationController |
| animated:YES |
| completion:nil]; |
| } |
| |
| // TODO(crbug.com/779791) : Remove show settings commands from MainController. |
| - (void)showSyncPassphraseSettingsFromViewController: |
| (UIViewController*)baseViewController { |
| DCHECK(!self.signinCoordinator) |
| << "self.signinCoordinator: " |
| << base::SysNSStringToUTF8([self.signinCoordinator description]); |
| if (self.settingsNavigationController) { |
| [self.settingsNavigationController |
| showSyncPassphraseSettingsFromViewController:baseViewController]; |
| return; |
| } |
| |
| Browser* browser = self.mainInterface.browser; |
| self.settingsNavigationController = |
| [SettingsNavigationController syncPassphraseControllerForBrowser:browser |
| delegate:self]; |
| [baseViewController presentViewController:self.settingsNavigationController |
| animated:YES |
| completion:nil]; |
| } |
| |
| // TODO(crbug.com/779791) : Remove show settings commands from MainController. |
| - (void)showSavedPasswordsSettingsFromViewController: |
| (UIViewController*)baseViewController |
| showCancelButton:(BOOL)showCancelButton { |
| if (!baseViewController) { |
| // TODO(crbug.com/779791): Don't pass base view controller through |
| // dispatched command. |
| baseViewController = self.currentInterface.viewController; |
| } |
| DCHECK(!self.signinCoordinator) |
| << "self.signinCoordinator: " |
| << base::SysNSStringToUTF8([self.signinCoordinator description]); |
| if (self.settingsNavigationController) { |
| [self.settingsNavigationController |
| showSavedPasswordsSettingsFromViewController:baseViewController |
| showCancelButton:showCancelButton]; |
| return; |
| } |
| Browser* browser = self.mainInterface.browser; |
| self.settingsNavigationController = [SettingsNavigationController |
| savePasswordsControllerForBrowser:browser |
| delegate:self |
| startPasswordCheckAutomatically:YES |
| showCancelButton:showCancelButton]; |
| [baseViewController presentViewController:self.settingsNavigationController |
| animated:YES |
| completion:nil]; |
| } |
| |
| - (void)showSavedPasswordsSettingsAndStartPasswordCheckFromViewController: |
| (UIViewController*)baseViewController { |
| DCHECK(!self.signinCoordinator) |
| << "self.signinCoordinator: " |
| << base::SysNSStringToUTF8([self.signinCoordinator description]); |
| [self dismissModalDialogs]; |
| if (self.settingsNavigationController) { |
| [self.settingsNavigationController |
| showSavedPasswordsSettingsAndStartPasswordCheckFromViewController: |
| baseViewController]; |
| return; |
| } |
| Browser* browser = self.mainInterface.browser; |
| self.settingsNavigationController = |
| [SettingsNavigationController savePasswordsControllerForBrowser:browser |
| delegate:self |
| startPasswordCheckAutomatically:YES |
| showCancelButton:NO]; |
| [baseViewController presentViewController:self.settingsNavigationController |
| animated:YES |
| completion:nil]; |
| } |
| |
| // TODO(crbug.com/779791) : Remove show settings commands from MainController. |
| - (void)showProfileSettingsFromViewController: |
| (UIViewController*)baseViewController { |
| DCHECK(!self.signinCoordinator) |
| << "self.signinCoordinator: " |
| << base::SysNSStringToUTF8([self.signinCoordinator description]); |
| if (self.settingsNavigationController) { |
| [self.settingsNavigationController |
| showProfileSettingsFromViewController:baseViewController]; |
| return; |
| } |
| Browser* browser = self.mainInterface.browser; |
| |
| self.settingsNavigationController = |
| [SettingsNavigationController autofillProfileControllerForBrowser:browser |
| delegate:self]; |
| [baseViewController presentViewController:self.settingsNavigationController |
| animated:YES |
| completion:nil]; |
| } |
| |
| // TODO(crbug.com/779791) : Remove show settings commands from MainController. |
| - (void)showCreditCardSettings { |
| DCHECK(!self.signinCoordinator) |
| << "self.signinCoordinator: " |
| << base::SysNSStringToUTF8([self.signinCoordinator description]); |
| if (self.settingsNavigationController) { |
| [self.settingsNavigationController showCreditCardSettings]; |
| return; |
| } |
| |
| Browser* browser = self.mainInterface.browser; |
| self.settingsNavigationController = [SettingsNavigationController |
| autofillCreditCardControllerForBrowser:browser |
| delegate:self]; |
| [self.currentInterface.viewController |
| presentViewController:self.settingsNavigationController |
| animated:YES |
| completion:nil]; |
| } |
| |
| - (void)showDefaultBrowserSettingsFromViewController: |
| (UIViewController*)baseViewController |
| sourceForUMA: |
| (DefaultBrowserPromoSource)source { |
| if (!baseViewController) { |
| baseViewController = self.currentInterface.viewController; |
| } |
| if (self.settingsNavigationController) { |
| [self.settingsNavigationController |
| showDefaultBrowserSettingsFromViewController:baseViewController |
| sourceForUMA:source]; |
| return; |
| } |
| Browser* browser = self.mainInterface.browser; |
| |
| self.settingsNavigationController = |
| [SettingsNavigationController defaultBrowserControllerForBrowser:browser |
| delegate:self]; |
| [baseViewController presentViewController:self.settingsNavigationController |
| animated:YES |
| completion:nil]; |
| } |
| |
| - (void)showClearBrowsingDataSettings { |
| UIViewController* baseViewController = self.currentInterface.viewController; |
| if (self.settingsNavigationController) { |
| [self.settingsNavigationController showClearBrowsingDataSettings]; |
| return; |
| } |
| Browser* browser = self.mainInterface.browser; |
| |
| self.settingsNavigationController = [SettingsNavigationController |
| clearBrowsingDataControllerForBrowser:browser |
| delegate:self]; |
| [baseViewController presentViewController:self.settingsNavigationController |
| animated:YES |
| completion:nil]; |
| } |
| |
| - (void)showSafetyCheckSettingsAndStartSafetyCheck { |
| UIViewController* baseViewController = self.currentInterface.viewController; |
| if (self.settingsNavigationController) { |
| [self.settingsNavigationController |
| showSafetyCheckSettingsAndStartSafetyCheck]; |
| return; |
| } |
| Browser* browser = self.mainInterface.browser; |
| |
| self.settingsNavigationController = |
| [SettingsNavigationController safetyCheckControllerForBrowser:browser |
| delegate:self]; |
| [baseViewController presentViewController:self.settingsNavigationController |
| animated:YES |
| completion:nil]; |
| } |
| |
| - (void)showSafeBrowsingSettings { |
| UIViewController* baseViewController = self.currentInterface.viewController; |
| if (self.settingsNavigationController) { |
| [self.settingsNavigationController showSafeBrowsingSettings]; |
| return; |
| } |
| Browser* browser = self.mainInterface.browser; |
| |
| self.settingsNavigationController = |
| [SettingsNavigationController safeBrowsingControllerForBrowser:browser |
| delegate:self]; |
| [baseViewController presentViewController:self.settingsNavigationController |
| animated:YES |
| completion:nil]; |
| } |
| |
| #pragma mark - SettingsNavigationControllerDelegate |
| |
| - (void)closeSettings { |
| [self closeSettingsUI]; |
| } |
| |
| - (void)settingsWasDismissed { |
| [self.settingsNavigationController cleanUpSettings]; |
| self.settingsNavigationController = nil; |
| } |
| |
| - (id<ApplicationCommands, BrowserCommands>)handlerForSettings { |
| // Assume that settings always wants the dispatcher from the main BVC. |
| return static_cast<id<ApplicationCommands, BrowserCommands>>( |
| self.mainInterface.browser->GetCommandDispatcher()); |
| } |
| |
| - (id<ApplicationCommands>)handlerForApplicationCommands { |
| // Assume that settings always wants the dispatcher from the main BVC. |
| return HandlerForProtocol(self.mainInterface.browser->GetCommandDispatcher(), |
| ApplicationCommands); |
| } |
| |
| - (id<SnackbarCommands>)handlerForSnackbarCommands { |
| // Assume that settings always wants the dispatcher from the main BVC. |
| return HandlerForProtocol(self.mainInterface.browser->GetCommandDispatcher(), |
| SnackbarCommands); |
| } |
| |
| #pragma mark - TabGridCoordinatorDelegate |
| |
| - (void)tabGrid:(TabGridCoordinator*)tabGrid |
| shouldActivateBrowser:(Browser*)browser |
| dismissTabGrid:(BOOL)dismissTabGrid |
| focusOmnibox:(BOOL)focusOmnibox { |
| DCHECK(dismissTabGrid || self.mainCoordinator.isThumbStripEnabled); |
| [self beginActivatingBrowser:browser |
| dismissTabSwitcher:dismissTabGrid |
| focusOmnibox:focusOmnibox]; |
| } |
| |
| - (void)tabGridDismissTransitionDidEnd:(TabGridCoordinator*)tabGrid { |
| if (!self.sceneState.UIEnabled) { |
| return; |
| } |
| [self finishActivatingBrowserDismissingTabSwitcher:YES]; |
| } |
| |
| - (TabGridPage)activePageForTabGrid:(TabGridCoordinator*)tabGrid { |
| return self.activePage; |
| } |
| |
| // Begins the process of activating the given current model, switching which BVC |
| // is suspended if necessary. If `dismissTabSwitcher` is set, the tab switcher |
| // will also be dismissed. Note that this means that a browser can be activated |
| // without closing the tab switcher (e.g. thumb strip), but dismissing the tab |
| // switcher requires activating a browser. The omnibox will be focused after the |
| // tab switcher dismissal is completed if `focusOmnibox` is YES. |
| - (void)beginActivatingBrowser:(Browser*)browser |
| dismissTabSwitcher:(BOOL)dismissTabSwitcher |
| focusOmnibox:(BOOL)focusOmnibox { |
| DCHECK(browser == self.mainInterface.browser || |
| browser == self.incognitoInterface.browser); |
| DCHECK(dismissTabSwitcher || self.mainCoordinator.isThumbStripEnabled); |
| |
| self.activatingBrowser = YES; |
| ApplicationMode mode = (browser == self.mainInterface.browser) |
| ? ApplicationMode::NORMAL |
| : ApplicationMode::INCOGNITO; |
| [self setCurrentInterfaceForMode:mode]; |
| |
| // The call to set currentBVC above does not actually display the BVC, because |
| // _activatingBrowser is YES. So: Force the BVC transition to start. |
| [self displayCurrentBVCAndFocusOmnibox:focusOmnibox |
| dismissTabSwitcher:dismissTabSwitcher]; |
| // If the tab switcher was not dismissed, finish the activation process now. |
| if (!dismissTabSwitcher) { |
| [self finishActivatingBrowserDismissingTabSwitcher:NO]; |
| } |
| } |
| |
| // Completes the process of activating the given browser. If necessary, also |
| // finishes dismissing the tab switcher, removing it from the |
| // screen and showing the appropriate BVC. |
| - (void)finishActivatingBrowserDismissingTabSwitcher: |
| (BOOL)dismissingTabSwitcher { |
| // In real world devices, it is possible to have an empty tab model at the |
| // finishing block of a BVC presentation animation. This can happen when the |
| // following occur: a) There is JS that closes the last incognito tab, b) that |
| // JS was paused while the user was in the tab switcher, c) the user enters |
| // the tab, activating the JS while the tab is being presented. Effectively, |
| // the BVC finishes the presentation animation, but there are no tabs to |
| // display. The only appropriate action is to dismiss the BVC and return the |
| // user to the tab switcher. |
| if (self.currentInterface.browser && |
| self.currentInterface.browser->GetWebStateList() && |
| self.currentInterface.browser->GetWebStateList()->count() == 0U) { |
| self.activatingBrowser = NO; |
| if (dismissingTabSwitcher) { |
| self.modeToDisplayOnTabSwitcherDismissal = TabSwitcherDismissalMode::NONE; |
| self.NTPActionAfterTabSwitcherDismissal = NO_ACTION; |
| [self showTabSwitcher]; |
| } |
| return; |
| } |
| |
| if (self.modeToDisplayOnTabSwitcherDismissal == |
| TabSwitcherDismissalMode::NORMAL) { |
| [self setCurrentInterfaceForMode:ApplicationMode::NORMAL]; |
| } else if (self.modeToDisplayOnTabSwitcherDismissal == |
| TabSwitcherDismissalMode::INCOGNITO) { |
| [self setCurrentInterfaceForMode:ApplicationMode::INCOGNITO]; |
| } |
| self.activatingBrowser = NO; |
| |
| if (dismissingTabSwitcher) { |
| self.modeToDisplayOnTabSwitcherDismissal = TabSwitcherDismissalMode::NONE; |
| |
| ProceduralBlock action = [self completionBlockForTriggeringAction: |
| self.NTPActionAfterTabSwitcherDismissal]; |
| self.NTPActionAfterTabSwitcherDismissal = NO_ACTION; |
| if (action) { |
| action(); |
| } |
| } |
| } |
| |
| #pragma mark Tab opening utility methods. |
| |
| - (ProceduralBlock)completionBlockForTriggeringAction: |
| (TabOpeningPostOpeningAction)action { |
| __weak __typeof(self) weakSelf = self; |
| switch (action) { |
| case START_VOICE_SEARCH: |
| return ^{ |
| [weakSelf startVoiceSearchInCurrentBVC]; |
| }; |
| case START_QR_CODE_SCANNER: |
| return ^{ |
| [weakSelf startQRCodeScanner]; |
| }; |
| case START_LENS: |
| return ^{ |
| [weakSelf startLens]; |
| }; |
| case FOCUS_OMNIBOX: |
| return ^{ |
| [weakSelf focusOmnibox]; |
| }; |
| case SHOW_DEFAULT_BROWSER_SETTINGS: |
| return ^{ |
| [weakSelf showDefaultBrowserSettings]; |
| }; |
| default: |
| return nil; |
| } |
| } |
| |
| // Starts a voice search on the current BVC. |
| - (void)startVoiceSearchInCurrentBVC { |
| // If the background (non-current) BVC is playing TTS audio, call |
| // -startVoiceSearch on it to stop the TTS. |
| BrowserViewController* backgroundBVC = |
| self.mainInterface == self.currentInterface ? self.incognitoInterface.bvc |
| : self.mainInterface.bvc; |
| // TODO(crbug.com/1329104): playingTTS will be removed as an API from the BVC |
| // and something else will be used instead. |
| if (backgroundBVC.playingTTS) |
| [backgroundBVC startVoiceSearch]; |
| else |
| [self.currentInterface.bvc startVoiceSearch]; |
| } |
| |
| - (void)startQRCodeScanner { |
| if (!self.currentInterface.browser) { |
| return; |
| } |
| id<QRScannerCommands> QRHandler = HandlerForProtocol( |
| self.currentInterface.browser->GetCommandDispatcher(), QRScannerCommands); |
| [QRHandler showQRScanner]; |
| } |
| |
| - (void)startLens { |
| if (!self.currentInterface.browser) { |
| return; |
| } |
| id<LensCommands> lensHandler = HandlerForProtocol( |
| self.currentInterface.browser->GetCommandDispatcher(), LensCommands); |
| [lensHandler |
| openInputSelectionForEntrypoint:LensEntrypoint::HomeScreenWidget]; |
| } |
| |
| - (void)focusOmnibox { |
| if (!self.currentInterface.browser) { |
| return; |
| } |
| id<OmniboxCommands> omniboxCommandsHandler = HandlerForProtocol( |
| self.currentInterface.browser->GetCommandDispatcher(), OmniboxCommands); |
| [omniboxCommandsHandler focusOmnibox]; |
| } |
| |
| - (void)showDefaultBrowserSettings { |
| if (!self.currentInterface.browser) { |
| return; |
| } |
| id<ApplicationSettingsCommands> applicationSettingsCommandsHandler = |
| HandlerForProtocol(self.currentInterface.browser->GetCommandDispatcher(), |
| ApplicationSettingsCommands); |
| [applicationSettingsCommandsHandler |
| showDefaultBrowserSettingsFromViewController:nil |
| sourceForUMA:DefaultBrowserPromoSource:: |
| kExternalIntent]; |
| } |
| |
| #pragma mark - TabOpening implementation. |
| |
| - (void)dismissModalsAndMaybeOpenSelectedTabInMode: |
| (ApplicationModeForTabOpening)targetMode |
| withUrlLoadParams: |
| (const UrlLoadParams&)urlLoadParams |
| dismissOmnibox:(BOOL)dismissOmnibox |
| completion:(ProceduralBlock)completion { |
| // Fallback to NORMAL or INCOGNITO mode if the Incognito interstitial is not |
| // available. |
| if (targetMode == ApplicationModeForTabOpening::UNDETERMINED) { |
| PrefService* prefs = self.mainInterface.browserState->GetPrefs(); |
| BOOL canShowIncognitoInterstitial = |
| base::FeatureList::IsEnabled(kIOS3PIntentsInIncognito) && |
| prefs->GetBoolean(prefs::kIncognitoInterstitialEnabled); |
| |
| if (!canShowIncognitoInterstitial) { |
| targetMode = [self isIncognitoForced] |
| ? ApplicationModeForTabOpening::INCOGNITO |
| : ApplicationModeForTabOpening::NORMAL; |
| } |
| } |
| |
| UrlLoadParams copyOfUrlLoadParams = urlLoadParams; |
| |
| __weak SceneController* weakSelf = self; |
| void (^dismissModalsCompletion)() = ^{ |
| if (targetMode == ApplicationModeForTabOpening::UNDETERMINED) { |
| [weakSelf showIncognitoInterstitialWithUrlLoadParams:copyOfUrlLoadParams]; |
| completion(); |
| } else { |
| [weakSelf openSelectedTabInMode:targetMode |
| withUrlLoadParams:copyOfUrlLoadParams |
| completion:completion]; |
| } |
| }; |
| |
| // Wrap the post-dismiss-modals action with the incognito auth check. |
| if (targetMode == ApplicationModeForTabOpening::INCOGNITO) { |
| IncognitoReauthSceneAgent* reauthAgent = |
| [IncognitoReauthSceneAgent agentFromScene:self.sceneState]; |
| if (reauthAgent.authenticationRequired) { |
| void (^wrappedDismissModalCompletion)() = dismissModalsCompletion; |
| dismissModalsCompletion = ^{ |
| [reauthAgent |
| authenticateIncognitoContentWithCompletionBlock:^(BOOL success) { |
| if (success) { |
| wrappedDismissModalCompletion(); |
| } else { |
| // Do not open the tab, but still call completion. |
| if (completion) { |
| completion(); |
| } |
| } |
| }]; |
| }; |
| } |
| } |
| |
| [self dismissModalDialogsWithCompletion:dismissModalsCompletion |
| dismissOmnibox:dismissOmnibox]; |
| } |
| |
| - (void)dismissModalsAndOpenMultipleTabsWithURLs:(const std::vector<GURL>&)URLs |
| inIncognitoMode:(BOOL)incognitoMode |
| dismissOmnibox:(BOOL)dismissOmnibox |
| completion:(ProceduralBlock)completion { |
| __weak SceneController* weakSelf = self; |
| |
| std::vector<GURL> copyURLs = URLs; |
| |
| ApplicationModeForTabOpening targetMode = |
| incognitoMode ? ApplicationModeForTabOpening::INCOGNITO |
| : ApplicationModeForTabOpening::NORMAL; |
| id<BrowserInterface> targetInterface = |
| [self extractInterfaceBaseOnMode:targetMode]; |
| |
| web::WebState* currentWebState = |
| targetInterface.browser->GetWebStateList()->GetActiveWebState(); |
| |
| if (currentWebState) { |
| web::NavigationManager* navigation_manager = |
| currentWebState->GetNavigationManager(); |
| // Check if the current tab is in the procress of restoration and whether it |
| // is an NTP. If so, add the tabs-opening action to the |
| // RestoreCompletionCallback queue so that the tabs are opened only after |
| // the NTP finishes restoring. This is to avoid an edge where multiple tabs |
| // are trying to open in the middle of NTP restoration, as this will cause |
| // all tabs trying to load into the same NTP, causing a race condition that |
| // results in wrong behavior. |
| if (navigation_manager->IsRestoreSessionInProgress() && |
| IsURLNtp(currentWebState->GetVisibleURL())) { |
| navigation_manager->AddRestoreCompletionCallback(base::BindOnce(^{ |
| [self |
| dismissModalDialogsWithCompletion:^{ |
| [weakSelf openMultipleTabsWithURLs:copyURLs |
| inIncognitoMode:incognitoMode |
| completion:completion]; |
| } |
| dismissOmnibox:dismissOmnibox]; |
| })); |
| return; |
| } |
| } |
| |
| [self |
| dismissModalDialogsWithCompletion:^{ |
| [weakSelf openMultipleTabsWithURLs:copyURLs |
| inIncognitoMode:incognitoMode |
| completion:completion]; |
| } |
| dismissOmnibox:dismissOmnibox]; |
| } |
| |
| - (void)openTabFromLaunchWithParams:(URLOpenerParams*)params |
| startupInformation:(id<StartupInformation>)startupInformation |
| appState:(AppState*)appState { |
| if (params) { |
| [URLOpener |
| handleLaunchOptions:params |
| tabOpener:self |
| connectionInformation:self |
| startupInformation:startupInformation |
| appState:appState |
| prefService:self.currentInterface.browserState->GetPrefs()]; |
| } |
| } |
| |
| - (BOOL)URLIsOpenedInRegularMode:(const GURL&)URL { |
| WebStateList* webStateList = self.mainInterface.browser->GetWebStateList(); |
| return webStateList && webStateList->GetIndexOfWebStateWithURL(URL) != |
| WebStateList::kInvalidIndex; |
| } |
| |
| - (BOOL)shouldOpenNTPTabOnActivationOfBrowser:(Browser*)browser { |
| // Check if there are pending actions that would result in opening a new tab. |
| // In that case, it is not useful to open another tab. |
| for (NSUserActivity* activity in self.sceneState.connectionOptions |
| .userActivities) { |
| if (ActivityIsURLLoad(activity) || ActivityIsTabMove(activity)) { |
| return NO; |
| } |
| } |
| |
| if (self.startupParameters) { |
| return NO; |
| } |
| |
| if (self.mainCoordinator.isTabGridActive) { |
| Browser* mainBrowser = self.mainInterface.browser; |
| Browser* otrBrowser = self.incognitoInterface.browser; |
| // Only attempt to dismiss the tab switcher and open a new tab if: |
| // - there are no tabs open in either tab model, and |
| // - the tab switcher controller is not directly or indirectly presenting |
| // another view controller. |
| if (!(mainBrowser->GetWebStateList()->empty()) || |
| !(otrBrowser->GetWebStateList()->empty())) |
| return NO; |
| |
| // If the tabSwitcher is contained, check if the parent container is |
| // presenting another view controller. |
| if ([self.mainCoordinator.baseViewController |
| .parentViewController presentedViewController]) { |
| return NO; |
| } |
| |
| // Check if the tabSwitcher is directly presenting another view controller. |
| if (self.mainCoordinator.baseViewController.presentedViewController) { |
| return NO; |
| } |
| |
| return YES; |
| } |
| |
| return browser->GetWebStateList()->empty(); |
| } |
| |
| #pragma mark - SceneURLLoadingServiceDelegate |
| |
| // Note that the current tab of `browserCoordinator`'s BVC will normally be |
| // reloaded by this method. If a new tab is about to be added, call |
| // expectNewForegroundTab on the BVC first to avoid extra work and possible page |
| // load side-effects for the tab being replaced. |
| - (void)setCurrentInterfaceForMode:(ApplicationMode)mode { |
| DCHECK(self.interfaceProvider); |
| BOOL incognitio = mode == ApplicationMode::INCOGNITO; |
| id<BrowserInterface> currentInterface = |
| self.interfaceProvider.currentInterface; |
| id<BrowserInterface> newInterface = |
| incognitio ? self.interfaceProvider.incognitoInterface |
| : self.interfaceProvider.mainInterface; |
| if (currentInterface && currentInterface == newInterface) |
| return; |
| |
| // Update the snapshot before switching another application mode. This |
| // ensures that the snapshot is correct when links are opened in a different |
| // application mode. |
| [self updateActiveWebStateSnapshot]; |
| |
| self.interfaceProvider.currentInterface = newInterface; |
| |
| if (!self.activatingBrowser) |
| [self displayCurrentBVCAndFocusOmnibox:NO dismissTabSwitcher:YES]; |
| |
| // Tell the BVC that was made current that it can use the web. |
| [self activateBVCAndMakeCurrentBVCPrimary]; |
| } |
| |
| - (void)dismissModalDialogsWithCompletion:(ProceduralBlock)completion |
| dismissOmnibox:(BOOL)dismissOmnibox { |
| // Immediately hide modals from the provider (alert views, action sheets, |
| // popovers). They will be ultimately dismissed by their owners, but at least, |
| // they are not visible. |
| ios::provider::HideModalViewStack(); |
| |
| // ChromeIdentityService is responsible for the dialogs displayed by the |
| // services it wraps. |
| GetApplicationContext()->GetSystemIdentityManager()->DismissDialogs(); |
| |
| // MailtoHandlerService is responsible for the dialogs displayed by the |
| // services it wraps. |
| MailtoHandlerServiceFactory::GetForBrowserState( |
| self.currentInterface.browserState) |
| ->DismissAllMailtoHandlerInterfaces(); |
| |
| // Then, depending on what the SSO view controller is presented on, dismiss |
| // it. |
| ProceduralBlock completionWithBVC = ^{ |
| DCHECK(self.currentInterface.viewController); |
| DCHECK(!self.mainCoordinator.isTabGridActive); |
| DCHECK(!self.signinCoordinator) |
| << "self.signinCoordinator: " |
| << base::SysNSStringToUTF8([self.signinCoordinator description]); |
| // This will dismiss the SSO view controller. |
| [self.interfaceProvider.currentInterface |
| clearPresentedStateWithCompletion:completion |
| dismissOmnibox:dismissOmnibox]; |
| }; |
| ProceduralBlock completionWithoutBVC = ^{ |
| // `self.currentInterface.bvc` may exist but tab switcher should be |
| // active. |
| DCHECK(self.mainCoordinator.isTabGridActive); |
| DCHECK(!self.signinCoordinator) |
| << "self.signinCoordinator: " |
| << base::SysNSStringToUTF8([self.signinCoordinator description]); |
| // History coordinator can be started on top of the tab grid. |
| // This is not true of the other tab switchers. |
| DCHECK(self.mainCoordinator); |
| [self.mainCoordinator stopChildCoordinatorsWithCompletion:completion]; |
| }; |
| |
| // Select a completion based on whether the BVC is shown. |
| ProceduralBlock chosenCompletion = self.mainCoordinator.isTabGridActive |
| ? completionWithoutBVC |
| : completionWithBVC; |
| |
| [self closePresentedViews:NO completion:chosenCompletion]; |
| |
| // Verify that no modal views are left presented. |
| ios::provider::LogIfModalViewsArePresented(); |
| } |
| |
| - (void)openMultipleTabsWithURLs:(const std::vector<GURL>&)URLs |
| inIncognitoMode:(BOOL)openInIncognito |
| completion:(ProceduralBlock)completion { |
| [self recursiveOpenURLs:URLs |
| inIncognitoMode:openInIncognito |
| currentIndex:0 |
| totalCount:URLs.size() |
| completion:completion]; |
| } |
| |
| // Call `dismissModalsAndMaybeOpenSelectedTabInMode` recursively to open the |
| // list of URLs contained in `URLs`. Achieved through chaining |
| // `dismissModalsAndMaybeOpenSelectedTabInMode` in its completion handler. |
| - (void)recursiveOpenURLs:(const std::vector<GURL>&)URLs |
| inIncognitoMode:(BOOL)incognitoMode |
| currentIndex:(size_t)currentIndex |
| totalCount:(size_t)totalCount |
| completion:(ProceduralBlock)completion { |
| if (currentIndex >= totalCount) { |
| if (completion) { |
| completion(); |
| } |
| return; |
| } |
| |
| GURL webpageGURL = URLs.at(currentIndex); |
| |
| __weak SceneController* weakSelf = self; |
| |
| if (!webpageGURL.is_valid()) { |
| [self recursiveOpenURLs:URLs |
| inIncognitoMode:incognitoMode |
| currentIndex:(currentIndex + 1) |
| totalCount:totalCount |
| completion:completion]; |
| return; |
| } |
| |
| UrlLoadParams param = UrlLoadParams::InNewTab(webpageGURL, webpageGURL); |
| std::vector<GURL> copyURLs = URLs; |
| |
| ApplicationModeForTabOpening mode = |
| incognitoMode ? ApplicationModeForTabOpening::INCOGNITO |
| : ApplicationModeForTabOpening::NORMAL; |
| [self |
| dismissModalsAndMaybeOpenSelectedTabInMode:mode |
| withUrlLoadParams:param |
| dismissOmnibox:YES |
| completion:^{ |
| [weakSelf |
| recursiveOpenURLs:copyURLs |
| inIncognitoMode:incognitoMode |
| currentIndex:(currentIndex + 1) |
| totalCount:totalCount |
| completion:completion]; |
| }]; |
| } |
| |
| // Opens a tab in the target BVC, and switches to it in a way that's appropriate |
| // to the current UI, based on the `dismissModals` flag: |
| // - If a modal dialog is showing and `dismissModals` is NO, the selected tab of |
| // the main tab model will change in the background, but the view won't change. |
| // - Otherwise, any modal view will be dismissed, the tab switcher will animate |
| // out if it is showing, the target BVC will become active, and the new tab will |
| // be shown. |
| // If the current tab in `targetMode` is a NTP, it can be reused to open URL. |
| // `completion` is executed after the tab is opened. After Tab is open the |
| // virtual URL is set to the pending navigation item. |
| - (void)openSelectedTabInMode:(ApplicationModeForTabOpening)tabOpeningTargetMode |
| withUrlLoadParams:(const UrlLoadParams&)urlLoadParams |
| completion:(ProceduralBlock)completion { |
| DCHECK(tabOpeningTargetMode != ApplicationModeForTabOpening::UNDETERMINED); |
| // Update the snapshot before opening a new tab. This ensures that the |
| // snapshot is correct when tabs are openned via the dispatcher. |
| [self updateActiveWebStateSnapshot]; |
| |
| ApplicationMode targetMode; |
| |
| if (tabOpeningTargetMode == ApplicationModeForTabOpening::CURRENT) { |
| targetMode = self.interfaceProvider.currentInterface.incognito |
| ? ApplicationMode::INCOGNITO |
| : ApplicationMode::NORMAL; |
| } else if (tabOpeningTargetMode == ApplicationModeForTabOpening::NORMAL) { |
| targetMode = ApplicationMode::NORMAL; |
| } else { |
| targetMode = ApplicationMode::INCOGNITO; |
| } |
| |
| id<BrowserInterface> targetInterface = |
| targetMode == ApplicationMode::NORMAL |
| ? self.interfaceProvider.mainInterface |
| : self.interfaceProvider.incognitoInterface; |
| ProceduralBlock startupCompletion = |
| [self completionBlockForTriggeringAction:[self.startupParameters |
| postOpeningAction]]; |
| |
| ProceduralBlock tabOpenedCompletion = nil; |
| if (startupCompletion && completion) { |
| tabOpenedCompletion = ^{ |
| // Order is important here. `completion` may do cleaning tasks that will |
| // invalidate `startupCompletion`. |
| startupCompletion(); |
| completion(); |
| }; |
| } else if (startupCompletion) { |
| tabOpenedCompletion = startupCompletion; |
| } else { |
| tabOpenedCompletion = completion; |
| } |
| |
| if (self.mainCoordinator.isTabGridActive) { |
| // If the tab switcher is already being dismissed, simply add the tab and |
| // note that when the tab switcher finishes dismissing, the current BVC |
| // should be switched to be the main BVC if necessary. |
| if (self.activatingBrowser) { |
| self.modeToDisplayOnTabSwitcherDismissal = |
| targetMode == ApplicationMode::NORMAL |
| ? TabSwitcherDismissalMode::NORMAL |
| : TabSwitcherDismissalMode::INCOGNITO; |
| [targetInterface.bvc appendTabAddedCompletion:tabOpenedCompletion]; |
| UrlLoadParams savedParams = urlLoadParams; |
| savedParams.in_incognito = targetMode == ApplicationMode::INCOGNITO; |
| UrlLoadingBrowserAgent::FromBrowser(targetInterface.browser) |
| ->Load(savedParams); |
| } else { |
| // Voice search, QRScanner and the omnibox are presented by the BVC. |
| // They must be started after the BVC view is added in the hierarchy. |
| self.NTPActionAfterTabSwitcherDismissal = |
| [self.startupParameters postOpeningAction]; |
| [self setStartupParameters:nil]; |
| |
| [self addANewTabAndPresentBrowser:targetInterface.browser |
| withURLLoadParams:urlLoadParams]; |
| // In this particular usage, there should be no postOpeningAction, |
| // as triggering voice search while there are multiple windows opened is probably |
| // a bad idea both technically and as a user experience. |
| // It should be the caller duty to not set a completion if they don't need it. |
| if (completion) { |
| completion(); |
| } |
| } |
| } else { |
| if (!self.currentInterface.viewController.presentedViewController) { |
| [targetInterface.bvc expectNewForegroundTab]; |
| } |
| [self setCurrentInterfaceForMode:targetMode]; |
| [self openOrReuseTabInMode:targetMode |
| withUrlLoadParams:urlLoadParams |
| tabOpenedCompletion:tabOpenedCompletion]; |
| } |
| |
| if (self.sceneState.appState.startupInformation.restoreHelper) { |
| // Now that all the operations on the tabs have been done, display the |
| // restore infobar if needed. |
| dispatch_async(dispatch_get_main_queue(), ^{ |
| [self.sceneState.appState.startupInformation |
| .restoreHelper showRestorePrompt]; |
| self.sceneState.appState.startupInformation.restoreHelper = nil; |
| }); |
| } |
| } |
| |
| - (void)expectNewForegroundTabForMode:(ApplicationMode)targetMode { |
| id<BrowserInterface> interface = |
| targetMode == ApplicationMode::INCOGNITO |
| ? self.interfaceProvider.incognitoInterface |
| : self.interfaceProvider.mainInterface; |
| DCHECK(interface); |
| [interface.bvc expectNewForegroundTab]; |
| } |
| |
| - (void)openNewTabFromOriginPoint:(CGPoint)originPoint |
| focusOmnibox:(BOOL)focusOmnibox |
| inheritOpener:(BOOL)inheritOpener { |
| [self.currentInterface.bvc openNewTabFromOriginPoint:originPoint |
| focusOmnibox:focusOmnibox |
| inheritOpener:inheritOpener]; |
| } |
| |
| - (Browser*)currentBrowserForURLLoading { |
| return self.currentInterface.browser; |
| } |
| |
| // Asks the respective Snapshot helper to update the snapshot for the active |
| // WebState. |
| - (void)updateActiveWebStateSnapshot { |
| // Durinhg startup, there may be no current interface. Do nothing in that |
| // case. |
| if (!self.currentInterface) |
| return; |
| |
| WebStateList* webStateList = self.currentInterface.browser->GetWebStateList(); |
| web::WebState* webState = webStateList->GetActiveWebState(); |
| if (webState) { |
| SnapshotTabHelper::FromWebState(webState)->UpdateSnapshotWithCallback(nil); |
| } |
| } |
| |
| // Checks the target BVC's current tab's URL. If this URL is chrome://newtab, |
| // loads `urlLoadParams` in this tab. Otherwise, open `urlLoadParams` in a new |
| // tab in the target BVC. `tabDisplayedCompletion` will be called on the new tab |
| // (if not nil). |
| - (void)openOrReuseTabInMode:(ApplicationMode)targetMode |
| withUrlLoadParams:(const UrlLoadParams&)urlLoadParams |
| tabOpenedCompletion:(ProceduralBlock)tabOpenedCompletion { |
| id<BrowserInterface> targetInterface = targetMode == ApplicationMode::NORMAL |
| ? self.mainInterface |
| : self.incognitoInterface; |
| |
| BrowserViewController* targetBVC = targetInterface.bvc; |
| web::WebState* currentWebState = |
| targetInterface.browser->GetWebStateList()->GetActiveWebState(); |
| |
| BOOL alwaysInsertNewTab = |
| base::FeatureList::IsEnabled(kForceNewTabForIntentSearch) && |
| (self.startupParameters.postOpeningAction == FOCUS_OMNIBOX); |
| |
| // Don't call loadWithParams for chrome://newtab when it's already loaded. |
| // Note that it's safe to use -GetVisibleURL here, as it doesn't matter if the |
| // NTP hasn't finished loading. |
| if (!alwaysInsertNewTab && currentWebState && |
| IsURLNtp(currentWebState->GetVisibleURL()) && |
| IsURLNtp(urlLoadParams.web_params.url)) { |
| if (tabOpenedCompletion) { |
| tabOpenedCompletion(); |
| } |
| return; |
| } |
| |
| // If the current tab isn't an NTP, open a new tab. Be sure to use |
| // -GetLastCommittedURL incase the NTP is still loading. |
| if (alwaysInsertNewTab || |
| !(currentWebState && IsURLNtp(currentWebState->GetVisibleURL()))) { |
| [targetBVC appendTabAddedCompletion:tabOpenedCompletion]; |
| UrlLoadParams newTabParams = urlLoadParams; |
| newTabParams.disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB; |
| newTabParams.in_incognito = targetMode == ApplicationMode::INCOGNITO; |
| UrlLoadingBrowserAgent::FromBrowser(targetInterface.browser) |
| ->Load(newTabParams); |
| return; |
| } |
| // Otherwise, load `urlLoadParams` in the current tab. |
| UrlLoadParams sameTabParams = urlLoadParams; |
| sameTabParams.disposition = WindowOpenDisposition::CURRENT_TAB; |
| UrlLoadingBrowserAgent::FromBrowser(targetInterface.browser) |
| ->Load(sameTabParams); |
| if (tabOpenedCompletion) { |
| tabOpenedCompletion(); |
| } |
| } |
| |
| // Displays current (incognito/normal) BVC and optionally focuses the omnibox. |
| // If `dismissTabSwitcher` is NO, then the tab switcher is not dismissed, |
| // although the BVC will be visible. `dismissTabSwitcher` is only used in the |
| // thumb strip feature. |
| - (void)displayCurrentBVCAndFocusOmnibox:(BOOL)focusOmnibox |
| dismissTabSwitcher:(BOOL)dismissTabSwitcher { |
| ProceduralBlock completion = nil; |
| if (focusOmnibox) { |
| id<OmniboxCommands> omniboxHandler = HandlerForProtocol( |
| self.currentInterface.browser->GetCommandDispatcher(), OmniboxCommands); |
| completion = ^{ |
| [omniboxHandler focusOmnibox]; |
| }; |
| } |
| [self.mainCoordinator |
| showTabViewController:self.currentInterface.viewController |
| incognito:self.currentInterface.incognito |
| shouldCloseTabGrid:dismissTabSwitcher |
| completion:completion]; |
| [HandlerForProtocol(self.currentInterface.browser->GetCommandDispatcher(), |
| ApplicationCommands) |
| setIncognitoContentVisible:self.currentInterface.incognito]; |
| } |
| |
| #pragma mark - Sign In UI presentation |
| |
| // Show trusted vault dialog. |
| // `intent` Dialog to present. |
| // `trigger` UI elements where the trusted vault reauth has been triggered. |
| - (void) |
| showTrustedVaultDialogFromViewController:(UIViewController*)viewController |
| intent: |
| (SigninTrustedVaultDialogIntent)intent |
| trigger: |
| (syncer:: |
| TrustedVaultUserActionTriggerForUMA) |
| trigger { |
| DCHECK(!self.signinCoordinator) |
| << "self.signinCoordinator: " |
| << base::SysNSStringToUTF8([self.signinCoordinator description]); |
| Browser* mainBrowser = self.mainInterface.browser; |
| self.signinCoordinator = [SigninCoordinator |
| trustedVaultReAuthenticationCoordinatorWithBaseViewController: |
| viewController |
| browser:mainBrowser |
| intent:intent |
| trigger:trigger]; |
| [self startSigninCoordinatorWithCompletion:nil]; |
| } |
| |
| - (void)presentSignedInAccountsViewControllerForBrowserState: |
| (ChromeBrowserState*)browserState { |
| UMA_HISTOGRAM_BOOLEAN("Signin.SignedInAccountsViewImpression", true); |
| id<ApplicationSettingsCommands> settingsHandler = |
| HandlerForProtocol(self.mainInterface.browser->GetCommandDispatcher(), |
| ApplicationSettingsCommands); |
| UIViewController* accountsViewController = |
| [[SignedInAccountsViewController alloc] |
| initWithBrowserState:browserState |
| dispatcher:settingsHandler]; |
| [[self topPresentedViewController] |
| presentViewController:accountsViewController |
| animated:YES |
| completion:nil]; |
| } |
| |
| // Close Settings, or Signin or the 3rd-party intents Incognito interstitial. |
| - (void)closePresentedViews:(BOOL)animated |
| completion:(ProceduralBlock)completion { |
| __weak __typeof(self) weakSelf = self; |
| BOOL resetSigninState = self.signinCoordinator != nil; |
| completion = ^{ |
| __typeof(self) strongSelf = weakSelf; |
| if (completion) { |
| completion(); |
| } |
| if (resetSigninState) { |
| strongSelf.sceneState.signinInProgress = NO; |
| } |
| strongSelf.dismissingSigninPromptFromExternalTrigger = NO; |
| }; |
| |
| if (self.settingsNavigationController) { |
| ProceduralBlock dismissSettings = ^() { |
| [self.settingsNavigationController cleanUpSettings]; |
| UIViewController* presentingViewController = |
| [self.settingsNavigationController presentingViewController]; |
| // If presentingViewController is nil it means the VC was already |
| // dismissed by some other action like swiping down. |
| DCHECK(presentingViewController); |
| [presentingViewController dismissViewControllerAnimated:animated |
| completion:completion]; |
| self.settingsNavigationController = nil; |
| }; |
| // `self.signinCoordinator` can be presented on top of the settings, to |
| // present the Trusted Vault reauthentication `self.signinCoordinator` has |
| // to be closed first. |
| if (self.signinCoordinator) { |
| [self interruptSigninCoordinatorAnimated:animated |
| completion:dismissSettings]; |
| } else if (dismissSettings) { |
| dismissSettings(); |
| } |
| } else if (self.signinCoordinator) { |
| // `self.signinCoordinator` can be presented without settings, from the |
| // bookmarks or the recent tabs view. |
| [self interruptSigninCoordinatorAnimated:animated completion:completion]; |
| } else if (self.incognitoInterstitialCoordinator) { |
| [self.incognitoInterstitialCoordinator stop]; |
| self.incognitoInterstitialCoordinator = nil; |
| completion(); |
| } else { |
| completion(); |
| } |
| } |
| |
| - (UIViewController*)topPresentedViewController { |
| // TODO(crbug.com/754642): Implement TopPresentedViewControllerFrom() |
| // privately. |
| return top_view_controller::TopPresentedViewControllerFrom( |
| self.mainCoordinator.baseViewController); |
| } |
| |
| // Interrupts the sign-in coordinator actions and dismisses its views either |
| // with or without animation. |
| - (void)interruptSigninCoordinatorAnimated:(BOOL)animated |
| completion:(ProceduralBlock)completion { |
| DCHECK(self.signinCoordinator); |
| SigninCoordinatorInterruptAction action = |
| animated ? SigninCoordinatorInterruptActionDismissWithAnimation |
| : SigninCoordinatorInterruptActionDismissWithoutAnimation; |
| |
| self.dismissingSigninPromptFromExternalTrigger = YES; |
| [self.signinCoordinator interruptWithAction:action completion:completion]; |
| } |
| |
| // Starts the sign-in coordinator with a default cleanup completion. |
| - (void)startSigninCoordinatorWithCompletion: |
| (signin_ui::CompletionCallback)completion { |
| DCHECK(self.signinCoordinator); |
| AuthenticationService* authenticationService = |
| AuthenticationServiceFactory::GetForBrowserState( |
| self.sceneState.appState.mainBrowserState); |
| switch (authenticationService->GetServiceStatus()) { |
| case AuthenticationService::ServiceStatus::SigninDisabledByPolicy: { |
| if (completion) { |
| completion(/*success=*/NO); |
| } |
| [self.signinCoordinator stop]; |
| id<PolicyChangeCommands> handler = HandlerForProtocol( |
| self.signinCoordinator.browser->GetCommandDispatcher(), |
| PolicyChangeCommands); |
| [handler showForceSignedOutPrompt]; |
| self.signinCoordinator = nil; |
| return; |
| } |
| case AuthenticationService::ServiceStatus::SigninForcedByPolicy: |
| case AuthenticationService::ServiceStatus::SigninAllowed: { |
| break; |
| } |
| case AuthenticationService::ServiceStatus::SigninDisabledByInternal: |
| case AuthenticationService::ServiceStatus::SigninDisabledByUser: { |
| NOTREACHED(); |
| break; |
| } |
| } |
| |
| DCHECK(self.signinCoordinator); |
| self.sceneState.signinInProgress = YES; |
| |
| __block std::unique_ptr<ScopedUIBlocker> uiBlocker = |
| std::make_unique<ScopedUIBlocker>(self.sceneState); |
| __weak SceneController* weakSelf = self; |
| self.signinCoordinator.signinCompletion = |
| ^(SigninCoordinatorResult result, SigninCompletionInfo* info) { |
| if (!weakSelf) |
| return; |
| __typeof(self) strongSelf = weakSelf; |
| [strongSelf.signinCoordinator stop]; |
| strongSelf.signinCoordinator = nil; |
| uiBlocker.reset(); |
| |
| if (completion) { |
| completion(result == SigninCoordinatorResultSuccess); |
| } |
| |
| if (!weakSelf.dismissingSigninPromptFromExternalTrigger) { |
| // If the coordinator isn't stopped by an external trigger, sign-in |
| // is done. Otherwise, there might be extra steps to be done before |
| // considering sign-in as done. This is up to the handler that sets |
| // `self.dismissingSigninPromptFromExternalTrigger` to YES to set |
| // back `signinInProgress` to NO. |
| weakSelf.sceneState.signinInProgress = NO; |
| } |
| |
| switch (info.signinCompletionAction) { |
| case SigninCompletionActionNone: |
| break; |
| case SigninCompletionActionShowManagedLearnMore: |
| id<ApplicationCommands> dispatcher = HandlerForProtocol( |
| strongSelf.mainInterface.browser->GetCommandDispatcher(), |
| ApplicationCommands); |
| OpenNewTabCommand* command = [OpenNewTabCommand |
| commandWithURLFromChrome:GURL(kChromeUIManagementURL)]; |
| [dispatcher closeSettingsUIAndOpenURL:command]; |
| break; |
| } |
| |
| if (IsSigninForcedByPolicy()) { |
| // Handle intents after sign-in is done when the forced sign-in policy |
| // is enabled. |
| [strongSelf handleExternalIntents]; |
| } |
| |
| }; |
| |
| [self.signinCoordinator start]; |
| } |
| |
| #pragma mark - WebStateListObserving |
| |
| // Called when a WebState is removed. Triggers the switcher view when the last |
| // WebState is closed on a device that uses the switcher. |
| - (void)webStateList:(WebStateList*)notifiedWebStateList |
| didDetachWebState:(web::WebState*)webState |
| atIndex:(int)atIndex { |
| // Do nothing on initialization. |
| if (!self.currentInterface.browser) |
| return; |
| |
| if (notifiedWebStateList->empty()) { |
| if (webState->GetBrowserState()->IsOffTheRecord()) { |
| [self lastIncognitoTabClosed]; |
| } else { |
| [self lastRegularTabClosed]; |
| } |
| } |
| } |
| |
| #pragma mark - IncognitoInterstitialCoordinatorDelegate |
| |
| - (void)shouldStopIncognitoInterstitial: |
| (IncognitoInterstitialCoordinator*)incognitoInterstitial { |
| DCHECK(incognitoInterstitial == self.incognitoInterstitialCoordinator); |
| [self closePresentedViews:YES completion:nil]; |
| } |
| |
| #pragma mark - Helpers for web state list events |
| |
| // Called when the last incognito tab was closed. |
| - (void)lastIncognitoTabClosed { |
| // If no other window has incognito tab, then destroy and rebuild the |
| // BrowserState. Otherwise, just do the state transition animation. |
| if ([self shouldDestroyAndRebuildIncognitoBrowserState]) { |
| // Incognito browser state cannot be deleted before all the requests are |
| // deleted. Queue empty task on IO thread and destroy the BrowserState |
| // when the task has executed, again verifying that no incognito tabs are |
| // present. When an incognito tab is moved between browsers, there is |
| // a point where the tab isn't attached to any web state list. However, when |
| // this queued cleanup step executes, the moved tab will be attached, so |
| // the cleanup shouldn't proceed. |
| |
| auto cleanup = ^{ |
| if ([self shouldDestroyAndRebuildIncognitoBrowserState]) { |
| [self destroyAndRebuildIncognitoBrowserState]; |
| } |
| }; |
| |
| web::GetIOThreadTaskRunner({})->PostTaskAndReply( |
| FROM_HERE, base::DoNothing(), base::BindRepeating(cleanup)); |
| } |
| |
| // a) The first condition can happen when the last incognito tab is closed |
| // from the tab switcher. |
| // b) The second condition can happen if some other code (like JS) triggers |
| // closure of tabs from the otr tab model when it's not current. |
| // Nothing to do here. The next user action (like clicking on an existing |
| // regular tab or creating a new incognito tab from the settings menu) will |
| // take care of the logic to mode switch. |
| if (self.mainCoordinator.isTabGridActive || |
| !self.currentInterface.incognito) { |
| return; |
| } |
| [self showTabSwitcher]; |
| } |
| |
| // Called when the last regular tab was closed. |
| - (void)lastRegularTabClosed { |
| // a) The first condition can happen when the last regular tab is closed from |
| // the tab switcher. |
| // b) The second condition can happen if some other code (like JS) triggers |
| // closure of tabs from the main tab model when the main tab model is not |
| // current. |
| // Nothing to do here. |
| if (self.mainCoordinator.isTabGridActive || self.currentInterface.incognito) { |
| return; |
| } |
| |
| [self showTabSwitcher]; |
| } |
| |
| // Clears incognito data that is specific to iOS and won't be cleared by |
| // deleting the browser state. |
| - (void)clearIOSSpecificIncognitoData { |
| DCHECK(self.sceneState.appState.mainBrowserState |
| ->HasOffTheRecordChromeBrowserState()); |
| ChromeBrowserState* otrBrowserState = |
| self.sceneState.appState.mainBrowserState |
| ->GetOffTheRecordChromeBrowserState(); |
| [self.browsingDataCommandsHandler |
| removeBrowsingDataForBrowserState:otrBrowserState |
| timePeriod:browsing_data::TimePeriod::ALL_TIME |
| removeMask:BrowsingDataRemoveMask::REMOVE_ALL |
| completionBlock:^{ |
| [self activateBVCAndMakeCurrentBVCPrimary]; |
| }]; |
| } |
| |
| - (void)activateBVCAndMakeCurrentBVCPrimary { |
| // If there are pending removal operations, the activation will be deferred |
| // until the callback is received. |
| BrowsingDataRemover* browsingDataRemover = |
| BrowsingDataRemoverFactory::GetForBrowserStateIfExists( |
| self.currentInterface.browserState); |
| if (browsingDataRemover && browsingDataRemover->IsRemoving()) |
| return; |
| |
| self.interfaceProvider.mainInterface.userInteractionEnabled = YES; |
| self.interfaceProvider.incognitoInterface.userInteractionEnabled = YES; |
| [self.currentInterface setPrimary:YES]; |
| } |
| |
| // Shows the tab switcher UI. |
| - (void)showTabSwitcher { |
| DCHECK(self.mainCoordinator); |
| [self.mainCoordinator setActivePage:self.activePage]; |
| [self.mainCoordinator setActiveMode:TabGridModeNormal]; |
| [self.mainCoordinator showTabGrid]; |
| } |
| |
| - (void)openURLContexts:(NSSet<UIOpenURLContext*>*)URLContexts { |
| if (self.sceneState.appState.initStage <= InitStageNormalUI || |
| !self.currentInterface.browserState) { |
| // Don't handle the intent if the browser UI objects aren't yet initialized. |
| // This is the case when the app is in safe mode or may be the case when the |
| // app is going through an odd sequence of lifecyle events (shouldn't happen |
| // but happens somehow), see crbug.com/1211006 for more details. |
| return; |
| } |
| |
| NSMutableSet<URLOpenerParams*>* URLsToOpen = [[NSMutableSet alloc] init]; |
| for (UIOpenURLContext* context : URLContexts) { |
| URLOpenerParams* options = |
| [[URLOpenerParams alloc] initWithUIOpenURLContext:context]; |
| NSSet* URLContextSet = [NSSet setWithObject:context]; |
| if (!GetApplicationContext() |
| ->GetSystemIdentityManager() |
| ->HandleSessionOpenURLContexts(self.sceneState.scene, |
| URLContextSet)) { |
| [URLsToOpen addObject:options]; |
| } |
| } |
| // When opening with URLs for GetChromeIdentityService, it is expected that a |
| // single URL is passed. |
| DCHECK(URLsToOpen.count == URLContexts.count || URLContexts.count == 1); |
| BOOL active = [self canHandleIntents]; |
| |
| for (URLOpenerParams* options : URLsToOpen) { |
| [URLOpener openURL:options |
| applicationActive:active |
| tabOpener:self |
| connectionInformation:self |
| startupInformation:self.sceneState.appState.startupInformation |
| prefService:self.currentInterface.browserState->GetPrefs() |
| initStage:self.sceneState.appState.initStage]; |
| } |
| } |
| |
| - (id<BrowserInterface>)extractInterfaceBaseOnMode: |
| (ApplicationModeForTabOpening)targetMode { |
| DCHECK(targetMode != ApplicationModeForTabOpening::UNDETERMINED); |
| ApplicationMode applicationMode; |
| |
| if (targetMode == ApplicationModeForTabOpening::CURRENT) { |
| applicationMode = self.interfaceProvider.currentInterface.incognito |
| ? ApplicationMode::INCOGNITO |
| : ApplicationMode::NORMAL; |
| } else if (targetMode == ApplicationModeForTabOpening::NORMAL) { |
| applicationMode = ApplicationMode::NORMAL; |
| } else { |
| applicationMode = ApplicationMode::INCOGNITO; |
| } |
| |
| id<BrowserInterface> targetInterface = |
| applicationMode == ApplicationMode::NORMAL |
| ? self.interfaceProvider.mainInterface |
| : self.interfaceProvider.incognitoInterface; |
| |
| return targetInterface; |
| } |
| |
| #pragma mark - TabGrid helpers |
| |
| // Returns the page that should be active in the TabGrid. |
| - (TabGridPage)activePage { |
| if (self.currentInterface.browser == self.incognitoInterface.browser) |
| return TabGridPageIncognitoTabs; |
| return TabGridPageRegularTabs; |
| } |
| |
| // Adds a new tab to the `browser` based on `urlLoadParams` and then presents |
| // it. |
| - (void)addANewTabAndPresentBrowser:(Browser*)browser |
| withURLLoadParams:(const UrlLoadParams&)urlLoadParams { |
| TabInsertionBrowserAgent::FromBrowser(browser)->InsertWebState( |
| urlLoadParams.web_params, nil, false, browser->GetWebStateList()->count(), |
| /*in_background=*/false, /*inherit_opener=*/false, |
| /*should_show_start_surface=*/false, |
| /*should_skip_new_tab_animation=*/urlLoadParams.from_external); |
| [self beginActivatingBrowser:browser dismissTabSwitcher:YES focusOmnibox:NO]; |
| } |
| |
| #pragma mark - Handling of destroying the incognito BrowserState |
| |
| // The incognito BrowserState should be closed when the last incognito tab is |
| // closed (i.e. if there are other incognito tabs open in another Scene, the |
| // BrowserState must not be destroyed). |
| - (BOOL)shouldDestroyAndRebuildIncognitoBrowserState { |
| ChromeBrowserState* mainBrowserState = |
| self.sceneState.appState.mainBrowserState; |
| if (!mainBrowserState->HasOffTheRecordChromeBrowserState()) |
| return NO; |
| |
| ChromeBrowserState* otrBrowserState = |
| mainBrowserState->GetOffTheRecordChromeBrowserState(); |
| DCHECK(otrBrowserState); |
| |
| BrowserList* browserList = |
| BrowserListFactory::GetForBrowserState(otrBrowserState); |
| for (Browser* browser : browserList->AllIncognitoBrowsers()) { |
| WebStateList* webStateList = browser->GetWebStateList(); |
| if (!webStateList->empty()) |
| return NO; |
| } |
| |
| return YES; |
| } |
| |
| // Destroys and rebuilds the incognito BrowserState. This will inform all the |
| // other SceneController to destroy state tied to the BrowserState and to |
| // recreate it. |
| - (void)destroyAndRebuildIncognitoBrowserState { |
| // This seems the best place to mark the start of destroying the incognito |
| // browser state. |
| crash_keys::SetDestroyingAndRebuildingIncognitoBrowserState( |
| /*in_progress=*/true); |
| |
| [self clearIOSSpecificIncognitoData]; |
| |
| ChromeBrowserState* mainBrowserState = |
| self.sceneState.appState.mainBrowserState; |
| DCHECK(mainBrowserState->HasOffTheRecordChromeBrowserState()); |
| |
| NSMutableArray<SceneController*>* sceneControllers = |
| [[NSMutableArray alloc] init]; |
| for (SceneState* sceneState in [self.sceneState.appState connectedScenes]) { |
| SceneController* sceneController = sceneState.controller; |
| // In some circumstances, the scene state may still exist while the |
| // corresponding scene controller has been deallocated. |
| // (see crbug.com/1142782). |
| if (sceneController) { |
| [sceneControllers addObject:sceneController]; |
| } |
| } |
| |
| for (SceneController* sceneController in sceneControllers) { |
| [sceneController willDestroyIncognitoBrowserState]; |
| } |
| |
| // Record off-the-record metrics before detroying the BrowserState. |
| if (mainBrowserState->HasOffTheRecordChromeBrowserState()) { |
| ChromeBrowserState* otrBrowserState = |
| mainBrowserState->GetOffTheRecordChromeBrowserState(); |
| |
| SessionMetrics::FromBrowserState(otrBrowserState) |
| ->RecordAndClearSessionMetrics(MetricsToRecordFlags::kNoMetrics); |
| } |
| |
| // Destroy and recreate the off-the-record BrowserState. |
| mainBrowserState->DestroyOffTheRecordChromeBrowserState(); |
| mainBrowserState->GetOffTheRecordChromeBrowserState(); |
| |
| for (SceneController* sceneController in sceneControllers) { |
| [sceneController incognitoBrowserStateCreated]; |
| } |
| |
| if (base::FeatureList::IsEnabled(breadcrumbs::kLogBreadcrumbs)) { |
| BreadcrumbManagerKeyedServiceFactory::GetForBrowserState( |
| mainBrowserState->GetOffTheRecordChromeBrowserState()); |
| } |
| |
| // This seems the best place to deem the destroying and rebuilding the |
| // incognito browser state to be completed. |
| crash_keys::SetDestroyingAndRebuildingIncognitoBrowserState( |
| /*in_progress=*/false); |
| } |
| |
| - (void)willDestroyIncognitoBrowserState { |
| // Clear the Incognito Browser and notify the TabGrid that its otrBrowser |
| // will be destroyed. |
| self.mainCoordinator.incognitoBrowser = nil; |
| |
| if (base::FeatureList::IsEnabled(breadcrumbs::kLogBreadcrumbs)) { |
| BreadcrumbManagerBrowserAgent::FromBrowser(self.incognitoInterface.browser) |
| ->SetLoggingEnabled(false); |
| } |
| |
| self.incognitoInterface.browser->GetWebStateList()->RemoveObserver( |
| _webStateListForwardingObserver.get()); |
| [self.browserViewWrangler willDestroyIncognitoBrowserState]; |
| } |
| |
| - (void)incognitoBrowserStateCreated { |
| [self.browserViewWrangler incognitoBrowserStateCreated]; |
| |
| // There should be a new URL loading browser agent for the incognito browser, |
| // so set the scene URL loading service on it. |
| UrlLoadingBrowserAgent::FromBrowser(self.incognitoInterface.browser) |
| ->SetSceneService(self.sceneURLLoadingService); |
| self.incognitoInterface.browser->GetWebStateList()->AddObserver( |
| _webStateListForwardingObserver.get()); |
| |
| if (self.currentInterface.incognito) { |
| [self activateBVCAndMakeCurrentBVCPrimary]; |
| } |
| |
| // Always set the new otr Browser for the tablet or grid switcher. |
| // Notify the TabGrid with the new Incognito Browser. |
| self.mainCoordinator.incognitoBrowser = self.incognitoInterface.browser; |
| self.mainCoordinator.incognitoThumbStripSupporting = |
| self.incognitoInterface.bvc; |
| } |
| |
| #pragma mark - PolicyWatcherBrowserAgentObserving |
| |
| - (void)policyWatcherBrowserAgentNotifySignInDisabled: |
| (PolicyWatcherBrowserAgent*)policyWatcher { |
| auto signinInterrupted = ^{ |
| policyWatcher->SignInUIDismissed(); |
| }; |
| |
| if (self.signinCoordinator) { |
| [self interruptSigninCoordinatorAnimated:YES completion:signinInterrupted]; |
| UMA_HISTOGRAM_BOOLEAN( |
| "Enterprise.BrowserSigninIOS.SignInInterruptedByPolicy", true); |
| } |
| } |
| |
| #pragma mark - SceneUIProvider |
| |
| - (UIViewController*)activeViewController { |
| return self.mainCoordinator.activeViewController; |
| } |
| |
| @end |