chromium/ios/chrome/browser/lens_overlay/coordinator/lens_overlay_coordinator.mm

// Copyright 2024 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/lens_overlay/coordinator/lens_overlay_coordinator.h"

#import "base/check.h"
#import "components/prefs/pref_service.h"
#import "ios/chrome/browser/feature_engagement/model/tracker_factory.h"
#import "ios/chrome/browser/lens_overlay/coordinator/lens_omnibox_client.h"
#import "ios/chrome/browser/lens_overlay/coordinator/lens_omnibox_client_delegate.h"
#import "ios/chrome/browser/lens_overlay/coordinator/lens_overlay_availability.h"
#import "ios/chrome/browser/lens_overlay/coordinator/lens_overlay_mediator.h"
#import "ios/chrome/browser/lens_overlay/coordinator/lens_result_page_mediator.h"
#import "ios/chrome/browser/lens_overlay/coordinator/lens_result_page_web_state_delegate.h"
#import "ios/chrome/browser/lens_overlay/model/lens_overlay_snapshot_controller.h"
#import "ios/chrome/browser/lens_overlay/model/lens_overlay_tab_helper.h"
#import "ios/chrome/browser/lens_overlay/ui/lens_overlay_consent_view_controller.h"
#import "ios/chrome/browser/lens_overlay/ui/lens_overlay_container_view_controller.h"
#import "ios/chrome/browser/lens_overlay/ui/lens_result_page_consumer.h"
#import "ios/chrome/browser/lens_overlay/ui/lens_result_page_view_controller.h"
#import "ios/chrome/browser/lens_overlay/ui/lens_toolbar_consumer.h"
#import "ios/chrome/browser/shared/model/application_context/application_context.h"
#import "ios/chrome/browser/shared/model/browser/browser.h"
#import "ios/chrome/browser/shared/model/browser_state/chrome_browser_state.h"
#import "ios/chrome/browser/shared/model/prefs/pref_names.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.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/lens_overlay_commands.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/signin/model/authentication_service.h"
#import "ios/chrome/browser/signin/model/authentication_service_factory.h"
#import "ios/chrome/browser/snapshots/model/snapshot_tab_helper.h"
#import "ios/chrome/browser/ui/fullscreen/fullscreen_controller.h"
#import "ios/chrome/browser/ui/lens/lens_entrypoint.h"
#import "ios/chrome/browser/ui/omnibox/chrome_omnibox_client_ios.h"
#import "ios/chrome/browser/ui/omnibox/omnibox_coordinator.h"
#import "ios/chrome/browser/ui/omnibox/omnibox_focus_delegate.h"
#import "ios/chrome/browser/web/model/web_state_delegate_browser_agent.h"
#import "ios/public/provider/chrome/browser/lens/lens_configuration.h"
#import "ios/public/provider/chrome/browser/lens/lens_overlay_api.h"
#import "ios/web/public/web_state.h"
#import "url/gurl.h"

namespace {

LensEntrypoint LensEntrypointFromOverlayEntrypoint(
    LensOverlayEntrypoint overlayEntrypoint) {
  switch (overlayEntrypoint) {
    case LensOverlayEntrypoint::kLocationBar:
      return LensEntrypoint::LensOverlayLocationBar;
    case LensOverlayEntrypoint::kOverflowMenu:
      return LensEntrypoint::LensOverlayOverflowMenu;
  }
}

}  // namespace

@interface LensOverlayCoordinator () <LensOverlayCommands,
                                      UISheetPresentationControllerDelegate,
                                      LensOverlayResultConsumer,
                                      LensResultPageWebStateDelegate,
                                      LensOverlayConsentViewControllerDelegate>

@end

@implementation LensOverlayCoordinator {
  /// Container view controller.
  /// Hosts all of lens UI: contains the selection UI, presents the results UI
  /// modally.
  LensOverlayContainerViewController* _containerViewController;

  /// The mediator for lens overlay.
  LensOverlayMediator* _mediator;

  /// The view controller for lens results.
  LensResultPageViewController* _resultViewController;
  /// The mediator for lens results.
  LensResultPageMediator* _resultMediator;

  /// The tab helper associated with the current UI.
  LensOverlayTabHelper* _associatedTabHelper;

  /// Coordinator of the omnibox.
  OmniboxCoordinator* _omniboxCoordinator;

  LensOverlayConsentViewController* _consentViewController;

  UIViewController<ChromeLensOverlay>* _selectionViewController;
}

#pragma mark - public

- (UIViewController*)viewController {
  return _containerViewController;
}

#pragma mark - Helpers

// Returns whether the UI was created succesfully.
- (BOOL)createUIWithSnapshot:(UIImage*)snapshot
                  entrypoint:(LensOverlayEntrypoint)entrypoint {
  [self createContainerViewController];

  [self createSelectionViewControllerWithSnapshot:snapshot
                                       entrypoint:entrypoint];
  if (!_selectionViewController) {
    return NO;
  }

  [self createMediator];

  // Wire up consumers and delegates
  _containerViewController.selectionViewController = _selectionViewController;
  [_selectionViewController setLensOverlayDelegate:_mediator];
  _mediator.commandsHandler = self;

  if ([self termsOfServiceAccepted]) {
    [_selectionViewController start];
  }
  return YES;
}

- (void)createSelectionViewControllerWithSnapshot:(UIImage*)snapshot
                                       entrypoint:
                                           (LensOverlayEntrypoint)entrypoint {
  if (_selectionViewController) {
    return;
  }
  LensConfiguration* config =
      [self createLensConfigurationForEntrypoint:entrypoint];
  _selectionViewController =
      ios::provider::NewChromeLensOverlay(snapshot, config);
}

- (void)createContainerViewController {
  if (_containerViewController) {
    return;
  }
  _containerViewController = [[LensOverlayContainerViewController alloc]
      initWithLensOverlayCommandsHandler:self];
  _containerViewController.modalPresentationStyle =
      UIModalPresentationOverFullScreen;
  _containerViewController.modalTransitionStyle =
      UIModalTransitionStyleCrossDissolve;
}

- (void)createMediator {
  if (_mediator) {
    return;
  }
  _mediator = [[LensOverlayMediator alloc] init];

  // Results UI is lazily initialized; see comment in LensOverlayResultConsumer
  // section.
  _mediator.resultConsumer = self;
}

- (BOOL)createConsentViewController {
  _consentViewController = [[LensOverlayConsentViewController alloc] init];
  _consentViewController.delegate = self;

  return YES;
}

#pragma mark - ChromeCoordinator

- (void)start {
  CHECK(IsLensOverlayAvailable());
  [super start];

  Browser* browser = self.browser;
  CHECK(browser, kLensOverlayNotFatalUntil);

  [[NSNotificationCenter defaultCenter]
      addObserver:self
         selector:@selector(lowMemoryWarningReceived)
             name:UIApplicationDidReceiveMemoryWarningNotification
           object:nil];

  [browser->GetCommandDispatcher()
      startDispatchingToTarget:self
                   forProtocol:@protocol(LensOverlayCommands)];
}

- (void)stop {
  if (Browser* browser = self.browser) {
    [browser->GetCommandDispatcher() stopDispatchingToTarget:self];
  }

  [[NSNotificationCenter defaultCenter]
      removeObserver:self
                name:UIApplicationDidReceiveMemoryWarningNotification
              object:nil];

  [super stop];
}

#pragma mark - LensOverlayCommands

- (void)createAndShowLensUI:(BOOL)animated
                 entrypoint:(LensOverlayEntrypoint)entrypoint {
  if ([self isUICreated]) {
    // The UI is probably associated with the non-active tab. Destroy it with no
    // animation.
    [self destroyLensUI:NO];
  }

  _associatedTabHelper = [self activeTabHelper];
  CHECK(_associatedTabHelper, kLensOverlayNotFatalUntil);

  // The instance that creates the Lens UI designates itself as the command
  // handler for the associated tab.
  _associatedTabHelper->SetLensOverlayCommandsHandler(self);
  _associatedTabHelper->SetLensOverlayShown(true);

  __weak __typeof(self) weakSelf = self;
  [self captureSnapshotWithCompletion:^(UIImage* snapshot) {
    __typeof(self) strongSelf = weakSelf;
    if (!weakSelf) {
      return;
    }
    if (snapshot == nil) {
      return;
    }

    BOOL success = [strongSelf createUIWithSnapshot:snapshot
                                         entrypoint:entrypoint];
    if (success) {
      [strongSelf showLensUI:animated];
    } else {
      [strongSelf destroyLensUI:NO];
    }
  }];
}

- (void)showLensUI:(BOOL)animated {
  if (![self isUICreated]) {
    return;
  }

  __weak __typeof(self) weakSelf = self;
  [self.baseViewController
      presentViewController:_containerViewController
                   animated:animated
                 completion:^{
                   [weakSelf showConsentViewControllerIfNeeded];
                 }];
}

- (void)showConsentViewControllerIfNeeded {
  if (self.termsOfServiceAccepted) {
    return;
  }

  [self createConsentViewController];
  [self showViewControllerAsBottomSheet:_consentViewController];
}

- (void)hideLensUI:(BOOL)animated {
  if (![self isUICreated]) {
    return;
  }

  [_containerViewController.presentingViewController
      dismissViewControllerAnimated:animated
                         completion:nil];
}

- (void)destroyLensUI:(BOOL)animated {
  // The reason the UI is destroyed can be that Omnient gets associated to a
  // different tab. In this case mark the stale tab helper as not shown.
  if (_associatedTabHelper) {
    _associatedTabHelper->SetLensOverlayShown(false);
    _associatedTabHelper->UpdateSnapshot();
    _associatedTabHelper = nil;
  }

  // Taking the screenshot triggered fullscreen mode. Ensure it's reverted in
  // the cleanup process. Exiting fullscreen has to happen on destruction to
  // ensure a smooth transition back to the content.
  __weak __typeof(self) weakSelf = self;
  if (_containerViewController.presentingViewController) {
    [self exitFullscreenAnimated:YES];
    [_containerViewController.presentingViewController
        dismissViewControllerAnimated:animated
                           completion:^{
                             [weakSelf destroyViewControllersAndMediators];
                           }];
  } else {
    [self exitFullscreenAnimated:NO];
    [self destroyViewControllersAndMediators];
  }
}

#pragma mark - UISheetPresentationControllerDelegate

- (BOOL)presentationControllerShouldDismiss:
    (UIPresentationController*)presentationController {
  return NO;
}

#pragma mark - LensOverlayResultConsumer

// This coordinator acts as a proxy consumer to the result consumer to implement
// lazy initialization of the result UI.
// Upon any call, the results UI is created and set as consumer, then the call
// is repeated.
- (void)loadResultsURL:(GURL)url {
  DCHECK(!_resultMediator);

  [self startResultPage];
  [_resultMediator loadResultsURL:url];
}

#pragma mark - LensResultPageWebStateDelegate

- (void)lensResultPageWebStateDestroyed {
  [self stopResultPage];
}

- (void)lensResultPageDidChangeActiveWebState:(web::WebState*)webState {
  _mediator.webState = webState;
}

#pragma mark - LensOverlayConsentViewControllerDelegate

- (void)consentViewController:(LensOverlayConsentViewController*)viewController
    didFinishWithTermsAccepted:(BOOL)accepted {
  self.browser->GetBrowserState()->GetPrefs()->SetBoolean(
      prefs::kLensOverlayConditionsAccepted, accepted);

  if (accepted) {
    // consentViewController is still presented, so the strong reference can be
    // removed here.
    _consentViewController = nil;

    __weak __typeof(self) weakSelf = self;
    [_containerViewController
        dismissViewControllerAnimated:YES
                           completion:^{
                             [weakSelf startSelectionViewController];
                           }];
  } else {
    [self destroyLensUI:YES];
  }
}

- (void)startSelectionViewController {
  [_selectionViewController start];
}

#pragma mark - private

// Lens needs to have visibility into the user's identity and whether the search
// should be incognito or not.
- (LensConfiguration*)createLensConfigurationForEntrypoint:
    (LensOverlayEntrypoint)entrypoint {
  Browser* browser = self.browser;
  LensConfiguration* configuration = [[LensConfiguration alloc] init];
  BOOL isIncognito = browser->GetBrowserState()->IsOffTheRecord();
  configuration.isIncognito = isIncognito;
  configuration.singleSignOnService =
      GetApplicationContext()->GetSingleSignOnService();
  configuration.entrypoint = LensEntrypointFromOverlayEntrypoint(entrypoint);

  if (!isIncognito) {
    AuthenticationService* authenticationService =
        AuthenticationServiceFactory::GetForBrowserState(
            browser->GetBrowserState());
    id<SystemIdentity> identity = authenticationService->GetPrimaryIdentity(
        ::signin::ConsentLevel::kSignin);
    configuration.identity = identity;
  }

  return configuration;
}

- (BOOL)termsOfServiceAccepted {
  return self.browser->GetBrowserState()->GetPrefs()->GetBoolean(
      prefs::kLensOverlayConditionsAccepted);
}

- (void)startResultPage {
  Browser* browser = self.browser;
  ChromeBrowserState* browserState = browser->GetBrowserState();

  web::WebState::CreateParams params =
      web::WebState::CreateParams(browserState);
  web::WebStateDelegate* browserWebStateDelegate =
      WebStateDelegateBrowserAgent::FromBrowser(browser);
  _resultMediator = [[LensResultPageMediator alloc]
       initWithWebStateParams:params
      browserWebStateDelegate:browserWebStateDelegate
                  isIncognito:browserState->IsOffTheRecord()];
  _resultMediator.applicationHandler =
      HandlerForProtocol(browser->GetCommandDispatcher(), ApplicationCommands);
  _resultMediator.webStateDelegate = self;
  _mediator.resultConsumer = _resultMediator;

  _resultViewController = [[LensResultPageViewController alloc] init];

  _resultMediator.consumer = _resultViewController;
  _resultMediator.webViewContainer = _resultViewController.webViewContainer;

  [self showViewControllerAsBottomSheet:_resultViewController];

  // TODO(crbug.com/355179986): Implement omnibox navigation with
  // omnibox_delegate.
  auto omniboxClient = std::make_unique<LensOmniboxClient>(
      browserState,
      feature_engagement::TrackerFactory::GetForBrowserState(browserState),
      /*web_provider=*/_resultMediator,
      /*omnibox_delegate=*/_mediator);

  _omniboxCoordinator = [[OmniboxCoordinator alloc]
      initWithBaseViewController:nil
                         browser:browser
                   omniboxClient:std::move(omniboxClient)];

  // TODO(crbug.com/355179721): Add omnibox focus delegate.
  _omniboxCoordinator.presenterDelegate = _resultViewController;
  [_omniboxCoordinator start];

  [_omniboxCoordinator.managedViewController
      willMoveToParentViewController:_resultViewController];
  [_resultViewController
      addChildViewController:_omniboxCoordinator.managedViewController];
  [_resultViewController setEditView:_omniboxCoordinator.editView];
  [_omniboxCoordinator.managedViewController
      didMoveToParentViewController:_resultViewController];

  [_omniboxCoordinator updateOmniboxState];

  _mediator.omniboxCoordinator = _omniboxCoordinator;
  _mediator.toolbarConsumer = _resultViewController;
  _resultViewController.toolbarMutator = _mediator;
  _omniboxCoordinator.focusDelegate = _mediator;
}

- (void)exitFullscreenAnimated:(BOOL)animated {
  Browser* browser = self.browser;
  if (!browser) {
    return;
  }

  FullscreenController* fullscreenController =
      FullscreenController::FromBrowser(browser);

  if (animated) {
    fullscreenController->ExitFullscreen();
  } else {
    fullscreenController->ExitFullscreenWithoutAnimation();
  }
}

- (void)stopResultPage {
  [_resultViewController.presentingViewController
      dismissViewControllerAnimated:YES
                         completion:nil];
  _resultViewController = nil;
  [_resultMediator disconnect];
  _resultMediator = nil;
  _mediator.resultConsumer = self;
  [_omniboxCoordinator stop];
  _omniboxCoordinator = nil;
}

- (BOOL)isUICreated {
  return _containerViewController != nil;
}

// Disconnect and destroy all of the owned view controllers.
- (void)destroyViewControllersAndMediators {
  [self stopResultPage];
  _containerViewController = nil;
  [_mediator disconnect];
  _selectionViewController = nil;
  _mediator = nil;
  _consentViewController = nil;
}

// The tab helper for the active web state.
- (LensOverlayTabHelper*)activeTabHelper {
  if (!self.browser || !self.browser->GetWebStateList() ||
      !self.browser->GetWebStateList()->GetActiveWebState()) {
    return nullptr;
  }

  web::WebState* activeWebState =
      self.browser->GetWebStateList()->GetActiveWebState();
  LensOverlayTabHelper* tabHelper =
      LensOverlayTabHelper::FromWebState(activeWebState);

  CHECK(tabHelper, kLensOverlayNotFatalUntil);

  return tabHelper;
}

// Captures a screenshot of the active web state.
- (void)captureSnapshotWithCompletion:(void (^)(UIImage*))completion {
  Browser* browser = self.browser;
  if (!browser) {
    completion(nil);
    return;
  }

  web::WebState* activeWebState =
      browser->GetWebStateList()->GetActiveWebState();

  if (!activeWebState) {
    completion(nil);
    return;
  }

  CHECK(_associatedTabHelper, kLensOverlayNotFatalUntil);

  _associatedTabHelper->SetSnapshotController(
      std::make_unique<LensOverlaySnapshotController>(
          SnapshotTabHelper::FromWebState(activeWebState),
          FullscreenController::FromBrowser(browser),
          base::SequencedTaskRunner::GetCurrentDefault()));

  _associatedTabHelper->CaptureFullscreenSnapshot(base::BindOnce(completion));
}

- (void)lowMemoryWarningReceived {
  // Preserve the UI if it's currently visible to the user.
  if ([self isLensOverlayVisible]) {
    return;
  }

  [self destroyLensUI:NO];
}

- (BOOL)isLensOverlayVisible {
  return self.baseViewController.presentedViewController != nil;
}

- (void)showViewControllerAsBottomSheet:(UIViewController*)viewController {
  UISheetPresentationController* sheet =
      viewController.sheetPresentationController;
  sheet.delegate = self;
  sheet.prefersEdgeAttachedInCompactHeight = YES;
  sheet.largestUndimmedDetentIdentifier =
      [UISheetPresentationControllerDetent largeDetent].identifier;
  sheet.detents = @[
    [UISheetPresentationControllerDetent mediumDetent],
    [UISheetPresentationControllerDetent largeDetent]
  ];
  sheet.prefersGrabberVisible = YES;

  [_containerViewController presentViewController:viewController
                                         animated:YES
                                       completion:nil];
}

@end