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