chromium/ios/chrome/browser/autofill/ui_bundled/form_input_accessory/form_input_accessory_mediator.mm

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

#import "ios/chrome/browser/autofill/ui_bundled/form_input_accessory/form_input_accessory_mediator.h"

#import "base/apple/foundation_util.h"
#import "base/containers/contains.h"
#import "base/containers/fixed_flat_set.h"
#import "base/ios/block_types.h"
#import "base/ios/ios_util.h"
#import "base/memory/raw_ptr.h"
#import "base/metrics/histogram_functions.h"
#import "base/strings/sys_string_conversions.h"
#import "components/autofill/core/browser/address_data_manager.h"
#import "components/autofill/core/browser/payments_data_manager.h"
#import "components/autofill/core/browser/personal_data_manager.h"
#import "components/autofill/core/common/autofill_features.h"
#import "components/autofill/ios/browser/form_suggestion.h"
#import "components/autofill/ios/browser/form_suggestion_provider.h"
#import "components/autofill/ios/browser/personal_data_manager_observer_bridge.h"
#import "components/autofill/ios/form_util/form_activity_observer_bridge.h"
#import "components/autofill/ios/form_util/form_activity_params.h"
#import "components/feature_engagement/public/tracker.h"
#import "components/prefs/pref_service.h"
#import "ios/chrome/browser/autofill/model/bottom_sheet/autofill_bottom_sheet_observer_bridge.h"
#import "ios/chrome/browser/autofill/model/bottom_sheet/autofill_bottom_sheet_tab_helper.h"
#import "ios/chrome/browser/autofill/model/form_input_accessory_view_handler.h"
#import "ios/chrome/browser/autofill/model/form_input_suggestions_provider.h"
#import "ios/chrome/browser/autofill/model/form_suggestion_tab_helper.h"
#import "ios/chrome/browser/default_browser/model/default_browser_interest_signals.h"
#import "ios/chrome/browser/passwords/model/password_counter_delegate_bridge.h"
#import "ios/chrome/browser/shared/coordinator/chrome_coordinator/chrome_coordinator.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/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/commands/security_alert_commands.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/autofill/ui_bundled/form_input_accessory/form_input_accessory_chromium_text_data.h"
#import "ios/chrome/browser/autofill/ui_bundled/form_input_accessory/form_input_accessory_consumer.h"
#import "ios/chrome/browser/autofill/ui_bundled/form_input_accessory/form_input_accessory_mediator_handler.h"
#import "ios/chrome/browser/autofill/ui_bundled/form_input_accessory/form_suggestion_view.h"
#import "ios/chrome/browser/autofill/ui_bundled/form_input_accessory/scoped_form_input_accessory_reauth_module_override.h"
#import "ios/chrome/common/ui/elements/form_input_accessory_view.h"
#import "ios/chrome/common/ui/reauthentication/reauthentication_event.h"
#import "ios/chrome/common/ui/reauthentication/reauthentication_module.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/web/common/url_scheme_util.h"
#import "ios/web/public/js_messaging/web_frame.h"
#import "ios/web/public/js_messaging/web_frames_manager.h"
#import "ios/web/public/web_state.h"
#import "ios/web/public/web_state_observer_bridge.h"
#import "ui/base/l10n/l10n_util_mac.h"

using base::UmaHistogramEnumeration;

namespace {

// Returns whether the input field type triggers the keyboard to open. If the
// field type isn't recognized, it returns the provided default value.
bool InputTriggersKeyboard(std::string field_type, bool default_value) {
  static const auto triggers_keyboard = base::MakeFixedFlatSet<std::string>(
      {"email", "number", "password", "search", "tel", "text", "url", "week"});
  static const auto no_keyboard = base::MakeFixedFlatSet<std::string>(
      {"button", "checkbox", "color", "date", "datetime-local", "file",
       "hidden", "image", "month", "radio", "range", "reset", "submit",
       "time"});

  if (base::Contains(triggers_keyboard, field_type)) {
    return true;
  }

  if (base::Contains(no_keyboard, field_type)) {
    return false;
  }

  return default_value;
}

}  // namespace

@interface FormInputAccessoryMediator () <AutofillBottomSheetObserving,
                                          BooleanObserver,
                                          FormActivityObserver,
                                          FormInputAccessoryViewDelegate,
                                          CRWWebStateObserver,
                                          PasswordCounterObserver,
                                          PersonalDataManagerObserver,
                                          WebStateListObserving>

// The main consumer for this mediator.
@property(nonatomic, weak) id<FormInputAccessoryConsumer> consumer;

// The handler for this object.
@property(nonatomic, weak) id<FormInputAccessoryMediatorHandler> handler;

// The object that manages the currently-shown custom accessory view.
@property(nonatomic, weak) id<FormInputSuggestionsProvider> currentProvider;

// The form input handler. This is in charge of form navigation.
@property(nonatomic, strong)
    FormInputAccessoryViewHandler* formNavigationHandler;

// The object that provides suggestions while filling forms.
@property(nonatomic, weak) id<FormInputSuggestionsProvider> provider;

// YES if the latest form activity was made in a form that supports the
// accessory.
@property(nonatomic, assign) BOOL validActivityForAccessoryView;

// The WebState this instance is observing. Can be null.
@property(nonatomic, assign) web::WebState* webState;

// Reauthentication Module used for re-authentication.
@property(nonatomic, strong) ReauthenticationModule* reauthenticationModule;

// Used to present alerts.
@property(nonatomic, weak) id<SecurityAlertCommands> securityAlertHandler;

// ID of the latest query to get suggestions. Using a uint to handle overflow
// which will realistically never happen, but just in case.
@property(nonatomic, assign) uint latestQueryId;

// Feature engagement tracker for notifying promo events.
@property(nonatomic, assign) feature_engagement::Tracker* engagementTracker;

@end

@implementation FormInputAccessoryMediator {
  // The WebStateList this instance is observing in order to update the
  // active WebState.
  raw_ptr<WebStateList> _webStateList;

  // Personal data manager to be observed.
  raw_ptr<autofill::PersonalDataManager> _personalDataManager;

  // C++ to ObjC bridge for PersonalDataManagerObserver.
  std::unique_ptr<autofill::PersonalDataManagerObserverBridge>
      _personalDataManagerObserver;

  // Bridge to observe the web state list from Objective-C.
  std::unique_ptr<WebStateListObserverBridge> _webStateListObserver;

  // Bridge to observe the web state from Objective-C.
  std::unique_ptr<web::WebStateObserverBridge> _webStateObserverBridge;

  // The observer for number of passwords in the stores.
  std::unique_ptr<PasswordCounterDelegateBridge> _passwordCounter;

  // Bridge to observe form activity in `_webState`.
  std::unique_ptr<autofill::FormActivityObserverBridge>
      _formActivityObserverBridge;

  // Bridge for the AutofillBottomSheetObserver.
  std::unique_ptr<autofill::AutofillBottomSheetObserverBridge>
      _autofillBottomSheetObserverBridge;

  // Whether suggestions have previously been shown.
  BOOL _suggestionsHaveBeenShown;

  // The last seen valid params of a form before retrieving suggestions. Or
  // empty if `_hasLastSeenParams` is NO.
  autofill::FormActivityParams _lastSeenParams;

  // If YES `_lastSeenParams` is valid.
  BOOL _hasLastSeenParams;

  // Pref tracking if bottom omnibox is enabled.
  PrefBackedBoolean* _bottomOmniboxEnabled;

  // Whether the keyboard height change notifications are enabled.
  BOOL _keyboardHeightChangeNotificationsEnabled;
}

- (instancetype)
          initWithConsumer:(id<FormInputAccessoryConsumer>)consumer
                   handler:(id<FormInputAccessoryMediatorHandler>)handler
              webStateList:(WebStateList*)webStateList
       personalDataManager:(autofill::PersonalDataManager*)personalDataManager
      profilePasswordStore:
          (scoped_refptr<password_manager::PasswordStoreInterface>)
              profilePasswordStore
      accountPasswordStore:
          (scoped_refptr<password_manager::PasswordStoreInterface>)
              accountPasswordStore
      securityAlertHandler:(id<SecurityAlertCommands>)securityAlertHandler
    reauthenticationModule:(ReauthenticationModule*)reauthenticationModule
         engagementTracker:(feature_engagement::Tracker*)engagementTracker {
  self = [super init];
  if (self) {
    _consumer = consumer;
    _consumer.navigationDelegate = self;
    _handler = handler;
    if (webStateList) {
      _webStateList = webStateList;
      _webStateListObserver =
          std::make_unique<WebStateListObserverBridge>(self);
      _webStateList->AddObserver(_webStateListObserver.get());
      web::WebState* webState = webStateList->GetActiveWebState();
      if (webState) {
        _webState = webState;
        FormSuggestionTabHelper* tabHelper =
            FormSuggestionTabHelper::FromWebState(webState);
        if (tabHelper) {
          _provider = tabHelper->GetAccessoryViewProvider();
        }
        _formActivityObserverBridge =
            std::make_unique<autofill::FormActivityObserverBridge>(_webState,
                                                                   self);
        _autofillBottomSheetObserverBridge =
            std::make_unique<autofill::AutofillBottomSheetObserverBridge>(
                self, AutofillBottomSheetTabHelper::FromWebState(webState));
        _webStateObserverBridge =
            std::make_unique<web::WebStateObserverBridge>(self);
        webState->AddObserver(_webStateObserverBridge.get());
      }
    }
    _formNavigationHandler = [[FormInputAccessoryViewHandler alloc] init];
    _formNavigationHandler.webState = _webState;

    NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
    [defaultCenter addObserver:self
                      selector:@selector(applicationDidEnterBackground:)
                          name:UIApplicationDidEnterBackgroundNotification
                        object:nil];
    [defaultCenter addObserver:self
                      selector:@selector(keyboardWillShow:)
                          name:UIKeyboardWillShowNotification
                        object:nil];
    [defaultCenter addObserver:self
                      selector:@selector(keyboardWillChangeFrame:)
                          name:UIKeyboardWillChangeFrameNotification
                        object:nil];
    [defaultCenter addObserver:self
                      selector:@selector(textInputModeDidChange:)
                          name:UITextInputCurrentInputModeDidChangeNotification
                        object:nil];

    // In BVC unit tests the password store doesn't exist. Skip creating the
    // counter.
    // TODO:(crbug.com/878388) Remove this workaround.
    if (profilePasswordStore) {
      _passwordCounter = std::make_unique<PasswordCounterDelegateBridge>(
          self, profilePasswordStore.get(), accountPasswordStore.get());
    }
    if (personalDataManager) {
      _personalDataManager = personalDataManager;
      _personalDataManagerObserver.reset(
          new autofill::PersonalDataManagerObserverBridge(self));
      personalDataManager->AddObserver(_personalDataManagerObserver.get());

      // TODO:(crbug.com/845472) Add earl grey test to verify the credit card
      // button is hidden when local cards are saved and then
      // kAutofillCreditCardEnabled is changed to disabled.
      consumer.creditCardButtonHidden =
          personalDataManager->payments_data_manager().GetCreditCards().empty();

      consumer.addressButtonHidden = personalDataManager->address_data_manager()
                                         .GetProfilesToSuggest()
                                         .empty();
    } else {
      consumer.creditCardButtonHidden = YES;
      consumer.addressButtonHidden = YES;
    }
    _reauthenticationModule = reauthenticationModule;
    _securityAlertHandler = securityAlertHandler;

    // Prevent a flicker from happening by starting with valid activity. This
    // will get updated as soon as a form is interacted.
    _validActivityForAccessoryView = YES;

    _latestQueryId = 0;

    _engagementTracker = engagementTracker;

    self.suggestionsEnabled = YES;

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

- (void)dealloc {
  // TODO(crbug.com/40272467)
  DUMP_WILL_BE_CHECK(!_formActivityObserverBridge.get());
  DUMP_WILL_BE_CHECK(!_autofillBottomSheetObserverBridge.get());
  DUMP_WILL_BE_CHECK(!_personalDataManager);
  DUMP_WILL_BE_CHECK(!_webState);
  DUMP_WILL_BE_CHECK(!_webStateList);
}

- (void)disconnect {
  _formActivityObserverBridge.reset();
  _autofillBottomSheetObserverBridge.reset();
  if (_personalDataManager && _personalDataManagerObserver.get()) {
    _personalDataManager->RemoveObserver(_personalDataManagerObserver.get());
    _personalDataManagerObserver.reset();
    _personalDataManager = nullptr;
  }
  if (_webState) {
    _webState->RemoveObserver(_webStateObserverBridge.get());
    _webStateObserverBridge.reset();
    _webState = nullptr;
  }
  if (_webStateList) {
    _webStateList->RemoveObserver(_webStateListObserver.get());
    _webStateListObserver.reset();
    _webStateList = nullptr;
  }
  [_bottomOmniboxEnabled stop];
  [_bottomOmniboxEnabled setObserver:nil];
  _bottomOmniboxEnabled = nil;
}

- (void)detachFromWebState {
  [self reset];
  if (_webState) {
    _webState->RemoveObserver(_webStateObserverBridge.get());
    _webStateObserverBridge.reset();
    _webState = nullptr;
    _formActivityObserverBridge.reset();
    _autofillBottomSheetObserverBridge.reset();
  }
}

- (BOOL)lastFocusedFieldWasObfuscated {
  return _lastSeenParams.field_type == autofill::kObfuscatedFieldType;
}

#pragma mark - KeyboardNotification

- (void)keyboardWillShow:(NSNotification*)notification {
  [self updateSuggestionsIfNeeded];
  _keyboardHeightChangeNotificationsEnabled = YES;
}

- (void)keyboardWillChangeFrame:(NSNotification*)notification {
  if (!_keyboardHeightChangeNotificationsEnabled) {
    return;
  }

  if (@available(iOS 16.1, *)) {
    CGRect oldKeyboardRect =
        [notification.userInfo[UIKeyboardFrameBeginUserInfoKey] CGRectValue];
    CGRect newKeyboardRect =
        [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];

    // We're only interested in quick height only changes.
    if (oldKeyboardRect.origin.x != newKeyboardRect.origin.x ||
        oldKeyboardRect.size.width != newKeyboardRect.size.width) {
      return;
    }

    [self.consumer keyboardHeightChanged:newKeyboardRect.size.height
                               oldHeight:oldKeyboardRect.size.height];
  }
}

- (void)textInputModeDidChange:(NSNotification*)notification {
  // Disable height change notifications when they are caused by the keyboard
  // language or layout changing. They will get re-enabled in the next
  // "keyboardWillShow:" call above.
  _keyboardHeightChangeNotificationsEnabled = NO;
}

#pragma mark - AutofillBottomSheetObserving

- (void)willShowPaymentsBottomSheetWithParams:
    (const autofill::FormActivityParams&)params {
  // Update params in this mediator because -keyboardWillShow will be called
  // before the bottom sheet is being notified to show and that will call
  // -retrieveSuggestionsForForm with the last seen params. Depending on what
  // the current page is auto focused on, it could be the incorrect params and
  // we need to update it.
  _lastSeenParams = params;
  _hasLastSeenParams = YES;
  [self updateSuggestionsIfNeeded];
}

#pragma mark - FormActivityObserver

- (void)webState:(web::WebState*)webState
    didRegisterFormActivity:(const autofill::FormActivityParams&)params
                    inFrame:(web::WebFrame*)frame {
  DCHECK_EQ(_webState, webState);
  self.validActivityForAccessoryView = NO;

  // Return early if `params` is not complete.
  if (params.input_missing) {
    return;
  }

  // Return early if the URL can't be verified.
  std::optional<GURL> pageURL = webState->GetLastCommittedURLIfTrusted();
  if (!pageURL) {
    [self reset];
    return;
  }

  // Return early, pause and reset if the url is not HTML.
  if (!web::UrlHasWebScheme(*pageURL) || !webState->ContentIsHTML()) {
    [self reset];
    return;
  }

  // Return early and reset if frame is missing or can't call JS.
  if (!frame) {
    [self reset];
    return;
  }

  // Return early and reset if element is a picker.
  if (params.field_type == "select-one") {
    [self reset];
    return;
  }

  self.validActivityForAccessoryView = YES;
  NSString* frameID;
  if (frame) {
    frameID = base::SysUTF8ToNSString(frame->GetFrameId());
  }
  DCHECK(frameID.length);

  [self.formNavigationHandler setLastFocusFormActivityWebFrameID:frameID];
  [self synchronizeNavigationControls];

  // Don't look for suggestions in the next events.
  if (params.type == "blur" || params.type == "change" ||
      params.type == "form_changed") {
    return;
  }

  // Check if we need to reload input views after using an input which did not
  // require the keyboard accessory to show up. Err on the side of calling
  // "reloadInputViews" if the input field types are unrecognized.
  if (!InputTriggersKeyboard(_lastSeenParams.field_type,
                             /*default_value=*/false) &&
      InputTriggersKeyboard(params.field_type, /*default_value=*/true)) {
    [GetFirstResponder() reloadInputViews];
  }
  _lastSeenParams = params;
  _hasLastSeenParams = YES;
  [self retrieveSuggestionsForForm:params webState:webState];
}

#pragma mark - FormInputAccessoryViewDelegate

- (void)formInputAccessoryViewDidTapNextButton:(FormInputAccessoryView*)sender {
  [self.formNavigationHandler selectNextElementWithButtonPress];
}

- (void)formInputAccessoryViewDidTapPreviousButton:
    (FormInputAccessoryView*)sender {
  [self.formNavigationHandler selectPreviousElementWithButtonPress];
}

- (void)formInputAccessoryViewDidTapCloseButton:
    (FormInputAccessoryView*)sender {
  [self.formNavigationHandler closeKeyboardWithButtonPress];
}

- (void)formInputAccessoryViewDidTapManualFillButton:
    (FormInputAccessoryView*)sender {
  [self.consumer manualFillButtonPressed:sender.manualFillButton];
}

- (void)formInputAccessoryViewDidTapPasswordManualFillButton:
    (FormInputAccessoryView*)sender {
  [self.consumer
      passwordManualFillButtonPressed:sender.passwordManualFillButton];
}

- (void)formInputAccessoryViewDidTapCreditCardManualFillButton:
    (FormInputAccessoryView*)sender {
  [self.consumer
      creditCardManualFillButtonPressed:sender.creditCardManualFillButton];
}

- (void)formInputAccessoryViewDidTapAddressManualFillButton:
    (FormInputAccessoryView*)sender {
  [self.consumer addressManualFillButtonPressed:sender.addressManualFillButton];
}

- (FormInputAccessoryViewTextData*)textDataforFormInputAccessoryView:
    (FormInputAccessoryView*)sender {
  return ChromiumAccessoryViewTextData();
}

- (void)fromInputAccessoryViewDidTapOmniboxTypingShield:
    (FormInputAccessoryView*)sender {
  [self.formNavigationHandler closeKeyboardWithOmniboxTypingShield];
}

#pragma mark - CRWWebStateObserver

- (void)webStateWasShown:(web::WebState*)webState {
  DCHECK_EQ(_webState, webState);
  [self updateSuggestionsIfNeeded];
}

- (void)webStateWasHidden:(web::WebState*)webState {
  DCHECK_EQ(_webState, webState);
  [self reset];
}

- (void)webState:(web::WebState*)webState
    didFinishNavigation:(web::NavigationContext*)navigation {
  DCHECK_EQ(_webState, webState);
  [self reset];
}

- (void)webStateDestroyed:(web::WebState*)webState {
  DCHECK_EQ(_webState, webState);
  [self detachFromWebState];
}

#pragma mark - WebStateListObserving

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

#pragma mark - Public

- (void)setSuggestionsEnabled:(BOOL)enabled {
  // Disable height change notifications when they are caused by the keyboard
  // getting reset. They will get re-enabled in the next "keyboardWillShow:"
  // call above.
  _keyboardHeightChangeNotificationsEnabled = NO;

  _suggestionsEnabled = enabled;
  if (enabled) {
    [self updateSuggestionsIfNeeded];
  }
}

- (BOOL)isInputAccessoryViewActive {
  // Return early if there is no WebState.
  if (!_webState) {
    return NO;
  }

  // Return early if the URL can't be verified.
  std::optional<GURL> pageURL = _webState->GetLastCommittedURLIfTrusted();
  if (!pageURL) {
    return NO;
  }

  // Return early if the url is not HTML.
  if (!web::UrlHasWebScheme(*pageURL) || !_webState->ContentIsHTML()) {
    return NO;
  }

  return self.validActivityForAccessoryView;
}

#pragma mark - Setters

- (void)setCurrentProvider:(id<FormInputSuggestionsProvider>)currentProvider {
  if (_currentProvider == currentProvider) {
    return;
  }
  [_currentProvider inputAccessoryViewControllerDidReset];
  _currentProvider = currentProvider;
  _currentProvider.formInputNavigator = self.formNavigationHandler;
}

#pragma mark - Private

// Returns the reauthentication module, which can be an override for testing
// purposes.
- (ReauthenticationModule*)reauthenticationModule {
  id<ReauthenticationProtocol> overrideModule =
      ScopedFormInputAccessoryReauthModuleOverride::Get();
  return overrideModule ? overrideModule : _reauthenticationModule;
}

- (void)updateSuggestionsIfNeeded {
  if (_hasLastSeenParams && _webState) {
    [self retrieveSuggestionsForForm:_lastSeenParams webState:_webState];
  }
}

// Update the status of the consumer form navigation buttons to match the
// handler state.
- (void)synchronizeNavigationControls {
  __weak __typeof(self) weakSelf = self;
  [self.formNavigationHandler
      fetchPreviousAndNextElementsPresenceWithCompletionHandler:^(
          bool previousButtonEnabled, bool nextButtonEnabled) {
        weakSelf.consumer.formInputNextButtonEnabled = nextButtonEnabled;
        weakSelf.consumer.formInputPreviousButtonEnabled =
            previousButtonEnabled;
      }];
}

// Updates the accessory mediator with the passed web state, its JS suggestion
// manager and the registered provider. If nullptr is passed it will instead
// clear those properties in the mediator.
- (void)updateWithNewWebState:(web::WebState*)webState {
  [self detachFromWebState];
  if (webState) {
    self.webState = webState;
    _webStateObserverBridge =
        std::make_unique<web::WebStateObserverBridge>(self);
    webState->AddObserver(_webStateObserverBridge.get());
    _formActivityObserverBridge =
        std::make_unique<autofill::FormActivityObserverBridge>(webState, self);
    _autofillBottomSheetObserverBridge =
        std::make_unique<autofill::AutofillBottomSheetObserverBridge>(
            self, AutofillBottomSheetTabHelper::FromWebState(webState));

    FormSuggestionTabHelper* tabHelper =
        FormSuggestionTabHelper::FromWebState(webState);
    if (tabHelper) {
      self.provider = tabHelper->GetAccessoryViewProvider();
    }
    _formNavigationHandler.webState = webState;
  } else {
    self.webState = nullptr;
    self.provider = nil;
  }
}

// Resets the current provider, the consumer view and the navigation handler. As
// well as reenables suggestions.
- (void)reset {
  _lastSeenParams = autofill::FormActivityParams();
  _hasLastSeenParams = NO;
  [self.consumer showAccessorySuggestions:@[]];

  [self.handler resetFormInputView];

  self.suggestionsEnabled = YES;
  self.currentProvider = nil;
}

// Asynchronously queries the providers for an accessory view. Sends it to
// the consumer if found.
- (void)retrieveSuggestionsForForm:(const autofill::FormActivityParams&)params
                          webState:(web::WebState*)webState {
  DCHECK_EQ(webState, self.webState);
  DCHECK(_hasLastSeenParams);

  __weak id<FormInputSuggestionsProvider> weakProvider = self.provider;
  __weak __typeof(self) weakSelf = self;

  // Get the query ID for this query.
  uint queryID = ++_latestQueryId;

  [weakProvider
      retrieveSuggestionsForForm:params
                        webState:self.webState
        accessoryViewUpdateBlock:^(NSArray<FormSuggestion*>* suggestions,
                                   id<FormInputSuggestionsProvider> provider) {
          // Ignore suggestions if the results aren't from the latest query
          // which provides the most relevant suggestions to fit the current
          // context (i.e. for the field being focused).
          if (queryID != weakSelf.latestQueryId) {
            return;
          }

          // No suggestions found, return and don't update suggestions in view
          // model.
          if (!suggestions) {
            return;
          }

          [weakSelf updateWithProvider:provider suggestions:suggestions];
        }];
}

// Post the passed `suggestions` to the consumer.
- (void)updateWithProvider:(id<FormInputSuggestionsProvider>)provider
               suggestions:(NSArray<FormSuggestion*>*)suggestions {
  if (!self.suggestionsEnabled) {
    if (self.formInputInteractionDelegate) {
      [self.formInputInteractionDelegate
          focusDidChangedWithFillingProduct:provider.mainFillingProduct];
    }
    return;
  }

  // If suggestions are enabled, update `currentProvider`.
  self.currentProvider = provider;

  // Post it to the consumer.
  self.consumer.mainFillingProduct = provider.mainFillingProduct;
  self.consumer.currentFieldId = _lastSeenParams.field_renderer_id;
  [self.consumer showAccessorySuggestions:suggestions];
  if (suggestions.count) {
    if (provider.type == SuggestionProviderTypeAutofill) {
      default_browser::NotifyAutofillSuggestionsShown(self.engagementTracker);

      if (suggestions.firstObject.featureForIPH !=
          SuggestionFeatureForIPH::kUnknown) {
        [self.handler
            showAutofillSuggestionIPHIfNeededFor:suggestions.firstObject
                                                     .featureForIPH];
      }
    }
  }
}

// Handle applicationDidEnterBackground NSNotification.
- (void)applicationDidEnterBackground:(NSNotification*)notification {
  [self.handler resetFormInputView];
}

// Logs information about what type of suggestion the user selected.
- (void)logReauthenticationEvent:(ReauthenticationEvent)reauthenticationEvent
                            type:(autofill::SuggestionType)type {
  std::string histogramName;
  if (self.currentProvider.type == SuggestionProviderTypePassword) {
    histogramName = "IOS.Reauth.Password.Autofill";
  } else if (self.currentProvider.type == SuggestionProviderTypeAutofill) {
    switch (type) {
      case autofill::SuggestionType::kCreditCardEntry:
      case autofill::SuggestionType::kVirtualCreditCardEntry:
        histogramName = "IOS.Reauth.CreditCard.Autofill";
        break;
      case autofill::SuggestionType::kAddressEntry:
        histogramName = "IOS.Reauth.Address.Autofill";
        break;
      default:
        break;
    }
  }
  if (!histogramName.empty()) {
    UmaHistogramEnumeration(histogramName, reauthenticationEvent);
  }
}

// Handles the selection of a suggestion. `index` indicates the position of the
// suggestion among the available suggestions.
- (void)handleSuggestion:(FormSuggestion*)formSuggestion
                 atIndex:(NSInteger)index {
  if (self.currentProvider.type == SuggestionProviderTypePassword) {
    default_browser::NotifyPasswordAutofillSuggestionUsed(
        self.engagementTracker);
  } else if (formSuggestion.featureForIPH !=
             SuggestionFeatureForIPH::kUnknown) {
    [self.handler
        notifyAutofillSuggestionWithIPHSelectedFor:formSuggestion
                                                       .featureForIPH];
  }
  [self.currentProvider didSelectSuggestion:formSuggestion atIndex:index];
}

#pragma mark - Boolean Observer

- (void)booleanDidChange:(id<ObservableBoolean>)observableBoolean {
  if (observableBoolean == _bottomOmniboxEnabled) {
    [self.consumer newOmniboxPositionIsBottom:_bottomOmniboxEnabled.value];
  }
}

#pragma mark - FormSuggestionClient

- (void)didSelectSuggestion:(FormSuggestion*)formSuggestion
                    atIndex:(NSInteger)index {
  [self logReauthenticationEvent:ReauthenticationEvent::kAttempt
                            type:formSuggestion.type];

  if (!formSuggestion.requiresReauth) {
    [self logReauthenticationEvent:ReauthenticationEvent::kSuccess
                              type:formSuggestion.type];
    [self handleSuggestion:formSuggestion atIndex:index];
    return;
  }
  if ([self.reauthenticationModule canAttemptReauth]) {
    NSString* reason = l10n_util::GetNSString(IDS_IOS_AUTOFILL_REAUTH_REASON);
    auto completionHandler = ^(ReauthenticationResult result) {
      if (result != ReauthenticationResult::kFailure) {
        [self logReauthenticationEvent:ReauthenticationEvent::kSuccess
                                  type:formSuggestion.type];
        [self handleSuggestion:formSuggestion atIndex:index];
      } else {
        [self logReauthenticationEvent:ReauthenticationEvent::kFailure
                                  type:formSuggestion.type];
      }
    };

    [self.reauthenticationModule
        attemptReauthWithLocalizedReason:reason
                    canReusePreviousAuth:YES
                                 handler:completionHandler];
  } else {
    [self logReauthenticationEvent:ReauthenticationEvent::kMissingPasscode
                              type:formSuggestion.type];
    [self handleSuggestion:formSuggestion atIndex:index];
  }
}

- (void)didSelectSuggestion:(FormSuggestion*)formSuggestion
                    atIndex:(NSInteger)index
                     params:(const autofill::FormActivityParams&)params {
  CHECK(_lastSeenParams == params);
  [self didSelectSuggestion:formSuggestion atIndex:index];
}

#pragma mark - PasswordCounterObserver

- (void)passwordCounterChanged:(size_t)totalPasswords {
  self.consumer.passwordButtonHidden = !totalPasswords;
}

#pragma mark - PersonalDataManagerObserver

- (void)onPersonalDataChanged {
  DCHECK(_personalDataManager);

  self.consumer.creditCardButtonHidden =
      _personalDataManager->payments_data_manager().GetCreditCards().empty();

  self.consumer.addressButtonHidden =
      _personalDataManager->address_data_manager()
          .GetProfilesToSuggest()
          .empty();
}

#pragma mark - Tests

- (void)injectWebState:(web::WebState*)webState {
  [self detachFromWebState];
  _webState = webState;
  if (!_webState) {
    return;
  }
  _webStateObserverBridge = std::make_unique<web::WebStateObserverBridge>(self);
  _webState->AddObserver(_webStateObserverBridge.get());
  _formActivityObserverBridge =
      std::make_unique<autofill::FormActivityObserverBridge>(_webState, self);
}

- (void)injectProvider:(id<FormInputSuggestionsProvider>)provider {
  self.provider = provider;
}

@end