1// Copyright 2019 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#import "ios/chrome/browser/ui/settings/google_services/manage_sync_settings_coordinator.h" 6 7#include "base/check_op.h" 8#include "base/metrics/user_metrics.h" 9#include "base/metrics/user_metrics_action.h" 10#include "components/google/core/common/google_util.h" 11#include "components/sync/driver/sync_service.h" 12#include "components/sync/driver/sync_service_utils.h" 13#include "components/sync/driver/sync_user_settings.h" 14#include "ios/chrome/browser/application_context.h" 15#include "ios/chrome/browser/browser_state/chrome_browser_state.h" 16#include "ios/chrome/browser/chrome_url_constants.h" 17#import "ios/chrome/browser/main/browser.h" 18#import "ios/chrome/browser/signin/authentication_service.h" 19#import "ios/chrome/browser/signin/authentication_service_factory.h" 20#include "ios/chrome/browser/sync/profile_sync_service_factory.h" 21#include "ios/chrome/browser/sync/sync_observer_bridge.h" 22#include "ios/chrome/browser/sync/sync_setup_service.h" 23#include "ios/chrome/browser/sync/sync_setup_service_factory.h" 24#import "ios/chrome/browser/ui/authentication/authentication_flow.h" 25#import "ios/chrome/browser/ui/commands/application_commands.h" 26#import "ios/chrome/browser/ui/commands/browsing_data_commands.h" 27#import "ios/chrome/browser/ui/commands/command_dispatcher.h" 28#import "ios/chrome/browser/ui/commands/open_new_tab_command.h" 29#import "ios/chrome/browser/ui/icons/chrome_icon.h" 30#import "ios/chrome/browser/ui/settings/google_services/manage_sync_settings_command_handler.h" 31#import "ios/chrome/browser/ui/settings/google_services/manage_sync_settings_mediator.h" 32#import "ios/chrome/browser/ui/settings/google_services/manage_sync_settings_table_view_controller.h" 33#import "ios/chrome/browser/ui/settings/google_services/sync_error_settings_command_handler.h" 34#import "ios/chrome/browser/ui/settings/sync/sync_encryption_passphrase_table_view_controller.h" 35#import "ios/chrome/browser/ui/settings/sync/sync_encryption_table_view_controller.h" 36#include "ios/public/provider/chrome/browser/chrome_browser_provider.h" 37#import "ios/public/provider/chrome/browser/signin/chrome_identity_browser_opener.h" 38#include "ios/public/provider/chrome/browser/signin/chrome_identity_service.h" 39#import "net/base/mac/url_conversions.h" 40 41#if !defined(__has_feature) || !__has_feature(objc_arc) 42#error "This file requires ARC support." 43#endif 44 45using signin_metrics::AccessPoint; 46using signin_metrics::PromoAction; 47 48@interface ManageSyncSettingsCoordinator () < 49 ChromeIdentityBrowserOpener, 50 ManageSyncSettingsCommandHandler, 51 SyncErrorSettingsCommandHandler, 52 ManageSyncSettingsTableViewControllerPresentationDelegate, 53 SyncObserverModelBridge, 54 SyncSettingsViewState> { 55 // Sync observer. 56 std::unique_ptr<SyncObserverBridge> _syncObserver; 57} 58 59// View controller. 60@property(nonatomic, strong) 61 ManageSyncSettingsTableViewController* viewController; 62// Mediator. 63@property(nonatomic, strong) ManageSyncSettingsMediator* mediator; 64// Sync service. 65@property(nonatomic, assign, readonly) syncer::SyncService* syncService; 66// Authentication service. 67@property(nonatomic, assign, readonly) AuthenticationService* authService; 68// Dismiss callback for Web and app setting details view. 69@property(nonatomic, copy) ios::DismissASMViewControllerBlock 70 dismissWebAndAppSettingDetailsControllerBlock; 71// Manages the authentication flow for a given identity. 72@property(nonatomic, strong) AuthenticationFlow* authenticationFlow; 73// YES if the last sign-in has been interrupted. In that case, the sync UI will 74// be dismissed and the sync setup flag should not be marked as done. The sync 75// should be kept undecided, not marked as disabled. 76@property(nonatomic, assign) BOOL signinInterrupted; 77 78@end 79 80@implementation ManageSyncSettingsCoordinator 81 82@synthesize baseNavigationController = _baseNavigationController; 83 84- (instancetype)initWithBaseNavigationController: 85 (UINavigationController*)navigationController 86 browser:(Browser*)browser { 87 if (self = [super initWithBaseViewController:navigationController 88 browser:browser]) { 89 _baseNavigationController = navigationController; 90 } 91 return self; 92} 93 94- (void)start { 95 DCHECK(self.baseNavigationController); 96 self.authService->WaitUntilCacheIsPopulated(); 97 self.mediator = [[ManageSyncSettingsMediator alloc] 98 initWithSyncService:self.syncService 99 userPrefService:self.browser->GetBrowserState()->GetPrefs()]; 100 self.mediator.syncSetupService = SyncSetupServiceFactory::GetForBrowserState( 101 self.browser->GetBrowserState()); 102 self.mediator.authService = self.authService; 103 self.mediator.commandHandler = self; 104 self.mediator.syncErrorHandler = self; 105 self.viewController = [[ManageSyncSettingsTableViewController alloc] 106 initWithStyle:UITableViewStyleGrouped]; 107 self.viewController.serviceDelegate = self.mediator; 108 self.viewController.presentationDelegate = self; 109 self.viewController.modelDelegate = self.mediator; 110 self.mediator.consumer = self.viewController; 111 [self.baseNavigationController pushViewController:self.viewController 112 animated:YES]; 113 _syncObserver.reset(new SyncObserverBridge(self, self.syncService)); 114} 115 116- (void)stop { 117 // If kMobileIdentityConsistency is disabled, 118 // GoogleServicesSettingsCoordinator is in charge to enable sync or not when 119 // being closed. This coordinator displays a sub view. 120 // With kMobileIdentityConsistency enabled: 121 // This coordinator displays the main view and it is in charge to enable sync 122 // or not when being closed. 123 // Sync changes should only be commited if the user is authenticated and 124 // the sign-in has not been interrupted. 125 if (base::FeatureList::IsEnabled(signin::kMobileIdentityConsistency) && 126 (self.authService->IsAuthenticated() || !self.signinInterrupted)) { 127 SyncSetupService* syncSetupService = 128 SyncSetupServiceFactory::GetForBrowserState( 129 self.browser->GetBrowserState()); 130 if (syncSetupService->GetSyncServiceState() == 131 SyncSetupService::kSyncSettingsNotConfirmed) { 132 // If Sync is still in aborted state, this means the user didn't turn on 133 // sync, and wants Sync off. To acknowledge, Sync has to be turned off. 134 syncSetupService->SetSyncEnabled(false); 135 } 136 syncSetupService->CommitSyncChanges(); 137 } 138} 139 140#pragma mark - Properties 141 142- (syncer::SyncService*)syncService { 143 return ProfileSyncServiceFactory::GetForBrowserState( 144 self.browser->GetBrowserState()); 145} 146 147- (AuthenticationService*)authService { 148 return AuthenticationServiceFactory::GetForBrowserState( 149 self.browser->GetBrowserState()); 150} 151 152#pragma mark - SyncSettingsViewState 153 154- (BOOL)isSettingsViewShown { 155 return [self.viewController 156 isEqual:self.baseNavigationController.topViewController]; 157} 158 159- (UINavigationItem*)navigationItem { 160 return self.viewController.navigationItem; 161} 162 163#pragma mark - Private 164 165// Closes the Manage sync settings view controller. 166- (void)closeManageSyncSettings { 167 if (self.viewController.navigationController) { 168 if (self.dismissWebAndAppSettingDetailsControllerBlock) { 169 self.dismissWebAndAppSettingDetailsControllerBlock(NO); 170 self.dismissWebAndAppSettingDetailsControllerBlock = nil; 171 } 172 [self.baseNavigationController popToViewController:self.viewController 173 animated:NO]; 174 [self.baseNavigationController popViewControllerAnimated:YES]; 175 } 176} 177 178- (void)signinFinishedWithSuccess:(BOOL)success { 179 DCHECK(self.authenticationFlow); 180 self.authenticationFlow = nil; 181 [self.viewController allowUserInteraction]; 182 183 ChromeIdentity* primaryAccount = 184 AuthenticationServiceFactory::GetForBrowserState( 185 self.browser->GetBrowserState()) 186 ->GetAuthenticatedIdentity(); 187 // TODO(crbug.com/1101346): SigninCoordinatorResult should be received instead 188 // of guessing if the sign-in has been interrupted. 189 self.signinInterrupted = !success && primaryAccount; 190} 191 192#pragma mark - ManageSyncSettingsTableViewControllerPresentationDelegate 193 194- (void)manageSyncSettingsTableViewControllerWasRemoved: 195 (ManageSyncSettingsTableViewController*)controller { 196 DCHECK_EQ(self.viewController, controller); 197 [self.delegate manageSyncSettingsCoordinatorWasRemoved:self]; 198} 199 200#pragma mark - ChromeIdentityBrowserOpener 201 202- (void)openURL:(NSURL*)url 203 view:(UIView*)view 204 viewController:(UIViewController*)viewController { 205 OpenNewTabCommand* command = 206 [OpenNewTabCommand commandWithURLFromChrome:net::GURLWithNSURL(url)]; 207 id<ApplicationCommands> handler = HandlerForProtocol( 208 self.browser->GetCommandDispatcher(), ApplicationCommands); 209 [handler closeSettingsUIAndOpenURL:command]; 210} 211 212#pragma mark - ManageSyncSettingsCommandHandler 213 214- (void)openWebAppActivityDialog { 215 AuthenticationService* authService = 216 AuthenticationServiceFactory::GetForBrowserState( 217 self.browser->GetBrowserState()); 218 base::RecordAction(base::UserMetricsAction( 219 "Signin_AccountSettings_GoogleActivityControlsClicked")); 220 self.dismissWebAndAppSettingDetailsControllerBlock = 221 ios::GetChromeBrowserProvider() 222 ->GetChromeIdentityService() 223 ->PresentWebAndAppSettingDetailsController( 224 authService->GetAuthenticatedIdentity(), self.viewController, 225 YES); 226} 227 228- (void)openDataFromChromeSyncWebPage { 229 GURL url = google_util::AppendGoogleLocaleParam( 230 GURL(kSyncGoogleDashboardURL), 231 GetApplicationContext()->GetApplicationLocale()); 232 OpenNewTabCommand* command = [OpenNewTabCommand commandWithURLFromChrome:url]; 233 id<ApplicationCommands> handler = HandlerForProtocol( 234 self.browser->GetCommandDispatcher(), ApplicationCommands); 235 [handler closeSettingsUIAndOpenURL:command]; 236} 237 238#pragma mark - SyncErrorSettingsCommandHandler 239 240- (void)openPassphraseDialog { 241 DCHECK(self.mediator.shouldEncryptionItemBeEnabled); 242 UIViewController<SettingsRootViewControlling>* controllerToPush; 243 // If there was a sync error, prompt the user to enter the passphrase. 244 // Otherwise, show the full encryption options. 245 if (self.syncService->GetUserSettings()->IsPassphraseRequired()) { 246 controllerToPush = [[SyncEncryptionPassphraseTableViewController alloc] 247 initWithBrowser:self.browser]; 248 } else { 249 controllerToPush = [[SyncEncryptionTableViewController alloc] 250 initWithBrowser:self.browser]; 251 } 252 // TODO(crbug.com/1045047): Use HandlerForProtocol after commands protocol 253 // clean up. 254 controllerToPush.dispatcher = static_cast< 255 id<ApplicationCommands, BrowserCommands, BrowsingDataCommands>>( 256 self.browser->GetCommandDispatcher()); 257 [self.baseNavigationController pushViewController:controllerToPush 258 animated:YES]; 259} 260 261- (void)openTrustedVaultReauth { 262 id<ApplicationCommands> applicationCommands = 263 static_cast<id<ApplicationCommands>>( 264 self.browser->GetCommandDispatcher()); 265 [applicationCommands 266 showTrustedVaultReauthenticationFromViewController:self.viewController 267 retrievalTrigger: 268 syncer::KeyRetrievalTriggerForUMA:: 269 kSettings]; 270} 271 272- (void)restartAuthenticationFlow { 273 ChromeIdentity* authenticatedIdentity = 274 AuthenticationServiceFactory::GetForBrowserState( 275 self.browser->GetBrowserState()) 276 ->GetAuthenticatedIdentity(); 277 [self.viewController preventUserInteraction]; 278 DCHECK(!self.authenticationFlow); 279 self.authenticationFlow = 280 [[AuthenticationFlow alloc] initWithBrowser:self.browser 281 identity:authenticatedIdentity 282 shouldClearData:SHOULD_CLEAR_DATA_USER_CHOICE 283 postSignInAction:POST_SIGNIN_ACTION_START_SYNC 284 presentingViewController:self.viewController]; 285 self.authenticationFlow.dispatcher = HandlerForProtocol( 286 self.browser->GetCommandDispatcher(), BrowsingDataCommands); 287 __weak ManageSyncSettingsCoordinator* weakSelf = self; 288 [self.authenticationFlow startSignInWithCompletion:^(BOOL success) { 289 [weakSelf signinFinishedWithSuccess:success]; 290 }]; 291} 292 293- (void)openReauthDialogAsSyncIsInAuthError { 294 ChromeIdentity* identity = self.authService->GetAuthenticatedIdentity(); 295 if (self.authService->HasCachedMDMErrorForIdentity(identity)) { 296 self.authService->ShowMDMErrorDialogForIdentity(identity); 297 return; 298 } 299 // Sync enters in a permanent auth error state when fetching an access token 300 // fails with invalid credentials. This corresponds to Gaia responding with an 301 // "invalid grant" error. The current implementation of the iOS SSOAuth 302 // library user by Chrome removes the identity from the device when receiving 303 // an "invalid grant" response, which leads to the account being also signed 304 // out of Chrome. So the sync permanent auth error is a transient state on 305 // iOS. The decision was to avoid handling this error in the UI, which means 306 // that the reauth dialog is not actually presented on iOS. 307} 308 309#pragma mark - SyncObserverModelBridge 310 311- (void)onSyncStateChanged { 312 if (!self.syncService->GetDisableReasons().Empty()) { 313 [self closeManageSyncSettings]; 314 } 315} 316 317@end 318