// 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_mediator.h"
#import <optional>
#import <string>
#import "base/apple/foundation_util.h"
#import "base/auto_reset.h"
#import "base/check_op.h"
#import "base/containers/contains.h"
#import "base/containers/enum_set.h"
#import "base/containers/fixed_flat_map.h"
#import "base/i18n/message_formatter.h"
#import "base/memory/raw_ptr.h"
#import "base/notreached.h"
#import "base/strings/sys_string_conversions.h"
#import "components/prefs/pref_service.h"
#import "components/signin/public/base/consent_level.h"
#import "components/signin/public/base/signin_metrics.h"
#import "components/signin/public/identity_manager/account_info.h"
#import "components/signin/public/identity_manager/objc/identity_manager_observer_bridge.h"
#import "components/strings/grit/components_strings.h"
#import "components/sync/base/data_type.h"
#import "components/sync/base/user_selectable_type.h"
#import "components/sync/service/local_data_description.h"
#import "components/sync/service/sync_service.h"
#import "components/sync/service/sync_user_settings.h"
#import "ios/chrome/browser/net/model/crurl.h"
#import "ios/chrome/browser/policy/ui_bundled/management_util.h"
#import "ios/chrome/browser/settings/model/sync/utils/account_error_ui_info.h"
#import "ios/chrome/browser/settings/model/sync/utils/identity_error_util.h"
#import "ios/chrome/browser/settings/model/sync/utils/sync_util.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/shared/ui/list_model/list_model.h"
#import "ios/chrome/browser/shared/ui/symbols/symbols.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_detail_icon_item.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_image_item.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_info_button_item.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_item.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_link_header_footer_item.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_text_header_footer_item.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_text_item.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/signin/model/authentication_service.h"
#import "ios/chrome/browser/signin/model/chrome_account_manager_service.h"
#import "ios/chrome/browser/signin/model/chrome_account_manager_service_observer_bridge.h"
#import "ios/chrome/browser/signin/model/constants.h"
#import "ios/chrome/browser/sync/model/enterprise_utils.h"
#import "ios/chrome/browser/sync/model/sync_observer_bridge.h"
#import "ios/chrome/browser/ui/authentication/cells/central_account_view.h"
#import "ios/chrome/browser/ui/authentication/history_sync/history_sync_utils.h"
#import "ios/chrome/browser/ui/settings/cells/settings_image_detail_text_item.h"
#import "ios/chrome/browser/ui/settings/cells/sync_switch_item.h"
#import "ios/chrome/browser/ui/settings/google_services/features.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_consumer.h"
#import "ios/chrome/browser/ui/settings/google_services/sync_error_settings_command_handler.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/table_view/table_view_cells_constants.h"
#import "ios/chrome/grit/ios_branded_strings.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ui/base/l10n/l10n_util.h"
#import "url/gurl.h"
using l10n_util::GetNSString;
namespace {
// Ordered list of all sync switches.
// This is the list of available datatypes for account state kSyncing.
static const syncer::UserSelectableType kSyncSwitchItems[] = {
syncer::UserSelectableType::kAutofill,
syncer::UserSelectableType::kBookmarks,
syncer::UserSelectableType::kHistory,
syncer::UserSelectableType::kTabs,
syncer::UserSelectableType::kPasswords,
syncer::UserSelectableType::kReadingList,
syncer::UserSelectableType::kPreferences,
syncer::UserSelectableType::kPayments};
// Ordered list of all account data type switches.
// This is the list of available datatypes for account state `kSignedIn`.
static const syncer::UserSelectableType kAccountSwitchItems[] = {
syncer::UserSelectableType::kHistory,
syncer::UserSelectableType::kBookmarks,
syncer::UserSelectableType::kReadingList,
syncer::UserSelectableType::kAutofill,
syncer::UserSelectableType::kPasswords,
syncer::UserSelectableType::kPayments,
syncer::UserSelectableType::kPreferences};
// Enterprise icon.
NSString* const kGoogleServicesEnterpriseImage = @"google_services_enterprise";
constexpr CGFloat kErrorSymbolPointSize = 22.;
constexpr CGFloat kBatchUploadSymbolPointSize = 22.;
} // namespace
@interface ManageSyncSettingsMediator () <IdentityManagerObserverBridgeDelegate,
ChromeAccountManagerServiceObserver>
// Model item for sync everything.
@property(nonatomic, strong) TableViewItem* syncEverythingItem;
// Model item for each data types.
@property(nonatomic, strong) NSArray<TableViewItem*>* syncSwitchItems;
// Encryption item.
@property(nonatomic, strong) TableViewImageItem* encryptionItem;
// Sync error item.
@property(nonatomic, strong) TableViewItem* syncErrorItem;
// Batch upload item.
@property(nonatomic, strong) SettingsImageDetailTextItem* batchUploadItem;
// Sign out and turn off sync item.
@property(nonatomic, strong) TableViewItem* signOutAndTurnOffSyncItem;
// Returns YES if the sync data items should be enabled.
@property(nonatomic, assign, readonly) BOOL shouldSyncDataItemEnabled;
// Returns whether the Sync settings should be disabled because of a Sync error.
@property(nonatomic, assign, readonly) BOOL disabledBecauseOfSyncError;
// Returns YES if the user cannot turn on sync for enterprise policy reasons.
@property(nonatomic, assign, readonly) BOOL isSyncDisabledByAdministrator;
@end
@implementation ManageSyncSettingsMediator {
// Sync observer.
std::unique_ptr<SyncObserverBridge> _syncObserver;
// Whether Sync State changes should be currently ignored.
BOOL _ignoreSyncStateChanges;
// Sync service.
raw_ptr<syncer::SyncService> _syncService;
// Identity manager.
raw_ptr<signin::IdentityManager> _identityManager;
// Observer for `IdentityManager`.
std::unique_ptr<signin::IdentityManagerObserverBridge>
_identityManagerObserver;
// Authentication service.
raw_ptr<AuthenticationService> _authenticationService;
// Account manager service to retrieve Chrome identities.
raw_ptr<ChromeAccountManagerService> _chromeAccountManagerService;
// Chrome account manager service observer bridge.
std::unique_ptr<ChromeAccountManagerServiceObserverBridge>
_accountManagerServiceObserver;
// The pref service.
raw_ptr<PrefService> _prefService;
// Signed-in identity. Note: may be nil while signing out.
id<SystemIdentity> _signedInIdentity;
}
- (instancetype)
initWithSyncService:(syncer::SyncService*)syncService
identityManager:(signin::IdentityManager*)identityManager
authenticationService:(AuthenticationService*)authenticationService
accountManagerService:(ChromeAccountManagerService*)accountManagerService
prefService:(PrefService*)prefService
initialAccountState:(SyncSettingsAccountState)initialAccountState {
self = [super init];
if (self) {
DCHECK(syncService);
CHECK(authenticationService);
_syncService = syncService;
_syncObserver = std::make_unique<SyncObserverBridge>(self, syncService);
_identityManager = identityManager;
_identityManagerObserver =
std::make_unique<signin::IdentityManagerObserverBridge>(identityManager,
self);
_authenticationService = authenticationService;
_chromeAccountManagerService = accountManagerService;
_accountManagerServiceObserver =
std::make_unique<ChromeAccountManagerServiceObserverBridge>(
self, _chromeAccountManagerService);
_signedInIdentity = _authenticationService->GetPrimaryIdentity(
signin::ConsentLevel::kSignin);
_prefService = prefService;
_initialAccountState = initialAccountState;
// Register for font size change notifications
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(preferredContentSizeChanged:)
name:UIContentSizeCategoryDidChangeNotification
object:nil];
}
return self;
}
- (void)disconnect {
_syncObserver.reset();
_syncService = nullptr;
_identityManager = nullptr;
_identityManagerObserver.reset();
_authenticationService = nullptr;
_chromeAccountManagerService = nullptr;
_accountManagerServiceObserver.reset();
_prefService = nullptr;
_signedInIdentity = nil;
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)autofillAlertConfirmed:(BOOL)value {
_syncService->GetUserSettings()->SetSelectedType(
syncer::UserSelectableType::kAutofill, value);
}
#pragma mark - Loads sync data type section
// Loads the sync data type section.
- (void)loadSyncDataTypeSection {
TableViewModel* model = self.consumer.tableViewModel;
switch (self.syncAccountState) {
case SyncSettingsAccountState::kSignedOut:
return;
case SyncSettingsAccountState::kSyncing:
[model addSectionWithIdentifier:SyncDataTypeSectionIdentifier];
if (self.allItemsAreSynceable) {
SyncSwitchItem* button =
[[SyncSwitchItem alloc] initWithType:SyncEverythingItemType];
button.text = GetNSString(IDS_IOS_SYNC_EVERYTHING_TITLE);
button.accessibilityIdentifier =
kSyncEverythingItemAccessibilityIdentifier;
self.syncEverythingItem = button;
[self updateSyncEverythingItemNotifyConsumer:NO];
} else {
TableViewInfoButtonItem* button = [[TableViewInfoButtonItem alloc]
initWithType:SyncEverythingItemType];
button.text = GetNSString(IDS_IOS_SYNC_EVERYTHING_TITLE);
button.statusText = GetNSString(IDS_IOS_SETTING_OFF);
button.accessibilityIdentifier =
kSyncEverythingItemAccessibilityIdentifier;
self.syncEverythingItem = button;
}
self.syncEverythingItem.accessibilityIdentifier =
kSyncEverythingItemAccessibilityIdentifier;
[model addItem:self.syncEverythingItem
toSectionWithIdentifier:SyncDataTypeSectionIdentifier];
break;
case SyncSettingsAccountState::kSignedIn:
BOOL would_clear_data_on_signout =
_authenticationService->ShouldClearDataForSignedInPeriodOnSignOut();
[model addSectionWithIdentifier:SyncDataTypeSectionIdentifier];
TableViewTextHeaderFooterItem* headerItem =
[[TableViewTextHeaderFooterItem alloc]
initWithType:TypesListHeaderOrFooterType];
headerItem.text = l10n_util::GetNSString(
IDS_IOS_GOOGLE_ACCOUNT_SETTINGS_TYPES_LIST_HEADER);
[model setHeader:headerItem
forSectionWithIdentifier:SyncDataTypeSectionIdentifier];
TableViewTextHeaderFooterItem* footerItem =
[[TableViewTextHeaderFooterItem alloc]
initWithType:TypesListHeaderOrFooterType];
footerItem.subtitle =
would_clear_data_on_signout
? l10n_util::GetNSString(
IDS_IOS_GOOGLE_ACCOUNT_SETTINGS_TYPES_LIST_DESCRIPTION_FOR_MANAGED_ACCOUNT)
: l10n_util::GetNSString(
IDS_IOS_GOOGLE_ACCOUNT_SETTINGS_TYPES_LIST_DESCRIPTION);
[model setFooter:footerItem
forSectionWithIdentifier:SyncDataTypeSectionIdentifier];
break;
}
NSMutableArray* syncSwitchItems = [[NSMutableArray alloc] init];
if (self.syncAccountState == SyncSettingsAccountState::kSignedIn) {
for (syncer::UserSelectableType dataType : kAccountSwitchItems) {
TableViewItem* switchItem = [self tableViewItemWithDataType:dataType];
[syncSwitchItems addObject:switchItem];
[model addItem:switchItem
toSectionWithIdentifier:SyncDataTypeSectionIdentifier];
}
} else {
for (syncer::UserSelectableType dataType : kSyncSwitchItems) {
TableViewItem* switchItem = [self tableViewItemWithDataType:dataType];
[syncSwitchItems addObject:switchItem];
[model addItem:switchItem
toSectionWithIdentifier:SyncDataTypeSectionIdentifier];
}
}
self.syncSwitchItems = syncSwitchItems;
[self updateSyncItemsNotifyConsumer:NO];
}
// Updates the sync everything item, and notify the consumer if `notifyConsumer`
// is set to YES.
- (void)updateSyncEverythingItemNotifyConsumer:(BOOL)notifyConsumer {
switch (self.syncAccountState) {
case SyncSettingsAccountState::kSignedOut:
case SyncSettingsAccountState::kSignedIn:
return;
case SyncSettingsAccountState::kSyncing:
if ([self.syncEverythingItem
isKindOfClass:[TableViewInfoButtonItem class]]) {
// It's possible that the sync everything pref remains true when a
// policy change doesn't allow to sync everthing anymore. Fix that here.
BOOL isSyncingEverything =
_syncService->GetUserSettings()->IsSyncEverythingEnabled();
BOOL canSyncEverything = self.allItemsAreSynceable;
if (isSyncingEverything && !canSyncEverything) {
_syncService->GetUserSettings()->SetSelectedTypes(
false, _syncService->GetUserSettings()->GetSelectedTypes());
}
return;
}
BOOL shouldSyncEverythingBeEditable = !self.disabledBecauseOfSyncError;
BOOL shouldSyncEverythingItemBeOn =
_syncService->GetUserSettings()->IsSyncEverythingEnabled();
SyncSwitchItem* syncEverythingItem =
base::apple::ObjCCastStrict<SyncSwitchItem>(self.syncEverythingItem);
BOOL needsUpdate =
(syncEverythingItem.on != shouldSyncEverythingItemBeOn) ||
(syncEverythingItem.enabled != shouldSyncEverythingBeEditable);
syncEverythingItem.on = shouldSyncEverythingItemBeOn;
syncEverythingItem.enabled = shouldSyncEverythingBeEditable;
if (needsUpdate && notifyConsumer) {
[self.consumer reloadItem:self.syncEverythingItem];
}
break;
}
}
// Returns the management state for this browser and profile.
- (ManagementState)managementState {
return GetManagementState(_identityManager, _authenticationService,
_prefService);
}
// Updates the consumer when the primary account is updated.
- (void)updatePrimaryAccountDetails {
switch (self.syncAccountState) {
case SyncSettingsAccountState::kSignedOut:
case SyncSettingsAccountState::kSyncing:
return;
case SyncSettingsAccountState::kSignedIn:
[self.consumer
updatePrimaryAccountWithAvatarImage:
_chromeAccountManagerService->GetIdentityAvatarWithIdentity(
_signedInIdentity, IdentityAvatarSize::Large)
name:_signedInIdentity.userFullName
email:_signedInIdentity.userEmail
managementState:self.managementState];
break;
}
}
// Updates all the sync data type items, and notify the consumer if
// `notifyConsumer` is set to YES.
- (void)updateSyncItemsNotifyConsumer:(BOOL)notifyConsumer {
for (TableViewItem* item in self.syncSwitchItems) {
if ([item isKindOfClass:[TableViewInfoButtonItem class]])
continue;
SyncSwitchItem* syncSwitchItem =
base::apple::ObjCCast<SyncSwitchItem>(item);
syncer::UserSelectableType dataType =
static_cast<syncer::UserSelectableType>(syncSwitchItem.dataType);
BOOL isDataTypeSynced =
_syncService->GetUserSettings()->GetSelectedTypes().Has(dataType);
BOOL isEnabled = self.shouldSyncDataItemEnabled &&
![self isManagedSyncSettingsDataType:dataType];
if (self.syncAccountState == SyncSettingsAccountState::kSignedIn &&
dataType == syncer::UserSelectableType::kHistory) {
// kHistory toggle represents both kHistory and kTabs in this case.
// kHistory and kTabs should usually have the same value, but in some
// cases they may not, e.g. if one of them is disabled by policy. In that
// case, show the toggle as on if at least one of them is enabled. The
// toggle should reflect the value of the non-disabled type.
isDataTypeSynced =
_syncService->GetUserSettings()->GetSelectedTypes().Has(
syncer::UserSelectableType::kHistory) ||
_syncService->GetUserSettings()->GetSelectedTypes().Has(
syncer::UserSelectableType::kTabs);
isEnabled = self.shouldSyncDataItemEnabled &&
(![self isManagedSyncSettingsDataType:
syncer::UserSelectableType::kHistory] ||
![self isManagedSyncSettingsDataType:
syncer::UserSelectableType::kTabs]);
}
BOOL needsUpdate = (syncSwitchItem.on != isDataTypeSynced) ||
(syncSwitchItem.isEnabled != isEnabled);
syncSwitchItem.on = isDataTypeSynced;
syncSwitchItem.enabled = isEnabled;
if (needsUpdate && notifyConsumer) {
[self.consumer reloadItem:syncSwitchItem];
}
}
}
#pragma mark - Loads the advanced settings section
// Loads the advanced settings section.
- (void)loadAdvancedSettingsSection {
if (self.syncAccountState == SyncSettingsAccountState::kSignedOut) {
return;
}
if (self.syncAccountState == SyncSettingsAccountState::kSignedIn &&
self.isSyncDisabledByAdministrator) {
return;
}
TableViewModel* model = self.consumer.tableViewModel;
[model addSectionWithIdentifier:AdvancedSettingsSectionIdentifier];
// EncryptionItemType.
self.encryptionItem =
[[TableViewImageItem alloc] initWithType:EncryptionItemType];
self.encryptionItem.accessibilityIdentifier =
kEncryptionAccessibilityIdentifier;
self.encryptionItem.title = GetNSString(IDS_IOS_MANAGE_SYNC_ENCRYPTION);
// The detail text (if any) is an error message, so color it in red.
self.encryptionItem.detailTextColor = [UIColor colorNamed:kRedColor];
// For kNeedsTrustedVaultKey, the disclosure indicator should not
// be shown since the reauth dialog for the trusted vault is presented from
// the bottom, and is not part of navigation controller.
const syncer::SyncService::UserActionableError error =
_syncService->GetUserActionableError();
BOOL hasDisclosureIndicator =
error != syncer::SyncService::UserActionableError::
kNeedsTrustedVaultKeyForPasswords &&
error != syncer::SyncService::UserActionableError::
kNeedsTrustedVaultKeyForEverything;
if (hasDisclosureIndicator) {
self.encryptionItem.accessoryView = [[UIImageView alloc]
initWithImage:DefaultAccessorySymbolConfigurationWithRegularWeight(
kChevronForwardSymbol)];
self.encryptionItem.accessoryView.tintColor =
[UIColor colorNamed:kTextQuaternaryColor];
} else {
self.encryptionItem.accessoryView = nil;
}
self.encryptionItem.accessibilityTraits |= UIAccessibilityTraitButton;
[self updateEncryptionItem:NO];
[model addItem:self.encryptionItem
toSectionWithIdentifier:AdvancedSettingsSectionIdentifier];
if (IsLinkedServicesSettingIosEnabled()) {
// PersonalizeGoogleServicesItemType.
TableViewImageItem* personalizeGoogleServicesItem =
[[TableViewImageItem alloc]
initWithType:PersonalizeGoogleServicesItemType];
if (self.isEEAAccount) {
personalizeGoogleServicesItem.title = GetNSString(
IDS_IOS_MANAGE_SYNC_PERSONALIZE_GOOGLE_SERVICES_TITLE_EEA);
personalizeGoogleServicesItem.accessoryView = [[UIImageView alloc]
initWithImage:DefaultAccessorySymbolConfigurationWithRegularWeight(
kChevronForwardSymbol)];
} else {
personalizeGoogleServicesItem.title =
GetNSString(IDS_IOS_MANAGE_SYNC_PERSONALIZE_GOOGLE_SERVICES_TITLE);
personalizeGoogleServicesItem.accessoryView = [[UIImageView alloc]
initWithImage:DefaultAccessorySymbolConfigurationWithRegularWeight(
kExternalLinkSymbol)];
}
personalizeGoogleServicesItem.accessoryView.tintColor =
[UIColor colorNamed:kTextQuaternaryColor];
personalizeGoogleServicesItem.detailText = GetNSString(
IDS_IOS_MANAGE_SYNC_PERSONALIZE_GOOGLE_SERVICES_DESCRIPTION);
personalizeGoogleServicesItem.accessibilityIdentifier =
kPersonalizeGoogleServicesIdentifier;
personalizeGoogleServicesItem.accessibilityTraits |=
UIAccessibilityTraitButton;
[model addItem:personalizeGoogleServicesItem
toSectionWithIdentifier:AdvancedSettingsSectionIdentifier];
} else {
// GoogleActivityControlsItemType.
TableViewImageItem* googleActivityControlsItem = [[TableViewImageItem alloc]
initWithType:GoogleActivityControlsItemType];
googleActivityControlsItem.accessoryView = [[UIImageView alloc]
initWithImage:DefaultAccessorySymbolConfigurationWithRegularWeight(
kExternalLinkSymbol)];
googleActivityControlsItem.accessoryView.tintColor =
[UIColor colorNamed:kTextQuaternaryColor];
googleActivityControlsItem.title =
GetNSString(IDS_IOS_MANAGE_SYNC_GOOGLE_ACTIVITY_CONTROLS_TITLE);
googleActivityControlsItem.detailText =
GetNSString(IDS_IOS_MANAGE_SYNC_GOOGLE_ACTIVITY_CONTROLS_DESCRIPTION);
googleActivityControlsItem.accessibilityTraits |=
UIAccessibilityTraitButton;
[model addItem:googleActivityControlsItem
toSectionWithIdentifier:AdvancedSettingsSectionIdentifier];
}
// AdvancedSettingsSectionIdentifier.
TableViewImageItem* dataFromChromeSyncItem =
[[TableViewImageItem alloc] initWithType:DataFromChromeSync];
dataFromChromeSyncItem.accessoryView = [[UIImageView alloc]
initWithImage:DefaultAccessorySymbolConfigurationWithRegularWeight(
kExternalLinkSymbol)];
dataFromChromeSyncItem.accessoryView.tintColor =
[UIColor colorNamed:kTextQuaternaryColor];
dataFromChromeSyncItem.accessibilityIdentifier =
kDataFromChromeSyncAccessibilityIdentifier;
dataFromChromeSyncItem.accessibilityTraits |= UIAccessibilityTraitButton;
switch (self.syncAccountState) {
case SyncSettingsAccountState::kSignedIn:
dataFromChromeSyncItem.title =
GetNSString(IDS_IOS_MANAGE_DATA_IN_YOUR_ACCOUNT_TITLE);
dataFromChromeSyncItem.detailText =
GetNSString(IDS_IOS_MANAGE_DATA_IN_YOUR_ACCOUNT_DESCRIPTION);
[model addItem:dataFromChromeSyncItem
toSectionWithIdentifier:AdvancedSettingsSectionIdentifier];
break;
case SyncSettingsAccountState::kSyncing:
dataFromChromeSyncItem.title =
GetNSString(IDS_IOS_MANAGE_SYNC_DATA_FROM_CHROME_SYNC_TITLE);
dataFromChromeSyncItem.detailText =
GetNSString(IDS_IOS_MANAGE_SYNC_DATA_FROM_CHROME_SYNC_DESCRIPTION);
[model addItem:dataFromChromeSyncItem
toSectionWithIdentifier:AdvancedSettingsSectionIdentifier];
break;
case SyncSettingsAccountState::kSignedOut:
NOTREACHED_IN_MIGRATION();
}
}
// Updates encryption item, and notifies the consumer if `notifyConsumer` is set
// to YES.
- (void)updateEncryptionItem:(BOOL)notifyConsumer {
if (![self.consumer.tableViewModel
hasSectionForSectionIdentifier:AdvancedSettingsSectionIdentifier]) {
return;
}
if (self.syncAccountState == SyncSettingsAccountState::kSignedIn &&
self.isSyncDisabledByAdministrator) {
[self.consumer.tableViewModel
removeSectionWithIdentifier:AdvancedSettingsSectionIdentifier];
return;
}
BOOL needsUpdate =
self.shouldEncryptionItemBeEnabled &&
(self.encryptionItem.enabled != self.shouldEncryptionItemBeEnabled);
self.encryptionItem.enabled = self.shouldEncryptionItemBeEnabled;
if (self.shouldEncryptionItemBeEnabled) {
self.encryptionItem.textColor = nil;
} else {
self.encryptionItem.textColor = [UIColor colorNamed:kTextSecondaryColor];
}
if (needsUpdate && notifyConsumer) {
[self.consumer reloadItem:self.encryptionItem];
}
}
#pragma mark - Loads sign out section
// Creates a footer item to display below the sign out button when forced
// sign-in is enabled.
- (TableViewItem*)createForcedSigninFooterItem {
// Add information about the forced sign-in policy below the sign-out
// button when forced sign-in is enabled.
TableViewLinkHeaderFooterItem* footerItem =
[[TableViewLinkHeaderFooterItem alloc]
initWithType:SignOutItemFooterType];
footerItem.text = l10n_util::GetNSString(
IDS_IOS_ENTERPRISE_FORCED_SIGNIN_MESSAGE_WITH_LEARN_MORE);
footerItem.urls =
@[ [[CrURL alloc] initWithGURL:GURL("chrome://management/")] ];
return footerItem;
}
- (void)loadSignOutAndTurnOffSyncSection {
// The SignOutAndTurnOffSyncSection only exists in
// SyncSettingsAccountState::kSyncing state.
switch (self.syncAccountState) {
case SyncSettingsAccountState::kSignedOut:
// kSignedOut is a temporary state; it only exists if the user just signed
// out and the UI is in the process of being dismissed. In this case,
// don't bother updating the section.
return;
case SyncSettingsAccountState::kSignedIn:
// For kSignedIn, loadSignOutAndManageAccountsSection will load the
// corresponding section.
return;
case SyncSettingsAccountState::kSyncing:
break;
}
// Creates the sign-out item and its section.
TableViewModel* model = self.consumer.tableViewModel;
NSInteger syncDataTypeSectionIndex =
[model sectionForSectionIdentifier:SyncDataTypeSectionIdentifier];
CHECK_NE(NSNotFound, syncDataTypeSectionIndex);
[model insertSectionWithIdentifier:ManageAndSignOutSectionIdentifier
atIndex:syncDataTypeSectionIndex + 1];
TableViewTextItem* item =
[[TableViewTextItem alloc] initWithType:SignOutAndTurnOffSyncItemType];
item.text = GetNSString(IDS_IOS_OPTIONS_ACCOUNTS_SIGN_OUT_TURN_OFF_SYNC);
item.textColor = [UIColor colorNamed:kRedColor];
self.signOutAndTurnOffSyncItem = item;
[model addItem:self.signOutAndTurnOffSyncItem
toSectionWithIdentifier:ManageAndSignOutSectionIdentifier];
if (self.forcedSigninEnabled) {
[model setFooter:[self createForcedSigninFooterItem]
forSectionWithIdentifier:ManageAndSignOutSectionIdentifier];
}
}
- (void)updateSignOutSection {
TableViewModel* model = self.consumer.tableViewModel;
BOOL hasSignOutSection =
[model hasSectionForSectionIdentifier:ManageAndSignOutSectionIdentifier];
switch (self.syncAccountState) {
case SyncSettingsAccountState::kSignedOut:
break;
case SyncSettingsAccountState::kSignedIn:
// There should be a sign-out section. Load it if it's not there yet.
if (!hasSignOutSection) {
[self loadSignOutAndManageAccountsSection];
NSUInteger sectionIndex = [model
sectionForSectionIdentifier:ManageAndSignOutSectionIdentifier];
[self.consumer
insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]
rowAnimation:NO];
}
break;
case SyncSettingsAccountState::kSyncing:
// There should be a sign-out section. Load it if it's not there yet.
if (!hasSignOutSection) {
[self loadSignOutAndTurnOffSyncSection];
DCHECK(self.signOutAndTurnOffSyncItem);
NSUInteger sectionIndex = [model
sectionForSectionIdentifier:ManageAndSignOutSectionIdentifier];
[self.consumer
insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]
rowAnimation:NO];
}
break;
}
}
- (void)loadSignOutAndManageAccountsSection {
if (self.syncAccountState != SyncSettingsAccountState::kSignedIn) {
return;
}
// Creates the manage accounts and sign-out section.
TableViewModel* model = self.consumer.tableViewModel;
// The AdvancedSettingsSectionIdentifier does not exist when sync is disabled
// by administrator for a signed-in not syncing account.
NSInteger previousSection =
[model hasSectionForSectionIdentifier:AdvancedSettingsSectionIdentifier]
? [model
sectionForSectionIdentifier:AdvancedSettingsSectionIdentifier]
: [model sectionForSectionIdentifier:SyncDataTypeSectionIdentifier];
CHECK_NE(NSNotFound, previousSection);
[model insertSectionWithIdentifier:ManageAndSignOutSectionIdentifier
atIndex:previousSection + 1];
// Creates items in the manage accounts and sign-out section.
// Manage Google Account item.
TableViewTextItem* item =
[[TableViewTextItem alloc] initWithType:ManageGoogleAccountItemType];
item.text =
GetNSString(IDS_IOS_GOOGLE_ACCOUNT_SETTINGS_MANAGE_GOOGLE_ACCOUNT_ITEM);
item.textColor = [UIColor colorNamed:kBlueColor];
[model addItem:item
toSectionWithIdentifier:ManageAndSignOutSectionIdentifier];
// Manage accounts on this device item.
item = [[TableViewTextItem alloc] initWithType:ManageAccountsItemType];
item.text = GetNSString(IDS_IOS_GOOGLE_ACCOUNT_SETTINGS_MANAGE_ACCOUNTS_ITEM);
item.textColor = [UIColor colorNamed:kBlueColor];
[model addItem:item
toSectionWithIdentifier:ManageAndSignOutSectionIdentifier];
// Sign out item.
item = [[TableViewTextItem alloc] initWithType:SignOutItemType];
item.text = GetNSString(IDS_IOS_GOOGLE_ACCOUNT_SETTINGS_SIGN_OUT_ITEM);
item.textColor = [UIColor colorNamed:kBlueColor];
[model addItem:item
toSectionWithIdentifier:ManageAndSignOutSectionIdentifier];
if (self.forcedSigninEnabled) {
[model setFooter:[self createForcedSigninFooterItem]
forSectionWithIdentifier:ManageAndSignOutSectionIdentifier];
}
}
#pragma mark - Loads batch upload section
- (TableViewTextItem*)batchUploadButtonItem {
TableViewTextItem* item =
[[TableViewTextItem alloc] initWithType:BatchUploadButtonItemType];
item.text = l10n_util::GetNSString(
IDS_IOS_GOOGLE_ACCOUNT_SETTINGS_BATCH_UPLOAD_BUTTON_ITEM);
item.textColor = [UIColor colorNamed:kBlueColor];
item.accessibilityIdentifier = kBatchUploadAccessibilityIdentifier;
return item;
}
- (NSString*)itemsToUploadRecommendationString {
// _localPasswordsToUpload and _localItemsToUpload should be updated by
// updateBatchUploadSectionWithNotifyConsumer before calling this method,
// which also checks for the case of having no items to upload, thus this case
// is not reached here.
if (!_localPasswordsToUpload && !_localItemsToUpload) {
NOTREACHED_IN_MIGRATION();
}
std::u16string userEmail =
base::SysNSStringToUTF16(_signedInIdentity.userEmail);
std::u16string itemsToUploadString;
if (_localPasswordsToUpload == 0) {
// No passwords, but there are other items to upload.
itemsToUploadString = base::i18n::MessageFormatter::FormatWithNamedArgs(
l10n_util::GetStringUTF16(
IDS_IOS_GOOGLE_ACCOUNT_SETTINGS_BATCH_UPLOAD_ITEMS_ITEM),
"count", static_cast<int>(_localItemsToUpload), "email", userEmail);
} else if (_localItemsToUpload > 0) {
// Multiple passwords and items to upload.
itemsToUploadString = base::i18n::MessageFormatter::FormatWithNamedArgs(
l10n_util::GetStringUTF16(
IDS_IOS_GOOGLE_ACCOUNT_SETTINGS_BATCH_UPLOAD_PASSWORDS_AND_ITEMS_ITEM),
"count", static_cast<int>(_localPasswordsToUpload), "email", userEmail);
} else {
// No items, but there are passwords to upload.
itemsToUploadString = base::i18n::MessageFormatter::FormatWithNamedArgs(
l10n_util::GetStringUTF16(
IDS_IOS_GOOGLE_ACCOUNT_SETTINGS_BATCH_UPLOAD_PASSWORDS_ITEM),
"count", static_cast<int>(_localPasswordsToUpload), "email", userEmail);
}
return base::SysUTF16ToNSString(itemsToUploadString);
}
- (SettingsImageDetailTextItem*)batchUploadRecommendationItem {
SettingsImageDetailTextItem* item = [[SettingsImageDetailTextItem alloc]
initWithType:BatchUploadRecommendationItemType];
item.detailText = [self itemsToUploadRecommendationString];
item.image = CustomSymbolWithPointSize(kCloudAndArrowUpSymbol,
kBatchUploadSymbolPointSize);
item.imageViewTintColor = [UIColor colorNamed:kBlueColor];
item.accessibilityIdentifier =
kBatchUploadRecommendationItemAccessibilityIdentifier;
return item;
}
// Loads the batch upload section.
- (void)loadBatchUploadSection {
// Drop the batch upload item from a previous loading.
self.batchUploadItem = nil;
// Create the batch upload section and item if needed.
[self updateBatchUploadSectionWithNotifyConsumer:NO firstLoad:YES];
}
// Fetches the local data descriptions from the sync server, and calls
// `-[ManageSyncSettingsMediator localDataDescriptionsFetchedWithDescription:]`
// to process those description.
- (void)fetchLocalDataDescriptionsForBatchUploadWithFirstLoad:(BOOL)firstLoad {
if (self.syncAccountState != SyncSettingsAccountState::kSignedIn) {
return;
}
// Types that are disabled by policy will be ignored.
syncer::DataTypeSet requestedTypes;
for (syncer::UserSelectableType userSelectableType : kAccountSwitchItems) {
if (![self isManagedSyncSettingsDataType:userSelectableType]) {
requestedTypes.Put(
syncer::UserSelectableTypeToCanonicalDataType(userSelectableType));
}
}
__weak __typeof__(self) weakSelf = self;
_syncService->GetLocalDataDescriptions(
requestedTypes,
base::BindOnce(^(std::map<syncer::DataType, syncer::LocalDataDescription>
description) {
[weakSelf localDataDescriptionsFetchedWithDescription:description
firstLoad:firstLoad];
}));
}
// Saves the local data description, and update the batch upload section.
- (void)localDataDescriptionsFetchedWithDescription:
(std::map<syncer::DataType, syncer::LocalDataDescription>)
description
firstLoad:(BOOL)firstLoad {
self.localPasswordsToUpload = 0;
self.localItemsToUpload = 0;
for (auto& type : description) {
if (type.first == syncer::PASSWORDS) {
self.localPasswordsToUpload = type.second.item_count;
continue;
} else {
self.localItemsToUpload += type.second.item_count;
}
}
[self updateBatchUploadSectionWithNotifyConsumer:YES firstLoad:firstLoad];
}
// Deletes the batch upload section and notifies the consumer about model
// changes.
- (void)removeBatchUploadSection {
TableViewModel* model = self.consumer.tableViewModel;
if (![model hasSectionForSectionIdentifier:BatchUploadSectionIdentifier]) {
return;
}
NSInteger sectionIndex =
[model sectionForSectionIdentifier:BatchUploadSectionIdentifier];
[model removeSectionWithIdentifier:BatchUploadSectionIdentifier];
self.batchUploadItem = nil;
// Remove the batch upload section from the table view model.
NSIndexSet* indexSet = [NSIndexSet indexSetWithIndex:sectionIndex];
[self.consumer deleteSections:indexSet rowAnimation:YES];
}
// Updates the batch upload section according to data already fetched.
// `notifyConsummer` if YES, call the consumer to update the table view.
// `firstLoad` if YES, load the section without animations.
- (void)updateBatchUploadSectionWithNotifyConsumer:(BOOL)notifyConsummer
firstLoad:(BOOL)firstLoad {
// Batch upload option is not shown if sync is disabled by policy, if the
// account is in a persistent error state that requires a user action, or if
// there is no local data to offer the batch upload.
if (self.syncErrorItem || self.isSyncDisabledByAdministrator ||
(!_localPasswordsToUpload && !_localItemsToUpload)) {
[self removeBatchUploadSection];
return;
}
TableViewModel* model = self.consumer.tableViewModel;
DCHECK(![model hasSectionForSectionIdentifier:SyncErrorsSectionIdentifier]);
switch (self.syncAccountState) {
case SyncSettingsAccountState::kSignedOut:
case SyncSettingsAccountState::kSyncing:
return;
case SyncSettingsAccountState::kSignedIn:
break;
}
NSInteger batchUploadSectionIndex = 0;
BOOL batchUploadSectionAlreadyExists = self.batchUploadItem;
if (!batchUploadSectionAlreadyExists) {
// Creates the batch upload section.
[model insertSectionWithIdentifier:BatchUploadSectionIdentifier
atIndex:batchUploadSectionIndex];
self.batchUploadItem = [self batchUploadRecommendationItem];
[model addItem:self.batchUploadItem
toSectionWithIdentifier:BatchUploadSectionIdentifier];
[model addItem:[self batchUploadButtonItem]
toSectionWithIdentifier:BatchUploadSectionIdentifier];
} else {
// The section already exists, update it.
self.batchUploadItem.detailText = [self itemsToUploadRecommendationString];
[self.consumer reloadItem:self.batchUploadItem];
}
if (!notifyConsummer) {
return;
}
NSIndexSet* indexSet = [NSIndexSet indexSetWithIndex:batchUploadSectionIndex];
if (batchUploadSectionAlreadyExists) {
// The section should be updated if it already exists.
[self.consumer reloadSections:indexSet];
} else {
// The animation is not needed if this is a first time load of the card.
[self.consumer insertSections:indexSet rowAnimation:!firstLoad];
}
}
#pragma mark - Private
// Creates a SyncSwitchItem or TableViewInfoButtonItem instance if the item is
// managed.
- (TableViewItem*)tableViewItemWithDataType:
(syncer::UserSelectableType)dataType {
NSInteger itemType = 0;
int textStringID = 0;
NSString* accessibilityIdentifier = nil;
switch (dataType) {
case syncer::UserSelectableType::kBookmarks:
itemType = BookmarksDataTypeItemType;
textStringID = IDS_SYNC_DATATYPE_BOOKMARKS;
accessibilityIdentifier = kSyncBookmarksIdentifier;
break;
case syncer::UserSelectableType::kHistory:
itemType = HistoryDataTypeItemType;
textStringID =
self.syncAccountState == SyncSettingsAccountState::kSignedIn
? IDS_SYNC_DATATYPE_HISTORY_AND_TABS
: IDS_SYNC_DATATYPE_TYPED_URLS;
accessibilityIdentifier =
self.syncAccountState == SyncSettingsAccountState::kSignedIn
? kSyncHistoryAndTabsIdentifier
: kSyncOmniboxHistoryIdentifier;
break;
case syncer::UserSelectableType::kPasswords:
itemType = PasswordsDataTypeItemType;
textStringID = IDS_SYNC_DATATYPE_PASSWORDS;
accessibilityIdentifier = kSyncPasswordsIdentifier;
break;
case syncer::UserSelectableType::kTabs:
itemType = OpenTabsDataTypeItemType;
textStringID = IDS_SYNC_DATATYPE_TABS;
accessibilityIdentifier = kSyncOpenTabsIdentifier;
break;
case syncer::UserSelectableType::kAutofill:
itemType = AutofillDataTypeItemType;
textStringID = IDS_SYNC_DATATYPE_AUTOFILL;
accessibilityIdentifier = kSyncAutofillIdentifier;
break;
case syncer::UserSelectableType::kPayments:
itemType = PaymentsDataTypeItemType;
textStringID = IDS_SYNC_DATATYPE_PAYMENTS;
accessibilityIdentifier = kSyncPaymentsIdentifier;
break;
case syncer::UserSelectableType::kPreferences:
itemType = SettingsDataTypeItemType;
textStringID = IDS_SYNC_DATATYPE_PREFERENCES;
accessibilityIdentifier = kSyncPreferencesIdentifier;
break;
case syncer::UserSelectableType::kReadingList:
itemType = ReadingListDataTypeItemType;
textStringID = IDS_SYNC_DATATYPE_READING_LIST;
accessibilityIdentifier = kSyncReadingListIdentifier;
break;
case syncer::UserSelectableType::kThemes:
case syncer::UserSelectableType::kExtensions:
case syncer::UserSelectableType::kApps:
case syncer::UserSelectableType::kSavedTabGroups:
case syncer::UserSelectableType::kSharedTabGroupData:
case syncer::UserSelectableType::kProductComparison:
case syncer::UserSelectableType::kCookies:
NOTREACHED_IN_MIGRATION();
break;
}
DCHECK_NE(itemType, 0);
DCHECK_NE(textStringID, 0);
DCHECK(accessibilityIdentifier);
BOOL isToggleEnabled = ![self isManagedSyncSettingsDataType:dataType];
if (self.syncAccountState == SyncSettingsAccountState::kSignedIn &&
dataType == syncer::UserSelectableType::kHistory) {
// kHistory toggle represents both kHistory and kTabs in this case.
// kHistory and kTabs should usually have the same value, but in some
// cases they may not, e.g. if one of them is disabled by policy. In that
// case, show the toggle as on if at least one of them is enabled. The
// toggle should reflect the value of the non-disabled type.
isToggleEnabled =
![self isManagedSyncSettingsDataType:syncer::UserSelectableType::
kHistory] ||
![self isManagedSyncSettingsDataType:syncer::UserSelectableType::kTabs];
}
if (isToggleEnabled) {
SyncSwitchItem* switchItem = [[SyncSwitchItem alloc] initWithType:itemType];
switchItem.text = GetNSString(textStringID);
switchItem.dataType = static_cast<NSInteger>(dataType);
switchItem.accessibilityIdentifier = accessibilityIdentifier;
return switchItem;
} else {
TableViewInfoButtonItem* button =
[[TableViewInfoButtonItem alloc] initWithType:itemType];
button.text = GetNSString(textStringID);
button.statusText = GetNSString(IDS_IOS_SETTING_OFF);
button.accessibilityIdentifier = accessibilityIdentifier;
return button;
}
}
// Updates the consumer when the content size is updated.
- (void)preferredContentSizeChanged:(NSNotification*)notification {
[self updatePrimaryAccountDetails];
}
#pragma mark - Properties
- (BOOL)disabledBecauseOfSyncError {
return _syncService->GetDisableReasons().Has(
syncer::SyncService::DISABLE_REASON_UNRECOVERABLE_ERROR);
}
- (BOOL)shouldSyncDataItemEnabled {
switch (self.syncAccountState) {
case SyncSettingsAccountState::kSignedOut:
return NO;
case SyncSettingsAccountState::kSignedIn:
return !self.disabledBecauseOfSyncError;
case SyncSettingsAccountState::kSyncing:
return (!_syncService->GetUserSettings()->IsSyncEverythingEnabled() ||
!self.allItemsAreSynceable) &&
!self.disabledBecauseOfSyncError;
}
}
// Only requires Sync-the-feature to not be disabled because of a sync error and
// to not need a trusted vault key.
- (BOOL)shouldEncryptionItemBeEnabled {
return !self.disabledBecauseOfSyncError &&
_syncService->GetUserActionableError() !=
syncer::SyncService::UserActionableError::
kNeedsTrustedVaultKeyForPasswords &&
_syncService->GetUserActionableError() !=
syncer::SyncService::UserActionableError::
kNeedsTrustedVaultKeyForEverything &&
_syncService->GetUserSettings()->IsCustomPassphraseAllowed();
}
- (NSString*)overrideViewControllerTitle {
switch (self.syncAccountState) {
case SyncSettingsAccountState::kSignedIn:
return l10n_util::GetNSString(IDS_IOS_GOOGLE_ACCOUNT_SETTINGS_TITLE);
case SyncSettingsAccountState::kSyncing:
case SyncSettingsAccountState::kSignedOut:
return nil;
}
}
- (SyncSettingsAccountState)syncAccountState {
// As the manage sync settings mediator is running, the sync account state
// does not change except only when the user signs out of their account.
if (_syncService->GetAccountInfo().IsEmpty()) {
return SyncSettingsAccountState::kSignedOut;
}
return _initialAccountState;
}
#pragma mark - ManageSyncSettingsTableViewControllerModelDelegate
- (void)manageSyncSettingsTableViewControllerLoadModel:
(id<ManageSyncSettingsConsumer>)controller {
DCHECK_EQ(self.consumer, controller);
if (!_authenticationService->GetPrimaryIdentity(
signin::ConsentLevel::kSignin)) {
// If the user signed out from this view or a child controller the view is
// closing and should not re-load the model.
return;
}
[self loadSyncErrorsSection];
[self loadBatchUploadSection];
[self loadSyncDataTypeSection];
[self loadSignOutAndTurnOffSyncSection];
[self loadAdvancedSettingsSection];
[self loadSignOutAndManageAccountsSection];
[self fetchLocalDataDescriptionsForBatchUploadWithFirstLoad:YES];
// Loading the header asks the consumer to reload the data, so it should be
// done after all sections are initially loaded.
[self updatePrimaryAccountDetails];
}
#pragma mark - SyncObserverModelBridge
- (void)onSyncStateChanged {
if (_ignoreSyncStateChanges) {
// The UI should not updated so the switch animations can run smoothly.
return;
}
[self updateSyncErrorsSection:YES];
[self updateBatchUploadSectionWithNotifyConsumer:YES firstLoad:NO];
[self updateSyncEverythingItemNotifyConsumer:YES];
[self updateSyncItemsNotifyConsumer:YES];
[self updateEncryptionItem:YES];
[self updateSignOutSection];
[self fetchLocalDataDescriptionsForBatchUploadWithFirstLoad:NO];
}
#pragma mark - IdentityManagerObserverBridgeDelegate
- (void)onPrimaryAccountChanged:
(const signin::PrimaryAccountChangeEvent&)event {
switch (event.GetEventTypeFor(signin::ConsentLevel::kSignin)) {
case signin::PrimaryAccountChangeEvent::Type::kSet:
_signedInIdentity = _authenticationService->GetPrimaryIdentity(
signin::ConsentLevel::kSignin);
[self updatePrimaryAccountDetails];
break;
case signin::PrimaryAccountChangeEvent::Type::kCleared:
// Temporary state, we can ignore this event, until the UI is signed out.
case signin::PrimaryAccountChangeEvent::Type::kNone:
break;
}
}
#pragma mark - ChromeAccountManagerServiceObserver
- (void)identityUpdated:(id<SystemIdentity>)identity {
if ([_signedInIdentity isEqual:identity]) {
[self updatePrimaryAccountDetails];
[self updateSyncItemsNotifyConsumer:YES];
[self updateSyncErrorsSection:YES];
[self updateBatchUploadSectionWithNotifyConsumer:YES firstLoad:NO];
[self updateEncryptionItem:YES];
[self fetchLocalDataDescriptionsForBatchUploadWithFirstLoad:NO];
}
}
- (void)onChromeAccountManagerServiceShutdown:
(ChromeAccountManagerService*)accountManagerService {
// TODO(crbug.com/40284086): Remove `[self disconnect]`.
[self disconnect];
}
#pragma mark - ManageSyncSettingsServiceDelegate
- (void)toggleSwitchItem:(TableViewItem*)item withValue:(BOOL)value {
{
SyncSwitchItem* syncSwitchItem =
base::apple::ObjCCast<SyncSwitchItem>(item);
syncSwitchItem.on = value;
if (value &&
static_cast<syncer::UserSelectableType>(syncSwitchItem.dataType) ==
syncer::UserSelectableType::kAutofill &&
_syncService->GetUserSettings()->IsUsingExplicitPassphrase()) {
[self.commandHandler showAdressesNotEncryptedDialog];
return;
}
// The notifications should be ignored to get smooth switch animations.
// Notifications are sent by SyncObserverModelBridge while changing
// settings.
base::AutoReset<BOOL> autoReset(&_ignoreSyncStateChanges, YES);
SyncSettingsItemType itemType =
static_cast<SyncSettingsItemType>(item.type);
switch (itemType) {
case SyncEverythingItemType:
if ([self.syncEverythingItem
isKindOfClass:[TableViewInfoButtonItem class]]) {
return;
}
_syncService->GetUserSettings()->SetSelectedTypes(
value, _syncService->GetUserSettings()->GetSelectedTypes());
break;
case HistoryDataTypeItemType: {
DCHECK(syncSwitchItem);
// Update History Sync decline prefs.
value ? history_sync::ResetDeclinePrefs(_prefService)
: history_sync::RecordDeclinePrefs(_prefService);
// Don't try to toggle the managed item.
if (![self isManagedSyncSettingsDataType:syncer::UserSelectableType::
kHistory]) {
_syncService->GetUserSettings()->SetSelectedType(
syncer::UserSelectableType::kHistory, value);
}
// In kSignedIn case, the kTabs toggle does not exist. Instead it's
// controlled by the history toggle.
if (self.syncAccountState == SyncSettingsAccountState::kSignedIn &&
![self isManagedSyncSettingsDataType:syncer::UserSelectableType::
kTabs]) {
_syncService->GetUserSettings()->SetSelectedType(
syncer::UserSelectableType::kTabs, value);
}
break;
}
case PaymentsDataTypeItemType:
case AutofillDataTypeItemType:
case BookmarksDataTypeItemType:
case OpenTabsDataTypeItemType:
case PasswordsDataTypeItemType:
case ReadingListDataTypeItemType:
case SettingsDataTypeItemType: {
// Don't try to toggle if item is managed.
DCHECK(syncSwitchItem);
syncer::UserSelectableType dataType =
static_cast<syncer::UserSelectableType>(syncSwitchItem.dataType);
if ([self isManagedSyncSettingsDataType:dataType]) {
break;
}
_syncService->GetUserSettings()->SetSelectedType(dataType, value);
break;
}
case SignOutAndTurnOffSyncItemType:
case ManageGoogleAccountItemType:
case ManageAccountsItemType:
case SignOutItemType:
case EncryptionItemType:
case GoogleActivityControlsItemType:
case DataFromChromeSync:
case PersonalizeGoogleServicesItemType:
case PrimaryAccountReauthErrorItemType:
case ShowPassphraseDialogErrorItemType:
case SyncNeedsTrustedVaultKeyErrorItemType:
case SyncTrustedVaultRecoverabilityDegradedErrorItemType:
case SyncDisabledByAdministratorErrorItemType:
case SignOutItemFooterType:
case TypesListHeaderOrFooterType:
case AccountErrorMessageItemType:
case BatchUploadButtonItemType:
case BatchUploadRecommendationItemType:
NOTREACHED_IN_MIGRATION();
break;
}
}
[self updateSyncEverythingItemNotifyConsumer:YES];
[self updateSyncItemsNotifyConsumer:YES];
// Switching toggles might affect the batch upload recommendation.
[self fetchLocalDataDescriptionsForBatchUploadWithFirstLoad:NO];
}
- (void)didSelectItem:(TableViewItem*)item cellRect:(CGRect)cellRect {
SyncSettingsItemType itemType = static_cast<SyncSettingsItemType>(item.type);
switch (itemType) {
case EncryptionItemType: {
const syncer::SyncService::UserActionableError error =
_syncService->GetUserActionableError();
if (error == syncer::SyncService::UserActionableError::
kNeedsTrustedVaultKeyForPasswords ||
error == syncer::SyncService::UserActionableError::
kNeedsTrustedVaultKeyForEverything) {
[self.syncErrorHandler openTrustedVaultReauthForFetchKeys];
break;
}
[self.syncErrorHandler openPassphraseDialogWithModalPresentation:NO];
break;
}
case GoogleActivityControlsItemType:
[self.commandHandler openWebAppActivityDialog];
break;
case DataFromChromeSync:
[self.commandHandler openDataFromChromeSyncWebPage];
break;
case PersonalizeGoogleServicesItemType:
if (self.isEEAAccount) {
[self.commandHandler openPersonalizeGoogleServices];
} else {
[self.commandHandler openWebAppActivityDialog];
}
break;
case PrimaryAccountReauthErrorItemType: {
id<SystemIdentity> identity = _authenticationService->GetPrimaryIdentity(
signin::ConsentLevel::kSignin);
if (_authenticationService->HasCachedMDMErrorForIdentity(identity)) {
[self.syncErrorHandler openMDMErrodDialogWithSystemIdentity:identity];
} else {
[self.syncErrorHandler openPrimaryAccountReauthDialog];
}
break;
}
case ShowPassphraseDialogErrorItemType:
[self.syncErrorHandler openPassphraseDialogWithModalPresentation:YES];
break;
case SyncNeedsTrustedVaultKeyErrorItemType:
[self.syncErrorHandler openTrustedVaultReauthForFetchKeys];
break;
case SyncTrustedVaultRecoverabilityDegradedErrorItemType:
[self.syncErrorHandler openTrustedVaultReauthForDegradedRecoverability];
break;
case SignOutAndTurnOffSyncItemType:
case SignOutItemType:
[self.commandHandler signOutFromTargetRect:cellRect];
break;
case ManageGoogleAccountItemType:
[self.commandHandler showManageYourGoogleAccount];
break;
case ManageAccountsItemType:
[self.commandHandler showAccountsPage];
break;
case BatchUploadButtonItemType:
[self.commandHandler openBulkUpload];
break;
case SyncEverythingItemType:
case AutofillDataTypeItemType:
case BookmarksDataTypeItemType:
case HistoryDataTypeItemType:
case OpenTabsDataTypeItemType:
case PasswordsDataTypeItemType:
case ReadingListDataTypeItemType:
case SettingsDataTypeItemType:
case PaymentsDataTypeItemType:
case SyncDisabledByAdministratorErrorItemType:
case SignOutItemFooterType:
case TypesListHeaderOrFooterType:
case AccountErrorMessageItemType:
case BatchUploadRecommendationItemType:
// Nothing to do.
break;
}
}
// Creates an item to display and handle the sync error for syncing users.
// `itemType` should only be one of those types:
// + PrimaryAccountReauthErrorItemType
// + ShowPassphraseDialogErrorItemType
// + SyncNeedsTrustedVaultKeyErrorItemType
// + SyncTrustedVaultRecoverabilityDegradedErrorItemType
- (TableViewItem*)createSyncErrorIconItemWithItemType:(NSInteger)itemType {
DCHECK((itemType == PrimaryAccountReauthErrorItemType) ||
(itemType == ShowPassphraseDialogErrorItemType) ||
(itemType == SyncNeedsTrustedVaultKeyErrorItemType) ||
(itemType == SyncTrustedVaultRecoverabilityDegradedErrorItemType))
<< "itemType: " << itemType;
TableViewDetailIconItem* syncErrorItem =
[[TableViewDetailIconItem alloc] initWithType:itemType];
syncErrorItem.textLayoutConstraintAxis = UILayoutConstraintAxisVertical;
syncErrorItem.text = GetNSString(IDS_IOS_SYNC_ERROR_TITLE);
syncErrorItem.detailText =
GetSyncErrorDescriptionForSyncService(_syncService);
syncErrorItem.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
switch (itemType) {
case ShowPassphraseDialogErrorItemType:
// Special case only for the sync passphrase error message. The regular
// error message should be still be displayed in the first settings
// screen.
syncErrorItem.detailText = GetNSString(
IDS_IOS_GOOGLE_SERVICES_SETTINGS_ENTER_PASSPHRASE_TO_START_SYNC);
break;
case SyncNeedsTrustedVaultKeyErrorItemType:
syncErrorItem.detailText =
GetNSString(IDS_IOS_GOOGLE_SERVICES_SETTINGS_SYNC_ENCRYPTION_FIX_NOW);
// Also override the title to be more accurate, if only passwords are
// being encrypted.
if (!_syncService->GetUserSettings()->IsEncryptEverythingEnabled()) {
syncErrorItem.text = GetNSString(IDS_IOS_SYNC_PASSWORDS_ERROR_TITLE);
}
break;
case SyncTrustedVaultRecoverabilityDegradedErrorItemType:
syncErrorItem.detailText = GetNSString(
_syncService->GetUserSettings()->IsEncryptEverythingEnabled()
? IDS_IOS_GOOGLE_SERVICES_SETTINGS_SYNC_FIX_RECOVERABILITY_DEGRADED_FOR_EVERYTHING
: IDS_IOS_GOOGLE_SERVICES_SETTINGS_SYNC_FIX_RECOVERABILITY_DEGRADED_FOR_PASSWORDS);
// Also override the title to be more accurate.
syncErrorItem.text = GetNSString(IDS_SYNC_NEEDS_VERIFICATION_TITLE);
break;
}
syncErrorItem.iconImage = DefaultSettingsRootSymbol(kSyncErrorSymbol);
syncErrorItem.iconBackgroundColor = [UIColor colorNamed:kRed500Color];
syncErrorItem.iconTintColor = UIColor.whiteColor;
syncErrorItem.iconCornerRadius = kColorfulBackgroundSymbolCornerRadius;
return syncErrorItem;
}
// Creates a message item to display the sync error description for signed in
// not syncing users.
- (TableViewItem*)createSyncErrorMessageItem:(int)messageID {
CHECK(self.syncAccountState == SyncSettingsAccountState::kSignedIn);
SettingsImageDetailTextItem* syncErrorItem =
[[SettingsImageDetailTextItem alloc]
initWithType:AccountErrorMessageItemType];
syncErrorItem.detailText = l10n_util::GetNSString(messageID);
syncErrorItem.image =
DefaultSymbolWithPointSize(kErrorCircleFillSymbol, kErrorSymbolPointSize);
syncErrorItem.imageViewTintColor = [UIColor colorNamed:kRed500Color];
return syncErrorItem;
}
// Creates an error action button item to handle the indicated sync error type
// for signed in not syncing users.
- (TableViewItem*)createSyncErrorButtonItemWithItemType:(NSInteger)itemType
buttonLabelID:(int)buttonLabelID {
CHECK((itemType == PrimaryAccountReauthErrorItemType) ||
(itemType == ShowPassphraseDialogErrorItemType) ||
(itemType == SyncNeedsTrustedVaultKeyErrorItemType) ||
(itemType == SyncTrustedVaultRecoverabilityDegradedErrorItemType))
<< "itemType: " << itemType;
CHECK(self.syncAccountState == SyncSettingsAccountState::kSignedIn);
TableViewTextItem* item = [[TableViewTextItem alloc] initWithType:itemType];
item.text = l10n_util::GetNSString(buttonLabelID);
item.textColor = [UIColor colorNamed:kBlueColor];
item.accessibilityTraits = UIAccessibilityTraitButton;
item.accessibilityIdentifier = kSyncErrorButtonIdentifier;
return item;
}
// Deletes the error section. If `notifyConsumer` is YES, the consumer is
// notified about model changes.
- (void)removeSyncErrorsSection:(BOOL)notifyConsumer {
TableViewModel* model = self.consumer.tableViewModel;
if (![model hasSectionForSectionIdentifier:SyncErrorsSectionIdentifier]) {
return;
}
NSInteger sectionIndex =
[model sectionForSectionIdentifier:SyncErrorsSectionIdentifier];
[model removeSectionWithIdentifier:SyncErrorsSectionIdentifier];
self.syncErrorItem = nil;
// Remove the sync error section from the table view model.
if (notifyConsumer) {
NSIndexSet* indexSet = [NSIndexSet indexSetWithIndex:sectionIndex];
[self.consumer deleteSections:indexSet rowAnimation:NO];
}
}
// Loads the sync errors section.
- (void)loadSyncErrorsSection {
// The `self.consumer.tableViewModel` will be reset prior to this method.
// Ignore any previous value the `self.syncErrorItem` may have contained.
self.syncErrorItem = nil;
[self updateSyncErrorsSection:NO];
}
// Updates the sync errors section. If `notifyConsumer` is YES, the consumer is
// notified about model changes.
- (void)updateSyncErrorsSection:(BOOL)notifyConsumer {
// Checks if the sync setup service state has changed from the saved state in
// the table view model.
std::optional<SyncSettingsItemType> type = [self syncErrorItemType];
if (![self needsErrorSectionUpdate:type]) {
return;
}
TableViewModel* model = self.consumer.tableViewModel;
// There is no sync error now, but there previously was an error.
if (!type.has_value()) {
[self removeSyncErrorsSection:notifyConsumer];
return;
}
// There is an error now and there might be a previous error.
BOOL errorSectionAlreadyExists = self.syncErrorItem;
if (self.syncAccountState == SyncSettingsAccountState::kSignedIn &&
errorSectionAlreadyExists) {
// As the previous error might not have a message item in case it is
// SyncDisabledByAdministratorError, clear the whole section instead of
// updating it's items.
errorSectionAlreadyExists = NO;
[self removeSyncErrorsSection:notifyConsumer];
}
if (self.syncAccountState == SyncSettingsAccountState::kSignedIn &&
GetAccountErrorUIInfo(_syncService) == nil) {
// In some transient states like in SyncService::TransportState::PAUSED,
// GetAccountErrorUIInfo returns nil and thus will not be able to fetch the
// current error data. In this case, do not update/add the error item.
return;
}
// Create the new sync error item.
DCHECK(type.has_value());
if (type.value() == SyncDisabledByAdministratorErrorItemType) {
self.syncErrorItem = [self createSyncDisabledByAdministratorErrorItem];
} else if (self.syncAccountState == SyncSettingsAccountState::kSignedIn) {
// For signed in not syncing users, the sync error item will be displayed as
// a button.
self.syncErrorItem =
[self createSyncErrorButtonItemWithItemType:type.value()
buttonLabelID:GetAccountErrorUIInfo(
_syncService)
.buttonLabelID];
} else {
// For syncing users, the sync error item will be displayed as
// an icon with descriptive text.
self.syncErrorItem =
[self createSyncErrorIconItemWithItemType:type.value()];
}
NSInteger syncErrorSectionIndex = 0;
if (!errorSectionAlreadyExists) {
if (self.syncAccountState == SyncSettingsAccountState::kSignedIn &&
type.value() != SyncDisabledByAdministratorErrorItemType) {
[model insertSectionWithIdentifier:SyncErrorsSectionIdentifier
atIndex:syncErrorSectionIndex];
// For signed in not syncing users, the sync error item will be preceded
// by a descriptive message item.
[model addItem:[self createSyncErrorMessageItem:GetAccountErrorUIInfo(
_syncService)
.messageID]
toSectionWithIdentifier:SyncErrorsSectionIdentifier];
[model addItem:self.syncErrorItem
toSectionWithIdentifier:SyncErrorsSectionIdentifier];
} else if (self.syncAccountState != SyncSettingsAccountState::kSignedIn) {
[model insertSectionWithIdentifier:SyncErrorsSectionIdentifier
atIndex:syncErrorSectionIndex];
[model addItem:self.syncErrorItem
toSectionWithIdentifier:SyncErrorsSectionIdentifier];
}
}
if (notifyConsumer) {
NSIndexSet* indexSet = [NSIndexSet indexSetWithIndex:syncErrorSectionIndex];
if (errorSectionAlreadyExists) {
[self.consumer reloadSections:indexSet];
} else {
[self.consumer insertSections:indexSet rowAnimation:NO];
}
}
}
// Returns the sync error item type or std::nullopt if the item
// is not an actionable error.
- (std::optional<SyncSettingsItemType>)syncErrorItemType {
if (self.isSyncDisabledByAdministrator) {
return SyncDisabledByAdministratorErrorItemType;
}
switch (_syncService->GetUserActionableError()) {
case syncer::SyncService::UserActionableError::kSignInNeedsUpdate:
return PrimaryAccountReauthErrorItemType;
case syncer::SyncService::UserActionableError::kNeedsPassphrase:
return ShowPassphraseDialogErrorItemType;
case syncer::SyncService::UserActionableError::
kNeedsTrustedVaultKeyForPasswords:
case syncer::SyncService::UserActionableError::
kNeedsTrustedVaultKeyForEverything:
return SyncNeedsTrustedVaultKeyErrorItemType;
case syncer::SyncService::UserActionableError::
kTrustedVaultRecoverabilityDegradedForPasswords:
case syncer::SyncService::UserActionableError::
kTrustedVaultRecoverabilityDegradedForEverything:
return SyncTrustedVaultRecoverabilityDegradedErrorItemType;
case syncer::SyncService::UserActionableError::kNone:
return std::nullopt;
}
NOTREACHED_IN_MIGRATION();
return std::nullopt;
}
// Returns whether the error state has changed since the last update.
- (BOOL)needsErrorSectionUpdate:(std::optional<SyncSettingsItemType>)type {
BOOL hasError = type.has_value();
return (hasError && !self.syncErrorItem) ||
(!hasError && self.syncErrorItem) ||
(hasError && self.syncErrorItem &&
type.value() != self.syncErrorItem.type);
}
// Returns an item to show to the user the sync cannot be turned on for an
// enterprise policy reason.
- (TableViewItem*)createSyncDisabledByAdministratorErrorItem {
TableViewImageItem* item = [[TableViewImageItem alloc]
initWithType:SyncDisabledByAdministratorErrorItemType];
item.image = [UIImage imageNamed:kGoogleServicesEnterpriseImage];
item.title = GetNSString(
IDS_IOS_GOOGLE_SERVICES_SETTINGS_SYNC_DISABLBED_BY_ADMINISTRATOR_TITLE);
item.enabled = NO;
item.textColor = [UIColor colorNamed:kTextSecondaryColor];
return item;
}
// Returns YES if the given type is managed by policies (i.e. is not syncable)
- (BOOL)isManagedSyncSettingsDataType:(syncer::UserSelectableType)type {
return _syncService->GetUserSettings()->IsTypeManagedByPolicy(type) ||
(self.syncAccountState == SyncSettingsAccountState::kSignedIn &&
self.isSyncDisabledByAdministrator);
}
#pragma mark - Properties
- (BOOL)isSyncDisabledByAdministrator {
return _syncService->HasDisableReason(
syncer::SyncService::DISABLE_REASON_ENTERPRISE_POLICY);
}
// Returns NO if any syncable item is managed, YES otherwise.
- (BOOL)allItemsAreSynceable {
// This method in not be called in the kSignedIn state, as there is no sync
// everything item.
CHECK(self.syncAccountState != SyncSettingsAccountState::kSignedIn);
for (const auto& type : kSyncSwitchItems) {
if ([self isManagedSyncSettingsDataType:type]) {
return NO;
}
}
return YES;
}
@end