| // 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_mediator.h" |
| |
| #import <memory> |
| |
| #import "base/base64url.h" |
| #import "base/metrics/user_metrics.h" |
| #import "base/metrics/user_metrics_action.h" |
| #import "base/timer/elapsed_timer.h" |
| #import "components/lens/lens_overlay_metrics.h" |
| #import "components/lens/proto/server/lens_overlay_response.pb.h" |
| #import "components/search_engines/template_url_service.h" |
| #import "ios/chrome/browser/default_browser/model/default_browser_interest_signals.h" |
| #import "ios/chrome/browser/lens_overlay/coordinator/lens_omnibox_client.h" |
| #import "ios/chrome/browser/lens_overlay/coordinator/lens_overlay_availability.h" |
| #import "ios/chrome/browser/lens_overlay/coordinator/lens_overlay_mediator_delegate.h" |
| #import "ios/chrome/browser/lens_overlay/model/lens_overlay_navigation_manager.h" |
| #import "ios/chrome/browser/lens_overlay/model/lens_overlay_navigation_mutator.h" |
| #import "ios/chrome/browser/lens_overlay/model/lens_overlay_url_utils.h" |
| #import "ios/chrome/browser/lens_overlay/public/lens_overlay_constants.h" |
| #import "ios/chrome/browser/lens_overlay/ui/lens_toolbar_consumer.h" |
| #import "ios/chrome/browser/omnibox/ui_bundled/omnibox_coordinator.h" |
| #import "ios/chrome/browser/orchestrator/ui_bundled/edit_view_animatee.h" |
| #import "ios/chrome/browser/search_engines/model/search_engine_observer_bridge.h" |
| #import "ios/chrome/browser/search_engines/model/search_engines_util.h" |
| #import "ios/chrome/browser/shared/public/commands/application_commands.h" |
| #import "ios/chrome/browser/shared/public/commands/lens_overlay_commands.h" |
| #import "ios/chrome/browser/shared/public/commands/open_new_tab_command.h" |
| #import "ios/chrome/browser/shared/public/features/features.h" |
| #import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h" |
| #import "ios/chrome/common/NSString+Chromium.h" |
| #import "ios/chrome/common/ui/colors/semantic_color_names.h" |
| #import "ios/public/provider/chrome/browser/lens/lens_overlay_result.h" |
| #import "ios/web/public/navigation/referrer.h" |
| #import "net/base/apple/url_conversions.h" |
| #import "url/gurl.h" |
| |
| @interface LensOverlayMediator () <LensOverlayNavigationMutator, |
| SearchEngineObserving> |
| |
| /// Current lens result. |
| @property(nonatomic, strong, readwrite) id<ChromeLensOverlayResult> |
| currentLensResult; |
| /// Number of tab opened by the lens overlay. |
| @property(nonatomic, assign, readwrite) NSInteger generatedTabCount; |
| |
| @end |
| |
| @implementation LensOverlayMediator { |
| /// Whether the browser is off the record. |
| BOOL _isIncognito; |
| /// Search engine observer. |
| std::unique_ptr<SearchEngineObserverBridge> _searchEngineObserver; |
| /// Orchestrates the navigation in the bottom sheet of the lens result page. |
| std::unique_ptr<LensOverlayNavigationManager> _navigationManager; |
| /// Time where lens started the search request. |
| base::ElapsedTimer _lensStartSearchRequestTime; |
| /// Whether the thumbnail/selection of the `currentLensResult` was removed. |
| BOOL _thumbnailRemoved; |
| } |
| |
| - (instancetype)initWithIsIncognito:(BOOL)isIncognito { |
| self = [super init]; |
| if (self) { |
| _isIncognito = isIncognito; |
| _navigationManager = std::make_unique<LensOverlayNavigationManager>(self); |
| } |
| return self; |
| } |
| |
| - (void)setTemplateURLService:(TemplateURLService*)templateURLService { |
| _templateURLService = templateURLService; |
| if (_templateURLService) { |
| _searchEngineObserver = |
| std::make_unique<SearchEngineObserverBridge>(self, _templateURLService); |
| [self searchEngineChanged]; |
| } else { |
| _searchEngineObserver.reset(); |
| } |
| } |
| |
| - (void)disconnect { |
| _searchEngineObserver.reset(); |
| _navigationManager.reset(); |
| _currentLensResult = nil; |
| } |
| |
| #pragma mark - SearchEngineObserving |
| |
| - (void)searchEngineChanged { |
| BOOL isLensAvailable = |
| search_engines::SupportsSearchImageWithLens(_templateURLService); |
| if (!isLensAvailable) { |
| [self.commandsHandler destroyLensUI:YES |
| reason:lens::LensOverlayDismissalSource:: |
| kDefaultSearchEngineChange]; |
| } |
| } |
| |
| #pragma mark - Omnibox |
| |
| #pragma mark LensOmniboxClientDelegate |
| |
| - (void)omniboxDidAcceptText:(const std::u16string&)text |
| destinationURL:(const GURL&)destinationURL |
| textClobbered:(BOOL)textClobbered { |
| [self defocusOmnibox]; |
| |
| const BOOL isUnimodalTextQuery = |
| _thumbnailRemoved || _currentLensResult.isTextSelection; |
| if (isUnimodalTextQuery) { |
| if (textClobbered) { |
| if (IsLensOverlaySameTabNavigationEnabled()) { |
| __weak LensOverlayMediator* weakSelf = self; |
| // Delay navigation until after omnibox defocus and toolbar button hide |
| // animations complete. This ensures a smooth transition and avoids |
| // interrupting the UI animations. |
| GURL URL = destinationURL; |
| CGFloat totalAnimationDuration = |
| kLensResultPageButtonAnimationDuration + |
| kLensResultPageToolbarLayoutDuration; |
| dispatch_after(dispatch_time(DISPATCH_TIME_NOW, |
| totalAnimationDuration * NSEC_PER_SEC), |
| dispatch_get_main_queue(), ^{ |
| [weakSelf.delegate |
| lensOverlayMediatorOpenURLInNewTabRequsted:URL]; |
| }); |
| } else { |
| [self.delegate |
| lensOverlayMediatorOpenURLInNewTabRequsted:destinationURL]; |
| } |
| |
| [self recordNewTabGeneratedBy:lens::LensOverlayNewTabSource::kOmnibox]; |
| if (_omniboxClient) { |
| [self updateOmniboxText:_omniboxClient->GetOmniboxSteadyStateText()]; |
| } |
| } else if (_navigationManager) { |
| // Hide the Lens selection as the omnibox content no longer reflect it. |
| [self.lensHandler hideUserSelection]; |
| _navigationManager->LoadUnimodalOmniboxNavigation(destinationURL, text); |
| } |
| } else { // Multimodal query. |
| // Setting the query text generates new results. |
| NSString* nsText = [NSString cr_fromString16:text]; |
| [self updateOmniboxText:nsText]; |
| [self.lensHandler setQueryText:nsText clearSelection:_thumbnailRemoved]; |
| } |
| } |
| |
| - (void)omniboxDidRemoveThumbnail { |
| _thumbnailRemoved = YES; |
| [self.lensHandler hideUserSelection]; |
| } |
| |
| #pragma mark LensToolbarMutator |
| |
| - (void)focusOmnibox { |
| RecordAction(base::UserMetricsAction("Mobile.LensOverlay.FocusOmnibox")); |
| [self.omniboxCoordinator focusOmnibox]; |
| [self.toolbarConsumer setOmniboxFocused:YES]; |
| [self.omniboxCoordinator.animatee setClearButtonFaded:NO]; |
| [self.presentationDelegate requestMaximizeBottomSheet]; |
| } |
| |
| - (void)defocusOmnibox { |
| [self.omniboxCoordinator endEditing]; |
| [self.toolbarConsumer setOmniboxFocused:NO]; |
| [self.omniboxCoordinator.animatee setClearButtonFaded:YES]; |
| } |
| |
| - (void)goBack { |
| RecordAction(base::UserMetricsAction("Mobile.LensOverlay.Back")); |
| if (_navigationManager) { |
| _navigationManager->GoBack(); |
| } |
| } |
| |
| #pragma mark OmniboxFocusDelegate |
| |
| - (void)omniboxDidBecomeFirstResponder { |
| [self focusOmnibox]; |
| } |
| |
| - (void)omniboxDidResignFirstResponder { |
| [self defocusOmnibox]; |
| } |
| |
| #pragma mark - ChromeLensOverlayDelegate |
| |
| // The lens overlay started searching for a result. |
| - (void)lensOverlayDidStartSearchRequest:(id<ChromeLensOverlay>)lensOverlay { |
| [self.resultConsumer handleSearchRequestStarted]; |
| _lensStartSearchRequestTime = base::ElapsedTimer(); |
| [self.toolbarConsumer setOmniboxEnabled:YES]; |
| } |
| |
| // The lens overlay search request produced an error. |
| - (void)lensOverlayDidReceiveError:(id<ChromeLensOverlay>)lensOverlay { |
| [self.resultConsumer handleSearchRequestErrored]; |
| [self.toolbarConsumer setOmniboxEnabled:YES]; |
| } |
| |
| // The lens overlay search request produced a valid result. |
| - (void)lensOverlay:(id<ChromeLensOverlay>)lensOverlay |
| didGenerateResult:(id<ChromeLensOverlayResult>)result { |
| RecordAction(base::UserMetricsAction("Mobile.LensOverlay.NewResult")); |
| lens::RecordLensResponseTime(_lensStartSearchRequestTime.Elapsed()); |
| if (_navigationManager) { |
| _navigationManager->LensOverlayDidGenerateResult(result); |
| } |
| [self.toolbarConsumer setOmniboxEnabled:YES]; |
| } |
| |
| - (void)lensOverlayDidTapOnCloseButton:(id<ChromeLensOverlay>)lensOverlay { |
| [self.commandsHandler |
| destroyLensUI:YES |
| reason:lens::LensOverlayDismissalSource::kOverlayCloseButton]; |
| } |
| |
| - (void)lensOverlay:(id<ChromeLensOverlay>)lensOverlay |
| suggestSignalsAvailableOnResult:(id<ChromeLensOverlayResult>)result { |
| [self lensOverlay:lensOverlay hasSuggestSignalsAvailableOnResult:result]; |
| } |
| |
| - (void)lensOverlay:(id<ChromeLensOverlay>)lensOverlay |
| hasSuggestSignalsAvailableOnResult:(id<ChromeLensOverlayResult>)result { |
| if (result != _currentLensResult) { |
| return; |
| } |
| [self setOmniboxSuggestSignals:result]; |
| } |
| |
| - (void)lensOverlay:(id<ChromeLensOverlay>)lensOverlay |
| didRequestToOpenURL:(GURL)URL { |
| [self.resultConsumer loadResultsURL:URL]; |
| } |
| |
| - (void)lensOverlayDidOpenOverlayMenu:(id<ChromeLensOverlay>)lensOverlay { |
| [self.delegate lensOverlayMediatorDidOpenOverlayMenu:self]; |
| } |
| |
| - (void)lensOverlayDidDeferGesture:(id<ChromeLensOverlay>)lensOverlay { |
| [self.resultConsumer handleSlowRequestHasStarted]; |
| UIImage* placeholder = ImageWithColor([UIColor colorNamed:kGrey200Color]); |
| [self.omniboxCoordinator setThumbnailImage:placeholder]; |
| [self.toolbarConsumer setOmniboxEnabled:NO]; |
| } |
| |
| #pragma mark - LensOverlayNavigationMutator |
| |
| - (void)loadLensResult:(id<ChromeLensOverlayResult>)result { |
| _currentLensResult = result; |
| _thumbnailRemoved = NO; |
| // Load the URL, it will start the result UI. |
| [self.resultConsumer loadResultsURL:result.searchResultURL]; |
| [self updateForLensResult:result]; |
| } |
| |
| - (void)reloadLensResult:(id<ChromeLensOverlayResult>)result { |
| // Pre update the UI. |
| [self updateForLensResult:result]; |
| // Reload the result. |
| [self.lensHandler reloadResult:result]; |
| } |
| |
| - (void)loadURL:(const GURL&)URL omniboxText:(NSString*)omniboxText { |
| // Restore the thumbnail when navigating back to an LRP. |
| if (!_currentLensResult.isTextSelection && _thumbnailRemoved && |
| !lens::IsLensOverlaySRP(URL)) { |
| _thumbnailRemoved = NO; |
| [self.omniboxCoordinator |
| setThumbnailImage:_currentLensResult.selectionPreviewImage]; |
| } |
| [self updateOmniboxText:omniboxText]; |
| [self.resultConsumer loadResultsURL:URL]; |
| } |
| |
| - (void)onBackNavigationAvailabilityMaybeChanged:(BOOL)canGoBack { |
| [self.toolbarConsumer setCanGoBack:canGoBack]; |
| } |
| |
| - (void)onSRPLoadWithOmniboxText:(NSString*)omniboxText { |
| if (![omniboxText isEqualToString:_currentLensResult.queryText]) { |
| if (!_currentLensResult.isTextSelection) { |
| [self.omniboxCoordinator setThumbnailImage:nil]; |
| _thumbnailRemoved = YES; |
| } |
| [self.lensHandler hideUserSelection]; |
| } |
| [self updateOmniboxText:omniboxText]; |
| } |
| |
| #pragma mark - LensResultPageMediatorDelegate |
| |
| - (void)lensResultPageWebStateDestroyed { |
| [self.commandsHandler |
| destroyLensUI:YES |
| reason:lens::LensOverlayDismissalSource::kTabClosed]; |
| } |
| |
| - (void)lensResultPageDidChangeActiveWebState:(web::WebState*)webState { |
| if (_navigationManager) { |
| _navigationManager->SetWebState(webState); |
| } |
| } |
| |
| - (void)lensResultPageMediator:(LensResultPageMediator*)mediator |
| didOpenNewTabFromSource:(lens::LensOverlayNewTabSource)newTabSource { |
| [self recordNewTabGeneratedBy:newTabSource]; |
| } |
| |
| - (void)lensResultPageOpenURLInNewTabRequsted:(GURL)URL { |
| [self.delegate lensOverlayMediatorOpenURLInNewTabRequsted:URL]; |
| } |
| |
| - (void)respondToTabWillChange { |
| [self.delegate respondToTabWillChange]; |
| } |
| |
| #pragma mark - Private |
| |
| /// Updates the UI for lens `result`. |
| - (void)updateForLensResult:(id<ChromeLensOverlayResult>)result { |
| [self.omniboxCoordinator |
| setThumbnailImage:result.isTextSelection ? nil |
| : result.selectionPreviewImage]; |
| if (self.omniboxClient) { |
| [self setOmniboxSuggestSignals:result]; |
| self.omniboxClient->SetLensResultHasThumbnail(!result.isTextSelection); |
| } |
| [self updateOmniboxText:result.queryText]; |
| } |
| |
| /// Updates the steady state omnibox text. |
| - (void)updateOmniboxText:(NSString*)text { |
| if (self.omniboxClient) { |
| self.omniboxClient->SetOmniboxSteadyStateText(text); |
| } |
| [self.omniboxCoordinator updateOmniboxState]; |
| } |
| |
| /// Sets the omnibox suggest signals with `result`. |
| - (void)setOmniboxSuggestSignals:(id<ChromeLensOverlayResult>)result { |
| if (!self.omniboxClient) { |
| return; |
| } |
| |
| NSData* data = result.suggestSignals; |
| if (!data.length) { |
| self.omniboxClient->SetLensOverlaySuggestInputs(std::nullopt); |
| return; |
| } |
| std::string encodedString; |
| base::span<const uint8_t> signals = base::span<const uint8_t>( |
| static_cast<const uint8_t*>(result.suggestSignals.bytes), |
| result.suggestSignals.length); |
| |
| Base64UrlEncode(signals, base::Base64UrlEncodePolicy::INCLUDE_PADDING, |
| &encodedString); |
| |
| if (!encodedString.empty()) { |
| lens::proto::LensOverlaySuggestInputs response; |
| response.set_encoded_image_signals(encodedString); |
| self.omniboxClient->SetLensOverlaySuggestInputs(response); |
| } |
| } |
| |
| /// Records lens overlay opening a new tab. |
| - (void)recordNewTabGeneratedBy:(lens::LensOverlayNewTabSource)newTabSource { |
| self.generatedTabCount += 1; |
| lens::RecordNewTabGenerated(newTabSource); |
| } |
| |
| @end |