// Copyright 2012 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/browser_view/ui_bundled/browser_view_controller.h"

#import "base/apple/bundle_locations.h"
#import "base/apple/foundation_util.h"
#import "base/ios/ios_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/task/sequenced_task_runner.h"
#import "components/enterprise/idle/idle_pref_names.h"
#import "components/prefs/pref_service.h"
#import "components/signin/public/identity_manager/identity_manager.h"
#import "components/strings/grit/components_strings.h"
#import "components/ukm/ios/ukm_url_recorder.h"
#import "ios/chrome/browser/app_bar/ui/app_bar_constants.h"
#import "ios/chrome/browser/authentication/ui_bundled/re_signin_infobar_delegate.h"
#import "ios/chrome/browser/bookmarks/ui_bundled/home/bookmarks_coordinator.h"
#import "ios/chrome/browser/browser_content/ui_bundled/browser_content_view_controller.h"
#import "ios/chrome/browser/browser_view/public/browser_view_visibility_state.h"
#import "ios/chrome/browser/browser_view/public/browser_view_visibility_state_changed_callback.h"
#import "ios/chrome/browser/browser_view/ui_bundled/browser_view_controller+private.h"
#import "ios/chrome/browser/browser_view/ui_bundled/key_commands_provider.h"
#import "ios/chrome/browser/browser_view/ui_bundled/safe_area_provider.h"
#import "ios/chrome/browser/content_suggestions/public/ntp_home_constants.h"
#import "ios/chrome/browser/crash_report/model/crash_keys_helper.h"
#import "ios/chrome/browser/default_browser/promo/non_modal/coordinator/default_promo_non_modal_presentation_delegate.h"
#import "ios/chrome/browser/discover_feed/model/feed_constants.h"
#import "ios/chrome/browser/first_run/public/first_run_util.h"
#import "ios/chrome/browser/fullscreen/model/fullscreen_browser_agent.h"
#import "ios/chrome/browser/fullscreen/model/fullscreen_browser_agent_observer_bridge.h"
#import "ios/chrome/browser/fullscreen/public/fullscreen_metrics.h"
#import "ios/chrome/browser/fullscreen/ui_bundled/fullscreen_animator.h"
#import "ios/chrome/browser/fullscreen/ui_bundled/fullscreen_ui_element.h"
#import "ios/chrome/browser/fullscreen/ui_bundled/fullscreen_ui_updater.h"
#import "ios/chrome/browser/incognito_reauth/ui_bundled/incognito_reauth_constants.h"
#import "ios/chrome/browser/incognito_reauth/ui_bundled/incognito_reauth_scene_agent.h"
#import "ios/chrome/browser/incognito_reauth/ui_bundled/incognito_reauth_view.h"
#import "ios/chrome/browser/intelligence/bwg/utils/gemini_constants.h"
#import "ios/chrome/browser/intelligence/features/features.h"
#import "ios/chrome/browser/intents/model/intents_donation_helper.h"
#import "ios/chrome/browser/main_content/ui_bundled/main_content_ui.h"
#import "ios/chrome/browser/main_content/ui_bundled/main_content_ui_broadcasting_util.h"
#import "ios/chrome/browser/main_content/ui_bundled/main_content_ui_state.h"
#import "ios/chrome/browser/main_content/ui_bundled/web_scroll_view_main_content_ui_forwarder.h"
#import "ios/chrome/browser/metrics/model/activity_reporter.h"
#import "ios/chrome/browser/metrics/model/tab_usage_recorder_browser_agent.h"
#import "ios/chrome/browser/ntp/model/new_tab_page_util.h"
#import "ios/chrome/browser/ntp/ui_bundled/logo_animation_controller.h"
#import "ios/chrome/browser/ntp/ui_bundled/new_tab_page_coordinator.h"
#import "ios/chrome/browser/omnibox/public/omnibox_ui_features.h"
#import "ios/chrome/browser/popup_menu/coordinator/popup_menu_coordinator.h"
#import "ios/chrome/browser/reading_list/model/reading_list_browser_agent.h"
#import "ios/chrome/browser/shared/coordinator/layout_guide/layout_guide_util.h"
#import "ios/chrome/browser/shared/coordinator/scene/state/scene_layout_state.h"
#import "ios/chrome/browser/shared/model/application_context/application_context.h"
#import "ios/chrome/browser/shared/model/url/chrome_url_constants.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
#import "ios/chrome/browser/shared/public/commands/browser_coordinator_commands.h"
#import "ios/chrome/browser/shared/public/commands/find_in_page_commands.h"
#import "ios/chrome/browser/shared/public/commands/gemini_commands.h"
#import "ios/chrome/browser/shared/public/commands/help_commands.h"
#import "ios/chrome/browser/shared/public/commands/omnibox_commands.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/scene_commands.h"
#import "ios/chrome/browser/shared/public/commands/settings_commands.h"
#import "ios/chrome/browser/shared/public/commands/text_zoom_commands.h"
#import "ios/chrome/browser/shared/public/commands/toolbar_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/named_guide.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/shared/ui/util/url_with_title.h"
#import "ios/chrome/browser/shared/ui/util/util_swift.h"
#import "ios/chrome/browser/side_swipe/ui_bundled/card_swipe_view_delegate.h"
#import "ios/chrome/browser/side_swipe/ui_bundled/side_swipe_coordinator.h"
#import "ios/chrome/browser/side_swipe/ui_bundled/side_swipe_mediator.h"
#import "ios/chrome/browser/side_swipe/ui_bundled/side_swipe_ui_controller_delegate.h"
#import "ios/chrome/browser/side_swipe/ui_bundled/swipe_view.h"
#import "ios/chrome/browser/signin/model/identity_manager_factory.h"
#import "ios/chrome/browser/snapshots/model/snapshot_browser_agent.h"
#import "ios/chrome/browser/snapshots/model/snapshot_kind.h"
#import "ios/chrome/browser/snapshots/model/snapshot_tab_helper.h"
#import "ios/chrome/browser/tabs/ui_bundled/background_tab_animation_view.h"
#import "ios/chrome/browser/tabs/ui_bundled/foreground_tab_animation_view.h"
#import "ios/chrome/browser/tabs/ui_bundled/switch_to_tab_animation_view.h"
#import "ios/chrome/browser/toolbar/coordinator/main_toolbar_coordinator.h"
#import "ios/chrome/browser/toolbar/legacy/ui_bundled/accessory/toolbar_accessory_presenter.h"
#import "ios/chrome/browser/toolbar/legacy/ui_bundled/buttons/toolbar_configuration.h"
#import "ios/chrome/browser/toolbar/legacy/ui_bundled/fullscreen/toolbars_size.h"
#import "ios/chrome/browser/toolbar/legacy/ui_bundled/fullscreen/toolbars_size_broadcasting_util.h"
#import "ios/chrome/browser/url_loading/model/url_loading_browser_agent.h"
#import "ios/chrome/browser/url_loading/model/url_loading_params.h"
#import "ios/chrome/browser/voice/ui_bundled/voice_search_notification_names.h"
#import "ios/chrome/browser/web/model/page_placeholder_browser_agent.h"
#import "ios/chrome/browser/web/model/web_navigation_browser_agent.h"
#import "ios/chrome/browser/web/model/web_navigation_util.h"
#import "ios/chrome/browser/web_state_list/model/web_usage_enabler/web_usage_enabler_browser_agent.h"
#import "ios/chrome/browser/webui/model/show_mail_composer_context.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/promo_style/promo_style_view_controller.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"
#import "ios/chrome/common/ui/util/ui_util.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/public/provider/chrome/browser/fullscreen/fullscreen_api.h"
#import "ios/public/provider/chrome/browser/voice_search/voice_search_controller.h"
#import "ios/web/public/ui/crw_web_view_proxy.h"
#import "ios/web/public/ui/crw_web_view_scroll_view_proxy.h"
#import "ios/web/public/web_state_observer_bridge.h"
#import "net/base/apple/url_conversions.h"
#import "services/metrics/public/cpp/ukm_builders.h"
#import "ui/base/device_form_factor.h"
#import "ui/base/l10n/l10n_util.h"

namespace {

enum HeaderBehaviour {
  // The header moves completely out of the screen.
  Hideable = 0,
  // This header stay on screen and covers part of the content.
  Overlap
};

// Inset to remove from the toolbar height when in full-screen mode with the
// dynamic island visible.
const CGFloat kTopDynamicIslandInset = 24;

// Returns true if Chrome Next IA layout and full-screen refactoring are active.
bool IsFullscreenNextIAEnabled() {
  return IsFullscreenRefactoringEnabled() && IsChromeNextIaEnabled();
}

}  // namespace

#pragma mark - HeaderDefinition helper

// Class used to define a header, an object displayed at the top of the browser.
@interface HeaderDefinition : NSObject

// The header view.
@property(nonatomic, strong) UIView* view;
// How to place the view, and its behaviour when the headers move.
@property(nonatomic, assign) HeaderBehaviour behaviour;

- (instancetype)initWithView:(UIView*)view
             headerBehaviour:(HeaderBehaviour)behaviour;

+ (instancetype)definitionWithView:(UIView*)view
                   headerBehaviour:(HeaderBehaviour)behaviour;

@end

@implementation HeaderDefinition
@synthesize view = _view;
@synthesize behaviour = _behaviour;

+ (instancetype)definitionWithView:(UIView*)view
                   headerBehaviour:(HeaderBehaviour)behaviour {
  return [[self alloc] initWithView:view headerBehaviour:behaviour];
}

- (instancetype)initWithView:(UIView*)view
             headerBehaviour:(HeaderBehaviour)behaviour {
  self = [super init];
  if (self) {
    _view = view;
    _behaviour = behaviour;
  }
  return self;
}

@end

#pragma mark - BVC

// Note other delegates defined in the Delegates category header.
@interface BrowserViewController () <CardSwipeViewDelegate,
                                     FullscreenBrowserAgentObserving,
                                     FullscreenUIElement,
                                     LogoAnimationControllerOwnerOwner,
                                     MainContentUI,
                                     SceneLayoutStateObserver,
                                     SideSwipeUIControllerDelegate,
                                     UIGestureRecognizerDelegate> {
  // Identifier for each animation of an NTP opening.
  NSInteger _NTPAnimationIdentifier;

  // Mediator for edge swipe gestures for page and tab navigation.
  SideSwipeCoordinator* _sideSwipeCoordinator;

  // Keyboard commands provider.  It offloads most of the keyboard commands
  // management off of the BVC.
  KeyCommandsProvider* _keyCommandsProvider;

  // Used to display the Voice Search UI.  Nil if not visible.
  id<VoiceSearchController> _voiceSearchController;

  // YES if Voice Search should be started when the new tab animation is
  // finished.
  BOOL _startVoiceSearchAfterNewTabAnimation;
  ActivityReporterWithIncognito* _activityReporter;

  // Whether or not -shutdown has been called.
  BOOL _isShutdown;

  // Whether or not Incognito* is enabled.
  BOOL _isOffTheRecord;

  // The Browser's WebStateList.
  base::WeakPtr<WebStateList> _webStateList;

  // Whether the current content is incognito and requires biometric
  // authentication from the user before it can be accessed.
  BOOL _itemsRequireAuthentication;

  // The last point within `contentArea` that's received a touch.
  CGPoint _lastTapPoint;

  // The time at which `_lastTapPoint` was most recently set.
  CFTimeInterval _lastTapTime;

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

  // Toolbars size that broadcasts changes to min and max heights.
  ToolbarsSize* _toolbarsSize;

  // The main content UI updater for the content displayed by this BVC.
  MainContentUIStateUpdater* _mainContentUIUpdater;

  // The forwarder for web scroll view interation events.
  WebScrollViewMainContentUIForwarder* _webMainContentUIForwarder;

  // The updater that adjusts the toolbar's layout for fullscreen events.
  std::unique_ptr<FullscreenUIUpdater> _fullscreenUIUpdater;

  // Bridge to observe the FullscreenBrowserAgent.
  std::unique_ptr<FullscreenBrowserAgentObserverBridge>
      _fullscreenBrowserAgentObserverBridge;

  // The fullscreen browser agent.
  raw_ptr<FullscreenBrowserAgent> _fullscreenBrowserAgent;

  // The service used to load url parameters in current or new tab.
  raw_ptr<UrlLoadingBrowserAgent> _urlLoadingBrowserAgent;

  // Used to report usage of a single Browser's tab.
  raw_ptr<TabUsageRecorderBrowserAgent> _tabUsageRecorderBrowserAgent;

  // Used to fetch snapshots.
  raw_ptr<SnapshotBrowserAgent> _snapshotBrowserAgent;

  // Callback invoked when the browser view visibility changes.
  BrowserViewVisibilityStateChangedCallback
      _browserViewVisibilityStateChangedCallback;

  // Used to get the layout guide center.
  LayoutGuideCenter* _layoutGuideCenter;

  // Left and right constraints for the toolbars. Physical left/right anchors
  // are used instead of leading/trailing because `AppBarPosition::kLeft` and
  // `kRight` refer to physical screen edges that do not flip in RTL.
  NSLayoutConstraint* _toolbarLeftConstraint;
  NSLayoutConstraint* _toolbarRightConstraint;

  // Whether the Lens Overlay is currently active and visible for the browser
  // view.
  BOOL _lensOverlayVisible;

  // Tracks if the secondary toolbar is currently above the keyboard.
  BOOL _isSecondaryToolbarAboveKeyboard;

  __weak id<BrowserCoordinatorCommands> _browserCoordinatorHandler;
  __weak id<ToolbarCommands> _toolbarHandler;

  NSArray<NSLayoutConstraint*>* _NTPConstraints;
  // The last recorded view size to avoid redundant updates on layout.
  CGSize _lastViewSize;
}

// Activates/deactivates the object. This will enable/disable the ability for
// this object to browse, and to have live UIWebViews associated with it. While
// not active, the UI will not react to changes in the active web state, so
// generally an inactive BVC should not be visible.
@property(nonatomic, assign, getter=isActive) BOOL active;
// Browser content view controller.
@property(nonatomic, strong)
    BrowserContentViewController* browserContentViewController;
// Invisible button used to dismiss the keyboard.
@property(nonatomic, strong) UIButton* typingShield;
// The visibility state of the browser view. Value will be set to `kVisible` on
// viewDidAppear and to `kNotInViewHierarchy` on viewWillDisappear.
@property(nonatomic, assign) BrowserViewVisibilityState visibilityState;
// Whether the controller should broadcast its UI.
@property(nonatomic, assign, getter=isBroadcasting) BOOL broadcasting;
// A view to obscure incognito content when the user isn't authorized to
// see it.
@property(nonatomic, strong) IncognitoReauthView* blockingView;
// Whether the controller is currently dismissing a presented view controller.
@property(nonatomic, assign, getter=isDismissingModal) BOOL dismissingModal;
// Whether a new tab animation is occurring.
@property(nonatomic, assign, getter=isInNewTabAnimation) BOOL inNewTabAnimation;
// Whether BVC prefers to hide the status bar. This value is used to determine
// the response from the `prefersStatusBarHidden` method.
@property(nonatomic, assign) BOOL hideStatusBar;
// A block to be run when the `tabWasAdded:` method completes the animation
// for the presentation of a new tab. Can be used to record performance metrics.
@property(nonatomic, strong, nullable)
    ProceduralBlock foregroundTabWasAddedCompletionBlock;

// Returns the header views, all the chrome on top of the page, including the
// ones that cannot be scrolled off screen by full screen.
@property(nonatomic, strong, readonly) NSArray<HeaderDefinition*>* headerViews;

// Coordinator for the popup menus.
@property(nonatomic, strong) PopupMenuCoordinator* popupMenuCoordinator;

// Presenter used to display accessories over the toolbar (e.g. Find In Page).
@property(nonatomic, strong)
    ToolbarAccessoryPresenter* toolbarAccessoryPresenter;

// Command handler for text zoom commands.
@property(nonatomic, weak) id<TextZoomCommands> textZoomHandler;

// Command handler for help commands.
@property(nonatomic, weak) id<HelpCommands> helpHandler;

// Command handler for popup menu commands.
@property(nonatomic, weak) id<PopupMenuCommands> popupMenuCommandsHandler;

// Command handler for scene commands.
@property(nonatomic, weak) id<SceneCommands> sceneHandler;

// Command handler for find in page commands.
@property(nonatomic, weak) id<FindInPageCommands> findInPageCommandsHandler;

// The FullscreenController.
@property(nonatomic, assign) FullscreenController* fullscreenController;

// Coordinator of primary and secondary toolbars.
@property(nonatomic, strong) MainToolbarCoordinator* toolbarCoordinator;

// Vertical offset for the primary toolbar, used for fullscreen.
@property(nonatomic, strong) NSLayoutConstraint* primaryToolbarOffsetConstraint;
// Height constraint for the primary toolbar.
@property(nonatomic, strong) NSLayoutConstraint* primaryToolbarHeightConstraint;
// Height constraint for the secondary toolbar.
@property(nonatomic, strong)
    NSLayoutConstraint* secondaryToolbarHeightConstraint;
// Constraint anchoring the secondary toolbar to the bottom of the view.
@property(nonatomic, strong)
    NSLayoutConstraint* secondaryToolbarRegularBottomConstraint;
// Constraint anchoring the secondary toolbar to the top of the App Bar.
@property(nonatomic, strong)
    NSLayoutConstraint* secondaryToolbarAppBarBottomConstraint;
// Current Fullscreen progress for the footers.
@property(nonatomic, assign) CGFloat footerFullscreenProgress;
// Height of the header view.
@property(nonatomic, readonly) CGFloat headerHeight;

// The webState of the active tab.
@property(nonatomic, readonly) web::WebState* currentWebState;

// A gesture recognizer to track the last tapped window and the coordinates of
// the last tap.
@property(nonatomic, strong) UIGestureRecognizer* contentAreaGestureRecognizer;

// The coordinator for all NTPs in the BVC. Only used if kSingleNtp is enabled.
@property(nonatomic, strong) NewTabPageCoordinator* ntpCoordinator;

// Provider used to offload SceneStateBrowserAgent usage from BVC.
@property(nonatomic, strong) SafeAreaProvider* safeAreaProvider;

// Whether the content area is currently blocked by another view.
@property(nonatomic, readonly, getter=isContentAreaObstructed)
    BOOL contentAreaObstructed;

// When YES, the Fullscreen progress may be dirty and should be applied at
// next opportunity even if it appears to have not changed. This is only
// used for the legacy fullscreen implementation.
@property(nonatomic, assign) BOOL fullscreenProgressDirty;

@end

@implementation BrowserViewController

@synthesize topToolbarInset = _topToolbarInset;

#pragma mark - Object lifecycle

- (instancetype)initWithBrowserContentViewController:
                    (BrowserContentViewController*)browserContentViewController
                                 keyCommandsProvider:
                                     (KeyCommandsProvider*)keyCommandsProvider
                                        dependencies:
                                            (BrowserViewControllerDependencies)
                                                dependencies {
  self = [super initWithNibName:nil bundle:base::apple::FrameworkBundle()];
  if (self) {
    _browserContentViewController = browserContentViewController;
    _keyCommandsProvider = keyCommandsProvider;
    _sideSwipeCoordinator = dependencies.sideSwipeCoordinator;
    [_sideSwipeCoordinator setSideSwipeUIControllerDelegate:self];
    [_sideSwipeCoordinator setCardSwipeViewDelegate:self];
    _bookmarksCoordinator = dependencies.bookmarksCoordinator;
    _browserCoordinatorHandler = dependencies.browserCoordinatorHandler;
    self.toolbarAccessoryPresenter = dependencies.toolbarAccessoryPresenter;
    _toolbarHandler = dependencies.toolbarHandler;
    self.ntpCoordinator = dependencies.ntpCoordinator;
    self.popupMenuCoordinator = dependencies.popupMenuCoordinator;
    self.toolbarCoordinator = dependencies.toolbarCoordinator;

    self.textZoomHandler = dependencies.textZoomHandler;
    self.helpHandler = dependencies.helpHandler;
    self.popupMenuCommandsHandler = dependencies.popupMenuCommandsHandler;
    self.sceneHandler = dependencies.sceneHandler;
    self.findInPageCommandsHandler = dependencies.findInPageCommandsHandler;
    self.geminiHandler = dependencies.geminiHandler;
    _isOffTheRecord = dependencies.isOffTheRecord;
    _activityReporter = [[ActivityReporterWithIncognito alloc]
        initWithDomain:ActivityReportDomainTab];
    _visibilityState = BrowserViewVisibilityState::kNotInViewHierarchy;
    _urlLoadingBrowserAgent = dependencies.urlLoadingBrowserAgent;
    _tabUsageRecorderBrowserAgent = dependencies.tabUsageRecorderBrowserAgent;
    _snapshotBrowserAgent = dependencies.snapshotBrowserAgent;
    _layoutGuideCenter = dependencies.layoutGuideCenter;
    _webStateList = dependencies.webStateList;
    _voiceSearchController = dependencies.voiceSearchController;
    self.safeAreaProvider = dependencies.safeAreaProvider;

    self.inNewTabAnimation = NO;
    self.fullscreenController = dependencies.fullscreenController;
    _footerFullscreenProgress = 1.0;

    if (IsFullscreenRefactoringEnabled()) {
      _fullscreenBrowserAgent = dependencies.fullscreenBrowserAgent;
      _fullscreenBrowserAgentObserverBridge =
          std::make_unique<FullscreenBrowserAgentObserverBridge>(
              self, dependencies.fullscreenBrowserAgent);
    }

    // When starting the browser with an open tab, it is necessary to reset the
    // clipsToBounds property of the WKWebView so the page can bleed behind the
    // toolbar.
    if (self.currentWebState) {
      self.currentWebState->GetWebViewProxy().scrollViewProxy.clipsToBounds =
          NO;
    }
  }
  return self;
}

- (void)dealloc {
  DCHECK(_isShutdown) << "-shutdown must be called before dealloc.";
}

#pragma mark - Public Properties

- (UIView*)contentArea {
  return self.browserContentViewController.view;
}

- (const BrowserViewVisibilityStateChangedCallback&)
    browserViewVisibilityStateChangedCallback {
  return _browserViewVisibilityStateChangedCallback;
}

- (void)setBrowserViewVisibilityStateChangedCallback:
    (const BrowserViewVisibilityStateChangedCallback&)callback {
  _browserViewVisibilityStateChangedCallback = callback;
}

#pragma mark - Private Properties

- (BOOL)isContentAreaObstructed {
  return _visibilityState ==
             BrowserViewVisibilityState::kCoveredByOmniboxPopup ||
         _visibilityState ==
             BrowserViewVisibilityState::kCoveredByVoiceSearch ||
         _lensOverlayVisible;
}

- (void)setVisibilityState:(BrowserViewVisibilityState)state {
  if (_visibilityState == state) {
    return;
  }
  const BrowserViewVisibilityState previousState =
      std::exchange(_visibilityState, state);
  if (_browserViewVisibilityStateChangedCallback) {
    _browserViewVisibilityStateChangedCallback.Run(_visibilityState,
                                                   previousState);
  }
  [self updateBroadcastState];
  self.contentArea.accessibilityElementsHidden = self.contentAreaObstructed;
}

- (void)setBroadcasting:(BOOL)broadcasting {
  if (IsFullscreenRefactoringEnabled()) {
    if (broadcasting && _fullscreenBrowserAgent) {
      _fullscreenBrowserAgent->InvalidateInsetRange();
    }
    // Broadcasting is not needed for FullscreenRefactoring.
    return;
  }

  if (_broadcasting == broadcasting) {
    return;
  }
  _broadcasting = broadcasting;

  ChromeBroadcaster* broadcaster = self.fullscreenController->broadcaster();
  if (_broadcasting) {
    _toolbarsSize = [[ToolbarsSize alloc] init];
    // Must update _toolbarsSize with current toolbar height state before
    // starting broadcasting.
    [self updateToolbarState];
    self.fullscreenController->SetToolbarsSize(_toolbarsSize);

    if (!IsRefactorToolbarsSize()) {
      StartBroadcastingToolbarsSize(_toolbarsSize, broadcaster);
    }
    _mainContentUIUpdater = [[MainContentUIStateUpdater alloc]
        initWithState:[[MainContentUIState alloc] init]];
    _webMainContentUIForwarder = [[WebScrollViewMainContentUIForwarder alloc]
        initWithUpdater:_mainContentUIUpdater
           webStateList:self.webStateList];
    StartBroadcastingMainContentUI(self, broadcaster);

    if (!IsFullscreenRefactoringEnabled()) {
      _fullscreenUIUpdater = std::make_unique<FullscreenUIUpdater>(
          self.fullscreenController, self);
      [self
          updateForFullscreenProgress:self.fullscreenController->GetProgress()];
    }
  } else {
    if (!IsRefactorToolbarsSize()) {
      StopBroadcastingToolbarsSize(broadcaster);
    }
    StopBroadcastingMainContentUI(broadcaster);
    _mainContentUIUpdater = nil;
    _toolbarsSize = nil;
    [_webMainContentUIForwarder disconnect];
    _webMainContentUIForwarder = nil;

    _fullscreenUIUpdater = nullptr;
  }
}

- (void)setInNewTabAnimation:(BOOL)inNewTabAnimation {
  if (_inNewTabAnimation == inNewTabAnimation) {
    return;
  }
  _inNewTabAnimation = inNewTabAnimation;
  [self updateBroadcastState];
}

- (void)setHideStatusBar:(BOOL)hideStatusBar {
  if (_hideStatusBar == hideStatusBar) {
    return;
  }
  _hideStatusBar = hideStatusBar;
  [self setNeedsStatusBarAppearanceUpdate];
}

- (NSArray<HeaderDefinition*>*)headerViews {
  NSMutableArray<HeaderDefinition*>* results = [NSMutableArray array];

  if (![self isViewLoaded]) {
    return results;
  }

  // Add the primary toolbar.
  if (self.toolbarCoordinator.primaryToolbarViewController.view) {
    [results
        addObject:[HeaderDefinition
                      definitionWithView:self.toolbarCoordinator
                                             .primaryToolbarViewController.view
                         headerBehaviour:Hideable]];
  }

  // Add the toolbar accessory if presenting.
  if (CanShowTabStrip(self) && self.toolbarAccessoryPresenter.isPresenting) {
    [results addObject:[HeaderDefinition
                           definitionWithView:self.toolbarAccessoryPresenter
                                                  .backgroundView
                              headerBehaviour:Overlap]];
  }
  return [results copy];
}

// Returns the safeAreaInsets of the root window for self.view. In some cases,
// the self.view.safeAreaInsets are cleared when the view is unattached ( for
// example on the incognito BVC when the normal BVC is the one active or vice
// versa). Attached or unattached, going to the window through the SceneState
// for the self.browser solves both issues.
- (UIEdgeInsets)rootSafeAreaInsets {
  if (_isShutdown) {
    return UIEdgeInsetsZero;
  }

  UIEdgeInsets safeArea = self.safeAreaProvider.safeArea;
  if (!UIEdgeInsetsEqualToEdgeInsets(safeArea, UIEdgeInsetsZero)) {
    return safeArea;
  }

  if ([self isViewLoaded]) {
    return self.view.safeAreaInsets;
  }

  return UIEdgeInsetsZero;
}

- (CGFloat)headerHeight {
  NSArray<HeaderDefinition*>* views = [self headerViews];

  CGFloat height = self.topToolbarInset;
  for (HeaderDefinition* header in views) {
    if (header.view && header.behaviour == Hideable) {
      height += CGRectGetHeight(header.view.frame);
    }
  }

  return height;
}

- (UIView*)viewForCurrentWebState {
  return [self viewForWebState:self.currentWebState];
}

- (void)updateWebStateVisibility:(BOOL)isVisible {
  if (isVisible) {
    // TODO(crbug.com/40630853): The webState is not necessarily added to the
    // view hierarchy, even though the bookkeeping says that the WebState is
    // visible. Do not DCHECK([webState->GetView() window]) here since this is a
    // known issue.
    self.currentWebState->WasShown();
  } else {
    self.currentWebState->WasHidden();
  }
}

- (web::WebState*)currentWebState {
  return self.webStateList ? _webStateList->GetActiveWebState() : nullptr;
}

- (WebStateList*)webStateList {
  WebStateList* webStateList = _webStateList.get();
  return webStateList ? webStateList : nullptr;
}

#pragma mark - BrowserLayoutConsumer

- (void)setTopToolbarInset:(CGFloat)inset {
  if (_topToolbarInset == inset) {
    return;
  }
  _topToolbarInset = inset;
  self.primaryToolbarOffsetConstraint.constant = inset;
  [self updateToolbarState];
  [self.view setNeedsLayout];
}

- (void)setLayoutState:(SceneLayoutState*)layoutState {
  if (_layoutState == layoutState) {
    return;
  }
  [_layoutState removeObserver:self];
  _layoutState = layoutState;
  [_layoutState addObserver:self];
  if (layoutState) {
    [self updateToolbarConstraints];
    [self updateSecondaryToolbarBottomConstraint];
  }
}

- (void)setLensOverlayStateNotifier:
    (LensOverlayStateNotifier*)lensOverlayStateNotifier {
  if (_lensOverlayStateNotifier == lensOverlayStateNotifier) {
    return;
  }
  [_lensOverlayStateNotifier removeObserver:self];
  _lensOverlayStateNotifier = lensOverlayStateNotifier;
  [_lensOverlayStateNotifier addObserver:self];
}

#pragma mark - SceneLayoutStateObserver

- (void)layoutState:(SceneLayoutState*)layoutState
    didChangeAppBarPosition:(AppBarPosition)appBarPosition {
  if (!self.view.window) {
    return;
  }

  [self updateToolbarConstraints];
  [self updateSecondaryToolbarBottomConstraint];
  [self animateTransition];
}

#pragma mark - Public methods

- (void)shieldWasTapped:(id)sender {
  [_browserCoordinatorHandler hideComposebox];
}

- (void)openNewTabFromOriginPoint:(CGPoint)originPoint
                     focusOmnibox:(BOOL)focusOmnibox
                    inheritOpener:(BOOL)inheritOpener {
  if (self.inNewTabAnimation) {
    return;
  }
  const BOOL offTheRecord = _isOffTheRecord;
  ProceduralBlock oldForegroundTabWasAddedCompletionBlock =
      self.foregroundTabWasAddedCompletionBlock;
  __weak id<BrowserCoordinatorCommands> browserCoordinatorHandler =
      _browserCoordinatorHandler;
  self.foregroundTabWasAddedCompletionBlock = ^{
    if (oldForegroundTabWasAddedCompletionBlock) {
      oldForegroundTabWasAddedCompletionBlock();
    }
    if (focusOmnibox) {
      [browserCoordinatorHandler showComposebox];
    }
  };

  [self setLastTapPointFromCommand:originPoint];

  // In most cases, we want to take a snapshot of the current tab before opening
  // a new tab. However, if the current tab is not fully visible (did not finish
  // `-viewDidAppear:`, then we must not take an empty snapshot, replacing an
  // existing snapshot for the tab. This can happen when a new regular tab is
  // opened from an incognito tab. A different BVC is displayed, which may not
  // have enough time to finish appearing before a snapshot is requested.
  if (self.currentWebState && [self appeared]) {
    SnapshotTabHelper::FromWebState(self.currentWebState)
        ->UpdateSnapshotWithCallback(nil);
  }

  UrlLoadParams params = UrlLoadParams::InNewTab(GURL(kChromeUINewTabURL));
  params.web_params.transition_type = ui::PAGE_TRANSITION_TYPED;
  params.in_incognito = offTheRecord;
  params.inherit_opener = inheritOpener;
  _urlLoadingBrowserAgent->Load(params);
}

- (void)appendTabAddedCompletion:(ProceduralBlock)tabAddedCompletion {
  if (tabAddedCompletion) {
    if (self.foregroundTabWasAddedCompletionBlock) {
      ProceduralBlock oldForegroundTabWasAddedCompletionBlock =
          self.foregroundTabWasAddedCompletionBlock;
      self.foregroundTabWasAddedCompletionBlock = ^{
        oldForegroundTabWasAddedCompletionBlock();
        tabAddedCompletion();
      };
    } else {
      self.foregroundTabWasAddedCompletionBlock = tabAddedCompletion;
    }
  }
}

- (void)startVoiceSearch {
  // Delay Voice Search until new tab animations have finished.
  if (self.inNewTabAnimation) {
    _startVoiceSearchAfterNewTabAnimation = YES;
    return;
  }

  // Keyboard shouldn't overlay the ecoutez window, so dismiss find in page and
  // dismiss the keyboard.
  [self.findInPageCommandsHandler closeFindInPage];
  [self.textZoomHandler closeTextZoom];
  [self.viewForCurrentWebState endEditing:NO];

  // Present voice search.
  [_voiceSearchController
      startRecognitionOnViewController:self
                              webState:self.currentWebState];
  [_browserCoordinatorHandler hideComposebox];
}

#pragma mark - browser_view_controller+private.h

- (void)setActive:(BOOL)active {
  if (_active == active) {
    return;
  }
  _active = active;

  [self updateBroadcastState];

  if (active) {
    // Force loading the view in case it was not loaded yet.
    [self loadViewIfNeeded];
    if (self.viewForCurrentWebState) {
      [self displayTabView];
    }
  }
  [self setNeedsStatusBarAppearanceUpdate];
  [self updateTabActivityReporting];
}

// TODO(crbug.com/40842434): Federate ClearPresentedState.
- (void)clearPresentedStateWithCompletion:(ProceduralBlock)completion
                           dismissOmnibox:(BOOL)dismissOmnibox
           dismissPresentedViewController:(BOOL)dismissPresentedViewController {
  [_bookmarksCoordinator dismissBookmarkModalControllerAnimated:NO];
  if (dismissOmnibox) {
    [_browserCoordinatorHandler hideComposebox];
  } else {
    [_browserCoordinatorHandler dismissMultimodalActionsMenu];
  }
  [self.helpHandler hideAllHelpBubbles];
  [_voiceSearchController dismissMicPermissionHelp];
  [self.findInPageCommandsHandler closeFindInPage];
  [self.textZoomHandler closeTextZoom];

  [self.popupMenuCommandsHandler dismissPopupMenuAnimated:NO];

  if (self.presentedViewController && dismissPresentedViewController) {
    // Dismisses any other modal controllers that may be present, e.g. Recent
    // Tabs.
    //
    // Note that currently, some controllers like the bookmark ones were already
    // dismissed (in this example in -dismissBookmarkModalControllerAnimated:),
    // but are still reported as the presentedViewController.  Calling
    // `dismissViewControllerAnimated:completion:` again would dismiss the BVC
    // itself, so instead check the value of `self.dismissingModal` and only
    // call dismiss if one of the above calls has not already triggered a
    // dismissal.
    //
    // To ensure the completion is called, nil is passed to the call to dismiss,
    // and the completion is called explicitly below.
    if (!self.dismissingModal) {
      [self dismissViewControllerAnimated:NO completion:nil];
    }
    // Dismissed controllers will be so after a delay. Queue the completion
    // callback after that.
    if (completion) {
      base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
          FROM_HERE, base::BindOnce(completion), base::Milliseconds(400));
    }
  } else if (completion) {
    // If no view controllers are presented, we should be ok with dispatching
    // the completion block directly.
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, base::BindOnce(completion));
  }
}

- (void)animateOpenBackgroundTabFromOriginPoint:(CGPoint)originPoint
                                     completion:(void (^)())completion {
  if (CanShowTabStrip(self) || CGPointEqualToPoint(originPoint, CGPointZero)) {
    completion();
  } else {
    self.inNewTabAnimation = YES;
    // Exit fullscreen if needed.
    self.fullscreenController->ExitFullscreen(
        FullscreenModeTransitionTrigger::kForcedByCode);
    const CGFloat kAnimatedViewSize = 50;
    BackgroundTabAnimationView* animatedView =
        [[BackgroundTabAnimationView alloc]
            initWithFrame:CGRectMake(0, 0, kAnimatedViewSize, kAnimatedViewSize)
                incognito:_isOffTheRecord];
    animatedView.layoutGuideCenter = _layoutGuideCenter;
    __weak UIView* weakAnimatedView = animatedView;
    auto completionBlock = ^() {
      self.inNewTabAnimation = NO;
      [weakAnimatedView removeFromSuperview];
      completion();
    };
    [self.view addSubview:animatedView];
    [animatedView animateFrom:originPoint
        toTabGridButtonWithCompletion:completionBlock];
  }
}

- (void)shutdown {
  DCHECK(!_isShutdown);
  _isShutdown = YES;

  _fullscreenUIUpdater = nullptr;
  _fullscreenController = nullptr;
  _fullscreenBrowserAgentObserverBridge = nullptr;
  _fullscreenBrowserAgent = nullptr;

  [self.contentArea removeGestureRecognizer:self.contentAreaGestureRecognizer];

  [self.toolbarCoordinator stop];
  self.toolbarCoordinator = nil;
  _toolbarHandler = nil;
  [self cleanUpToolbarConstraints];
  _sideSwipeCoordinator = nil;
  [_voiceSearchController disconnect];
  [_layoutState removeObserver:self];
  [_lensOverlayStateNotifier removeObserver:self];
  _layoutState = nil;
  [[NSNotificationCenter defaultCenter] removeObserver:self];
  _bookmarksCoordinator = nil;
  self.popupMenuCoordinator = nil;

  // Clears the pointer to C++ objects.
  _urlLoadingBrowserAgent = nullptr;
  _tabUsageRecorderBrowserAgent = nullptr;
  _snapshotBrowserAgent = nullptr;
}

#pragma mark - UIAccessibilityAction

- (BOOL)accessibilityPerformEscape {
  [self dismissPopups];
  return YES;
}

#pragma mark - UIResponder

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

- (UIResponder*)nextResponder {
  UIResponder* nextResponder = [super nextResponder];
  if (_keyCommandsProvider && [self shouldSupportKeyCommands]) {
    [_keyCommandsProvider respondBetweenViewController:self
                                          andResponder:nextResponder];
    return _keyCommandsProvider;
  } else {
    return nextResponder;
  }
}

#pragma mark - UIResponder Helpers

// Whether the BVC should declare keyboard commands.
// Since `-keyCommands` can be called by UIKit at any time, no assumptions
// about the state of `self` can be made; accordingly, if there's anything
// not initialized (or being torn down), this method should return NO.
- (BOOL)shouldSupportKeyCommands {
  if (_isShutdown) {
    return NO;
  }

  if (self.presentedViewController) {
    return NO;
  }

  return self.visibilityState == BrowserViewVisibilityState::kVisible;
}

#pragma mark - UIViewController

- (void)viewDidLoad {
  if (IsFullscreenRefactoringEnabled()) {
    self.view.translatesAutoresizingMaskIntoConstraints = NO;
  }
  [self registerNotifications];

  CGRect initialViewsRect = self.view.bounds;
  UIViewAutoresizing initialViewAutoresizing =
      UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;

  self.contentArea.frame = initialViewsRect;

  // Create the typing shield.  It is initially hidden, and is made visible when
  // the keyboard appears.
  if (!IsComposeboxIpadEnabled()) {
    self.typingShield = [[UIButton alloc] initWithFrame:initialViewsRect];
    self.typingShield.hidden = YES;
    self.typingShield.autoresizingMask = initialViewAutoresizing;
    self.typingShield.accessibilityIdentifier = @"Typing Shield";
    self.typingShield.accessibilityLabel = l10n_util::GetNSString(IDS_CANCEL);
    if (ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET) {
      self.typingShield.backgroundColor =
          [UIColor colorNamed:kOmniboxPopoutOverlayColor];
    }
    [self.typingShield addTarget:self
                          action:@selector(shieldWasTapped:)
                forControlEvents:UIControlEventTouchUpInside];
  }
  self.view.autoresizingMask = initialViewAutoresizing;

  [self addChildViewController:self.browserContentViewController];
  if (IsFullscreenRefactoringEnabled()) {
    self.contentArea.translatesAutoresizingMaskIntoConstraints = NO;
    [self.view addSubview:self.contentArea];
    AddSameConstraints(self.view, self.contentArea);
  } else {
    [self.view addSubview:self.contentArea];
  }
  [self.browserContentViewController didMoveToParentViewController:self];
  if (!IsComposeboxIpadEnabled()) {
    [self.view addSubview:self.typingShield];
  }
  [super viewDidLoad];

  [self buildToolbarAndTabStrip];
  [self setUpViewLayout:YES];
  [self addConstraintsToToolbar];

  [_sideSwipeCoordinator addHorizontalGesturesToView:self.view];

  // Add a tap gesture recognizer to save the last tap location for the source
  // location of the new tab animation.
  self.contentAreaGestureRecognizer = [[UITapGestureRecognizer alloc]
      initWithTarget:self
              action:@selector(saveContentAreaTapLocation:)];
  [self.contentAreaGestureRecognizer setDelegate:self];
  [self.contentAreaGestureRecognizer setCancelsTouchesInView:NO];
  [self.contentArea addGestureRecognizer:self.contentAreaGestureRecognizer];

  self.view.backgroundColor = [UIColor colorNamed:kBackgroundColor];
  if (_isOffTheRecord) {
    self.view.overrideUserInterfaceStyle = UIUserInterfaceStyleDark;
  }

  NSArray<UITrait>* traits = TraitCollectionSetForTraits(nil);
  __weak __typeof(self) weakSelf = self;
  UITraitChangeHandler handler = ^(id<UITraitEnvironment> traitEnvironment,
                                   UITraitCollection* previousCollection) {
    [weakSelf updateUIOnTraitChange:previousCollection];
  };
  [self registerForTraitChanges:traits withHandler:handler];
}

- (void)viewSafeAreaInsetsDidChange {
  [super viewSafeAreaInsetsDidChange];
  [self setUpViewLayout:NO];
  // Update the heights of the toolbars to account for the new insets.
  self.primaryToolbarHeightConstraint.constant =
      [self primaryToolbarHeightWithInset];

  CGFloat secondaryToolbarHeightWithInset =
      [self secondaryToolbarHeightWithInset];
  [self.toolbarCoordinator
      setBottomOmniboxOffsetForPopup:secondaryToolbarHeightWithInset];
  self.secondaryToolbarHeightConstraint.constant =
      secondaryToolbarHeightWithInset;
  if (IsChromeNextIaEnabled()) {
    [self updateForFullscreenProgress:self.footerFullscreenProgress];
  }
}

- (void)viewWillLayoutSubviews {
  [super viewWillLayoutSubviews];
  // Update the secondary toolbar bottom constraint once after the view is added
  // to a window. The window is required to accurately determine the App
  // Bar's position. This is done in `viewWillLayoutSubviews` rather than
  // `viewDidLayoutSubviews` so that Auto Layout can resolve the new constraints
  // in the upcoming pass, avoiding a redundant extra layout pass.
  if (IsFullscreenNextIAEnabled() && self.view.window) {
    [self addConstraintsToAppBar];
    [self updateToolbarConstraints];
    [self updateSecondaryToolbarBottomConstraint];
  }
}

- (void)viewDidLayoutSubviews {
  [super viewDidLayoutSubviews];
  // Update the toolbar height to account for `topLayoutGuide` changes.
  self.primaryToolbarHeightConstraint.constant =
      [self primaryToolbarHeightWithInset];

  if ([self topInsetWithCornerAdaptation] - self.rootSafeAreaInsets.top > 0) {
    // On iOS 26, the safe area layout guide doesn't automatically adjust
    // for the control setting island's dimensions.
    // Update the collapsedTopToolbarHeight when the dynamic island has moved.
    [self updateToolbarState];
  }

  if (!IsFullscreenRefactoringEnabled()) {
    if (self.ntpCoordinator.isNTPActiveForCurrentWebState &&
        self.webUsageEnabled) {
      self.ntpCoordinator.viewController.view.frame =
          [self ntpFrameForCurrentWebState];
    }
    if (!ios::provider::IsFullscreenSmoothScrollingSupported()) {
      if (!CGSizeEqualToSize(_lastViewSize, self.view.bounds.size)) {
        _lastViewSize = self.view.bounds.size;
        // When BVC view bounds size changes, UIKit automatically resizes the
        // WebView via autoresizing mask, shifting its y-position (e.g., from 62
        // to 77). In order to force Fullscreen to resize the WebView, we toggle
        // the toolbar height to zero and force a recalculation of its size.
        _toolbarsSize.expandedTopToolbarHeight = 0;
        [self updateToolbarState];
      }
    }
  }
}

- (void)viewDidAppear:(BOOL)animated {
  [super viewDidAppear:animated];
  self.visibilityState = BrowserViewVisibilityState::kVisible;
  [self updateBroadcastState];
  [self updateToolbarState];

  // If there is no first responder, try to make the webview the first
  // responder to have it answer keyboard commands (e.g. space bar to scroll
  // and respond to gampad controllers). The WKContentView must be the first
  // responder before (or very shortly after) a load starts in order for
  // gamepads to work. (Ref: crbug.com/325307469)
  web::WebState* activeWebState = self.currentWebState;
  if (activeWebState && !GetFirstResponder()) {
    if (!IsVisibleURLNewTabPage(activeWebState)) {
      [activeWebState->GetWebViewProxy() becomeFirstResponder];
    }
  }
}

- (void)viewWillAppear:(BOOL)animated {
  if (IsFullscreenRefactoringEnabled()) {
    [self.view.superview layoutIfNeeded];
  }
  [super viewWillAppear:animated];

  self.visibilityState = BrowserViewVisibilityState::kAppearing;

  // If the controller is suspended, or has been paged out due to low memory,
  // updating the view will be handled when it's displayed again.
  if (!self.webUsageEnabled || !self.contentArea) {
    return;
  }
  // Update the displayed view (if any; the switcher may not have created
  // one yet) in case it changed while showing the switcher.
  if (self.viewForCurrentWebState) {
    [self displayTabView];
  }
}

- (void)viewIsAppearing:(BOOL)animated {
  [super viewIsAppearing:animated];
  if (IsChromeNextIaEnabled()) {
    return;
  }
  [self.toolbarCoordinator updateToolbarPositionForActiveBrowser];
}

- (void)viewWillDisappear:(BOOL)animated {
  self.visibilityState = BrowserViewVisibilityState::kNotInViewHierarchy;
  [self updateBroadcastState];
  web::WebState* activeWebState = self.currentWebState;
  if (activeWebState) {
    [self updateWebStateVisibility:NO];
    if (!self.presentedViewController) {
      activeWebState->SetKeepRenderProcessAlive(false);
    }
  }

  [super viewWillDisappear:animated];
}

- (BOOL)prefersStatusBarHidden {
  return self.hideStatusBar || [super prefersStatusBarHidden];
}

// Called when in the foreground and the OS needs more memory. Release as much
// as possible.
- (void)didReceiveMemoryWarning {
  // Releases the view if it doesn't have a superview.
  [super didReceiveMemoryWarning];

  if (![self isViewLoaded]) {
    self.typingShield = nil;
    [self.toolbarCoordinator stop];
    self.toolbarCoordinator = nil;
    [self cleanUpToolbarConstraints];
    _toolbarsSize = nil;
    [_sideSwipeCoordinator stop];
    _sideSwipeCoordinator = nil;
  }
}

- (void)viewWillTransitionToSize:(CGSize)size
       withTransitionCoordinator:
           (id<UIViewControllerTransitionCoordinator>)coordinator {
  [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];

  // After `-shutdown` is called, browser is invalid and will cause a crash.
  if (_isShutdown) {
    return;
  }

  // TODO(crbug.com/40432185): Support size changes for all popups and modal
  // dialogs.
  [self.helpHandler hideAllHelpBubbles];

  crash_keys::SetCurrentOrientation(GetInterfaceOrientation(),
                                    [[UIDevice currentDevice] orientation]);

  if (IsFullscreenNextIAEnabled()) {
    __weak BrowserViewController* weakSelf = self;
    [coordinator
        animateAlongsideTransition:^(
            id<UIViewControllerTransitionCoordinatorContext>) {
          [weakSelf.popupMenuCommandsHandler adjustPopupSize];
          [weakSelf invalidateFullscreenInsets];
        }
                        completion:nil];
  } else {
    __weak BrowserViewController* weakSelf = self;
    [coordinator
        animateAlongsideTransition:^(
            id<UIViewControllerTransitionCoordinatorContext>) {
          [weakSelf animateTransition];
          [weakSelf invalidateFullscreenInsets];
        }
                        completion:nil];
  }
}

- (void)invalidateFullscreenInsets {
  if (!IsFullscreenRefactoringEnabled()) {
    return;
  }
  CHECK(_fullscreenBrowserAgent);
  // Reset the keyboard inset to 0 during rotation to avoid using a stale
  // portrait height on the smaller landscape frame, which might be larger
  // than the new viewport height.
  _fullscreenBrowserAgent->SetKeyboardObscuredInset(0);
  _fullscreenBrowserAgent->InvalidateInsetRange();
}

- (void)animateTransition {
  // Force updates of the toolbars' size as the toolbar height might
  // change on rotation.
  [self updateToolbarState];
  // Resize horizontal viewport if Smooth Scrolling is on.
  if (!IsFullscreenRefactoringEnabled() &&
      ios::provider::IsFullscreenSmoothScrollingSupported()) {
    self.fullscreenController->ResizeHorizontalViewport();
  }

  [self.popupMenuCommandsHandler adjustPopupSize];
}

- (void)dismissViewControllerAnimated:(BOOL)flag
                           completion:(void (^)())completion {
  self.dismissingModal = YES;
  self.visibilityState = BrowserViewVisibilityState::kVisible;

  __weak BrowserViewController* weakSelf = self;
  [super dismissViewControllerAnimated:flag
                            completion:^{
                              [weakSelf viewControllerDismissedWithCompletion:
                                            completion];
                            }];
}

// The BVC does not define its own presentation context, so any presentation
// here ultimately travels up the chain for presentation.
- (void)presentViewController:(UIViewController*)viewControllerToPresent
                     animated:(BOOL)flag
                   completion:(void (^)())completion {
  ProceduralBlock finalCompletionHandler = [completion copy];
  // TODO(crbug.com/41235932) This is an interim fix for the flicker between the
  // launch screen and the FRE Animation. The fix is, if the FRE is about to be
  // presented, to show a temporary view of the launch screen and then remove it
  // when the controller for the FRE has been presented. This fix should be
  // removed when the FRE startup code is rewritten.
  const bool firstRunLaunch = ShouldPresentFirstRunExperience();
  // These if statements check that `presentViewController` is being called for
  // the FRE case.
  if (firstRunLaunch &&
      [viewControllerToPresent isKindOfClass:[UINavigationController class]]) {
    UINavigationController* navController =
        base::apple::ObjCCastStrict<UINavigationController>(
            viewControllerToPresent);
    if ([navController.topViewController
            isKindOfClass:[PromoStyleViewController class]]) {
      self.hideStatusBar = YES;

      // Load view from Launch Screen and add it to window.
      NSBundle* mainBundle = base::apple::FrameworkBundle();
      UIStoryboard* storyboard =
          [UIStoryboard storyboardWithName:@"LaunchScreen" bundle:mainBundle];
      UIViewController* launchScreenController =
          [storyboard instantiateInitialViewController];
      // `launchScreenView` is loaded as an autoreleased object, and is retained
      // by the `completion` block below.
      UIView* launchScreenView = launchScreenController.view;
      launchScreenView.userInteractionEnabled = NO;
      // TODO(crbug.com/40101769): Displaying the launch screen is a hack to
      // hide the build up of the UI from the user. To implement the hack, this
      // view controller uses information that it should not know or care about:
      // this BVC is contained and its parent bounds to the full screen.
      launchScreenView.frame = self.parentViewController.view.bounds;
      [self.parentViewController addChildViewController:launchScreenController];
      [self.parentViewController.view addSubview:launchScreenView];
      [launchScreenController
          didMoveToParentViewController:self.parentViewController];
      [launchScreenView setNeedsLayout];
      [launchScreenView layoutIfNeeded];

      // Replace the completion handler sent to the superclass with one which
      // removes `launchScreenView` and resets the status bar. If `completion`
      // exists, it is called from within the new completion handler.
      __weak BrowserViewController* weakSelf = self;
      finalCompletionHandler = ^{
        [launchScreenView removeFromSuperview];
        weakSelf.hideStatusBar = NO;
        if (completion) {
          completion();
        }
      };
    }
  }

  [_sideSwipeCoordinator stopActiveSideSwipeAnimation];
  // TODO(crbug.com/406544789): Currently, some of the views are presented with
  // `browserViewController` but not dismissed with this, therefore we cannot
  // update the visibility state to `kCoveredByModal` without being sure that it
  // would be changed back to `kVisible` afterwards. Fix the bug and update the
  // visibility state.

  if (IsPageActionMenuEnabled()) {
    [self.geminiHandler
        hideFloatyIfInvokedAnimated:YES
                         fromSource:gemini::FloatyUpdateSource::ViewTransition];
  }
  void (^superCall)() = ^{
    [super presentViewController:viewControllerToPresent
                        animated:flag
                      completion:finalCompletionHandler];
  };
  // TODO(crbug.com/40628488): The Default Browser Promo is
  // currently the only presented controller that allows interaction with the
  // rest of the App while they are being presented. Dismiss it in case the
  // user or system has triggered another presentation.
  if ([self.nonModalPromoPresentationDelegate defaultNonModalPromoIsShowing]) {
    self.visibilityState = BrowserViewVisibilityState::kVisible;
    [self.nonModalPromoPresentationDelegate
        dismissDefaultNonModalPromoAnimated:NO
                                 completion:superCall];

  } else {
    superCall();
  }
}

- (BOOL)shouldAutorotate {
  if (self.presentedViewController.beingPresented ||
      self.presentedViewController.beingDismissed) {
    // Don't rotate while a presentation or dismissal animation is occurring.
    return NO;
  } else if (_sideSwipeCoordinator.swipeInProgress) {
    // Don't auto rotate if a side swipe is in progress.
    return NO;
  } else {
    return [super shouldAutorotate];
  }
}

- (UIStatusBarStyle)preferredStatusBarStyle {
  return _isOffTheRecord ? UIStatusBarStyleLightContent
                         : UIStatusBarStyleDefault;
}

#pragma mark - ** Private BVC Methods **

// Whether the browser view has appeared.
- (BOOL)appeared {
  return self.visibilityState !=
             BrowserViewVisibilityState::kNotInViewHierarchy &&
         self.visibilityState != BrowserViewVisibilityState::kAppearing;
}

// Register notifications to NSNotification center.
- (void)registerNotifications {
  NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
  [center addObserver:self
             selector:@selector(voiceSearchWillAppear)
                 name:kVoiceSearchWillShowNotification
               object:nil];
  [center addObserver:self
             selector:@selector(voiceSearchWillHide)
                 name:kVoiceSearchWillHideNotification
               object:nil];
}

// Builds the UI parts of tab strip and the toolbar. Does not matter whether
// or not profile and browser are valid.
- (void)buildToolbarAndTabStrip {
  DCHECK([self isViewLoaded]);

  [self updateBroadcastState];
}

// The height of the primary toolbar with the top safe area inset included.
- (CGFloat)primaryToolbarHeightWithInset {
  CGFloat height = self.toolbarCoordinator.expandedPrimaryToolbarHeight;
  // When the tab strip is active, it acts as the topmost header and covers
  // the safe area. The primary toolbar is laid out below it.
  if (!CanShowTabStrip(self)) {
    height += self.rootSafeAreaInsets.top;
  }

  return height;
}

// The height of the secondary toolbar with the bottom safe area inset included.
// Returns 0 if the toolbar should be hidden.
- (CGFloat)secondaryToolbarHeightWithInset {
  if (IsChromeNextIaEnabled()) {
    return self.toolbarCoordinator.expandedSecondaryToolbarHeight;
  }
  CGFloat height = self.toolbarCoordinator.expandedSecondaryToolbarHeight;
  if (!height) {
    return 0.0;
  }

  // Add the safe area inset to the toolbar height.
  CGFloat unsafeHeight = self.rootSafeAreaInsets.bottom;
  return height + unsafeHeight;
}

// Sets up the constraints on the toolbar.
- (void)addConstraintsToPrimaryToolbar {
  UIView* primaryView =
      self.toolbarCoordinator.primaryToolbarViewController.view;

  UIView* view = self.view;
  _toolbarLeftConstraint =
      [primaryView.leftAnchor constraintEqualToAnchor:view.leftAnchor];
  _toolbarRightConstraint =
      [primaryView.rightAnchor constraintEqualToAnchor:view.rightAnchor];
  [NSLayoutConstraint activateConstraints:@[
    _toolbarLeftConstraint,
    _toolbarRightConstraint,
  ]];
  [self updateToolbarConstraints];

  // Create a constraint for the vertical positioning of the toolbar.
  self.primaryToolbarOffsetConstraint =
      [primaryView.topAnchor constraintEqualToAnchor:view.topAnchor];
  self.primaryToolbarOffsetConstraint.constant = self.topToolbarInset;

  // Create a constraint for the height of the toolbar to include the unsafe
  // area height.
  self.primaryToolbarHeightConstraint = [primaryView.heightAnchor
      constraintEqualToConstant:[self primaryToolbarHeightWithInset]];

  self.primaryToolbarOffsetConstraint.active = YES;
  self.primaryToolbarHeightConstraint.active = YES;
}

// Updates the physical left and right constraints of the toolbar based on the
// App Bar position. Since `kLeft` and `kRight` correspond to physical screen
// boundaries regardless of layout direction, left and right offsets are
// updated directly.
- (void)updateToolbarConstraints {
  if (!IsFullscreenNextIAEnabled()) {
    return;
  }
  AppBarPosition position = self.layoutState.appBarPosition;
  switch (position) {
    case AppBarPosition::kLeft:
      _toolbarLeftConstraint.constant = AppBarHeightLandscape();
      _toolbarRightConstraint.constant = 0;
      break;
    case AppBarPosition::kRight:
      _toolbarLeftConstraint.constant = 0;
      _toolbarRightConstraint.constant = -AppBarHeightLandscape();
      break;
    default:
      _toolbarLeftConstraint.constant = 0;
      _toolbarRightConstraint.constant = 0;
      break;
  }
}

// Cleans up the toolbar constraints.
- (void)cleanUpToolbarConstraints {
  self.primaryToolbarOffsetConstraint.active = NO;
  self.primaryToolbarOffsetConstraint = nil;
  self.primaryToolbarHeightConstraint.active = NO;
  self.primaryToolbarHeightConstraint = nil;
  self.secondaryToolbarHeightConstraint.active = NO;
  self.secondaryToolbarHeightConstraint = nil;
}

- (void)addConstraintsToSecondaryToolbar {
  UIView* toolbarView =
      self.toolbarCoordinator.secondaryToolbarViewController.view;
  // Create a constraint for the height of the toolbar to include the unsafe
  CGFloat secondaryToolbarBaseHeight = [self secondaryToolbarHeightWithInset];
  self.secondaryToolbarHeightConstraint = [toolbarView.heightAnchor
      constraintEqualToConstant:secondaryToolbarBaseHeight];
  [self.toolbarCoordinator
      setBottomOmniboxOffsetForPopup:secondaryToolbarBaseHeight];
  // The bottom toolbar can be constraint to the keyboard in some cases.
  self.secondaryToolbarHeightConstraint.priority = UILayoutPriorityRequired - 1;
  self.secondaryToolbarHeightConstraint.active = YES;
  UIView* primaryToolbar =
      self.toolbarCoordinator.primaryToolbarViewController.view;
  AddSameConstraintsToSides(toolbarView, primaryToolbar,
                            LayoutSides::kHorizontal);

  if (IsFullscreenNextIAEnabled()) {
    // Create constraint for when the App Bar is not active or on the side
    // (Landscape).
    self.secondaryToolbarRegularBottomConstraint = [toolbarView.bottomAnchor
        constraintEqualToAnchor:self.view.bottomAnchor];
    self.secondaryToolbarRegularBottomConstraint.active =
        (self.layoutState.appBarPosition != AppBarPosition::kBottom);
  } else {
    [toolbarView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor]
        .active = YES;
  }
}

// Create constraint for when the App Bar is at the bottom (Portrait).
- (void)addConstraintsToAppBar {
  if (self.secondaryToolbarAppBarBottomConstraint) {
    return;
  }
  UIView* appBar = [_layoutGuideCenter referencedViewUnderName:kAppBarGuide];
  CHECK(appBar);
  UIView* toolbarView =
      self.toolbarCoordinator.secondaryToolbarViewController.view;
  self.secondaryToolbarAppBarBottomConstraint =
      [toolbarView.bottomAnchor constraintEqualToAnchor:appBar.topAnchor];
  [self updateSecondaryToolbarBottomConstraint];
}

// Adds constraints to the primary and secondary toolbars, anchoring them to the
// top and bottom of the browser view.
- (void)addConstraintsToToolbar {
  [self addConstraintsToPrimaryToolbar];
  [self addConstraintsToSecondaryToolbar];
  [[self view] layoutIfNeeded];
}

// Sets the correct frame and hierarchy for subviews and helper views.  Only
// insert views on `initialLayout`.
- (void)setUpViewLayout:(BOOL)initialLayout {
  DCHECK([self isViewLoaded]);

  if (initialLayout) {
    // Add the toolbars as child view controllers.
    UIViewController* primaryToolbarViewController =
        self.toolbarCoordinator.primaryToolbarViewController;
    [self addChildViewController:primaryToolbarViewController];

    UIViewController* secondaryToolbarViewController =
        self.toolbarCoordinator.secondaryToolbarViewController;
    [self addChildViewController:secondaryToolbarViewController];

    // Add the primary toolbar.
    UIView* primaryToolbarView = primaryToolbarViewController.view;
    [self.view addSubview:primaryToolbarView];
    [self.view insertSubview:secondaryToolbarViewController.view
                aboveSubview:primaryToolbarView];

    [primaryToolbarViewController didMoveToParentViewController:self];
    [secondaryToolbarViewController didMoveToParentViewController:self];

    // TODO(crbug.com/40270239): Migrate kContentAreaGuide to LayoutGuideCenter.
    // Add guide kContentAreaGuide to the browser view.
    [self.view
        addLayoutGuide:[[NamedGuide alloc] initWithName:kContentAreaGuide]];

    // Configure the content area guide.
    NamedGuide* contentAreaGuide = [NamedGuide guideWithName:kContentAreaGuide
                                                        view:self.view];

    // Sometimes, `contentAreaGuide` and `primaryToolbarView` aren't in the same
    // view hierarchy; this seems to be impossible,  but it does still happen.
    // This will cause an exception in when activiating these constraints. To
    // gather more information about this state, explciitly check the view
    // hierarchy roots. Local variables are used so that the CHECK message is
    // clearer.
    UIView* rootViewForToolbar = ViewHierarchyRootForView(primaryToolbarView);
    UIView* rootViewForContentGuide =
        ViewHierarchyRootForView(contentAreaGuide.owningView);
    CHECK_EQ(rootViewForToolbar, rootViewForContentGuide);

    // Constrain top to bottom of top toolbar.
    [contentAreaGuide.topAnchor
        constraintEqualToAnchor:primaryToolbarView.bottomAnchor]
        .active = YES;

    LayoutSides contentSides = LayoutSides::kHorizontal;
    // If there's a bottom toolbar, the content area guide is constrained to
    // its top.
    UIView* secondaryToolbarView =
        self.toolbarCoordinator.secondaryToolbarViewController.view;

    [contentAreaGuide.bottomAnchor
        constraintEqualToAnchor:secondaryToolbarView.topAnchor]
        .active = YES;

    AddSameConstraintsToSides(self.view, contentAreaGuide, contentSides);
  }

  // Resize the typing shield to cover the entire browser view and bring it to
  // the front.
  self.typingShield.frame = self.contentArea.frame;
  if (initialLayout) {
    [self.view bringSubviewToFront:self.typingShield];
  }
}

// Displays the current webState view.
- (void)displayTabView {
  UIView* view = self.viewForCurrentWebState;
  DCHECK(view);
  [self loadViewIfNeeded];

  if (!self.inNewTabAnimation) {
    // TODO(crbug.com/40842406): -updateToolbar will move out of the BVC; make
    // sure this comment remains accurate. Hide findbar.  `updateToolbar` will
    // restore the findbar later.
    [self.findInPageCommandsHandler hideFindUI];
    [self.textZoomHandler hideTextZoomUI];

    // Make new content visible, resizing it first as the orientation may
    // have changed from the last time it was displayed.
    CGRect viewFrame = self.contentArea.bounds;
    if (!IsFullscreenRefactoringEnabled() &&
        !ios::provider::IsFullscreenSmoothScrollingSupported()) {
      // If the Smooth Scrolling is on, the WebState view is not
      // resized, and should always match the bounds of the content area.  When
      // the provider is not initialized, viewport insets resize the webview, so
      // they should be accounted for here to prevent animation jitter.
      UIEdgeInsets viewportInsets =
          self.fullscreenController->GetCurrentViewportInsets();
      viewFrame = UIEdgeInsetsInsetRect(viewFrame, viewportInsets);
    }
    view.frame = viewFrame;

    [self updateToolbarState];

    NewTabPageCoordinator* NTPCoordinator = self.ntpCoordinator;
    if (NTPCoordinator.isNTPActiveForCurrentWebState) {
      UIViewController* viewController = NTPCoordinator.viewController;
      if (!IsFullscreenRefactoringEnabled()) {
        viewController.view.frame = [self ntpFrameForCurrentWebState];
        [viewController.view layoutIfNeeded];
      }
      // TODO(crbug.com/41407753): For a newly created WebState, the session
      // will not be restored until LoadIfNecessary call. Remove when fixed.
      self.currentWebState->GetNavigationManager()->LoadIfNecessary();
      self.browserContentViewController.contentView = nil;
      self.browserContentViewController.contentViewController = viewController;
      if (IsFullscreenRefactoringEnabled()) {
        viewController.view.translatesAutoresizingMaskIntoConstraints = NO;
        [self updateNTPConstraints];
      }
    } else {
      self.browserContentViewController.contentView = view;
      if (IsFullscreenRefactoringEnabled()) {
        view.translatesAutoresizingMaskIntoConstraints = NO;
        AddSameConstraints(self.browserContentViewController.view, view);
      }
      [self invalidateFullscreenInsets];
    }
    // Resize horizontal viewport if Smooth Scrolling is on.
    if (!IsFullscreenRefactoringEnabled() &&
        ios::provider::IsFullscreenSmoothScrollingSupported()) {
      self.fullscreenController->ResizeHorizontalViewport();
    }
  }

  // TODO(crbug.com/40842406): Remove this and let `ToolbarCoordinator` call the
  // update, somehow. Toolbar needs to know when NTP isActive state changes.
  [self.toolbarCoordinator updateToolbar];

  [self updateWebStateVisibility:YES];
  [self updateTabActivityReporting];
}

// Invoked when voice search shows.
- (void)voiceSearchWillAppear {
  self.visibilityState = BrowserViewVisibilityState::kCoveredByVoiceSearch;
}

// Invoked when voice search hides.
- (void)voiceSearchWillHide {
  self.visibilityState = BrowserViewVisibilityState::kVisible;
}

// Animates hiding and showing the typing shield.
- (void)animateTypingShieldHidden:(BOOL)hidden {
  if (self.typingShield.hidden == hidden) {
    return;
  }

  CGFloat finalAlpha = hidden ? 0.0 : 1.0;

  if (!hidden) {
    [self.typingShield setAlpha:0.0];
    [self.typingShield setHidden:NO];
  }

  [UIView animateWithDuration:0.3
      animations:^{
        [self.typingShield setAlpha:finalAlpha];
      }
      completion:^(BOOL finished) {
        if (!hidden) {
          // Already revealed before the animation started.
          return;
        }

        // This can happen if one quickly resigns the omnibox and then taps
        // on the omnibox again during this animation. If the animation is
        // interrupted and the toolbar controller is first responder, it's safe
        // to assume `self.typingShield` shouldn't be hidden here.
        if (!finished && [self.toolbarCoordinator isOmniboxFirstResponder]) {
          return;
        }

        [self.typingShield setHidden:YES];
      }];
}

// Calls `callback` for each edge that has a safe area inset.
- (void)getSafeAreaInsets:(void (^)(UIRectEdge edge, CGFloat amount))callback {
  callback(UIRectEdgeTop, [self topInset]);
  if (IsSplitToolbarMode(self) && !IsChromeNextIaEnabled() &&
      self.rootSafeAreaInsets.bottom > 0) {
    // Avoid adding the bottom safe area inset when Chrome Next is enabled
    // because the bottom UI elements report heights that already include the
    // safe area.
    callback(UIRectEdgeBottom, self.rootSafeAreaInsets.bottom);
  }
}

#pragma mark - Private Methods: UI Configuration, update and Layout

// Starts or stops broadcasting the toolbar UI and main content UI depending on
// whether the BVC is visible and active.
- (void)updateBroadcastState {
  self.broadcasting = self.active && [self appeared];
}

// Dismisses popups and modal dialogs that are displayed above the BVC when the
// accessibility escape gesture is performed.
- (void)dismissPopups {
  // The dispatcher may not be fully connected during shutdown, so selectors may
  // be unrecognized.
  if (_isShutdown) {
    return;
  }

  [self.popupMenuCommandsHandler dismissPopupMenuAnimated:NO];
  [self.helpHandler hideAllHelpBubbles];
  [_browserCoordinatorHandler hideComposebox];
}

// Updates the constraints for the NTP view.
- (void)updateNTPConstraints {
  CHECK(IsFullscreenRefactoringEnabled());
  UIViewController* NTPViewController = self.ntpCoordinator.viewController;
  if (!NTPViewController.isViewLoaded || !NTPViewController.view.superview) {
    return;
  }
  [NSLayoutConstraint deactivateConstraints:_NTPConstraints];
  DCHECK(self.ntpCoordinator.isNTPActiveForCurrentWebState);

  BOOL canShowTabStrip = CanShowTabStrip(self);
  BOOL isSplitToolbarMode = IsSplitToolbarMode(self);

  // Logic for pinning the top anchor to the UI view edge instead of the primary
  // toolbar.
  BOOL useTopViewEdge = NO;
  if (IsChromeNextIaEnabled()) {
    useTopViewEdge = !canShowTabStrip && !_isOffTheRecord;
  } else {
    useTopViewEdge = !canShowTabStrip && isSplitToolbarMode && !_isOffTheRecord;
  }

  // Logic for pinning the bottom anchor.
  // If ChromeNextIa is disabled, it always pins to the view edge.
  BOOL useBottomViewEdge = NO;
  if (IsChromeNextIaEnabled()) {
    useBottomViewEdge = !canShowTabStrip && !_isOffTheRecord;
  }

  NSLayoutYAxisAnchor* topAnchor =
      useTopViewEdge ? self.view.topAnchor
                     : self.toolbarCoordinator.primaryToolbarViewController.view
                           .bottomAnchor;

  NSLayoutYAxisAnchor* bottomAnchor =
      useBottomViewEdge ? self.view.bottomAnchor
                        : self.toolbarCoordinator.secondaryToolbarViewController
                              .view.topAnchor;

  UIView* primaryToolbarView =
      self.toolbarCoordinator.primaryToolbarViewController.view;
  _NTPConstraints = @[
    [NTPViewController.view.topAnchor constraintEqualToAnchor:topAnchor],
    [NTPViewController.view.bottomAnchor constraintEqualToAnchor:bottomAnchor],
    [NTPViewController.view.leadingAnchor
        constraintEqualToAnchor:primaryToolbarView.leadingAnchor],
    [NTPViewController.view.trailingAnchor
        constraintEqualToAnchor:primaryToolbarView.trailingAnchor],
  ];
  [NSLayoutConstraint activateConstraints:_NTPConstraints];
}

// Returns the appropriate frame for the NTP.
- (CGRect)ntpFrameForCurrentWebState {
  CHECK(!IsFullscreenRefactoringEnabled(), base::NotFatalUntil::M160);
  DCHECK(self.ntpCoordinator.isNTPActiveForCurrentWebState);
  // NTP is laid out only in the visible part of the screen.
  UIEdgeInsets viewportInsets = UIEdgeInsetsZero;

  BOOL canShowTabStrip = CanShowTabStrip(self);

  if (!IsChromeNextIaEnabled() || canShowTabStrip || _isOffTheRecord) {
    viewportInsets.bottom = [self secondaryToolbarHeightWithInset];
  }

  BOOL useTopToolbarEdgeAsInset = NO;
  if (IsChromeNextIaEnabled()) {
    // Toolbar is always visible when the tab strip can be shown or in
    // incognito.
    useTopToolbarEdgeAsInset = canShowTabStrip || _isOffTheRecord;
  } else {
    // Add toolbar margin to the frame for every scenario except compact-width
    // non-otr, as that is the only case where there isn't a primary toolbar.
    // (see crbug.com/1063173)
    useTopToolbarEdgeAsInset =
        canShowTabStrip || !IsSplitToolbarMode(self) || _isOffTheRecord;
  }

  if (useTopToolbarEdgeAsInset) {
    viewportInsets.top = [self expandedTopToolbarHeight];
  }

  return UIEdgeInsetsInsetRect(self.contentArea.bounds, viewportInsets);
}

// Sets the frame for the headers.
- (void)setFramesForHeaders:(NSArray<HeaderDefinition*>*)headers
                   atOffset:(CGFloat)offset {
  CGFloat height = self.topToolbarInset;
  for (HeaderDefinition* header in headers) {
    CGFloat yOrigin = height - offset;
    BOOL isPrimaryToolbar =
        header.view ==
        self.toolbarCoordinator.primaryToolbarViewController.view;
    // Make sure the toolbarView's constraints are also updated.  Leaving the
    // -setFrame call to minimize changes in this CL -- otherwise the way
    // toolbar_view manages it's alpha changes would also need to be updated.
    // TODO(crbug.com/40546808): This can be cleaned up when the new fullscreen
    // is enabled.
    if (isPrimaryToolbar) {
      self.primaryToolbarOffsetConstraint.constant = yOrigin;
    }
    CGRect frame = [header.view frame];
    frame.origin.y = yOrigin;
    [header.view setFrame:frame];
    if (header.behaviour != Overlap) {
      height += CGRectGetHeight(frame);
    }
  }
}

- (UIView*)viewForWebState:(web::WebState*)webState {
  if (!webState) {
    return nil;
  }
  if (self.ntpCoordinator.isNTPActiveForCurrentWebState) {
    return self.ntpCoordinator.started ? self.ntpCoordinator.viewController.view
                                       : nil;
  }
  DCHECK(self.webStateList->GetIndexOfWebState(webState) !=
         WebStateList::kInvalidIndex);
  if (webState->IsEvicted() && _tabUsageRecorderBrowserAgent) {
    _tabUsageRecorderBrowserAgent->RecordPageLoadStart(webState);
  }
  if (!webState->IsCrashed()) {
    // Load the page if it was evicted by browsing data clearing logic or if
    // page was never loaded yet after launch.
    webState->GetNavigationManager()->LoadIfNecessary();
  }
  return webState->GetView();
}

// Notifies or modifies BVC owned UI elements when a UITrait has been changed.
- (void)updateUIOnTraitChange:(UITraitCollection*)previousTraitCollection {
  if (!IsChromeNextIaEnabled() &&
      base::FeatureList::IsEnabled(kEnableTraitCollectionWorkAround)) {
    [self updateTraitsIfNeeded];
  }

  // After `-shutdown` is called, profile is invalid and will cause a
  // crash.
  if (_isShutdown) {
    return;
  }

  if (self.traitCollection.horizontalSizeClass ==
          previousTraitCollection.horizontalSizeClass &&
      self.traitCollection.verticalSizeClass ==
          previousTraitCollection.verticalSizeClass) {
    return;
  }

  if (IsFullscreenRefactoringEnabled() &&
      self.ntpCoordinator.isNTPActiveForCurrentWebState &&
      self.webUsageEnabled) {
    [self updateNTPConstraints];
  }

  self.fullscreenController->BrowserTraitCollectionChangedBegin();

  if (self.currentWebState) {
    UIEdgeInsets contentPadding =
        self.currentWebState->GetWebViewProxy().contentInset;
    contentPadding.bottom = AlignValueToLowerPixel(
        self.footerFullscreenProgress * [self secondaryToolbarHeightWithInset]);
    self.currentWebState->GetWebViewProxy().contentInset = contentPadding;
  }

  // Toolbars size must be updated before
  // `updateFootersForFullscreenProgress` as the later uses the insets from
  // fullscreen model.
  [self updateToolbarState];

  // Change the height of the secondary toolbar to show/hide it.
  self.secondaryToolbarHeightConstraint.constant =
      [self secondaryToolbarHeightWithInset];
  [self updateFootersForFullscreenProgress:self.footerFullscreenProgress];

  // If the device's size class has changed from RegularXRegular to another and
  // vice-versa, the find bar should switch between regular mode and compact
  // mode accordingly. Hide the findbar here and it will be reshown in [self
  // updateToobar];
  if (ShouldShowCompactToolbar(previousTraitCollection) !=
      ShouldShowCompactToolbar(self)) {
    [self.textZoomHandler hideTextZoomUI];
  }

  // Update the toolbar visibility.
  // TODO(crbug.com/40842406): Remove this and let
  // `PrimaryToolbarViewController` or `ToolbarCoordinator` call the update ?
  [self.toolbarCoordinator updateToolbar];
  if (IsPageActionMenuEnabled()) {
    [self.geminiHandler updateFloatyWithTraitCollection:self.traitCollection];
  }

  self.fullscreenController->BrowserTraitCollectionChangedEnd();
}

// On iOS 26, returns the top inset with corner adapation, otherwise returns 0.
- (CGFloat)topInsetWithCornerAdaptation {
  if (@available(iOS 26, *)) {
    UIViewLayoutRegion* safeAreaRegion =
        [UIViewLayoutRegion safeAreaLayoutRegionWithCornerAdaptation:
                                UIViewLayoutRegionAdaptivityAxisVertical];
    NSDirectionalEdgeInsets calculatedInsets = [self.safeAreaProvider
        directionalEdgeInsetsForLayoutRegion:safeAreaRegion];
    return calculatedInsets.top;
  }

  return 0;
}

// The top inset.
- (CGFloat)topInset {
  CGFloat topInset = self.rootSafeAreaInsets.top;

  // On iOS 26, the safe area layout guide doesn't automatically adjust for the
  // control setting island's dimensions.
  // If the app is windowed, the dynamic island is not included in the
  // status bar. In that case, `topInset` should be updated.
  CGFloat topInsetWithCornerAdaptation = [self topInsetWithCornerAdaptation];
  if (topInsetWithCornerAdaptation - topInset > 0) {
    topInset =
        fmax(topInset, topInsetWithCornerAdaptation - kTopDynamicIslandInset);
  }
  return topInset;
}

// Helper method for dismissing view controller with `completion` block.
- (void)viewControllerDismissedWithCompletion:(void (^)())completion {
  self.dismissingModal = NO;
  if (completion) {
    completion();
  }

  if (IsPageActionMenuEnabled()) {
    [self.geminiHandler
        updateFloatyVisibilityIfEligibleAnimated:NO
                                      fromSource:gemini::FloatyUpdateSource::
                                                     ViewTransition];
  }
}

#pragma mark - Private Methods: Tap handling

// Record the last tap point based on the `originPoint` (if any) passed in
// command.
- (void)setLastTapPointFromCommand:(CGPoint)originPoint {
  if (CGPointEqualToPoint(originPoint, CGPointZero)) {
    _lastTapPoint = CGPointZero;
  } else {
    _lastTapPoint = [self.view.window convertPoint:originPoint
                                            toView:self.view];
  }
  _lastTapTime = CACurrentMediaTime();
}

// Returns the last stored `_lastTapPoint` if it's been set within the past
// second.
- (CGPoint)lastTapPoint {
  if (CACurrentMediaTime() - _lastTapTime < 1) {
    return _lastTapPoint;
  }
  return CGPointZero;
}

// Store the tap CGPoint in `_lastTapPoint` and the current timestamp.
- (void)saveContentAreaTapLocation:(UIGestureRecognizer*)gestureRecognizer {
  if (_isShutdown) {
    return;
  }
  UIView* view = gestureRecognizer.view;
  CGPoint viewCoordinate = [gestureRecognizer locationInView:view];
  _lastTapPoint = [[view superview] convertPoint:viewCoordinate
                                          toView:self.view];
  _lastTapTime = CACurrentMediaTime();
  // Last tap timestamp will be consumed by `IdleService` if IdleTimeout
  // policy is set.
  GetApplicationContext()->GetLocalState()->SetTime(
      enterprise_idle::prefs::kLastActiveTimestamp, base::Time::Now());
}

#pragma mark - ** Protocol Implementations and Helpers **

#pragma mark - Helpers

- (UIEdgeInsets)snapshotEdgeInsetsForWebState:(web::WebState*)webState {
  UIEdgeInsets maxViewportInsets =
      self.fullscreenController->GetMaxViewportInsets();

  if (IsVisibleURLNewTabPage(webState)) {
    // If the NTP is active, then it's used as the base view for snapshotting.
    // When the tab strip is visible, or for the incognito NTP, the NTP is laid
    // out between the toolbars, so it should not be inset while snapshotting.
    if (CanShowTabStrip(self) || _isOffTheRecord) {
      return UIEdgeInsetsZero;
    }

    // For the regular NTP without tab strip, it sits above the bottom toolbar
    // but, since it is displayed as full-screen at the top, it requires maximum
    // viewport insets.
    maxViewportInsets.bottom = 0;
    return maxViewportInsets;
  } else {
    // If the NTP is inactive, the WebState's view is used as the base view for
    // snapshotting.  If fullscreen is implemented by resizing the scroll view,
    // then the WebState view is already laid out within the visible viewport
    // and doesn't need to be inset.  If fullscreen uses the content inset, then
    // the WebState view is laid out fullscreen and should be inset by the
    // viewport insets.
    return self.fullscreenController->ResizesScrollView() ? UIEdgeInsetsZero
                                                          : maxViewportInsets;
  }
}

#pragma mark - WebStateContainerViewProvider

- (UIView*)containerView {
  return self.contentArea;
}

- (CGPoint)dialogLocation {
  CGRect bounds = self.view.bounds;
  return CGPointMake(CGRectGetMidX(bounds),
                     CGRectGetMinY(bounds) + self.headerHeight);
}

#pragma mark - OmniboxPopupPresenterDelegate methods.

- (UIView*)popupParentViewForPresenter:(OmniboxPopupPresenter*)presenter {
  return self.view;
}

- (UIViewController*)popupParentViewControllerForPresenter:
    (OmniboxPopupPresenter*)presenter {
  return self;
}

- (UIColor*)popupBackgroundColorForPresenter:(OmniboxPopupPresenter*)presenter {
  ToolbarConfiguration* configuration = [[ToolbarConfiguration alloc]
      initWithStyle:_isOffTheRecord ? ToolbarStyle::kIncognito
                                    : ToolbarStyle::kNormal];
  return configuration.backgroundColor;
}

- (GuideName*)omniboxGuideNameForPresenter:(OmniboxPopupPresenter*)presenter {
  return kTopOmniboxGuide;
}

- (void)popupDidOpenForPresenter:(OmniboxPopupPresenter*)presenter {
  self.visibilityState = BrowserViewVisibilityState::kCoveredByOmniboxPopup;
  self.toolbarCoordinator.secondaryToolbarViewController.view
      .accessibilityElementsHidden = YES;

  if (_lensOverlayVisible) {
    // The typing shield has to be inserted right below the presented popup
    // omnibox to avoid being obstructed by the Lens Overlay.
    self.typingShield.frame = UIEdgeInsetsInsetRect(
        self.contentArea.bounds,
        UIEdgeInsetsMake([self expandedTopToolbarHeight], 0, 0, 0));
    [self.view insertSubview:self.typingShield
                belowSubview:presenter.popupContainerView];
    [self animateTypingShieldHidden:NO];
  }
}

- (void)popupDidCloseForPresenter:(OmniboxPopupPresenter*)presenter {
  self.visibilityState = BrowserViewVisibilityState::kVisible;
  self.toolbarCoordinator.secondaryToolbarViewController.view
      .accessibilityElementsHidden = NO;
  if (IsPageActionMenuEnabled()) {
    [self.geminiHandler
        updateFloatyVisibilityIfEligibleAnimated:NO
                                      fromSource:gemini::FloatyUpdateSource::
                                                     ViewTransition];
  }
}

#pragma mark - FullscreenUIElement methods

- (void)updateForFullscreenProgress:(CGFloat)progress {
  if (_isShutdown) {
    return;
  }
  [self updateHeadersForFullscreenProgress:progress];
  [self updateFootersForFullscreenProgress:progress];
  if (!IsFullscreenRefactoringEnabled() &&
      !ios::provider::IsFullscreenSmoothScrollingSupported()) {
    [self updateBrowserViewportForFullscreenProgress:progress];
  }
}

- (void)updateForFullscreenEnabled:(BOOL)enabled {
  if (!enabled) {
    [self updateForFullscreenProgress:1.0];
  }
}

- (void)animateFullscreenWithAnimator:(FullscreenAnimator*)animator {
  // If the headers are being hidden, it's possible that this will reveal a
  // portion of the webview beyond the top of the page's rendered content.  In
  // order to prevent that, update the top padding and content before the
  // animation begins.
  CGFloat finalProgress = animator.finalProgress;
  BOOL hidingHeaders = animator.finalProgress < animator.startProgress;
  if (hidingHeaders) {
    id<CRWWebViewProxy> webProxy = self.currentWebState->GetWebViewProxy();
    CRWWebViewScrollViewProxy* scrollProxy = webProxy.scrollViewProxy;
    CGPoint contentOffset = scrollProxy.contentOffset;
    if (contentOffset.y - scrollProxy.contentInset.top <
        webProxy.contentInset.top) {
      [self updateBrowserViewportForFullscreenProgress:finalProgress];
      contentOffset.y = -scrollProxy.contentInset.top;
      scrollProxy.contentOffset = contentOffset;
    }
  }

  // Add animations to update the headers and footers.
  __weak BrowserViewController* weakSelf = self;
  [animator addAnimations:^{
    [weakSelf updateHeadersForFullscreenProgress:finalProgress];
    [weakSelf updateFootersForFullscreenProgress:finalProgress];
    // This animation can be canceled in the middle, and there is no way to
    // know when this happens. Setting `fullscreenProgressDirty` will force
    // a layout to happen on the next fullscreen update.
    weakSelf.fullscreenProgressDirty = YES;
  }];

  // Animating layout changes of the rendered content in the WKWebView is not
  // supported, so update the content padding in the completion block of the
  // animator to trigger a rerender in the page's new viewport.
  __weak FullscreenAnimator* weakAnimator = animator;
  [animator addCompletion:^(UIViewAnimatingPosition finalPosition) {
    [weakSelf updateBrowserViewportForFullscreenProgress:
                  [weakAnimator progressForAnimatingPosition:finalPosition]];
  }];
}

#pragma mark - FullscreenBrowserAgentObserving

- (void)fullscreenWillUpdateObscuredInsetRange:(FullscreenBrowserAgent*)agent {
  CHECK(IsFullscreenRefactoringEnabled());

  [self getSafeAreaInsets:^(UIRectEdge edge, CGFloat amount) {
    CGFloat min = amount, max = amount;
    if (edge == UIRectEdgeBottom &&
        [self collapsedBottomToolbarHeight] == 0.0) {
      // If bottom toolbar collapses completely, then the safe area inset should
      // collapse also.
      min = 0;
    }
    agent->AddObscuredInsetRange(edge, min, max);
  }];
}

- (void)fullscreenWillUpdateState:(FullscreenBrowserAgent*)agent {
  CHECK(IsFullscreenRefactoringEnabled());
  [self updateHeadersForFullscreenProgress:agent->top_progress()];
  [self updateFootersForFullscreenProgress:agent->bottom_progress()];

  [self getSafeAreaInsets:^(UIRectEdge edge, CGFloat amount) {
    if (edge == UIRectEdgeBottom) {
      if ([self collapsedBottomToolbarHeight] == 0.0) {
        amount *= agent->bottom_progress();
      }
    }
    agent->AddObscuredInset(edge, amount);
  }];
}

- (void)fullscreenDidUpdateState:(FullscreenBrowserAgent*)agent {
  // If this is inside an animation, layout immediately.
  if (!agent->animation_duration().is_zero()) {
    [self.view layoutIfNeeded];
  }
}

#pragma mark - FullscreenUIElement helpers

// The minimum amount by which the top toolbar overlaps the browser content
// area.
- (CGFloat)collapsedTopToolbarHeight {
  return
      [self topInset] + self.toolbarCoordinator.collapsedPrimaryToolbarHeight;
}

// The minimum amount by which the bottom toolbar overlaps the browser content
// area.
- (CGFloat)collapsedBottomToolbarHeight {
  if (IsChromeNextIaEnabled()) {
    return self.toolbarCoordinator.collapsedSecondaryToolbarHeight;
  }
  CGFloat height = self.toolbarCoordinator.collapsedSecondaryToolbarHeight;
  if (!height) {
    return 0.0;
  }
  // Height is non-zero only when bottom omnibox is enabled.
  return self.rootSafeAreaInsets.bottom + height;
}

// The maximum amount by which the top toolbar overlaps the browser content
// area.
- (CGFloat)expandedTopToolbarHeight {
  return [self primaryToolbarHeightWithInset] + self.topToolbarInset;
}

// Updates the ToolbarsSize, which broadcasts any changes to registered
// listeners.
- (void)updateToolbarState {
  if (IsRefactorToolbarsSize()) {
    [_toolbarsSize
        setCollapsedTopToolbarHeight:[self collapsedTopToolbarHeight]
            expandedTopToolbarHeight:[self expandedTopToolbarHeight]
         expandedBottomToolbarHeight:[self secondaryToolbarHeightWithInset]
        collapsedBottomToolbarHeight:[self collapsedBottomToolbarHeight]];
  } else {
    _toolbarsSize.collapsedTopToolbarHeight = [self collapsedTopToolbarHeight];
    _toolbarsSize.expandedTopToolbarHeight = [self expandedTopToolbarHeight];
    _toolbarsSize.collapsedBottomToolbarHeight =
        [self collapsedBottomToolbarHeight];
    _toolbarsSize.expandedBottomToolbarHeight =
        [self secondaryToolbarHeightWithInset];
  }

  if (IsFullscreenNextIAEnabled()) {
    [self updateSecondaryToolbarBottomConstraint];
  }
}

// Updates the bottom constraint of the secondary toolbar depending on the
// AppBar's position.
- (void)updateSecondaryToolbarBottomConstraint {
  if (!self.isViewLoaded || !self.view.window) {
    return;
  }

  BOOL shouldUseAppBar =
      (self.layoutState.appBarPosition == AppBarPosition::kBottom) &&
      (self.secondaryToolbarAppBarBottomConstraint != nil);

  if (shouldUseAppBar) {
    // Sometimes, `appBar` and `toolbarView` aren't in the same view hierarchy,
    // which causes an exception when activating
    // `secondaryToolbarAppBarBottomConstraint`. Ensure they share a common
    // ancestor view before activating the constraint.
    UIView* appBar = [_layoutGuideCenter referencedViewUnderName:kAppBarGuide];
    UIView* toolbarView =
        self.toolbarCoordinator.secondaryToolbarViewController.view;
    if (appBar && toolbarView &&
        ViewHierarchyRootForView(appBar) ==
            ViewHierarchyRootForView(toolbarView)) {
      self.secondaryToolbarRegularBottomConstraint.active = NO;
      self.secondaryToolbarAppBarBottomConstraint.active = YES;
      return;
    }
  }

  self.secondaryToolbarAppBarBottomConstraint.active = NO;
  self.secondaryToolbarRegularBottomConstraint.active = YES;
}

// Returns the height difference between the fully expanded and fully collapsed
// primary toolbar.
- (CGFloat)primaryToolbarHeightDelta {
  if (IsFullscreenRefactoringEnabled()) {
    return std::max(0.0, _fullscreenBrowserAgent->max_insets().top -
                             _fullscreenBrowserAgent->min_insets().top);
  }
  CGFloat fullyExpandedHeight =
      self.fullscreenController->GetMaxViewportInsets().top;
  CGFloat fullyCollapsedHeight =
      self.fullscreenController->GetMinViewportInsets().top;
  return std::max(0.0, fullyExpandedHeight - fullyCollapsedHeight);
}

// Returns the height difference between the fully expanded and fully collapsed
// secondary toolbar.
- (CGFloat)secondaryToolbarHeightDelta {
  if (IsFullscreenRefactoringEnabled()) {
    return std::max(0.0, _fullscreenBrowserAgent->max_insets().bottom -
                             _fullscreenBrowserAgent->min_insets().bottom);
  }
  CGFloat fullyExpandedHeight =
      self.fullscreenController->GetMaxViewportInsets().bottom;
  CGFloat fullyCollapsedHeight =
      self.fullscreenController->GetMinViewportInsets().bottom;
  return std::max(0.0, fullyExpandedHeight - fullyCollapsedHeight);
}

// Translates the header views up and down according to `progress`, where a
// progress of 1.0 fully shows the headers and a progress of 0.0 fully hides
// them.
- (void)updateHeadersForFullscreenProgress:(CGFloat)progress {
  CGFloat offset = AlignValueToLowerPixel((1.0 - progress) *
                                          [self primaryToolbarHeightDelta]);
  [self setFramesForHeaders:[self headerViews] atOffset:offset];
}

// Resizes the secondary toolbar according to `progress`, where a progress of
// 1.0 fully expands the toolbar and a progress of 0.0 collapses it.
- (void)updateNextIASecondaryToolbarForFullscreenProgress:(CGFloat)progress {
  // Early return if the toolbar is currently managed by keyboard avoidance.
  if (_isSecondaryToolbarAboveKeyboard) {
    return;
  }

  const CGFloat expandedHeight = [self secondaryToolbarHeightWithInset];
  if (expandedHeight <= 0.0) {
    return;
  }

  CGFloat height = expandedHeight;
  if (IsAppBarHiddenInFullscreen() &&
      self.layoutState.appBarPosition == AppBarPosition::kBottom) {
    CGFloat collapsedHeightWithSafeArea = [self collapsedBottomToolbarHeight];
    CGFloat targetHeight =
        collapsedHeightWithSafeArea +
        progress * (expandedHeight - collapsedHeightWithSafeArea);
    height = AlignValueToLowerPixel(targetHeight);
  } else {
    const CGFloat isolatedDelta =
        std::max(0.0, expandedHeight - [self collapsedBottomToolbarHeight]);
    const CGFloat offset =
        AlignValueToLowerPixel((1.0 - progress) * isolatedDelta);
    height = expandedHeight - offset;
  }

  self.secondaryToolbarHeightConstraint.constant = height;
}

// Translates the footer view up and down according to `progress`, where a
// progress of 1.0 fully shows the footer and a progress of 0.0 fully hides it.
- (void)updateFootersForFullscreenProgress:(CGFloat)progress {
  self.footerFullscreenProgress = progress;

  if (IsChromeNextIaEnabled()) {
    [self updateNextIASecondaryToolbarForFullscreenProgress:progress];
    return;
  }

  // Don't update the height of the secondary toolbar if it is hidden.
  if (!IsSplitToolbarMode(self)) {
    return;
  }

  CGFloat expandedToolbarHeight;
  if (IsFullscreenRefactoringEnabled()) {
    expandedToolbarHeight = _fullscreenBrowserAgent->max_insets().bottom;
  } else {
    expandedToolbarHeight =
        self.fullscreenController->GetMaxViewportInsets().bottom;
  }
  if (!expandedToolbarHeight) {
    // If `expandedToolbarHeight` is 0, secondary toolbar is hidden. In that
    // case don't update it's height on fullscreen progress.
    return;
  }

  const CGFloat offset = AlignValueToLowerPixel(
      (1.0 - progress) * [self secondaryToolbarHeightDelta]);
  // Update the height constraint and force a layout on the container view
  // so that the update is animatable.
  const CGFloat height = expandedToolbarHeight - offset;
  // Check that the computed height has a realistic value (crbug.com/1446068).
  DUMP_WILL_BE_CHECK(height >= (0.0 - FLT_EPSILON) &&
                     height <= (expandedToolbarHeight + FLT_EPSILON));
  if (IsChromeNextIaEnabled() ||
      ![self.toolbarCoordinator showingOmniboxPopup]) {
    if (self.secondaryToolbarHeightConstraint.constant != height) {
      self.secondaryToolbarHeightConstraint.constant = height;
      // Force a layout to cause the frame to be recalculated.
      if ([self isViewLoaded]) {
        UIView* view = self.view;
        [view setNeedsLayout];
        [view layoutIfNeeded];
      }
    } else if (self.fullscreenProgressDirty) {
      CHECK(!IsFullscreenRefactoringEnabled());
      UIView* view = self.view;
      [view setNeedsLayout];
      [view layoutIfNeeded];
      self.fullscreenProgressDirty = NO;
    }
  }
}

// Updates the browser container view such that its viewport is the space
// between the primary and secondary toolbars.
- (void)updateBrowserViewportForFullscreenProgress:(CGFloat)progress {
  if (!self.currentWebState) {
    return;
  }

  // Calculate the heights of the toolbars for `progress`.  `-toolbarHeight`
  // returns the height of the toolbar extending below this view controller's
  // safe area, so the unsafe top height must be added.
  CGFloat top = AlignValueToLowerPixel(
      self.headerHeight + (progress - 1.0) * [self primaryToolbarHeightDelta]);
  CGFloat bottom = AlignValueToLowerPixel(
      [self secondaryToolbarHeightWithInset] +
      (progress - 1.0) * [self secondaryToolbarHeightDelta]);

  [self updateContentPaddingForTopToolbarHeight:top bottomToolbarHeight:bottom];
}

// Updates the padding of the web view proxy. This either resets the frame of
// the WKWebView or the contentInsets of the WKWebView's UIScrollView, depending
// on the the proxy's `shouldUseViewContentInset` property.
- (void)updateContentPaddingForTopToolbarHeight:(CGFloat)topToolbarHeight
                            bottomToolbarHeight:(CGFloat)bottomToolbarHeight {
  if (!self.currentWebState) {
    return;
  }

  id<CRWWebViewProxy> webViewProxy = self.currentWebState->GetWebViewProxy();
  UIEdgeInsets contentPadding = webViewProxy.contentInset;
  contentPadding.top = topToolbarHeight;
  contentPadding.bottom = bottomToolbarHeight;
  webViewProxy.contentInset = contentPadding;
}

#pragma mark - MainContentUI

- (MainContentUIState*)mainContentUIState {
  return _mainContentUIUpdater.state;
}

#pragma mark - OmniboxFocusDelegate (Public)

- (void)omniboxDidBecomeFirstResponder {
  CHECK(!IsComposeboxIOSEnabled());
  if (self.ntpCoordinator.isNTPActiveForCurrentWebState) {
    [self.ntpCoordinator locationBarDidBecomeFirstResponder];
  }
  [_sideSwipeCoordinator setEnabled:NO];

  if (!IsVisibleURLNewTabPage(self.currentWebState) ||
      ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET) {
    // Tapping on web content area should dismiss the keyboard. Tapping on NTP
    // gesture should propagate to NTP view.

    if (self.typingShield.hidden) {
      [self.view insertSubview:self.typingShield aboveSubview:self.contentArea];
      [self animateTypingShieldHidden:NO];
    }
  }

  [self.toolbarCoordinator transitionToLocationBarFocusedState:YES
                                                    completion:nil];
}

- (void)omniboxDidResignFirstResponder {
  // NO-OP
}

- (void)omniboxDidEndEditing {
  CHECK(!IsComposeboxIOSEnabled());
  [_sideSwipeCoordinator setEnabled:YES];

  [self.ntpCoordinator locationBarWillResignFirstResponder];

  [self animateTypingShieldHidden:YES];

  ProceduralBlock completion = ^{
    // Show the NTP's fake toolbar after the defocus animation completes.
    [self.ntpCoordinator locationBarDidResignFirstResponder];
  };

  [self.toolbarCoordinator transitionToLocationBarFocusedState:NO
                                                    completion:completion];
}

#pragma mark - BrowserCommands

- (void)dismissSoftKeyboard {
  DCHECK(self.visibilityState !=
             BrowserViewVisibilityState::kNotInViewHierarchy ||
         self.dismissingModal);
  [self.viewForCurrentWebState endEditing:NO];
}

#pragma mark - TabConsumer (Public)

- (void)resetTab {
  self.browserContentViewController.contentView = nil;
}

- (void)prepareForNewTabAnimation {
  [self dismissPopups];
}

- (void)webStateSelected {
  // Ignore changes while the tab stack view is visible (or while suspended).
  // The display will be refreshed when this view becomes active again.
  if (self.visibilityState == BrowserViewVisibilityState::kNotInViewHierarchy ||
      !self.webUsageEnabled) {
    return;
  }

  if (!self.viewForCurrentWebState) {
    return;
  }

  [self displayTabView];
}

- (void)initiateNewTabForegroundAnimationForWebState:(web::WebState*)webState {
  BOOL isNTP = IsURLNewTabPage(webState->GetVisibleURL());
  BOOL isIncognito = _isOffTheRecord;
  __weak id<ToolbarCommands> toolbarHandler = _toolbarHandler;

  // Initiates the new tab foreground animation, which is phone-specific.
  if (CanShowTabStrip(self)) {
    if (self.foregroundTabWasAddedCompletionBlock) {
      // This callback is called before webState is activated. Dispatch the
      // callback asynchronously to be sure the activation is complete.
      __weak BrowserViewController* weakSelf = self;
      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(^{
            if (isNTP && isIncognito) {
              [toolbarHandler focusLocationBarForVoiceOver];
            }

            [weakSelf executeAndClearForegroundTabWasAddedCompletionBlock:YES];
          }));
    } else {
      if (isNTP && isIncognito) {
        [toolbarHandler focusLocationBarForVoiceOver];
      }
    }
    return;
  }
  // Do nothing if browsing is currently suspended.  The BVC will set everything
  // up correctly when browsing resumes.
  if (self.visibilityState == BrowserViewVisibilityState::kNotInViewHierarchy ||
      !self.webUsageEnabled) {
    return;
  }

  self.inNewTabAnimation = YES;
  __weak __typeof(self) weakSelf = self;
  [self animateNewTabForWebState:webState
      inForegroundWithCompletion:^{
        [weakSelf startVoiceSearchIfNecessary];
      }];
}

- (void)initiateNewTabBackgroundAnimation {
  if (self.foregroundTabWasAddedCompletionBlock) {
    // This callback is called before webState is activated. Dispatch the
    // callback asynchronously to be sure the activation is complete.
    __weak BrowserViewController* weakSelf = self;
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, base::BindOnce(^{
          [weakSelf executeAndClearForegroundTabWasAddedCompletionBlock:NO];
        }));
  }
  self.inNewTabAnimation = NO;
}

- (void)displayTabViewIfActive {
  if (self.active) {
    [self displayTabView];
  }
}

- (void)switchToTabWithWebState:(web::WebState*)webState
              animationPosition:(SwitchToTabAnimationPosition)position
                topToolbarImage:(UIImage*)topToolbarImage
             bottomToolbarImage:(UIImage*)bottomToolbarImage {
  if (CanShowTabStrip(self)) {
    return;
  }

  // Add animations only if the tab strip isn't shown.
  UIView* snapshotView = [self.view snapshotViewAfterScreenUpdates:NO];

  SwipeView* swipeView = [[SwipeView alloc]
      initWithFrame:self.contentArea.frame
          topMargin:[self snapshotEdgeInsetsForWebState:webState].top
       bottomMargin:[self snapshotEdgeInsetsForWebState:webState].bottom];

  [swipeView setTopToolbarImage:topToolbarImage];
  [swipeView setBottomToolbarImage:bottomToolbarImage];

  const BOOL willAddPlaceholder =
      PagePlaceholderBrowserAgent::IsPagePlaceholderPlannedForWebState(
          webState);

  _snapshotBrowserAgent->RetrieveSnapshotWithID(
      SnapshotID(webState->GetUniqueIdentifier()), SnapshotKindColor,
      ^(UIImage* image) {
        [swipeView setImage:(willAddPlaceholder ? nil : image)];
      });

  SwitchToTabAnimationView* animationView =
      [[SwitchToTabAnimationView alloc] initWithFrame:self.view.bounds];
  [self.view addSubview:animationView];

  [animationView animateFromCurrentView:snapshotView
                              toNewView:swipeView
                             inPosition:position];
}

- (void)dismissBookmarkModalController {
  [_bookmarksCoordinator dismissBookmarkModalControllerAnimated:YES];
}

#pragma mark - TabConsumer helpers

// Helper which execute and then clears `foregroundTabWasAddedCompletionBlock`
// if it is still set, or does nothing.
- (void)executeAndClearForegroundTabWasAddedCompletionBlock:(BOOL)animated {
  // Test existence again as the block may have been deleted.
  ProceduralBlock completion = self.foregroundTabWasAddedCompletionBlock;
  if (!completion) {
    return;
  }

  // Clear the property before executing the completion, in case the
  // completion calls appendTabAddedCompletion:tabAddedCompletion.
  // Clearing the property after running the completion would cause any
  // newly appended completion to be immediately cleared without ever
  // getting run. An example where this would happen is when opening
  // multiple tabs via the "Open URLs in Chrome" Siri Shortcut.
  self.foregroundTabWasAddedCompletionBlock = nil;
  if (animated) {
    completion();
  } else {
    [UIView performWithoutAnimation:^{
      completion();
    }];
  }
}

// Helper which starts voice search at the end of new Tab animation if
// necessary.
- (void)startVoiceSearchIfNecessary {
  if (_startVoiceSearchAfterNewTabAnimation) {
    _startVoiceSearchAfterNewTabAnimation = NO;
    [self startVoiceSearch];
    [IntentDonationHelper donateIntent:IntentType::kOpenVoiceSearch];
  }
}

// Returns the frame for the new tab page view for the given web state.
- (CGRect)newPageFrameForWebState:(web::WebState*)webState {
  CHECK(!IsFullscreenRefactoringEnabled(), base::NotFatalUntil::M160);
  GURL tabURL = webState->GetVisibleURL();
  BOOL isNTP = tabURL == kChromeUINewTabURL;

  // The frame should cover the visible space: either the full view if it is a
  // non-incognito NTP or the area between the toolbars.
  if (isNTP && !_isOffTheRecord && !CanShowTabStrip(self)) {
    return self.view.bounds;
  }
  if (self.ntpCoordinator.isNTPActiveForCurrentWebState &&
      self.webUsageEnabled) {
    return [self ntpFrameForCurrentWebState];
  }
  return self.contentArea.bounds;
}

// Returns the frame for the foreground tab animation view.
- (CGRect)foregroundTabAnimationViewFrameForWebState:(web::WebState*)webState {
  CHECK(!CanShowTabStrip(self), base::NotFatalUntil::M160);

  GURL tabURL = webState->GetVisibleURL();
  BOOL isNTP = tabURL == kChromeUINewTabURL;

  UIEdgeInsets insets = UIEdgeInsetsZero;
  // On iPhone landscape, the AppBar is covering part of screen under which the
  // NTP is not displayed.
  if (isNTP && !CanShowTabStrip(self) && IsChromeNextIaEnabled()) {
    AppBarPosition position = self.layoutState.appBarPosition;

    if (position == AppBarPosition::kLeft) {
      insets.left = AppBarHeightLandscape();
    } else if (position == AppBarPosition::kRight) {
      insets.right = AppBarHeightLandscape();
    }
  }

  if (isNTP && _isOffTheRecord) {
    if (IsChromeNextIaEnabled()) {
      insets.bottom = [self secondaryToolbarHeightWithInset];
      insets.top = [self expandedTopToolbarHeight];
      if (self.layoutState.appBarPosition == AppBarPosition::kBottom) {
        insets.bottom += AppBarHeightPortrait();
      }
    } else {
      insets.top = [self expandedTopToolbarHeight];
      insets.bottom = [self secondaryToolbarHeightWithInset];
    }
  }
  CGRect frameInView = UIEdgeInsetsInsetRect(self.view.bounds, insets);
  return [self.contentArea convertRect:frameInView fromView:self.view];
}

// Completes the new tab animation.
- (void)completeNewTabAnimationWithNewPage:(UIView*)newPage
                              webStateView:(UIView*)webStateView
                                identifier:(NSInteger)identifier
                                     isNTP:(BOOL)isNTP
                               isIncognito:(BOOL)isIncognito
                                completion:(ProceduralBlock)completion {
  newPage.userInteractionEnabled = YES;
  if (IsFullscreenRefactoringEnabled()) {
    newPage.translatesAutoresizingMaskIntoConstraints = NO;
  }

  // Do not resize the same view.
  if (webStateView != newPage) {
    webStateView.frame = self.contentArea.bounds;
  }

  if (identifier != _NTPAnimationIdentifier) {
    // Prevent the completion block from being executed if a new animation has
    // started in between. `self.foregroundTabWasAddedCompletionBlock` isn't
    // called because it is overridden when a new animation is started.
    // Calling it here would call the block from the lastest animation that
    // haved started.
    return;
  }

  self.inNewTabAnimation = NO;

  [self webStateSelected];
  if (completion) {
    completion();
  }

  if (isNTP && isIncognito) {
    [_toolbarHandler focusLocationBarForVoiceOver];
  }

  [self executeAndClearForegroundTabWasAddedCompletionBlock:YES];
}

- (void)animateNewTabForWebState:(web::WebState*)webState
      inForegroundWithCompletion:(ProceduralBlock)completion {
  CHECK(!CanShowTabStrip(self), base::NotFatalUntil::M160);

  // Create the new page image, and load with the new tab snapshot except if
  // it is the NTP.
  UIView* newPage = [self viewForWebState:webState];
  DCHECK(newPage);
  if (IsFullscreenRefactoringEnabled()) {
    newPage.translatesAutoresizingMaskIntoConstraints = YES;
  }
  GURL tabURL = webState->GetVisibleURL();
  // Toolbar snapshot is only used for the UIRefresh animation.
  UIView* toolbarSnapshot;

  BOOL isNTP = tabURL == kChromeUINewTabURL;
  BOOL isIncognito = _isOffTheRecord;
  BOOL useDeviceCornerRadius = NO;

  if (IsFullscreenRefactoringEnabled()) {
    newPage.frame = [self foregroundTabAnimationViewFrameForWebState:webState];
  }

  if (isNTP && !isIncognito && !CanShowTabStrip(self)) {
    // Add a snapshot of the primary toolbar to the background as the
    // animation runs.
    UIViewController* toolbarViewController =
        self.toolbarCoordinator.primaryToolbarViewController;
    toolbarSnapshot =
        [toolbarViewController.view snapshotViewAfterScreenUpdates:NO];
    toolbarSnapshot.frame = [self.contentArea convertRect:toolbarSnapshot.frame
                                                 fromView:self.view];
    [self.contentArea addSubview:toolbarSnapshot];
    // `newPage` takes the full screen and the corner radius should be the same
    // as the device's.
    useDeviceCornerRadius = YES;
  }

  newPage.userInteractionEnabled = NO;
  NSInteger currentAnimationIdentifier = ++_NTPAnimationIdentifier;

  UIView* webStateView = [self viewForWebState:webState];
  __weak __typeof(self) weakSelf = self;
  auto commonCompletion = ^{
    [weakSelf completeNewTabAnimationWithNewPage:newPage
                                    webStateView:webStateView
                                      identifier:currentAnimationIdentifier
                                           isNTP:isNTP
                                     isIncognito:isIncognito
                                      completion:completion];
  };

  // Skip animation if animations are disabled (e.g. new search action from
  // toolbar).
  if (!UIView.areAnimationsEnabled) {
    [toolbarSnapshot removeFromSuperview];
    commonCompletion();
    return;
  }

  CGPoint origin = [self lastTapPoint];
  CGRect frame = [self foregroundTabAnimationViewFrameForWebState:webState];

  ForegroundTabAnimationView* animatedView =
      [[ForegroundTabAnimationView alloc] initWithFrame:frame];
  animatedView.contentView = newPage;
  newPage.frame = animatedView.bounds;
  animatedView.useDeviceCornerRadius = useDeviceCornerRadius;
  if (IsChromeNextIaEnabled()) {
    animatedView.appBarPosition = self.layoutState.appBarPosition;
  }
  if (isNTP) {
    animatedView.backgroundView =
        [self.contentArea resizableSnapshotViewFromRect:frame
                                     afterScreenUpdates:NO
                                          withCapInsets:UIEdgeInsetsZero];
  } else {
    animatedView.backgroundView =
        [self.contentArea snapshotViewAfterScreenUpdates:NO];
  }

  __weak UIView* weakAnimatedView = animatedView;
  auto completionBlock = ^() {
    [weakAnimatedView removeFromSuperview];
    [toolbarSnapshot removeFromSuperview];
    commonCompletion();
  };
  [self.contentArea addSubview:animatedView];
  [animatedView animateFrom:origin withCompletion:completionBlock];
}

#pragma mark - IncognitoReauthConsumer

- (void)setItemsRequireAuthentication:(BOOL)require {
  _itemsRequireAuthentication = require;
  if (require) {
    if (!self.blockingView) {
      self.blockingView = [[IncognitoReauthView alloc] init];
      self.blockingView.translatesAutoresizingMaskIntoConstraints = NO;
      self.blockingView.layer.zPosition = FLT_MAX;

      DCHECK(self.reauthHandler);
      [self.blockingView.authenticateButton
                 addTarget:self.reauthHandler
                    action:@selector(authenticateIncognitoContent)
          forControlEvents:UIControlEventTouchUpInside];

      if (IsIOSSoftLockEnabled()) {
        base::WeakPtr<WebStateList> webStateList = _webStateList;
        id<IncognitoReauthCommands> reauthHandler = self.reauthHandler;
        [self.blockingView.secondaryButton
                   addAction:[UIAction actionWithHandler:^(UIAction* action) {
                     if (IsIOSSoftLockEnabled()) {
                       base::UmaHistogramEnumeration(
                           kIncognitoLockOverlayInteractionHistogram,
                           IncognitoLockOverlayInteraction::
                               kCloseIncognitoTabsButtonClicked);
                       base::RecordAction(base::UserMetricsAction(
                           "IOS.IncognitoLock.Overlay.CloseIncognitoTabs"));
                     }
                     if (webStateList) {
                       CloseAllWebStates(
                           *(webStateList),
                           WebStateList::ClosingReason::kUserAction);
                     }
                     [reauthHandler manualAuthenticationOverride];
                   }]
            forControlEvents:UIControlEventTouchUpInside];
      } else {
        DCHECK(self.sceneHandler);
        __weak __typeof(self) weakSelf = self;
        [self.blockingView.secondaryButton
                   addAction:[UIAction actionWithHandler:^(UIAction* action) {
                     if (IsIOSSoftLockEnabled()) {
                       base::UmaHistogramEnumeration(
                           kIncognitoLockOverlayInteractionHistogram,
                           IncognitoLockOverlayInteraction::
                               kSeeOtherTabsButtonClicked);
                       base::RecordAction(base::UserMetricsAction(
                           "IOS.IncognitoLock.Overlay.SeeOtherTabs"));
                     }
                     [weakSelf.sceneHandler
                         displayTabGridInMode:TabGridOpeningMode::kRegular];
                   }]
            forControlEvents:UIControlEventTouchUpInside];
      }
    }

    [self.view addSubview:self.blockingView];
    AddSameConstraints(self.view, self.blockingView);
    self.blockingView.alpha = 1;
    [_browserCoordinatorHandler hideComposebox];
    // Resign the first responder. This achieves multiple goals:
    // 1. The keyboard is dismissed.
    // 2. Hardware keyboard events (such as space to scroll) will be ignored.
    UIResponder* firstResponder = GetFirstResponder();
    [firstResponder resignFirstResponder];
    // Close presented view controllers, e.g. share sheets.
    if (self.presentedViewController) {
      [self.sceneHandler dismissModalDialogsWithCompletion:nil];
    }

  } else {
    [UIView animateWithDuration:0.2
        animations:^{
          self.blockingView.alpha = 0;
        }
        completion:^(BOOL finished) {
          // In an extreme case, this method can be called twice in quick
          // succession, before the animation completes. Check if the blocking
          // UI should be shown or the animation needs to be rolled back.
          if (self->_itemsRequireAuthentication) {
            self.blockingView.alpha = 1;
          } else {
            [self.blockingView removeFromSuperview];
          }
        }];
  }
}

- (void)setItemsRequireAuthentication:(BOOL)require
                withPrimaryButtonText:(NSString*)text
                   accessibilityLabel:(NSString*)accessibilityLabel {
  [self setItemsRequireAuthentication:require];
  if (require) {
    [self.blockingView setAuthenticateButtonText:text
                              accessibilityLabel:accessibilityLabel];
  } else {
    // No primary button text or accessibility label should be set when
    // authentication is not required.
    CHECK(!text);
    CHECK(!accessibilityLabel);
  }
}
#pragma mark - UIGestureRecognizerDelegate

// Always return yes, as this tap should work with various recognizers,
// including UITextTapRecognizer, UILongPressGestureRecognizer,
// UIScrollViewPanGestureRecognizer and others.
- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
    shouldRecognizeSimultaneouslyWithGestureRecognizer:
        (UIGestureRecognizer*)otherGestureRecognizer {
  return YES;
}

// Tap gestures should only be recognized within `contentArea`.
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer*)gesture {
  CGPoint location = [gesture locationInView:self.view];

  // Only allow touches on descendant views of `contentArea`.
  UIView* hitView = [self.view hitTest:location withEvent:nil];
  return [hitView isDescendantOfView:self.contentArea];
}

#pragma mark - CardSwipeViewDelegate

- (void)sideSwipeViewDismissAnimationDidEnd:(UIView*)sideSwipeView {
  DCHECK(!CanShowTabStrip(self));
  // TODO(crbug.com/40842406): Signal to the toolbar coordinator to perform this
  // update. Longer-term, make SideSwipeMediatorDelegate observable instead of
  // delegating.
  [self.toolbarCoordinator updateToolbar];

  // Reset horizontal stack view.
  [sideSwipeView removeFromSuperview];
  [_sideSwipeCoordinator setSwipeInProgress:NO];
}

// TODO(crbug.com/40842427): Factor this delegate into a mediator or other
// helper
#pragma mark - SideSwipeUIControllerDelegate

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

- (UIView*)sideSwipeContentView {
  return self.contentArea;
}

- (void)sideSwipeRedisplayTabView {
  [self displayTabView];
}

// TODO(crbug.com/40842427): Federate side swipe logic.
- (BOOL)preventSideSwipe {
  if ([self.popupMenuCoordinator isShowingPopupMenu]) {
    return YES;
  }

  if (_voiceSearchController.visible) {
    return YES;
  }

  if (!self.active) {
    return YES;
  }

  BOOL isShowingIncognitoBlocker = (self.blockingView.superview != nil);
  if (isShowingIncognitoBlocker) {
    return YES;
  }

  return NO;
}

- (void)updateAccessoryViewsForSideSwipeWithVisibility:(BOOL)visible {
  if (visible) {
    // TODO(crbug.com/40842406): Signal to the toolbar coordinator to perform
    // this update. Longer-term, make SideSwipeMediatorDelegate observable
    // instead of delegating.
    [self.toolbarCoordinator updateToolbar];
  } else {
    // Hide UI accessories such as find bar and first visit overlays
    // for welcome page.
    [self.findInPageCommandsHandler hideFindUI];
    [self.textZoomHandler hideTextZoomUI];
  }
}

- (CGFloat)headerHeightForSideSwipe {
  UIView* primaryToolbarView =
      self.toolbarCoordinator.primaryToolbarViewController.view;
  if (IsChromeNextIaEnabled() && primaryToolbarView.alpha == 0.0) {
    // When Chrome Next is enabled, the toolbar on the NTP is hidden by
    // setting its alpha to 0.0 instead of setting hidden to YES.
    return 0;
  }

  // If the toolbar is hidden, only inset the side swipe navigation view by
  // `safeAreaInsets.top`.  Otherwise insetting by `self.headerHeight` would
  // show a grey strip where the toolbar would normally be.
  if (primaryToolbarView.hidden) {
    return self.rootSafeAreaInsets.top;
  }
  return self.headerHeight;
}

- (BOOL)canBeginToolbarSwipe {
  if (IsChromeNextIaEnabled()) {
    return YES;
  }
  return ![self.toolbarCoordinator isOmniboxFirstResponder] &&
         ![self.toolbarCoordinator showingOmniboxPopup];
}

- (UIView*)topToolbarView {
  return self.toolbarCoordinator.primaryToolbarViewController.view;
}

#pragma mark - ToolbarHeightDelegate

- (void)toolbarsHeightChanged {
  if (![self isViewLoaded]) {
    return;
  }

  // Toolbars size must be updated before `updateForFullscreenProgress` as
  // the later uses the insets from fullscreen model.
  [self updateToolbarState];

  self.primaryToolbarHeightConstraint.constant =
      [self primaryToolbarHeightWithInset];
  self.secondaryToolbarHeightConstraint.constant =
      [self secondaryToolbarHeightWithInset];

  [self updateForFullscreenProgress:self.footerFullscreenProgress];
}

- (void)secondaryToolbarMovedAboveKeyboard {
  _isSecondaryToolbarAboveKeyboard = YES;

  // Lower the height constraint priority, allowing UIKeyboardLayoutGuide to
  // move the toolbar above the keyboard.
  self.secondaryToolbarHeightConstraint.priority = UILayoutPriorityDefaultHigh;
}

- (void)secondaryToolbarRemovedFromKeyboard {
  _isSecondaryToolbarAboveKeyboard = NO;

  // Return to required priority, otherwise UIKeyboardLayoutGuide would set the
  // toolbar minimum height to the bottom safe area.
  self.secondaryToolbarHeightConstraint.priority = UILayoutPriorityRequired - 1;
}

- (void)adjustSecondaryToolbarForKeyboardHeight:(CGFloat)keyboardHeight
                                    isCollapsed:(BOOL)isCollapsed
                                       duration:(NSTimeInterval)duration
                                          curve:(UIViewAnimationCurve)curve {
  CHECK(ui::GetDeviceFormFactor() != ui::DEVICE_FORM_FACTOR_TABLET);
  _isSecondaryToolbarAboveKeyboard = (keyboardHeight > 0);
  CGFloat keyboardAttachedOffset =
      keyboardHeight +
      self.toolbarCoordinator.keyboardAttachedBottomOmniboxHeight;
  if (IsChromeNextIaEnabled()) {
    // When the App Bar is at the bottom (Portrait), the secondary toolbar is
    // already taller by the height of the App Bar, so we subtract the App Bar
    // height.
    if (self.layoutState.appBarPosition == AppBarPosition::kBottom) {
      CGFloat minHeight =
          IsAppBarHiddenInFullscreen() ? 0 : kAppBarHeightFullscreen;
      keyboardAttachedOffset -= minHeight;
    }
  }
  CGFloat baseHeight = [self secondaryToolbarHeightWithInset];
  CGFloat offsetRequired = isCollapsed
                               ? keyboardAttachedOffset
                               : MAX(keyboardAttachedOffset, baseHeight);

  // No need to start an animation when the offset is already set.
  BOOL alreadyInPosition =
      self.secondaryToolbarHeightConstraint.constant == offsetRequired;
  // Always animate when keyboard is being hidden. When showing the keyboard,
  // make sure to check editing to avoid manipulating the toolbar when other
  // textfield is focused.
  if (alreadyInPosition) {
    [self.toolbarCoordinator setBottomOmniboxOffsetForPopup:offsetRequired];
    return;
  }

  // The shift converts an animation curve to animation options.
  // Shifting by 16 is a result of Apple reserving 4 bits in
  // `UIViewAnimationOptions` for the type of curve.
  UIViewAnimationOptions animationOptions = curve << 16;
  [UIView animateWithDuration:duration
                        delay:0.0
                      options:animationOptions
                   animations:^{
                     self.secondaryToolbarHeightConstraint.constant =
                         offsetRequired;
                     [self.toolbarCoordinator
                         setBottomOmniboxOffsetForPopup:offsetRequired];
                     [self.view layoutIfNeeded];
                   }
                   completion:nil];
}

#pragma mark - LogoAnimationControllerOwnerOwner (Public)

- (id<LogoAnimationControllerOwner>)logoAnimationControllerOwner {
  // This is required to enable voice search in the NTP.
  return nil;
}

#pragma mark - ContextualSheetPresenter

- (void)insertContextualSheet:(UIView*)contextualSheet {
  [self.view
      insertSubview:contextualSheet
       aboveSubview:self.toolbarCoordinator.primaryToolbarViewController.view];
}

#pragma mark - LensOverlayStateNotifierObserver

- (void)lensOverlayDidPrepare:
    (LensOverlayStateNotifier*)lensOverlayStateNotifier {
  [self.sceneHandler hideAssistant];
  if (IsPageActionMenuEnabled()) {
    [self.geminiHandler
        hideFloatyIfInvokedAnimated:NO
                         fromSource:gemini::FloatyUpdateSource::Overlay];
  }
}

- (void)lensOverlayWillAppear:
    (LensOverlayStateNotifier*)lensOverlayStateNotifier {
  [_sideSwipeCoordinator setEnabled:NO];
  _lensOverlayVisible = YES;
  self.contentArea.accessibilityElementsHidden = self.contentAreaObstructed;
}

- (void)lensOverlayWillDisappear:
    (LensOverlayStateNotifier*)lensOverlayStateNotifier {
  [_sideSwipeCoordinator setEnabled:YES];
  _lensOverlayVisible = NO;
  [self.sceneHandler revealAssistant];
  self.contentArea.accessibilityElementsHidden = self.contentAreaObstructed;
}

- (void)lensOverlayDidDisappear:
    (LensOverlayStateNotifier*)lensOverlayStateNotifier {
  if (IsPageActionMenuEnabled()) {
    [self.geminiHandler
        updateFloatyVisibilityIfEligibleAnimated:NO
                                      fromSource:gemini::FloatyUpdateSource::
                                                     Overlay];
  }
}

- (void)lensOverlayDidReadjustPresentation:
    (LensOverlayStateNotifier*)lensOverlayStateNotifier {
  [_browserCoordinatorHandler hideComposebox];
}

#pragma mark - LensOverlayPresentationEnvironment

- (NSDirectionalEdgeInsets)presentationInsetsForLensOverlay {
  if (IsRegularXRegularSizeClass(self)) {
    return NSDirectionalEdgeInsetsMake([self expandedTopToolbarHeight], 0, 0,
                                       0);
  }
  return NSDirectionalEdgeInsetsZero;
}

#pragma mark - Activity Reporting

- (void)updateTabActivityReporting {
  if (self.active && self.currentWebState) {
    if (IsVisibleURLNewTabPage(self.currentWebState)) {
      [_activityReporter reportInactive];
    } else {
      [_activityReporter reportActiveWithIncognito:_isOffTheRecord];
    }
  } else {
    [_activityReporter reportInactive];
  }
}

@end
