chromium/ios/chrome/browser/history/ui_bundled/history_table_view_controller.mm

// Copyright 2018 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/history/ui_bundled/history_table_view_controller.h"

#import "base/apple/foundation_util.h"
#import "base/i18n/time_formatting.h"
#import "base/ios/ios_util.h"
#import "base/metrics/user_metrics.h"
#import "base/metrics/user_metrics_action.h"
#import "base/strings/sys_string_conversions.h"
#import "components/strings/grit/components_strings.h"
#import "components/sync/base/data_type.h"
#import "components/sync/service/sync_service.h"
#import "components/url_formatter/elide_url.h"
#import "components/url_formatter/url_formatter.h"
#import "ios/chrome/app/tests_hook.h"
#import "ios/chrome/browser/drag_and_drop/model/drag_item_util.h"
#import "ios/chrome/browser/drag_and_drop/model/table_view_url_drag_drop_handler.h"
#import "ios/chrome/browser/history/ui_bundled/history_entries_status_item.h"
#import "ios/chrome/browser/history/ui_bundled/history_entries_status_item_delegate.h"
#import "ios/chrome/browser/history/ui_bundled/history_entry_inserter.h"
#import "ios/chrome/browser/history/ui_bundled/history_entry_item.h"
#import "ios/chrome/browser/history/ui_bundled/history_menu_provider.h"
#import "ios/chrome/browser/history/ui_bundled/history_table_view_controller_delegate.h"
#import "ios/chrome/browser/history/ui_bundled/history_ui_constants.h"
#import "ios/chrome/browser/history/ui_bundled/history_util.h"
#import "ios/chrome/browser/history/ui_bundled/public/history_presentation_delegate.h"
#import "ios/chrome/browser/keyboard/ui_bundled/UIKeyCommand+Chrome.h"
#import "ios/chrome/browser/metrics/model/new_tab_page_uma.h"
#import "ios/chrome/browser/net/model/crurl.h"
#import "ios/chrome/browser/policy/model/policy_util.h"
#import "ios/chrome/browser/shared/coordinator/alert/action_sheet_coordinator.h"
#import "ios/chrome/browser/shared/model/browser/browser.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/shared/model/url/chrome_url_constants.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
#import "ios/chrome/browser/shared/public/commands/application_commands.h"
#import "ios/chrome/browser/shared/public/commands/command_dispatcher.h"
#import "ios/chrome/browser/shared/public/commands/quick_delete_commands.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/table_view/cells/table_view_text_link_item.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_url_item.h"
#import "ios/chrome/browser/shared/ui/table_view/table_view_favicon_data_source.h"
#import "ios/chrome/browser/shared/ui/table_view/table_view_navigation_controller_constants.h"
#import "ios/chrome/browser/shared/ui/table_view/table_view_utils.h"
#import "ios/chrome/browser/shared/ui/util/pasteboard_util.h"
#import "ios/chrome/browser/sync/model/sync_service_factory.h"
#import "ios/chrome/browser/ui/settings/clear_browsing_data/features.h"
#import "ios/chrome/browser/url_loading/model/url_loading_browser_agent.h"
#import "ios/chrome/browser/url_loading/model/url_loading_params.h"
#import "ios/chrome/browser/window_activities/model/window_activity_helpers.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/favicon/favicon_view.h"
#import "ios/chrome/common/ui/table_view/table_view_cells_constants.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/web/public/navigation/navigation_manager.h"
#import "ios/web/public/navigation/referrer.h"
#import "ios/web/public/web_state.h"
#import "ui/base/l10n/l10n_util.h"
#import "ui/base/l10n/l10n_util_mac.h"
#import "ui/strings/grit/ui_strings.h"

using history::BrowsingHistoryService;

namespace {
typedef NS_ENUM(NSInteger, ItemType) {
  ItemTypeHistoryEntry = kItemTypeEnumZero,
  ItemTypeEntriesStatus,
  ItemTypeEntriesStatusWithLink,
  ItemTypeActivityIndicator,
};
// Section identifier for the header (sync information) section.
const NSInteger kEntriesStatusSectionIdentifier = kSectionIdentifierEnumZero;
// Maximum number of entries to retrieve in a single query to history service.
const int kMaxFetchCount = 100;
// Separation space between sections.
const CGFloat kSeparationSpaceBetweenSections = 9;
// The default UIButton font size used by UIKit.
const CGFloat kButtonDefaultFontSize = 15.0;
// Horizontal width representing UIButton's padding.
const CGFloat kButtonHorizontalPadding = 30.0;
}  // namespace

@interface HistoryTableViewController () <HistoryEntriesStatusItemDelegate,
                                          HistoryEntryInserterDelegate,
                                          TableViewLinkHeaderFooterItemDelegate,
                                          TableViewURLDragDataSource,
                                          UISearchControllerDelegate,
                                          UISearchResultsUpdating,
                                          UISearchBarDelegate> {
  // Closure to request next page of history.
  base::OnceClosure _query_history_continuation;
}

// Object to manage insertion of history entries into the table view model.
@property(nonatomic, strong) HistoryEntryInserter* entryInserter;
// The current query for visible history entries.
@property(nonatomic, copy) NSString* currentQuery;
// The current status message for the tableView, it might be nil.
@property(nonatomic, copy) NSString* currentStatusMessage;
// YES if there are no results to show.
@property(nonatomic, assign) BOOL empty;
// YES if the history panel should show a notice about additional forms of
// browsing history.
@property(nonatomic, assign)
    BOOL shouldShowNoticeAboutOtherFormsOfBrowsingHistory;
// YES if there is an outstanding history query.
@property(nonatomic, assign, getter=isLoading) BOOL loading;
// YES if there is a search happening.
@property(nonatomic, assign) BOOL searchInProgress;
// NSMutableArray that holds all indexPaths for entries that will be filtered
// out by the search controller.
@property(nonatomic, strong)
    NSMutableArray<NSIndexPath*>* filteredOutEntriesIndexPaths;
// YES if there are no more history entries to load.
@property(nonatomic, assign, getter=hasFinishedLoading) BOOL finishedLoading;
// YES if the table should be filtered by the next received query result.
@property(nonatomic, assign) BOOL filterQueryResult;
// This ViewController's searchController;
@property(nonatomic, strong) UISearchController* searchController;
// NavigationController UIToolbar Buttons.
@property(nonatomic, strong) UIBarButtonItem* cancelButton;
@property(nonatomic, strong) UIBarButtonItem* clearBrowsingDataButton;
@property(nonatomic, strong) UIBarButtonItem* deleteButton;
@property(nonatomic, strong) UIBarButtonItem* editButton;
// Scrim when search box in focused.
@property(nonatomic, strong) UIControl* scrimView;
// Handler for URL drag interactions.
@property(nonatomic, strong) TableViewURLDragDropHandler* dragDropHandler;
@end

@implementation HistoryTableViewController

#pragma mark - ViewController Lifecycle.

- (instancetype)init {
  UITableViewStyle style = ChromeTableViewStyle();
  return [super initWithStyle:style];
}

- (void)viewDidLoad {
  [super viewDidLoad];
  [self loadModel];

  // TableView configuration
  self.tableView.estimatedRowHeight = 56;
  self.tableView.rowHeight = UITableViewAutomaticDimension;
  self.tableView.estimatedSectionHeaderHeight = 56;
  self.tableView.sectionFooterHeight = 0.0;
  self.tableView.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag;
  self.tableView.allowsMultipleSelectionDuringEditing = YES;
  self.clearsSelectionOnViewWillAppear = NO;
  self.tableView.allowsMultipleSelection = YES;
  self.tableView.accessibilityIdentifier = kHistoryTableViewIdentifier;
  // Add a tableFooterView in order to hide the separator lines where there's no
  // history content.
  self.tableView.tableFooterView = [[UIView alloc] init];

  self.dragDropHandler = [[TableViewURLDragDropHandler alloc] init];
  self.dragDropHandler.origin = WindowActivityHistoryOrigin;
  self.dragDropHandler.dragDataSource = self;
  self.tableView.dragDelegate = self.dragDropHandler;
  self.tableView.dragInteractionEnabled = true;

  // NavigationController configuration.
  self.title = l10n_util::GetNSString(IDS_HISTORY_TITLE);
  // Configures NavigationController Toolbar buttons.
  [self configureViewsForNonEditModeWithAnimation:NO];
  // Adds the "Done" button and hooks it up to `dismissHistory`.
  UIBarButtonItem* dismissButton = [[UIBarButtonItem alloc]
      initWithBarButtonSystemItem:UIBarButtonSystemItemDone
                           target:self
                           action:@selector(dismissHistory)];
  [dismissButton setAccessibilityIdentifier:
                     kHistoryNavigationControllerDoneButtonIdentifier];
  self.navigationItem.rightBarButtonItem = dismissButton;

  // SearchController Configuration.
  // Init the searchController with nil so the results are displayed on the same
  // TableView.
  self.searchController =
      [[UISearchController alloc] initWithSearchResultsController:nil];
  self.searchController.obscuresBackgroundDuringPresentation = NO;
  self.searchController.searchBar.delegate = self;
  self.searchController.searchResultsUpdater = self;
  self.searchController.searchBar.backgroundColor = UIColor.clearColor;
  self.searchController.searchBar.accessibilityIdentifier =
      kHistorySearchControllerSearchBarIdentifier;
  if (self.searchTerms.length) {
    self.searchController.searchBar.text = self.searchTerms;
    self.searchInProgress = YES;
  }
  // UIKit needs to know which controller will be presenting the
  // searchController. If we don't add this trying to dismiss while
  // SearchController is active will fail.
  self.definesPresentationContext = YES;

  self.scrimView = [[UIControl alloc] init];
  self.scrimView.alpha = 0.0f;
  self.scrimView.backgroundColor = [UIColor colorNamed:kScrimBackgroundColor];
  self.scrimView.translatesAutoresizingMaskIntoConstraints = NO;
  self.scrimView.accessibilityIdentifier = kHistorySearchScrimIdentifier;
  [self.scrimView addTarget:self
                     action:@selector(dismissSearchController:)
           forControlEvents:UIControlEventTouchUpInside];

  // Place the search bar in the navigation bar.
  [self updateNavigationBar];
  self.navigationItem.hidesSearchBarWhenScrolling = NO;
}

- (void)detachFromBrowser {
  // Clear C++ ivars.
  _browser = nullptr;
  _historyService = nullptr;
  [self dismissContextMenuCoordinator];
}

#pragma mark - TableViewModel

- (void)loadModel {
  [super loadModel];
  // Add Status section, this section will always exist during the lifetime of
  // HistoryTableVC. Its content will be driven by `updateEntriesStatusMessage`.
  [self.tableViewModel
      addSectionWithIdentifier:kEntriesStatusSectionIdentifier];
  _entryInserter =
      [[HistoryEntryInserter alloc] initWithModel:self.tableViewModel];
  _entryInserter.delegate = self;
  _empty = YES;
  [self showHistoryMatchingQuery:nil];
}

#pragma mark - Protocols

#pragma mark HistoryConsumer

- (void)historyQueryWasCompletedWithResults:
            (const std::vector<BrowsingHistoryService::HistoryEntry>&)results
                           queryResultsInfo:
                               (const BrowsingHistoryService::QueryResultsInfo&)
                                   queryResultsInfo
                        continuationClosure:
                            (base::OnceClosure)continuationClosure {
  if (!self.browser)
    return;

  self.loading = NO;
  _query_history_continuation = std::move(continuationClosure);

  // If history sync is enabled and there hasn't been a response from synced
  // history, try fetching again.
  syncer::SyncService* syncService =
      SyncServiceFactory::GetForBrowserState(self.browser->GetBrowserState());
  if (syncService->GetActiveDataTypes().Has(
          syncer::HISTORY_DELETE_DIRECTIVES) &&
      queryResultsInfo.sync_timed_out) {
    [self showHistoryMatchingQuery:_currentQuery];
    return;
  }

  // At this point there has been a response, stop the loading indicator.
  [self stopLoadingIndicatorWithCompletion:nil];

  // If there are no results and no URLs have been loaded, report that no
  // history entries were found.
  if (results.empty() && self.empty && !self.searchInProgress) {
    [self addEmptyTableViewBackground];
    [self updateToolbarButtonsWithAnimation:NO];
    return;
  }

  self.finishedLoading = queryResultsInfo.reached_beginning;
  self.empty = NO;
  [self removeEmptyTableViewBackground];

  // Header section should be updated outside of batch updates, otherwise
  // loading indicator removal will not be observed.
  [self updateEntriesStatusMessage];

  NSMutableArray* resultsItems = [NSMutableArray array];
  NSString* searchQuery =
      [base::SysUTF16ToNSString(queryResultsInfo.search_text) copy];

  // There should always be at least a header section present.
  DCHECK([[self tableViewModel] numberOfSections]);
  for (const BrowsingHistoryService::HistoryEntry& entry : results) {
    HistoryEntryItem* item =
        [[HistoryEntryItem alloc] initWithType:ItemTypeHistoryEntry
                         accessibilityDelegate:self];
    item.text = [history::FormattedTitle(entry.title, entry.url) copy];
    item.detailText = base::SysUTF16ToNSString(
        url_formatter::
            FormatUrlForDisplayOmitSchemePathTrivialSubdomainsAndMobilePrefix(
                entry.url));
    item.timeText =
        [base::SysUTF16ToNSString(base::TimeFormatTimeOfDay(entry.time)) copy];
    item.URL = entry.url;
    item.timestamp = entry.time;
    [resultsItems addObject:item];
  }

  [self updateToolbarButtonsWithAnimation:YES];

  if ((self.searchInProgress && [searchQuery length] > 0 &&
       [self.currentQuery isEqualToString:searchQuery]) ||
      self.filterQueryResult) {
    // If in search mode, filter out entries that are not part of the
    // search result.
    [self filterForHistoryEntries:resultsItems];
    [self
        deleteItemsFromTableViewModelWithIndex:self.filteredOutEntriesIndexPaths
                      deleteItemsFromTableView:NO];
    // Clear all objects that were just deleted from the tableViewModel.
    [self.filteredOutEntriesIndexPaths removeAllObjects];
    self.filterQueryResult = NO;
  }

  // Insert result items into the model.
  for (HistoryEntryItem* item in resultsItems) {
    [self.entryInserter insertHistoryEntryItem:item];
  }

  // Save the currently selected rows to preserve its state after the tableView
  // is reloaded. Since a query with selected rows can only happen when
  // scrolling down the tableView this should be safe. If this changes in the
  // future e.g. being able to search while selected rows exist, we should
  // update this.
  NSIndexPath* currentSelectedCells = [self.tableView indexPathForSelectedRow];
  [self.tableView reloadData];
  [self.tableView selectRowAtIndexPath:currentSelectedCells
                              animated:NO
                        scrollPosition:UITableViewScrollPositionNone];
  [self updateTableViewAfterDeletingEntries];
}

- (void)showNoticeAboutOtherFormsOfBrowsingHistory:(BOOL)shouldShowNotice {
  self.shouldShowNoticeAboutOtherFormsOfBrowsingHistory = shouldShowNotice;
  // Update the history entries status message if there is no query in progress.
  if (!self.isLoading) {
    [self updateEntriesStatusMessage];
  }
}

- (void)historyWasDeleted {
  // If history has been deleted, reload history filtering for the current
  // results. This only observes local changes to history, i.e. removing
  // history via the clear browsing data page.
  self.filterQueryResult = YES;
  [self showHistoryMatchingQuery:nil];
}

#pragma mark HistoryEntriesStatusItemDelegate

- (void)historyEntriesStatusItem:(HistoryEntriesStatusItem*)item
               didRequestOpenURL:(const GURL&)URL {
  // TODO(crbug.com/41366648): Migrate. This will navigate to the status message
  // "Show Full History" URL.
}

#pragma mark HistoryEntryInserterDelegate

- (void)historyEntryInserter:(HistoryEntryInserter*)inserter
    didInsertItemAtIndexPath:(NSIndexPath*)indexPath {
  // NO-OP since [self.tableView reloadData] will be called after the inserter
  // has completed its updates.
}

- (void)historyEntryInserter:(HistoryEntryInserter*)inserter
     didInsertSectionAtIndex:(NSInteger)sectionIndex {
  // NO-OP since [self.tableView reloadData] will be called after the inserter
  // has completed its updates.
}

- (void)historyEntryInserter:(HistoryEntryInserter*)inserter
     didRemoveSectionAtIndex:(NSInteger)sectionIndex {
  // NO-OP since [self.tableView reloadData] will be called after the inserter
  // has completed its updates.
}

#pragma mark HistoryEntryItemDelegate

- (void)historyEntryItemDidRequestOpen:(HistoryEntryItem*)item {
  [self openURL:item.URL];
}

- (void)historyEntryItemDidRequestDelete:(HistoryEntryItem*)item {
  NSInteger sectionIdentifier =
      [self.entryInserter sectionIdentifierForTimestamp:item.timestamp];
  if ([self.tableViewModel hasSectionForSectionIdentifier:sectionIdentifier] &&
      [self.tableViewModel hasItem:item
           inSectionWithIdentifier:sectionIdentifier]) {
    NSIndexPath* indexPath = [self.tableViewModel indexPathForItem:item];
    [self.tableView selectRowAtIndexPath:indexPath
                                animated:NO
                          scrollPosition:UITableViewScrollPositionNone];
    [self deleteSelectedItemsFromHistory];
  }
}

- (void)historyEntryItemDidRequestCopy:(HistoryEntryItem*)item {
  StoreURLInPasteboard(item.URL);
}

- (void)historyEntryItemDidRequestOpenInNewTab:(HistoryEntryItem*)item {
  [self openURLInNewTab:item.URL];
}

- (void)historyEntryItemDidRequestOpenInNewIncognitoTab:
    (HistoryEntryItem*)item {
  [self openURLInNewIncognitoTab:item.URL];
}

- (void)historyEntryItemShouldUpdateView:(HistoryEntryItem*)item {
  NSInteger sectionIdentifier =
      [self.entryInserter sectionIdentifierForTimestamp:item.timestamp];
  // If the item is still in the model, reconfigure it.
  if ([self.tableViewModel hasSectionForSectionIdentifier:sectionIdentifier] &&
      [self.tableViewModel hasItem:item
           inSectionWithIdentifier:sectionIdentifier]) {
    [self reconfigureCellsForItems:@[ item ]];
  }
}

#pragma mark TableViewLinkHeaderFooterItemDelegate

- (void)view:(TableViewLinkHeaderFooterView*)view didTapLinkURL:(CrURL*)URL {
  [self openURLInNewTab:URL.gurl];
}

#pragma mark UISearchResultsUpdating

- (void)updateSearchResultsForSearchController:
    (UISearchController*)searchController {
  DCHECK_EQ(self.searchController, searchController);
  NSString* text = searchController.searchBar.text;

  if (text.length == 0 && self.searchController.active) {
    [self showScrim];
  } else {
    [self hideScrim];
  }

  if (text.length != 0) {
    self.searchInProgress = YES;
  }

  [self showHistoryMatchingQuery:text];
}

#pragma mark UISearchControllerDelegate

- (void)willPresentSearchController:(UISearchController*)searchController {
  [self showScrim];
}

- (void)didDismissSearchController:(UISearchController*)searchController {
  [self hideScrim];
}

#pragma mark UISearchBarDelegate

- (void)searchBarTextDidBeginEditing:(UISearchBar*)searchBar {
  self.searchInProgress = YES;
  [self updateEntriesStatusMessage];
}

- (void)searchBarTextDidEndEditing:(UISearchBar*)searchBar {
  self.searchInProgress = NO;
  [self updateEntriesStatusMessage];
}

#pragma mark UIAdaptivePresentationControllerDelegate

- (void)presentationControllerWillDismiss:
    (UIPresentationController*)presentationController {
  if (self.searchInProgress) {
    // Dismiss the keyboard if trying to dismiss the VC so the keyboard doesn't
    // linger until the VC dismissal has completed.
    [self.searchController.searchBar endEditing:YES];
  }
}

- (void)presentationControllerDidDismiss:
    (UIPresentationController*)presentationController {
  base::RecordAction(base::UserMetricsAction("IOSHistoryCloseWithSwipe"));
  // Call the delegate dismissHistoryTableViewController:withCompletion: to
  // clean up state and stop the Coordinator.
  [self.delegate dismissHistoryTableViewController:self withCompletion:nil];
}

#pragma mark - History Data Updates

// Search history for text `query` and display the results. `query` may be nil.
// If query is empty, show all history items.
- (void)showHistoryMatchingQuery:(NSString*)query {
  self.finishedLoading = NO;
  self.currentQuery = query;
  [self fetchHistoryForQuery:query continuation:false];
}

// Deletes selected items from browser history and removes them from the
// tableView.
- (void)deleteSelectedItemsFromHistory {
  if (!self.browser)
    return;

  if (!self.historyService)
    return;

  // Validate indexes of items to delete and abort if any have been made invalid
  // by a crossing actions (like query refresh or animations).
  NSArray* toDeleteIndexPaths = self.tableView.indexPathsForSelectedRows;
  for (NSIndexPath* indexPath in toDeleteIndexPaths) {
    if (![self.tableViewModel hasItemAtIndexPath:indexPath]) {
      return;
    }
  }

  // Delete items from Browser History.
  std::vector<BrowsingHistoryService::HistoryEntry> entries;
  for (NSIndexPath* indexPath in toDeleteIndexPaths) {
    HistoryEntryItem* object = base::apple::ObjCCastStrict<HistoryEntryItem>(
        [self.tableViewModel itemAtIndexPath:indexPath]);
    BrowsingHistoryService::HistoryEntry entry;
    entry.url = object.URL;
    // TODO(crbug.com/40479288) Remove base::TimeXXX::ToInternalValue().
    entry.all_timestamps.insert(object.timestamp.ToInternalValue());
    entries.push_back(entry);
  }
  self.historyService->RemoveVisits(entries);

  // Delete items from `self.tableView` using performBatchUpdates.
  __weak __typeof(self) weakSelf = self;
  [self.tableView
      performBatchUpdates:^{
        [weakSelf deleteItemsFromTableViewModelWithIndex:toDeleteIndexPaths
                                deleteItemsFromTableView:YES];
      }
      completion:^(BOOL) {
        [weakSelf updateTableViewAfterDeletingEntries];
        [weakSelf configureViewsForNonEditModeWithAnimation:YES];
      }];
  base::RecordAction(base::UserMetricsAction("HistoryPage_RemoveSelected"));
}

#pragma mark - UITableViewDelegate

- (CGFloat)tableView:(UITableView*)tableView
    heightForFooterInSection:(NSInteger)section {
  if ([self.tableViewModel sectionIdentifierForSectionIndex:section] ==
      kEntriesStatusSectionIdentifier)
    return 0;
  return kSeparationSpaceBetweenSections;
}

- (CGFloat)tableView:(UITableView*)tableView
    heightForHeaderInSection:(NSInteger)section {
  // Hide the status header if the currentStatusMessage is nil.
  if ([self.tableViewModel sectionIdentifierForSectionIndex:section] ==
          kEntriesStatusSectionIdentifier &&
      [self.currentStatusMessage length] == 0)
    return 0;
  return UITableViewAutomaticDimension;
}

- (void)tableView:(UITableView*)tableView
    didSelectRowAtIndexPath:(NSIndexPath*)indexPath {
  DCHECK_EQ(tableView, self.tableView);
  if (self.isEditing) {
    [self updateToolbarButtonsWithAnimation:YES];
  } else {
    TableViewItem* item = [self.tableViewModel itemAtIndexPath:indexPath];
    // Only navigate and record metrics if a ItemTypeHistoryEntry was selected.
    if (item.type == ItemTypeHistoryEntry) {
      if (self.searchInProgress) {
        // Set the searchController active property to NO or the SearchBar will
        // cause the navigation controller to linger for a second  when
        // dismissing.
        self.searchController.active = NO;
        base::RecordAction(
            base::UserMetricsAction("HistoryPage_SearchResultClick"));
      } else {
        base::RecordAction(
            base::UserMetricsAction("HistoryPage_EntryLinkClick"));
      }
      HistoryEntryItem* historyItem =
          base::apple::ObjCCastStrict<HistoryEntryItem>(item);
      [self openURL:historyItem.URL];
    }
  }
}

- (void)tableView:(UITableView*)tableView
    didDeselectRowAtIndexPath:(NSIndexPath*)indexPath {
  DCHECK_EQ(tableView, self.tableView);
  if (self.editing)
    [self updateToolbarButtonsWithAnimation:YES];
}

- (BOOL)tableView:(UITableView*)tableView
    canEditRowAtIndexPath:(NSIndexPath*)indexPath {
  TableViewItem* item = [self.tableViewModel itemAtIndexPath:indexPath];
  return (item.type == ItemTypeHistoryEntry);
}

- (UIContextMenuConfiguration*)tableView:(UITableView*)tableView
    contextMenuConfigurationForRowAtIndexPath:(NSIndexPath*)indexPath
                                        point:(CGPoint)point {
  if (self.isEditing) {
    // Don't show the context menu when currently in editing mode.
    return nil;
  }
  if (![self.tableViewModel hasItemAtIndexPath:indexPath]) {
    // It's possible that indexPath is invalid due to crossing action (like
    // query refresh or animations).
    return nil;
  }
  if (indexPath.section ==
      [self.tableViewModel
          sectionForSectionIdentifier:kEntriesStatusSectionIdentifier]) {
    return nil;
  }

  HistoryEntryItem* entry = base::apple::ObjCCastStrict<HistoryEntryItem>(
      [self.tableViewModel itemAtIndexPath:indexPath]);
  UIView* cell = [self.tableView cellForRowAtIndexPath:indexPath];
  return [self.menuProvider contextMenuConfigurationForItem:entry
                                                   withView:cell];
}

#pragma mark - UITableViewDataSource

- (UIView*)tableView:(UITableView*)tableView
    viewForHeaderInSection:(NSInteger)section {
  UIView* view = [super tableView:tableView viewForHeaderInSection:section];
  NSInteger sectionIdentifier =
      [self.tableViewModel sectionIdentifierForSectionIndex:section];
  switch (sectionIdentifier) {
    case kEntriesStatusSectionIdentifier: {
      // Might be a different type of header.
      TableViewLinkHeaderFooterView* linkView =
          base::apple::ObjCCast<TableViewLinkHeaderFooterView>(view);
      linkView.delegate = self;
    } break;
    default:
      break;
  }
  return view;
}

- (UITableViewCell*)tableView:(UITableView*)tableView
        cellForRowAtIndexPath:(NSIndexPath*)indexPath {
  UITableViewCell* cellToReturn =
      [super tableView:tableView cellForRowAtIndexPath:indexPath];
  TableViewItem* item = [self.tableViewModel itemAtIndexPath:indexPath];
  cellToReturn.userInteractionEnabled = !(item.type == ItemTypeEntriesStatus);
  if (item.type == ItemTypeHistoryEntry) {
    HistoryEntryItem* URLItem =
        base::apple::ObjCCastStrict<HistoryEntryItem>(item);
    TableViewURLCell* URLCell =
        base::apple::ObjCCastStrict<TableViewURLCell>(cellToReturn);
    CrURL* crurl = [[CrURL alloc] initWithGURL:URLItem.URL];
    [self.imageDataSource
        faviconForPageURL:crurl
               completion:^(FaviconAttributes* attributes) {
                 // Only set favicon if the cell hasn't been reused.
                 if ([URLCell.cellUniqueIdentifier
                         isEqualToString:URLItem.uniqueIdentifier]) {
                   DCHECK(attributes);
                   [URLCell.faviconView configureWithAttributes:attributes];
                 }
               }];
  }
  return cellToReturn;
}

#pragma mark - UIScrollViewDelegate

- (void)scrollViewDidScroll:(UIScrollView*)scrollView {

  if (self.hasFinishedLoading)
    return;

  CGFloat insetHeight =
      scrollView.contentInset.top + scrollView.contentInset.bottom;
  CGFloat contentViewHeight = scrollView.bounds.size.height - insetHeight;
  CGFloat contentHeight = scrollView.contentSize.height;
  CGFloat contentOffset = scrollView.contentOffset.y;
  CGFloat buffer = contentViewHeight;
  // If the scroll view is approaching the end of loaded history, try to fetch
  // more history. Do so when the content offset is greater than the content
  // height minus the view height, minus a buffer to start the fetch early.
  if (contentOffset > (contentHeight - contentViewHeight) - buffer &&
      !self.isLoading) {
    // If at end, try to grab more history.
    NSInteger lastSection = [self.tableViewModel numberOfSections] - 1;
    NSInteger lastItemIndex =
        [self.tableViewModel numberOfItemsInSection:lastSection] - 1;
    if (lastSection == 0 || lastItemIndex < 0) {
      return;
    }

    [self fetchHistoryForQuery:_currentQuery continuation:true];
  }
}

#pragma mark - UIResponder

// To always be able to register key commands via -keyCommands, the VC must be
// able to become first responder.
- (BOOL)canBecomeFirstResponder {
  return YES;
}

- (NSArray*)keyCommands {
  return @[ UIKeyCommand.cr_close ];
}

- (void)keyCommand_close {
  base::RecordAction(base::UserMetricsAction("MobileKeyCommandClose"));
  [self.delegate dismissHistoryTableViewController:self withCompletion:nil];
}

#pragma mark - TableViewURLDragDataSource

- (URLInfo*)tableView:(UITableView*)tableView
    URLInfoAtIndexPath:(NSIndexPath*)indexPath {
  if (self.tableView.isEditing)
    return nil;

  TableViewItem* item = [self.tableViewModel itemAtIndexPath:indexPath];
  switch (item.type) {
    case ItemTypeHistoryEntry: {
      HistoryEntryItem* URLItem =
          base::apple::ObjCCastStrict<HistoryEntryItem>(item);
      return [[URLInfo alloc] initWithURL:URLItem.URL title:URLItem.text];
    }
    case ItemTypeEntriesStatus:
    case ItemTypeActivityIndicator:
    case ItemTypeEntriesStatusWithLink:
      break;
  }
  return nil;
}

#pragma mark - Private methods

- (void)dismissContextMenuCoordinator {
  [self.contextMenuCoordinator stop];
  self.contextMenuCoordinator = nil;
}

// Fetches history for search text `query`. If `query` is nil or the empty
// string, all history is fetched. If continuation is false, then the most
// recent results are fetched, otherwise the results more recent than the
// previous query will be returned.
- (void)fetchHistoryForQuery:(NSString*)query continuation:(BOOL)continuation {
  if (!self.browser)
    return;

  if (!self.historyService)
    return;

  self.loading = YES;
  // Add loading indicator if no items are shown.
  if (self.empty && !self.searchInProgress) {
    [self startLoadingIndicatorWithLoadingMessage:l10n_util::GetNSString(
                                                      IDS_HISTORY_NO_RESULTS)];
  }

  if (continuation) {
    DCHECK(_query_history_continuation);
    std::move(_query_history_continuation).Run();
  } else {
    _query_history_continuation.Reset();

    BOOL fetchAllHistory = !query || [query isEqualToString:@""];
    std::u16string queryString =
        fetchAllHistory ? std::u16string() : base::SysNSStringToUTF16(query);
    history::QueryOptions options;
    options.duplicate_policy =
        fetchAllHistory ? history::QueryOptions::REMOVE_DUPLICATES_PER_DAY
                        : history::QueryOptions::REMOVE_ALL_DUPLICATES;
    options.max_count = kMaxFetchCount;
    options.matching_algorithm =
        query_parser::MatchingAlgorithm::ALWAYS_PREFIX_SEARCH;
    self.historyService->QueryHistory(queryString, options);
  }
}

// Updates various elements after history items have been deleted from the
// TableView.
- (void)updateTableViewAfterDeletingEntries {
  // If only the header section remains, there are no history entries.
  if ([self.tableViewModel numberOfSections] == 1) {
    self.empty = YES;
    if (!self.searchInProgress) {
      [self addEmptyTableViewBackground];
    }
  }
  [self updateEntriesStatusMessage];
  [self updateToolbarButtonsWithAnimation:YES];
}

// Updates header section to provide relevant information about the currently
// displayed history entries. There should only ever be at most one item in this
// section.
- (void)updateEntriesStatusMessage {
  // Get the new status message, newStatusMessage could be nil.
  NSString* newStatusMessage = nil;
  BOOL messageWillContainLink = NO;
  if (self.empty) {
    newStatusMessage =
        self.searchController.isActive
            ? l10n_util::GetNSString(IDS_HISTORY_NO_SEARCH_RESULTS)
            : nil;
  } else if (self.shouldShowNoticeAboutOtherFormsOfBrowsingHistory &&
             !self.searchController.isActive) {
    newStatusMessage =
        l10n_util::GetNSString(IDS_IOS_HISTORY_OTHER_FORMS_OF_HISTORY);
    messageWillContainLink = YES;
  }

  // If the new message is the same as the old one, there's no need to do
  // anything else. Compare the objects since they might both be nil.
  if ([self.currentStatusMessage isEqualToString:newStatusMessage] ||
      newStatusMessage == self.currentStatusMessage)
    return;

  self.currentStatusMessage = newStatusMessage;

  TableViewHeaderFooterItem* item = nil;
  if (messageWillContainLink) {
    TableViewLinkHeaderFooterItem* header =
        [[TableViewLinkHeaderFooterItem alloc]
            initWithType:ItemTypeEntriesStatusWithLink];
    header.text = newStatusMessage;
    header.urls = @[ [[CrURL alloc] initWithGURL:GURL(kHistoryMyActivityURL)] ];
    item = header;
  } else {
    TableViewTextHeaderFooterItem* header =
        [[TableViewTextHeaderFooterItem alloc]
            initWithType:ItemTypeEntriesStatus];
    header.text = newStatusMessage;
    item = header;
  }

  // Block to hold any tableView and model updates that will be performed.
  // Change the header then reload the section to have it taken into
  // account.
  void (^tableUpdates)(void) = ^{
    [self.tableViewModel setHeader:item
          forSectionWithIdentifier:kEntriesStatusSectionIdentifier];
    NSInteger sectionIndex = [self.tableViewModel
        sectionForSectionIdentifier:kEntriesStatusSectionIdentifier];
    [self.tableView reloadSections:[NSIndexSet indexSetWithIndex:sectionIndex]
                  withRowAnimation:UITableViewRowAnimationAutomatic];
  };
  [self.tableView performBatchUpdates:tableUpdates completion:nil];
}

// Deletes all items in the tableView which indexes are included in indexArray,
// if `deleteItemsFromTableView` is YES this method needs to be run inside a
// performBatchUpdates block.
- (void)deleteItemsFromTableViewModelWithIndex:(NSArray*)indexArray
                      deleteItemsFromTableView:(BOOL)deleteItemsFromTableView {
  NSArray* sortedIndexPaths =
      [indexArray sortedArrayUsingSelector:@selector(compare:)];
  for (NSIndexPath* indexPath in [sortedIndexPaths reverseObjectEnumerator]) {
    NSInteger sectionIdentifier = [self.tableViewModel
        sectionIdentifierForSectionIndex:indexPath.section];
    NSInteger itemType = [self.tableViewModel itemTypeForIndexPath:indexPath];
    NSUInteger index =
        [self.tableViewModel indexInItemTypeForIndexPath:indexPath];
    [self.tableViewModel removeItemWithType:itemType
                  fromSectionWithIdentifier:sectionIdentifier
                                    atIndex:index];
  }
  if (deleteItemsFromTableView)
    [self.tableView deleteRowsAtIndexPaths:indexArray
                          withRowAnimation:UITableViewRowAnimationNone];

  // Remove any empty sections, except the header section.
  for (int section = self.tableView.numberOfSections - 1; section > 0;
       --section) {
    if (![self.tableViewModel numberOfItemsInSection:section]) {
      [self.entryInserter removeSection:section];
      if (deleteItemsFromTableView)
        [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:section]
                      withRowAnimation:UITableViewRowAnimationAutomatic];
    }
  }
}

// Selects all items in the tableView that are not included in entries.
- (void)filterForHistoryEntries:(NSArray*)entries {
  for (int section = 1; section < [self.tableViewModel numberOfSections];
       ++section) {
    NSInteger sectionIdentifier =
        [self.tableViewModel sectionIdentifierForSectionIndex:section];
    if ([self.tableViewModel
            hasSectionForSectionIdentifier:sectionIdentifier]) {
      NSArray* items =
          [self.tableViewModel itemsInSectionWithIdentifier:sectionIdentifier];
      for (id item in items) {
        HistoryEntryItem* historyItem =
            base::apple::ObjCCastStrict<HistoryEntryItem>(item);
        if (![entries containsObject:historyItem]) {
          NSIndexPath* indexPath =
              [self.tableViewModel indexPathForItem:historyItem];
          [self.filteredOutEntriesIndexPaths addObject:indexPath];
        }
      }
    }
  }
}

// Dismisses the search controller when there's a touch event on the scrim.
- (void)dismissSearchController:(UIControl*)sender {
  if (self.searchController.active) {
    self.searchController.active = NO;
  }
}

// Shows scrim overlay and hide toolbar.
- (void)showScrim {
  if (self.scrimView.alpha < 1.0f) {
    self.navigationController.toolbarHidden = YES;
    self.scrimView.alpha = 0.0f;
    [self.tableView addSubview:self.scrimView];
    // We attach our constraints to the superview because the tableView is
    // a scrollView and it seems that we get an empty frame when attaching to
    // it.
    AddSameConstraints(self.scrimView, self.view.superview);
    self.tableView.accessibilityElementsHidden = YES;
    self.tableView.scrollEnabled = NO;
    __weak __typeof(self) weakSelf = self;
    [UIView animateWithDuration:kTableViewNavigationScrimFadeDuration
                     animations:^{
                       weakSelf.scrimView.alpha = 1.0f;
                       [weakSelf.view layoutIfNeeded];
                     }];
  }
}

// Hides scrim and restore toolbar.
- (void)hideScrim {
  if (self.scrimView.alpha > 0.0f) {
    self.navigationController.toolbarHidden = NO;
    __weak __typeof(self) weakSelf = self;
    [UIView animateWithDuration:kTableViewNavigationScrimFadeDuration
        animations:^{
          weakSelf.scrimView.alpha = 0.0f;
        }
        completion:^(BOOL finished) {
          [weakSelf.scrimView removeFromSuperview];
          weakSelf.tableView.accessibilityElementsHidden = NO;
          weakSelf.tableView.scrollEnabled = YES;
        }];
  }
}

- (BOOL)scrimIsVisible {
  return self.scrimView.superview ? YES : NO;
}

#pragma mark Navigation Toolbar Configuration

// Animates the view configuration after flipping the current status of `[self
// setEditing]`.
- (void)animateViewsConfigurationForEditingChange {
  if (self.isEditing) {
    [self configureViewsForNonEditModeWithAnimation:YES];
  } else {
    [self configureViewsForEditModeWithAnimation:YES];
  }

  // Force update a11y actions based on edit mode.
  for (int section = 0; section < self.tableViewModel.numberOfSections;
       section++) {
    if (![self.tableViewModel numberOfItemsInSection:section]) {
      continue;
    }
    NSInteger sectionIdentifier =
        [self.tableViewModel sectionIdentifierForSectionIndex:section];
    [self reconfigureCellsForItems:
              [self.tableViewModel
                  itemsInSectionWithIdentifier:sectionIdentifier]];
  }
}

// Default TableView and NavigationBar UIToolbar configuration.
- (void)configureViewsForNonEditModeWithAnimation:(BOOL)animated {
  [self setEditing:NO animated:animated];

  [self.searchController.searchBar setUserInteractionEnabled:YES];
  self.searchController.searchBar.alpha = 1.0;
  [self updateToolbarButtonsWithAnimation:animated];
}

// Configures the TableView and NavigationBar UIToolbar for edit mode.
- (void)configureViewsForEditModeWithAnimation:(BOOL)animated {
  [self setEditing:YES animated:animated];
  [self.searchController.searchBar setUserInteractionEnabled:NO];
  self.searchController.searchBar.alpha =
      kTableViewNavigationAlphaForDisabledSearchBar;
  [self updateToolbarButtonsWithAnimation:animated];
}

// Updates the NavigationBar UIToolbar buttons.
- (void)updateToolbarButtonsWithAnimation:(BOOL)animated {
  self.deleteButton.enabled =
      [[self.tableView indexPathsForSelectedRows] count];
  self.editButton.enabled = !self.empty;
  [self setToolbarItems:[self toolbarButtons] animated:animated];
}

// Configure the navigationItem contents for the current state.
- (void)updateNavigationBar {
  if ([self isEmptyState]) {
    self.navigationItem.searchController = nil;
    self.navigationItem.largeTitleDisplayMode =
        UINavigationItemLargeTitleDisplayModeNever;
  } else {
    self.navigationItem.searchController = self.searchController;
    self.navigationItem.largeTitleDisplayMode =
        UINavigationItemLargeTitleDisplayModeAutomatic;
  }
}

#pragma mark Context Menu

// Displays a context menu on the cell pressed with gestureRecognizer.
- (void)displayContextMenuInvokedByGestureRecognizer:
    (UILongPressGestureRecognizer*)gestureRecognizer {
  if (!self.browser) {
    return;
  }
  if (gestureRecognizer.numberOfTouches != 1 || self.editing ||
      gestureRecognizer.state != UIGestureRecognizerStateBegan) {
    return;
  }
  if ([self scrimIsVisible]) {
    self.searchController.active = NO;
    return;
  }

  CGPoint touchLocation =
      [gestureRecognizer locationOfTouch:0 inView:self.tableView];
  NSIndexPath* touchedItemIndexPath =
      [self.tableView indexPathForRowAtPoint:touchLocation];
  // If there's no index path, or the index path is for the header item, do not
  // display a contextual menu.
  if (!touchedItemIndexPath ||
      [touchedItemIndexPath
          isEqual:[NSIndexPath indexPathForItem:0 inSection:0]])
    return;

  HistoryEntryItem* entry = base::apple::ObjCCastStrict<HistoryEntryItem>(
      [self.tableViewModel itemAtIndexPath:touchedItemIndexPath]);

  __weak HistoryTableViewController* weakSelf = self;
  NSString* menuTitle =
      base::SysUTF16ToNSString(url_formatter::FormatUrl(entry.URL));
  self.contextMenuCoordinator = [[ActionSheetCoordinator alloc]
      initWithBaseViewController:self.navigationController
                         browser:self.browser
                           title:menuTitle
                         message:nil
                            rect:CGRectMake(touchLocation.x, touchLocation.y,
                                            1.0, 1.0)
                            view:self.tableView];

  // Add "Open in New Tab" option.
  NSString* openInNewTabTitle =
      l10n_util::GetNSStringWithFixup(IDS_IOS_CONTENT_CONTEXT_OPENLINKNEWTAB);
  ProceduralBlock openInNewTabAction = ^{
    [weakSelf openURLInNewTab:entry.URL];
    [weakSelf dismissContextMenuCoordinator];
  };
  [self.contextMenuCoordinator addItemWithTitle:openInNewTabTitle
                                         action:openInNewTabAction
                                          style:UIAlertActionStyleDefault];

  if (base::ios::IsMultipleScenesSupported()) {
    // Add "Open In New Window" option.
    NSString* openInNewWindowTitle =
        l10n_util::GetNSString(IDS_IOS_CONTENT_CONTEXT_OPENINNEWWINDOW);
    ProceduralBlock openInNewWindowAction = ^{
      [weakSelf openURLInNewWindow:entry.URL];
    };
    [self.contextMenuCoordinator addItemWithTitle:openInNewWindowTitle
                                           action:openInNewWindowAction
                                            style:UIAlertActionStyleDefault];
  }

  // Add "Open in New Incognito Tab" option.
  NSString* openInNewIncognitoTabTitle = l10n_util::GetNSStringWithFixup(
      IDS_IOS_CONTENT_CONTEXT_OPENLINKNEWINCOGNITOTAB);
  ProceduralBlock openInNewIncognitoTabAction = ^{
    [weakSelf openURLInNewIncognitoTab:entry.URL];
    [weakSelf dismissContextMenuCoordinator];
  };
  BOOL incognitoEnabled =
      !IsIncognitoModeDisabled(self.browser->GetBrowserState()->GetPrefs());
  [self.contextMenuCoordinator addItemWithTitle:openInNewIncognitoTabTitle
                                         action:openInNewIncognitoTabAction
                                          style:UIAlertActionStyleDefault
                                        enabled:incognitoEnabled];

  // Add "Copy URL" option.
  NSString* copyURLTitle =
      l10n_util::GetNSStringWithFixup(IDS_IOS_CONTENT_CONTEXT_COPY);
  ProceduralBlock copyURLAction = ^{
    StoreURLInPasteboard(entry.URL);
    [weakSelf dismissContextMenuCoordinator];
  };
  [self.contextMenuCoordinator addItemWithTitle:copyURLTitle
                                         action:copyURLAction
                                          style:UIAlertActionStyleDefault];

  [self.contextMenuCoordinator
      addItemWithTitle:l10n_util::GetNSString(IDS_APP_CANCEL)
                action:^{
                  [weakSelf dismissContextMenuCoordinator];
                }
                 style:UIAlertActionStyleCancel];
  [self.contextMenuCoordinator start];
}

// Opens URL in a new non-incognito tab and dismisses the history view.
- (void)openURLInNewTab:(const GURL&)URL {
  base::RecordAction(
      base::UserMetricsAction("MobileHistoryPage_EntryLinkOpenNewTab"));
  UrlLoadParams params = UrlLoadParams::InNewTab(URL);
  __weak __typeof(self) weakSelf = self;
  [self.delegate
      dismissHistoryTableViewController:self
                         withCompletion:^{
                           [weakSelf
                               loadAndActivateTabFromHistoryWithParams:params
                                                             incognito:NO];
                         }];
}

// Opens URL in a new non-incognito tab in a new window and dismisses the
// history view.
- (void)openURLInNewWindow:(const GURL&)URL {
  if (!self.browser) {
    return;
  }
  id<ApplicationCommands> windowOpener = HandlerForProtocol(
      self.browser->GetCommandDispatcher(), ApplicationCommands);
  [windowOpener
      openNewWindowWithActivity:ActivityToLoadURL(WindowActivityHistoryOrigin,
                                                  URL)];
}

// Opens URL in a new incognito tab and dismisses the history view.
- (void)openURLInNewIncognitoTab:(const GURL&)URL {
  base::RecordAction(base::UserMetricsAction(
      "MobileHistoryPage_EntryLinkOpenNewIncognitoTab"));
  UrlLoadParams params = UrlLoadParams::InNewTab(URL);
  params.in_incognito = YES;
  __weak __typeof(self) weakSelf = self;
  [self.delegate
      dismissHistoryTableViewController:self
                         withCompletion:^{
                           [weakSelf
                               loadAndActivateTabFromHistoryWithParams:params
                                                             incognito:YES];
                         }];
}

#pragma mark Helper Methods

// Loads and opens a tab using `params`. If `incognito` is YES the tab will be
// opened in incognito mode.
- (void)loadAndActivateTabFromHistoryWithParams:(const UrlLoadParams&)params
                                      incognito:(BOOL)incognito {
  if (!self.browser)
    return;

  UrlLoadingBrowserAgent::FromBrowser(_browser)->Load(params);
  if (incognito) {
    [self.presentationDelegate showActiveIncognitoTabFromHistory];
  } else {
    [self.presentationDelegate showActiveRegularTabFromHistory];
  }
}

// Returns YES if the history is actually empty, and the user is neither
// searching nor editing.
- (BOOL)isEmptyState {
  return !self.loading && self.empty && !self.searchInProgress;
}

- (UIBarButtonItem*)createSpacerButton {
  return [[UIBarButtonItem alloc]
      initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace
                           target:nil
                           action:nil];
}

// Returns the toolbar buttons for the current state.
- (NSArray<UIBarButtonItem*>*)toolbarButtons {
  if ([self isEmptyState]) {
    return @[
      [self createSpacerButton], self.clearBrowsingDataButton,
      [self createSpacerButton]
    ];
  }
  if (self.isEditing) {
    return @[ self.deleteButton, [self createSpacerButton], self.cancelButton ];
  }
  return @[
    self.clearBrowsingDataButton, [self createSpacerButton], self.editButton
  ];
}

// Adds a view as background of the TableView.
- (void)addEmptyTableViewBackground {
  [self addEmptyTableViewWithImage:[UIImage imageNamed:@"history_empty"]
                             title:l10n_util::GetNSString(
                                       IDS_IOS_HISTORY_EMPTY_TITLE)
                          subtitle:l10n_util::GetNSString(
                                       IDS_IOS_HISTORY_EMPTY_MESSAGE)];
  [self updateNavigationBar];
}

// Clears the background of the TableView.
- (void)removeEmptyTableViewBackground {
  [self removeEmptyTableView];
  [self updateNavigationBar];
}

// Opens URL in the current tab and dismisses the history view.
- (void)openURL:(const GURL&)URL {
  if (!self.browser) {
    return;
  }
  web::WebState* currentWebState =
      self.browser->GetWebStateList()->GetActiveWebState();
  bool is_ntp =
      currentWebState && currentWebState->GetVisibleURL() == kChromeUINewTabURL;
  new_tab_page_uma::RecordNTPAction(
      self.browser->GetBrowserState()->IsOffTheRecord(), is_ntp,
      new_tab_page_uma::ACTION_OPENED_HISTORY_ENTRY);
  UrlLoadParams params = UrlLoadParams::InCurrentTab(URL);
  params.web_params.transition_type = ui::PAGE_TRANSITION_AUTO_BOOKMARK;
  params.load_strategy = self.loadStrategy;
  __weak __typeof(self) weakSelf = self;
  [self.delegate
      dismissHistoryTableViewController:self
                         withCompletion:^{
                           [weakSelf
                               loadAndActivateTabFromHistoryWithParams:params
                                                             incognito:NO];
                         }];
}

// Dismisses this ViewController.
- (void)dismissHistory {
  base::RecordAction(base::UserMetricsAction("MobileHistoryClose"));
  [self.delegate dismissHistoryTableViewController:self withCompletion:nil];
}

- (void)openPrivacySettings {
  base::RecordAction(
      base::UserMetricsAction("HistoryPage_InitClearBrowsingData"));

  if (IsIosQuickDeleteEnabled()) {
    if (!self.browser) {
      return;
    }
    id<QuickDeleteCommands> quickDeleteHandler = HandlerForProtocol(
        self.browser->GetCommandDispatcher(), QuickDeleteCommands);
    [quickDeleteHandler showQuickDeleteAndCanPerformTabsClosureAnimation:NO];
    return;
  }

  [self.delegate displayClearHistoryData];
}

#pragma mark - Accessibility

- (BOOL)accessibilityPerformEscape {
  [self.delegate dismissHistoryTableViewController:self withCompletion:nil];
  return YES;
}

#pragma mark Setter & Getters

- (UIBarButtonItem*)cancelButton {
  if (!_cancelButton) {
    NSString* titleString =
        l10n_util::GetNSString(IDS_HISTORY_CANCEL_EDITING_BUTTON);
    _cancelButton = [[UIBarButtonItem alloc]
        initWithTitle:titleString
                style:UIBarButtonItemStylePlain
               target:self
               action:@selector(animateViewsConfigurationForEditingChange)];
    _cancelButton.accessibilityIdentifier =
        kHistoryToolbarCancelButtonIdentifier;
  }
  return _cancelButton;
}

// TODO(crbug.com/41382611): Find a way to disable the button when a VC is
// presented.
- (UIBarButtonItem*)clearBrowsingDataButton {
  if (!_clearBrowsingDataButton) {
    NSString* titleString = l10n_util::GetNSStringWithFixup(
        IDS_HISTORY_OPEN_CLEAR_BROWSING_DATA_DIALOG);
    _clearBrowsingDataButton =
        [[UIBarButtonItem alloc] initWithTitle:titleString
                                         style:UIBarButtonItemStylePlain
                                        target:self
                                        action:@selector(openPrivacySettings)];
    _clearBrowsingDataButton.accessibilityIdentifier =
        kHistoryToolbarClearBrowsingButtonIdentifier;
    _clearBrowsingDataButton.tintColor = [UIColor colorNamed:kRedColor];
  }
  return _clearBrowsingDataButton;
}

- (UIBarButtonItem*)deleteButton {
  if (!_deleteButton) {
    NSString* titleString =
        l10n_util::GetNSString(IDS_HISTORY_DELETE_SELECTED_ENTRIES_BUTTON);
    _deleteButton = [[UIBarButtonItem alloc]
        initWithTitle:titleString
                style:UIBarButtonItemStylePlain
               target:self
               action:@selector(deleteSelectedItemsFromHistory)];
    _deleteButton.accessibilityIdentifier =
        kHistoryToolbarDeleteButtonIdentifier;
    _deleteButton.tintColor = [UIColor colorNamed:kRedColor];
  }
  return _deleteButton;
}

- (UIBarButtonItem*)editButton {
  if (!_editButton) {
    NSString* titleString =
        l10n_util::GetNSString(IDS_HISTORY_START_EDITING_BUTTON);
    _editButton = [[UIBarButtonItem alloc]
        initWithTitle:titleString
                style:UIBarButtonItemStylePlain
               target:self
               action:@selector(animateViewsConfigurationForEditingChange)];
    _editButton.accessibilityIdentifier = kHistoryToolbarEditButtonIdentifier;
    // Buttons don't conform to dynamic types. So it's safe to just use the
    // default font size.
    CGSize stringSize = [titleString sizeWithAttributes:@{
      NSFontAttributeName : [UIFont boldSystemFontOfSize:kButtonDefaultFontSize]
    }];
    // Include button padding to ensure string does not get truncated
    _editButton.width = stringSize.width + kButtonHorizontalPadding;
  }
  return _editButton;
}

- (NSMutableArray<NSIndexPath*>*)filteredOutEntriesIndexPaths {
  if (!_filteredOutEntriesIndexPaths)
    _filteredOutEntriesIndexPaths = [[NSMutableArray alloc] init];
  return _filteredOutEntriesIndexPaths;
}

@end