chromium/ios/chrome/browser/ui/settings/google_services/bulk_upload/bulk_upload_mediator.mm

// Copyright 2023 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/bulk_upload/bulk_upload_mediator.h"

#import "base/check_op.h"
#import "base/containers/contains.h"
#import "base/i18n/message_formatter.h"
#import "base/memory/raw_ptr.h"
#import "base/metrics/user_metrics.h"
#import "base/strings/sys_string_conversions.h"
#import "base/strings/utf_string_conversions.h"
#import "components/signin/public/base/consent_level.h"
#import "components/signin/public/identity_manager/objc/identity_manager_observer_bridge.h"
#import "components/signin/public/identity_manager/primary_account_change_event.h"
#import "components/sync/base/data_type.h"
#import "components/sync/service/local_data_description.h"
#import "components/sync/service/sync_service.h"
#import "ios/chrome/browser/signin/model/system_identity.h"
#import "ios/chrome/browser/ui/settings/google_services/bulk_upload/bulk_upload_consumer.h"
#import "ios/chrome/browser/ui/settings/google_services/bulk_upload/bulk_upload_mediator_delegate.h"
#import "ios/chrome/browser/ui/settings/utils/password_utils.h"
#import "ios/chrome/common/ui/reauthentication/reauthentication_module.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ui/base/l10n/l10n_util.h"
#import "ui/base/l10n/l10n_util_mac.h"

namespace {

struct BulkUploadModelItem {
  syncer::DataType data_type;
  BulkUploadType bulk_upload_type;
  int title_string_id;
  NSString* const view_accessibility_id;
};

// List of data type to display for the bulk upload. The order will be used
// in the table view.

const std::array<BulkUploadModelItem, 3> GetUploadModelItems() {
  static const std::array<BulkUploadModelItem, 3> items = {
      {{
           syncer::BOOKMARKS,
           BulkUploadType::kBookmark,
           IDS_IOS_BULK_UPLOAD_BOOKMARK_TITLE,
           kBulkUploadTableViewBookmarksItemAccessibilityIdentifer,
       },
       {
           syncer::PASSWORDS,
           BulkUploadType::kPassword,
           IDS_IOS_BULK_UPLOAD_PASSWORD_TITLE,
           kBulkUploadTableViewPasswordsItemAccessibilityIdentifer,
       },
       {
           syncer::READING_LIST,
           BulkUploadType::kReadinglist,
           IDS_IOS_BULK_UPLOAD_READING_LIST_TITLE,
           kBulkUploadTableViewReadingListItemAccessibilityIdentifer,
       }},
  };

  return items;
}

}  // namespace

@interface BulkUploadMediator () <IdentityManagerObserverBridgeDelegate>
@end

@implementation BulkUploadMediator {
  // The identity manager for this service
  raw_ptr<signin::IdentityManager> _identityManager;
  // The email of the user, to display in snackbar message.
  // Observes changes in identity.
  std::unique_ptr<signin::IdentityManagerObserverBridge>
      _identityObserverBridge;
  // The sync service.
  raw_ptr<syncer::SyncService> _syncService;
  // Set of BulkUploadType whose item is selected.
  std::set<BulkUploadType> _selectedTypes;
  // Map returned by syncServer::GetLocalDataDescriptions, associating to each
  // type the descripton of the elements to upload.
  std::map<syncer::DataType, syncer::LocalDataDescription> _map;
  // Provides the face id or password authentication. It is required to bulk
  // upload passwords.
  // _reauthenticationModule needs to be retained until the callback is called.
  ReauthenticationModule* _reauthenticationModule;
}

- (instancetype)initWithSyncService:(syncer::SyncService*)syncService
                    identityManager:(signin::IdentityManager*)identityManager {
  self = [super init];
  CHECK(syncService);
  CHECK(identityManager);
  if (self) {
    _syncService = syncService;
    _identityManager = identityManager;
    _identityObserverBridge.reset(
        new signin::IdentityManagerObserverBridge(identityManager, self));
  }

  return self;
}

- (void)dealloc {
  DCHECK(!_syncService);
}

- (void)disconnect {
  _identityObserverBridge.reset();
  _syncService = nullptr;
}

- (void)setConsumer:(id<BulkUploadConsumer>)consumer {
  _consumer = consumer;
  if (!consumer) {
    return;
  }
  __weak BulkUploadMediator* weakSelf = self;
  syncer::DataTypeSet dataTypeSet;
  for (auto& modelItem : GetUploadModelItems()) {
    dataTypeSet.Put(modelItem.data_type);
  }
  _syncService->GetLocalDataDescriptions(
      dataTypeSet,
      base::BindOnce(
          ^(std::map<syncer::DataType, syncer::LocalDataDescription> map) {
            [weakSelf updateConsumer:map];
          }));
}

#pragma mark - Private

- (void)save {
  syncer::DataTypeSet selectedDataTypes = [self selectedDataTypeEnumSet];
  _syncService->TriggerLocalDataMigration(selectedDataTypes);
  int count = 0;
  // Count items for the selected types.
  for (syncer::DataType data_type : selectedDataTypes) {
    count += _map[data_type].item_count;
  }
  const std::string email =
      _identityManager->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin)
          .email;
  [self.delegate
      displayInSnackbar:base::SysUTF16ToNSString(
                            base::i18n::MessageFormatter::FormatWithNamedArgs(
                                l10n_util::GetStringUTF16(
                                    IDS_IOS_BULK_UPLOAD_SNACKBAR_MESSAGE),
                                "count", count, "email", email))

  ];
  [self.delegate mediatorWantsToBeDismissed:self];
}

// Processes the data and request the consumer to update its view accordingly.
// The map is assumed to contain data for all types the batch upload process can
// work with.
- (void)updateConsumer:
    (std::map<syncer::DataType, syncer::LocalDataDescription>)map {
  _map = map;
  NSMutableArray<BulkUploadViewItem*>* viewItems = [NSMutableArray array];
  for (auto& modelItem : GetUploadModelItems()) {
    if (!_map.contains(modelItem.data_type) ||
        _map.at(modelItem.data_type).item_count == 0) {
      continue;
    }
    _selectedTypes.insert(modelItem.bulk_upload_type);
    BulkUploadViewItem* viewItem =
        [self generateBulkUploadItemWithModelItem:modelItem];
    [viewItems addObject:viewItem];
  }
  [self.consumer updateViewWithViewItems:[viewItems copy]];
  [self updateButtonEnabledState];
}

// Process a single data type.
// `enabled` is the property that tracks whether this data is enabled.
// `oneElementId`, `twoElements`, `threeElementsId` are the id of the strings to
// display when there is one, two or three elements to display.
// `consumerUpdater` is the block that update the number and text of the view
- (BulkUploadViewItem*)generateBulkUploadItemWithModelItem:
    (BulkUploadModelItem)modelItem {
  syncer::LocalDataDescription description = _map[modelItem.data_type];
  CHECK_GT(description.domains.size(), 0ul)
      << "data type: " << static_cast<int>(modelItem.data_type);
  NSString* subtitle =
      base::SysUTF16ToNSString(syncer::GetDomainsDisplayText(description));
  BulkUploadViewItem* bulkUploadViewItem = [[BulkUploadViewItem alloc] init];
  bulkUploadViewItem.type = modelItem.bulk_upload_type;
  bulkUploadViewItem.title =
      base::SysUTF16ToNSString(l10n_util::GetPluralStringFUTF16(
          modelItem.title_string_id, description.item_count));
  bulkUploadViewItem.subtitle = subtitle;
  bulkUploadViewItem.selected =
      base::Contains(_selectedTypes, modelItem.bulk_upload_type);
  bulkUploadViewItem.accessibilityIdentifier = modelItem.view_accessibility_id;
  return bulkUploadViewItem;
}

// Updates the enabled state of the Save in Account button
- (void)updateButtonEnabledState {
  syncer::DataTypeSet selectedDataTypes = [self selectedDataTypeEnumSet];
  [self.consumer setValidationButtonEnabled:selectedDataTypes.size() > 0];
}

#pragma mark - BulkUploadMutator

- (void)bulkUploadViewItemWithType:(BulkUploadType)type
                        isSelected:(BOOL)selected {
  if (selected) {
    _selectedTypes.insert(type);
  } else {
    _selectedTypes.erase(type);
  }
  [self updateButtonEnabledState];
}

- (void)requestSave {
  base::RecordAction(base::UserMetricsAction("Signin_BulkUpload_Save"));
  // The user must authenticate if and only if they request to upload passwords.
  syncer::DataTypeSet selectedDataTypes = [self selectedDataTypeEnumSet];
  if (!selectedDataTypes.Has(syncer::DataType::PASSWORDS)) {
    [self save];
    return;
  }
  // Use util from password_utils to create reauth module to allow easy testing.
  _reauthenticationModule = password_manager::BuildReauthenticationModule();
  if (![_reauthenticationModule canAttemptReauth]) {
    base::RecordAction(
        base::UserMetricsAction("Signin_BulkUpload_FaceID_CannotBeStarted"));
    return;
  }
  __weak BulkUploadMediator* weakSelf = self;
  // The message to request authentification must mention the app will upload
  // passwords. If there are other types, they also get mentionned, even if they
  // don’t formally require authentication. This is because, if the
  // authentication fails, those other types are not saved either.
  int authentification_identifier =
      (selectedDataTypes.size() == 1)
          ? IDS_IOS_BULK_UPLOAD_PASSWORD_AUTHENTIFY_MESSAGE
          : IDS_IOS_BULK_UPLOAD_PASSWORD_AND_OTHER_TYPE_AUTHENTIFY_MESSAGE;
  [_reauthenticationModule
      attemptReauthWithLocalizedReason:l10n_util::GetNSString(
                                           authentification_identifier)
                  canReusePreviousAuth:NO
                               handler:^(ReauthenticationResult result) {
                                 [weakSelf
                                     reauthenticationModuleDidFinishWithResult:
                                         result];
                               }];
}

#pragma mark - IdentityManagerObserverBridgeDelegate

- (void)onPrimaryAccountChanged:
    (const signin::PrimaryAccountChangeEvent&)event {
  if (event.GetEventTypeFor(signin::ConsentLevel::kSignin) !=
      signin::PrimaryAccountChangeEvent::Type::kNone) {
    [self.delegate mediatorWantsToBeDismissed:self];
  }
}

#pragma mark - Private

// Returns data type set of the selected data types.
- (syncer::DataTypeSet)selectedDataTypeEnumSet {
  syncer::DataTypeSet dataTypeSet;
  for (auto& modelItem : GetUploadModelItems()) {
    if (base::Contains(_selectedTypes, modelItem.bulk_upload_type) &&
        _map[modelItem.data_type].item_count > 0) {
      dataTypeSet.Put(modelItem.data_type);
    }
  }
  return dataTypeSet;
}

// Called when `_reauthenticationModule` is finished.
- (void)reauthenticationModuleDidFinishWithResult:
    (ReauthenticationResult)result {
  CHECK(_reauthenticationModule);
  _reauthenticationModule = nil;
  switch (result) {
    case ReauthenticationResult::kSuccess: {
      base::RecordAction(
          base::UserMetricsAction("Signin_BulkUpload_FaceID_Success"));
      [self save];
      break;
    }
    case ReauthenticationResult::kFailure: {
      base::RecordAction(
          base::UserMetricsAction("Signin_BulkUpload_FaceID_Failed"));
      // TODO(crbug.com/40071049): Warns the user.
      break;
    }
    case ReauthenticationResult::kSkipped: {
      // This should not happens since `canReusePreviousAuth` is set to `NO`.
      NOTREACHED();
    }
  }
}

@end