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