blob: 8dc5025aec7df25e30042dcff1323900f93c4b52 [file] [log] [blame]
// Copyright 2025 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/omnibox/model/placeholder_service/placeholder_service.h"
#import "base/functional/callback_helpers.h"
#import "base/strings/sys_string_conversions.h"
#import "base/time/time.h"
#import "components/omnibox/common/omnibox_features.h"
#import "components/search_engines/template_url.h"
#import "components/search_engines/template_url_service.h"
#import "components/strings/grit/components_strings.h"
#import "ios/chrome/browser/favicon/model/favicon_loader.h"
#import "ios/chrome/browser/shared/ui/symbols/symbols.h"
#import "ios/chrome/common/ui/favicon/favicon_attributes.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ui/base/l10n/l10n_util.h"
#import "ui/base/l10n/l10n_util_mac.h"
namespace {
// Cooldown period before re-fetching a failed icon.
const base::TimeDelta kFetchCooldown = base::Seconds(2);
} // namespace
PlaceholderService::PlaceholderService(FaviconLoader* favicon_loader,
TemplateURLService* template_url_service)
: favicon_loader_(favicon_loader),
template_url_service_(template_url_service),
current_dse_(nullptr),
icon_callbacks_() {
template_url_service->AddObserver(this);
icon_cache_ = [[NSCache alloc] init];
}
PlaceholderService::~PlaceholderService() {
for (auto& observer : model_observers_) {
observer.OnPlaceholderServiceShuttingDown();
}
template_url_service_->RemoveObserver(this);
icon_cache_ = nil;
}
void PlaceholderService::FetchDefaultSearchEngineIcon(
CGFloat icon_point_size,
PlaceholderImageCallback callback) {
// Return the cached image if there is one.
UIImage* cached_icon = [icon_cache_ objectForKey:@(icon_point_size)];
if (cached_icon) {
if (callback) {
callback.Run(cached_icon);
}
return;
}
// Return the placeholder icon if there is no default search provider.
UIImage* placeholder_icon =
DefaultSymbolWithPointSize(kSearchSymbol, icon_point_size);
const TemplateURL* default_provider =
template_url_service_ ? template_url_service_->GetDefaultSearchProvider()
: nullptr;
if (!default_provider) {
if (callback) {
callback.Run(placeholder_icon);
}
return;
}
// Return the bundled icon if there is one. Also cache it.
UIImage* bundled_icon =
GetBundledIconForTemplateURL(default_provider, icon_point_size);
if (bundled_icon) {
[icon_cache_ setObject:bundled_icon forKey:@(icon_point_size)];
if (callback) {
callback.Run(bundled_icon);
}
return;
}
// Set a placeholder before fetching the icon.
if (callback) {
callback.Run(placeholder_icon);
}
// The original `callback` is moved and will be invoked when the fetch
// completes.
icon_callbacks_[icon_point_size].push_back(std::move(callback));
PerformIconFetch(default_provider, icon_point_size);
}
UIImage* PlaceholderService::GetDefaultSearchEngineIcon(
CGFloat icon_point_size) {
// Return the cached image if there is one.
UIImage* cached_icon = [icon_cache_ objectForKey:@(icon_point_size)];
if (cached_icon) {
return cached_icon;
}
// Return the placeholder icon if there is no default search provider.
UIImage* placeholder_icon =
DefaultSymbolWithPointSize(kSearchSymbol, icon_point_size);
const TemplateURL* default_provider =
template_url_service_ ? template_url_service_->GetDefaultSearchProvider()
: nullptr;
if (!default_provider) {
return placeholder_icon;
}
// Fetch the icon after return.
base::ScopedClosureRunner run_after_return =
base::ScopedClosureRunner(base::BindOnce(
&PlaceholderService::FetchDefaultSearchEngineIcon,
base::Unretained(this), icon_point_size, base::DoNothing()));
return placeholder_icon;
}
NSString* PlaceholderService::GetCurrentPlaceholderText() {
if (!base::FeatureList::IsEnabled(omnibox::kOmniboxMobileParityUpdate)) {
return l10n_util::GetNSString(IDS_OMNIBOX_EMPTY_HINT);
}
CHECK(template_url_service_);
std::u16string provider_name = u"";
if (const TemplateURL* search_provider =
template_url_service_->GetDefaultSearchProvider()) {
provider_name = search_provider->short_name();
}
return l10n_util::GetNSStringF(IDS_OMNIBOX_EMPTY_HINT_WITH_DSE_NAME,
provider_name);
}
NSString* PlaceholderService::GetCurrentSearchOnlyPlaceholderText() {
CHECK(template_url_service_);
std::u16string provider_name = u"";
if (const TemplateURL* search_provider =
template_url_service_->GetDefaultSearchProvider()) {
provider_name = search_provider->short_name();
}
return l10n_util::GetNSStringF(IDS_IOS_OMNIBOX_PLACEHOLDER_SEARCH_ONLY,
provider_name);
}
base::WeakPtr<PlaceholderService> PlaceholderService::AsWeakPtr() {
return weak_ptr_factory_.GetWeakPtr();
}
void PlaceholderService::AddObserver(PlaceholderServiceObserver* observer) {
model_observers_.AddObserver(observer);
}
void PlaceholderService::RemoveObserver(PlaceholderServiceObserver* observer) {
model_observers_.RemoveObserver(observer);
}
#pragma mark - TemplateURLServiceObserver
void PlaceholderService::OnTemplateURLServiceChanged() {
if (!template_url_service_ ||
template_url_service_->GetDefaultSearchProvider() == current_dse_) {
return;
}
current_dse_ = template_url_service_->GetDefaultSearchProvider();
[icon_cache_ removeAllObjects];
icon_callbacks_.clear();
fetch_cooldowns_.clear();
for (auto& observer : model_observers_) {
observer.OnPlaceholderTextChanged();
observer.OnPlaceholderImageChanged();
}
}
#pragma mark - Private
UIImage* PlaceholderService::GetBundledIconForTemplateURL(
const TemplateURL* template_url,
CGFloat icon_point_size) {
CHECK(template_url);
CHECK(template_url_service_);
// Google bundled icon.
if (template_url->GetEngineType(template_url_service_->search_terms_data()) ==
SEARCH_ENGINE_GOOGLE) {
#if BUILDFLAG(IOS_USE_BRANDED_SYMBOLS)
return MakeSymbolMulticolor(
CustomSymbolWithPointSize(kGoogleIconSymbol, icon_point_size));
#endif
}
return nil;
}
void PlaceholderService::OnIconReceivedForTemplateURL(
TemplateURLID template_url_id,
CGFloat icon_point_size,
UIImage* icon) {
if (icon && template_url_service_ &&
template_url_service_->GetDefaultSearchProvider()->id() ==
template_url_id &&
![icon_cache_ objectForKey:@(icon_point_size)]) {
[icon_cache_ setObject:icon forKey:@(icon_point_size)];
for (auto& observer : model_observers_) {
observer.OnPlaceholderImageChanged();
}
if (icon_callbacks_.contains(icon_point_size)) {
std::vector<PlaceholderImageCallback> callbacks =
std::move(icon_callbacks_[icon_point_size]);
icon_callbacks_.erase(icon_point_size);
for (PlaceholderImageCallback& callback : callbacks) {
if (callback) {
std::move(callback).Run(icon);
}
}
}
}
}
void PlaceholderService::PerformIconFetch(const TemplateURL* template_url,
CGFloat icon_point_size) {
if (!favicon_loader_) {
return;
}
// Do not fetch if the cooldown is still active.
auto it = fetch_cooldowns_.find(icon_point_size);
if (it != fetch_cooldowns_.end() && base::TimeTicks::Now() < it->second) {
return;
}
// Set a cooldown to prevent rapid refetching.
fetch_cooldowns_[icon_point_size] = base::TimeTicks::Now() + kFetchCooldown;
auto favicon_completion = base::CallbackToBlock(base::BindRepeating(
[](base::WeakPtr<PlaceholderService> weak_self,
TemplateURLID template_url_id, CGFloat icon_point_size,
FaviconAttributes* favicon_result) {
if (!favicon_result.faviconImage || favicon_result.usesDefaultImage) {
return;
}
UIImage* favicon = favicon_result.faviconImage;
if (weak_self) {
weak_self->OnIconReceivedForTemplateURL(template_url_id,
icon_point_size, favicon);
}
},
AsWeakPtr(), template_url->id(), icon_point_size));
// Prepopulated search engines don't have a favicon URL, so the favicon is
// loaded with an empty query search page URL.
if (template_url->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 empty_page_url = template_url->url_ref().ReplaceSearchTerms(
TemplateURLRef::SearchTermsArgs(std::u16string()),
template_url_service_->search_terms_data());
favicon_loader_->FaviconForPageUrlOrHost(
GURL(empty_page_url), icon_point_size, favicon_completion);
} else {
// Download the favicon.
favicon_loader_->FaviconForIconUrl(template_url->favicon_url(),
icon_point_size, icon_point_size,
favicon_completion);
}
}