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

#import "base/apple/bundle_locations.h"
#import "base/apple/foundation_util.h"
#import "base/memory/raw_ptr.h"
#import "base/metrics/histogram_functions.h"
#import "base/metrics/user_metrics.h"
#import "base/metrics/user_metrics_action.h"
#import "base/strings/sys_string_conversions.h"
#import "base/time/time.h"
#import "components/bookmarks/browser/bookmark_model.h"
#import "components/feature_engagement/public/event_constants.h"
#import "components/feature_engagement/public/feature_constants.h"
#import "components/feature_engagement/public/tracker.h"
#import "components/search_engines/template_url_service.h"
#import "components/strings/grit/components_strings.h"
#import "components/supervised_user/core/browser/supervised_user_utils.h"
#import "ios/chrome/browser/bookmarks/model/bookmark_model_factory.h"
#import "ios/chrome/browser/bookmarks/ui_bundled/home/bookmarks_coordinator.h"
#import "ios/chrome/browser/bring_android_tabs/model/bring_android_tabs_to_ios_service.h"
#import "ios/chrome/browser/bring_android_tabs/model/bring_android_tabs_to_ios_service_factory.h"
#import "ios/chrome/browser/bring_android_tabs/ui_bundled/bring_android_tabs_prompt_coordinator.h"
#import "ios/chrome/browser/bring_android_tabs/ui_bundled/tab_list_from_android_coordinator.h"
#import "ios/chrome/browser/bubble/ui_bundled/bubble_constants.h"
#import "ios/chrome/browser/commerce/ui_bundled/price_card/price_card_mediator.h"
#import "ios/chrome/browser/favicon/model/ios_chrome_favicon_loader_factory.h"
#import "ios/chrome/browser/feature_engagement/model/tracker_factory.h"
#import "ios/chrome/browser/find_in_page/model/find_tab_helper.h"
#import "ios/chrome/browser/find_in_page/model/util.h"
#import "ios/chrome/browser/history/ui_bundled/history_coordinator.h"
#import "ios/chrome/browser/history/ui_bundled/history_coordinator_delegate.h"
#import "ios/chrome/browser/history/ui_bundled/public/history_presentation_delegate.h"
#import "ios/chrome/browser/policy/model/policy_util.h"
#import "ios/chrome/browser/reading_list/model/reading_list_browser_agent.h"
#import "ios/chrome/browser/search_engines/model/template_url_service_factory.h"
#import "ios/chrome/browser/sessions/model/ios_chrome_tab_restore_service_factory.h"
#import "ios/chrome/browser/shared/coordinator/alert/action_sheet_coordinator.h"
#import "ios/chrome/browser/shared/coordinator/default_browser_promo/non_modal_default_browser_promo_scheduler_scene_agent.h"
#import "ios/chrome/browser/shared/coordinator/layout_guide/layout_guide_util.h"
#import "ios/chrome/browser/shared/coordinator/scene/scene_state.h"
#import "ios/chrome/browser/shared/model/application_context/application_context.h"
#import "ios/chrome/browser/shared/model/browser/browser.h"
#import "ios/chrome/browser/shared/model/browser/browser_list_factory.h"
#import "ios/chrome/browser/shared/model/prefs/pref_names.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/utils/first_run_util.h"
#import "ios/chrome/browser/shared/model/web_state_list/browser_util.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/bookmarks_commands.h"
#import "ios/chrome/browser/shared/public/commands/bring_android_tabs_commands.h"
#import "ios/chrome/browser/shared/public/commands/browser_commands.h"
#import "ios/chrome/browser/shared/public/commands/command_dispatcher.h"
#import "ios/chrome/browser/shared/public/commands/open_new_tab_command.h"
#import "ios/chrome/browser/shared/public/commands/popup_menu_commands.h"
#import "ios/chrome/browser/shared/public/commands/reading_list_add_command.h"
#import "ios/chrome/browser/shared/public/commands/settings_commands.h"
#import "ios/chrome/browser/shared/public/commands/tab_grid_commands.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/shared/ui/util/layout_guide_names.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/shared/ui/util/util_swift.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/sync/model/sync_service_factory.h"
#import "ios/chrome/browser/synced_sessions/model/distant_session.h"
#import "ios/chrome/browser/synced_sessions/model/synced_sessions_util.h"
#import "ios/chrome/browser/tabs/model/inactive_tabs/features.h"
#import "ios/chrome/browser/ui/authentication/history_sync/history_sync_coordinator.h"
#import "ios/chrome/browser/ui/authentication/history_sync/history_sync_popup_coordinator.h"
#import "ios/chrome/browser/ui/main/bvc_container_view_controller.h"
#import "ios/chrome/browser/ui/menu/tab_context_menu_delegate.h"
#import "ios/chrome/browser/ui/recent_tabs/recent_tabs_mediator.h"
#import "ios/chrome/browser/ui/recent_tabs/recent_tabs_menu_helper.h"
#import "ios/chrome/browser/ui/recent_tabs/recent_tabs_presentation_delegate.h"
#import "ios/chrome/browser/ui/recent_tabs/recent_tabs_table_view_controller.h"
#import "ios/chrome/browser/ui/sharing/sharing_coordinator.h"
#import "ios/chrome/browser/ui/sharing/sharing_params.h"
#import "ios/chrome/browser/ui/snackbar/snackbar_coordinator.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/base_grid_view_controller.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/grid_commands.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/grid_constants.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/grid_container_view_controller.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/grid_coordinator_audience.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/grid_mediator_delegate.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/incognito/incognito_grid_coordinator.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/incognito/incognito_grid_mediator.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/incognito/incognito_grid_view_controller.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/regular/regular_grid_coordinator.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/regular/regular_grid_mediator.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/regular/regular_grid_view_controller.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/inactive_tabs/inactive_tabs_button_mediator.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/inactive_tabs/inactive_tabs_coordinator.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/inactive_tabs/inactive_tabs_coordinator_delegate.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/inactive_tabs/inactive_tabs_mediator.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/pinned_tabs/pinned_tabs_mediator.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/pinned_tabs/pinned_tabs_view_controller.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_context_menu/tab_context_menu_helper.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_context_menu/tab_item.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_grid_coordinator_delegate.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_grid_mediator.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_paging.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_grid_view_controller.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_groups/tab_group_positioner.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_groups/tab_groups_panel_coordinator.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_groups/tab_groups_panel_mediator.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/toolbars/tab_grid_toolbars_coordinator.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/toolbars/tab_grid_top_toolbar.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/transitions/legacy_grid_transition_animation_layout_providing.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/transitions/legacy_grid_transition_layout.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/transitions/legacy_tab_grid_transition_handler.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/transitions/tab_grid_transition_handler.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_utils.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/grit/ios_strings.h"
#import "ui/base/l10n/l10n_util.h"

namespace {

// If Find in Page uses the system Find panel and if the Find UI is marked as
// active in the current web state of `browser`, this returns true. Otherwise,
// returns false.
bool FindNavigatorShouldBePresentedInBrowser(Browser* browser) {
  if (!IsNativeFindInPageAvailable() || !browser) {
    return false;
  }

  web::WebState* currentWebState =
      browser->GetWebStateList()->GetActiveWebState();
  if (!currentWebState) {
    return false;
  }

  FindTabHelper* helper = FindTabHelper::FromWebState(currentWebState);
  if (!helper) {
    return false;
  }

  return helper->IsFindUIActive();
}

}  // namespace

@interface TabGridCoordinator () <BringAndroidTabsCommands,
                                  GridCoordinatorAudience,
                                  GridMediatorDelegate,
                                  HistoryCoordinatorDelegate,
                                  HistoryPresentationDelegate,
                                  HistorySyncPopupCoordinatorDelegate,
                                  InactiveTabsCoordinatorDelegate,
                                  LegacyGridTransitionAnimationLayoutProviding,
                                  RecentTabsPresentationDelegate,
                                  SceneStateObserver,
                                  SnackbarCoordinatorDelegate,
                                  TabContextMenuDelegate,
                                  TabGridCommands,
                                  TabGridViewControllerDelegate,
                                  TabGroupPositioner,
                                  TabPresentationDelegate> {
  // Use an explicit ivar instead of synthesizing as the setter isn't using the
  // ivar.
  raw_ptr<Browser> _incognitoBrowser;

  // Browser that contain tabs, from the regular browser, that have not been
  // open since a certain amount of time.
  raw_ptr<Browser> _inactiveBrowser;

  // The coordinator that shows the bookmarking UI after the user taps the Add
  // to Bookmarks button.
  BookmarksCoordinator* _bookmarksCoordinator;

  // The coordinator that manages the "Bring Android Tabs" prompt for Android
  // switchers.
  BringAndroidTabsPromptCoordinator* _bringAndroidTabsPromptCoordinator;

  // Coordinator for the history sync opt-in screen that should appear after
  // sign-in.
  HistorySyncPopupCoordinator* _historySyncPopupCoordinator;

  // Coordinator for the "Tab List From Android Prompt" for Android switchers.
  TabListFromAndroidCoordinator* _tabListFromAndroidCoordinator;

  // Coordinator for the toolbars.
  TabGridToolbarsCoordinator* _toolbarsCoordinator;

  // Mediator of the tab grid.
  TabGridMediator* _mediator;

  // Incognito grid coordinator.
  IncognitoGridCoordinator* _incognitoGridCoordinator;

  // Regular grid coordinator.
  RegularGridCoordinator* _regularGridCoordinator;

  // Tab Groups panel coordinator.
  TabGroupsPanelCoordinator* _tabGroupsPanelCoordinator;

  // Remote grid container.
  // TODO(crbug.com/40273478): To remove when remote coordinator handles it.
  GridContainerViewController* _remoteGridContainerViewController;

  // The frame of the Tab Grid when it is presented.
  CGRect _frameWhenEntering;

  // Holder for the current mode of the whole tab grid.
  TabGridModeHolder* _modeHolder;
}

// Browser that contain tabs from the main pane (i.e. non-incognito).
// TODO(crbug.com/40893775): Make regular ivar as incognito and inactive.
@property(nonatomic, assign, readonly) Browser* regularBrowser;
// Superclass property specialized for the class that this coordinator uses.
@property(nonatomic, weak) TabGridViewController* baseViewController;
// Commad dispatcher used while this coordinator's view controller is active.
@property(nonatomic, strong) CommandDispatcher* dispatcher;
// Container view controller for the BVC to live in; this class's view
// controller will present this.
@property(nonatomic, strong) BVCContainerViewController* bvcContainer;
// Handler for the transitions between the TabGrid and the Browser.
@property(nonatomic, strong)
    LegacyTabGridTransitionHandler* legacyTransitionHandler;
// New handler for the transitions between the TabGrid and the Browser.
@property(nonatomic, strong) TabGridTransitionHandler* transitionHandler;
// Mediator for regular Tabs.
@property(nonatomic, weak) RegularGridMediator* regularTabsMediator;
// Mediator for incognito Tabs.
@property(nonatomic, weak) IncognitoGridMediator* incognitoTabsMediator;
// Mediator for PriceCardView - this is only for regular Tabs.
@property(nonatomic, strong) PriceCardMediator* priceCardMediator;
// Mediator for remote Tabs.
@property(nonatomic, strong) RecentTabsMediator* remoteTabsMediator;
// TODO(crbug.com/346302283): Some tests depend on a
// RecentTabsTableViewController to have been loaded and kept in memory.
// Investigate and remove this dependency.
@property(nonatomic, strong)
    RecentTabsTableViewController* hackRecentTabsTableViewController;
// Mediator for the inactive tabs button.
@property(nonatomic, strong)
    InactiveTabsButtonMediator* inactiveTabsButtonMediator;
// Coordinator for history, which can be started from recent tabs.
@property(nonatomic, strong) HistoryCoordinator* historyCoordinator;
// YES if the TabViewController has never been shown yet.
@property(nonatomic, assign) BOOL firstPresentation;
@property(nonatomic, strong) SharingCoordinator* sharingCoordinator;
@property(nonatomic, strong)
    RecentTabsContextMenuHelper* recentTabsContextMenuHelper;
// The action sheet coordinator, if one is currently being shown.
@property(nonatomic, strong) ActionSheetCoordinator* actionSheetCoordinator;
// Coordinator for snackbar presentation on `_regularBrowser`.
@property(nonatomic, strong) SnackbarCoordinator* snackbarCoordinator;
// Coordinator for snackbar presentation on `_incognitoBrowser`.
@property(nonatomic, strong) SnackbarCoordinator* incognitoSnackbarCoordinator;
// Coordinator for inactive tabs.
@property(nonatomic, strong) InactiveTabsCoordinator* inactiveTabsCoordinator;
// The timestamp of the user entering the tab grid.
@property(nonatomic, assign) base::TimeTicks tabGridEnterTime;

// The page configuration used when create the tab grid view controller;
@property(nonatomic, assign) TabGridPageConfiguration pageConfiguration;

@property(weak, nonatomic, readonly) UIWindow* window;

@end

@implementation TabGridCoordinator
// Superclass property.
@synthesize baseViewController = _baseViewController;
// Ivars are not auto-synthesized when accessors are overridden.
@synthesize regularBrowser = _regularBrowser;

- (instancetype)initWithWindow:(nullable UIWindow*)window
     applicationCommandEndpoint:
         (id<ApplicationCommands>)applicationCommandEndpoint
                 regularBrowser:(Browser*)regularBrowser
                inactiveBrowser:(Browser*)inactiveBrowser
               incognitoBrowser:(Browser*)incognitoBrowser {
  if ((self = [super initWithBaseViewController:nil browser:nullptr])) {
    CHECK(inactiveBrowser->IsInactive());
    CHECK(!regularBrowser->IsInactive());
    _window = window;
    _dispatcher = [[CommandDispatcher alloc] init];
    [_dispatcher startDispatchingToTarget:applicationCommandEndpoint
                              forProtocol:@protocol(ApplicationCommands)];
    // -startDispatchingToTarget:forProtocol: doesn't pick up protocols the
    // passed protocol conforms to, so SettingsCommands is explicitly dispatched
    // to the endpoint as well.
    [_dispatcher startDispatchingToTarget:applicationCommandEndpoint
                              forProtocol:@protocol(SettingsCommands)];

    _regularBrowser = regularBrowser;
    _inactiveBrowser = inactiveBrowser;
    _incognitoBrowser = incognitoBrowser;

    if (IsIncognitoModeDisabled(
            _regularBrowser->GetBrowserState()->GetPrefs())) {
      _pageConfiguration = TabGridPageConfiguration::kIncognitoPageDisabled;
    } else if (IsIncognitoModeForced(
                   _incognitoBrowser->GetBrowserState()->GetPrefs())) {
      _pageConfiguration = TabGridPageConfiguration::kIncognitoPageOnly;
    } else {
      _pageConfiguration = TabGridPageConfiguration::kAllPagesEnabled;
    }
  }
  return self;
}

#pragma mark - Public

- (Browser*)browser {
  NOTREACHED_IN_MIGRATION();
  return nil;
}

- (Browser*)regularBrowser {
  // Ensure browser which is actually used by the regular coordinator is
  // returned, as it may have been updated.
  return _regularGridCoordinator ? _regularGridCoordinator.browser
                                 : _regularBrowser;
}

- (Browser*)incognitoBrowser {
  // Ensure browser which is actually used by the incognito coordinator is
  // returned, as it may have been updated.
  return _incognitoGridCoordinator ? _incognitoGridCoordinator.browser
                                   : _incognitoBrowser.get();
}

- (void)setIncognitoBrowser:(Browser*)incognitoBrowser {
  DCHECK(_incognitoGridCoordinator);
  [_incognitoGridCoordinator setIncognitoBrowser:incognitoBrowser];

  if (self.incognitoSnackbarCoordinator) {
    [self.incognitoSnackbarCoordinator stop];
    self.incognitoSnackbarCoordinator = nil;
  }

  if (incognitoBrowser) {
    self.incognitoSnackbarCoordinator = [[SnackbarCoordinator alloc]
        initWithBaseViewController:_baseViewController
                           browser:incognitoBrowser
                          delegate:self];
    [self.incognitoSnackbarCoordinator start];

    [incognitoBrowser->GetCommandDispatcher()
        startDispatchingToTarget:[self bookmarksCoordinator]
                     forProtocol:@protocol(BookmarksCommands)];
    [incognitoBrowser->GetCommandDispatcher()
        startDispatchingToTarget:self
                     forProtocol:@protocol(TabGridCommands)];
  }
}

- (void)stopChildCoordinatorsWithCompletion:(ProceduralBlock)completion {
  // A modal may be presented on top of the Recent Tabs or tab grid.
  [self.baseViewController dismissModals];
  [self setActiveMode:TabGridMode::kNormal];

  [_incognitoGridCoordinator stopChildCoordinators];
  [_regularGridCoordinator stopChildCoordinators];
  if (IsTabGroupSyncEnabled()) {
    [_tabGroupsPanelCoordinator stopChildCoordinators];
  }

  [self dismissPopovers];

  [self.inactiveTabsCoordinator hide];

  if (_bookmarksCoordinator) {
    [_bookmarksCoordinator dismissBookmarkModalControllerAnimated:YES];
  }
  // History may be presented on top of the tab grid.
  if (self.historyCoordinator) {
    [self closeHistoryWithCompletion:completion];
  } else if (completion) {
    completion();
  }
}

- (void)setActiveMode:(TabGridMode)mode {
  _modeHolder.mode = mode;
}

- (UIViewController*)activeViewController {
  if (self.bvcContainer) {
    return self.bvcContainer.currentBVC;
  }
  return self.baseViewController;
}

- (BOOL)isTabGridActive {
  return self.bvcContainer == nil && !self.firstPresentation;
}

- (void)showTabGridPage:(TabGridPage)page {
  CHECK_NE(page, TabGridPageRemoteTabs);
  CHECK_NE(page, TabGridPageTabGroups);
  [_mediator setActivePage:page];

  BOOL animated = !self.animationsDisabledForTesting;

  SceneState* sceneState = self.regularBrowser->GetSceneState();
  [[NonModalDefaultBrowserPromoSchedulerSceneAgent agentFromScene:sceneState]
      logTabGridEntered];

  // Store the currentActivePage at this point in code, to be potentially used
  // during execution of the dispatched block to get the transition from Browser
  // to Tab Grid. That is because in some instances the active page might change
  // before the block gets executed, for example when closing the last tab in
  // incognito (crbug.com/1136882).
  TabGridPage currentActivePage = self.baseViewController.activePage;

  // Show "Bring Android Tabs" prompt if the user is an Android switcher and has
  // open tabs from their previous Android device.
  // Note: if the coordinator is already created, the prompt should have already
  // been displayed, therefore we should not need to display it again.
  BOOL shouldDisplayBringAndroidTabsPrompt = NO;
  if (currentActivePage == TabGridPageRegularTabs &&
      !_bringAndroidTabsPromptCoordinator) {
    BringAndroidTabsToIOSService* bringAndroidTabsService =
        BringAndroidTabsToIOSServiceFactory::GetForBrowserState(
            self.regularBrowser->GetBrowserState());
    if (bringAndroidTabsService != nil) {
      bringAndroidTabsService->LoadTabs();
      shouldDisplayBringAndroidTabsPrompt =
          bringAndroidTabsService->GetNumberOfAndroidTabs() > 0;
    }
  }

  // Determine the tab group, if any, of the active web state.
  const TabGroup* tabGroup = nullptr;
  WebStateList* webStateList = nullptr;
  if (currentActivePage == TabGridPageRegularTabs) {
    webStateList = self.regularBrowser->GetWebStateList();
  } else if (currentActivePage == TabGridPageIncognitoTabs) {
    webStateList = self.incognitoBrowser->GetWebStateList();
  }
  if (webStateList) {
    int activeWebStateIndex =
        webStateList->GetIndexOfWebState(webStateList->GetActiveWebState());
    if (webStateList->ContainsIndex(activeWebStateIndex)) {
      tabGroup = webStateList->GetGroupOfWebStateAt(activeWebStateIndex);
    }
  }

  BOOL toTabGroup = tabGroup != nullptr;

  __weak __typeof(self) weakSelf = self;

  ProceduralBlock transitionCompletionBlock = ^{
    [weakSelf transitionToGridCompleteForAndroidTabsPrompt:
                  shouldDisplayBringAndroidTabsPrompt];
  };

  ProceduralBlock transitionBlock = ^{
    __typeof(self) strongSelf = weakSelf;
    if (!strongSelf) {
      return;
    }

    strongSelf.baseViewController.childViewControllerForStatusBarStyle = nil;

    if (IsNewTabGridTransitionsEnabled()) {
      [strongSelf
          performBrowserToTabGridTransitionWithAnimationEnabled:animated
                                                     completion:
                                                         transitionCompletionBlock];
    } else {
      [strongSelf
          performLegacyBrowserToTabGridTransitionWithActivePage:
              currentActivePage
                                               animationEnabled:animated
                                                     toTabGroup:toTabGroup
                                                     completion:
                                                         transitionCompletionBlock];
    }
  };

  // If a BVC is currently being presented, dismiss it.  This will trigger any
  // necessary animations.
  if (self.bvcContainer) {
    [self.baseViewController contentWillAppearAnimated:animated];
    // This is done with a dispatch to make sure that the view isn't added to
    // the view hierarchy right away, as it is not the expectations of the
    // API.
    dispatch_async(dispatch_get_main_queue(), transitionBlock);
  } else if (shouldDisplayBringAndroidTabsPrompt) {
    [self displayBringAndroidTabsPrompt];
  }

  if (tabGroup) {
    if (currentActivePage == TabGridPageRegularTabs) {
      [_regularGridCoordinator showTabGroupForTabGridOpening:tabGroup];
    } else if (currentActivePage == TabGridPageIncognitoTabs) {
      [_incognitoGridCoordinator showTabGroupForTabGridOpening:tabGroup];
    }
  }

  // Notify the Tab Groups panel (if any).
  [_tabGroupsPanelCoordinator prepareForAppearance];

  // Record when the tab switcher is presented.
  self.tabGridEnterTime = base::TimeTicks::Now();
  base::RecordAction(base::UserMetricsAction("MobileTabGridEntered"));
  [self.priceCardMediator logMetrics:TAB_SWITCHER];
}

- (void)showTabViewController:(UIViewController*)viewController
                    incognito:(BOOL)incognito
                   completion:(ProceduralBlock)completion {
  DCHECK(viewController || self.bvcContainer);

  __weak TabGridCoordinator* weakSelf = self;

  completion = ^{
    [weakSelf hideTabGroupsViews];
    if (completion) {
      completion();
    }
  };

  if (!self.tabGridEnterTime.is_null()) {
    // Record when the tab switcher is dismissed.
    base::RecordAction(base::UserMetricsAction("MobileTabGridExited"));

    // Record how long the tab switcher was presented.
    base::TimeDelta duration = base::TimeTicks::Now() - self.tabGridEnterTime;
    base::UmaHistogramLongTimes("IOS.TabSwitcher.TimeSpent", duration);
    self.tabGridEnterTime = base::TimeTicks();
  }

  SceneState* sceneState = self.regularBrowser->GetSceneState();
  sceneState.window.overrideUserInterfaceStyle =
      UIUserInterfaceStyleUnspecified;

  // If another BVC is already being presented, swap this one into the
  // container.
  if (self.bvcContainer) {
    self.bvcContainer.currentBVC = viewController;
    self.bvcContainer.incognito = incognito;
    self.baseViewController.childViewControllerForStatusBarStyle =
        viewController;
    [self.baseViewController setNeedsStatusBarAppearanceUpdate];
    if (completion) {
      completion();
    }
    return;
  }

  self.bvcContainer = [[BVCContainerViewController alloc] init];
  self.bvcContainer.currentBVC = viewController;
  self.bvcContainer.incognito = incognito;
  // Set fallback presenter, because currentBVC can be nil if the tab grid is
  // up but no tabs exist in current page.
  self.bvcContainer.fallbackPresenterViewController = self.baseViewController;

  BOOL animated = !self.animationsDisabledForTesting;
  // Never animate the first time.
  if (self.firstPresentation)
    animated = NO;

  // Extend `completion` to signal the tab switcher delegate
  // that the animated "tab switcher dismissal" (that is, presenting something
  // on top of the tab switcher) transition has completed.
  // Finally, the launch mask view should be removed.
  ProceduralBlock extendedCompletion = ^{
    [self.delegate tabGridDismissTransitionDidEnd:self];
    // In search mode, the tabgrid mode is not reset before the animation so
    // the animation can start from the correct cell. Once the animation is
    // complete, reset the tab grid mode.
    [self setActiveMode:TabGridMode::kNormal];
    Browser* browser = self.bvcContainer.incognito ? self.incognitoBrowser
                                                   : self.regularBrowser;
    if (!GetFirstResponderInWindowScene(
            self.baseViewController.view.window.windowScene) &&
        !FindNavigatorShouldBePresentedInBrowser(browser)) {
      // It is possible to already have a first responder (for example the
      // omnibox). In that case, we don't want to mark BVC as first responder.
      [self.bvcContainer.currentBVC becomeFirstResponder];
    }
    if (completion) {
      completion();
    }
    self.firstPresentation = NO;
    [weakSelf hideTabGroupsViews];
  };

  self.baseViewController.childViewControllerForStatusBarStyle =
      self.bvcContainer.currentBVC;

  [self.baseViewController contentWillDisappearAnimated:animated];

  if (IsNewTabGridTransitionsEnabled()) {
    [self performTabGridToBrowserTransitionWithAnimationEnabled:animated
                                                     completion:
                                                         extendedCompletion];
  } else {
    [self performLegacyTabGridToBrowserTransitionWithActivePage:
              self.baseViewController.activePage
                                               animationEnabled:animated
                                                     completion:
                                                         extendedCompletion];
  }
}

#pragma mark - Private

// Hides tab group views.
- (void)hideTabGroupsViews {
  [_incognitoGridCoordinator hideTabGroup];
  [_regularGridCoordinator hideTabGroup];
}

// Lazily creates the bookmarks coordinator.
- (BookmarksCoordinator*)bookmarksCoordinator {
  if (!_bookmarksCoordinator) {
    _bookmarksCoordinator =
        [[BookmarksCoordinator alloc] initWithBrowser:self.regularBrowser];
    _bookmarksCoordinator.baseViewController = self.baseViewController;
  }
  return _bookmarksCoordinator;
}

- (void)displayBringAndroidTabsPrompt {
  if (!_bringAndroidTabsPromptCoordinator) {
    _bringAndroidTabsPromptCoordinator =
        [[BringAndroidTabsPromptCoordinator alloc]
            initWithBaseViewController:self.baseViewController
                               browser:self.regularBrowser];
    _bringAndroidTabsPromptCoordinator.commandHandler = self;
  }
  [_bringAndroidTabsPromptCoordinator start];
  [self.baseViewController
      presentViewController:_bringAndroidTabsPromptCoordinator.viewController
                   animated:YES
                 completion:nil];
}

// Performs the new Browser to Tab Grid transition.
- (void)performBrowserToTabGridTransitionWithAnimationEnabled:
            (BOOL)animationEnabled
                                                   completion:
                                                       (ProceduralBlock)
                                                           completionHandler {
  TabGridTransitionDirection direction =
      TabGridTransitionDirection::kFromBrowserToTabGrid;
  TabGridTransitionType transitionType = [self
      determineTabGridTransitionTypeWithAnimationEnabled:animationEnabled];

  self.transitionHandler = [[TabGridTransitionHandler alloc]
          initWithTransitionType:transitionType
                       direction:direction
           tabGridViewController:self.baseViewController
      bvcContainerViewController:self.bvcContainer];
  [self.transitionHandler performTransitionWithCompletion:completionHandler];
}

// Performs the new Tab Grid to Browser transition.
- (void)performTabGridToBrowserTransitionWithAnimationEnabled:
            (BOOL)animationEnabled
                                                   completion:
                                                       (ProceduralBlock)
                                                           completionHandler {
  TabGridTransitionDirection direction =
      TabGridTransitionDirection::kFromTabGridToBrowser;
  TabGridTransitionType transitionType = [self
      determineTabGridTransitionTypeWithAnimationEnabled:animationEnabled];

  self.transitionHandler = [[TabGridTransitionHandler alloc]
          initWithTransitionType:transitionType
                       direction:direction
           tabGridViewController:self.baseViewController
      bvcContainerViewController:self.bvcContainer];
  [self.transitionHandler performTransitionWithCompletion:completionHandler];
}

// Performs the legacy Browser to Tab Grid transition, `toTabGroup` or not.
- (void)
    performLegacyBrowserToTabGridTransitionWithActivePage:
        (TabGridPage)activePage
                                         animationEnabled:(BOOL)animationEnabled
                                               toTabGroup:(BOOL)toTabGroup
                                               completion:
                                                   (ProceduralBlock)completion {
  if (!self.bvcContainer) {
    // It is possible that the Grid is presented twice in a row. Because the
    // detection of "the Browser is visible" is based on a null check of
    // `self.bvcContainer` which is nullified at the end of the animation, so
    // two animations could be started in a short sequence.
    return;
  }
  self.legacyTransitionHandler =
      [self createTransitionHanlderWithAnimationEnabled:animationEnabled];
  [self.legacyTransitionHandler transitionFromBrowser:self.bvcContainer
                                            toTabGrid:self.baseViewController
                                           toTabGroup:toTabGroup
                                           activePage:activePage
                                       withCompletion:completion];
}

// Performs the legacy Tab Grid to Browser transition.
- (void)
    performLegacyTabGridToBrowserTransitionWithActivePage:
        (TabGridPage)activePage
                                         animationEnabled:(BOOL)animationEnabled
                                               completion:
                                                   (ProceduralBlock)completion {
  self.legacyTransitionHandler =
      [self createTransitionHanlderWithAnimationEnabled:animationEnabled];
  [self.legacyTransitionHandler transitionFromTabGrid:self.baseViewController
                                            toBrowser:self.bvcContainer
                                           activePage:activePage
                                       withCompletion:completion];
}

// Called when the transition from Browser to Tab Grid is complete and whether
// it `shouldDisplayBringAndroidTabsPrompt`.
- (void)transitionToGridCompleteForAndroidTabsPrompt:
    (BOOL)shouldDisplayBringAndroidTabsPrompt {
  self.bvcContainer = nil;
  _frameWhenEntering = self.baseViewController.view.frame;
  [self.baseViewController contentDidAppear];

  if (shouldDisplayBringAndroidTabsPrompt) {
    [self displayBringAndroidTabsPrompt];
  }

  UIWindow* sceneWindow = self.regularBrowser->GetSceneState().window;
  sceneWindow.overrideUserInterfaceStyle = UIUserInterfaceStyleDark;
}

// Creates a transition handler with `animationEnabled` parameter.
- (LegacyTabGridTransitionHandler*)createTransitionHanlderWithAnimationEnabled:
    (BOOL)animationEnabled {
  LegacyTabGridTransitionHandler* transitionHandler =
      [[LegacyTabGridTransitionHandler alloc] initWithLayoutProvider:self];
  transitionHandler.animationDisabled = !animationEnabled;

  return transitionHandler;
}

// Determines the transion type to be used in the transition.
- (TabGridTransitionType)determineTabGridTransitionTypeWithAnimationEnabled:
    (BOOL)animationEnabled {
  if (!animationEnabled) {
    return TabGridTransitionType::kAnimationDisabled;
  } else if (UIAccessibilityIsReduceMotionEnabled()) {
    return TabGridTransitionType::kReducedMotion;
  }

  return TabGridTransitionType::kNormal;
}

// YES if there are tabs present on `page`. Should be called for regular or
// incognito.
- (BOOL)tabsPresentForPage:(TabGridPage)page {
  switch (page) {
    case TabGridPageRegularTabs:
      return !self.regularBrowser->GetWebStateList()->empty();
    case TabGridPageIncognitoTabs:
      return !self.incognitoBrowser->GetWebStateList()->empty();
    case TabGridPageRemoteTabs:
    case TabGridPageTabGroups:
      NOTREACHED_NORETURN();
  }
}

#pragma mark - ChromeCoordinator

- (void)start {
  _modeHolder = [[TabGridModeHolder alloc] init];

  [_regularBrowser->GetCommandDispatcher()
      startDispatchingToTarget:self
                   forProtocol:@protocol(TabGridCommands)];
  [_incognitoBrowser->GetCommandDispatcher()
      startDispatchingToTarget:self
                   forProtocol:@protocol(TabGridCommands)];

  ChromeBrowserState* browser_state = self.regularBrowser->GetBrowserState();
  _mediator = [[TabGridMediator alloc]
       initWithIdentityManager:IdentityManagerFactory::GetForBrowserState(
                                   browser_state)
                   prefService:browser_state->GetPrefs()
      featureEngagementTracker:feature_engagement::TrackerFactory::
                                   GetForBrowserState(browser_state)
                    modeHolder:_modeHolder];

  id<ApplicationCommands> applicationCommandsHandler =
      HandlerForProtocol(self.dispatcher, ApplicationCommands);

  TabGridViewController* baseViewController = [[TabGridViewController alloc]
      initWithPageConfiguration:_pageConfiguration];
  baseViewController.handler = applicationCommandsHandler;
  baseViewController.tabPresentationDelegate = self;
  baseViewController.layoutGuideCenter = LayoutGuideCenterForBrowser(nil);
  baseViewController.delegate = self;
  baseViewController.tabGridHandler = self;
  baseViewController.mutator = _mediator;
  _baseViewController = baseViewController;

  _mediator.consumer = _baseViewController;

  _toolbarsCoordinator = [[TabGridToolbarsCoordinator alloc]
      initWithBaseViewController:baseViewController
                         browser:_regularBrowser];
  _toolbarsCoordinator.searchDelegate = self.baseViewController;
  _toolbarsCoordinator.toolbarTabGridDelegate = self.baseViewController;
  _toolbarsCoordinator.modeHolder = _modeHolder;
  [_toolbarsCoordinator start];
  self.baseViewController.topToolbar = _toolbarsCoordinator.topToolbar;
  self.baseViewController.bottomToolbar = _toolbarsCoordinator.bottomToolbar;

  _regularGridCoordinator = [[RegularGridCoordinator alloc]
      initWithBaseViewController:self.baseViewController
                         browser:_regularBrowser
                 toolbarsMutator:_toolbarsCoordinator.toolbarsMutator
            gridMediatorDelegate:self];
  _regularGridCoordinator.disabledTabViewControllerDelegate =
      self.baseViewController;
  _regularGridCoordinator.tabGroupPositioner = self;
  _regularGridCoordinator.tabContextMenuDelegate = self;
  _regularGridCoordinator.modeHolder = _modeHolder;

  [_regularGridCoordinator start];

  baseViewController.regularTabsViewController =
      _regularGridCoordinator.gridViewController;
  baseViewController.regularDisabledGridViewController =
      _regularGridCoordinator.disabledViewController;
  baseViewController.regularGridContainerViewController =
      _regularGridCoordinator.gridContainerViewController;
  baseViewController.pinnedTabsViewController =
      _regularGridCoordinator.pinnedTabsViewController;
  baseViewController.regularGridHandler = _regularGridCoordinator.gridHandler;
  self.regularTabsMediator = _regularGridCoordinator.regularGridMediator;

  ChromeBrowserState* regularBrowserState =
      _regularBrowser ? _regularBrowser->GetBrowserState() : nullptr;
  WebStateList* regularWebStateList =
      _regularBrowser ? _regularBrowser->GetWebStateList() : nullptr;
  self.priceCardMediator =
      [[PriceCardMediator alloc] initWithWebStateList:regularWebStateList];

  // Offer to manage inactive regular tabs iff the regular tabs grid is
  // available. The regular tabs can be disabled by policy, making the grid
  // unavailable.
  if (IsInactiveTabsAvailable() &&
      _pageConfiguration != TabGridPageConfiguration::kIncognitoPageOnly) {
    CHECK(_regularGridCoordinator.gridViewController);
    self.inactiveTabsButtonMediator = [[InactiveTabsButtonMediator alloc]
        initWithConsumer:_regularGridCoordinator.gridViewController
            webStateList:_inactiveBrowser->GetWebStateList()
             prefService:GetApplicationContext()->GetLocalState()];
  }

  baseViewController.priceCardDataSource = self.priceCardMediator;


  _incognitoGridCoordinator = [[IncognitoGridCoordinator alloc]
      initWithBaseViewController:self.baseViewController
                         browser:_incognitoBrowser
                 toolbarsMutator:_toolbarsCoordinator.toolbarsMutator
            gridMediatorDelegate:self];
  _incognitoGridCoordinator.disabledTabViewControllerDelegate =
      self.baseViewController;
  _incognitoGridCoordinator.tabGroupPositioner = self;
  _incognitoGridCoordinator.audience = self;
  _incognitoGridCoordinator.tabContextMenuDelegate = self;
  _incognitoGridCoordinator.modeHolder = _modeHolder;

  [_incognitoGridCoordinator start];

  self.incognitoTabsMediator = _incognitoGridCoordinator.incognitoGridMediator;
  [self.incognitoTabsMediator
      initializeSupervisedUserCapabilitiesObserver:
          IdentityManagerFactory::GetForBrowserState(browser_state)];

  baseViewController.incognitoGridHandler =
      _incognitoGridCoordinator.gridHandler;

  baseViewController.incognitoTabsViewController =
      _incognitoGridCoordinator.gridViewController;
  baseViewController.incognitoDisabledGridViewController =
      _incognitoGridCoordinator.disabledViewController;
  baseViewController.incognitoGridContainerViewController =
      _incognitoGridCoordinator.gridContainerViewController;

  self.recentTabsContextMenuHelper =
      [[RecentTabsContextMenuHelper alloc] initWithBrowser:self.regularBrowser
                            recentTabsPresentationDelegate:self
                                    tabContextMenuDelegate:self];
  self.baseViewController.remoteTabsViewController.menuProvider =
      self.recentTabsContextMenuHelper;

  if (IsTabGroupSyncEnabled()) {
    _tabGroupsPanelCoordinator = [[TabGroupsPanelCoordinator alloc]
            initWithBaseViewController:_baseViewController
                        regularBrowser:_regularBrowser
                       toolbarsMutator:_toolbarsCoordinator.toolbarsMutator
        disabledViewControllerDelegate:_baseViewController];

    [_tabGroupsPanelCoordinator start];

    baseViewController.tabGroupsPanelViewController =
        _tabGroupsPanelCoordinator.gridViewController;
    baseViewController.tabGroupsDisabledGridViewController =
        _tabGroupsPanelCoordinator.disabledViewController;
    baseViewController.tabGroupsGridContainerViewController =
        _tabGroupsPanelCoordinator.gridContainerViewController;

    // TODO(crbug.com/346302283): Some tests depend on a
    // RecentTabsTableViewController to have been loaded and kept in memory.
    // Investigate and remove this dependency.
    RecentTabsTableViewController* remoteTabsViewController =
        [[RecentTabsTableViewController alloc] init];
    remoteTabsViewController.browser = self.regularBrowser;
    [remoteTabsViewController loadModel];
    [remoteTabsViewController.tableView reloadData];
    _hackRecentTabsTableViewController = remoteTabsViewController;
  } else {
    // TODO(crbug.com/41390276) : Remove RecentTabsTableViewController
    // dependency on ChromeBrowserState so that we don't need to expose the view
    // controller.
    baseViewController.remoteTabsViewController.browser = self.regularBrowser;
    sync_sessions::SessionSyncService* syncService =
        SessionSyncServiceFactory::GetForBrowserState(regularBrowserState);
    signin::IdentityManager* identityManager =
        IdentityManagerFactory::GetForBrowserState(regularBrowserState);
    sessions::TabRestoreService* restoreService =
        IOSChromeTabRestoreServiceFactory::GetForBrowserState(
            regularBrowserState);
    FaviconLoader* faviconLoader =
        IOSChromeFaviconLoaderFactory::GetForBrowserState(regularBrowserState);
    syncer::SyncService* service =
        SyncServiceFactory::GetForBrowserState(regularBrowserState);
    BrowserList* browserList =
        BrowserListFactory::GetForBrowserState(regularBrowserState);
    SceneState* currentSceneState = self.regularBrowser->GetSceneState();
    // TODO(crbug.com/40273478): Rename in recentTabsMediator.
    self.remoteTabsMediator = [[RecentTabsMediator alloc]
        initWithSessionSyncService:syncService
                   identityManager:identityManager
                    restoreService:restoreService
                     faviconLoader:faviconLoader
                       syncService:service
                       browserList:browserList
                        sceneState:currentSceneState
                  disabledByPolicy:_pageConfiguration ==
                                   TabGridPageConfiguration::kIncognitoPageOnly
                 engagementTracker:feature_engagement::TrackerFactory::
                                       GetForBrowserState(regularBrowserState)
                        modeHolder:_modeHolder];
    self.remoteTabsMediator.consumer = baseViewController.remoteTabsConsumer;
    self.remoteTabsMediator.tabGridHandler = self;
    baseViewController.remoteTabsViewController.imageDataSource =
        self.remoteTabsMediator;
    baseViewController.remoteTabsViewController.delegate =
        self.remoteTabsMediator;
    baseViewController.remoteTabsViewController.applicationHandler =
        applicationCommandsHandler;
    baseViewController.remoteTabsViewController.loadStrategy =
        UrlLoadStrategy::ALWAYS_NEW_FOREGROUND_TAB;
    baseViewController.remoteTabsViewController.presentationDelegate = self;
    baseViewController.activityObserver = self.remoteTabsMediator;

    _remoteGridContainerViewController =
        [[GridContainerViewController alloc] init];
    self.baseViewController.remoteGridContainerViewController =
        _remoteGridContainerViewController;
  }

  if (IsInactiveTabsAvailable()) {
    self.inactiveTabsCoordinator = [[InactiveTabsCoordinator alloc]
        initWithBaseViewController:self.baseViewController
                           browser:_inactiveBrowser
                          delegate:self];
    self.inactiveTabsCoordinator.tabContextMenuDelegate = self;

    [self.inactiveTabsCoordinator start];

    self.regularTabsMediator.containedGridToolbarsProvider =
        self.inactiveTabsCoordinator.toolbarsConfigurationProvider;
    self.regularTabsMediator.inactiveTabsGridCommands =
        self.inactiveTabsCoordinator.gridCommandsHandler;
  }

  self.firstPresentation = YES;

  // TODO(crbug.com/41393201) : Currently, consumer calls from the mediator
  // prematurely loads the view in `RecentTabsTableViewController`. Fix this so
  // that the view is loaded only by an explicit placement in the view
  // hierarchy. As a workaround, the view controller hierarchy is loaded here
  // before `RecentTabsMediator` updates are started.
  self.window.rootViewController = self.baseViewController;
  if (regularBrowserState) {
    [self.remoteTabsMediator initObservers];
    [self.remoteTabsMediator refreshSessionsView];
  }

  _mediator.regularPageMutator = _regularGridCoordinator.regularGridMediator;
  _mediator.incognitoPageMutator = self.incognitoTabsMediator;
  if (IsTabGroupSyncEnabled()) {
    _mediator.tabGroupsPageMutator = _tabGroupsPanelCoordinator.mediator;
  } else {
    _mediator.remotePageMutator = self.remoteTabsMediator;
  }
  _mediator.toolbarsMutator = _toolbarsCoordinator.toolbarsMutator;

  self.remoteTabsMediator.toolbarsMutator =
      _toolbarsCoordinator.toolbarsMutator;

  self.incognitoTabsMediator.tabPresentationDelegate = self;
  self.regularTabsMediator.tabPresentationDelegate = self;

  self.incognitoTabsMediator.gridConsumer = self.baseViewController;
  self.regularTabsMediator.gridConsumer = self.baseViewController;
  self.remoteTabsMediator.gridConsumer = self.baseViewController;

  self.incognitoTabsMediator.tabGridIdleStatusHandler = self.baseViewController;
  self.regularTabsMediator.tabGridIdleStatusHandler = self.baseViewController;

  self.snackbarCoordinator =
      [[SnackbarCoordinator alloc] initWithBaseViewController:baseViewController
                                                      browser:_regularBrowser
                                                     delegate:self];
  [self.snackbarCoordinator start];
  self.incognitoSnackbarCoordinator =
      [[SnackbarCoordinator alloc] initWithBaseViewController:baseViewController
                                                      browser:_incognitoBrowser
                                                     delegate:self];
  [self.incognitoSnackbarCoordinator start];

  [_regularBrowser->GetCommandDispatcher()
      startDispatchingToTarget:[self bookmarksCoordinator]
                   forProtocol:@protocol(BookmarksCommands)];
  [_incognitoBrowser->GetCommandDispatcher()
      startDispatchingToTarget:[self bookmarksCoordinator]
                   forProtocol:@protocol(BookmarksCommands)];

  SceneState* sceneState = self.regularBrowser->GetSceneState();
  [sceneState addObserver:self];

  // Once the mediators are set up, stop keeping pointers to the browsers used
  // to initialize them.
  _regularBrowser = nil;
  _incognitoBrowser = nil;
}

- (void)stop {
  SceneState* sceneState = self.regularBrowser->GetSceneState();
  [sceneState removeObserver:self];

  // The TabGridViewController may still message its application commands
  // handler after this coordinator has stopped; make this action a no-op by
  // setting the handler to nil.
  self.baseViewController.handler = nil;
  self.recentTabsContextMenuHelper = nil;
  [self.sharingCoordinator stop];
  self.sharingCoordinator = nil;
  [self.incognitoBrowser->GetCommandDispatcher() stopDispatchingToTarget:self];
  [self.regularBrowser->GetCommandDispatcher() stopDispatchingToTarget:self];
  [self.dispatcher stopDispatchingForProtocol:@protocol(ApplicationCommands)];
  [self.dispatcher stopDispatchingForProtocol:@protocol(SettingsCommands)];

  [_toolbarsCoordinator stop];
  _toolbarsCoordinator = nil;

  [_incognitoGridCoordinator stop];
  _incognitoGridCoordinator = nil;

  [_regularGridCoordinator stop];
  _regularGridCoordinator = nil;

  [_tabGroupsPanelCoordinator stop];
  _tabGroupsPanelCoordinator = nil;

  if (IsTabGroupSyncEnabled()) {
    // This disconnects the Recent Tabs' SigninPromoViewMediator.
    [_hackRecentTabsTableViewController dismissModals];
  }

  // TODO(crbug.com/41390276) : RecentTabsTableViewController behaves like a
  // coordinator and that should be factored out.
  [self.baseViewController.remoteTabsViewController dismissModals];
  self.baseViewController.remoteTabsViewController.browser = nil;
  [self.remoteTabsMediator disconnect];
  self.remoteTabsMediator = nil;
  [self dismissActionSheetCoordinator];

  [self.snackbarCoordinator stop];
  self.snackbarCoordinator = nil;
  [self.incognitoSnackbarCoordinator stop];
  self.incognitoSnackbarCoordinator = nil;

  [_bringAndroidTabsPromptCoordinator stop];
  _bringAndroidTabsPromptCoordinator = nil;
  [_tabListFromAndroidCoordinator stop];
  _tabListFromAndroidCoordinator = nil;

  [self.inactiveTabsButtonMediator disconnect];
  self.inactiveTabsButtonMediator = nil;
  [self.inactiveTabsCoordinator stop];
  self.inactiveTabsCoordinator = nil;

  [self.historyCoordinator stop];
  self.historyCoordinator = nil;

  _historySyncPopupCoordinator.delegate = nil;
  [_historySyncPopupCoordinator stop];
  _historySyncPopupCoordinator = nil;

  [_bookmarksCoordinator stop];
  _bookmarksCoordinator = nil;

  [_mediator disconnect];
}

#pragma mark - TabPresentationDelegate

- (void)showActiveTabInPage:(TabGridPage)page focusOmnibox:(BOOL)focusOmnibox {
  DCHECK(self.regularBrowser && self.incognitoBrowser);
  [_mediator setActivePage:page];

  Browser* activeBrowser = nullptr;
  switch (page) {
    case TabGridPageIncognitoTabs:
      DCHECK_GT(self.incognitoBrowser->GetWebStateList()->count(), 0);
      activeBrowser = self.incognitoBrowser;
      break;
    case TabGridPageRegularTabs:
      DCHECK_GT(self.regularBrowser->GetWebStateList()->count(), 0);
      activeBrowser = self.regularBrowser;
      break;
    case TabGridPageRemoteTabs:
      DUMP_WILL_BE_NOTREACHED()
          << "It is invalid to have an active tab in Recent Tabs.";
      // This appears to come up in release -- see crbug.com/1069243.
      // Defensively early return instead of continuing.
      return;
    case TabGridPageTabGroups:
      DUMP_WILL_BE_NOTREACHED()
          << "It is invalid to have an active tab in Tab Groups.";
      // This may come up in release -- see crbug.com/1069243.
      // Defensively early return instead of continuing.
      return;
  }
  // Trigger the transition through the delegate. This will in turn call back
  // into this coordinator.
  [self.delegate tabGrid:self
      shouldActivateBrowser:activeBrowser
               focusOmnibox:focusOmnibox];
}

#pragma mark - GridMediatorDelegate

- (void)baseGridMediator:(BaseGridMediator*)baseGridMediator
    showCloseConfirmationWithTabIDs:(const std::set<web::WebStateID>&)tabIDs
                           groupIDs:
                               (const std::set<tab_groups::TabGroupId>&)groupIDs
                           tabCount:(int)tabCount
                             anchor:(UIBarButtonItem*)buttonAnchor {
  if (baseGridMediator == self.regularTabsMediator) {
    base::RecordAction(base::UserMetricsAction(
        "MobileTabGridSelectionCloseRegularTabsConfirmationPresented"));

    self.actionSheetCoordinator = [[ActionSheetCoordinator alloc]
        initWithBaseViewController:self.baseViewController
                           browser:self.regularBrowser
                             title:nil
                           message:nil
                     barButtonItem:buttonAnchor];

    // IOS 17 Bug: The alert arrow direction presentation is broken.
    // Workaround: Specifically set the popover arrow direction. (crbug/1490535)
    if (@available(iOS 17, *)) {
      self.actionSheetCoordinator.popoverArrowDirection =
          UIPopoverArrowDirectionDown | UIPopoverArrowDirectionUp;
    }
  } else {
    base::RecordAction(base::UserMetricsAction(
        "MobileTabGridSelectionCloseIncognitoTabsConfirmationPresented"));

    self.actionSheetCoordinator = [[ActionSheetCoordinator alloc]
        initWithBaseViewController:self.baseViewController
                           browser:self.incognitoBrowser
                             title:nil
                           message:nil
                     barButtonItem:buttonAnchor];
  }

  self.actionSheetCoordinator.alertStyle = UIAlertControllerStyleActionSheet;

  __weak BaseGridMediator* weakBaseGridMediator = baseGridMediator;
  __weak TabGridCoordinator* weakSelf = self;
  // Copy the set of tab and group identifiers, so that the following block can
  // use it.
  std::set<web::WebStateID> tabIDsCopy = tabIDs;
  std::set<tab_groups::TabGroupId> groupIDsCopy = groupIDs;

  [self.actionSheetCoordinator
      addItemWithTitle:base::SysUTF16ToNSString(
                           l10n_util::GetPluralStringFUTF16(
                               IDS_IOS_TAB_GRID_CLOSE_ALL_TABS_CONFIRMATION,
                               tabCount))
                action:^{
                  base::RecordAction(base::UserMetricsAction(
                      "MobileTabGridSelectionCloseTabsConfirmed"));
                  [weakBaseGridMediator closeItemsWithTabIDs:tabIDsCopy
                                                    groupIDs:groupIDsCopy
                                                    tabCount:tabCount];
                  [weakSelf dismissActionSheetCoordinator];
                }
                 style:UIAlertActionStyleDestructive];
  [self.actionSheetCoordinator
      addItemWithTitle:l10n_util::GetNSString(IDS_CANCEL)
                action:^{
                  base::RecordAction(base::UserMetricsAction(
                      "MobileTabGridSelectionCloseTabsCanceled"));
                  [weakSelf dismissActionSheetCoordinator];
                }
                 style:UIAlertActionStyleCancel];
  [self.actionSheetCoordinator start];
}

- (void)baseGridMediator:(BaseGridMediator*)baseGridMediator
               shareURLs:(NSArray<URLWithTitle*>*)URLs
                  anchor:(UIBarButtonItem*)buttonAnchor {
  SharingParams* params = [[SharingParams alloc]
      initWithURLs:URLs
          scenario:SharingScenario::TabGridSelectionMode];

  self.sharingCoordinator = [[SharingCoordinator alloc]
      initWithBaseViewController:self.baseViewController
                         browser:self.regularBrowser
                          params:params
                          anchor:buttonAnchor];
  [self.sharingCoordinator start];
}

- (void)dismissActionSheetCoordinator {
  [self.actionSheetCoordinator stop];
  self.actionSheetCoordinator = nil;
}

- (void)dismissPopovers {
  [self dismissActionSheetCoordinator];
  [self.sharingCoordinator stop];
  self.sharingCoordinator = nil;
}

#pragma mark - GridCoordinatorAudience

- (void)incognitoGridDidChange {
  self.baseViewController.incognitoTabsViewController =
      _incognitoGridCoordinator.gridViewController;
}

#pragma mark - TabGridViewControllerDelegate

- (void)openLinkWithURL:(const GURL&)URL {
  id<ApplicationCommands> handler =
      HandlerForProtocol(self.dispatcher, ApplicationCommands);
  [handler openURLInNewTab:[OpenNewTabCommand commandWithURLFromChrome:URL]];
}

- (void)showInactiveTabs {
  CHECK(IsInactiveTabsEnabled());
  [self.inactiveTabsCoordinator show];
}

- (BOOL)tabGridIsUserEligibleForSwipeToIncognitoIPH {
  return _pageConfiguration == TabGridPageConfiguration::kAllPagesEnabled &&
         IsFirstRunRecent(base::Days(60)) &&
         feature_engagement::TrackerFactory::GetForBrowserState(
             self.regularBrowser->GetBrowserState())
             ->WouldTriggerHelpUI(
                 feature_engagement::kIPHiOSTabGridSwipeRightForIncognito);
}

- (BOOL)tabGridShouldPresentSwipeToIncognitoIPH {
  return feature_engagement::TrackerFactory::GetForBrowserState(
             self.regularBrowser->GetBrowserState())
      ->ShouldTriggerHelpUI(
          feature_engagement::kIPHiOSTabGridSwipeRightForIncognito);
}

- (void)tabGridDidDismissSwipeToIncognitoIPHWithReason:
    (IPHDismissalReasonType)reason {
  feature_engagement::Tracker* tracker =
      feature_engagement::TrackerFactory::GetForBrowserState(
          self.regularBrowser->GetBrowserState());
  if (tracker) {
    tracker->DismissedWithSnooze(
        feature_engagement::kIPHiOSTabGridSwipeRightForIncognito,
        feature_engagement::Tracker::SnoozeAction::DISMISSED);
    if (reason == IPHDismissalReasonType::kTappedClose) {
      tracker->NotifyEvent(
          feature_engagement::events::
              kIOSSwipeRightForIncognitoIPHDismissButtonTapped);
    }
  }
}

#pragma mark - InactiveTabsCoordinatorDelegate

- (void)inactiveTabsCoordinator:
            (InactiveTabsCoordinator*)inactiveTabsCoordinator
            didSelectItemWithID:(web::WebStateID)itemID {
  WebStateList* regularWebStateList = self.regularBrowser->GetWebStateList();
  int toInsertIndex = regularWebStateList->count();

  MoveTabToBrowser(itemID, self.regularBrowser, toInsertIndex);

  // TODO(crbug.com/40896001): Adapt the animation so the grid animation is
  // coming from the inactive panel.
  regularWebStateList->ActivateWebStateAt(toInsertIndex);
  [self.delegate tabGrid:self
      shouldActivateBrowser:self.regularBrowser
               focusOmnibox:NO];
}

- (void)inactiveTabsCoordinatorDidFinish:
    (InactiveTabsCoordinator*)inactiveTabsCoordinator {
  CHECK(IsInactiveTabsAvailable());
  [self.inactiveTabsCoordinator hide];
}

#pragma mark - RecentTabsPresentationDelegate

- (void)showHistoryFromRecentTabsFilteredBySearchTerms:(NSString*)searchTerms {
  [self showHistoryForText:searchTerms];
}

- (void)showActiveRegularTabFromRecentTabs {
  [self.delegate tabGrid:self
      shouldActivateBrowser:self.regularBrowser
               focusOmnibox:NO];
}

- (void)showRegularTabGridFromRecentTabs {
  [self.baseViewController setCurrentPageAndPageControl:TabGridPageRegularTabs
                                               animated:YES];
}

- (void)showHistorySyncOptInAfterDedicatedSignIn:(BOOL)dedicatedSignInDone {
  // Stop the previous coordinator since the user can tap on the promo button
  // to open a new History Sync Page while the dismiss animation of the previous
  // one is in progress.
  _historySyncPopupCoordinator.delegate = nil;
  [_historySyncPopupCoordinator stop];
  _historySyncPopupCoordinator = nil;
  // Show the History Sync Opt-In screen. The coordinator will dismiss itself
  // if there is no signed-in account (eg. if sign-in unsuccessful) or if sync
  // is disabled by policies.
  _historySyncPopupCoordinator = [[HistorySyncPopupCoordinator alloc]
      initWithBaseViewController:_baseViewController
                         browser:self.regularBrowser
                   showUserEmail:!dedicatedSignInDone
               signOutIfDeclined:dedicatedSignInDone
                      isOptional:NO
                     accessPoint:signin_metrics::AccessPoint::
                                     ACCESS_POINT_RECENT_TABS];
  _historySyncPopupCoordinator.delegate = self;
  [_historySyncPopupCoordinator start];
}

#pragma mark - HistoryPresentationDelegate

- (void)showActiveRegularTabFromHistory {
  [self.delegate tabGrid:self
      shouldActivateBrowser:self.regularBrowser
               focusOmnibox:NO];
}

- (void)showActiveIncognitoTabFromHistory {
  [self.delegate tabGrid:self
      shouldActivateBrowser:self.incognitoBrowser
               focusOmnibox:NO];
}

- (void)openAllTabsFromSession:(const synced_sessions::DistantSession*)session {
  base::RecordAction(base::UserMetricsAction(
      "MobileRecentTabManagerOpenAllTabsFromOtherDevice"));
  base::UmaHistogramCounts100(
      "Mobile.RecentTabsManager.TotalTabsFromOtherDevicesOpenAll",
      session->tabs.size());

  BOOL inIncognito = self.regularBrowser->GetBrowserState()->IsOffTheRecord();
  OpenDistantSessionInBackground(
      session, inIncognito, GetDefaultNumberOfTabsToLoadSimultaneously(),
      UrlLoadingBrowserAgent::FromBrowser(self.regularBrowser),
      self.baseViewController.remoteTabsViewController.loadStrategy);

  [self showActiveRegularTabFromRecentTabs];
}

#pragma mark - HistoryCoordinatorDelegate

- (void)closeHistoryWithCompletion:(ProceduralBlock)completion {
  __weak __typeof(self) weakSelf = self;
  [self.historyCoordinator dismissWithCompletion:^{
    if (completion) {
      completion();
    }
    [weakSelf.historyCoordinator stop];
    weakSelf.historyCoordinator = nil;
  }];
}

#pragma mark - TabContextMenuDelegate

- (void)shareURL:(const GURL&)URL
           title:(NSString*)title
        scenario:(SharingScenario)scenario
        fromView:(UIView*)view {
  SharingParams* params = [[SharingParams alloc] initWithURL:URL
                                                       title:title
                                                    scenario:scenario];
  self.sharingCoordinator = [[SharingCoordinator alloc]
      initWithBaseViewController:self.baseViewController
                         browser:self.regularBrowser
                          params:params
                      originView:view];
  [self.sharingCoordinator start];
}

- (void)addToReadingListURL:(const GURL&)URL title:(NSString*)title {
  ReadingListAddCommand* command =
      [[ReadingListAddCommand alloc] initWithURL:URL title:title];
  ReadingListBrowserAgent* readingListBrowserAgent =
      ReadingListBrowserAgent::FromBrowser(self.regularBrowser);
  readingListBrowserAgent->AddURLsToReadingList(command.URLs);
}

- (void)bookmarkURL:(const GURL&)URL title:(NSString*)title {
  bookmarks::BookmarkModel* bookmarkModel =
      ios::BookmarkModelFactory::GetForBrowserState(
          self.regularBrowser->GetBrowserState());
  if (bookmarkModel->IsBookmarked(URL)) {
    [self editBookmarkWithURL:URL];
  } else {
    base::RecordAction(base::UserMetricsAction(
        "MobileTabGridOpenedBookmarkEditorForNewBookmark"));
    [self.bookmarksCoordinator createBookmarkURL:URL title:title];
  }
}

- (void)editBookmarkWithURL:(const GURL&)URL {
  base::RecordAction(base::UserMetricsAction(
      "MobileTabGridOpenedBookmarkEditorForExistingBookmark"));
  [self.bookmarksCoordinator presentBookmarkEditorForURL:URL];
}

- (void)pinTabWithIdentifier:(web::WebStateID)identifier {
  [self.regularTabsMediator setPinState:YES forItemWithID:identifier];
}

- (void)unpinTabWithIdentifier:(web::WebStateID)identifier {
  [self.regularTabsMediator setPinState:NO forItemWithID:identifier];
}

- (void)createNewTabGroupWithIdentifier:(web::WebStateID)identifier
                              incognito:(BOOL)incognito {
  CHECK(IsTabGroupInGridEnabled())
      << "You should not be able to create a new tab group outside the Tab "
         "Groups experiment.";
  std::set<web::WebStateID> webStateIDSet = {identifier};
  if (incognito) {
    [_incognitoGridCoordinator showTabGroupCreationForTabs:webStateIDSet];
  } else {
    [_regularGridCoordinator showTabGroupCreationForTabs:webStateIDSet];
  }
}

- (void)editTabGroup:(base::WeakPtr<const TabGroup>)group
           incognito:(BOOL)incognito {
  if (!group) {
    return;
  }
  CHECK(IsTabGroupInGridEnabled())
      << "You should not be able to edit a tab group outside the Tab Groups "
         "experiment.";

  BaseGridCoordinator* coordinator;
  if (incognito) {
    coordinator = _incognitoGridCoordinator;
  } else {
    coordinator = _regularGridCoordinator;
  }
  [coordinator showTabGroupEditionForGroup:group.get()];
}

- (void)closeTabWithIdentifier:(web::WebStateID)identifier
                     incognito:(BOOL)incognito {
  if (incognito) {
    [self.incognitoTabsMediator closeItemWithID:identifier];
    return;
  }

  [self.regularTabsMediator closeItemWithID:identifier];
}

- (void)deleteTabGroup:(base::WeakPtr<const TabGroup>)group
             incognito:(BOOL)incognito
            sourceView:(UIView*)sourceView {
  CHECK(IsTabGroupInGridEnabled())
      << "You should not be able to delete a tab group outside the Tab Groups "
         "experiment.";
  if (incognito) {
    CHECK(!IsTabGroupSyncEnabled());
    [self.incognitoTabsMediator deleteTabGroup:group sourceView:sourceView];
    return;
  }

  [self.regularTabsMediator deleteTabGroup:group sourceView:sourceView];
}

- (void)closeTabGroup:(base::WeakPtr<const TabGroup>)group
            incognito:(BOOL)incognito {
  CHECK(IsTabGroupInGridEnabled())
      << "You should not be able to close a tab group outside the Tab Groups "
         "experiment.";
  if (incognito) {
    [self.incognitoTabsMediator closeTabGroup:group];
    return;
  }

  [self.regularTabsMediator closeTabGroup:group];
}

- (void)ungroupTabGroup:(base::WeakPtr<const TabGroup>)group
              incognito:(BOOL)incognito
             sourceView:(UIView*)sourceView {
  CHECK(IsTabGroupInGridEnabled())
      << "You should not be able to ungroup a tab group outside the Tab Groups "
         "experiment.";
  if (incognito) {
    [self.incognitoTabsMediator ungroupTabGroup:group sourceView:sourceView];
    return;
  }

  [self.regularTabsMediator ungroupTabGroup:group sourceView:sourceView];
}

- (void)selectTabs {
  base::RecordAction(
      base::UserMetricsAction("MobileTabGridTabContextMenuSelectTabs"));
  [self setActiveMode:TabGridMode::kSelection];
}

- (void)removeSessionAtTableSectionWithIdentifier:(NSInteger)sectionIdentifier {
  [self.baseViewController.remoteTabsViewController
      removeSessionAtTableSectionWithIdentifier:sectionIdentifier];
}

- (synced_sessions::DistantSession const*)sessionForTableSectionWithIdentifier:
    (NSInteger)sectionIdentifier {
  return [self.baseViewController.remoteTabsViewController
      sessionForTableSectionWithIdentifier:sectionIdentifier];
}

#pragma mark - SceneStateObserver

- (void)sceneState:(SceneState*)sceneState
    transitionedToActivationLevel:(SceneActivationLevel)level {
  if (level == SceneActivationLevelBackground) {
    // When going in the background, hide the Inactive Tabs UI.
    [self.inactiveTabsCoordinator hide];
  }
  if (level < SceneActivationLevelForegroundActive) {
    // User has put the app into background, which constitutes of a meaningful
    // action.
    [self.baseViewController
        tabGridDidPerformAction:TabGridActionType::kBackground];
  }
}

#pragma mark - BringAndroidTabsCommands

- (void)reviewAllBringAndroidTabs {
  [self onUserInteractionWithBringAndroidTabsPrompt:YES];
}

- (void)dismissBringAndroidTabsPrompt {
  [self onUserInteractionWithBringAndroidTabsPrompt:NO];
}

// Helper method to handle BringAndroidTabsCommands.
- (void)onUserInteractionWithBringAndroidTabsPrompt:(BOOL)reviewTabs {
  DCHECK(_bringAndroidTabsPromptCoordinator);
  [self.baseViewController dismissViewControllerAnimated:YES completion:nil];
  if (reviewTabs) {
    _tabListFromAndroidCoordinator = [[TabListFromAndroidCoordinator alloc]
        initWithBaseViewController:self.baseViewController
                           browser:self.regularBrowser];
    [_tabListFromAndroidCoordinator start];
  } else {
    // The user journey to bring recent tabs on Android to iOS has finished.
    // Reload the service to update/clear the tabs.
    BringAndroidTabsToIOSServiceFactory::GetForBrowserStateIfExists(
        self.regularBrowser->GetBrowserState())
        ->LoadTabs();
  }
  [_bringAndroidTabsPromptCoordinator stop];
  _bringAndroidTabsPromptCoordinator = nil;
}

#pragma mark - TabGridCommands

- (void)bringGroupIntoView:(const TabGroup*)group animated:(BOOL)animated {
  TabGridPage pageToOpen;
  if ([_regularGridCoordinator bringTabGroupIntoViewIfPresent:group
                                                     animated:animated]) {
    pageToOpen = TabGridPageRegularTabs;
  } else if ([_incognitoGridCoordinator
                 bringTabGroupIntoViewIfPresent:group
                                       animated:animated]) {
    pageToOpen = TabGridPageIncognitoTabs;
  } else {
    // Tab group is not opened, return;
    return;
  }
  [self.baseViewController setCurrentPageAndPageControl:pageToOpen
                                               animated:YES];
}

- (void)showHistoryForText:(NSString*)text {
  // A history coordinator from main_controller won't work properly from the
  // tab grid. Using a local coordinator works better and we need to set
  // `loadStrategy` to YES to ALWAYS_NEW_FOREGROUND_TAB.
  self.historyCoordinator = [[HistoryCoordinator alloc]
      initWithBaseViewController:self.baseViewController
                         browser:self.regularBrowser];
  self.historyCoordinator.searchTerms = text;
  self.historyCoordinator.loadStrategy =
      UrlLoadStrategy::ALWAYS_NEW_FOREGROUND_TAB;
  self.historyCoordinator.presentationDelegate = self;
  self.historyCoordinator.delegate = self;
  [self.historyCoordinator start];
}

- (void)showWebSearchForText:(NSString*)text {
  TemplateURLService* templateURLService =
      ios::TemplateURLServiceFactory::GetForBrowserState(
          self.regularBrowser->GetBrowserState());

  const TemplateURL* searchURLTemplate =
      templateURLService->GetDefaultSearchProvider();
  DCHECK(searchURLTemplate);

  TemplateURLRef::SearchTermsArgs searchArgs(base::SysNSStringToUTF16(text));

  GURL searchURL(searchURLTemplate->url_ref().ReplaceSearchTerms(
      searchArgs, templateURLService->search_terms_data()));
  [self openLinkWithURL:searchURL];
}

- (void)showRecentTabsForText:(NSString*)text {
  [self.baseViewController setCurrentPageAndPageControl:TabGridPageRemoteTabs
                                               animated:YES];
}

- (void)showTabGroupsPanelAnimated:(BOOL)animated {
  CHECK(IsTabGroupSyncEnabled());
  [self.baseViewController setCurrentPageAndPageControl:TabGridPageTabGroups
                                               animated:animated];
}

- (void)exitTabGrid {
  [self.baseViewController updateActivePageToCurrent];
  TabGridPage targetPage = self.baseViewController.activePage;

  // Holding the done button down when it is enabled could result in done tap
  // being triggered on release after tabs have been closed and the button
  // disabled. Ensure that action is only taken on a valid state.
  if (![self tabsPresentForPage:targetPage]) {
    return;
  }
  [self showActiveTabInPage:targetPage focusOmnibox:NO];
}

#pragma mark - SnackbarCoordinatorDelegate

- (CGFloat)snackbarCoordinatorBottomOffsetForCurrentlyPresentedView:
               (SnackbarCoordinator*)snackbarCoordinator
                                                forceBrowserToolbar:
                                                    (BOOL)forceBrowserToolbar {
  if (!self.bvcContainer.currentBVC) {
    // The tab grid is being show so use tab grid bottom bar.
    // kTabGridBottomToolbarGuide is stored in the shared layout guide center.
    UIView* tabGridBottomToolbarView = [LayoutGuideCenterForBrowser(nil)
        referencedViewUnderName:kTabGridBottomToolbarGuide];
    return CGRectGetHeight(tabGridBottomToolbarView.bounds);
  }

  if (!forceBrowserToolbar &&
      self.bvcContainer.currentBVC.presentedViewController) {
    UIViewController* presentedViewController =
        self.bvcContainer.currentBVC.presentedViewController;

    // When the presented view is a navigation controller, return the navigation
    // controller's toolbar height.
    if ([presentedViewController isKindOfClass:UINavigationController.class]) {
      UINavigationController* navigationController =
          base::apple::ObjCCastStrict<UINavigationController>(
              presentedViewController);

      if (navigationController.toolbar &&
          !navigationController.isToolbarHidden) {
        CGFloat toolbarHeight =
            CGRectGetHeight(presentedViewController.view.frame) -
            CGRectGetMinY(navigationController.toolbar.frame);
        return toolbarHeight;
      } else {
        return 0.0;
      }
    }
  }

  // Use the BVC bottom bar as the offset.
  Browser* browser = nil;
  if (snackbarCoordinator == self.snackbarCoordinator) {
    browser = self.regularBrowser;
  } else if (snackbarCoordinator == self.incognitoSnackbarCoordinator) {
    browser = self.incognitoBrowser;
  }
  CHECK(browser);

  UIView* bottomToolbar = [LayoutGuideCenterForBrowser(browser)
      referencedViewUnderName:kSecondaryToolbarGuide];

  return CGRectGetHeight(bottomToolbar.bounds);
}

#pragma mark - HistorySyncPopupCoordinatorDelegate

- (void)historySyncPopupCoordinator:(HistorySyncPopupCoordinator*)coordinator
                didFinishWithResult:(SigninCoordinatorResult)result {
  _historySyncPopupCoordinator.delegate = nil;
  [_historySyncPopupCoordinator stop];
  _historySyncPopupCoordinator = nil;
  [self.remoteTabsMediator refreshSessionsView];
}

#pragma mark - TabGroupPositioner

- (UIView*)viewAboveTabGroup {
  return self.bvcContainer.view;
}

#pragma mark - LegacyGridTransitionAnimationLayoutProviding

- (BOOL)isSelectedCellVisible {
  if (self.baseViewController.activePage !=
      self.baseViewController.currentPage) {
    return NO;
  }

  switch (self.baseViewController.activePage) {
    case TabGridPageIncognitoTabs:
      return [_incognitoGridCoordinator isSelectedCellVisible];
    case TabGridPageRegularTabs:
      return [_regularGridCoordinator isSelectedCellVisible];
    case TabGridPageRemoteTabs:
    case TabGridPageTabGroups:
      return NO;
  }
}

- (BOOL)shouldReparentSelectedCell:(GridAnimationDirection)animationDirection {
  switch (animationDirection) {
      // For contracting animation only selected pinned cells should be
      // reparented.
    case GridAnimationDirectionContracting:
      return [self isPinnedCellSelected];
      // For expanding animation any selected cell should be reparented.
    case GridAnimationDirectionExpanding:
      return YES;
  }
}

- (LegacyGridTransitionLayout*)transitionLayout:(TabGridPage)activePage {
  LegacyGridTransitionLayout* layout =
      [self transitionLayoutForPage:activePage];
  if (!layout) {
    return nil;
  }
  layout.frameChanged = !CGRectEqualToRect(self.baseViewController.view.frame,
                                           _frameWhenEntering);
  return layout;
}

- (UIView*)animationViewsContainer {
  return self.baseViewController.view;
}

- (UIView*)animationViewsContainerBottomView {
  // The animation should happen just above the direct subview of the TabGrid
  // containing the visible grid.
  UIView* potentialGridContainer;
  switch (self.baseViewController.activePage) {
    case TabGridPageIncognitoTabs:
      potentialGridContainer = [_incognitoGridCoordinator gridView];
      break;
    case TabGridPageRegularTabs:
      potentialGridContainer = [_regularGridCoordinator gridView];
      break;
    case TabGridPageRemoteTabs:
    case TabGridPageTabGroups:
      NOTREACHED();
  }
  UIView* baseView = self.baseViewController.view;
  while (potentialGridContainer.superview != baseView) {
    potentialGridContainer = potentialGridContainer.superview;
  }
  return potentialGridContainer;
}

- (CGRect)gridContainerFrame {
  UIView* potentialAnimationContainer;
  switch (self.baseViewController.activePage) {
    case TabGridPageIncognitoTabs:
      potentialAnimationContainer =
          [_incognitoGridCoordinator gridContainerForAnimation];
      break;
    case TabGridPageRegularTabs:
      potentialAnimationContainer =
          [_regularGridCoordinator gridContainerForAnimation];
      break;
    case TabGridPageRemoteTabs:
    case TabGridPageTabGroups:
      NOTREACHED();
  }
  if (potentialAnimationContainer) {
    return potentialAnimationContainer.frame;
  }
  return self.baseViewController.view.bounds;
}

// Returns wether there is a selected pinned cell.
- (BOOL)isPinnedCellSelected {
  if (!IsPinnedTabsEnabled() ||
      self.baseViewController.currentPage != TabGridPageRegularTabs) {
    return NO;
  }

  return [_regularGridCoordinator.pinnedTabsViewController hasSelectedCell];
}

// Returns transition layout for the provided `page`.
- (LegacyGridTransitionLayout*)transitionLayoutForPage:(TabGridPage)page {
  switch (page) {
    case TabGridPageIncognitoTabs:
      return [_incognitoGridCoordinator transitionLayout];
    case TabGridPageRegularTabs:
      return [_regularGridCoordinator transitionLayout];
    case TabGridPageRemoteTabs:
    case TabGridPageTabGroups:
      return nil;
  }
}

@end