chromium/ios/chrome/browser/ui/settings/clear_browsing_data/quick_delete_view_controller.mm

// Copyright 2024 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/clear_browsing_data/quick_delete_view_controller.h"

#import <UIKit/UIKit.h>

#import "base/check.h"
#import "components/browsing_data/core/browsing_data_utils.h"
#import "ios/chrome/browser/net/model/crurl.h"
#import "ios/chrome/browser/shared/model/url/chrome_url_constants.h"
#import "ios/chrome/browser/shared/ui/bottom_sheet/table_view_bottom_sheet_view_controller+subclassing.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_text_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/table_view_utils.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/ui/settings/cells/clear_browsing_data_constants.h"
#import "ios/chrome/browser/ui/settings/clear_browsing_data/quick_delete_mutator.h"
#import "ios/chrome/browser/ui/settings/clear_browsing_data/quick_delete_presentation_commands.h"
#import "ios/chrome/browser/ui/settings/clear_browsing_data/table_view_pop_up_cell.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/confirmation_alert/confirmation_alert_action_handler.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ui/base/l10n/l10n_util_mac.h"

namespace {

// Delay to observe when dismissing the UI after showing the confirmation
// indicator that the deletion has concluded.
constexpr NSTimeInterval kDismissDelay = 1;

// Trash icon view size.
constexpr CGFloat kTrashIconContainerViewSize = 64;

// Trash icon view corner radius.
constexpr CGFloat kTrashIconContainerViewCornerRadius = 15;

// Trash icon size that sits inside the entire view.
constexpr CGFloat kTrashIconSize = 32;

// Top padding for the trash icon view.
constexpr CGFloat kTrashIconContainerViewTopPadding = 33;

// Vertical padding for the title.
constexpr CGFloat kTitleVerticalPadding = 22;

// TableView's header and footer section heights.
constexpr CGFloat kSectionHeaderHeight = 10;
constexpr CGFloat kSectionFooterHeight = 0;

// TableView's corner radius size.
constexpr CGFloat kTableViewCornerRadius = 10;

// Section identifiers in Quick Delete's table view.
typedef NS_ENUM(NSInteger, SectionIdentifier) {
  SectionIdentifierTimeRange = kSectionIdentifierEnumZero,
  SectionIdentifierBrowsingData,
  SectionIdentifierFooter,
};

// Item identifiers in Quick Delete's table view.
typedef NS_ENUM(NSInteger, ItemIdentifier) {
  ItemIdentifierTimeRange = kItemTypeEnumZero,
  ItemIdentifierBrowsingData,
};

}  // namespace

@interface QuickDeleteViewController () <
    ConfirmationAlertActionHandler,
    UITableViewDelegate,
    TableViewLinkHeaderFooterItemDelegate> {
  UITableViewDiffableDataSource<NSNumber*, NSNumber*>* _dataSource;
  UITableView* _tableView;
  browsing_data::TimePeriod _timeRange;
  NSString* _browsingDataSummary;
  BOOL _shouldShowFooter;
  NSLayoutConstraint* _tableViewHeightConstraint;
}
@end

@implementation QuickDeleteViewController

- (void)focusOnBrowsingDataRow {
  UIView* browsingDataRow = [_tableView
      cellForRowAtIndexPath:[_dataSource indexPathForItemIdentifier:
                                             @(ItemIdentifierBrowsingData)]];
  UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification,
                                  browsingDataRow);
}

#pragma mark - UIViewController

- (instancetype)init {
  self = [super init];
  return self;
}

- (void)viewDidLoad {
  _tableView = [self createTableView];
  _dataSource = [self createAndFillDataSource];
  _tableView.dataSource = _dataSource;

  self.view.accessibilityViewIsModal = YES;

  // Set the properties read by the super when constructing the views in
  // `-[ConfirmationAlertViewController viewDidLoad]`.
  self.aboveTitleView = [self trashIconView];
  self.titleTextStyle = UIFontTextStyleTitle2;
  self.titleString = l10n_util::GetNSString(IDS_IOS_CLEAR_BROWSING_DATA_TITLE);
  self.customSpacing = kTitleVerticalPadding;
  self.primaryActionString =
      l10n_util::GetNSString(IDS_IOS_DELETE_BROWSING_DATA_BUTTON);
  self.secondaryActionString =
      l10n_util::GetNSString(IDS_IOS_DELETE_BROWSING_DATA_CANCEL);

  self.underTitleView = _tableView;

  self.showsVerticalScrollIndicator = NO;
  self.showDismissBarButton = NO;
  self.topAlignedLayout = YES;
  self.customScrollViewBottomInsets = 0;
  self.actionHandler = self;

  [super viewDidLoad];

  [self displayGradientView:NO];

  // Configure the color of the primary button to red, as the default colour is
  // blue.
  UIButtonConfiguration* buttonConfiguration =
      self.primaryActionButton.configuration;
  buttonConfiguration.background.backgroundColor =
      [UIColor colorNamed:kRedColor];
  self.primaryActionButton.configuration = buttonConfiguration;
  self.confirmationCheckmarkColor = [UIColor colorNamed:kRed600Color];
  self.confirmationButtonColor = [UIColor colorNamed:kRed100Color];

  // Assign the table view's anchors now that it is in the same hierarchy as the
  // top view and that the content has been loaded.
  _tableViewHeightConstraint = [_tableView.heightAnchor
      constraintEqualToConstant:_tableView.contentSize.height];

  [NSLayoutConstraint activateConstraints:@[
    [_tableView.widthAnchor
        constraintEqualToAnchor:self.primaryActionButton.widthAnchor],
    _tableViewHeightConstraint
  ]];
}

- (void)viewWillLayoutSubviews {
  [super viewWillLayoutSubviews];

  // Update the bottom sheet height since the browsing data row with the detail
  // text is bigger then the standard row height.
  [self updateBottomSheetHeight];
}

- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
  [super traitCollectionDidChange:previousTraitCollection];
  // Update the bottomsheet height when trait collection changed (for example
  // when the user uses large font).
  if (self.traitCollection.preferredContentSizeCategory !=
      previousTraitCollection.preferredContentSizeCategory) {
    [self updateBottomSheetHeight];
  }
}

#pragma mark - ConfirmationAlertActionHandler

- (void)confirmationAlertPrimaryAction {
  [_mutator triggerDeletion];
}

- (void)confirmationAlertSecondaryAction {
  [self dismissQuickDelete];
}

#pragma mark - UITableViewDelegate

- (void)tableView:(UITableView*)tableView
    didSelectRowAtIndexPath:(NSIndexPath*)indexPath {
  [_tableView deselectRowAtIndexPath:indexPath animated:NO];
  ItemIdentifier itemType = static_cast<ItemIdentifier>(
      [_dataSource itemIdentifierForIndexPath:indexPath].integerValue);
  CHECK(itemType == ItemIdentifierBrowsingData) << itemType;
  [self.presentationHandler showBrowsingDataPage];
}

- (UIView*)tableView:(UITableView*)tableView
    viewForFooterInSection:(NSInteger)section {
  SectionIdentifier sectionIdentifier = static_cast<SectionIdentifier>(
      [_dataSource sectionIdentifierForIndex:section].integerValue);
  switch (sectionIdentifier) {
    case SectionIdentifierFooter: {
      if (!_shouldShowFooter) {
        return nil;
      }
      TableViewLinkHeaderFooterView* footer =
          DequeueTableViewHeaderFooter<TableViewLinkHeaderFooterView>(
              _tableView);
      footer.accessibilityIdentifier = kQuickDeleteFooterIdentifier;
      footer.delegate = self;
      footer.urls = @[
        [[CrURL alloc]
            initWithGURL:GURL(kClearBrowsingDataDSESearchUrlInFooterURL)],
        [[CrURL alloc]
            initWithGURL:GURL(kClearBrowsingDataDSEMyActivityUrlInFooterURL)]
      ];
      [footer setText:l10n_util::GetNSString(
                          IDS_IOS_DELETE_BROWSING_DATA_BOTTOM_SHEET_FOOTER)
            withColor:[UIColor colorNamed:kTextSecondaryColor]];
      return footer;
    }
    case SectionIdentifierTimeRange:
    case SectionIdentifierBrowsingData: {
      return nil;
    }
  }
  NOTREACHED();
}

- (CGFloat)tableView:(UITableView*)tableView
    heightForFooterInSection:(NSInteger)section {
  SectionIdentifier sectionIdentifier = static_cast<SectionIdentifier>(
      [_dataSource sectionIdentifierForIndex:section].integerValue);
  if (sectionIdentifier == SectionIdentifierFooter && _shouldShowFooter) {
    return UITableViewAutomaticDimension;
  }
  return kSectionFooterHeight;
}

#pragma mark - TableViewLinkHeaderFooterItemDelegate

- (void)view:(TableViewLinkHeaderFooterView*)view didTapLinkURL:(CrURL*)url {
  DCHECK(url.gurl == kClearBrowsingDataDSESearchUrlInFooterURL ||
         url.gurl == kClearBrowsingDataDSEMyActivityUrlInFooterURL);
  [self.presentationHandler openMyActivityURL:url.gurl];
}

#pragma mark - QuickDeleteConsumer

- (void)setTimeRange:(browsing_data::TimePeriod)timeRange {
  if (_timeRange == timeRange) {
    return;
  }
  _timeRange = timeRange;

  // Reload the time range row with the new value.
  NSDiffableDataSourceSnapshot<NSNumber*, NSNumber*>* snapshot =
      [_dataSource snapshot];
  [snapshot reconfigureItemsWithIdentifiers:@[ @(ItemIdentifierTimeRange) ]];
  [_dataSource applySnapshot:snapshot animatingDifferences:NO completion:nil];
}

- (void)setBrowsingDataSummary:(NSString*)summary {
  if ([_browsingDataSummary isEqualToString:summary]) {
    return;
  }
  _browsingDataSummary = summary;

  // Reload the browsing data row with the new summary.
  __weak __typeof(self) weakSelf = self;
  NSDiffableDataSourceSnapshot<NSNumber*, NSNumber*>* snapshot =
      [_dataSource snapshot];
  [snapshot reconfigureItemsWithIdentifiers:@[ @(ItemIdentifierBrowsingData) ]];
  [_dataSource applySnapshot:snapshot
        animatingDifferences:NO
                  completion:^{
                    // Update the bottom sheet height since the browsing data
                    // row can change height depending on the length of summary.
                    [weakSelf updateBottomSheetHeight];
                  }];
}

- (void)setShouldShowFooter:(BOOL)shouldShowFooter {
  if (_shouldShowFooter == shouldShowFooter) {
    return;
  }
  _shouldShowFooter = shouldShowFooter;
  // Reload the footer section.
  __weak __typeof(self) weakSelf = self;
  NSDiffableDataSourceSnapshot<NSNumber*, NSNumber*>* snapshot =
      [_dataSource snapshot];
  [snapshot reloadSectionsWithIdentifiers:@[ @(SectionIdentifierFooter) ]];
  [_dataSource applySnapshot:snapshot
        animatingDifferences:NO
                  completion:^{
                    // Update the bottom sheet height in case the footer is
                    // added or removed.
                    [weakSelf updateBottomSheetHeight];
                  }];
}

- (void)updateHistoryWithResult:
    (const browsing_data::BrowsingDataCounter::Result&)result {
  // TODO(crbug.com/353211728): Refactor summary using this result.
}

- (void)updateTabsWithResult:
    (const browsing_data::BrowsingDataCounter::Result&)result {
  // TODO(crbug.com/353211728): Refactor summary using this result.
}

- (void)updateCacheWithResult:
    (const browsing_data::BrowsingDataCounter::Result&)result {
  // TODO(crbug.com/353211728): Refactor summary using this result.
}

- (void)updatePasswordsWithResult:
    (const browsing_data::BrowsingDataCounter::Result&)result {
  // TODO(crbug.com/353211728): Refactor summary using this result.
}

- (void)updateAutofillWithResult:
    (const browsing_data::BrowsingDataCounter::Result&)result {
  // TODO(crbug.com/353211728): Refactor summary using this result.
}

- (void)setHistorySelection:(BOOL)selected {
  // TODO(crbug.com/353211728): Refactor summary using this type selection.
}

- (void)setTabsSelection:(BOOL)selected {
  // TODO(crbug.com/353211728): Refactor summary using this type selection.
}

- (void)setSiteDataSelection:(BOOL)selected {
  // TODO(crbug.com/353211728): Refactor summary using this type selection.
}

- (void)setCacheSelection:(BOOL)selected {
  // TODO(crbug.com/353211728): Refactor summary using this type selection.
}

- (void)setPasswordsSelection:(BOOL)selected {
  // TODO(crbug.com/353211728): Refactor summary using this type selection.
}

- (void)setAutofillSelection:(BOOL)selected {
  // TODO(crbug.com/353211728): Refactor summary using this type selection.
}

- (void)deletionInProgress {
  self.view.window.userInteractionEnabled = NO;
  self.isLoading = YES;
  self.isConfirmed = NO;
}

- (void)deletionFinished {
  self.view.window.userInteractionEnabled = YES;
  self.isLoading = NO;
  self.isConfirmed = YES;
  TriggerHapticFeedbackForNotification(UINotificationFeedbackTypeSuccess);

  // Add an artificial delay for dimissing the UI, so the user is able to see
  // the confirmation state.
  [self performSelector:@selector(dismissQuickDelete)
             withObject:nil
             afterDelay:kDismissDelay];
}

#pragma mark - Private

// Triggers the dismission of the Quick Delete UI.
- (void)dismissQuickDelete {
  [self.presentationHandler dismissQuickDelete];
}

// Updates the bottom sheet height by also updating the table view height. The
// table view might have a different height after the browsing data summary is
// updated.
- (void)updateBottomSheetHeight {
  // Trigger any pending layout updates.
  [self.view layoutIfNeeded];

  _tableViewHeightConstraint.constant = _tableView.contentSize.height;
  [self setUpBottomSheetDetents];
}

// Returns `_tableView` used to show the time range and browsing data rows.
- (UITableView*)createTableView {
  UITableView* tableView =
      [[UITableView alloc] initWithFrame:CGRectZero
                                   style:ChromeTableViewStyle()];
  tableView.layer.cornerRadius = kTableViewCornerRadius;
  tableView.sectionHeaderHeight = kSectionHeaderHeight;
  tableView.sectionFooterHeight = kSectionFooterHeight;
  tableView.scrollEnabled = NO;
  tableView.showsVerticalScrollIndicator = NO;
  tableView.delegate = self;
  tableView.userInteractionEnabled = YES;

  tableView.translatesAutoresizingMaskIntoConstraints = NO;

  tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
  tableView.backgroundColor = UIColor.clearColor;

  // Remove extra space from UITableViewWrapperView.
  tableView.directionalLayoutMargins =
      NSDirectionalEdgeInsetsMake(0, CGFLOAT_MIN, 0, CGFLOAT_MIN);
  tableView.tableHeaderView =
      [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, CGFLOAT_MIN)];
  tableView.tableFooterView =
      [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, CGFLOAT_MIN)];

  return tableView;
}

// Returns `_tableView`'s data source with the time range and browsing data rows
// in different sections.
- (UITableViewDiffableDataSource<NSNumber*, NSNumber*>*)
    createAndFillDataSource {
  __weak __typeof(self) weakSelf = self;
  UITableViewDiffableDataSource* dataSource =
      [[UITableViewDiffableDataSource alloc]
          initWithTableView:_tableView
               cellProvider:^UITableViewCell*(UITableView* tableView,
                                              NSIndexPath* indexPath,
                                              NSNumber* itemIdentifier) {
                 return [weakSelf
                     cellForTableView:tableView
                            indexPath:indexPath
                       itemIdentifier:static_cast<ItemIdentifier>(
                                          itemIdentifier.integerValue)];
               }];

  RegisterTableViewCell<TableViewPopUpCell>(_tableView);
  RegisterTableViewCell<TableViewDetailTextCell>(_tableView);
  RegisterTableViewHeaderFooter<TableViewLinkHeaderFooterView>(_tableView);

  NSDiffableDataSourceSnapshot* snapshot =
      [[NSDiffableDataSourceSnapshot alloc] init];
  [snapshot appendSectionsWithIdentifiers:@[
    @(SectionIdentifierTimeRange), @(SectionIdentifierBrowsingData),
    @(SectionIdentifierFooter)
  ]];
  [snapshot appendItemsWithIdentifiers:@[ @(ItemIdentifierTimeRange) ]
             intoSectionWithIdentifier:@(SectionIdentifierTimeRange)];
  [snapshot appendItemsWithIdentifiers:@[ @(ItemIdentifierBrowsingData) ]
             intoSectionWithIdentifier:@(SectionIdentifierBrowsingData)];

  [dataSource applySnapshot:snapshot animatingDifferences:NO];

  return dataSource;
}

// Returns a cell for the specified `indexPath`.
- (UITableViewCell*)cellForTableView:(UITableView*)tableView
                           indexPath:(NSIndexPath*)indexPath
                      itemIdentifier:(ItemIdentifier)itemIdentifier {
  switch (itemIdentifier) {
    case ItemIdentifierTimeRange: {
      TableViewPopUpCell* timeRangeCell =
          DequeueTableViewCell<TableViewPopUpCell>(tableView);
      [timeRangeCell setMenu:[self timeRangeMenu]];
      [timeRangeCell
          setTitle:l10n_util::GetNSString(
                       IDS_IOS_CLEAR_BROWSING_DATA_TIME_RANGE_SELECTOR_TITLE)];
      timeRangeCell.userInteractionEnabled = YES;

      return timeRangeCell;
    }
    case ItemIdentifierBrowsingData: {
      TableViewDetailTextCell* browsingDataCell =
          DequeueTableViewCell<TableViewDetailTextCell>(tableView);
      browsingDataCell.textLabel.text =
          l10n_util::GetNSString(IDS_IOS_DELETE_BROWSING_DATA_TITLE);
      browsingDataCell.detailTextLabel.text = _browsingDataSummary;
      browsingDataCell.detailTextLabel.textColor =
          [UIColor colorNamed:kTextSecondaryColor];
      browsingDataCell.allowMultilineDetailText = YES;
      browsingDataCell.accessoryType =
          UITableViewCellAccessoryDisclosureIndicator;
      browsingDataCell.userInteractionEnabled = YES;
      browsingDataCell.backgroundColor =
          [UIColor colorNamed:kSecondaryBackgroundColor];
      browsingDataCell.accessibilityIdentifier =
          kQuickDeleteBrowsingDataButtonIdentifier;
      return browsingDataCell;
    }
  }
  NOTREACHED();
}

// Returns a UIMenu with all supported time ranges for deletion.
- (UIMenu*)timeRangeMenu {
  return [UIMenu
      menuWithTitle:@""
              image:nil
         identifier:nil
            options:UIMenuOptionsSingleSelection
           children:@[
             [self timeRangeAction:browsing_data::TimePeriod::LAST_15_MINUTES],
             [self timeRangeAction:browsing_data::TimePeriod::LAST_HOUR],
             [self timeRangeAction:browsing_data::TimePeriod::LAST_DAY],
             [self timeRangeAction:browsing_data::TimePeriod::LAST_WEEK],
             [self timeRangeAction:browsing_data::TimePeriod::FOUR_WEEKS],
             [self timeRangeAction:browsing_data::TimePeriod::ALL_TIME],
           ]];
}

// Returns a UIAction for the specified `timeRange`.
- (UIAction*)timeRangeAction:(browsing_data::TimePeriod)timeRange {
  __weak __typeof(self) weakSelf = self;
  UIAction* action =
      [UIAction actionWithTitle:[self titleForTimeRange:timeRange]
                          image:nil
                     identifier:nil
                        handler:^(id ignored) {
                          [weakSelf handleAction:timeRange];
                        }];

  if (timeRange == _timeRange) {
    action.state = UIMenuElementStateOn;
  }
  return action;
}

// Handle invoked when a `timeRange` is selected.
- (void)handleAction:(browsing_data::TimePeriod)timeRange {
  _timeRange = timeRange;
  [_mutator timeRangeSelected:_timeRange];
}

// Returns the title string based on the `timeRange`.
- (NSString*)titleForTimeRange:(browsing_data::TimePeriod)timeRange {
  switch (timeRange) {
    case browsing_data::TimePeriod::LAST_15_MINUTES:
      return l10n_util::GetNSString(
          IDS_IOS_CLEAR_BROWSING_DATA_TIME_RANGE_OPTION_LAST_15_MINUTES);
    case browsing_data::TimePeriod::LAST_HOUR:
      return l10n_util::GetNSString(
          IDS_IOS_CLEAR_BROWSING_DATA_TIME_RANGE_OPTION_PAST_HOUR);
    case browsing_data::TimePeriod::LAST_DAY:
      return l10n_util::GetNSString(
          IDS_IOS_CLEAR_BROWSING_DATA_TIME_RANGE_OPTION_PAST_DAY);
    case browsing_data::TimePeriod::LAST_WEEK:
      return l10n_util::GetNSString(
          IDS_IOS_CLEAR_BROWSING_DATA_TIME_RANGE_OPTION_PAST_WEEK);
    case browsing_data::TimePeriod::FOUR_WEEKS:
      return l10n_util::GetNSString(
          IDS_IOS_CLEAR_BROWSING_DATA_TIME_RANGE_OPTION_LAST_FOUR_WEEKS);
    case browsing_data::TimePeriod::ALL_TIME:
      return l10n_util::GetNSString(
          IDS_IOS_CLEAR_BROWSING_DATA_TIME_RANGE_OPTION_BEGINNING_OF_TIME);
    case browsing_data::TimePeriod::OLDER_THAN_30_DAYS:
      // This value should not be reached since it's not a part of the menu.
      break;
  }
  NOTREACHED();
}

// Returns a view of a trash icon with a red background with vertical padding.
- (UIView*)trashIconView {
  // Container of the trash icon that has the red background.
  UIView* iconContainerView = [[UIView alloc] init];
  iconContainerView.translatesAutoresizingMaskIntoConstraints = NO;
  iconContainerView.layer.cornerRadius = kTrashIconContainerViewCornerRadius;
  iconContainerView.backgroundColor = [UIColor colorNamed:kRed50Color];

  // Trash icon that inside the container with the red background.
  UIImageView* icon =
      [[UIImageView alloc] initWithImage:DefaultSymbolTemplateWithPointSize(
                                             kTrashSymbol, kTrashIconSize)];
  icon.clipsToBounds = YES;
  icon.translatesAutoresizingMaskIntoConstraints = NO;
  icon.tintColor = [UIColor colorNamed:kRedColor];
  [iconContainerView addSubview:icon];

  [NSLayoutConstraint activateConstraints:@[
    [iconContainerView.widthAnchor
        constraintEqualToConstant:kTrashIconContainerViewSize],
    [iconContainerView.heightAnchor
        constraintEqualToConstant:kTrashIconContainerViewSize],
  ]];
  AddSameCenterConstraints(iconContainerView, icon);

  // Padding for the icon container view.
  UIView* outerView = [[UIView alloc] init];
  [outerView addSubview:iconContainerView];
  AddSameCenterXConstraint(outerView, iconContainerView);
  AddSameConstraintsToSidesWithInsets(
      iconContainerView, outerView, LayoutSides::kTop | LayoutSides::kBottom,
      NSDirectionalEdgeInsetsMake(kTrashIconContainerViewTopPadding, 0, 0, 0));

  return outerView;
}

@end