1// Copyright 2018 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/google_services_settings_coordinator.h" 6 7#include "base/mac/foundation_util.h" 8#include "components/google/core/common/google_util.h" 9#include "components/sync/driver/sync_service_utils.h" 10#include "ios/chrome/browser/application_context.h" 11#include "ios/chrome/browser/browser_state/chrome_browser_state.h" 12#include "ios/chrome/browser/chrome_url_constants.h" 13#import "ios/chrome/browser/main/browser.h" 14#include "ios/chrome/browser/signin/authentication_service.h" 15#import "ios/chrome/browser/signin/authentication_service_factory.h" 16#include "ios/chrome/browser/signin/identity_manager_factory.h" 17#include "ios/chrome/browser/sync/profile_sync_service_factory.h" 18#include "ios/chrome/browser/sync/sync_setup_service.h" 19#include "ios/chrome/browser/sync/sync_setup_service_factory.h" 20#import "ios/chrome/browser/ui/authentication/authentication_flow.h" 21#import "ios/chrome/browser/ui/commands/application_commands.h" 22#import "ios/chrome/browser/ui/commands/browsing_data_commands.h" 23#import "ios/chrome/browser/ui/commands/command_dispatcher.h" 24#import "ios/chrome/browser/ui/commands/open_new_tab_command.h" 25#import "ios/chrome/browser/ui/commands/show_signin_command.h" 26#import "ios/chrome/browser/ui/settings/google_services/accounts_table_view_controller.h" 27#import "ios/chrome/browser/ui/settings/google_services/google_services_settings_command_handler.h" 28#import "ios/chrome/browser/ui/settings/google_services/google_services_settings_mediator.h" 29#import "ios/chrome/browser/ui/settings/google_services/google_services_settings_view_controller.h" 30#import "ios/chrome/browser/ui/settings/google_services/manage_sync_settings_coordinator.h" 31#import "ios/chrome/browser/ui/settings/google_services/sync_error_settings_command_handler.h" 32#import "ios/chrome/browser/ui/settings/sync/sync_encryption_passphrase_table_view_controller.h" 33#include "ios/chrome/browser/ui/ui_feature_flags.h" 34#import "ios/public/provider/chrome/browser/chrome_browser_provider.h" 35 36#if !defined(__has_feature) || !__has_feature(objc_arc) 37#error "This file requires ARC support." 38#endif 39 40using signin_metrics::AccessPoint; 41using signin_metrics::PromoAction; 42 43@interface GoogleServicesSettingsCoordinator () < 44 GoogleServicesSettingsCommandHandler, 45 GoogleServicesSettingsViewControllerPresentationDelegate, 46 ManageSyncSettingsCoordinatorDelegate, 47 SyncErrorSettingsCommandHandler, 48 SyncSettingsViewState> 49 50// Google services settings mode. 51@property(nonatomic, assign, readonly) GoogleServicesSettingsMode mode; 52// Google services settings mediator. 53@property(nonatomic, strong) GoogleServicesSettingsMediator* mediator; 54// Returns the authentication service. 55@property(nonatomic, assign, readonly) AuthenticationService* authService; 56// Manages the authentication flow for a given identity. 57@property(nonatomic, strong) AuthenticationFlow* authenticationFlow; 58// View controller presented by this coordinator. 59@property(nonatomic, strong, readonly) 60 GoogleServicesSettingsViewController* googleServicesSettingsViewController; 61// Coordinator to present the manage sync settings. 62@property(nonatomic, strong) 63 ManageSyncSettingsCoordinator* manageSyncSettingsCoordinator; 64// YES if stop has been called. 65@property(nonatomic, assign) BOOL stopDone; 66// YES if the last sign-in has been interrupted. In that case, the sync UI will 67// be dismissed and the sync setup flag should not be marked as done. The sync 68// should be kept undecided, not marked as disabled. 69@property(nonatomic, assign) BOOL signinInterrupted; 70 71@end 72 73@implementation GoogleServicesSettingsCoordinator 74 75@synthesize baseNavigationController = _baseNavigationController; 76 77- (instancetype)initWithBaseNavigationController: 78 (UINavigationController*)navigationController 79 browser:(Browser*)browser 80 mode:(GoogleServicesSettingsMode) 81 mode { 82 if ([super initWithBaseViewController:navigationController browser:browser]) { 83 _baseNavigationController = navigationController; 84 _mode = mode; 85 } 86 return self; 87} 88 89- (void)dealloc { 90 // -[GoogleServicesSettingsCoordinator stop] needs to be called explicitly. 91 DCHECK(self.stopDone); 92} 93 94- (void)start { 95 self.authService->WaitUntilCacheIsPopulated(); 96 UITableViewStyle style = base::FeatureList::IsEnabled(kSettingsRefresh) 97 ? UITableViewStylePlain 98 : UITableViewStyleGrouped; 99 100 GoogleServicesSettingsViewController* viewController = 101 [[GoogleServicesSettingsViewController alloc] initWithStyle:style]; 102 viewController.presentationDelegate = self; 103 self.viewController = viewController; 104 SyncSetupService* syncSetupService = 105 SyncSetupServiceFactory::GetForBrowserState( 106 self.browser->GetBrowserState()); 107 self.mediator = [[GoogleServicesSettingsMediator alloc] 108 initWithUserPrefService:self.browser->GetBrowserState()->GetPrefs() 109 localPrefService:GetApplicationContext()->GetLocalState() 110 syncSetupService:syncSetupService 111 mode:self.mode]; 112 self.mediator.consumer = viewController; 113 self.mediator.authService = self.authService; 114 self.mediator.identityManager = IdentityManagerFactory::GetForBrowserState( 115 self.browser->GetBrowserState()); 116 self.mediator.commandHandler = self; 117 self.mediator.syncErrorHandler = self; 118 self.mediator.syncService = ProfileSyncServiceFactory::GetForBrowserState( 119 self.browser->GetBrowserState()); 120 viewController.modelDelegate = self.mediator; 121 viewController.serviceDelegate = self.mediator; 122 viewController.dispatcher = static_cast< 123 id<ApplicationCommands, BrowserCommands, BrowsingDataCommands>>( 124 self.browser->GetCommandDispatcher()); 125 DCHECK(self.baseNavigationController); 126 [self.baseNavigationController pushViewController:self.viewController 127 animated:YES]; 128} 129 130- (void)stop { 131 if (self.stopDone) { 132 return; 133 } 134 // Sync changes should only be commited if the user is authenticated and 135 // the sign-in has not been interrupted. 136 if (self.authService->IsAuthenticated() && !self.signinInterrupted) { 137 SyncSetupService* syncSetupService = 138 SyncSetupServiceFactory::GetForBrowserState( 139 self.browser->GetBrowserState()); 140 if (self.mode == GoogleServicesSettingsModeSettings && 141 syncSetupService->GetSyncServiceState() == 142 SyncSetupService::kSyncSettingsNotConfirmed) { 143 // If Sync is still in aborted state, this means the user didn't turn on 144 // sync, and wants Sync off. To acknowledge, Sync has to be turned off. 145 syncSetupService->SetSyncEnabled(false); 146 } 147 syncSetupService->CommitSyncChanges(); 148 } 149 self.stopDone = YES; 150} 151 152#pragma mark - Private 153 154- (void)authenticationFlowDidComplete { 155 DCHECK(self.authenticationFlow); 156 self.authenticationFlow = nil; 157 [self.googleServicesSettingsViewController allowUserInteraction]; 158} 159 160#pragma mark - Properties 161 162- (AuthenticationService*)authService { 163 return AuthenticationServiceFactory::GetForBrowserState( 164 self.browser->GetBrowserState()); 165} 166 167- (GoogleServicesSettingsViewController*)googleServicesSettingsViewController { 168 return base::mac::ObjCCast<GoogleServicesSettingsViewController>( 169 self.viewController); 170} 171 172#pragma mark - SyncSettingsViewState 173 174- (BOOL)isSettingsViewShown { 175 return [self.viewController 176 isEqual:self.baseNavigationController.topViewController]; 177} 178 179- (UINavigationItem*)navigationItem { 180 return self.viewController.navigationItem; 181} 182 183#pragma mark - SyncErrorSettingsCommandHandler 184 185- (void)restartAuthenticationFlow { 186 ChromeIdentity* authenticatedIdentity = 187 AuthenticationServiceFactory::GetForBrowserState( 188 self.browser->GetBrowserState()) 189 ->GetAuthenticatedIdentity(); 190 [self.googleServicesSettingsViewController preventUserInteraction]; 191 DCHECK(!self.authenticationFlow); 192 self.authenticationFlow = 193 [[AuthenticationFlow alloc] initWithBrowser:self.browser 194 identity:authenticatedIdentity 195 shouldClearData:SHOULD_CLEAR_DATA_USER_CHOICE 196 postSignInAction:POST_SIGNIN_ACTION_START_SYNC 197 presentingViewController:self.viewController]; 198 self.authenticationFlow.dispatcher = HandlerForProtocol( 199 self.browser->GetCommandDispatcher(), BrowsingDataCommands); 200 __weak GoogleServicesSettingsCoordinator* weakSelf = self; 201 [self.authenticationFlow startSignInWithCompletion:^(BOOL success) { 202 // TODO(crbug.com/889919): Needs to add histogram for |success|. 203 [weakSelf authenticationFlowDidComplete]; 204 }]; 205} 206 207- (void)openReauthDialogAsSyncIsInAuthError { 208 ChromeIdentity* identity = self.authService->GetAuthenticatedIdentity(); 209 if (self.authService->HasCachedMDMErrorForIdentity(identity)) { 210 self.authService->ShowMDMErrorDialogForIdentity(identity); 211 return; 212 } 213 // Sync enters in a permanent auth error state when fetching an access token 214 // fails with invalid credentials. This corresponds to Gaia responding with an 215 // "invalid grant" error. The current implementation of the iOS SSOAuth 216 // library user by Chrome removes the identity from the device when receiving 217 // an "invalid grant" response, which leads to the account being also signed 218 // out of Chrome. So the sync permanent auth error is a transient state on 219 // iOS. The decision was to avoid handling this error in the UI, which means 220 // that the reauth dialog is not actually presented on iOS. 221} 222 223- (void)openPassphraseDialog { 224 SyncEncryptionPassphraseTableViewController* controller = 225 [[SyncEncryptionPassphraseTableViewController alloc] 226 initWithBrowser:self.browser]; 227 // TODO(crbug.com/1045047): Use HandlerForProtocol after commands protocol 228 // clean up. 229 controller.dispatcher = static_cast< 230 id<ApplicationCommands, BrowserCommands, BrowsingDataCommands>>( 231 self.browser->GetCommandDispatcher()); 232 [self.baseNavigationController pushViewController:controller animated:YES]; 233} 234 235- (void)openTrustedVaultReauth { 236 id<ApplicationCommands> applicationCommands = 237 static_cast<id<ApplicationCommands>>( 238 self.browser->GetCommandDispatcher()); 239 [applicationCommands 240 showTrustedVaultReauthenticationFromViewController: 241 self.googleServicesSettingsViewController 242 retrievalTrigger: 243 syncer::KeyRetrievalTriggerForUMA:: 244 kSettings]; 245} 246 247#pragma mark - GoogleServicesSettingsCommandHandler 248 249- (void)showSignIn { 250 __weak __typeof(self) weakSelf = self; 251 DCHECK(self.handler); 252 signin_metrics::RecordSigninUserActionForAccessPoint( 253 AccessPoint::ACCESS_POINT_GOOGLE_SERVICES_SETTINGS, 254 PromoAction::PROMO_ACTION_NO_SIGNIN_PROMO); 255 ShowSigninCommand* command = [[ShowSigninCommand alloc] 256 initWithOperation:AUTHENTICATION_OPERATION_SIGNIN 257 identity:nil 258 accessPoint:AccessPoint::ACCESS_POINT_GOOGLE_SERVICES_SETTINGS 259 promoAction:PromoAction::PROMO_ACTION_NO_SIGNIN_PROMO 260 callback:^(BOOL success) { 261 [weakSelf signinFinishedWithSuccess:success]; 262 }]; 263 [self.handler showSignin:command 264 baseViewController:self.googleServicesSettingsViewController]; 265} 266 267- (void)signinFinishedWithSuccess:(BOOL)success { 268 // TODO(crbug.com/1101346): SigninCoordinatorResult should be received instead 269 // of guessing if the sign-in has been interrupted. 270 ChromeIdentity* primaryAccount = 271 AuthenticationServiceFactory::GetForBrowserState( 272 self.browser->GetBrowserState()) 273 ->GetAuthenticatedIdentity(); 274 self.signinInterrupted = !success && primaryAccount; 275} 276 277- (void)openAccountSettings { 278 AccountsTableViewController* controller = 279 [[AccountsTableViewController alloc] initWithBrowser:self.browser 280 closeSettingsOnAddAccount:NO]; 281 controller.dispatcher = static_cast< 282 id<ApplicationCommands, BrowserCommands, BrowsingDataCommands>>( 283 self.browser->GetCommandDispatcher()); 284 [self.baseNavigationController pushViewController:controller animated:YES]; 285} 286 287- (void)openManageSyncSettings { 288 DCHECK(!self.manageSyncSettingsCoordinator); 289 self.manageSyncSettingsCoordinator = [[ManageSyncSettingsCoordinator alloc] 290 initWithBaseNavigationController:self.baseNavigationController 291 browser:self.browser]; 292 self.manageSyncSettingsCoordinator.delegate = self; 293 [self.manageSyncSettingsCoordinator start]; 294} 295 296- (void)openManageGoogleAccount { 297 ios::GetChromeBrowserProvider() 298 ->GetChromeIdentityService() 299 ->PresentAccountDetailsController( 300 self.authService->GetAuthenticatedIdentity(), 301 self.googleServicesSettingsViewController, /*animated=*/YES); 302} 303 304- (void)openManageGoogleAccountWebPage { 305 GURL url = google_util::AppendGoogleLocaleParam( 306 GURL(kManageYourGoogleAccountURL), 307 GetApplicationContext()->GetApplicationLocale()); 308 OpenNewTabCommand* command = [OpenNewTabCommand commandWithURLFromChrome:url]; 309 id<ApplicationCommands> handler = HandlerForProtocol( 310 self.browser->GetCommandDispatcher(), ApplicationCommands); 311 [handler closeSettingsUIAndOpenURL:command]; 312} 313 314#pragma mark - GoogleServicesSettingsViewControllerPresentationDelegate 315 316- (void)googleServicesSettingsViewControllerDidRemove: 317 (GoogleServicesSettingsViewController*)controller { 318 DCHECK_EQ(self.viewController, controller); 319 [self.delegate googleServicesSettingsCoordinatorDidRemove:self]; 320} 321 322#pragma mark - ManageSyncSettingsCoordinatorDelegate 323 324- (void)manageSyncSettingsCoordinatorWasRemoved: 325 (ManageSyncSettingsCoordinator*)coordinator { 326 DCHECK_EQ(self.manageSyncSettingsCoordinator, coordinator); 327 [self.manageSyncSettingsCoordinator stop]; 328 self.manageSyncSettingsCoordinator = nil; 329} 330 331@end 332