// 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/settings/google_services/manage_sync_settings_coordinator.h"
#import "base/check_op.h"
#import "base/ios/block_types.h"
#import "base/metrics/user_metrics.h"
#import "base/metrics/user_metrics_action.h"
#import "base/notreached.h"
#import "components/google/core/common/google_util.h"
#import "components/search_engines/template_url_service.h"
#import "components/signin/public/base/signin_metrics.h"
#import "components/strings/grit/components_strings.h"
#import "components/sync/service/sync_service.h"
#import "components/sync/service/sync_service_utils.h"
#import "components/sync/service/sync_user_settings.h"
#import "components/trusted_vault/trusted_vault_server_constants.h"
#import "ios/chrome/browser/search_engines/model/template_url_service_factory.h"
#import "ios/chrome/browser/shared/coordinator/alert/action_sheet_coordinator.h"
#import "ios/chrome/browser/shared/coordinator/alert/alert_coordinator.h"
#import "ios/chrome/browser/shared/model/application_context/application_context.h"
#import "ios/chrome/browser/shared/model/browser/browser.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/shared/model/url/chrome_url_constants.h"
#import "ios/chrome/browser/shared/public/commands/application_commands.h"
#import "ios/chrome/browser/shared/public/commands/browser_commands.h"
#import "ios/chrome/browser/shared/public/commands/command_dispatcher.h"
#import "ios/chrome/browser/shared/public/commands/open_new_tab_command.h"
#import "ios/chrome/browser/shared/public/commands/settings_commands.h"
#import "ios/chrome/browser/shared/public/commands/show_signin_command.h"
#import "ios/chrome/browser/shared/public/commands/snackbar_commands.h"
#import "ios/chrome/browser/shared/ui/symbols/chrome_icon.h"
#import "ios/chrome/browser/shared/ui/table_view/table_view_utils.h"
#import "ios/chrome/browser/signin/model/authentication_service.h"
#import "ios/chrome/browser/signin/model/authentication_service_factory.h"
#import "ios/chrome/browser/signin/model/chrome_account_manager_service_factory.h"
#import "ios/chrome/browser/signin/model/identity_manager_factory.h"
#import "ios/chrome/browser/signin/model/system_identity_manager.h"
#import "ios/chrome/browser/sync/model/sync_observer_bridge.h"
#import "ios/chrome/browser/sync/model/sync_service_factory.h"
#import "ios/chrome/browser/ui/authentication/signout_action_sheet/signout_action_sheet_coordinator.h"
#import "ios/chrome/browser/ui/settings/google_services/bulk_upload/bulk_upload_coordinator.h"
#import "ios/chrome/browser/ui/settings/google_services/bulk_upload/bulk_upload_coordinator_delegate.h"
#import "ios/chrome/browser/ui/settings/google_services/features.h"
#import "ios/chrome/browser/ui/settings/google_services/manage_accounts/accounts_coordinator.h"
#import "ios/chrome/browser/ui/settings/google_services/manage_sync_settings_command_handler.h"
#import "ios/chrome/browser/ui/settings/google_services/manage_sync_settings_constants.h"
#import "ios/chrome/browser/ui/settings/google_services/manage_sync_settings_mediator.h"
#import "ios/chrome/browser/ui/settings/google_services/manage_sync_settings_table_view_controller.h"
#import "ios/chrome/browser/ui/settings/google_services/personalize_google_services_coordinator.h"
#import "ios/chrome/browser/ui/settings/google_services/sync_error_settings_command_handler.h"
#import "ios/chrome/browser/ui/settings/settings_controller_protocol.h"
#import "ios/chrome/browser/ui/settings/settings_navigation_controller.h"
#import "ios/chrome/browser/ui/settings/sync/sync_encryption_passphrase_table_view_controller.h"
#import "ios/chrome/browser/ui/settings/sync/sync_encryption_table_view_controller.h"
#import "ios/chrome/grit/ios_strings.h"
#import "net/base/apple/url_conversions.h"
#import "ui/base/l10n/l10n_util.h"
using signin_metrics::AccessPoint;
using signin_metrics::PromoAction;
using DismissViewCallback = SystemIdentityManager::DismissViewCallback;
@interface ManageSyncSettingsCoordinator () <
BulkUploadCoordinatorDelegate,
ManageSyncSettingsCommandHandler,
ManageSyncSettingsTableViewControllerPresentationDelegate,
PersonalizeGoogleServicesCoordinatorDelegate,
SettingsNavigationControllerDelegate,
SignoutActionSheetCoordinatorDelegate,
SyncErrorSettingsCommandHandler,
SyncObserverModelBridge> {
// Sync observer.
std::unique_ptr<SyncObserverBridge> _syncObserver;
// Whether Settings have been dismissed.
BOOL _settingsAreDismissed;
// The coordinator for the view Save in Account.
BulkUploadCoordinator* _bulkUploadCoordinator;
// The coordinator for the Accounts view.
AccountsCoordinator* _accountsCoordinator;
SyncEncryptionTableViewController* _syncEncryptionTableViewController;
SyncEncryptionPassphraseTableViewController*
_syncEncryptionPassphraseTableViewController;
}
// View controller.
@property(nonatomic, strong)
ManageSyncSettingsTableViewController* viewController;
// Mediator.
@property(nonatomic, strong) ManageSyncSettingsMediator* mediator;
// The navigation controller used to present child controllers of
// ManageSyncSettings.
@property(nonatomic, readonly)
UINavigationController* _navigationControllerForChildPages;
// Sync service.
@property(nonatomic, assign, readonly) syncer::SyncService* syncService;
// Authentication service.
@property(nonatomic, assign, readonly) AuthenticationService* authService;
// Displays the sign-out options for a syncing user.
@property(nonatomic, strong)
SignoutActionSheetCoordinator* signoutActionSheetCoordinator;
@property(nonatomic, assign) BOOL signOutFlowInProgress;
@end
@implementation ManageSyncSettingsCoordinator {
// Dismiss callback for Web and app setting details view.
DismissViewCallback _dismissWebAndAppSettingDetailsController;
// Dismiss callback for account details view.
DismissViewCallback _accountDetailsControllerDismissCallback;
// The account sync state.
SyncSettingsAccountState _accountState;
// The navigation controller to use only when presenting the
// ManageSyncSettings modally.
SettingsNavigationController* _navigationControllerInModalView;
// The coordinator for the Personalize Google Services view.
PersonalizeGoogleServicesCoordinator* _personalizeGoogleServicesCoordinator;
// Prevents any data from syncing while the UI is open.
// TODO(crbug.com/40066949): This is currently needed for syncing users,
// otherwise accidentally touching a toggle immediately uploads existing data.
// For non-syncing users that's not true. So remove this after the syncing
// state is gone on iOS.
std::unique_ptr<syncer::SyncSetupInProgressHandle> _syncSetupInProgressHandle;
}
@synthesize baseNavigationController = _baseNavigationController;
- (instancetype)initWithBaseNavigationController:
(UINavigationController*)navigationController
browser:(Browser*)browser
accountState:
(SyncSettingsAccountState)accountState {
if ((self = [super initWithBaseViewController:navigationController
browser:browser])) {
_baseNavigationController = navigationController;
_accountState = accountState;
}
return self;
}
- (void)start {
ChromeBrowserState* browserState = self.browser->GetBrowserState();
syncer::SyncService* syncService =
SyncServiceFactory::GetForBrowserState(browserState);
switch (_accountState) {
case SyncSettingsAccountState::kSyncing:
// Ensure that SyncService::IsSetupInProgress is true while the
// manage-sync-settings UI is open.
_syncSetupInProgressHandle = syncService->GetSetupInProgressHandle();
break;
case SyncSettingsAccountState::kSignedIn:
break;
case SyncSettingsAccountState::kSignedOut:
NOTREACHED_IN_MIGRATION();
break;
}
self.mediator = [[ManageSyncSettingsMediator alloc]
initWithSyncService:self.syncService
identityManager:IdentityManagerFactory::GetForBrowserState(
browserState)
authenticationService:self.authService
accountManagerService:ChromeAccountManagerServiceFactory::
GetForBrowserState(browserState)
prefService:browserState->GetPrefs()
initialAccountState:_accountState];
self.mediator.commandHandler = self;
self.mediator.syncErrorHandler = self;
self.mediator.forcedSigninEnabled =
self.authService->GetServiceStatus() ==
AuthenticationService::ServiceStatus::SigninForcedByPolicy;
if (IsLinkedServicesSettingIosEnabled()) {
self.mediator.isEEAAccount =
ios::TemplateURLServiceFactory::GetForBrowserState(
self.browser->GetBrowserState())
->IsEeaChoiceCountry();
}
ManageSyncSettingsTableViewController* viewController =
[[ManageSyncSettingsTableViewController alloc]
initWithStyle:ChromeTableViewStyle()];
self.viewController = viewController;
NSString* title = self.mediator.overrideViewControllerTitle;
if (!title) {
title = self.delegate.manageSyncSettingsCoordinatorTitle;
}
viewController.title = title;
viewController.isAccountStateSignedIn =
_accountState == SyncSettingsAccountState::kSignedIn;
viewController.serviceDelegate = self.mediator;
viewController.presentationDelegate = self;
viewController.modelDelegate = self.mediator;
CommandDispatcher* dispatcher = self.browser->GetCommandDispatcher();
viewController.applicationHandler =
HandlerForProtocol(dispatcher, ApplicationCommands);
viewController.browserHandler =
HandlerForProtocol(dispatcher, BrowserCommands);
viewController.settingsHandler =
HandlerForProtocol(dispatcher, SettingsCommands);
viewController.snackbarHandler =
HandlerForProtocol(dispatcher, SnackbarCommands);
self.mediator.consumer = viewController;
CHECK(_baseNavigationController, base::NotFatalUntil::M129);
[self.baseNavigationController pushViewController:viewController
animated:YES];
_syncObserver = std::make_unique<SyncObserverBridge>(self, self.syncService);
}
- (void)stop {
[super stop];
[self.mediator disconnect];
[self stopBulkUpload];
[_accountsCoordinator stop];
_accountsCoordinator = nil;
self.mediator = nil;
self.viewController = nil;
// Unblock any sync data type changes.
_syncSetupInProgressHandle.reset();
[_syncEncryptionPassphraseTableViewController settingsWillBeDismissed];
_syncEncryptionPassphraseTableViewController = nil;
[_syncEncryptionTableViewController settingsWillBeDismissed];
_syncEncryptionTableViewController = nil;
_syncObserver.reset();
[self.signoutActionSheetCoordinator stop];
_signoutActionSheetCoordinator = nil;
[self stopPersonalizedGoogleServicesCoordinator];
}
#pragma mark - Properties
- (UINavigationController*)navigationControllerForChildPages {
if (_baseNavigationController) {
return _baseNavigationController;
}
CHECK(_navigationControllerInModalView);
return _navigationControllerInModalView;
}
- (syncer::SyncService*)syncService {
return SyncServiceFactory::GetForBrowserState(
self.browser->GetBrowserState());
}
- (AuthenticationService*)authService {
return AuthenticationServiceFactory::GetForBrowserState(
self.browser->GetBrowserState());
}
#pragma mark - Private
- (void)resetDismissAccountDetailsController {
_accountDetailsControllerDismissCallback.Reset();
}
- (void)stopBulkUpload {
[_bulkUploadCoordinator stop];
_bulkUploadCoordinator.delegate = nil;
_bulkUploadCoordinator = nil;
}
- (void)stopPersonalizedGoogleServicesCoordinator {
[_personalizeGoogleServicesCoordinator stop];
_personalizeGoogleServicesCoordinator = nil;
}
// Closes the Manage sync settings view controller.
- (void)closeManageSyncSettings {
if (_settingsAreDismissed) {
return;
}
if (self.viewController.navigationController) {
if (!_dismissWebAndAppSettingDetailsController.is_null()) {
std::move(_dismissWebAndAppSettingDetailsController)
.Run(/*animated*/ false);
}
if (!_accountDetailsControllerDismissCallback.is_null()) {
std::move(_accountDetailsControllerDismissCallback)
.Run(/*animated=*/false);
}
NSEnumerator<UIViewController*>* inversedViewControllers =
[self.navigationControllerForChildPages
.viewControllers reverseObjectEnumerator];
for (UIViewController* controller in inversedViewControllers) {
if (controller == self.viewController) {
break;
}
if ([controller respondsToSelector:@selector(settingsWillBeDismissed)]) {
[controller performSelector:@selector(settingsWillBeDismissed)];
}
}
if (_baseNavigationController) {
if (self.viewController.presentedViewController) {
if ([self.viewController.presentedViewController
isKindOfClass:[SettingsNavigationController class]]) {
[self.viewController.presentedViewController
performSelector:@selector(closeSettings)];
} else {
[self.viewController.presentedViewController.presentingViewController
dismissViewControllerAnimated:YES
completion:nil];
}
}
if (self.baseNavigationController.viewControllers.count == 1) {
// If the manage sync settings is the only view in
// `baseNavigationController`, `baseNavigationController` needs to be
// closed.
CHECK([self.baseNavigationController
isKindOfClass:[SettingsNavigationController class]],
base::NotFatalUntil::M129);
[self.baseNavigationController
performSelector:@selector(closeSettings)];
} else {
[self.baseNavigationController popToViewController:self.viewController
animated:NO];
[self.baseNavigationController popViewControllerAnimated:YES];
[self.delegate manageSyncSettingsCoordinatorWasRemoved:self];
}
} else {
[self.navigationControllerForChildPages.presentingViewController
dismissViewControllerAnimated:YES
completion:nil];
[self.delegate manageSyncSettingsCoordinatorWasRemoved:self];
}
}
_settingsAreDismissed = YES;
}
#pragma mark - ManageSyncSettingsTableViewControllerPresentationDelegate
- (void)manageSyncSettingsTableViewControllerWasRemoved:
(ManageSyncSettingsTableViewController*)controller {
DCHECK_EQ(self.viewController, controller);
[self.delegate manageSyncSettingsCoordinatorWasRemoved:self];
}
#pragma mark - PersonalizeGoogleServicesCoordinatorDelegate
- (void)personalizeGoogleServicesCoordinatorWasRemoved:
(PersonalizeGoogleServicesCoordinator*)coordinator {
CHECK_EQ(_personalizeGoogleServicesCoordinator, coordinator);
[self stopPersonalizedGoogleServicesCoordinator];
}
#pragma mark - ManageSyncSettingsCommandHandler
- (void)openBulkUpload {
[self stopBulkUpload];
base::RecordAction(base::UserMetricsAction("BulkUploadSettingsOpen"));
_bulkUploadCoordinator = [[BulkUploadCoordinator alloc]
initWithBaseViewController:self.viewController
browser:self.browser];
_bulkUploadCoordinator.delegate = self;
[_bulkUploadCoordinator start];
}
- (void)openWebAppActivityDialog {
base::RecordAction(base::UserMetricsAction(
"Signin_AccountSettings_GoogleActivityControlsClicked"));
id<SystemIdentity> identity =
self.authService->GetPrimaryIdentity(signin::ConsentLevel::kSignin);
_dismissWebAndAppSettingDetailsController =
GetApplicationContext()
->GetSystemIdentityManager()
->PresentWebAndAppSettingDetailsController(
identity, self.viewController, /*animated=*/YES,
base::DoNothing());
}
- (void)openPersonalizeGoogleServices {
CHECK(!_personalizeGoogleServicesCoordinator);
base::RecordAction(base::UserMetricsAction(
"Signin_AccountSettings_PersonalizeGoogleServicesClicked"));
_personalizeGoogleServicesCoordinator = [[PersonalizeGoogleServicesCoordinator
alloc]
initWithBaseNavigationController:self.navigationControllerForChildPages
browser:self.browser];
_personalizeGoogleServicesCoordinator.delegate = self;
[_personalizeGoogleServicesCoordinator start];
}
- (void)openDataFromChromeSyncWebPage {
if ([self.delegate
respondsToSelector:@selector
(manageSyncSettingsCoordinatorNeedToOpenChromeSyncWebPage:)]) {
[self.delegate
manageSyncSettingsCoordinatorNeedToOpenChromeSyncWebPage:self];
}
GURL url = google_util::AppendGoogleLocaleParam(
GURL(kSyncGoogleDashboardURL),
GetApplicationContext()->GetApplicationLocale());
OpenNewTabCommand* command = [OpenNewTabCommand commandWithURLFromChrome:url];
id<ApplicationCommands> handler = HandlerForProtocol(
self.browser->GetCommandDispatcher(), ApplicationCommands);
[handler closeSettingsUIAndOpenURL:command];
}
- (void)signOutFromTargetRect:(CGRect)targetRect {
if (!self.authService->HasPrimaryIdentity(signin::ConsentLevel::kSignin)) {
// This could happen in very rare cases, if the account somehow got removed
// after the settings UI was created.
return;
}
self.signoutActionSheetCoordinator = [[SignoutActionSheetCoordinator alloc]
initWithBaseViewController:self.viewController
browser:self.browser
rect:targetRect
view:self.viewController.view
withSource:signin_metrics::ProfileSignout::
kUserClickedSignoutSettings];
self.signoutActionSheetCoordinator.delegate = self;
__weak ManageSyncSettingsCoordinator* weakSelf = self;
self.signoutActionSheetCoordinator.completion = ^(BOOL success) {
[weakSelf.signoutActionSheetCoordinator stop];
weakSelf.signoutActionSheetCoordinator = nil;
if (success) {
[weakSelf closeManageSyncSettings];
}
};
[self.signoutActionSheetCoordinator start];
}
- (void)showAdressesNotEncryptedDialog {
AlertCoordinator* alertCoordinator = [[AlertCoordinator alloc]
initWithBaseViewController:self.viewController
browser:self.browser
title:l10n_util::GetNSString(
IDS_IOS_SYNC_ADDRESSES_DIALOG_TITLE)
message:l10n_util::GetNSString(
IDS_IOS_SYNC_ADDRESSES_DIALOG_MESSAGE)];
__weak __typeof(self) weakSelf = self;
[alertCoordinator addItemWithTitle:l10n_util::GetNSString(
IDS_IOS_SYNC_ADDRESSES_DIALOG_CONTINUE)
action:^{
[weakSelf.mediator autofillAlertConfirmed:YES];
}
style:UIAlertActionStyleDefault];
[alertCoordinator addItemWithTitle:l10n_util::GetNSString(IDS_CANCEL)
action:^{
[weakSelf.mediator autofillAlertConfirmed:NO];
}
style:UIAlertActionStyleCancel];
[alertCoordinator start];
}
- (void)showAccountsPage {
AccountsCoordinator* accountsCoordinator = [[AccountsCoordinator alloc]
initWithBaseViewController:self.viewController
browser:self.browser
closeSettingsOnAddAccount:NO];
accountsCoordinator.signoutDismissalByParentCoordinator = YES;
_accountsCoordinator = accountsCoordinator;
[accountsCoordinator start];
}
- (void)showManageYourGoogleAccount {
__weak __typeof(self) weakself = self;
_accountDetailsControllerDismissCallback =
GetApplicationContext()
->GetSystemIdentityManager()
->PresentAccountDetailsController(
self.authService->GetPrimaryIdentity(
signin::ConsentLevel::kSignin),
self.viewController,
/*animated=*/YES,
base::BindOnce(
[](__typeof(self) weakSelf) {
[weakSelf resetDismissAccountDetailsController];
},
weakself));
}
#pragma mark - SignoutActionSheetCoordinatorDelegate
- (void)signoutActionSheetCoordinatorPreventUserInteraction:
(SignoutActionSheetCoordinator*)coordinator {
self.signOutFlowInProgress = YES;
[self.viewController preventUserInteraction];
}
- (void)signoutActionSheetCoordinatorAllowUserInteraction:
(SignoutActionSheetCoordinator*)coordinator {
[self.viewController allowUserInteraction];
self.signOutFlowInProgress = NO;
}
#pragma mark - SyncErrorSettingsCommandHandler
- (void)openPassphraseDialogWithModalPresentation:(BOOL)presentModally {
if (presentModally) {
_syncEncryptionPassphraseTableViewController =
[[SyncEncryptionPassphraseTableViewController alloc]
initWithBrowser:self.browser];
_syncEncryptionPassphraseTableViewController.presentModally = YES;
UINavigationController* navigationController =
[[UINavigationController alloc]
initWithRootViewController:
_syncEncryptionPassphraseTableViewController];
navigationController.modalPresentationStyle = UIModalPresentationFormSheet;
[self.viewController configureHandlersForRootViewController:
_syncEncryptionPassphraseTableViewController];
[self.viewController presentViewController:navigationController
animated:YES
completion:nil];
return;
}
// If there was a sync error, prompt the user to enter the passphrase.
// Otherwise, show the full encryption options.
UIViewController<SettingsRootViewControlling>* controllerToPush;
if (self.syncService->GetUserSettings()->IsPassphraseRequired()) {
controllerToPush = _syncEncryptionPassphraseTableViewController =
[[SyncEncryptionPassphraseTableViewController alloc]
initWithBrowser:self.browser];
} else {
controllerToPush = _syncEncryptionTableViewController =
[[SyncEncryptionTableViewController alloc]
initWithBrowser:self.browser];
}
[self.viewController configureHandlersForRootViewController:controllerToPush];
[self.navigationControllerForChildPages pushViewController:controllerToPush
animated:YES];
}
- (void)openTrustedVaultReauthForFetchKeys {
id<ApplicationCommands> applicationCommands =
static_cast<id<ApplicationCommands>>(
self.browser->GetCommandDispatcher());
trusted_vault::SecurityDomainId chromeSyncID =
trusted_vault::SecurityDomainId::kChromeSync;
syncer::TrustedVaultUserActionTriggerForUMA settingsTrigger =
syncer::TrustedVaultUserActionTriggerForUMA::kSettings;
AccessPoint settingsAccessPoint = AccessPoint::ACCESS_POINT_SETTINGS;
[applicationCommands
showTrustedVaultReauthForFetchKeysFromViewController:self.viewController
securityDomainID:chromeSyncID
trigger:settingsTrigger
accessPoint:settingsAccessPoint];
}
- (void)openTrustedVaultReauthForDegradedRecoverability {
id<ApplicationCommands> applicationCommands =
static_cast<id<ApplicationCommands>>(
self.browser->GetCommandDispatcher());
trusted_vault::SecurityDomainId chromeSyncID =
trusted_vault::SecurityDomainId::kChromeSync;
syncer::TrustedVaultUserActionTriggerForUMA settingsTrigger =
syncer::TrustedVaultUserActionTriggerForUMA::kSettings;
AccessPoint settingsAccessPoint = AccessPoint::ACCESS_POINT_SETTINGS;
[applicationCommands
showTrustedVaultReauthForDegradedRecoverabilityFromViewController:
self.viewController
securityDomainID:
chromeSyncID
trigger:
settingsTrigger
accessPoint:
settingsAccessPoint];
}
- (void)openMDMErrodDialogWithSystemIdentity:(id<SystemIdentity>)identity {
self.authService->ShowMDMErrorDialogForIdentity(identity);
}
- (void)openPrimaryAccountReauthDialog {
id<ApplicationCommands> applicationCommands =
static_cast<id<ApplicationCommands>>(
self.browser->GetCommandDispatcher());
ShowSigninCommand* signinCommand = [[ShowSigninCommand alloc]
initWithOperation:AuthenticationOperation::kPrimaryAccountReauth
accessPoint:AccessPoint::ACCESS_POINT_SETTINGS];
[applicationCommands showSignin:signinCommand
baseViewController:self.viewController];
}
#pragma mark - BulkUploadCoordinatorDelegate
- (void)bulkUploadCoordinatorShouldStop:(BulkUploadCoordinator*)coordinator {
DCHECK_EQ(coordinator, _bulkUploadCoordinator);
[self stopBulkUpload];
}
#pragma mark - SyncObserverModelBridge
- (void)onSyncStateChanged {
if (self.signOutFlowInProgress) {
return;
}
if (!self.syncService->GetDisableReasons().empty()) {
[self closeManageSyncSettings];
}
}
#pragma mark - SettingsNavigationControllerDelegate
- (void)closeSettings {
[self.navigationControllerForChildPages.presentingViewController
dismissViewControllerAnimated:YES
completion:nil];
[self.delegate manageSyncSettingsCoordinatorWasRemoved:self];
}
- (void)settingsWasDismissed {
[self.delegate manageSyncSettingsCoordinatorWasRemoved:self];
}
@end