chromium/ios/chrome/browser/browser_view/ui_bundled/tab_events_mediator.mm

// Copyright 2022 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/tab_events_mediator.h"

#import "base/memory/raw_ptr.h"
#import "ios/chrome/browser/browser_view/ui_bundled/tab_consumer.h"
#import "ios/chrome/browser/feature_engagement/model/tracker_util.h"
#import "ios/chrome/browser/metrics/model/new_tab_page_uma.h"
#import "ios/chrome/browser/ntp/model/new_tab_page_tab_helper.h"
#import "ios/chrome/browser/ntp/ui_bundled/new_tab_page_coordinator.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/shared/model/web_state_list/all_web_state_observation_forwarder.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list_observer_bridge.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/snapshots/model/snapshot_tab_helper.h"
#import "ios/chrome/browser/tabs/ui_bundled/switch_to_tab_animation_view.h"
#import "ios/chrome/browser/ui/toolbar/public/side_swipe_toolbar_snapshot_providing.h"
#import "ios/chrome/browser/ui/toolbar/public/toolbar_type.h"
#import "ios/chrome/browser/url_loading/model/new_tab_animation_tab_helper.h"
#import "ios/chrome/browser/url_loading/model/url_loading_notifier_browser_agent.h"
#import "ios/chrome/browser/url_loading/model/url_loading_observer_bridge.h"
#import "ios/chrome/browser/web/model/page_placeholder_tab_helper.h"
#import "ios/web/public/ui/crw_web_view_proxy.h"
#import "ios/web/public/web_state.h"
#import "ios/web/public/web_state_observer_bridge.h"

@interface TabEventsMediator () <CRWWebStateObserver,
                                 WebStateListObserving,
                                 URLLoadingObserving>

@end

@implementation TabEventsMediator {
  // Bridges C++ WebStateObserver methods to this mediator.
  std::unique_ptr<web::WebStateObserverBridge> _webStateObserverBridge;

  // Bridges C++ WebStateListObserver methods to TabEventsMediator.
  std::unique_ptr<WebStateListObserverBridge> _webStateListObserverBridge;

  // Forwards observer methods for all WebStates in the WebStateList to
  // this mediator.
  std::unique_ptr<AllWebStateObservationForwarder>
      _allWebStateObservationForwarder;

  // Bridges C++ UrlLoadingObserver methods to TabEventsMediator.
  std::unique_ptr<UrlLoadingObserverBridge> _loadingObserverBridge;

  raw_ptr<WebStateList> _webStateList;
  __weak NewTabPageCoordinator* _ntpCoordinator;
  raw_ptr<UrlLoadingNotifierBrowserAgent> _loadingNotifier;
  raw_ptr<ChromeBrowserState> _browserState;
}

- (instancetype)initWithWebStateList:(WebStateList*)webStateList
                      ntpCoordinator:(NewTabPageCoordinator*)ntpCoordinator
                        browserState:(ChromeBrowserState*)browserState
                     loadingNotifier:
                         (UrlLoadingNotifierBrowserAgent*)urlLoadingNotifier {
  if ((self = [super init])) {
    _webStateList = webStateList;
    // TODO(crbug.com/40233361): Stop lazy loading in NTPCoordinator and remove
    // this dependency.
    _ntpCoordinator = ntpCoordinator;
    _browserState = browserState;

    _webStateObserverBridge =
        std::make_unique<web::WebStateObserverBridge>(self);
    _webStateListObserverBridge =
        std::make_unique<WebStateListObserverBridge>(self);
    webStateList->AddObserver(_webStateListObserverBridge.get());
    _allWebStateObservationForwarder =
        std::make_unique<AllWebStateObservationForwarder>(
            _webStateList, _webStateObserverBridge.get());
    _loadingObserverBridge = std::make_unique<UrlLoadingObserverBridge>(self);
    _loadingNotifier = urlLoadingNotifier;
    _loadingNotifier->AddObserver(_loadingObserverBridge.get());
  }
  return self;
}

- (void)disconnect {
  _allWebStateObservationForwarder = nullptr;
  _webStateObserverBridge = nullptr;

  _loadingNotifier->RemoveObserver(_loadingObserverBridge.get());
  _loadingObserverBridge.reset();

  _webStateList->RemoveObserver(_webStateListObserverBridge.get());
  _webStateListObserverBridge.reset();

  _webStateList = nullptr;
  _ntpCoordinator = nil;
  _browserState = nil;
  self.consumer = nil;
}

#pragma mark - CRWWebStateObserver methods.

- (void)webState:(web::WebState*)webState didLoadPageWithSuccess:(BOOL)success {
  web::WebState* currentWebState = _webStateList->GetActiveWebState();

  // If there is no first responder, try to make the NTP first responder to have
  // it answer keyboard commands (e.g. space bar to scroll). This is too late to
  // make the WebView first responder for some features such as the Gamepad API
  // which requires the WebView to be first responder when the page load starts.
  // Thus, Webview will also become first responder in [BrowserViewController
  // viewDidAppear:].
  if (!GetFirstResponder() && currentWebState) {
    NewTabPageTabHelper* NTPHelper =
        NewTabPageTabHelper::FromWebState(webState);
    if (NTPHelper && NTPHelper->IsActive()) {
      // TODO(crbug.com/40233361): Stop lazy loading in NTPCoordinator and
      // remove this dependency.
      UIViewController* viewController = _ntpCoordinator.viewController;
      [viewController becomeFirstResponder];
    } else {
      [currentWebState->GetWebViewProxy() becomeFirstResponder];
    }
  }
}

#pragma mark - WebStateListObserving methods

- (void)willChangeWebStateList:(WebStateList*)webStateList
                        change:(const WebStateListChangeDetach&)detachChange
                        status:(const WebStateListStatus&)status {
  // When the active webState is detached, the view should be reset.
  if (detachChange.detached_web_state() == _webStateList->GetActiveWebState()) {
    [self.consumer resetTab];
  }
}

- (void)didChangeWebStateList:(WebStateList*)webStateList
                       change:(const WebStateListChange&)change
                       status:(const WebStateListStatus&)status {
  BOOL isActivationHandled = NO;
  switch (change.type()) {
    case WebStateListChange::Type::kStatusOnly:
      // The activation is handled after this switch statement.
      break;
    case WebStateListChange::Type::kDetach: {
      // When an NTP web state is closed, check if the coordinator should be
      // stopped.
      const WebStateListChangeDetach& detachChange =
          change.As<WebStateListChangeDetach>();
      if (detachChange.is_closing()) {
        NewTabPageTabHelper* NTPTabHelper = NewTabPageTabHelper::FromWebState(
            detachChange.detached_web_state());
        if (status.active_web_state_change()) {
          // Closing one or multiple WebStates may cause the active WebState to
          // change. Need to update NTP and record metrics before stopping NTP.
          [self didChangeActiveWebState:status.new_active_web_state
                      oldActiveWebState:status.old_active_web_state
                             isInserted:NO];
          isActivationHandled = YES;
        }
        if (NTPTabHelper->IsActive()) {
          [self stopNTPIfNeeded];
        }
      }
      break;
    }
    case WebStateListChange::Type::kMove:
      // Do nothing when a WebState is moved.
      break;
    case WebStateListChange::Type::kReplace: {
      const WebStateListChangeReplace& replaceChange =
          change.As<WebStateListChangeReplace>();
      NewTabPageTabHelper* NTPTabHelper =
          NewTabPageTabHelper::FromWebState(replaceChange.replaced_web_state());
      if (NTPTabHelper->IsActive()) {
        [self stopNTPIfNeeded];
      }

      web::WebState* currentWebState = _webStateList->GetActiveWebState();
      web::WebState* newWebState = replaceChange.inserted_web_state();
      // Add `newTab`'s view to the hierarchy if it's the current Tab.
      if (currentWebState == newWebState) {
        // Set this before triggering any of the possible page loads in
        // displayTabViewIfActive.
        newWebState->SetKeepRenderProcessAlive(true);
        [self.consumer displayTabViewIfActive];
      }
      break;
    }
    case WebStateListChange::Type::kInsert: {
      // If a tab is inserted in the background (not activating), trigger an
      // animation. (The animation for foreground tab insertion is handled in
      // `didChangeActiveWebState`).
      if (!status.active_web_state_change()) {
        [self.consumer initiateNewTabBackgroundAnimation];
      }
      break;
    }
    case WebStateListChange::Type::kGroupCreate:
      // Do nothing when a group is created.
      break;
    case WebStateListChange::Type::kGroupVisualDataUpdate:
      // Do nothing when a tab group's visual data are updated.
      break;
    case WebStateListChange::Type::kGroupMove:
      // Do nothing when a tab group is moved.
      break;
    case WebStateListChange::Type::kGroupDelete:
      // Do nothing when a group is deleted.
      break;
  }

  if (!isActivationHandled && status.active_web_state_change()) {
    [self didChangeActiveWebState:status.new_active_web_state
                oldActiveWebState:status.old_active_web_state
                       isInserted:change.type() ==
                                  WebStateListChange::Type::kInsert];
  }
}

#pragma mark - WebStateListObserving helpers (Private)

- (void)didChangeActiveWebState:(web::WebState*)newActiveWebState
              oldActiveWebState:(web::WebState*)oldActiveWebState
                     isInserted:(bool)isInserted {
  // If the user is leaving an NTP web state, trigger a visibility change.
  if (oldActiveWebState && _ntpCoordinator.started) {
    NewTabPageTabHelper* NTPHelper =
        NewTabPageTabHelper::FromWebState(oldActiveWebState);
    if (NTPHelper->IsActive()) {
      [_ntpCoordinator didNavigateAwayFromNTP];
    }
  }

  if (oldActiveWebState) {
    [self.consumer prepareForNewTabAnimation];
  }
  if (newActiveWebState) {
    // Activating without inserting an NTP requires starting it in two
    // scenarios: 1) After doing a batch tab restore (i.e. undo tab removals,
    // initial startup). 2) After re-activating the Browser and a non-active
    // WebState is showing the NTP. BrowserCoordinator's -setActive: only starts
    // the NTP if it is the active view.
    [self startNTPIfNeededForActiveWebState:newActiveWebState];

    // If the user is entering an NTP web state, trigger a visibility change.
    NewTabPageTabHelper* NTPHelper =
        NewTabPageTabHelper::FromWebState(newActiveWebState);
    if (NTPHelper->IsActive()) {
      [_ntpCoordinator didNavigateToNTPInWebState:newActiveWebState];
    }

    if (isInserted) {
      // This starts the new tab animation. It is important for the
      // NTPCoordinator to know about the new web state
      // (via the call to `-didNavigateToNTPInWebState:` above) before this is
      // called.
      if (!_webStateList->IsBatchInProgress()) {
        [self didInsertActiveWebState:newActiveWebState];
      }
    }

    [self.consumer webStateSelected];
  }
}

- (void)startNTPIfNeededForActiveWebState:(web::WebState*)webState {
  NewTabPageTabHelper* NTPHelper = NewTabPageTabHelper::FromWebState(webState);
  if (NTPHelper && NTPHelper->IsActive() && !_ntpCoordinator.started) {
    [_ntpCoordinator start];
  }
}

- (void)stopNTPIfNeeded {
  for (int i = 0; i < _webStateList->count(); i++) {
    NewTabPageTabHelper* iterNtpHelper =
        NewTabPageTabHelper::FromWebState(_webStateList->GetWebStateAt(i));
    if (iterNtpHelper->IsActive()) {
      return;
    }
  }
  [_ntpCoordinator stop];
}

- (void)didInsertActiveWebState:(web::WebState*)newWebState {
  DCHECK(newWebState);
  auto* animationTabHelper =
      NewTabAnimationTabHelper::FromWebState(newWebState);
  BOOL animated =
      !animationTabHelper || animationTabHelper->ShouldAnimateNewTab();
  if (animationTabHelper) {
    // Remove the helper because it isn't needed anymore.
    NewTabAnimationTabHelper::RemoveFromWebState(newWebState);
  }
  NewTabPageTabHelper* NTPHelper =
      NewTabPageTabHelper::FromWebState(newWebState);
  BOOL inBackground =
      (NTPHelper && NTPHelper->ShouldShowStartSurface()) || !animated;
  if (inBackground) {
    [self.consumer initiateNewTabBackgroundAnimation];
  } else {
    [self.consumer initiateNewTabForegroundAnimationForWebState:newWebState];
  }
}

#pragma mark - URLLoadingObserving

- (void)newTabWillLoadURL:(const GURL&)URL
          isUserInitiated:(BOOL)isUserInitiated {
  if (isUserInitiated) {
    // Send either the "New Tab Opened" or "New Incognito Tab" opened to the
    // feature_engagement::Tracker based on `inIncognito`.
    feature_engagement::NotifyNewTabEvent(_browserState,
                                          _browserState->IsOffTheRecord());
  }
}

- (void)tabWillLoadURL:(const GURL&)URL
        transitionType:(ui::PageTransition)transitionType {
  [self.consumer dismissBookmarkModalController];

  web::WebState* currentWebState = _webStateList->GetActiveWebState();
  if (currentWebState &&
      (transitionType & ui::PAGE_TRANSITION_FROM_ADDRESS_BAR)) {
    new_tab_page_uma::RecordActionFromOmnibox(
        _browserState->IsOffTheRecord(), currentWebState, URL, transitionType);
  }
}
- (void)willSwitchToTabWithURL:(const GURL&)URL
              newWebStateIndex:(NSInteger)newWebStateIndex {
  base::WeakPtr<web::WebState> weakWebStateBeingActivated =
      _webStateList->GetWebStateAt(newWebStateIndex)->GetWeakPtr();
  web::WebState* webStateBeingActivated = weakWebStateBeingActivated.get();
  if (!webStateBeingActivated) {
    return;
  }
  SnapshotTabHelper* snapshotTabHelper =
      SnapshotTabHelper::FromWebState(webStateBeingActivated);
  BOOL willAddPlaceholder =
      PagePlaceholderTabHelper::FromWebState(webStateBeingActivated)
          ->will_add_placeholder_for_next_navigation();
  NewTabPageTabHelper* NTPHelper =
      NewTabPageTabHelper::FromWebState(webStateBeingActivated);
  UIImage* topToolbarImage = [self.toolbarSnapshotProvider
      toolbarSideSwipeSnapshotForWebState:webStateBeingActivated
                          withToolbarType:ToolbarType::kPrimary];
  UIImage* bottomToolbarImage = [self.toolbarSnapshotProvider
      toolbarSideSwipeSnapshotForWebState:webStateBeingActivated
                          withToolbarType:ToolbarType::kSecondary];
  SwitchToTabAnimationPosition position =
      newWebStateIndex > _webStateList->active_index()
          ? SwitchToTabAnimationPositionAfter
          : SwitchToTabAnimationPositionBefore;

  [self.consumer switchToTabAnimationPosition:position
                            snapshotTabHelper:snapshotTabHelper
                           willAddPlaceholder:willAddPlaceholder
                          newTabPageTabHelper:NTPHelper
                              topToolbarImage:topToolbarImage
                           bottomToolbarImage:bottomToolbarImage];
}

#pragma mark - NewTabPageTabHelperDelegate

- (void)newTabPageHelperDidChangeVisibility:(NewTabPageTabHelper*)NTPHelper
                                forWebState:(web::WebState*)webState {
  if (webState != _webStateList->GetActiveWebState()) {
    // In the instance that a pageload starts while the WebState is not the
    // active WebState anymore, do nothing.
    return;
  }
  // Handle NTP visibility changes within a web state.
  if (NTPHelper->IsActive()) {
    if (!_ntpCoordinator.started) {
      [_ntpCoordinator start];
    }
    [_ntpCoordinator didNavigateToNTPInWebState:webState];
  } else {
    [_ntpCoordinator didNavigateAwayFromNTP];
    [_ntpCoordinator stopIfNeeded];
  }
  [self.consumer displayTabViewIfActive];
}

@end