blob: 76017fcee4efb722828a3401d6ff12bfe17e7ac3 [file] [log] [blame]
// Copyright 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/ui/browser_view/browser_view_controller.h"
#import "ios/chrome/browser/ui/browser_view/browser_view_controller+delegates.h"
#import "ios/chrome/browser/ui/browser_view/browser_view_controller+private.h"
#import <MaterialComponents/MaterialSnackbar.h>
#import "base/mac/bundle_locations.h"
#import "base/mac/foundation_util.h"
#import "base/metrics/histogram_macros.h"
#import "base/metrics/user_metrics.h"
#import "base/metrics/user_metrics_action.h"
#import "base/strings/sys_string_conversions.h"
#import "components/feature_engagement/public/event_constants.h"
#import "components/feature_engagement/public/tracker.h"
#import "components/omnibox/browser/location_bar_model_impl.h"
#import "components/reading_list/core/reading_list_model.h"
#import "components/sessions/core/tab_restore_service_helper.h"
#import "components/signin/ios/browser/active_state_manager.h"
#import "components/strings/grit/components_strings.h"
#import "components/translate/core/browser/translate_manager.h"
#import "components/ukm/ios/ukm_url_recorder.h"
#import "ios/chrome/app/application_delegate/app_state.h"
#import "ios/chrome/browser/application_context.h"
#import "ios/chrome/browser/browser_state/chrome_browser_state.h"
#import "ios/chrome/browser/chrome_url_constants.h"
#import "ios/chrome/browser/crash_report/crash_keys_helper.h"
#import "ios/chrome/browser/discover_feed/feed_constants.h"
#import "ios/chrome/browser/feature_engagement/tracker_factory.h"
#import "ios/chrome/browser/feature_engagement/tracker_util.h"
#import "ios/chrome/browser/find_in_page/find_tab_helper.h"
#import "ios/chrome/browser/infobars/infobar_manager_impl.h"
#import "ios/chrome/browser/main/browser.h"
#import "ios/chrome/browser/metrics/new_tab_page_uma.h"
#import "ios/chrome/browser/metrics/tab_usage_recorder_browser_agent.h"
#import "ios/chrome/browser/ntp/new_tab_page_tab_helper.h"
#import "ios/chrome/browser/ntp/new_tab_page_tab_helper_delegate.h"
#import "ios/chrome/browser/overscroll_actions/overscroll_actions_tab_helper.h"
#import "ios/chrome/browser/passwords/password_controller.h"
#import "ios/chrome/browser/prerender/preload_controller_delegate.h"
#import "ios/chrome/browser/prerender/prerender_service.h"
#import "ios/chrome/browser/prerender/prerender_service_factory.h"
#import "ios/chrome/browser/reading_list/offline_page_tab_helper.h"
#import "ios/chrome/browser/reading_list/reading_list_model_factory.h"
#import "ios/chrome/browser/sessions/ios_chrome_tab_restore_service_factory.h"
#import "ios/chrome/browser/sessions/session_restoration_browser_agent.h"
#import "ios/chrome/browser/signin/authentication_service.h"
#import "ios/chrome/browser/signin/authentication_service_factory.h"
#import "ios/chrome/browser/snapshots/snapshot_tab_helper.h"
#import "ios/chrome/browser/ssl/captive_portal_tab_helper.h"
#import "ios/chrome/browser/ssl/captive_portal_tab_helper_delegate.h"
#import "ios/chrome/browser/tabs/tab_title_util.h"
#import "ios/chrome/browser/translate/chrome_ios_translate_client.h"
#import "ios/chrome/browser/ui/authentication/re_signin_infobar_delegate.h"
#import "ios/chrome/browser/ui/bookmarks/bookmark_interaction_controller.h"
#import "ios/chrome/browser/ui/browser_container/browser_container_view_controller.h"
#import "ios/chrome/browser/ui/browser_view/browser_view_controller_dependency_factory.h"
#import "ios/chrome/browser/ui/browser_view/browser_view_controller_helper.h"
#import "ios/chrome/browser/ui/browser_view/key_commands_provider.h"
#import "ios/chrome/browser/ui/bubble/bubble_presenter.h"
#import "ios/chrome/browser/ui/bubble/bubble_presenter_delegate.h"
#import "ios/chrome/browser/ui/commands/browser_commands.h"
#import "ios/chrome/browser/ui/commands/command_dispatcher.h"
#import "ios/chrome/browser/ui/commands/help_commands.h"
#import "ios/chrome/browser/ui/commands/load_query_commands.h"
#import "ios/chrome/browser/ui/commands/reading_list_add_command.h"
#import "ios/chrome/browser/ui/commands/show_signin_command.h"
#import "ios/chrome/browser/ui/commands/text_zoom_commands.h"
#import "ios/chrome/browser/ui/content_suggestions/content_suggestions_feature.h"
#import "ios/chrome/browser/ui/content_suggestions/ntp_home_constant.h"
#import "ios/chrome/browser/ui/default_promo/default_browser_promo_non_modal_scheduler.h"
#import "ios/chrome/browser/ui/default_promo/default_promo_non_modal_presentation_delegate.h"
#import "ios/chrome/browser/ui/download/download_manager_coordinator.h"
#import "ios/chrome/browser/ui/first_run/first_run_util.h"
#import "ios/chrome/browser/ui/first_run/welcome_to_chrome_view_controller.h"
#import "ios/chrome/browser/ui/fullscreen/fullscreen_animator.h"
#import "ios/chrome/browser/ui/fullscreen/fullscreen_features.h"
#import "ios/chrome/browser/ui/fullscreen/fullscreen_ui_element.h"
#import "ios/chrome/browser/ui/fullscreen/fullscreen_ui_updater.h"
#import "ios/chrome/browser/ui/fullscreen/scoped_fullscreen_disabler.h"
#import "ios/chrome/browser/ui/gestures/view_revealing_vertical_pan_handler.h"
#import "ios/chrome/browser/ui/incognito_reauth/incognito_reauth_commands.h"
#import "ios/chrome/browser/ui/incognito_reauth/incognito_reauth_scene_agent.h"
#import "ios/chrome/browser/ui/incognito_reauth/incognito_reauth_view.h"
#import "ios/chrome/browser/ui/infobars/infobar_positioner.h"
#import "ios/chrome/browser/ui/location_bar/location_bar_model_delegate_ios.h"
#import "ios/chrome/browser/ui/main/scene_state.h"
#import "ios/chrome/browser/ui/main/scene_state_browser_agent.h"
#import "ios/chrome/browser/ui/main_content/main_content_ui.h"
#import "ios/chrome/browser/ui/main_content/main_content_ui_broadcasting_util.h"
#import "ios/chrome/browser/ui/main_content/main_content_ui_state.h"
#import "ios/chrome/browser/ui/main_content/web_scroll_view_main_content_ui_forwarder.h"
#import "ios/chrome/browser/ui/ntp/new_tab_page_coordinator.h"
#import "ios/chrome/browser/ui/ntp/ntp_util.h"
#import "ios/chrome/browser/ui/omnibox/popup/omnibox_popup_presenter.h"
#import "ios/chrome/browser/ui/popup_menu/popup_menu_coordinator.h"
#import "ios/chrome/browser/ui/presenters/vertical_animation_container.h"
#import "ios/chrome/browser/ui/send_tab_to_self/send_tab_to_self_coordinator.h"
#import "ios/chrome/browser/ui/settings/sync/utils/sync_util.h"
#import "ios/chrome/browser/ui/side_swipe/side_swipe_controller.h"
#import "ios/chrome/browser/ui/side_swipe/swipe_view.h"
#import "ios/chrome/browser/ui/start_surface/start_surface_features.h"
#import "ios/chrome/browser/ui/sync/utils/features.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_strip/tab_strip_coordinator.h"
#import "ios/chrome/browser/ui/tabs/background_tab_animation_view.h"
#import "ios/chrome/browser/ui/tabs/foreground_tab_animation_view.h"
#import "ios/chrome/browser/ui/tabs/requirements/tab_strip_presentation.h"
#import "ios/chrome/browser/ui/tabs/switch_to_tab_animation_view.h"
#import "ios/chrome/browser/ui/tabs/tab_strip_constants.h"
#import "ios/chrome/browser/ui/tabs/tab_strip_containing.h"
#import "ios/chrome/browser/ui/tabs/tab_strip_legacy_coordinator.h"
#import "ios/chrome/browser/ui/toolbar/accessory/toolbar_accessory_presenter.h"
#import "ios/chrome/browser/ui/toolbar/adaptive_toolbar_coordinator.h"
#import "ios/chrome/browser/ui/toolbar/adaptive_toolbar_view_controller.h"
#import "ios/chrome/browser/ui/toolbar/fullscreen/toolbar_ui.h"
#import "ios/chrome/browser/ui/toolbar/fullscreen/toolbar_ui_broadcasting_util.h"
#import "ios/chrome/browser/ui/toolbar/primary_toolbar_coordinator.h"
#import "ios/chrome/browser/ui/toolbar/public/toolbar_constants.h"
#import "ios/chrome/browser/ui/toolbar/public/toolbar_utils.h"
#import "ios/chrome/browser/ui/toolbar/secondary_toolbar_coordinator.h"
#import "ios/chrome/browser/ui/toolbar/toolbar_coordinator_adaptor.h"
#import "ios/chrome/browser/ui/toolbar_container/toolbar_container_coordinator.h"
#import "ios/chrome/browser/ui/toolbar_container/toolbar_container_features.h"
#import "ios/chrome/browser/ui/ui_feature_flags.h"
#import "ios/chrome/browser/ui/util/keyboard_observer_helper.h"
#import "ios/chrome/browser/ui/util/named_guide.h"
#import "ios/chrome/browser/ui/util/named_guide_util.h"
#import "ios/chrome/browser/ui/util/page_animation_util.h"
#import "ios/chrome/browser/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/ui/util/url_with_title.h"
#import "ios/chrome/browser/ui/voice/text_to_speech_playback_controller.h"
#import "ios/chrome/browser/ui/voice/text_to_speech_playback_controller_factory.h"
#import "ios/chrome/browser/upgrade/upgrade_center.h"
#import "ios/chrome/browser/url_loading/url_loading_browser_agent.h"
#import "ios/chrome/browser/url_loading/url_loading_notifier_browser_agent.h"
#import "ios/chrome/browser/url_loading/url_loading_observer_bridge.h"
#import "ios/chrome/browser/url_loading/url_loading_params.h"
#import "ios/chrome/browser/url_loading/url_loading_util.h"
#import "ios/chrome/browser/voice/voice_search_navigations_tab_helper.h"
#import "ios/chrome/browser/web/page_placeholder_tab_helper.h"
#import "ios/chrome/browser/web/sad_tab_tab_helper.h"
#import "ios/chrome/browser/web/web_navigation_browser_agent.h"
#import "ios/chrome/browser/web/web_navigation_util.h"
#import "ios/chrome/browser/web_state_list/all_web_state_observation_forwarder.h"
#import "ios/chrome/browser/web_state_list/tab_insertion_browser_agent.h"
#import "ios/chrome/browser/web_state_list/web_state_list.h"
#import "ios/chrome/browser/web_state_list/web_state_list_observer_bridge.h"
#import "ios/chrome/browser/web_state_list/web_usage_enabler/web_usage_enabler_browser_agent.h"
#import "ios/chrome/browser/webui/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/grit/ios_strings.h"
#import "ios/public/provider/chrome/browser/voice_search/voice_search_api.h"
#import "ios/public/provider/chrome/browser/voice_search/voice_search_controller.h"
#import "ios/web/public/deprecated/crw_js_injection_receiver.h"
#import "ios/web/public/navigation/navigation_item.h"
#import "ios/web/public/ui/crw_web_view_proxy.h"
#import "ios/web/public/web_state_observer_bridge.h"
#import "net/base/mac/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"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
using base::UserMetricsAction;
namespace {
const size_t kMaxURLDisplayChars = 32 * 1024;
// Duration of the toolbar animation.
const NSTimeInterval kLegacyFullscreenControllerToolbarAnimationDuration = 0.3;
// When the tab strip moves beyond this origin offset, switch the status bar
// appearance from light to dark.
const CGFloat kTabStripAppearanceOffset = -29;
enum HeaderBehaviour {
// The header moves completely out of the screen.
Hideable = 0,
// This header stay on screen and covers part of the content.
// Snackbar category for browser view controller.
NSString* const kBrowserViewControllerSnackbarCategory =
} // namespace
#pragma mark - ToolbarContainerView
// TODO( This is a temporary solution. This logic should be
// handled by ToolbarContainerViewController.
@interface LegacyToolbarContainerView : UIView
@implementation LegacyToolbarContainerView
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event {
// Don't receive events that don't occur within a subview. This is necessary
// because the container view overlaps with web content and the default
// behavior will intercept touches meant for the web page when the toolbars
// are collapsed.
for (UIView* subview in self.subviews) {
if (CGRectContainsPoint(subview.frame, point))
return [super hitTest:point withEvent:event];
return nil;
#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
+ (instancetype)definitionWithView:(UIView*)view
@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;
#pragma mark - BVC
// Note other delegates defined in the Delegates category header.
@interface BrowserViewController () <CaptivePortalTabHelperDelegate,
WebStateListObserving> {
// The dependency factory passed on initialization. Used to vend objects used
// by the BVC.
BrowserViewControllerDependencyFactory* _dependencyFactory;
// Identifier for each animation of an NTP opening.
NSInteger _NTPAnimationIdentifier;
// Facade objects used by |_toolbarCoordinator|.
// Must outlive |_toolbarCoordinator|.
std::unique_ptr<LocationBarModelDelegateIOS> _locationBarModelDelegate;
std::unique_ptr<LocationBarModel> _locationBarModel;
// Controller for edge swipe gestures for page and tab navigation.
SideSwipeController* _sideSwipeController;
// Keyboard commands provider. It offloads most of the keyboard commands
// management off of the BVC.
KeyCommandsProvider* _keyCommandsProvider;
// TODO( Remove all use of the prerender service from BVC
PrerenderService* _prerenderService;
// Used to display the Voice Search UI. Nil if not visible.
// TODO( Move voice search controller/coordinator to
// BrowserCoordinator
id<VoiceSearchController> _voiceSearchController;
// YES if new tab is animating in.
BOOL _inNewTabAnimation;
// YES if Voice Search should be started when the new tab animation is
// finished.
BOOL _startVoiceSearchAfterNewTabAnimation;
// YES if waiting for a foreground tab due to expectNewForegroundTab.
// TODO( Move this to a browser agent or web event
// mediator.
BOOL _expectingForegroundTab;
// Whether or not -shutdown has been called.
BOOL _isShutdown;
// Whether or not Incognito* is enabled.
// TODO( Set this in the init.
BOOL _isOffTheRecord;
// 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 controller that shows the bookmarking UI after the user taps the star
// button.
BookmarkInteractionController* _bookmarkInteractionController;
// TODO( Move this out of the BVC; inject it as the
// ToolbarInterface as an interim step if needed.
ToolbarCoordinatorAdaptor* _toolbarCoordinatorAdaptor;
// Toolbar state that broadcasts changes to min and max heights.
ToolbarUIState* _toolbarUIState;
// 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;
// Coordinator for the Download Manager UI.
// TODO( Move DownloadManagerCoordinator to
// BrowserCoordinator.
DownloadManagerCoordinator* _downloadManagerCoordinator;
// A map associating webStates with their NTP coordinators.
// TODO( Factor NTPCoordinator ownership out of the BVC
std::map<web::WebState*, NewTabPageCoordinator*> _ntpCoordinatorsForWebStates;
// Fake status bar view used to blend the toolbar into the status bar.
UIView* _fakeStatusBarView;
// Forwards observer methods for all WebStates in the WebStateList to this
// BrowserViewController object.
// Bridges C++ WebStateObserver methods to this BrowserViewController.
std::unique_ptr<web::WebStateObserverBridge> _webStateObserverBridge;
std::unique_ptr<UrlLoadingObserverBridge> _URLLoadingObserverBridge;
// Bridges C++ WebStateListObserver methods to this BrowserViewController.
std::unique_ptr<WebStateListObserverBridge> _webStateListObserver;
// The disabler that prevents the toolbar from being scrolled offscreen when
// the thumb strip is visible.
std::unique_ptr<ScopedFullscreenDisabler> _fullscreenDisabler;
// For thumb strip, when YES, fullscreen disabler is reset only when web view
// dragging stops, to avoid closing thumb strip and going fullscreen in
// one single drag gesture. When NO, full screen disabler is reset when
// the thumb strip animation ends.
BOOL _deferEndFullscreenDisabler;
// 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;
// The Browser whose UI is managed by this instance.
@property(nonatomic, assign) Browser* browser;
// Browser container view controller.
@property(nonatomic, strong)
BrowserContainerViewController* browserContainerViewController;
// Invisible button used to dismiss the keyboard.
@property(nonatomic, strong) UIButton* typingShield;
// The object that manages keyboard commands on behalf of the BVC.
@property(nonatomic, strong, readonly) KeyCommandsProvider* keyCommandsProvider;
// Whether the controller's view is currently available.
// YES from viewWillAppear to viewWillDisappear.
@property(nonatomic, assign, getter=isVisible) BOOL visible;
// Whether the controller's view is currently visible.
// YES from viewDidAppear to viewWillDisappear.
@property(nonatomic, assign) BOOL viewVisible;
// 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 web usage is enabled for the WebStates in |self.browser|.
@property(nonatomic, assign, getter=isWebUsageEnabled) BOOL webUsageEnabled;
// 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;
// Whether the BVC is positioned at the bottom of the window, for example after
// switching from thumb strip to tab grid.
@property(nonatomic, assign) BOOL bottomPosition;
// 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;
// Coordinator for tablet tab strip.
@property(nonatomic, strong)
TabStripLegacyCoordinator* legacyTabStripCoordinator;
// Coordinator for the new tablet tab strip.
@property(nonatomic, strong) TabStripCoordinator* tabStripCoordinator;
// A weak reference to the view of the tab strip on tablet.
@property(nonatomic, weak) UIView<TabStripContaining>* tabStripView;
// A snapshot of the tab strip used on the thumb strip reveal/hide animation.
@property(nonatomic, strong) UIView* tabStripSnapshot;
// Helper for the bvc.
// TODO( Remove this property.
@property(nonatomic, strong) BrowserViewControllerHelper* helper;
// The user agent type used to load the currently visible page. User agent
// type is NONE if there is no visible page.
// TODO( Move BubblePresenter to BrowserCoordinator.
@property(nonatomic, assign, readonly) web::UserAgentType userAgentType;
// 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;
@property(nonatomic, strong) BubblePresenter* bubblePresenter;
// 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 omnibox commands
@property(nonatomic, weak) id<OmniboxCommands> omniboxHandler;
// The FullscreenController.
@property(nonatomic, assign) FullscreenController* fullscreenController;
// Primary toolbar.
@property(nonatomic, strong)
PrimaryToolbarCoordinator* primaryToolbarCoordinator;
// Secondary toolbar.
@property(nonatomic, strong)
AdaptiveToolbarCoordinator* secondaryToolbarCoordinator;
// The container view for the secondary toolbar.
// TODO( Convert to a container coordinator.
@property(nonatomic, strong) UIView* secondaryToolbarContainerView;
// Coordinator used to manage the secondary toolbar view.
@property(nonatomic, strong)
ToolbarContainerCoordinator* secondaryToolbarContainerCoordinator;
// Interface object with the toolbars.
@property(nonatomic, strong) id<ToolbarCoordinating> toolbarInterface;
// 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;
// Height constraint for the frame the secondary toolbar would have if
// fullscreen was disabled.
@property(nonatomic, strong)
NSLayoutConstraint* secondaryToolbarNoFullscreenHeightConstraint;
// Current Fullscreen progress for the footers.
@property(nonatomic, assign) CGFloat footerFullscreenProgress;
// Y-dimension offset for placement of the header.
@property(nonatomic, readonly) CGFloat headerOffset;
// Height of the header view.
@property(nonatomic, readonly) CGFloat headerHeight;
// The webState of the active tab.
@property(nonatomic, readonly) web::WebState* currentWebState;
// The NewTabPageCoordinator associated with a WebState.
- (NewTabPageCoordinator*)ntpCoordinatorForWebState:(web::WebState*)webState;
// Whether the keyboard observer helper is viewed
@property(nonatomic, strong) KeyboardObserverHelper* observer;
// The coordinator that shows the Send Tab To Self UI.
@property(nonatomic, strong) SendTabToSelfCoordinator* sendTabToSelfCoordinator;
// Whether the view has been translated for thumb strip usage when smooth
// scrolling has been enabled. This allows the correct setup to be done when
// displaying a new web state.
@property(nonatomic, assign) BOOL viewTranslatedForSmoothScrolling;
// 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;
// The thumb strip's pan gesture handler that will be added to the toolbar and
// tab strip.
@property(nonatomic, weak)
ViewRevealingVerticalPanHandler* thumbStripPanHandler;
@implementation BrowserViewController
@synthesize thumbStripEnabled = _thumbStripEnabled;
#pragma mark - Object lifecycle
- (instancetype)initWithBrowser:(Browser*)browser
bubblePresenter:(BubblePresenter*)bubblePresenter {
self = [super initWithNibName:nil bundle:base::mac::FrameworkBundle()];
if (self) {
// TODO( DCHECK that |browser| is non-null.
_commandDispatcher = dispatcher;
_browserContainerViewController = browserContainerViewController;
_dependencyFactory = factory;
// TODO( Remove all use of the prerender service from BVC
_prerenderService = prerenderService;
_bubblePresenter = bubblePresenter;
// TODO( Inject this handler.
self.textZoomHandler =
HandlerForProtocol(self.commandDispatcher, TextZoomCommands);
// TODO( Have BrowserCoordinator set up dispatch to the
// BVC for these commands.
_toolbarCoordinatorAdaptor =
[[ToolbarCoordinatorAdaptor alloc] initWithDispatcher:self.dispatcher];
self.toolbarInterface = _toolbarCoordinatorAdaptor;
// TODO( Move DownloadManagerCoordinator to
// BrowserCoordinator.
_downloadManagerCoordinator = [[DownloadManagerCoordinator alloc]
_downloadManagerCoordinator.presenter =
[[VerticalAnimationContainer alloc] init];
_inNewTabAnimation = NO;
_fullscreenController = FullscreenController::FromBrowser(browser);
_footerFullscreenProgress = 1.0;
_observer = [[KeyboardObserverHelper alloc] init];
if (browser)
[self updateWithBrowser:browser];
return self;
- (void)dealloc {
DCHECK(_isShutdown) << "-shutdown must be called before dealloc.";
#pragma mark - Public Properties
// TODO( This uses PopupMenuCommands via inclusion in
// BrowserCommands. This is also not a public property. Instead of using
// |self.dispatcher| internally, this should use the currect pattern for handler
// injection into a view controller with a dedicated id<PopupMenuCommands>
// public property set externally.
// TODO( This uses SnackbarCommands via inclusion in
// BrowserCommands. Instead a a dedicated id<SnackbarCommands> property should
// be injected.
- (id<ApplicationCommands,
ToolbarCommands>)dispatcher {
return static_cast<
id<ApplicationCommands, BrowserCommands, FindInPageCommands,
PasswordBreachCommands, ToolbarCommands>>(self.commandDispatcher);
- (UIView*)contentArea {
return self.browserContainerViewController.view;
// TODO( Move voice search controller/coordinator to
// BrowserCoordinator, remove this as a public property.
- (BOOL)isPlayingTTS {
return _voiceSearchController.audioPlaying;
// TODO( Remove this property. Also not a public property.
- (ChromeBrowserState*)browserState {
return self.browser ? self.browser->GetBrowserState() : nullptr;
- (void)setInfobarBannerOverlayContainerViewController:
(UIViewController*)infobarBannerOverlayContainerViewController {
if (_infobarBannerOverlayContainerViewController ==
infobarBannerOverlayContainerViewController) {
_infobarBannerOverlayContainerViewController =
if (!_infobarBannerOverlayContainerViewController)
[self updateOverlayContainerOrder];
- (void)setInfobarModalOverlayContainerViewController:
(UIViewController*)infobarModalOverlayContainerViewController {
if (_infobarModalOverlayContainerViewController ==
infobarModalOverlayContainerViewController) {
_infobarModalOverlayContainerViewController =
if (!_infobarModalOverlayContainerViewController)
[self updateOverlayContainerOrder];
#pragma mark - Delegates Category Properties
// Lazily creates the SideSwipeController on first access.
- (SideSwipeController*)sideSwipeController {
if (!_sideSwipeController) {
_sideSwipeController =
[[SideSwipeController alloc] initWithBrowser:self.browser];
[_sideSwipeController setSnapshotDelegate:self];
_sideSwipeController.toolbarInteractionHandler = self.toolbarInterface;
_sideSwipeController.primaryToolbarSnapshotProvider =
_sideSwipeController.secondaryToolbarSnapshotProvider =
[_sideSwipeController setSwipeDelegate:self];
if (!base::FeatureList::IsEnabled(kModernTabStrip)) {
[_sideSwipeController setTabStripDelegate:self.legacyTabStripCoordinator];
return _sideSwipeController;
// TODO( Move DownloadManagerCoordinator to
// BrowserCoordinator.
- (DownloadManagerCoordinator*)downloadManagerCoordinator {
return _downloadManagerCoordinator;
#pragma mark - Private Properties
- (KeyCommandsProvider*)keyCommandsProvider {
if (!_keyCommandsProvider) {
_keyCommandsProvider = [_dependencyFactory newKeyCommandsProvider];
return _keyCommandsProvider;
- (BOOL)canShowTabStrip {
return IsRegularXRegularSizeClass(self);
- (web::UserAgentType)userAgentType {
web::WebState* webState = self.currentWebState;
if (!webState)
return web::UserAgentType::NONE;
web::NavigationItem* visibleItem =
if (!visibleItem)
return web::UserAgentType::NONE;
return visibleItem->GetUserAgentType();
- (void)setVisible:(BOOL)visible {
if (_visible == visible)
_visible = visible;
- (void)setViewVisible:(BOOL)viewVisible {
if (_viewVisible == viewVisible)
_viewVisible = viewVisible;
self.visible = viewVisible;
[self updateBroadcastState];
- (void)setBroadcasting:(BOOL)broadcasting {
if (_broadcasting == broadcasting)
_broadcasting = broadcasting;
ChromeBroadcaster* broadcaster = self.fullscreenController->broadcaster();
if (_broadcasting) {
_toolbarUIState = [[ToolbarUIState alloc] init];
// Must update _toolbarUIState with current toolbar height state before
// starting broadcasting.
[self updateToolbarState];
StartBroadcastingToolbarUI(_toolbarUIState, broadcaster);
_mainContentUIUpdater = [[MainContentUIStateUpdater alloc]
initWithState:[[MainContentUIState alloc] init]];
_webMainContentUIForwarder = [[WebScrollViewMainContentUIForwarder alloc]
StartBroadcastingMainContentUI(self, broadcaster);
_fullscreenUIUpdater =
std::make_unique<FullscreenUIUpdater>(self.fullscreenController, self);
[self updateForFullscreenProgress:self.fullscreenController->GetProgress()];
} else {
_mainContentUIUpdater = nil;
_toolbarUIState = nil;
[_webMainContentUIForwarder disconnect];
_webMainContentUIForwarder = nil;
_fullscreenUIUpdater = nullptr;
// TODO( Change webUsageEnabled to be a regular BOOL ivar.
- (BOOL)isWebUsageEnabled {
return self.browserState && !_isShutdown &&
// TODO( Change webUsageEnabled to be a regular BOOL ivar.
// BrowserCoordinator should update the WebUsageEnablerBrowserAgent.
- (void)setWebUsageEnabled:(BOOL)webUsageEnabled {
if (!self.browserState || _isShutdown)
- (void)setInNewTabAnimation:(BOOL)inNewTabAnimation {
if (_inNewTabAnimation == inNewTabAnimation)
_inNewTabAnimation = inNewTabAnimation;
[self updateBroadcastState];
- (BOOL)isInNewTabAnimation {
return _inNewTabAnimation;
- (void)setHideStatusBar:(BOOL)hideStatusBar {
if (_hideStatusBar == hideStatusBar)
_hideStatusBar = hideStatusBar;
[self setNeedsStatusBarAppearanceUpdate];
- (NSArray<HeaderDefinition*>*)headerViews {
NSMutableArray<HeaderDefinition*>* results = [[NSMutableArray alloc] init];
if (![self isViewLoaded])
return results;
if (![self canShowTabStrip]) {
if (self.primaryToolbarCoordinator.viewController.view) {
[results addObject:[HeaderDefinition
} else {
if (self.tabStripView) {
[results addObject:[HeaderDefinition definitionWithView:self.tabStripView
if (self.primaryToolbarCoordinator.viewController.view) {
[results addObject:[HeaderDefinition
if (self.toolbarAccessoryPresenter.isPresenting) {
[results addObject:[HeaderDefinition
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 has moved (like with
// thumbstrip, starting with iOS 15) or if it is unattached ( for example on the
// incognito BVC when the normal BVC is the one active or vice versa). Attached
// or unttached, going to the window through the SceneState for the self.browser
// solves both issues.
- (UIEdgeInsets)rootSafeAreaInsets {
if (_isShutdown) {
return UIEdgeInsetsZero;
// TODO( Create an external provider thingy for this.
UIView* view =
return view ? view.safeAreaInsets : self.view.safeAreaInsets;
- (CGFloat)headerOffset {
CGFloat headerOffset =;
return [self canShowTabStrip] ? headerOffset : 0.0;
- (CGFloat)headerHeight {
NSArray<HeaderDefinition*>* views = [self headerViews];
CGFloat height = self.headerOffset;
for (HeaderDefinition* header in views) {
if (header.view && header.behaviour == Hideable) {
height += CGRectGetHeight([header.view frame]);
CGFloat statusBarOffset = 0;
return height - statusBarOffset;
- (web::WebState*)currentWebState {
return self.browser ? self.browser->GetWebStateList()->GetActiveWebState()
: nullptr;
// TODO( Remove once kSingleNtp feature is launched and
// directly reference |self.ntpCoordinator|.
- (NewTabPageCoordinator*)ntpCoordinatorForWebState:(web::WebState*)webState {
if (IsSingleNtpEnabled()) {
return _ntpCoordinator;
auto found = _ntpCoordinatorsForWebStates.find(webState);
if (found != _ntpCoordinatorsForWebStates.end())
return found->second;
return nil;
#pragma mark - Public methods
- (id<ActivityServicePositioner>)activityServicePositioner {
return [self.primaryToolbarCoordinator activityServicePositioner];
- (void)setPrimary:(BOOL)primary {
TabUsageRecorderBrowserAgent* tabUsageRecorder =
if (tabUsageRecorder) {
primary, _browser->GetWebStateList()->GetActiveWebState());
if (primary) {
[self updateBroadcastState];
- (void)shieldWasTapped:(id)sender {
[self.omniboxHandler cancelOmniboxEdit];
- (void)userEnteredTabSwitcher {
[_bubblePresenter userEnteredTabSwitcher];
- (void)openNewTabFromOriginPoint:(CGPoint)originPoint
inheritOpener:(BOOL)inheritOpener {
NSTimeInterval startTime = [NSDate timeIntervalSinceReferenceDate];
BOOL offTheRecord = self.isOffTheRecord;
ProceduralBlock oldForegroundTabWasAddedCompletionBlock =
id<OmniboxCommands> omniboxCommandHandler = self.omniboxHandler;
self.foregroundTabWasAddedCompletionBlock = ^{
if (oldForegroundTabWasAddedCompletionBlock) {
double duration = [NSDate timeIntervalSinceReferenceDate] - startTime;
base::TimeDelta timeDelta = base::Seconds(duration);
if (offTheRecord) {
} else {
UMA_HISTOGRAM_TIMES("Toolbar.Menu.NewTabPresentationDuration", timeDelta);
if (focusOmnibox) {
[omniboxCommandHandler focusOmnibox];
[self setLastTapPointFromCommand:originPoint];
// The new tab can be opened before BVC has been made visible onscreen. Test
// for this case by checking if the parent container VC is currently in the
// process of being presented.
DCHECK(self.visible || self.dismissingModal ||
// 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.viewVisible) {
UrlLoadParams params = UrlLoadParams::InNewTab(GURL(kChromeUINewTabURL));
params.web_params.transition_type = ui::PAGE_TRANSITION_TYPED;
params.in_incognito = self.isOffTheRecord;
params.inherit_opener = inheritOpener;
- (void)appendTabAddedCompletion:(ProceduralBlock)tabAddedCompletion {
if (tabAddedCompletion) {
if (self.foregroundTabWasAddedCompletionBlock) {
ProceduralBlock oldForegroundTabWasAddedCompletionBlock =
self.foregroundTabWasAddedCompletionBlock = ^{
} else {
self.foregroundTabWasAddedCompletionBlock = tabAddedCompletion;
// TODO( Move this to a browser agent or web event mediator.
- (void)expectNewForegroundTab {
_expectingForegroundTab = YES;
- (void)startVoiceSearch {
// Delay Voice Search until new tab animations have finished.
if (self.inNewTabAnimation) {
_startVoiceSearchAfterNewTabAnimation = YES;
// Keyboard shouldn't overlay the ecoutez window, so dismiss find in page and
// dismiss the keyboard.
[self.dispatcher closeFindInPage];
[self.textZoomHandler closeTextZoom];
[[self viewForWebState:self.currentWebState] endEditing:NO];
// Ensure that voice search objects are created.
[self ensureVoiceSearchControllerCreated];
// Present voice search.
[self.omniboxHandler cancelOmniboxEdit];
// TODO( Get rid of this (move it to MetricsMediator). Looks
// like its busted for kSingleNTP in any case?
- (int)liveNTPCount {
NSUInteger count = 0;
WebStateList* webStateList = self.browser->GetWebStateList();
for (int i = 0; i < webStateList->count(); i++) {
web::WebState* webState = webStateList->GetWebStateAt(i);
auto found = _ntpCoordinatorsForWebStates.find(webState);
if (found != _ntpCoordinatorsForWebStates.end() &&
[found->second isStarted]) {
return count;
#pragma mark - browser_view_controller+private.h
- (void)setActive:(BOOL)active {
if (_active == active) {
_active = active;
// TODO( Move these updates to BrowserCoordinator.
if (self.browserState) {
// TODO( Refactor ActiveStateManager for multiwindow.
ActiveStateManager* active_state_manager =
self.webUsageEnabled = active;
[self updateBroadcastState];
// Stop the NTP on web usage toggle. This happens when clearing browser
// data, and forces the NTP to be recreated in -displayWebState below.
// TODO( Move this to the NewTabPageTabHelper when
// WebStateObserver has a webUsage callback.
if (!active) {
if (IsSingleNtpEnabled()) {
[self stopNTP];
} else {
for (const auto& element : _ntpCoordinatorsForWebStates)
[element.second stop];
if (active) {
// Make sure the tab (if any; it's possible to get here without a current
// tab if the caller is about to create one) ends up on screen completely.
// Force loading the view in case it was not loaded yet.
[self loadViewIfNeeded];
// TODO( Move this to a browser agent or web event
// mediator.
if (self.currentWebState && _expectingForegroundTab) {
if (self.currentWebState)
[self displayWebState:self.currentWebState];
[self setNeedsStatusBarAppearanceUpdate];
// TODO( Federate ClearPresentedState.
- (void)clearPresentedStateWithCompletion:(ProceduralBlock)completion
dismissOmnibox:(BOOL)dismissOmnibox {
[_bookmarkInteractionController dismissBookmarkModalControllerAnimated:NO];
[_bookmarkInteractionController dismissSnackbar];
if (dismissOmnibox) {
[self.omniboxHandler cancelOmniboxEdit];
[self.helpHandler hideAllHelpBubbles];
[_voiceSearchController dismissMicPermissionHelp];
web::WebState* webState = self.currentWebState;
if (webState) {
[self.dispatcher closeFindInPage];
[self.textZoomHandler closeTextZoom];
// TODO( This will need to be called on the
// PopupMenuCommands handler.
[self.dispatcher dismissPopupMenuAnimated:NO];
if (self.presentedViewController) {
// 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) {
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.4 * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
} else if (completion) {
// If no view controllers are presented, we should be ok with dispatching
// the completion block directly.
dispatch_async(dispatch_get_main_queue(), completion);
- (void)animateOpenBackgroundTabFromOriginPoint:(CGPoint)originPoint
completion:(void (^)())completion {
if ([self canShowTabStrip] || CGPointEqualToPoint(originPoint, CGPointZero)) {
} else {
self.inNewTabAnimation = YES;
// Exit fullscreen if needed.
const CGFloat kAnimatedViewSize = 50;
BackgroundTabAnimationView* animatedView =
[[BackgroundTabAnimationView alloc]
initWithFrame:CGRectMake(0, 0, kAnimatedViewSize, kAnimatedViewSize)
__weak UIView* weakAnimatedView = animatedView;
auto completionBlock = ^() {
self.inNewTabAnimation = NO;
[weakAnimatedView removeFromSuperview];
[self.view addSubview:animatedView];
[animatedView animateFrom:originPoint
- (void)shutdown {
_isShutdown = YES;
[self setActive:NO];
// TODO( Move these updates to BrowserCoordinator.
if (self.browserState) {
TextToSpeechPlaybackController* controller =
if (controller)
UrlLoadingNotifierBrowserAgent* notifier =
if (notifier)
// Uninstall delegates so that any delegate callbacks triggered by subsequent
// WebStateDestroyed() signals are not handled.
WebStateList* webStateList = self.browser->GetWebStateList();
for (int index = 0; index < webStateList->count(); ++index)
[self uninstallDelegatesForWebState:webStateList->GetWebStateAt(index)];
// Disconnect child coordinators.
[self.popupMenuCoordinator stop];
if (base::FeatureList::IsEnabled(kModernTabStrip)) {
[self.tabStripCoordinator stop];
self.tabStripCoordinator = nil;
} else {
[self.legacyTabStripCoordinator stop];
self.legacyTabStripCoordinator = nil;
self.tabStripView = nil;
[self.commandDispatcher stopDispatchingToTarget:_bubblePresenter];
[_bubblePresenter stop];
_bubblePresenter = nil;
[self.commandDispatcher stopDispatchingToTarget:self];
self.browser = nullptr;
[self.contentArea removeGestureRecognizer:self.contentAreaGestureRecognizer];
[self.primaryToolbarCoordinator stop];
self.primaryToolbarCoordinator = nil;
[self.secondaryToolbarContainerCoordinator stop];
self.secondaryToolbarContainerCoordinator = nil;
[self.secondaryToolbarCoordinator stop];
self.secondaryToolbarCoordinator = nil;
[_downloadManagerCoordinator stop];
_downloadManagerCoordinator = nil;
self.toolbarInterface = nil;
_sideSwipeController = nil;
_allWebStateObservationForwarder = nullptr;
[_voiceSearchController disconnect];
_voiceSearchController = nil;
_fullscreenDisabler = nullptr;
[[NSNotificationCenter defaultCenter] removeObserver:self];
[_bookmarkInteractionController shutdown];
_bookmarkInteractionController = nil;
#pragma mark - NSObject
- (BOOL)accessibilityPerformEscape {
[self dismissPopups];
return YES;
#pragma mark - UIResponder
- (BOOL)canBecomeFirstResponder {
return YES;
- (NSArray*)keyCommands {
if (![self shouldRegisterKeyboardCommands]) {
return nil;
UIResponder* firstResponder = GetFirstResponder();
// TODO( Inject the key commands provider.
WebNavigationBrowserAgent* navigationAgent =
return [self.keyCommandsProvider
isKindOfClass:[UITextField class]] ||
isKindOfClass:[UITextView class]] ||
[ isKeyboardOnScreen]];
#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)shouldRegisterKeyboardCommands {
if (_isShutdown)
return NO;
if (!self.browser)
return NO;
if ([self presentedViewController])
return NO;
if (_voiceSearchController.visible)
return NO;
if (self.bottomPosition)
return NO;
return YES;
#pragma mark - UIViewController
- (void)viewDidLoad {
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.
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);
[self.typingShield addTarget:self
self.view.autoresizingMask = initialViewAutoresizing;
[self addChildViewController:self.browserContainerViewController];
[self.view addSubview:self.contentArea];
[self.browserContainerViewController didMoveToParentViewController:self];
[self.view addSubview:self.typingShield];
[super viewDidLoad];
// Install fake status bar for iPad iOS7
[self installFakeStatusBar];
_bubblePresenter.delegate = self;
_bubblePresenter.rootViewController = self;
_bubblePresenter.toolbarHandler =
HandlerForProtocol(self.browser->GetCommandDispatcher(), ToolbarCommands);
[self buildToolbarAndTabStrip];
[self setUpViewLayout:YES];
[self addConstraintsToToolbar];
// Finish initialization.
[self addUIFunctionalityForBrowserAndBrowserState];
// Add a tap gesture recognizer to save the last tap location for the source
// location of the new tab animation.
self.contentAreaGestureRecognizer = [[UITapGestureRecognizer alloc]
[self.contentAreaGestureRecognizer setDelegate:self];
[self.contentAreaGestureRecognizer setCancelsTouchesInView:NO];
[self.contentArea addGestureRecognizer:self.contentAreaGestureRecognizer];
self.view.backgroundColor = [UIColor colorNamed:kBackgroundColor];
- (void)viewSafeAreaInsetsDidChange {
[super viewSafeAreaInsetsDidChange];
[self setUpViewLayout:NO];
// Update the heights of the toolbars to account for the new insets.
self.primaryToolbarHeightConstraint.constant =
[self primaryToolbarHeightWithInset];
self.secondaryToolbarHeightConstraint.constant =
[self secondaryToolbarHeightWithInset];
self.secondaryToolbarNoFullscreenHeightConstraint.constant =
[self secondaryToolbarHeightWithInset];
// Update the tab strip placement.
if (self.tabStripView) {
[self showTabStripView:self.tabStripView];
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
// Update the toolbar height to account for |topLayoutGuide| changes.
self.primaryToolbarHeightConstraint.constant =
[self primaryToolbarHeightWithInset];
if (self.isNTPActiveForCurrentWebState && self.webUsageEnabled) {
[self ntpCoordinatorForWebState:self.currentWebState]
.viewController.view.frame =
[self ntpFrameForWebState:self.currentWebState];
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
self.viewVisible = YES;
[self updateBroadcastState];
[self updateToolbarState];
// |viewDidAppear| can be called after |browserState| is destroyed. Since
// |presentBubblesIfEligible| requires that |self.browserState| is not NULL,
// check for |self.browserState| before calling the presenting the bubbles.
// TODO( determine if this check is still needed?
if (self.browserState) {
[self.helpHandler showHelpBubbleIfEligible];
[self.helpHandler showLongPressHelpBubbleIfEligible];
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
self.visible = YES;
// 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)
// Update the displayed WebState (if any; the switcher may not have created
// one yet) in case it changed while showing the switcher.
if (self.currentWebState)
[self displayWebState:self.currentWebState];
- (void)viewWillDisappear:(BOOL)animated {
self.viewVisible = NO;
[self updateBroadcastState];
web::WebState* activeWebState =
self.browser ? self.browser->GetWebStateList()->GetActiveWebState()
: nullptr;
if (activeWebState) {
if (!self.presentedViewController)
[_bookmarkInteractionController dismissSnackbar];
[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;
_voiceSearchController.dispatcher = nil;
[self.primaryToolbarCoordinator stop];
self.primaryToolbarCoordinator = nil;
[self.secondaryToolbarContainerCoordinator stop];
self.secondaryToolbarContainerCoordinator = nil;
[self.secondaryToolbarCoordinator stop];
self.secondaryToolbarCoordinator = nil;
self.toolbarInterface = nil;
_toolbarUIState = nil;
_locationBarModelDelegate = nil;
_locationBarModel = nil;
self.helper = nil;
if (base::FeatureList::IsEnabled(kModernTabStrip)) {
[self.tabStripCoordinator stop];
self.tabStripCoordinator = nil;
} else {
[self.legacyTabStripCoordinator stop];
self.legacyTabStripCoordinator = nil;
self.tabStripView = nil;
_sideSwipeController = nil;
- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
[super traitCollectionDidChange:previousTraitCollection];
// After |-shutdown| is called, |self.browserState| is invalid and will cause
// a crash.
if (!self.browserState || _isShutdown)
// TODO( - traitCollectionDidChange: is not always forwarded
// because in some cases the presented view controller isn't a child of the
// BVC in the view controller hierarchy (some intervening object isn't a
// view controller).
// Change the height of the secondary toolbar to show/hide it.
self.secondaryToolbarHeightConstraint.constant =
[self secondaryToolbarHeightWithInset];
self.secondaryToolbarNoFullscreenHeightConstraint.constant =
[self secondaryToolbarHeightWithInset];
[self updateFootersForFullscreenProgress:self.footerFullscreenProgress];
if (self.currentWebState) {
UIEdgeInsets contentPadding =
contentPadding.bottom = AlignValueToPixel(
self.footerFullscreenProgress * [self secondaryToolbarHeightWithInset]);
self.currentWebState->GetWebViewProxy().contentInset = contentPadding;
[self updateToolbarState];
// 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.dispatcher hideFindUI];
[self.textZoomHandler hideTextZoomUI];
// Update the toolbar visibility.
// TODO( Move this update to the toolbar view
// controller(s)?
[self updateToolbar];
// Update the tab strip visibility.
if (self.tabStripView) {
[self showTabStripView:self.tabStripView];
[self.tabStripView layoutSubviews];
if (base::FeatureList::IsEnabled(kModernTabStrip)) {
[self.tabStripCoordinator hideTabStrip:![self canShowTabStrip]];
} else {
[self.legacyTabStripCoordinator hideTabStrip:![self canShowTabStrip]];
_fakeStatusBarView.hidden = ![self canShowTabStrip];
[self addConstraintsToPrimaryToolbar];
// If tabstrip is coming back due to a window resize or screen rotation,
// reset the full screen controller to adjust the tabstrip position.
if (ShouldShowCompactToolbar(previousTraitCollection) &&
!ShouldShowCompactToolbar(self)) {
[self setNeedsStatusBarAppearanceUpdate];
- (void)viewWillTransitionToSize:(CGSize)size
(id<UIViewControllerTransitionCoordinator>)coordinator {
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
// After |-shutdown| is called, |self.browser| is invalid and will cause
// a crash.
if (_isShutdown)
[self dismissPopups];
__weak BrowserViewController* weakSelf = self;
id<UIViewControllerTransitionCoordinatorContext>) {
[weakSelf animateTransition];
completion:^(id<UIViewControllerTransitionCoordinatorContext>) {
[weakSelf completedTransition];
if (self.currentWebState) {
id<CRWWebViewProxy> webViewProxy = self.currentWebState->GetWebViewProxy();
[webViewProxy surfaceSizeChanged];
[[UIDevice currentDevice] orientation]);
- (void)animateTransition {
// Force updates of the toolbar state as the toolbar height might
// change on rotation.
[self updateToolbarState];
// Resize horizontal viewport if Smooth Scrolling is on.
if (fullscreen::features::ShouldUseSmoothScrolling()) {
- (void)completedTransition {
if (!base::FeatureList::IsEnabled(kModernTabStrip)) {
if (self.tabStripView) {
[self.legacyTabStripCoordinator tabStripSizeDidChange];
- (void)dismissViewControllerAnimated:(BOOL)flag
completion:(void (^)())completion {
if (!self.presentedViewController) {
// TODO( On iOS10, UIDocumentMenuViewController and
// WKFileUploadPanel somehow combine to call dismiss twice instead of once.
// The second call would dismiss the BVC itself, so look for that case and
// return early.
// TODO( A similar bug exists on all iOS versions with
// WKFileUploadPanel and UIDocumentPickerViewController.
// To make M65 as safe as possible, return early whenever this method is
// invoked but no VC appears to be presented. These cases will always end
// up dismissing the BVC itself, which would put the app into an
// unresponsive state.
// Some calling code invokes |dismissViewControllerAnimated:completion:|
// multiple times. Because the BVC is presented, subsequent calls end up
// dismissing the BVC itself. This is never what should happen, so check for
// this case and return early. It is not enough to check
// |self.dismissingModal| because some dismissals do not go through
// -[BrowserViewController dismissViewControllerAnimated:completion:|.
// TODO( Fix callers and remove this early return.
if (self.dismissingModal || self.presentedViewController.isBeingDismissed) {
self.dismissingModal = YES;
__weak BrowserViewController* weakSelf = self;
[super dismissViewControllerAnimated:flag
BrowserViewController* strongSelf = weakSelf;
strongSelf.dismissingModal = NO;
if (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
completion:(void (^)())completion {
ProceduralBlock finalCompletionHandler = [completion copy];
// TODO( 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 =
if ([navController.topViewController
isMemberOfClass:[WelcomeToChromeViewController class]] ||
isKindOfClass:[PromoStyleViewController class]]) {
self.hideStatusBar = YES;
// Load view from Launch Screen and add it to window.
NSBundle* mainBundle = base::mac::FrameworkBundle();
NSArray* topObjects = [mainBundle loadNibNamed:@"LaunchScreen"
UIViewController* launchScreenController =
base::mac::ObjCCastStrict<UIViewController>([topObjects lastObject]);
// |launchScreenView| is loaded as an autoreleased object, and is retained
// by the |completion| block below.
UIView* launchScreenView = launchScreenController.view;
launchScreenView.userInteractionEnabled = NO;
// TODO( 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.view addSubview:launchScreenView];
[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)
if ([self.sideSwipeController inSwipe]) {
[self.sideSwipeController resetContentView];
void (^superCall)() = ^{
[super presentViewController:viewControllerToPresent
// TODO( 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]) {
} else {
- (BOOL)shouldAutorotate {
if (self.presentedViewController.beingPresented ||
self.presentedViewController.beingDismissed) {
// Don't rotate while a presentation or dismissal animation is occurring.
return NO;
} else if (_sideSwipeController &&
![self.sideSwipeController shouldAutorotate]) {
// Don't auto rotate if side swipe controller view says not to.
return NO;
} else {
return [super shouldAutorotate];
- (UIStatusBarStyle)preferredStatusBarStyle {
if ([self canShowTabStrip] && !_isOffTheRecord &&
!base::FeatureList::IsEnabled(kModernTabStrip)) {
return self.tabStripView.frame.origin.y < kTabStripAppearanceOffset
? UIStatusBarStyleDefault
: UIStatusBarStyleLightContent;
return _isOffTheRecord ? UIStatusBarStyleLightContent
: UIStatusBarStyleDefault;
#pragma mark - ** Private BVC Methods **
#pragma mark - Private Methods: BVC Initialization
// BVC initialization
// ------------------
// If the BVC is initialized with a valid browser state & browser immediately,
// the path is straightforward: functionality is enabled, and the UI is built
// when -viewDidLoad is called.
// If the BVC is initialized without a browser state or browser, the browser
// and browser state may or may not be provided before -viewDidLoad is called.
// In most cases, they will not, to improve startup performance.
// In order to handle this, initialization of various aspects of BVC have been
// broken out into the following functions, which have expectations (enforced
// with DCHECKs) regarding |self.browserState|, |self.browser|, and [self
// isViewLoaded].
// Updates non-view-related functionality with the given browser and tab
// model.
// Does not matter whether or not the view has been loaded.
// TODO( Move this all into the init. Update the rest of the
// code to assume that if the BVC isn't shutdown, it has a valid Browser. Update
// the comments above to reflect reality.
- (void)updateWithBrowser:(Browser*)browser {
self.browser = browser;
_isOffTheRecord = self.browserState->IsOffTheRecord();
_webStateObserverBridge = std::make_unique<web::WebStateObserverBridge>(self);
_allWebStateObservationForwarder =
self.browser->GetWebStateList(), _webStateObserverBridge.get());
_webStateListObserver = std::make_unique<WebStateListObserverBridge>(self);
_URLLoadingObserverBridge = std::make_unique<UrlLoadingObserverBridge>(self);
WebStateList* webStateList = self.browser->GetWebStateList();
for (int index = 0; index < webStateList->count(); ++index)
[self installDelegatesForWebState:webStateList->GetWebStateAt(index)];
// Set the TTS playback controller's WebStateList.
// TODO( Move this somewhere else -- BrowserCoordinator at
// least.
// 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;
// On iOS7, iPad should match iOS6 status bar. Install a simple black bar under
// the status bar to mimic this layout.
- (void)installFakeStatusBar {
// This method is called when the view is loaded and when the thumb strip is
// installed via addAnimatee -> didAnimateViewRevealFromState ->
// installFakeStatusBar.
// Remove the _fakeStatusBarView if present.
[_fakeStatusBarView removeFromSuperview];
_fakeStatusBarView = nil;
if (self.thumbStripEnabled &&
!fullscreen::features::ShouldUseSmoothScrolling()) {
// A fake status bar on the browser view is not necessary when the thumb
// strip feature is enabled because the view behind the browser view already
// has a dark background. Adding a fake status bar would block the
// visibility of the thumb strip thumbnails when moving the browser view.
// However, if the Fullscreen Provider is used, then the web content extends
// up to behind the tab strip, making the fake status bar necessary.
CGRect statusBarFrame = CGRectMake(0, 0, CGRectGetWidth(self.view.bounds), 0);
_fakeStatusBarView = [[UIView alloc] initWithFrame:statusBarFrame];
[_fakeStatusBarView setAutoresizingMask:UIViewAutoresizingFlexibleWidth];
if (ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET) {
_fakeStatusBarView.backgroundColor = UIColor.blackColor;
_fakeStatusBarView.autoresizingMask = UIViewAutoresizingFlexibleWidth;
[self.view insertSubview:_fakeStatusBarView aboveSubview:self.contentArea];
} else {
// Add a white bar when there is no tab strip so that the status bar on the
// NTP is white.
_fakeStatusBarView.backgroundColor = ntp_home::kNTPBackgroundColor();
[self.view insertSubview:_fakeStatusBarView atIndex:0];
// Builds the UI parts of tab strip and the toolbar. Does not matter whether
// or not browser state and browser are valid.
- (void)buildToolbarAndTabStrip {
DCHECK([self isViewLoaded]);
// Create the location bar model and controller.
// TODO( Move all of this to the coordinator which owns the
// location bar.
new LocationBarModelDelegateIOS(self.browser->GetWebStateList()));
_locationBarModel = std::make_unique<LocationBarModelImpl>(
_locationBarModelDelegate.get(), kMaxURLDisplayChars);
// TODO( Remove |self.helper|.
self.helper = [_dependencyFactory newBrowserViewControllerHelper];
// TODO( Move this coordinator to BrowserCoordinator
self.popupMenuCoordinator =
[[PopupMenuCoordinator alloc] initWithBaseViewController:self
self.popupMenuCoordinator.bubblePresenter = _bubblePresenter;
self.popupMenuCoordinator.UIUpdater = _toolbarCoordinatorAdaptor;
[self.popupMenuCoordinator start];
// TODO( Inject this coordinator instead of creating it.
PrimaryToolbarCoordinator* topToolbarCoordinator =
[[PrimaryToolbarCoordinator alloc] initWithBrowser:self.browser];
self.primaryToolbarCoordinator = topToolbarCoordinator;
topToolbarCoordinator.delegate = self;
topToolbarCoordinator.popupPresenterDelegate = self;
topToolbarCoordinator.longPressDelegate = self.popupMenuCoordinator;
[topToolbarCoordinator start];
// TODO( Inject this coordinator instead of creating it.
SecondaryToolbarCoordinator* bottomToolbarCoordinator =
[[SecondaryToolbarCoordinator alloc] initWithBrowser:self.browser];
self.secondaryToolbarCoordinator = bottomToolbarCoordinator;
bottomToolbarCoordinator.longPressDelegate = self.popupMenuCoordinator;
// TODO( Finish ToolbarContainer work.
if (base::FeatureList::IsEnabled(
toolbar_container::kToolbarContainerEnabled)) {
self.secondaryToolbarContainerCoordinator =
[[ToolbarContainerCoordinator alloc]
self.secondaryToolbarContainerCoordinator.toolbarCoordinators =
@[ bottomToolbarCoordinator ];
[self.secondaryToolbarContainerCoordinator start];
} else {
[bottomToolbarCoordinator start];
[_toolbarCoordinatorAdaptor addToolbarCoordinator:topToolbarCoordinator];
[_toolbarCoordinatorAdaptor addToolbarCoordinator:bottomToolbarCoordinator];
self.sideSwipeController.toolbarInteractionHandler = self.toolbarInterface;
self.sideSwipeController.primaryToolbarSnapshotProvider =
self.sideSwipeController.secondaryToolbarSnapshotProvider =
[self updateBroadcastState];
if (_voiceSearchController) {
// TODO( Inject LoadQueryCommands as a handler and pass
// into the voice search controller.
_voiceSearchController.dispatcher =
HandlerForProtocol(self.commandDispatcher, LoadQueryCommands);
// TODO( Move tab strip setup to BrowserCoordinator.
// Potentially inject these coordinators as a stopgap.
if (ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET) {
if (base::FeatureList::IsEnabled(kModernTabStrip)) {
self.tabStripCoordinator =
[[TabStripCoordinator alloc] initWithBrowser:self.browser];
[self.tabStripCoordinator start];
} else {
self.legacyTabStripCoordinator = [[TabStripLegacyCoordinator alloc]
self.legacyTabStripCoordinator.presentationProvider = self;
self.legacyTabStripCoordinator.animationWaitDuration =
self.legacyTabStripCoordinator.longPressDelegate =
[self.legacyTabStripCoordinator start];
// Called by NSNotificationCenter when the view's window becomes key to account
// for topLayoutGuide length updates.
- (void)updateToolbarHeightForKeyWindow {
// Update the toolbar height to account for |topLayoutGuide| changes.
self.primaryToolbarHeightConstraint.constant =
[self primaryToolbarHeightWithInset];
// Stop listening for the key window notification.
[[NSNotificationCenter defaultCenter]
// The height of the primary toolbar with the top safe area inset included.
- (CGFloat)primaryToolbarHeightWithInset {
UIView* primaryToolbar = self.primaryToolbarCoordinator.viewController.view;
CGFloat intrinsicHeight = primaryToolbar.intrinsicContentSize.height;
if (!IsSplitToolbarMode(self)) {
// When the adaptive toolbar is unsplit, add a margin.
intrinsicHeight += kTopToolbarUnsplitMargin;
// If the primary toolbar is not the topmost header, it does not overlap with
// the unsafe area.
// TODO( Update implementation such that this calculates the
// topmost header's height.
UIView* topmostHeader = [self.headerViews firstObject].view;
if (primaryToolbar != topmostHeader)
return intrinsicHeight;
// If the primary toolbar is topmost, subtract the height of the portion of
// the unsafe area.
CGFloat unsafeHeight =;
// The topmost header is laid out |headerOffset| from the top of |view|, so
// subtract that from the unsafe height.
unsafeHeight -= self.headerOffset;
return intrinsicHeight + unsafeHeight;
// The height of the secondary toolbar with the bottom safe area inset included.
// Returns 0 if the toolbar should be hidden.
- (CGFloat)secondaryToolbarHeightWithInset {
if (!IsSplitToolbarMode(self))
return 0;
UIView* secondaryToolbar =
// Add the safe area inset to the toolbar height.
CGFloat unsafeHeight = self.rootSafeAreaInsets.bottom;
return secondaryToolbar.intrinsicContentSize.height + unsafeHeight;
- (void)addConstraintsToTabStrip {
if (!base::FeatureList::IsEnabled(kModernTabStrip))
self.tabStripView.translatesAutoresizingMaskIntoConstraints = NO;
[NSLayoutConstraint activateConstraints:@[
[self.tabStripView.heightAnchor constraintEqualToConstant:kTabStripHeight],
// Sets up the constraints on the toolbar.
- (void)addConstraintsToPrimaryToolbar {
NSLayoutYAxisAnchor* topAnchor;
// On iPad, the toolbar is underneath the tab strip.
// On iPhone, it is underneath the top of the screen.
if ([self canShowTabStrip]) {
topAnchor = self.tabStripView.bottomAnchor;
} else {
topAnchor = [self view].topAnchor;
// Only add leading and trailing constraints once as they are never updated.
// This uses the existence of |primaryToolbarOffsetConstraint| as a proxy for
// whether we've already added the leading and trailing constraints.
if (!self.primaryToolbarOffsetConstraint) {
[NSLayoutConstraint activateConstraints:@[
constraintEqualToAnchor:[self view].leadingAnchor],
constraintEqualToAnchor:[self view].trailingAnchor],
// Offset and Height can be updated, so reset first. = NO; = NO;
// Create a constraint for the vertical positioning of the toolbar.
UIView* primaryView = self.primaryToolbarCoordinator.viewController.view;
self.primaryToolbarOffsetConstraint =
[primaryView.topAnchor constraintEqualToAnchor:topAnchor];
// Create a constraint for the height of the toolbar to include the unsafe
// area height.
self.primaryToolbarHeightConstraint = [primaryView.heightAnchor
constraintEqualToConstant:[self primaryToolbarHeightWithInset]]; = YES; = YES;
- (void)addConstraintsToSecondaryToolbar {
if (self.secondaryToolbarCoordinator) {
// Create a constraint for the height of the toolbar to include the unsafe
// area height.
UIView* toolbarView = self.secondaryToolbarCoordinator.viewController.view;
self.secondaryToolbarHeightConstraint = [toolbarView.heightAnchor
constraintEqualToConstant:[self secondaryToolbarHeightWithInset]]; = YES;
self.secondaryToolbarContainerView, toolbarView,
LayoutSides::kBottom | LayoutSides::kLeading | LayoutSides::kTrailing);
// Constrain the container view to the bottom of self.view, and add a
// constant height constraint such that the container's frame is equal to
// that of the secondary toolbar at a fullscreen progress of 1.0.
UIView* containerView = self.secondaryToolbarContainerView;
self.secondaryToolbarNoFullscreenHeightConstraint =
constraintEqualToConstant:[self secondaryToolbarHeightWithInset]]; = YES;
self.view, containerView,
LayoutSides::kBottom | LayoutSides::kLeading | LayoutSides::kTrailing);
// Adds constraints to the secondary toolbar container anchoring it to the
// bottom of the browser view.
- (void)addConstraintsToSecondaryToolbarContainer {
if (!self.secondaryToolbarContainerCoordinator)
// Constrain the container to the bottom of the view.
UIView* containerView =
self.view, containerView,
LayoutSides::kBottom | LayoutSides::kLeading | LayoutSides::kTrailing);
// Adds constraints to the primary and secondary toolbars, anchoring them to the
// top and bottom of the browser view.
- (void)addConstraintsToToolbar {
[self addConstraintsToPrimaryToolbar];
// TODO( Finish ToolbarContainer work.
if (base::FeatureList::IsEnabled(
toolbar_container::kToolbarContainerEnabled)) {
[self addConstraintsToSecondaryToolbarContainer];
} else {
[self addConstraintsToSecondaryToolbar];
[[self view] layoutIfNeeded];
// Updates view-related functionality with the given browser and browser
// state. The view must have been loaded. Uses |self.browserState| and
// |self.browser|.
- (void)addUIFunctionalityForBrowserAndBrowserState {
DCHECK([self isViewLoaded]);
[self.sideSwipeController addHorizontalGesturesToView:self.view];
// DownloadManagerCoordinator is already created.
_downloadManagerCoordinator.bottomMarginHeightAnchor =
[NamedGuide guideWithName:kSecondaryToolbarGuide view:self.contentArea]
// TODO( Inject this handler.
self.helpHandler =
HandlerForProtocol(self.browser->GetCommandDispatcher(), HelpCommands);
// TODO( Assuming all of the coordinators are in
// BrowserCoordinator, move this setup there as well.
self.primaryToolbarCoordinator.longPressDelegate = self.popupMenuCoordinator;
self.secondaryToolbarCoordinator.longPressDelegate =
if (!base::FeatureList::IsEnabled(kModernTabStrip)) {
self.legacyTabStripCoordinator.longPressDelegate =
// TODO( Inject this handler.
self.omniboxHandler =
HandlerForProtocol(self.browser->GetCommandDispatcher(), OmniboxCommands);
// Sets up the frame for the fake status bar. View must be loaded.
- (void)setupStatusBarLayout {
CGFloat topInset =;
// Update the fake toolbar background height.
CGRect fakeStatusBarFrame = _fakeStatusBarView.frame;
fakeStatusBarFrame.size.height = topInset;
_fakeStatusBarView.frame = fakeStatusBarFrame;
// Sets the correct frame and hierarchy for subviews and helper views. Only
// insert views on |initialLayout|.
- (void)setUpViewLayout:(BOOL)initialLayout {
DCHECK([self isViewLoaded]);
[self setupStatusBarLayout];
if (initialLayout) {
// Add the toolbars as child view controllers.
[self addChildViewController:self.primaryToolbarCoordinator.viewController];
if (self.secondaryToolbarCoordinator) {
// TODO( Finish ToolbarContainer work.
if (base::FeatureList::IsEnabled(
toolbar_container::kToolbarContainerEnabled)) {
[self addChildViewController:self.secondaryToolbarContainerCoordinator
} else {
[self addChildViewController:self.secondaryToolbarCoordinator
// Add the primary toolbar. On iPad, it should be in front of the tab strip
// because the tab strip slides behind it when showing the thumb strip.
UIView* primaryToolbarView =
if (ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET) {
if (base::FeatureList::IsEnabled(kModernTabStrip) &&
self.tabStripCoordinator) {
[self addChildViewController:self.tabStripCoordinator.viewController];
self.tabStripView = self.tabStripCoordinator.view;
[self.view addSubview:self.tabStripView];
[self addConstraintsToTabStrip];
[self.view insertSubview:primaryToolbarView
} else {
[self.view addSubview:primaryToolbarView];
// Add the secondary toolbar.
if (self.secondaryToolbarCoordinator) {
// TODO( Finish ToolbarContainer work.
if (base::FeatureList::IsEnabled(
toolbar_container::kToolbarContainerEnabled)) {
// Add the container view to the hierarchy.
UIView* containerView =
[self.view insertSubview:containerView aboveSubview:primaryToolbarView];
} else {
// Create the container view for the secondary toolbar and add it to
// the hierarchy
UIView* container = [[LegacyToolbarContainerView alloc] init];
container.translatesAutoresizingMaskIntoConstraints = NO;
[self.view insertSubview:container aboveSubview:primaryToolbarView];
self.secondaryToolbarContainerView = container;
// Create the NamedGuides and add them to the browser view.
NSArray<GuideName*>* guideNames = @[
AddNamedGuidesToView(guideNames, self.view);
// Configure the content area guide.
NamedGuide* contentAreaGuide = [NamedGuide guideWithName:kContentAreaGuide
// TODO( 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 cleared.
UIView* rootViewForToolbar = ViewHierarchyRootForView(primaryToolbarView);
UIView* rootViewForContentGuide =
CHECK_EQ(rootViewForToolbar, rootViewForContentGuide);
// Constrain top to bottom of top toolbar.
.active = YES;
LayoutSides contentSides = LayoutSides::kLeading | LayoutSides::kTrailing;
if (self.secondaryToolbarCoordinator) {
// If there's a bottom toolbar, the content area guide is constrained to
// its top.
UIView* secondaryToolbarView =
.active = YES;
} else {
// Otherwise, the content area guide is constrained to self.view's bootom
// along with its sides;
contentSides = contentSides | LayoutSides::kBottom;
AddSameConstraintsToSides(self.view, contentAreaGuide, contentSides);
// Complete child UIViewController containment flow now that the views are
// finished being added.
if (self.secondaryToolbarCoordinator) {
// TODO( Finish ToolbarContainer work.
if (base::FeatureList::IsEnabled(
toolbar_container::kToolbarContainerEnabled)) {
} else {
// Resize the typing shield to cover the entire browser view and bring it to
// the front.
self.typingShield.frame = self.contentArea.frame;
[self.view bringSubviewToFront:self.typingShield];
// Move the overlay containers in front of the hierarchy.
[self updateOverlayContainerOrder];
// TODO( Have a mediator inject the view to be displayed, not
// a webstate. Makes |webState| the currently visible WebState, displaying its
// view.
- (void)displayWebState:(web::WebState*)webState {
[self loadViewIfNeeded];
if (IsSingleNtpEnabled()) {
self.ntpCoordinator.webState = webState;
// Set this before triggering any of the possible page loads below.
if (!self.inNewTabAnimation) {
// TODO( -updateToolbar will move out of the BVC; make
// sure this comment remains accurate. Hide findbar. |updateToolbar| will
// restore the findbar later.
[self.dispatcher 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 webStateViewFrame = self.contentArea.bounds;
if (fullscreen::features::ShouldUseSmoothScrolling()) {
// If the view was translated for the thumb strip, make sure to re-apply
// that translation here.
if (self.viewTranslatedForSmoothScrolling) {
CGFloat toolbarHeight = [self expandedTopToolbarHeight];
webStateViewFrame = UIEdgeInsetsInsetRect(
webStateViewFrame, UIEdgeInsetsMake(toolbarHeight, 0, 0, 0));
} else {
// 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 =
webStateViewFrame =
UIEdgeInsetsInsetRect(webStateViewFrame, viewportInsets);
[self viewForWebState:webState].frame = webStateViewFrame;
[self updateToolbarState];
NewTabPageTabHelper* NTPHelper =
if (NTPHelper && NTPHelper->IsActive()) {
NewTabPageCoordinator* coordinator =
[self ntpCoordinatorForWebState:webState];
UIViewController* viewController = coordinator.viewController;
[coordinator ntpDidChangeVisibility:YES];
viewController.view.frame = [self ntpFrameForWebState:webState];
[viewController.view layoutIfNeeded];
// TODO( For a newly created WebState, the session will
// not be restored until LoadIfNecessary call. Remove when fixed.
self.browserContainerViewController.contentView = nil;
self.browserContainerViewController.contentViewController =
[coordinator constrainDiscoverHeaderMenuButtonNamedGuide];
} else {
self.browserContainerViewController.contentView =
[self viewForWebState:webState];
// Resize horizontal viewport if Smooth Scrolling is on.
if (fullscreen::features::ShouldUseSmoothScrolling()) {
// TODO( Move this update to the toolbar coordinator,
// somehow.
[self updateToolbar];
// TODO( 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.
// TODO( This update should happen in the mediator, not
// here.
// Initializes the bookmark interaction controller if not already initialized.
- (void)initializeBookmarkInteractionController {
if (_bookmarkInteractionController)
// TODO( Remove BookmarkInteractionController from BVC.
_bookmarkInteractionController =
[[BookmarkInteractionController alloc] initWithBrowser:self.browser
- (void)updateOverlayContainerOrder {
// Both infobar overlay container views should exist in front of the entire
// browser UI, and the banner container should appear behind the modal
// container.
[self bringOverlayContainerToFront:
[self bringOverlayContainerToFront:
- (void)bringOverlayContainerToFront:
(UIViewController*)containerViewController {
[self.view bringSubviewToFront:containerViewController.view];
// If |containerViewController| is presenting a view over its current context,
// its presentation container view is added as a sibling to
// |containerViewController|'s view. This presented view should be brought in
// front of the container view.
UIView* presentedContainerView =
if (presentedContainerView.superview == self.view)
[self.view bringSubviewToFront:presentedContainerView];
#pragma mark - Private Methods: UI Configuration, update and Layout
// TODO( Move this update to primaryToolbarCoordinator,
// triggered by mediators as apprpriate. Update the state of back and forward
// buttons, hiding the forward button if there is nowhere to go. Assumes the
// model's current tab is up to date.
- (void)updateToolbar {
// If the BVC has been partially torn down for low memory, wait for the
// view rebuild to handle toolbar updates.
if (!(self.helper && self.browserState))
web::WebState* webState = self.currentWebState;
if (!webState)
// TODO( Remove all use of the prerender service from BVC
BOOL isPrerendered =
(_prerenderService && _prerenderService->IsLoadingPrerender());
// TODO( Change -isToolbarLoading method to a free
// function.
if (isPrerendered && ![self.helper isToolbarLoading:self.currentWebState])
[self.primaryToolbarCoordinator showPrerenderingAnimation];
[self.dispatcher showFindUIIfActive];
[self.textZoomHandler showTextZoomUIIfActive];
BOOL hideToolbar = NO;
if (webState) {
// There are times when the NTP can be hidden but before the visibleURL
// changes. This can leave the BVC in a blank state where only the bottom
// toolbar is visible. Instead, if possible, use the NewTabPageTabHelper
// IsActive() value rather than checking -IsVisibleURLNewTabPage.
NewTabPageTabHelper* NTPHelper =
BOOL isNTP = NTPHelper && NTPHelper->IsActive();
// Hide the toolbar when displaying content suggestions without the tab
// strip, without the focused omnibox, and for UI Refresh, only when in
// split toolbar mode.
hideToolbar = isNTP && !_isOffTheRecord &&
![self.primaryToolbarCoordinator isOmniboxFirstResponder] &&
![self.primaryToolbarCoordinator showingOmniboxPopup] &&
![self canShowTabStrip] && IsSplitToolbarMode(self);
[self.primaryToolbarCoordinator.viewController.view setHidden:hideToolbar];
// 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.viewVisible;
// Dismisses popups and modal dialogs that are displayed above the BVC upon size
// changes (e.g. rotation, resizing,…) or when the accessibility escape gesture
// is performed.
// TODO( Support size changes for all popups and modal
// dialogs.
- (void)dismissPopups {
// The dispatcher may not be fully connected during shutdown, so selectors may
// be unrecognized.
if (_isShutdown)
// TODO( This will need to be called on the
// PopupMenuCommands handler.
[self.dispatcher dismissPopupMenuAnimated:NO];
[self.helpHandler hideAllHelpBubbles];
// Returns the footer view if one exists (e.g. the voice search bar).
- (UIView*)footerView {
return self.secondaryToolbarCoordinator.viewController.view;
// Returns the appropriate frame for the NTP.
- (CGRect)ntpFrameForWebState:(web::WebState*)webState {
NewTabPageTabHelper* NTPHelper = NewTabPageTabHelper::FromWebState(webState);
DCHECK(NTPHelper && NTPHelper->IsActive());
// NTP is laid out only in the visible part of the screen.
UIEdgeInsets viewportInsets = UIEdgeInsetsZero;
if (!IsRegularXRegularSizeClass(self)) {
viewportInsets.bottom = [self secondaryToolbarHeightWithInset];
// 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
if (!IsSplitToolbarMode(self) || self.isOffTheRecord) { = [self expandedTopToolbarHeight];
return UIEdgeInsetsInsetRect(self.contentArea.bounds, viewportInsets);
// Sets the frame for the headers.
- (void)setFramesForHeaders:(NSArray<HeaderDefinition*>*)headers
atOffset:(CGFloat)headerOffset {
CGFloat height = self.headerOffset;
for (HeaderDefinition* header in headers) {
CGFloat yOrigin = height - headerOffset;
BOOL isPrimaryToolbar =
header.view == self.primaryToolbarCoordinator.viewController.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( This can be cleaned up when the new fullscreen
// is enabled.
if (isPrimaryToolbar && ![self canShowTabStrip]) {
self.primaryToolbarOffsetConstraint.constant = yOrigin;
CGRect frame = [header.view frame];
frame.origin.y = yOrigin;
[header.view setFrame:frame];
if (header.behaviour != Overlap)
height += CGRectGetHeight(frame);
if (header.view == self.tabStripView)
[self setNeedsStatusBarAppearanceUpdate];
- (UIView*)viewForWebState:(web::WebState*)webState {
if (!webState)
return nil;
NewTabPageTabHelper* NTPHelper = NewTabPageTabHelper::FromWebState(webState);
if (NTPHelper && NTPHelper->IsActive()) {
return [self ntpCoordinatorForWebState:webState].viewController.view;
DCHECK(self.browser->GetWebStateList()->GetIndexOfWebState(webState) !=
TabUsageRecorderBrowserAgent* tabUsageRecoder =
// TODO( Move |RecordPageLoadStart| to TabUsageRecorder.
if (webState->IsEvicted() && tabUsageRecoder) {
if (!webState->IsCrashed()) {
// Load the page if it was evicted by browsing data clearing logic.
return webState->GetView();
#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
_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) {
UIView* view = gestureRecognizer.view;
CGPoint viewCoordinate = [gestureRecognizer locationInView:view];
_lastTapPoint = [[view superview] convertPoint:viewCoordinate
_lastTapTime = CACurrentMediaTime();
// This is a workaround for a bug in iOS multiwindow, in which you can touch a
// webView without the window getting the keyboard focus.
// The result is that a field in the new window gains focus, but keyboard
// typing continue to happen in the other window.
// TODO( Remove this workaround.
.appState.lastTappedWindow = view.window;
#pragma mark - Private Methods: Tab creation and selection
// DEPRECATED -- Do not add further logic to this method.
// Add all delegates to the provided |webState|.
// Unregistration happens when the WebState is removed from the WebStateList.
// TODO( Remove this method.
- (void)installDelegatesForWebState:(web::WebState*)webState {
// If the WebState is unrealized, don't install the delegate. Instead they
// will be installed when -webStateRealized: method is called.
if (!webState->IsRealized())
// TODO( Remove all use of the prerender service from BVC
// There should be no pre-rendered Tabs for this BrowserState.
DCHECK(!_prerenderService ||
// TODO( Remove CaptivePortalTabHelper from the BVC.
CaptivePortalTabHelper::CreateForWebState(webState, self);
// DEPRECATED -- Do not add further logic to this method.
// Remove delegates from the provided |webState|.
// TODO( Remove this method.
- (void)uninstallDelegatesForWebState:(web::WebState*)webState {
// If the WebState is unrealized, then the delegate had not been installed
// and thus don't need to be uninstalled.
if (!webState->IsRealized())
// TODO( Have BrowserCoordinator manage the NTP.
// No need to stop _ntpCoordinator with Single NTP enabled since shutdown will
// do that. In addition, uninstallDelegatesForWebState: is called for
// individual WebState removals, which should not trigger a stop.
if (!IsSingleNtpEnabled()) {
auto iterator = _ntpCoordinatorsForWebStates.find(webState);
if (iterator != _ntpCoordinatorsForWebStates.end()) {
[iterator->second stop];
// Called when a |webState| is selected in the WebStateList. Make any required
// view changes. The notification will not be sent when the |webState| is
// already the selected WebState. |notifyToolbar| indicates whether the toolbar
// is notified that the webState has changed.
- (void)webStateSelected:(web::WebState*)webState
notifyToolbar:(BOOL)notifyToolbar {
// 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.visible || !self.webUsageEnabled)
// TODO( Trigger this update from the mediator, or (as an
// interm step) pass the view to be displayed instead.
[self displayWebState:webState];
// TODO( Move this to a browser agent or web event
// mediator.
if (_expectingForegroundTab && !self.inNewTabAnimation) {
// Now that the new tab has been displayed, return to normal. Rather than
// keep a reference to the previous tab, just turn off preview mode for all
// tabs (since doing so is a no-op for the tabs that don't have it set).
_expectingForegroundTab = NO;
WebStateList* webStateList = self.browser->GetWebStateList();
for (int index = 0; index < webStateList->count(); ++index) {
web::WebState* webState = webStateList->GetWebStateAt(index);
#pragma mark - Private Methods: Voice Search
// Lazily instantiates |_voiceSearchController|.
- (void)ensureVoiceSearchControllerCreated {
if (_voiceSearchController)
// TODO( Move voice search controller to
// BrowserCoordinator, potentially refactoring to a coordinator.
_voiceSearchController =
if (self.primaryToolbarCoordinator) {
_voiceSearchController.dispatcher =
HandlerForProtocol(self.commandDispatcher, LoadQueryCommands);
#pragma mark - Private Methods: Reading List
// TODO( Remove these methods from the BVC.
// Adds the given urls to the reading list.
- (void)addURLsToReadingList:(NSArray<URLWithTitle*>*)URLs {
for (URLWithTitle* urlWithTitle in URLs) {
[self addURLToReadingList:urlWithTitle.URL withTitle:urlWithTitle.title];
[self.dispatcher triggerToolsMenuButtonAnimation];
NSString* text =
MDCSnackbarMessage* message = [MDCSnackbarMessage messageWithText:text];
message.accessibilityLabel = text;
message.duration = 2.0;
message.category = kBrowserViewControllerSnackbarCategory;
// TODO( This will need to be called on the
// SnackbarCommands handler.
[self.dispatcher showSnackbarMessage:message];
- (void)addURLToReadingList:(const GURL&)URL withTitle:(NSString*)title {
if (self.currentWebState &&
self.currentWebState->GetVisibleURL().spec() == URL.spec()) {
// Log UKM if the current page is being added to Reading List.
ukm::SourceId sourceID =
if (sourceID != ukm::kInvalidSourceId) {
ReadingListModel* readingModel =
readingModel->AddEntry(URL, base::SysNSStringToUTF8(title),
#pragma mark - Private SingleNTP feature helper methods
// Checks if there are any WebStates showing an NTP at this time. If not, then
// deconstructs |ntpCoordinator|.
- (void)stopNTPIfNeeded {
BOOL activeNTP = NO;
WebStateList* webStateList = self.browser->GetWebStateList();
for (int i = 0; i < webStateList->count(); i++) {
NewTabPageTabHelper* iterNtpHelper =
if (iterNtpHelper->IsActive()) {
activeNTP = YES;
if (!activeNTP) {
[self stopNTP];
- (void)stopNTP {
[_ntpCoordinator stop];
_ntpCoordinator = nullptr;
#pragma mark - ** Protocol Implementations and Helpers **
#pragma mark - ThumbStripSupporting
- (void)thumbStripEnabledWithPanHandler:
(ViewRevealingVerticalPanHandler*)panHandler {
DCHECK(![self isThumbStripEnabled]);
_thumbStripEnabled = YES;
self.thumbStripPanHandler = panHandler;
// Add self as animatee first to make sure that the BVC's view is loaded for
// the rest of setup
[panHandler addAnimatee:self];
DCHECK([self isViewLoaded]);
[panHandler addAnimatee:self.primaryToolbarCoordinator.animatee];
self.primaryToolbarCoordinator.panGestureHandler = panHandler;
if (!base::FeatureList::IsEnabled(kModernTabStrip)) {
self.legacyTabStripCoordinator.panGestureHandler = panHandler;
self.view.backgroundColor = UIColor.clearColor;
CGRect webStateViewFrame = self.contentArea.bounds;
if (self.thumbStripPanHandler.currentState == ViewRevealState::Revealed) {
CGFloat toolbarHeight = [self expandedTopToolbarHeight];
webStateViewFrame = UIEdgeInsetsInsetRect(
webStateViewFrame, UIEdgeInsetsMake(toolbarHeight, 0, 0, 0));
UIView* webStateView = [self viewForWebState:self.currentWebState];
webStateView.frame = webStateViewFrame;
if (IsSingleNtpEnabled()) {
} else {<