chromium/ios/chrome/browser/ui/recent_tabs/recent_tabs_mediator.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/ui/recent_tabs/recent_tabs_mediator.h"

#import "base/debug/dump_without_crashing.h"
#import "base/metrics/user_metrics.h"
#import "base/metrics/user_metrics_action.h"
#import "base/notreached.h"
#import "base/timer/timer.h"
#import "components/feature_engagement/public/tracker.h"
#import "components/sessions/core/tab_restore_service.h"
#import "components/signin/public/identity_manager/objc/identity_manager_observer_bridge.h"
#import "components/signin/public/identity_manager/primary_account_change_event.h"
#import "components/sync/service/sync_service.h"
#import "components/sync/service/sync_user_settings.h"
#import "components/sync_sessions/open_tabs_ui_delegate.h"
#import "components/sync_sessions/session_sync_service.h"
#import "components/sync_sessions/synced_session.h"
#import "ios/chrome/browser/default_browser/model/default_browser_interest_signals.h"
#import "ios/chrome/browser/favicon/model/favicon_loader.h"
#import "ios/chrome/browser/favicon/model/ios_chrome_favicon_loader_factory.h"
#import "ios/chrome/browser/net/model/crurl.h"
#import "ios/chrome/browser/sessions/model/ios_chrome_tab_restore_service_factory.h"
#import "ios/chrome/browser/shared/coordinator/scene/scene_state.h"
#import "ios/chrome/browser/shared/model/browser/browser.h"
#import "ios/chrome/browser/shared/model/browser/browser_provider.h"
#import "ios/chrome/browser/shared/model/browser/browser_provider_interface.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/shared/public/commands/tab_grid_commands.h"
#import "ios/chrome/browser/signin/model/identity_manager_factory.h"
#import "ios/chrome/browser/sync/model/session_sync_service_factory.h"
#import "ios/chrome/browser/ui/recent_tabs/recent_tabs_consumer.h"
#import "ios/chrome/browser/ui/recent_tabs/sessions_sync_user_state.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/grid_consumer.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/grid_toolbars_mutator.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_grid_mode_holder.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_grid_mode_observing.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/toolbars/tab_grid_toolbars_configuration.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/toolbars/tab_grid_toolbars_grid_delegate.h"
#import "ios/chrome/common/ui/favicon/favicon_constants.h"
#import "url/gurl.h"

namespace {

// Returns whether the user needs to enter a passphrase or enable sync to make
// tab sync work.
bool UserActionIsRequiredToHaveTabSyncWork(syncer::SyncService* sync_service) {
  if (!sync_service->GetDisableReasons().empty()) {
    return true;
  }

  if (!sync_service->GetUserSettings()->GetSelectedTypes().Has(
          syncer::UserSelectableType::kTabs)) {
    return true;
  }

  switch (sync_service->GetUserActionableError()) {
    // No error.
    case syncer::SyncService::UserActionableError::kNone:
      return false;

    // These errors effectively amount to disabled sync or effectively paused.
    case syncer::SyncService::UserActionableError::kSignInNeedsUpdate:
    case syncer::SyncService::UserActionableError::kNeedsPassphrase:
    case syncer::SyncService::UserActionableError::
        kNeedsTrustedVaultKeyForEverything:
      return true;

    // This error doesn't stop tab sync.
    case syncer::SyncService::UserActionableError::
        kNeedsTrustedVaultKeyForPasswords:
      return false;

    // These errors don't actually stop sync.
    case syncer::SyncService::UserActionableError::
        kTrustedVaultRecoverabilityDegradedForPasswords:
    case syncer::SyncService::UserActionableError::
        kTrustedVaultRecoverabilityDegradedForEverything:
      return false;
  }

  NOTREACHED();
}

}  // namespace

@interface RecentTabsMediator () <IdentityManagerObserverBridgeDelegate,
                                  SyncedSessionsObserver,
                                  TabGridModeObserving,
                                  TabGridToolbarsGridDelegate> {
  std::unique_ptr<synced_sessions::SyncedSessionsObserverBridge>
      _syncedSessionsObserver;
  std::unique_ptr<signin::IdentityManagerObserverBridge>
      _identityManagerObserver;
  std::unique_ptr<recent_tabs::ClosedTabsObserverBridge> _closedTabsObserver;
  SessionsSyncUserState _userState;
  // Current scene state.
  SceneState* _sceneState;
  // YES if remote grid is disabled by policy.
  BOOL _isDisabled;
  // Last active page.
  TabGridPage _lastActivePage;
  // Whether this screen is selected in the TabGrid.
  BOOL _selectedGrid;
  // Feature engagement tracker for notifying promo events.
  feature_engagement::Tracker* _engagementTracker;
  // Time to ensure that the updates to the consumer are only happening once all
  // the updates are complete.
  std::unique_ptr<base::RetainingOneShotTimer> _timer;
  // Holder for the current mode of the tab grid.
  TabGridModeHolder* _modeHolder;
}

// Return the user's current sign-in and chrome-sync state.
- (SessionsSyncUserState)userSignedInState;
// Reload the panel.
- (void)refreshSessionsView;
@property(nonatomic, assign)
    sync_sessions::SessionSyncService* sessionSyncService;
@property(nonatomic, assign) signin::IdentityManager* identityManager;
@property(nonatomic, assign) sessions::TabRestoreService* restoreService;
@property(nonatomic, assign) FaviconLoader* faviconLoader;
@property(nonatomic, assign) syncer::SyncService* syncService;
@property(nonatomic, assign) BrowserList* browserList;
@end

@implementation RecentTabsMediator

- (instancetype)
    initWithSessionSyncService:
        (sync_sessions::SessionSyncService*)sessionSyncService
               identityManager:(signin::IdentityManager*)identityManager
                restoreService:(sessions::TabRestoreService*)restoreService
                 faviconLoader:(FaviconLoader*)faviconLoader
                   syncService:(syncer::SyncService*)syncService
                   browserList:(BrowserList*)browserList
                    sceneState:(SceneState*)sceneState
              disabledByPolicy:(BOOL)disabled
             engagementTracker:(feature_engagement::Tracker*)engagementTracker
                    modeHolder:(TabGridModeHolder*)modeHolder {
  self = [super init];
  if (self) {
    _sessionSyncService = sessionSyncService;
    _identityManager = identityManager;
    _restoreService = restoreService;
    _faviconLoader = faviconLoader;
    _syncService = syncService;
    _browserList = browserList;
    _sceneState = sceneState;
    _isDisabled = disabled;
    _engagementTracker = engagementTracker;
    __weak __typeof(self) weakSelf = self;
    _timer = std::make_unique<base::RetainingOneShotTimer>(
        FROM_HERE, base::Milliseconds(100), base::BindRepeating(^{
          [weakSelf updateConsumerTabs];
        }));
    _modeHolder = modeHolder;
    [_modeHolder addObserver:self];
  }
  return self;
}

#pragma mark - Public Interface

- (void)initObservers {
  if (!_syncedSessionsObserver) {
    _syncedSessionsObserver =
        std::make_unique<synced_sessions::SyncedSessionsObserverBridge>(
            self, self.sessionSyncService);
  }
  if (!_identityManagerObserver) {
    _identityManagerObserver =
        std::make_unique<signin::IdentityManagerObserverBridge>(
            self.identityManager, self);
  }
  if (!_closedTabsObserver) {
    _closedTabsObserver =
        std::make_unique<recent_tabs::ClosedTabsObserverBridge>(self);
    if (self.restoreService) {
      self.restoreService->AddObserver(_closedTabsObserver.get());
    }
    [self.consumer setTabRestoreService:self.restoreService];
  }
}

- (void)disconnect {
  _syncedSessionsObserver.reset();
  _identityManagerObserver.reset();

  if (_closedTabsObserver) {
    if (self.restoreService) {
      self.restoreService->RemoveObserver(_closedTabsObserver.get());
    }
    _closedTabsObserver.reset();
    _sessionSyncService = nullptr;
    _identityManager = nullptr;
    _restoreService = nullptr;
    _faviconLoader = nullptr;
    _syncService = nullptr;
  }

  _sceneState = nil;

  [_modeHolder removeObserver:self];
  _modeHolder = nil;
}

- (void)configureConsumer {
  [self refreshSessionsView];
}

#pragma mark - SyncedSessionsObserver

- (void)onForeignSessionsChanged {
  [self refreshSessionsView];
}

#pragma mark - IdentityManagerObserverBridgeDelegate

- (void)onPrimaryAccountChanged:
    (const signin::PrimaryAccountChangeEvent&)event {
  switch (event.GetEventTypeFor(signin::ConsentLevel::kSignin)) {
    case signin::PrimaryAccountChangeEvent::Type::kNone:
      break;
    case signin::PrimaryAccountChangeEvent::Type::kSet:
    case signin::PrimaryAccountChangeEvent::Type::kCleared:
      // Sign-in could happen without onForeignSessionsChanged (e.g. if the user
      // signed-in without opting in to history sync; maybe also if sync ran
      // into an encryption error). The sign-in promo must still be updated in
      // that case, so handle it here.
      [self refreshSessionsView];
      break;
  }
}

#pragma mark - ClosedTabsObserving

- (void)tabRestoreServiceChanged:(sessions::TabRestoreService*)service {
  _timer->Reset();
}

- (void)tabRestoreServiceDestroyed:(sessions::TabRestoreService*)service {
  [self.consumer setTabRestoreService:nullptr];
}

#pragma mark - TableViewFaviconDataSource

- (void)faviconForPageURL:(CrURL*)URL
               completion:(void (^)(FaviconAttributes*))completion {
  self.faviconLoader->FaviconForPageUrl(
      URL.gurl, kDesiredSmallFaviconSizePt, kMinFaviconSizePt,
      /*fallback_to_google_server=*/false, ^(FaviconAttributes* attributes) {
        completion(attributes);
      });
}

#pragma mark - Private

// Returns whether this profile has any foreign sessions to sync.
- (SessionsSyncUserState)userSignedInState {
  if (!_identityManager->HasPrimaryAccount(signin::ConsentLevel::kSignin)) {
    return SessionsSyncUserState::USER_SIGNED_OUT;
  }

  if (UserActionIsRequiredToHaveTabSyncWork(_syncService)) {
    return SessionsSyncUserState::USER_SIGNED_IN_SYNC_OFF;
  }

  DCHECK(self.sessionSyncService);
  sync_sessions::OpenTabsUIDelegate* delegate =
      self.sessionSyncService->GetOpenTabsUIDelegate();
  if (!delegate) {
    return SessionsSyncUserState::USER_SIGNED_IN_SYNC_IN_PROGRESS;
  }

  std::vector<raw_ptr<const sync_sessions::SyncedSession, VectorExperimental>>
      sessions;
  return delegate->GetAllForeignSessions(&sessions)
             ? SessionsSyncUserState::USER_SIGNED_IN_SYNC_ON_WITH_SESSIONS
             : SessionsSyncUserState::USER_SIGNED_IN_SYNC_ON_NO_SESSIONS;
}

// Creates and send a tab grid toolbar configuration with button that should be
// displayed when recent grid is selected.
- (void)configureToolbarsButtons {
  if (!_selectedGrid) {
    return;
  }
  // Start to configure the delegate, so configured buttons will depend on the
  // correct delegate.
  [self.toolbarsMutator setToolbarsButtonsDelegate:self];
  if (_isDisabled) {
    [self.toolbarsMutator
        setToolbarConfiguration:
            [TabGridToolbarsConfiguration
                disabledConfigurationForPage:TabGridPageRemoteTabs]];
    return;
  }

  // Done button is enabled if there is at least one tab in the last active
  // page.
  BOOL tabsInOtherGrid = NO;
  if (_lastActivePage == TabGridPageRegularTabs) {
    Browser* regularBrowser =
        _sceneState.browserProviderInterface.mainBrowserProvider.browser;
    tabsInOtherGrid =
        regularBrowser && !regularBrowser->GetWebStateList()->empty();
  } else if (_lastActivePage == TabGridPageIncognitoTabs &&
             _sceneState.browserProviderInterface.hasIncognitoBrowserProvider) {
    Browser* incognitoBrowser =
        _sceneState.browserProviderInterface.incognitoBrowserProvider.browser;
    tabsInOtherGrid =
        incognitoBrowser && !incognitoBrowser->GetWebStateList()->empty();
  }

  TabGridToolbarsConfiguration* toolbarsConfiguration =
      [[TabGridToolbarsConfiguration alloc] initWithPage:TabGridPageRemoteTabs];
  toolbarsConfiguration.doneButton = tabsInOtherGrid;
  toolbarsConfiguration.searchButton = YES;
  [self.toolbarsMutator setToolbarConfiguration:toolbarsConfiguration];
}

// Update consumer tabs.
- (void)updateConsumerTabs {
  self.restoreService->LoadTabsFromLastSession();
  [self.consumer refreshRecentlyClosedTabs];
}

#pragma mark - RecentTabsTableViewControllerDelegate

- (void)refreshSessionsView {
  // This method is called from three places: 1) when this mediator observes a
  // change in the synced session state,  2) when the UI layer recognizes
  // that the signin process has completed, and 3) when the history & tabs sync
  // opt-in screen is dismissed.
  // The 2 latter calls are necessary because they can happen much more
  // immediately than the former call.
  [self.consumer refreshUserState:[self userSignedInState]];
}

#pragma mark - TabGridModeObserving

- (void)tabGridModeDidChange:(TabGridModeHolder*)modeHolder {
  [self configureToolbarsButtons];
}

#pragma mark - TabGridPageMutator

- (void)currentlySelectedGrid:(BOOL)selected {
  _selectedGrid = selected;

  if (selected) {
    base::RecordAction(
        base::UserMetricsAction("MobileTabGridSelectRemotePanel"));
    default_browser::NotifyRemoteTabsGridViewed(_engagementTracker);

    [self configureToolbarsButtons];
  }
}

- (void)setPageAsActive {
  NOTREACHED() << "Should not be called in remote tabs.";
}

#pragma mark - TabGridToolbarsGridDelegate

- (void)closeAllButtonTapped:(id)sender {
  NOTREACHED() << "Should not be called in remote tabs.";
}

- (void)doneButtonTapped:(id)sender {
  base::RecordAction(base::UserMetricsAction("MobileTabGridDone"));
  [self.tabGridHandler exitTabGrid];
}

- (void)newTabButtonTapped:(id)sender {
  NOTREACHED() << "Should not be called in remote tabs.";
}

- (void)selectAllButtonTapped:(id)sender {
  NOTREACHED() << "Should not be called in remote tabs.";
}

- (void)searchButtonTapped:(id)sender {
  base::RecordAction(base::UserMetricsAction("MobileTabGridSearchTabs"));
  _modeHolder.mode = TabGridMode::kSearch;
}

- (void)cancelSearchButtonTapped:(id)sender {
  base::RecordAction(base::UserMetricsAction("MobileTabGridCancelSearchTabs"));
  _modeHolder.mode = TabGridMode::kNormal;
}

- (void)closeSelectedTabs:(id)sender {
  NOTREACHED() << "Should not be called in remote tabs.";
}

- (void)shareSelectedTabs:(id)sender {
  NOTREACHED() << "Should not be called in remote tabs.";
}

- (void)selectTabsButtonTapped:(id)sender {
  NOTREACHED() << "Should not be called in remote tabs.";
}

#pragma mark - TabGridActivityObserver

- (void)updateLastActiveTabPage:(TabGridPage)page {
  _lastActivePage = page;
}

@end