// 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_mediator.h"
#import "base/apple/foundation_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/autocomplete_match.h"
#import "components/open_from_clipboard/clipboard_recent_content.h"
#import "ios/chrome/browser/default_browser/model/default_browser_interest_signals.h"
#import "ios/chrome/browser/favicon/model/favicon_loader.h"
#import "ios/chrome/browser/net/model/crurl.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/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/search_image_with_lens_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/browser/ui/lens/lens_entrypoint.h"
#import "ios/chrome/browser/ui/omnibox/omnibox_constants.h"
#import "ios/chrome/browser/ui/omnibox/omnibox_consumer.h"
#import "ios/chrome/browser/ui/omnibox/omnibox_suggestion_icon_util.h"
#import "ios/chrome/browser/ui/omnibox/omnibox_ui_features.h"
#import "ios/chrome/browser/ui/omnibox/omnibox_util.h"
#import "ios/chrome/browser/ui/omnibox/popup/autocomplete_suggestion.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/chrome/common/ui/favicon/favicon_attributes.h"
#import "ios/chrome/common/ui/favicon/favicon_constants.h"
#import "ios/public/provider/chrome/browser/branded_images/branded_images_api.h"
#import "ios/public/provider/chrome/browser/lens/lens_api.h"
#import "ios/web/public/navigation/navigation_manager.h"
using base::UserMetricsAction;
@interface OmniboxMediator () <SearchEngineObserving>
// Is Browser incognito.
@property(nonatomic, assign, readonly) BOOL isIncognito;
// FET reference.
@property(nonatomic, assign) feature_engagement::Tracker* tracker;
// Whether the current default search engine supports search-by-image.
@property(nonatomic, assign) BOOL searchEngineSupportsSearchByImage;
// Whether the current default search engine supports Lens.
@property(nonatomic, assign) BOOL searchEngineSupportsLens;
// The latest URL used to fetch the favicon.
@property(nonatomic, assign) GURL latestFaviconURL;
// The latest URL used to fetch the default search engine favicon.
@property(nonatomic, assign) const TemplateURL* latestDefaultSearchEngine;
// The favicon for the current default search engine. Cached to prevent
// needing to load it each time.
@property(nonatomic, strong) UIImage* currentDefaultSearchEngineFavicon;
@end
@implementation OmniboxMediator {
std::unique_ptr<SearchEngineObserverBridge> _searchEngineObserver;
}
- (instancetype)initWithIncognito:(BOOL)isIncognito
tracker:(feature_engagement::Tracker*)tracker {
self = [super init];
if (self) {
_searchEngineSupportsSearchByImage = NO;
_searchEngineSupportsLens = NO;
_isIncognito = isIncognito;
_tracker = tracker;
}
return self;
}
#pragma mark - Setters
- (void)setConsumer:(id<OmniboxConsumer>)consumer {
_consumer = consumer;
[self updateConsumerEmptyTextImage];
}
- (void)setTemplateURLService:(TemplateURLService*)templateURLService {
_templateURLService = templateURLService;
self.searchEngineSupportsSearchByImage =
search_engines::SupportsSearchByImage(templateURLService);
self.searchEngineSupportsLens =
search_engines::SupportsSearchImageWithLens(templateURLService);
if (_templateURLService) {
_searchEngineObserver =
std::make_unique<SearchEngineObserverBridge>(self, templateURLService);
} else {
_searchEngineObserver.reset();
}
}
- (void)setSearchEngineSupportsSearchByImage:
(BOOL)searchEngineSupportsSearchByImage {
BOOL supportChanged = self.searchEngineSupportsSearchByImage !=
searchEngineSupportsSearchByImage;
_searchEngineSupportsSearchByImage = searchEngineSupportsSearchByImage;
if (supportChanged) {
[self.consumer
updateSearchByImageSupported:searchEngineSupportsSearchByImage];
}
}
- (void)setSearchEngineSupportsLens:(BOOL)searchEngineSupportsLens {
BOOL supportChanged =
self.searchEngineSupportsLens != searchEngineSupportsLens;
_searchEngineSupportsLens = searchEngineSupportsLens;
if (supportChanged) {
[self.consumer updateLensImageSupported:searchEngineSupportsLens];
}
}
#pragma mark - SearchEngineObserving
- (void)searchEngineChanged {
TemplateURLService* templateUrlService = self.templateURLService;
self.searchEngineSupportsSearchByImage =
search_engines::SupportsSearchByImage(templateUrlService);
self.searchEngineSupportsLens =
search_engines::SupportsSearchImageWithLens(templateUrlService);
self.currentDefaultSearchEngineFavicon = nil;
[self updateConsumerEmptyTextImage];
}
#pragma mark - PopupMatchPreviewDelegate
- (void)setPreviewSuggestion:(id<AutocompleteSuggestion>)suggestion
isFirstUpdate:(BOOL)isFirstUpdate {
// On first update, don't set the preview text, as omnibox will automatically
// receive the suggestion as inline autocomplete through OmniboxViewIOS.
if (!isFirstUpdate) {
// Remove additional text when previewing suggestions.
if (IsRichAutocompletionEnabled()) {
[self.consumer updateAdditionalText:nil];
}
[self.consumer updateText:suggestion.omniboxPreviewText];
}
// When no suggestion is previewed, just show the default image.
if (!suggestion) {
[self setDefaultLeftImage];
return;
}
// Set the suggestion image, or load it if necessary.
[self.consumer updateAutocompleteIcon:suggestion.matchTypeIcon
withAccessibilityIdentifier:
kOmniboxLeadingImageSuggestionImageAccessibilityIdentifier];
__weak OmniboxMediator* weakSelf = self;
if ([suggestion isMatchTypeSearch]) {
// Show Default Search Engine favicon.
[self loadDefaultSearchEngineFaviconWithCompletion:^(UIImage* image) {
[weakSelf.consumer updateAutocompleteIcon:image
withAccessibilityIdentifier:
kOmniboxLeadingImageDefaultAccessibilityIdentifier];
}];
} else if (suggestion.destinationUrl.gurl.is_valid()) {
// Show url favicon when it's valid.
[self loadFaviconByPageURL:suggestion.destinationUrl.gurl
completion:^(UIImage* image) {
NSString* webPageUrl = base::SysUTF8ToNSString(
suggestion.destinationUrl.gurl.spec());
[weakSelf.consumer updateAutocompleteIcon:image
withAccessibilityIdentifier:webPageUrl];
}];
} else if (isFirstUpdate) {
// When no suggestion is highlighted (aka. isFirstUpdate) show the default
// browser icon.
[self setDefaultLeftImage];
} else {
// When a suggestion is highlighted, show the same icon as in the popup.
[self.consumer
updateAutocompleteIcon:suggestion.matchTypeIcon
withAccessibilityIdentifier:suggestion
.matchTypeIconAccessibilityIdentifier];
}
}
- (void)setDefaultLeftImage {
UIImage* image = GetOmniboxSuggestionIconForAutocompleteMatchType(
AutocompleteMatchType::SEARCH_WHAT_YOU_TYPED);
[self.consumer updateAutocompleteIcon:image
withAccessibilityIdentifier:
kOmniboxLeadingImageDefaultAccessibilityIdentifier];
__weak OmniboxMediator* weakSelf = self;
// Show Default Search Engine favicon.
[self loadDefaultSearchEngineFaviconWithCompletion:^(UIImage* icon) {
[weakSelf.consumer updateAutocompleteIcon:icon
withAccessibilityIdentifier:
kOmniboxLeadingImageDefaultAccessibilityIdentifier];
}];
}
// Loads a favicon for a given page URL.
// `pageURL` is url for the page that needs a favicon
// `completion` handler might be called multiple
// times, synchronously and asynchronously. It will always be called on the main
// thread.
- (void)loadFaviconByPageURL:(GURL)pageURL
completion:(void (^)(UIImage* image))completion {
// Can't load favicons without a favicon loader.
DCHECK(self.faviconLoader);
DCHECK(pageURL.is_valid());
// Remember which favicon is loaded in case we start loading a new one
// before this one completes.
self.latestFaviconURL = pageURL;
__weak __typeof(self) weakSelf = self;
auto handleFaviconResult = ^void(FaviconAttributes* faviconCacheResult) {
if (weakSelf.latestFaviconURL != pageURL ||
!faviconCacheResult.faviconImage ||
faviconCacheResult.usesDefaultImage) {
return;
}
if (completion) {
completion(faviconCacheResult.faviconImage);
}
};
// Download the favicon.
// The code below mimics that in OmniboxPopupMediator.
self.faviconLoader->FaviconForPageUrl(
pageURL, kMinFaviconSizePt, kMinFaviconSizePt,
/*fallback_to_google_server=*/false, handleFaviconResult);
}
// Loads a favicon for the current default search engine.
// `completion` handler might be called multiple times, synchronously
// and asynchronously. It will always be called on the main
// thread.
- (void)loadDefaultSearchEngineFaviconWithCompletion:
(void (^)(UIImage* image))completion {
// If default search engine image is currently loaded, just use it.
if (self.currentDefaultSearchEngineFavicon) {
if (completion) {
completion(self.currentDefaultSearchEngineFavicon);
}
}
const TemplateURL* defaultProvider =
self.templateURLService
? self.templateURLService->GetDefaultSearchProvider()
: nullptr;
if (!defaultProvider) {
// Service isn't available or default provider is disabled - either way we
// can't get the icon.
return;
}
// When the DSE is Google, use the bundled icon.
if (defaultProvider && defaultProvider->GetEngineType(
self.templateURLService->search_terms_data()) ==
SEARCH_ENGINE_GOOGLE) {
UIImage* bundledLogo = ios::provider::GetBrandedImage(
ios::provider::BrandedImage::kOmniboxAnswer);
if (bundledLogo) {
self.currentDefaultSearchEngineFavicon = bundledLogo;
if (completion) {
completion(bundledLogo);
}
return;
}
}
// Can't load favicons without a favicon loader.
DCHECK(self.faviconLoader);
__weak __typeof(self) weakSelf = self;
self.latestDefaultSearchEngine = defaultProvider;
auto handleFaviconResult = ^void(FaviconAttributes* faviconCacheResult) {
DCHECK_LE(faviconCacheResult.faviconImage.size.width, kMinFaviconSizePt);
if (weakSelf.latestDefaultSearchEngine != defaultProvider ||
!faviconCacheResult.faviconImage ||
faviconCacheResult.usesDefaultImage) {
return;
}
UIImage* favicon = faviconCacheResult.faviconImage;
weakSelf.currentDefaultSearchEngineFavicon = favicon;
if (completion) {
completion(favicon);
}
};
// Prepopulated search engines don't have a favicon URL, so the favicon is
// loaded with an empty query search page URL.
if (defaultProvider->prepopulate_id() != 0) {
// Fake up a page URL for favicons of prepopulated search engines, since
// favicons may be fetched from Google server which doesn't suppoprt
// icon URL.
std::string emptyPageUrl = defaultProvider->url_ref().ReplaceSearchTerms(
TemplateURLRef::SearchTermsArgs(std::u16string()),
_templateURLService->search_terms_data());
self.faviconLoader->FaviconForPageUrl(
GURL(emptyPageUrl), kMinFaviconSizePt, kMinFaviconSizePt,
/*fallback_to_google_server=*/YES, handleFaviconResult);
} else {
// Download the favicon.
// The code below mimics that in OmniboxPopupMediator.
self.faviconLoader->FaviconForIconUrl(defaultProvider->favicon_url(),
kMinFaviconSizePt, kMinFaviconSizePt,
handleFaviconResult);
}
}
- (void)updateConsumerEmptyTextImage {
[_consumer
updateSearchByImageSupported:self.searchEngineSupportsSearchByImage];
[_consumer updateLensImageSupported:self.searchEngineSupportsLens];
// Show Default Search Engine favicon.
// Remember what is the Default Search Engine provider that the icon is
// for, in case the user changes Default Search Engine while this is being
// loaded.
__weak __typeof(self) weakSelf = self;
[self loadDefaultSearchEngineFaviconWithCompletion:^(UIImage* image) {
[weakSelf.consumer setEmptyTextLeadingImage:image];
}];
}
#pragma mark - OmniboxViewControllerPasteDelegate
- (void)didTapPasteToSearchButton:(NSArray<NSItemProvider*>*)itemProviders {
__weak __typeof(self) weakSelf = self;
auto textCompletion =
^(__kindof id<NSItemProviderReading> providedItem, NSError* error) {
dispatch_async(dispatch_get_main_queue(), ^{
NSString* text = static_cast<NSString*>(providedItem);
if (text) {
[weakSelf.loadQueryCommandsHandler loadQuery:text immediately:YES];
[weakSelf.omniboxCommandsHandler cancelOmniboxEdit];
}
});
};
auto imageSearchCompletion =
^(__kindof id<NSItemProviderReading> providedItem, NSError* error) {
dispatch_async(dispatch_get_main_queue(), ^{
UIImage* image = static_cast<UIImage*>(providedItem);
if (image) {
[weakSelf loadImageQuery:image];
[weakSelf.omniboxCommandsHandler cancelOmniboxEdit];
}
});
};
auto lensCompletion =
^(__kindof id<NSItemProviderReading> providedItem, NSError* error) {
dispatch_async(dispatch_get_main_queue(), ^{
UIImage* image = base::apple::ObjCCast<UIImage>(providedItem);
if (image) {
[weakSelf lensImage:image];
}
});
};
for (NSItemProvider* itemProvider in itemProviders) {
if ([itemProvider canLoadObjectOfClass:[UIImage class]]) {
// Either provide a Lens option or a reverse-image-search option.
if ([self shouldUseLens]) {
RecordAction(
UserMetricsAction("Mobile.OmniboxPasteButton.LensCopiedImage"));
[itemProvider loadObjectOfClass:[UIImage class]
completionHandler:lensCompletion];
break;
} else if (self.searchEngineSupportsSearchByImage) {
RecordAction(
UserMetricsAction("Mobile.OmniboxPasteButton.SearchCopiedImage"));
[itemProvider loadObjectOfClass:[UIImage class]
completionHandler:imageSearchCompletion];
break;
}
} else if ([itemProvider canLoadObjectOfClass:[NSURL class]]) {
RecordAction(
UserMetricsAction("Mobile.OmniboxPasteButton.SearchCopiedLink"));
default_browser::NotifyOmniboxURLCopyPasteAndNavigate(
self.isIncognito, self.tracker, self.sceneState);
// Load URL as a NSString to avoid further conversion.
[itemProvider loadObjectOfClass:[NSString class]
completionHandler:textCompletion];
break;
} else if ([itemProvider canLoadObjectOfClass:[NSString class]]) {
RecordAction(
UserMetricsAction("Mobile.OmniboxPasteButton.SearchCopiedText"));
default_browser::NotifyOmniboxTextCopyPasteAndNavigate(self.tracker);
[itemProvider loadObjectOfClass:[NSString class]
completionHandler:textCompletion];
break;
}
}
}
- (void)didTapVisitCopiedLink {
default_browser::NotifyOmniboxURLCopyPasteAndNavigate(
self.isIncognito, self.tracker, self.sceneState);
__weak __typeof(self) weakSelf = self;
ClipboardRecentContent::GetInstance()->GetRecentURLFromClipboard(
base::BindOnce(^(std::optional<GURL> optionalURL) {
if (!optionalURL) {
return;
}
NSString* url = base::SysUTF8ToNSString(optionalURL.value().spec());
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf.loadQueryCommandsHandler loadQuery:url immediately:YES];
[weakSelf.omniboxCommandsHandler cancelOmniboxEdit];
});
}));
}
- (void)didTapSearchCopiedText {
default_browser::NotifyOmniboxTextCopyPasteAndNavigate(self.tracker);
__weak __typeof(self) weakSelf = self;
ClipboardRecentContent::GetInstance()->GetRecentTextFromClipboard(
base::BindOnce(^(std::optional<std::u16string> optionalText) {
if (!optionalText) {
return;
}
NSString* query = base::SysUTF16ToNSString(optionalText.value());
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf.loadQueryCommandsHandler loadQuery:query immediately:YES];
[weakSelf.omniboxCommandsHandler cancelOmniboxEdit];
});
}));
}
- (void)didTapSearchCopiedImage {
__weak __typeof(self) weakSelf = self;
ClipboardRecentContent::GetInstance()->GetRecentImageFromClipboard(
base::BindOnce(^(std::optional<gfx::Image> optionalImage) {
if (!optionalImage) {
return;
}
UIImage* image = optionalImage.value().ToUIImage();
[weakSelf loadImageQuery:image];
[weakSelf.omniboxCommandsHandler cancelOmniboxEdit];
}));
}
- (void)didTapLensCopiedImage {
__weak __typeof(self) weakSelf = self;
ClipboardRecentContent::GetInstance()->GetRecentImageFromClipboard(
base::BindOnce(^(std::optional<gfx::Image> optionalImage) {
if (!optionalImage) {
return;
}
UIImage* image = optionalImage.value().ToUIImage();
[weakSelf lensImage:image];
}));
}
#pragma mark - Private methods
// Loads an image-search query with `image`.
- (void)loadImageQuery:(UIImage*)image {
DCHECK(image);
web::NavigationManager::WebLoadParams webParams =
ImageSearchParamGenerator::LoadParamsForImage(image,
self.templateURLService);
UrlLoadParams params = UrlLoadParams::InCurrentTab(webParams);
self.URLLoadingBrowserAgent->Load(params);
}
// Performs a Lens search on the given `image`.
- (void)lensImage:(UIImage*)image {
DCHECK(image);
SearchImageWithLensCommand* command = [[SearchImageWithLensCommand alloc]
initWithImage:image
entryPoint:LensEntrypoint::OmniboxPostCapture];
[self.lensCommandsHandler searchImageWithLens:command];
[self.omniboxCommandsHandler cancelOmniboxEdit];
}
// Returns whether or not to use Lens for copied images.
- (BOOL)shouldUseLens {
return ios::provider::IsLensSupported() &&
base::FeatureList::IsEnabled(kEnableLensInOmniboxCopiedImage) &&
self.searchEngineSupportsLens;
}
@end