chromium/ios/chrome/browser/ui/omnibox/omnibox_coordinator.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/ui/omnibox/omnibox_coordinator.h"

#import "base/check.h"
#import "base/ios/ios_util.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/tracker.h"
#import "components/omnibox/browser/omnibox_controller.h"
#import "components/omnibox/browser/omnibox_edit_model.h"
#import "components/omnibox/common/omnibox_features.h"
#import "components/omnibox/common/omnibox_focus_state.h"
#import "components/open_from_clipboard/clipboard_recent_content.h"
#import "components/search_engines/template_url_service.h"
#import "components/strings/grit/components_strings.h"
#import "ios/chrome/browser/favicon/model/ios_chrome_favicon_loader_factory.h"
#import "ios/chrome/browser/feature_engagement/model/tracker_factory.h"
#import "ios/chrome/browser/location_bar/ui_bundled/location_bar_constants.h"
#import "ios/chrome/browser/search_engines/model/template_url_service_factory.h"
#import "ios/chrome/browser/shared/coordinator/layout_guide/layout_guide_util.h"
#import "ios/chrome/browser/shared/model/browser/browser.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/shared/public/commands/application_commands.h"
#import "ios/chrome/browser/shared/public/commands/command_dispatcher.h"
#import "ios/chrome/browser/shared/public/commands/help_commands.h"
#import "ios/chrome/browser/shared/public/commands/lens_commands.h"
#import "ios/chrome/browser/shared/public/commands/load_query_commands.h"
#import "ios/chrome/browser/shared/public/commands/omnibox_commands.h"
#import "ios/chrome/browser/shared/public/commands/qr_scanner_commands.h"
#import "ios/chrome/browser/shared/public/commands/toolbar_commands.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/ui/omnibox/keyboard_assist/omnibox_assistive_keyboard_delegate.h"
#import "ios/chrome/browser/ui/omnibox/keyboard_assist/omnibox_assistive_keyboard_mediator.h"
#import "ios/chrome/browser/ui/omnibox/keyboard_assist/omnibox_assistive_keyboard_views.h"
#import "ios/chrome/browser/ui/omnibox/keyboard_assist/omnibox_keyboard_accessory_view.h"
#import "ios/chrome/browser/ui/omnibox/omnibox_mediator.h"
#import "ios/chrome/browser/ui/omnibox/omnibox_return_key_forwarding_delegate.h"
#import "ios/chrome/browser/ui/omnibox/omnibox_text_field_ios.h"
#import "ios/chrome/browser/ui/omnibox/omnibox_text_field_paste_delegate.h"
#import "ios/chrome/browser/ui/omnibox/omnibox_util.h"
#import "ios/chrome/browser/ui/omnibox/omnibox_view_controller.h"
#import "ios/chrome/browser/ui/omnibox/omnibox_view_ios.h"
#import "ios/chrome/browser/ui/omnibox/popup/omnibox_popup_coordinator.h"
#import "ios/chrome/browser/ui/omnibox/popup/omnibox_popup_view_ios.h"
#import "ios/chrome/browser/ui/omnibox/popup/pedal_section_extractor.h"
#import "ios/chrome/browser/ui/omnibox/text_field_view_containing.h"
#import "ios/chrome/browser/ui/omnibox/zero_suggest_prefetch_helper.h"
#import "ios/chrome/browser/url_loading/model/image_search_param_generator.h"
#import "ios/chrome/browser/url_loading/model/url_loading_browser_agent.h"
#import "ios/chrome/browser/url_loading/model/url_loading_params.h"
#import "ios/web/public/navigation/navigation_manager.h"

@interface OmniboxCoordinator () <OmniboxViewControllerTextInputDelegate,
                                  OmniboxAssistiveKeyboardMediatorDelegate>

// View controller managed by this coordinator.
@property(nonatomic, strong) OmniboxViewController* viewController;

// The mediator for the omnibox.
@property(nonatomic, strong) OmniboxMediator* mediator;

// The paste delegate for the omnibox that prevents multipasting.
@property(nonatomic, strong) OmniboxTextFieldPasteDelegate* pasteDelegate;

// The return delegate.
@property(nonatomic, strong) ForwardingReturnDelegate* returnDelegate;

// Helper that starts ZPS prefetch when the user opens a NTP.
@property(nonatomic, strong)
    ZeroSuggestPrefetchHelper* zeroSuggestPrefetchHelper;

// The keyboard accessory view. Will be nil if the app is running on an iPad.
@property(nonatomic, strong)
    OmniboxKeyboardAccessoryView* keyboardAccessoryView;

// Redefined as readwrite.
@property(nonatomic, strong) OmniboxPopupCoordinator* popupCoordinator;

@end

@implementation OmniboxCoordinator {
  // TODO(crbug.com/40565663): use a slimmer subclass of OmniboxView,
  // OmniboxPopupViewSuggestionsDelegate instead of OmniboxViewIOS.
  std::unique_ptr<OmniboxViewIOS> _editView;

  // Omnibox client. Stored between init and start, then ownership is passed to
  // OmniboxView.
  std::unique_ptr<OmniboxClient> _client;

  /// Object handling interactions in the keyboard accessory view.
  OmniboxAssistiveKeyboardMediator* _keyboardMediator;

  // The handler for ToolbarCommands.
  id<ToolbarCommands> _toolbarHandler;
}
@synthesize viewController = _viewController;
@synthesize mediator = _mediator;

#pragma mark - public

- (instancetype)initWithBaseViewController:(UIViewController*)viewController
                                   browser:(Browser*)browser
                             omniboxClient:
                                 (std::unique_ptr<OmniboxClient>)client {
  self = [super initWithBaseViewController:viewController browser:browser];
  if (self) {
    _client = std::move(client);
  }
  return self;
}

- (void)start {
  DCHECK(!self.popupCoordinator);

  _toolbarHandler =
      HandlerForProtocol(self.browser->GetCommandDispatcher(), ToolbarCommands);

  self.viewController = [[OmniboxViewController alloc] init];
  self.viewController.defaultLeadingImage =
      GetOmniboxSuggestionIcon(OmniboxSuggestionIconType::kDefaultFavicon);
  self.viewController.textInputDelegate = self;
  self.viewController.layoutGuideCenter =
      LayoutGuideCenterForBrowser(self.browser);

  BOOL isIncognito = self.browser->GetBrowserState()->IsOffTheRecord();
  self.mediator = [[OmniboxMediator alloc]
      initWithIncognito:isIncognito
                tracker:feature_engagement::TrackerFactory::GetForBrowserState(
                            self.browser->GetBrowserState())];

  TemplateURLService* templateURLService =
      ios::TemplateURLServiceFactory::GetForBrowserState(
          self.browser->GetBrowserState());
  self.mediator.templateURLService = templateURLService;
  self.mediator.faviconLoader =
      IOSChromeFaviconLoaderFactory::GetForBrowserState(
          self.browser->GetBrowserState());
  self.mediator.consumer = self.viewController;
  self.mediator.omniboxCommandsHandler =
      HandlerForProtocol(self.browser->GetCommandDispatcher(), OmniboxCommands);
  self.mediator.lensCommandsHandler =
      HandlerForProtocol(self.browser->GetCommandDispatcher(), LensCommands);
  self.mediator.loadQueryCommandsHandler = HandlerForProtocol(
      self.browser->GetCommandDispatcher(), LoadQueryCommands);
  self.mediator.sceneState = self.browser->GetSceneState();
  self.mediator.URLLoadingBrowserAgent =
      UrlLoadingBrowserAgent::FromBrowser(self.browser);
  self.viewController.pasteDelegate = self.mediator;

  DCHECK(_client.get());

  id<OmniboxCommands> omniboxHandler =
      HandlerForProtocol(self.browser->GetCommandDispatcher(), OmniboxCommands);
  _editView = std::make_unique<OmniboxViewIOS>(
      self.textField, std::move(_client), self.browser->GetBrowserState(),
      omniboxHandler, self.focusDelegate, _toolbarHandler, self.viewController);
  self.pasteDelegate = [[OmniboxTextFieldPasteDelegate alloc] init];
  [self.textField setPasteDelegate:self.pasteDelegate];

  self.viewController.textChangeDelegate = _editView.get();

  _keyboardMediator = [[OmniboxAssistiveKeyboardMediator alloc] init];
  _keyboardMediator.applicationCommandsHandler = HandlerForProtocol(
      self.browser->GetCommandDispatcher(), ApplicationCommands);
  _keyboardMediator.lensCommandsHandler =
      HandlerForProtocol(self.browser->GetCommandDispatcher(), LensCommands);
  _keyboardMediator.qrScannerCommandsHandler = HandlerForProtocol(
      self.browser->GetCommandDispatcher(), QRScannerCommands);
  _keyboardMediator.layoutGuideCenter =
      LayoutGuideCenterForBrowser(self.browser);
  // TODO(crbug.com/40670043): Use HandlerForProtocol after commands protocol
  // clean up.
  _keyboardMediator.browserCoordinatorCommandsHandler =
      static_cast<id<BrowserCoordinatorCommands>>(
          self.browser->GetCommandDispatcher());
  _keyboardMediator.omniboxTextField = self.textField;
  _keyboardMediator.delegate = self;

  if (base::FeatureList::IsEnabled(omnibox::kZeroSuggestPrefetching)) {
    self.zeroSuggestPrefetchHelper = [[ZeroSuggestPrefetchHelper alloc]
        initWithWebStateList:self.browser->GetWebStateList()
                  controller:_editView->controller()];
  }

  self.popupCoordinator = [self createPopupCoordinator:self.presenterDelegate];
  [self.popupCoordinator start];
}

- (void)stop {
  [self.popupCoordinator stop];
  self.popupCoordinator = nil;

  self.viewController.textChangeDelegate = nil;
  self.returnDelegate.acceptDelegate = nil;
  _editView.reset();
  self.viewController = nil;
  self.mediator.templateURLService = nullptr;  // Unregister the observer.
  if (self.keyboardAccessoryView) {
    // Unregister the observer.
    self.keyboardAccessoryView.templateURLService = nil;
  }

  _keyboardMediator = nil;
  self.keyboardAccessoryView = nil;
  self.mediator = nil;
  self.returnDelegate = nil;
  self.zeroSuggestPrefetchHelper = nil;

  [NSNotificationCenter.defaultCenter removeObserver:self];
}

- (void)updateOmniboxState {
  _editView->UpdateAppearance();
}

- (BOOL)isOmniboxFirstResponder {
  return [self.textField isFirstResponder];
}

- (void)focusOmnibox {
  if (!self.keyboardAccessoryView) {
    TemplateURLService* templateURLService =
        ios::TemplateURLServiceFactory::GetForBrowserState(
            self.browser->GetBrowserState());
    self.keyboardAccessoryView = ConfigureAssistiveKeyboardViews(
        self.textField, kDotComTLD, _keyboardMediator, templateURLService,
        HandlerForProtocol(self.browser->GetCommandDispatcher(), HelpCommands));
  }

  if (![self.textField isFirstResponder]) {
    base::RecordAction(base::UserMetricsAction("MobileOmniboxFocused"));

    // In multiwindow context, -becomeFirstRepsonder is not enough to get the
    // keyboard input. The window will not automatically become key. Make it key
    // manually. UITextField does this under the hood when tapped from
    // -[UITextInteractionAssistant(UITextInteractionAssistant_Internal)
    // setFirstResponderIfNecessaryActivatingSelection:]
    if (base::ios::IsMultipleScenesSupported()) {
      [self.textField.window makeKeyAndVisible];
    }

    [self.textField becomeFirstResponder];
    // Ensures that the accessibility system focuses the text field instead of
    // the popup crbug.com/1469173.
    UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification,
                                    self.textField);
  }
}

- (void)endEditing {
  [self.textField resignFirstResponder];
  _editView->EndEditing();
}

- (void)insertTextToOmnibox:(NSString*)text {
  [self.textField insertTextWhileEditing:text];
  // The call to `setText` shouldn't be needed, but without it the "Go" button
  // of the keyboard is disabled.
  [self.textField setText:text];
  // Notify the accessibility system to start reading the new contents of the
  // Omnibox.
  UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification,
                                  self.textField);
}

- (OmniboxPopupCoordinator*)createPopupCoordinator:
    (id<OmniboxPopupPresenterDelegate>)presenterDelegate {
  DCHECK(!_popupCoordinator);
  std::unique_ptr<OmniboxPopupViewIOS> popupView =
      std::make_unique<OmniboxPopupViewIOS>(_editView->controller(),
                                            _editView.get());

  _editView->SetPopupProvider(popupView.get());

  OmniboxPopupCoordinator* coordinator = [[OmniboxPopupCoordinator alloc]
      initWithBaseViewController:nil
                         browser:self.browser
          autocompleteController:_editView->controller()
                                     ->autocomplete_controller()
                       popupView:std::move(popupView)];
  coordinator.presenterDelegate = presenterDelegate;

  self.returnDelegate = [[ForwardingReturnDelegate alloc] init];
  self.returnDelegate.acceptDelegate = _editView.get();

  coordinator.popupMatchPreviewDelegate = self.mediator;
  coordinator.acceptReturnDelegate = self.returnDelegate;
  self.viewController.returnKeyDelegate = coordinator.popupReturnDelegate;
  self.viewController.popupKeyboardDelegate = coordinator.KeyboardDelegate;

  _popupCoordinator = coordinator;
  return coordinator;
}

- (UIViewController*)managedViewController {
  return self.viewController;
}

- (id<LocationBarOffsetProvider>)offsetProvider {
  return self.viewController;
}

- (id<EditViewAnimatee>)animatee {
  return self.viewController;
}

- (id<ToolbarOmniboxConsumer>)toolbarOmniboxConsumer {
  return self.popupCoordinator.toolbarOmniboxConsumer;
}

- (UIView<TextFieldViewContaining>*)editView {
  return self.viewController.viewContainingTextField;
}

- (void)setThumbnailImage:(UIImage*)image {
  if (_editView) {
    _editView->SetThumbnailImage(image);
  }
}

#pragma mark Scribble

- (void)focusOmniboxForScribble {
  [self.textField becomeFirstResponder];
  [self.viewController prepareOmniboxForScribble];
}

- (UIResponder<UITextInput>*)scribbleInput {
  return self.viewController.textField;
}

#pragma mark - OmniboxAssistiveKeyboardMediatorDelegate

- (void)omniboxAssistiveKeyboardDidTapDebuggerButton {
  [self.popupCoordinator toggleOmniboxDebuggerView];
}

#pragma mark - OmniboxViewControllerTextInputDelegate

- (void)omniboxViewControllerTextInputModeDidChange:
    (OmniboxViewController*)omniboxViewController {
  _editView->UpdatePopupAppearance();
}

#pragma mark - private

// Convenience accessor.
- (OmniboxTextFieldIOS*)textField {
  return self.viewController.textField;
}

@end