chromium/ios/chrome/browser/ui/toolbar/toolbar_mediator.mm

// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/chrome/browser/ui/toolbar/toolbar_mediator.h"

#import "base/memory/raw_ptr.h"
#import "base/metrics/field_trial_params.h"
#import "base/metrics/histogram_functions.h"
#import "components/segmentation_platform/embedder/default_model/device_switcher_result_dispatcher.h"
#import "components/segmentation_platform/public/result.h"
#import "ios/chrome/browser/first_run/model/first_run.h"
#import "ios/chrome/browser/ntp/model/new_tab_page_tab_helper.h"
#import "ios/chrome/browser/segmentation_platform/model/segmentation_platform_service_factory.h"
#import "ios/chrome/browser/shared/model/application_context/application_context.h"
#import "ios/chrome/browser/shared/model/prefs/pref_backed_boolean.h"
#import "ios/chrome/browser/shared/model/prefs/pref_names.h"
#import "ios/chrome/browser/shared/model/utils/first_run_util.h"
#import "ios/chrome/browser/shared/model/web_state_list/active_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/public/features/features.h"
#import "ios/chrome/browser/shared/public/features/system_flags.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/ui/toolbar/public/omnibox_position_metrics.h"
#import "ios/chrome/browser/ui/toolbar/public/omnibox_position_util.h"
#import "ios/chrome/browser/ui/toolbar/public/toolbar_omnibox_consumer.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 ToolbarMediator () <BooleanObserver,
                               CRWWebStateObserver,
                               WebStateListObserving>

/// Type of toolbar containing the omnibox. Unlike
/// `steadyStateOmniboxPosition`, this tracks the omnibox position at all
/// time.
@property(nonatomic, assign) ToolbarType omniboxPosition;
/// Type of the toolbar that contains the omnibox when it's not focused. The
/// animation of focusing/defocusing the omnibox changes depending on this
/// position.
@property(nonatomic, assign) ToolbarType steadyStateOmniboxPosition;

@end

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

  /// Forwards observer methods for active WebStates in the WebStateList to
  /// this mediator.
  std::unique_ptr<ActiveWebStateObservationForwarder>
      _activeWebStateObservationForwarder;

  /// Observes web state activation.
  std::unique_ptr<WebStateListObserverBridge> _webStateListObserverBridge;

  raw_ptr<WebStateList> _webStateList;

  /// Pref tracking if bottom omnibox is enabled.
  PrefBackedBoolean* _bottomOmniboxEnabled;
  /// Whether the omnibox is currently focused.
  BOOL _locationBarFocused;
  /// Whether the browser is incognito.
  BOOL _isIncognito;
  /// Whether the last navigated web state is NTP.
  BOOL _isNTP;
  /// Last trait collection of the toolbars.
  UITraitCollection* _toolbarTraitCollection;
  /// Preferred toolbar to contain the omnibox.
  ToolbarType _preferredOmniboxPosition;

  /// Whether SafariSwitcher should be checked on FRE.
  BOOL _shouldCheckSafariSwitcherOnFRE;
  /// Whether the NTP was shown in FRE.
  BOOL _hasEnteredNTPOnFRE;
}

- (instancetype)initWithWebStateList:(WebStateList*)webStateList
                         isIncognito:(BOOL)isIncognito {
  if ((self = [super init])) {
    _webStateList = webStateList;
    _isIncognito = isIncognito;

    _webStateObserverBridge =
        std::make_unique<web::WebStateObserverBridge>(self);
    _activeWebStateObservationForwarder =
        std::make_unique<ActiveWebStateObservationForwarder>(
            webStateList, _webStateObserverBridge.get());
    _webStateListObserverBridge =
        std::make_unique<WebStateListObserverBridge>(self);
    _webStateList->AddObserver(_webStateListObserverBridge.get());

    if (IsBottomOmniboxAvailable()) {
      // Device switcher data is not available in incognito.
      _shouldCheckSafariSwitcherOnFRE = !isIncognito && IsFirstRun();

      _bottomOmniboxEnabled = [[PrefBackedBoolean alloc]
          initWithPrefService:GetApplicationContext()->GetLocalState()
                     prefName:prefs::kBottomOmnibox];
      [_bottomOmniboxEnabled setObserver:self];
      // Initialize to the correct value.
      [self booleanDidChange:_bottomOmniboxEnabled];
      [self updateOmniboxDefaultPosition];
      [self logOmniboxPosition];
    }
  }
  return self;
}

- (void)disconnect {
  _activeWebStateObservationForwarder = nullptr;
  _webStateObserverBridge = nullptr;
  _webStateList->RemoveObserver(_webStateListObserverBridge.get());
  _webStateListObserverBridge = nullptr;

  _webStateList = nullptr;
  [_bottomOmniboxEnabled stop];
  [_bottomOmniboxEnabled setObserver:nil];
  _bottomOmniboxEnabled = nil;
}

- (void)locationBarFocusChangedTo:(BOOL)focused {
  _locationBarFocused = focused;
  if (IsBottomOmniboxAvailable()) {
    [self updateOmniboxPosition];
  }
}

- (void)toolbarTraitCollectionChangedTo:(UITraitCollection*)traitCollection {
  _toolbarTraitCollection = traitCollection;
  if (IsBottomOmniboxAvailable()) {
    [self updateOmniboxPosition];
  }
}

- (void)setInitialOmniboxPosition {
  [self updateOmniboxPosition];
  [self.delegate transitionOmniboxToToolbarType:self.omniboxPosition];
  [self.delegate transitionSteadyStateOmniboxToToolbarType:
                     self.steadyStateOmniboxPosition];
  [self.omniboxConsumer
      steadyStateOmniboxMovedToToolbar:self.steadyStateOmniboxPosition];
}

- (void)didNavigateToNTPOnActiveWebState {
  _isNTP = YES;
  if (IsBottomOmniboxAvailable()) {
    [self updateOmniboxPosition];
  }
}

#pragma mark - Setters

- (void)setOmniboxPosition:(ToolbarType)omniboxPosition {
  if (_omniboxPosition != omniboxPosition) {
    _omniboxPosition = omniboxPosition;
    [self.delegate transitionOmniboxToToolbarType:omniboxPosition];
  }
}

- (void)setSteadyStateOmniboxPosition:(ToolbarType)steadyStateOmniboxPosition {
  if (_steadyStateOmniboxPosition != steadyStateOmniboxPosition) {
    _steadyStateOmniboxPosition = steadyStateOmniboxPosition;
    [self.delegate
        transitionSteadyStateOmniboxToToolbarType:steadyStateOmniboxPosition];
    [self.omniboxConsumer
        steadyStateOmniboxMovedToToolbar:steadyStateOmniboxPosition];
  }
}

#pragma mark - Boolean Observer

- (void)booleanDidChange:(id<ObservableBoolean>)observableBoolean {
  if (observableBoolean == _bottomOmniboxEnabled) {
    _preferredOmniboxPosition = _bottomOmniboxEnabled.value
                                    ? ToolbarType::kSecondary
                                    : ToolbarType::kPrimary;
    [self updateOmniboxPosition];
  }
}

#pragma mark - CRWWebStateObserver methods.

- (void)webStateWasShown:(web::WebState*)webState {
  [self updateForWebState:webState];
}

- (void)webState:(web::WebState*)webState
    didStartNavigation:(web::NavigationContext*)navigation {
  [self updateForWebState:webState];
}

#pragma mark - WebStateListObserving

- (void)didChangeWebStateList:(WebStateList*)webStateList
                       change:(const WebStateListChange&)change
                       status:(const WebStateListStatus&)status {
  if (status.active_web_state_change() && status.new_active_web_state) {
    [self updateForWebState:status.new_active_web_state];
  }
}

#pragma mark - Private

/// Updates the state variables and toolbars with `webState`.
- (void)updateForWebState:(web::WebState*)webState {
  [self.delegate updateToolbar];
  NewTabPageTabHelper* NTPHelper = NewTabPageTabHelper::FromWebState(webState);
  _isNTP = NTPHelper && NTPHelper->IsActive();
  if (IsBottomOmniboxAvailable()) {
    if (_shouldCheckSafariSwitcherOnFRE) {
      [self checkSafariSwitcherOnFRE];
    }
    [self updateOmniboxPosition];
  }
}

/// Computes the toolbar that should contain the unfocused omnibox in the
/// current state.
- (ToolbarType)steadyStateOmniboxPositionInCurrentState {
  if (_preferredOmniboxPosition == ToolbarType::kPrimary ||
      !IsSplitToolbarMode(_toolbarTraitCollection)) {
    return ToolbarType::kPrimary;
  }
  if (_isNTP && !_isIncognito) {
    return ToolbarType::kPrimary;
  }
  return _preferredOmniboxPosition;
}

/// Computes the toolbar that should contain the omnibox in the current state.
- (ToolbarType)omniboxPositionInCurrentState {
  if (_locationBarFocused) {
    return ToolbarType::kPrimary;
  } else {
    return [self steadyStateOmniboxPositionInCurrentState];
  }
}

/// Updates the omnibox position to the correct toolbar.
- (void)updateOmniboxPosition {
  if (!IsBottomOmniboxAvailable()) {
    [self.delegate transitionOmniboxToToolbarType:ToolbarType::kPrimary];
    return;
  }

  self.omniboxPosition = [self omniboxPositionInCurrentState];
  self.steadyStateOmniboxPosition =
      [self steadyStateOmniboxPositionInCurrentState];
}

#pragma mark Default omnibox position

/// Verifies if the user is a safari switcher on FRE.
- (void)checkSafariSwitcherOnFRE {
  CHECK(_shouldCheckSafariSwitcherOnFRE);
  CHECK(self.deviceSwitcherResultDispatcher);

  if (_isNTP) {
    _hasEnteredNTPOnFRE = YES;
  } else if (_hasEnteredNTPOnFRE) {
    // Check device switcher data when the user leaves NTP on FRE, as data is
    // only available on sync and takes time to fetch. This is only executed
    // once, if the data is unavailable user may still see the bottom omnibox on
    // next restart (see. `updateOmniboxDefaultPosition`).
    _shouldCheckSafariSwitcherOnFRE = NO;
    segmentation_platform::ClassificationResult result =
        self.deviceSwitcherResultDispatcher->GetCachedClassificationResult();
    if (result.status == segmentation_platform::PredictionStatus::kSucceeded) {
      if (omnibox::IsSafariSwitcher(result)) {
        std::string featureParam = base::GetFieldTrialParamValueByFeature(
            kBottomOmniboxDefaultSetting, kBottomOmniboxDefaultSettingParam);
        if (featureParam == kBottomOmniboxDefaultSettingParamSafariSwitcher) {
          PrefService* localState = GetApplicationContext()->GetLocalState();
          localState->SetDefaultPrefValue(prefs::kBottomOmnibox,
                                          base::Value(YES));
          localState->SetBoolean(prefs::kBottomOmniboxByDefault, YES);
        }
        base::UmaHistogramEnumeration(
            kOmniboxDeviceSwitcherResultAtFRE,
            OmniboxDeviceSwitcherResult::kBottomOmnibox);
      } else {
        base::UmaHistogramEnumeration(kOmniboxDeviceSwitcherResultAtFRE,
                                      OmniboxDeviceSwitcherResult::kTopOmnibox);
      }
    } else {
      base::UmaHistogramEnumeration(kOmniboxDeviceSwitcherResultAtFRE,
                                    OmniboxDeviceSwitcherResult::kUnavailable);
    }
  }
}

/// Returns whether user is a safari switcher at startup.
/// Used to set the default omnibox position to bottom for `IsNewUser`
/// that are not in FRE. If bottom omnibox is already default
/// `bottomOmniboxIsDefault`, still log the status as bottom as the user was
/// classified as safari switcher in a previous session.
- (BOOL)isSafariSwitcherAtStartup:(BOOL)bottomOmniboxIsDefault {

  if (!omnibox::IsNewUser()) {
    base::UmaHistogramEnumeration(kOmniboxDeviceSwitcherResultAtStartup,
                                  OmniboxDeviceSwitcherResult::kNotNewUser);
    return NO;
  }

  if (bottomOmniboxIsDefault) {
    base::UmaHistogramEnumeration(kOmniboxDeviceSwitcherResultAtStartup,
                                  OmniboxDeviceSwitcherResult::kBottomOmnibox);
    return YES;
  }

  segmentation_platform::ClassificationResult result =
      self.deviceSwitcherResultDispatcher->GetCachedClassificationResult();
  if (result.status != segmentation_platform::PredictionStatus::kSucceeded) {
    base::UmaHistogramEnumeration(kOmniboxDeviceSwitcherResultAtStartup,
                                  OmniboxDeviceSwitcherResult::kUnavailable);
    return NO;
  }

  if (omnibox::IsSafariSwitcher(result)) {
    base::UmaHistogramEnumeration(kOmniboxDeviceSwitcherResultAtStartup,
                                  OmniboxDeviceSwitcherResult::kBottomOmnibox);
    return YES;
  }
  base::UmaHistogramEnumeration(kOmniboxDeviceSwitcherResultAtStartup,
                                OmniboxDeviceSwitcherResult::kTopOmnibox);
  return NO;
}

/// Updates the default setting for bottom omnibox.
- (void)updateOmniboxDefaultPosition {
  PrefService* localState = GetApplicationContext()->GetLocalState();

  // This only needs to be executed once and deviceSwitcherResult are not
  // available in incognito.
  if (!self.deviceSwitcherResultDispatcher ||
      localState->GetUserPrefValue(prefs::kBottomOmnibox)) {
    return;
  }

  BOOL bottomOmniboxEnabledByDefault = NO;
  if (localState->GetUserPrefValue(prefs::kBottomOmniboxByDefault)) {
    bottomOmniboxEnabledByDefault =
        localState->GetBoolean(prefs::kBottomOmniboxByDefault);
  }

  std::string featureParam = base::GetFieldTrialParamValueByFeature(
      kBottomOmniboxDefaultSetting, kBottomOmniboxDefaultSettingParam);
  if (featureParam == kBottomOmniboxDefaultSettingParamBottom) {
    bottomOmniboxEnabledByDefault = YES;
  } else if (featureParam == kBottomOmniboxDefaultSettingParamTop) {
    bottomOmniboxEnabledByDefault = NO;
  }

  // Call `isSafariSwitcherAtStartup` in all cases to collect metrics on the
  // device switcher result availability.
  if ([self isSafariSwitcherAtStartup:bottomOmniboxEnabledByDefault] &&
      featureParam == kBottomOmniboxDefaultSettingParamSafariSwitcher) {
    bottomOmniboxEnabledByDefault = YES;
  }

  // Make sure that users who have already seen the bottom omnibox by default
  // keep it.
  if (bottomOmniboxEnabledByDefault) {
    localState->SetBoolean(prefs::kBottomOmniboxByDefault, YES);
  }

  localState->SetDefaultPrefValue(prefs::kBottomOmnibox,
                                  base::Value(bottomOmniboxEnabledByDefault));
}

/// Logs preferred omnibox position.
- (void)logOmniboxPosition {

  static dispatch_once_t once;
  dispatch_once(&once, ^{
    PrefService* localState = GetApplicationContext()->GetLocalState();
    const BOOL isBottomOmnibox = localState->GetBoolean(prefs::kBottomOmnibox);
    OmniboxPositionType positionType = isBottomOmnibox
                                           ? OmniboxPositionType::kBottom
                                           : OmniboxPositionType::kTop;
    base::UmaHistogramEnumeration(kOmniboxSteadyStatePositionAtStartup,
                                  positionType);

    if (localState->GetUserPrefValue(prefs::kBottomOmnibox)) {
      base::UmaHistogramEnumeration(
          kOmniboxSteadyStatePositionAtStartupSelected, positionType);
    }
  });
}

@end