// Copyright 2013 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/chrome_omnibox_client_ios.h"
#import "base/feature_list.h"
#import "base/metrics/user_metrics.h"
#import "base/strings/string_util.h"
#import "base/strings/sys_string_conversions.h"
#import "base/strings/utf_string_conversions.h"
#import "base/task/thread_pool.h"
#import "components/favicon/ios/web_favicon_driver.h"
#import "components/feature_engagement/public/event_constants.h"
#import "components/feature_engagement/public/tracker.h"
#import "components/omnibox/browser/autocomplete_match.h"
#import "components/omnibox/browser/autocomplete_result.h"
#import "components/omnibox/browser/location_bar_model.h"
#import "components/omnibox/browser/omnibox_log.h"
#import "components/omnibox/browser/shortcuts_backend.h"
#import "components/omnibox/common/omnibox_features.h"
#import "components/search_engines/template_url_service.h"
#import "ios/chrome/browser/autocomplete/model/autocomplete_classifier_factory.h"
#import "ios/chrome/browser/autocomplete/model/autocomplete_provider_client_impl.h"
#import "ios/chrome/browser/autocomplete/model/shortcuts_backend_factory.h"
#import "ios/chrome/browser/bookmarks/model/bookmark_model_factory.h"
#import "ios/chrome/browser/bookmarks/model/bookmarks_utils.h"
#import "ios/chrome/browser/default_browser/model/default_browser_interest_signals.h"
#import "ios/chrome/browser/https_upgrades/model/https_upgrade_service_factory.h"
#import "ios/chrome/browser/intents/intents_donation_helper.h"
#import "ios/chrome/browser/prerender/model/prerender_service.h"
#import "ios/chrome/browser/prerender/model/prerender_service_factory.h"
#import "ios/chrome/browser/search_engines/model/template_url_service_factory.h"
#import "ios/chrome/browser/sessions/model/ios_chrome_session_tab_helper.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/shared/model/url/chrome_url_constants.h"
#import "ios/chrome/browser/ui/omnibox/omnibox_ui_features.h"
#import "ios/chrome/browser/ui/omnibox/web_location_bar.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/web/public/navigation/navigation_context.h"
#import "ios/web/public/navigation/navigation_manager.h"
#import "ios/web/public/web_state.h"
#import "ui/base/l10n/l10n_util.h"
#import "url/gurl.h"
ChromeOmniboxClientIOS::ChromeOmniboxClientIOS(
WebLocationBar* location_bar,
ChromeBrowserState* browser_state,
feature_engagement::Tracker* tracker)
: location_bar_(location_bar),
browser_state_(browser_state),
engagement_tracker_(tracker),
web_state_tracker_() {
CHECK(engagement_tracker_);
}
ChromeOmniboxClientIOS::~ChromeOmniboxClientIOS() {
web_state_tracker_.clear();
}
std::unique_ptr<AutocompleteProviderClient>
ChromeOmniboxClientIOS::CreateAutocompleteProviderClient() {
return std::make_unique<AutocompleteProviderClientImpl>(browser_state_);
}
bool ChromeOmniboxClientIOS::CurrentPageExists() const {
return (location_bar_->GetWebState() != nullptr);
}
const GURL& ChromeOmniboxClientIOS::GetURL() const {
return CurrentPageExists() ? location_bar_->GetWebState()->GetVisibleURL()
: GURL::EmptyGURL();
}
bool ChromeOmniboxClientIOS::IsLoading() const {
return location_bar_->GetWebState()->IsLoading();
}
bool ChromeOmniboxClientIOS::IsPasteAndGoEnabled() const {
return false;
}
bool ChromeOmniboxClientIOS::IsDefaultSearchProviderEnabled() const {
// iOS does not have Enterprise policies
return true;
}
SessionID ChromeOmniboxClientIOS::GetSessionID() const {
return IOSChromeSessionTabHelper::FromWebState(location_bar_->GetWebState())
->session_id();
}
PrefService* ChromeOmniboxClientIOS::GetPrefs() {
return browser_state_->GetPrefs();
}
bookmarks::BookmarkModel* ChromeOmniboxClientIOS::GetBookmarkModel() {
return ios::BookmarkModelFactory::GetForBrowserState(browser_state_);
}
AutocompleteControllerEmitter*
ChromeOmniboxClientIOS::GetAutocompleteControllerEmitter() {
return nullptr;
}
TemplateURLService* ChromeOmniboxClientIOS::GetTemplateURLService() {
return ios::TemplateURLServiceFactory::GetForBrowserState(browser_state_);
}
const AutocompleteSchemeClassifier&
ChromeOmniboxClientIOS::GetSchemeClassifier() const {
return scheme_classifier_;
}
AutocompleteClassifier* ChromeOmniboxClientIOS::GetAutocompleteClassifier() {
return ios::AutocompleteClassifierFactory::GetForBrowserState(browser_state_);
}
bool ChromeOmniboxClientIOS::ShouldDefaultTypedNavigationsToHttps() const {
return base::FeatureList::IsEnabled(omnibox::kDefaultTypedNavigationsToHttps);
}
int ChromeOmniboxClientIOS::GetHttpsPortForTesting() const {
return HttpsUpgradeServiceFactory::GetForBrowserState(browser_state_)
->GetHttpsPortForTesting();
}
bool ChromeOmniboxClientIOS::IsUsingFakeHttpsForHttpsUpgradeTesting() const {
return HttpsUpgradeServiceFactory::GetForBrowserState(browser_state_)
->IsUsingFakeHttpsForTesting();
}
gfx::Image ChromeOmniboxClientIOS::GetIconIfExtensionMatch(
const AutocompleteMatch& match) const {
// Extensions are not supported on iOS.
return gfx::Image();
}
std::u16string ChromeOmniboxClientIOS::GetFormattedFullURL() const {
return location_bar_->GetLocationBarModel()->GetFormattedFullURL();
}
std::u16string ChromeOmniboxClientIOS::GetURLForDisplay() const {
return location_bar_->GetLocationBarModel()->GetURLForDisplay();
}
GURL ChromeOmniboxClientIOS::GetNavigationEntryURL() const {
return location_bar_->GetLocationBarModel()->GetURL();
}
metrics::OmniboxEventProto::PageClassification
ChromeOmniboxClientIOS::GetPageClassification(bool is_prefetch) {
return location_bar_->GetLocationBarModel()->GetPageClassification(
is_prefetch);
}
security_state::SecurityLevel ChromeOmniboxClientIOS::GetSecurityLevel() const {
return location_bar_->GetLocationBarModel()->GetSecurityLevel();
}
net::CertStatus ChromeOmniboxClientIOS::GetCertStatus() const {
return location_bar_->GetLocationBarModel()->GetCertStatus();
}
const gfx::VectorIcon& ChromeOmniboxClientIOS::GetVectorIcon() const {
return location_bar_->GetLocationBarModel()->GetVectorIcon();
}
bool ChromeOmniboxClientIOS::ProcessExtensionKeyword(
const std::u16string& text,
const TemplateURL* template_url,
const AutocompleteMatch& match,
WindowOpenDisposition disposition) {
// Extensions are not supported on iOS.
return false;
}
void ChromeOmniboxClientIOS::OnFocusChanged(OmniboxFocusState state,
OmniboxFocusChangeReason reason) {
// TODO(crbug.com/40534385): OnFocusChanged is not the correct place to be
// canceling prerenders, but this is the closest match to the original
// location of this code, which was in OmniboxViewIOS::OnDidEndEditing(). The
// goal of this code is to cancel prerenders when the omnibox loses focus.
// Otherwise, they will live forever in cases where the user navigates to a
// different URL than what is prerendered.
if (state == OMNIBOX_FOCUS_NONE) {
PrerenderService* service =
PrerenderServiceFactory::GetForBrowserState(browser_state_);
if (service) {
service->CancelPrerender();
}
}
}
void ChromeOmniboxClientIOS::OnUserPastedInOmniboxResultingInValidURL() {
base::RecordAction(
base::UserMetricsAction("Mobile.Omnibox.iOS.PastedValidURL"));
if (!browser_state_->IsOffTheRecord()) {
default_browser::NotifyOmniboxURLCopyPaste(engagement_tracker_);
}
}
void ChromeOmniboxClientIOS::OnResultChanged(
const AutocompleteResult& result,
bool default_match_changed,
bool should_prerender,
const BitmapFetchedCallback& on_bitmap_fetched) {
if (result.empty()) {
return;
}
PrerenderService* service =
PrerenderServiceFactory::GetForBrowserState(browser_state_);
if (!service) {
return;
}
const AutocompleteMatch& match = result.match_at(0);
bool is_inline_autocomplete = !match.inline_autocompletion.empty();
// TODO(crbug.com/40311794): When prerendering the result of a paste
// operation, we should change the transition to LINK instead of TYPED.
// Only prerender HISTORY_URL matches, which come from the history DB. Do
// not prerender other types of matches, including matches from the search
// provider.
if (is_inline_autocomplete &&
match.type == AutocompleteMatchType::HISTORY_URL) {
ui::PageTransition transition = ui::PageTransitionFromInt(
match.transition | ui::PAGE_TRANSITION_FROM_ADDRESS_BAR);
service->StartPrerender(match.destination_url, web::Referrer(), transition,
location_bar_->GetWebState(),
is_inline_autocomplete);
} else {
service->CancelPrerender();
}
}
void ChromeOmniboxClientIOS::OnURLOpenedFromOmnibox(OmniboxLog* log) {
// If a search was done, donate the Search In Chrome intent to the OS for
// future Siri suggestions.
if (!browser_state_->IsOffTheRecord() &&
(log->input_type == metrics::OmniboxInputType::QUERY ||
log->input_type == metrics::OmniboxInputType::UNKNOWN)) {
[IntentDonationHelper donateIntent:IntentType::kSearchInChrome];
}
engagement_tracker_->NotifyEvent(
feature_engagement::events::kOpenUrlFromOmnibox);
}
void ChromeOmniboxClientIOS::DiscardNonCommittedNavigations() {
location_bar_->GetWebState()
->GetNavigationManager()
->DiscardNonCommittedItems();
}
const std::u16string& ChromeOmniboxClientIOS::GetTitle() const {
return CurrentPageExists() ? location_bar_->GetWebState()->GetTitle()
: base::EmptyString16();
}
gfx::Image ChromeOmniboxClientIOS::GetFavicon() const {
return favicon::WebFaviconDriver::FromWebState(location_bar_->GetWebState())
->GetFavicon();
}
void ChromeOmniboxClientIOS::OnAutocompleteAccept(
const GURL& destination_url,
TemplateURLRef::PostContent* post_content,
WindowOpenDisposition disposition,
ui::PageTransition transition,
AutocompleteMatchType::Type match_type,
base::TimeTicks match_selection_timestamp,
bool destination_url_entered_without_scheme,
bool destination_url_entered_with_http_scheme,
const std::u16string& text,
const AutocompleteMatch& match,
const AutocompleteMatch& alternative_nav_match,
IDNA2008DeviationCharacter deviation_char_in_hostname) {
if (location_bar_->GetWebState()) {
web::WebState* web_state = location_bar_->GetWebState();
const int32_t web_state_id = web_state->GetUniqueIdentifier().identifier();
if (web_state_tracker_.find(web_state_id) == web_state_tracker_.end()) {
scoped_observations_.AddObservation(web_state);
}
const ShortcutElement shortcutElement{text, match};
web_state_tracker_.insert_or_assign(web_state_id, shortcutElement);
}
location_bar_->OnNavigate(destination_url, post_content, disposition,
transition, destination_url_entered_without_scheme,
match);
}
base::WeakPtr<OmniboxClient> ChromeOmniboxClientIOS::AsWeakPtr() {
return weak_factory_.GetWeakPtr();
}
void ChromeOmniboxClientIOS::DidFinishNavigation(
web::WebState* web_state,
web::NavigationContext* navigation_context) {
const int32_t web_state_id = web_state->GetUniqueIdentifier().identifier();
ShortcutElement shortcut = web_state_tracker_.extract(web_state_id).mapped();
scoped_observations_.RemoveObservation(web_state);
scoped_refptr<ShortcutsBackend> shortcuts_backend =
ios::ShortcutsBackendFactory::GetInstance()->GetForBrowserState(
browser_state_);
// Add the shortcut if the navigation from the omnibox was successful.
if (!navigation_context->GetError() && shortcuts_backend &&
(navigation_context->GetPageTransition() &
ui::PAGE_TRANSITION_FROM_ADDRESS_BAR)) {
shortcuts_backend->AddOrUpdateShortcut(shortcut.text, shortcut.match);
}
}
void ChromeOmniboxClientIOS::WebStateDestroyed(web::WebState* web_state) {
const int32_t web_state_id = web_state->GetUniqueIdentifier().identifier();
web_state_tracker_.erase(web_state_id);
scoped_observations_.RemoveObservation(web_state);
}