chromium/ios/chrome/browser/ui/tab_switcher/tab_grid/inactive_tabs/inactive_tabs_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/tab_switcher/tab_grid/inactive_tabs/inactive_tabs_mediator.h"

#import "base/memory/raw_ptr.h"
#import "base/notreached.h"
#import "base/scoped_multi_source_observation.h"
#import "base/scoped_observation.h"
#import "components/prefs/ios/pref_observer_bridge.h"
#import "components/prefs/pref_change_registrar.h"
#import "components/prefs/pref_service.h"
#import "ios/chrome/browser/shared/model/prefs/pref_names.h"
#import "ios/chrome/browser/shared/model/url/url_util.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list_observer_bridge.h"
#import "ios/chrome/browser/snapshots/model/model_swift.h"
#import "ios/chrome/browser/snapshots/model/snapshot_id_wrapper.h"
#import "ios/chrome/browser/snapshots/model/snapshot_storage_wrapper.h"
#import "ios/chrome/browser/snapshots/model/snapshot_tab_helper.h"
#import "ios/chrome/browser/tabs/model/inactive_tabs/features.h"
#import "ios/chrome/browser/tabs/model/tabs_closer.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_collection_consumer.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/grid_item_identifier.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/inactive_tabs/inactive_tabs_info_consumer.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/toolbars/tab_grid_toolbars_configuration.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_utils.h"
#import "ios/chrome/browser/ui/tab_switcher/web_state_tab_switcher_item.h"
#import "ios/web/public/web_state.h"
#import "ios/web/public/web_state_observer_bridge.h"
#import "ui/base/device_form_factor.h"

using ScopedWebStateListObservation =
    base::ScopedObservation<WebStateList, WebStateListObserver>;
using ScopedWebStateObservation =
    base::ScopedMultiSourceObservation<web::WebState, web::WebStateObserver>;

namespace {

// Constructs an array of TabSwitcherItems from a `web_state_list` sorted by
// recency, with the most recent first.
NSArray* CreateItemsOrderedByRecency(WebStateList* web_state_list) {
  NSMutableArray* items = [[NSMutableArray alloc] init];
  std::vector<web::WebState*> web_states;

  for (int i = 0; i < web_state_list->count(); i++) {
    web_states.push_back(web_state_list->GetWebStateAt(i));
  }
  std::sort(web_states.begin(), web_states.end(),
            [](web::WebState* a, web::WebState* b) -> bool {
              return a->GetLastActiveTime() > b->GetLastActiveTime();
            });

  for (web::WebState* web_state : web_states) {
    [items addObject:[GridItemIdentifier tabIdentifier:web_state]];
  }
  return items;
}

// Observes all web states from the list with the scoped web state observer.
void AddWebStateObservations(
    ScopedWebStateObservation* scoped_web_state_observation,
    WebStateList* web_state_list) {
  for (int i = 0; i < web_state_list->count(); i++) {
    web::WebState* web_state = web_state_list->GetWebStateAt(i);
    scoped_web_state_observation->AddObservation(web_state);
  }
}

// Pushes tab items created from the web state list to the consumer. They are
// sorted by recency (see `CreateItemsOrderedByRecency`).
void PopulateConsumerItems(id<TabCollectionConsumer> consumer,
                           WebStateList* web_state_list) {
  [consumer populateItems:CreateItemsOrderedByRecency(web_state_list)
      selectedItemIdentifier:nil];
}

}  // namespace

@interface InactiveTabsMediator () <CRWWebStateObserver,
                                    PrefObserverDelegate,
                                    SnapshotStorageObserver,
                                    WebStateListObserving> {
  // The list of inactive tabs.
  raw_ptr<WebStateList> _webStateList;
  // The snapshot storage of _webStateList.
  __weak SnapshotStorageWrapper* _snapshotStorage;
  // The observers of _webStateList.
  std::unique_ptr<WebStateListObserverBridge> _webStateListObserverBridge;
  std::unique_ptr<ScopedWebStateListObservation> _scopedWebStateListObservation;
  // The observers of web states from _webStateList.
  std::unique_ptr<web::WebStateObserverBridge> _webStateObserverBridge;
  std::unique_ptr<ScopedWebStateObservation> _scopedWebStateObservation;
  // Preference service from the application context.
  raw_ptr<PrefService> _prefService;
  // Pref observer to track changes to prefs.
  std::unique_ptr<PrefObserverBridge> _prefObserverBridge;
  // Registrar for pref changes notifications.
  PrefChangeRegistrar _prefChangeRegistrar;
  // TabsClosed used to implement the "close all tabs" operation with support
  // for undoing the operation.
  std::unique_ptr<TabsCloser> _tabsCloser;
}

@end

@implementation InactiveTabsMediator

- (instancetype)initWithWebStateList:(WebStateList*)webStateList
                         prefService:(PrefService*)prefService
                     snapshotStorage:(SnapshotStorageWrapper*)snapshotStorage
                          tabsCloser:(std::unique_ptr<TabsCloser>)tabsCloser {
  CHECK(IsInactiveTabsAvailable());
  CHECK(webStateList);
  CHECK(prefService);
  CHECK(snapshotStorage);
  self = [super init];
  if (self) {
    _webStateList = webStateList;

    // Observe the web state list.
    _webStateListObserverBridge =
        std::make_unique<WebStateListObserverBridge>(self);
    _scopedWebStateListObservation =
        std::make_unique<ScopedWebStateListObservation>(
            _webStateListObserverBridge.get());
    _scopedWebStateListObservation->Observe(_webStateList);

    // Observe all web states from the list.
    _webStateObserverBridge =
        std::make_unique<web::WebStateObserverBridge>(self);
    _scopedWebStateObservation = std::make_unique<ScopedWebStateObservation>(
        _webStateObserverBridge.get());
    AddWebStateObservations(_scopedWebStateObservation.get(), _webStateList);

    // Observe the preferences for changes to Inactive Tabs settings.
    _prefService = prefService;
    _prefChangeRegistrar.Init(_prefService);
    _prefObserverBridge = std::make_unique<PrefObserverBridge>(self);
    // Register to observe any changes on pref backed values displayed by the
    // screen.
    _prefObserverBridge->ObserveChangesForPreference(
        prefs::kInactiveTabsTimeThreshold, &_prefChangeRegistrar);

    _snapshotStorage = snapshotStorage;
    [_snapshotStorage addObserver:self];

    _tabsCloser = std::move(tabsCloser);
  }
  return self;
}

- (void)dealloc {
  [_snapshotStorage removeObserver:self];
}

- (void)setConsumer:
    (id<TabCollectionConsumer, InactiveTabsInfoConsumer>)consumer {
  if (_consumer == consumer) {
    return;
  }

  _consumer = consumer;

  // Push the tabs to the consumer.
  PopulateConsumerItems(_consumer, _webStateList);
  // Push the info to the consumer.
  NSInteger daysThreshold = InactiveTabsTimeThreshold().InDays();
  [_consumer updateInactiveTabsDaysThreshold:daysThreshold];
}

- (NSInteger)numberOfItems {
  return _webStateList->count();
}

- (void)disconnect {
  _consumer = nil;
  _scopedWebStateObservation.reset();
  _webStateObserverBridge.reset();
  _scopedWebStateListObservation.reset();
  _webStateListObserverBridge.reset();
  _webStateList = nullptr;
  _prefChangeRegistrar.RemoveAll();
  _prefObserverBridge.reset();
  _prefService = nullptr;
  [_snapshotStorage removeObserver:self];
  _snapshotStorage = nil;
  _tabsCloser.reset();
}

#pragma mark - CRWWebStateObserver

- (void)webStateDidStartLoading:(web::WebState*)webState {
  [self updateConsumerItemForWebState:webState];
}

- (void)webStateDidStopLoading:(web::WebState*)webState {
  [self updateConsumerItemForWebState:webState];
}

- (void)webStateDidChangeTitle:(web::WebState*)webState {
  [self updateConsumerItemForWebState:webState];
}

- (void)updateConsumerItemForWebState:(web::WebState*)webState {
  GridItemIdentifier* item = [GridItemIdentifier tabIdentifier:webState];
  [_consumer replaceItem:item withReplacementItem:item];
}

#pragma mark - PrefObserverDelegate

- (void)onPreferenceChanged:(const std::string&)preferenceName {
  if (preferenceName == prefs::kInactiveTabsTimeThreshold) {
    NSInteger daysThreshold =
        _prefService->GetInteger(prefs::kInactiveTabsTimeThreshold);
    [_consumer updateInactiveTabsDaysThreshold:daysThreshold];
  }
}

#pragma mark - SnapshotStorageObserver

- (void)didUpdateSnapshotStorageWithSnapshotID:(SnapshotIDWrapper*)snapshotID {
  web::WebState* webState = nullptr;
  for (int i = 0; i < _webStateList->count(); i++) {
    SnapshotTabHelper* snapshotTabHelper =
        SnapshotTabHelper::FromWebState(_webStateList->GetWebStateAt(i));
    if (snapshotID.snapshot_id == snapshotTabHelper->GetSnapshotID()) {
      webState = _webStateList->GetWebStateAt(i);
      break;
    }
  }
  if (webState) {
    // It is possible to observe an updated snapshot for a WebState before
    // observing that the WebState has been added to the WebStateList. It is the
    // consumer's responsibility to ignore any updates before inserts.
    GridItemIdentifier* item = [GridItemIdentifier tabIdentifier:webState];
    [_consumer replaceItem:item withReplacementItem:item];
  }
}

#pragma mark - WebStateListObserving

- (void)willChangeWebStateList:(WebStateList*)webStateList
                        change:(const WebStateListChangeDetach&)detachChange
                        status:(const WebStateListStatus&)status {
  DCHECK_EQ(_webStateList, webStateList);
  if (_webStateList->IsBatchInProgress()) {
    // Updates are handled in the batch operation observer methods.
    return;
  }

  web::WebState* detachedWebState = detachChange.detached_web_state();
  [_consumer removeItemWithIdentifier:[GridItemIdentifier
                                          tabIdentifier:detachedWebState]
               selectedItemIdentifier:nil];

  _scopedWebStateObservation->RemoveObservation(detachedWebState);
}

- (void)didChangeWebStateList:(WebStateList*)webStateList
                       change:(const WebStateListChange&)change
                       status:(const WebStateListStatus&)status {
  DCHECK_EQ(_webStateList, webStateList);
  if (_webStateList->IsBatchInProgress()) {
    // Updates are handled in the batch operation observer methods.
    return;
  }

  switch (change.type()) {
    case WebStateListChange::Type::kStatusOnly:
      // Do nothing when the status in WebStateList is updated.
      break;
    case WebStateListChange::Type::kDetach:
      // Do nothing when a WebState is detached.
      break;
    case WebStateListChange::Type::kMove:
    case WebStateListChange::Type::kReplace:
    case WebStateListChange::Type::kGroupCreate:
    case WebStateListChange::Type::kGroupVisualDataUpdate:
    case WebStateListChange::Type::kGroupMove:
    case WebStateListChange::Type::kGroupDelete:
      NOTREACHED();
    case WebStateListChange::Type::kInsert: {
      // Insertions are only supported for iPad multiwindow support when
      // changing the user settings for Inactive Tabs (i.e. when picking a
      // longer inactivity threshold).
      DCHECK_EQ(ui::GetDeviceFormFactor(), ui::DEVICE_FORM_FACTOR_TABLET);

      const WebStateListChangeInsert& insertChange =
          change.As<WebStateListChangeInsert>();
      web::WebState* insertedWebState = insertChange.inserted_web_state();
      int nextItemIndex = insertChange.index() + 1;
      GridItemIdentifier* nextItemIdentifier;
      if (webStateList->ContainsIndex(nextItemIndex)) {
        nextItemIdentifier = [GridItemIdentifier
            tabIdentifier:webStateList->GetWebStateAt(nextItemIndex)];
      }
      [_consumer insertItem:[GridItemIdentifier tabIdentifier:insertedWebState]
                    beforeItemID:nextItemIdentifier
          selectedItemIdentifier:nil];

      _scopedWebStateObservation->AddObservation(insertedWebState);
      break;
    }
  }
}

- (void)webStateListWillBeginBatchOperation:(WebStateList*)webStateList {
  DCHECK_EQ(_webStateList, webStateList);
  _scopedWebStateObservation->RemoveAllObservations();
}

- (void)webStateListBatchOperationEnded:(WebStateList*)webStateList {
  DCHECK_EQ(_webStateList, webStateList);

  AddWebStateObservations(_scopedWebStateObservation.get(), _webStateList);
  PopulateConsumerItems(_consumer, _webStateList);
}

- (void)webStateListDestroyed:(WebStateList*)webStateList {
  DCHECK_EQ(webStateList, _webStateList);
  _scopedWebStateListObservation.reset();
  _webStateList = nullptr;
}

#pragma mark - GridCommands

- (BOOL)addNewItem {
  NOTREACHED();
}

- (BOOL)isItemWithIDSelected:(web::WebStateID)itemID {
  NOTREACHED();
}

- (void)closeItemsWithTabIDs:(const std::set<web::WebStateID>&)tabIDs
                    groupIDs:(const std::set<tab_groups::TabGroupId>&)groupIDs
                    tabCount:(int)tabCount {
  NOTREACHED();
}

- (void)closeAllItems {
  // TODO(crbug.com/40257500): Add metrics when the user closes all inactive
  // tabs.
  CloseAllWebStates(*_webStateList, WebStateList::CLOSE_USER_ACTION);
  [_snapshotStorage removeAllImages];
}

- (void)saveAndCloseAllItems {
  if (![self canCloseTabs]) {
    return;
  }

  // TODO(crbug.com/40257500): Add metrics when the user closes all inactive
  // tabs from regular tab grid.
  _tabsCloser->CloseTabs();
}

- (void)undoCloseAllItems {
  if (![self canUndoCloseAllTabs]) {
    return;
  }
  // TODO(crbug.com/40257500): Add metrics when the user restores all inactive
  // tabs from regular tab grid.
  _tabsCloser->UndoCloseTabs();
}

- (void)discardSavedClosedItems {
  if (![self canUndoCloseAllTabs]) {
    return;
  }
  _tabsCloser->ConfirmDeletion();
}

- (void)showCloseItemsConfirmationActionSheetWithItems:
            (const std::set<web::WebStateID>&)itemIDs
                                                anchor:(UIBarButtonItem*)
                                                           buttonAnchor {
  NOTREACHED();
}

- (void)shareItems:(const std::set<web::WebStateID>&)itemIDs
            anchor:(UIBarButtonItem*)buttonAnchor {
  NOTREACHED();
}

- (void)searchItemsWithText:(NSString*)searchText {
  NOTREACHED();
}

- (void)resetToAllItems {
  NOTREACHED();
}

- (void)selectItemWithID:(web::WebStateID)itemID
                    pinned:(BOOL)pinned
    isFirstActionOnTabGrid:(BOOL)isFirstActionOnTabGrid {
  NOTREACHED();
}

- (void)selectTabGroup:(const TabGroup*)tabGroup {
  NOTREACHED();
}

- (void)closeItemWithID:(web::WebStateID)itemID {
  // TODO(crbug.com/40257500): Add metrics when the user closes an inactive tab.
  int index = GetWebStateIndex(_webStateList, WebStateSearchCriteria{
                                                  .identifier = itemID,
                                              });
  if (index != WebStateList::kInvalidIndex) {
    _webStateList->CloseWebStateAt(index, WebStateList::CLOSE_USER_ACTION);
  }
}

- (void)setPinState:(BOOL)pinState forItemWithID:(web::WebStateID)itemID {
  NOTREACHED();
}

- (void)deleteTabGroup:(base::WeakPtr<const TabGroup>)group
            sourceView:(UIView*)sourceView {
  NOTREACHED_NORETURN();
}

- (void)closeTabGroup:(base::WeakPtr<const TabGroup>)group {
  NOTREACHED_NORETURN();
}

- (void)ungroupTabGroup:(base::WeakPtr<const TabGroup>)group
             sourceView:(UIView*)sourceView {
  NOTREACHED_NORETURN();
}

#pragma mark - GridToolbarsConfigurationProvider

- (TabGridToolbarsConfiguration*)toolbarsConfiguration {
  TabGridToolbarsConfiguration* toolbarsConfiguration =
      [[TabGridToolbarsConfiguration alloc]
          initWithPage:TabGridPageRegularTabs];
  toolbarsConfiguration.closeAllButton = [self canCloseTabs];
  toolbarsConfiguration.searchButton = YES;
  toolbarsConfiguration.undoButton = [self canUndoCloseAllTabs];
  return toolbarsConfiguration;
}

- (BOOL)didSavedClosedTabs {
  return [self canUndoCloseAllTabs];
}

#pragma mark - Internal

- (BOOL)canCloseTabs {
  return _tabsCloser && _tabsCloser->CanCloseTabs();
}

- (BOOL)canUndoCloseAllTabs {
  return _tabsCloser && _tabsCloser->CanUndoCloseTabs();
}

#pragma mark - GridViewControllerMutator

- (void)userTappedOnItemID:(GridItemIdentifier*)itemID {
  // No-op
}

- (void)addToSelectionItemID:(GridItemIdentifier*)itemID {
  NOTREACHED();
}

- (void)removeFromSelectionItemID:(GridItemIdentifier*)itemID {
  // No-op
}

- (void)closeItemWithIdentifier:(GridItemIdentifier*)identifier {
  CHECK(identifier.type == GridItemType::kTab);
  [self closeItemWithID:identifier.tabSwitcherItem.identifier];
}

@end