blob: e2d451844eb757cfcd4f4ca73507419f1ba6935e [file] [log] [blame]
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/webui/searchbox/realbox_handler.h"
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/field_trial_params.h"
#include "base/metrics/histogram_macros.h"
#include "base/metrics/user_metrics.h"
#include "base/metrics/user_metrics_action.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "build/branding_buildflags.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/autocomplete/autocomplete_classifier_factory.h"
#include "chrome/browser/autocomplete/chrome_autocomplete_provider_client.h"
#include "chrome/browser/autocomplete/chrome_autocomplete_scheme_classifier.h"
#include "chrome/browser/bookmarks/bookmark_model_factory.h"
#include "chrome/browser/predictors/autocomplete_action_predictor.h"
#include "chrome/browser/predictors/autocomplete_action_predictor_factory.h"
#include "chrome/browser/preloading/prefetch/search_prefetch/search_prefetch_service.h"
#include "chrome/browser/preloading/prefetch/search_prefetch/search_prefetch_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/search_engines/template_url_service_factory.h"
#include "chrome/browser/ui/bookmarks/bookmark_stats.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/omnibox/omnibox_pedal_implementations.h"
#include "chrome/browser/ui/search/omnibox_utils.h"
#include "chrome/browser/ui/webui/metrics_reporter/metrics_reporter.h"
#include "chrome/browser/ui/webui/searchbox/lens_searchbox_client.h"
#include "chrome/grit/new_tab_page_resources.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "components/navigation_metrics/navigation_metrics.h"
#include "components/omnibox/browser/autocomplete_classifier.h"
#include "components/omnibox/browser/autocomplete_controller_emitter.h"
#include "components/omnibox/browser/autocomplete_input.h"
#include "components/omnibox/browser/autocomplete_match.h"
#include "components/omnibox/browser/autocomplete_match_type.h"
#include "components/omnibox/browser/autocomplete_result.h"
#include "components/omnibox/browser/omnibox_client.h"
#include "components/omnibox/browser/omnibox_controller.h"
#include "components/omnibox/browser/omnibox_edit_model.h"
#include "components/omnibox/browser/omnibox_event_global_tracker.h"
#include "components/omnibox/browser/omnibox_log.h"
#include "components/omnibox/browser/omnibox_prefs.h"
#include "components/omnibox/browser/omnibox_view.h"
#include "components/omnibox/browser/search_suggestion_parser.h"
#include "components/omnibox/browser/suggestion_answer.h"
#include "components/omnibox/common/omnibox_features.h"
#include "components/prefs/pref_service.h"
#include "components/profile_metrics/browser_profile_type.h"
#include "components/search_engines/template_url_service.h"
#include "components/sessions/content/session_tab_helper.h"
#include "components/strings/grit/components_strings.h"
#include "net/cookies/cookie_util.h"
#include "third_party/metrics_proto/omnibox_focus_type.pb.h"
#include "third_party/omnibox_proto/types.pb.h"
#include "ui/base/webui/resource_path.h"
#include "ui/base/webui/web_ui_util.h"
#include "ui/base/window_open_disposition_utils.h"
namespace {
// TODO(crbug.com/40263573): Consider inheriting from `ChromeOmniboxClient`
// to avoid reimplementation of methods like `OnBookmarkLaunched`.
class RealboxOmniboxClient final : public OmniboxClient {
public:
RealboxOmniboxClient(Profile* profile,
content::WebContents* web_contents,
LensSearchboxClient* lens_searchbox_client);
~RealboxOmniboxClient() override;
// OmniboxClient:
std::unique_ptr<AutocompleteProviderClient> CreateAutocompleteProviderClient()
override;
const GURL& GetURL() const override;
bool IsPasteAndGoEnabled() const override;
SessionID GetSessionID() const override;
PrefService* GetPrefs() override;
bookmarks::BookmarkModel* GetBookmarkModel() override;
AutocompleteControllerEmitter* GetAutocompleteControllerEmitter() override;
TemplateURLService* GetTemplateURLService() override;
const AutocompleteSchemeClassifier& GetSchemeClassifier() const override;
AutocompleteClassifier* GetAutocompleteClassifier() override;
bool ShouldDefaultTypedNavigationsToHttps() const override;
int GetHttpsPortForTesting() const override;
bool IsUsingFakeHttpsForHttpsUpgradeTesting() const override;
gfx::Image GetSizedIcon(const gfx::VectorIcon& vector_icon_type,
SkColor vector_icon_color) const override;
std::u16string GetFormattedFullURL() const override;
std::u16string GetURLForDisplay() const override;
GURL GetNavigationEntryURL() const override;
metrics::OmniboxEventProto::PageClassification GetPageClassification(
bool is_prefetch) override;
security_state::SecurityLevel GetSecurityLevel() const override;
net::CertStatus GetCertStatus() const override;
const gfx::VectorIcon& GetVectorIcon() const override;
std::optional<lens::proto::LensOverlayInteractionResponse>
GetLensOverlayInteractionResponse() const override;
void OnThumbnailRemoved() override;
gfx::Image GetFaviconForPageUrl(
const GURL& page_url,
FaviconFetchedCallback on_favicon_fetched) override;
void OnBookmarkLaunched() override;
void OnURLOpenedFromOmnibox(OmniboxLog* log) override;
void 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) override;
base::WeakPtr<OmniboxClient> AsWeakPtr() override;
void SetLensSearchboxClientForTesting( // IN-TEST
LensSearchboxClient* lens_searchbox_client) {
lens_searchbox_client_ = lens_searchbox_client;
}
private:
raw_ptr<Profile> profile_;
raw_ptr<content::WebContents> web_contents_;
// Owns RealboxHandler which owns this.
raw_ptr<LensSearchboxClient> lens_searchbox_client_;
ChromeAutocompleteSchemeClassifier scheme_classifier_;
// This is unused, but needed for `GetVectorIcon()`.
gfx::VectorIcon vector_icon_{nullptr, 0u, ""};
base::WeakPtrFactory<RealboxOmniboxClient> weak_factory_{this};
};
RealboxOmniboxClient::RealboxOmniboxClient(
Profile* profile,
content::WebContents* web_contents,
LensSearchboxClient* lens_searchbox_client)
: profile_(profile),
web_contents_(web_contents),
lens_searchbox_client_(lens_searchbox_client),
scheme_classifier_(ChromeAutocompleteSchemeClassifier(profile)) {}
RealboxOmniboxClient::~RealboxOmniboxClient() = default;
std::unique_ptr<AutocompleteProviderClient>
RealboxOmniboxClient::CreateAutocompleteProviderClient() {
return std::make_unique<ChromeAutocompleteProviderClient>(profile_);
}
const GURL& RealboxOmniboxClient::GetURL() const {
if (lens_searchbox_client_) {
return lens_searchbox_client_->GetPageURL();
}
return GURL::EmptyGURL();
}
bool RealboxOmniboxClient::IsPasteAndGoEnabled() const {
return false;
}
SessionID RealboxOmniboxClient::GetSessionID() const {
if (lens_searchbox_client_) {
return lens_searchbox_client_->GetTabId();
}
return sessions::SessionTabHelper::IdForTab(web_contents_);
}
PrefService* RealboxOmniboxClient::GetPrefs() {
return profile_->GetPrefs();
}
bookmarks::BookmarkModel* RealboxOmniboxClient::GetBookmarkModel() {
return BookmarkModelFactory::GetForBrowserContext(profile_);
}
AutocompleteControllerEmitter*
RealboxOmniboxClient::GetAutocompleteControllerEmitter() {
return AutocompleteControllerEmitter::GetForBrowserContext(profile_);
}
TemplateURLService* RealboxOmniboxClient::GetTemplateURLService() {
return TemplateURLServiceFactory::GetForProfile(profile_);
}
const AutocompleteSchemeClassifier& RealboxOmniboxClient::GetSchemeClassifier()
const {
return scheme_classifier_;
}
AutocompleteClassifier* RealboxOmniboxClient::GetAutocompleteClassifier() {
return AutocompleteClassifierFactory::GetForProfile(profile_);
}
bool RealboxOmniboxClient::ShouldDefaultTypedNavigationsToHttps() const {
return false;
}
int RealboxOmniboxClient::GetHttpsPortForTesting() const {
return 0;
}
bool RealboxOmniboxClient::IsUsingFakeHttpsForHttpsUpgradeTesting() const {
return false;
}
gfx::Image RealboxOmniboxClient::GetSizedIcon(
const gfx::VectorIcon& vector_icon_type,
SkColor vector_icon_color) const {
return gfx::Image();
}
std::u16string RealboxOmniboxClient::GetFormattedFullURL() const {
return u"";
}
std::u16string RealboxOmniboxClient::GetURLForDisplay() const {
return u"";
}
GURL RealboxOmniboxClient::GetNavigationEntryURL() const {
return GURL();
}
metrics::OmniboxEventProto::PageClassification
RealboxOmniboxClient::GetPageClassification(bool is_prefetch) {
if (lens_searchbox_client_) {
return lens_searchbox_client_->GetPageClassification();
}
return metrics::OmniboxEventProto::NTP_REALBOX;
}
security_state::SecurityLevel RealboxOmniboxClient::GetSecurityLevel() const {
return security_state::SecurityLevel::NONE;
}
net::CertStatus RealboxOmniboxClient::GetCertStatus() const {
return 0;
}
const gfx::VectorIcon& RealboxOmniboxClient::GetVectorIcon() const {
return vector_icon_;
}
gfx::Image RealboxOmniboxClient::GetFaviconForPageUrl(
const GURL& page_url,
FaviconFetchedCallback on_favicon_fetched) {
return gfx::Image();
}
std::optional<lens::proto::LensOverlayInteractionResponse>
RealboxOmniboxClient::GetLensOverlayInteractionResponse() const {
if (lens_searchbox_client_ &&
lens_searchbox_client_->GetLensResponse().has_suggest_signals()) {
return lens_searchbox_client_->GetLensResponse();
}
return std::nullopt;
}
void RealboxOmniboxClient::OnThumbnailRemoved() {
if (lens_searchbox_client_) {
lens_searchbox_client_->OnThumbnailRemoved();
}
}
void RealboxOmniboxClient::OnBookmarkLaunched() {
RecordBookmarkLaunch(BookmarkLaunchLocation::kOmnibox,
profile_metrics::GetBrowserProfileType(profile_));
}
void RealboxOmniboxClient::OnURLOpenedFromOmnibox(OmniboxLog* log) {
if (auto* search_prefetch_service =
SearchPrefetchServiceFactory::GetForProfile(profile_)) {
search_prefetch_service->OnURLOpenedFromOmnibox(log);
}
predictors::AutocompleteActionPredictorFactory::GetForProfile(profile_)
->OnOmniboxOpenedUrl(*log);
}
void RealboxOmniboxClient::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 (lens_searchbox_client_) {
lens_searchbox_client_->OnSuggestionAccepted(
destination_url, match.type,
match.subtypes.contains(omnibox::SUBTYPE_ZERO_PREFIX));
return;
}
web_contents_->OpenURL(
content::OpenURLParams(destination_url, content::Referrer(), disposition,
transition, false),
/*navigation_handle_callback=*/{});
}
base::WeakPtr<OmniboxClient> RealboxOmniboxClient::AsWeakPtr() {
return weak_factory_.GetWeakPtr();
}
} // namespace
RealboxHandler::RealboxHandler(
mojo::PendingReceiver<searchbox::mojom::PageHandler> pending_page_handler,
Profile* profile,
content::WebContents* web_contents,
MetricsReporter* metrics_reporter,
LensSearchboxClient* lens_searchbox_client,
OmniboxController* omnibox_controller)
: SearchboxHandler(std::move(pending_page_handler),
profile,
web_contents,
metrics_reporter),
lens_searchbox_client_(lens_searchbox_client) {
// Keep a reference to the OmniboxController instance owned by the OmniboxView
// when the handler is being used in the context of the omnibox popup.
// Otherwise, create own instance of OmniboxController. Either way, observe
// the AutocompleteController instance owned by the OmniboxController.
if (omnibox_controller) {
controller_ = omnibox_controller;
} else {
owned_controller_ = std::make_unique<OmniboxController>(
/*view=*/nullptr, std::make_unique<RealboxOmniboxClient>(
profile_, web_contents_, lens_searchbox_client));
controller_ = owned_controller_.get();
}
autocomplete_controller_observation_.Observe(autocomplete_controller());
}
RealboxHandler::~RealboxHandler() = default;
bool RealboxHandler::IsRemoteBound() const {
return page_set_;
}
void RealboxHandler::AddObserver(OmniboxWebUIPopupChangeObserver* observer) {
observers_.AddObserver(observer);
observer->OnPopupElementSizeChanged(webui_size_);
}
void RealboxHandler::RemoveObserver(OmniboxWebUIPopupChangeObserver* observer) {
observers_.RemoveObserver(observer);
}
bool RealboxHandler::HasObserver(
const OmniboxWebUIPopupChangeObserver* observer) const {
return observers_.HasObserver(observer);
}
void RealboxHandler::SetPage(
mojo::PendingRemote<searchbox::mojom::Page> pending_page) {
page_.Bind(std::move(pending_page));
page_set_ = page_.is_bound();
// The client may have text waiting to be sent to the searchbox that it
// couldn't do earlier since the page binding was not set. So now we let the
// client know the binding is ready.
if (lens_searchbox_client_) {
lens_searchbox_client_->OnPageBound();
}
}
void RealboxHandler::OnFocusChanged(bool focused) {
if (focused) {
edit_model()->OnSetFocus(false);
} else {
edit_model()->OnWillKillFocus();
edit_model()->OnKillFocus();
}
}
void RealboxHandler::QueryAutocomplete(const std::u16string& input,
bool prevent_inline_autocomplete) {
if (lens_searchbox_client_) {
lens_searchbox_client_->OnTextModified();
}
// TODO(tommycli): We use the input being empty as a signal we are requesting
// on-focus suggestions. It would be nice if we had a more explicit signal.
bool is_on_focus = input.empty();
// Early exit if a query is already in progress for on focus inputs.
if (!autocomplete_controller()->done() && is_on_focus) {
return;
}
// This will SetInputInProgress and consequently mark the input timer so that
// Omnibox.TypingDuration will be logged correctly.
edit_model()->SetUserText(input);
// RealboxOmniboxClient::GetPageClassification() ignores the arguments.
const auto page_classification =
omnibox_controller()->client()->GetPageClassification(
/*is_prefetch=*/false);
AutocompleteInput autocomplete_input(
input, page_classification, ChromeAutocompleteSchemeClassifier(profile_));
autocomplete_input.set_current_url(controller_->client()->GetURL());
autocomplete_input.set_focus_type(
is_on_focus ? metrics::OmniboxFocusType::INTERACTION_FOCUS
: metrics::OmniboxFocusType::INTERACTION_DEFAULT);
autocomplete_input.set_prevent_inline_autocomplete(
prevent_inline_autocomplete);
// Disable keyword matches as NTP realbox has no UI affordance for it.
autocomplete_input.set_prefer_keyword(false);
autocomplete_input.set_allow_exact_keyword_match(false);
// Set the lens overlay interaction response, if available.
if (std::optional<lens::proto::LensOverlayInteractionResponse> response =
controller_->client()->GetLensOverlayInteractionResponse()) {
autocomplete_input.set_lens_overlay_interaction_response(*response);
}
omnibox_controller()->StartAutocomplete(autocomplete_input);
}
void RealboxHandler::StopAutocomplete(bool clear_result) {
omnibox_controller()->StopAutocomplete(clear_result);
}
void RealboxHandler::OpenAutocompleteMatch(uint8_t line,
const GURL& url,
bool are_matches_showing,
uint8_t mouse_button,
bool alt_key,
bool ctrl_key,
bool meta_key,
bool shift_key) {
const AutocompleteMatch* match = GetMatchWithUrl(line, url);
if (!match) {
// This can happen due to asynchronous updates changing the result while
// the web UI is referencing a stale match.
return;
}
const base::TimeTicks timestamp = base::TimeTicks::Now();
const WindowOpenDisposition disposition = ui::DispositionFromClick(
/*middle_button=*/mouse_button == 1, alt_key, ctrl_key, meta_key,
shift_key);
edit_model()->OpenSelection(OmniboxPopupSelection(line), timestamp,
disposition);
}
void RealboxHandler::OnNavigationLikely(
uint8_t line,
const GURL& url,
omnibox::mojom::NavigationPredictor navigation_predictor) {
const AutocompleteMatch* match = GetMatchWithUrl(line, url);
if (!match) {
// This can happen due to asynchronous updates changing the result while
// the web UI is referencing a stale match.
return;
}
if (auto* search_prefetch_service =
SearchPrefetchServiceFactory::GetForProfile(profile_)) {
search_prefetch_service->OnNavigationLikely(
line, *match, navigation_predictor, web_contents_);
}
}
void RealboxHandler::OnThumbnailRemoved() {
omnibox_controller()->client()->OnThumbnailRemoved();
}
void RealboxHandler::PopupElementSizeChanged(const gfx::Size& size) {
webui_size_ = size;
for (OmniboxWebUIPopupChangeObserver& observer : observers_) {
observer.OnPopupElementSizeChanged(size);
}
}
void RealboxHandler::DeleteAutocompleteMatch(uint8_t line, const GURL& url) {
const AutocompleteMatch* match = GetMatchWithUrl(line, url);
if (!match || !match->SupportsDeletion()) {
// This can happen due to asynchronous updates changing the result while
// the web UI is referencing a stale match.
return;
}
omnibox_controller()->StopAutocomplete(/*clear_result=*/false);
autocomplete_controller()->DeleteMatch(*match);
}
void RealboxHandler::ToggleSuggestionGroupIdVisibility(
int32_t suggestion_group_id) {
const auto& group_id = omnibox::GroupIdForNumber(suggestion_group_id);
DCHECK_NE(omnibox::GROUP_INVALID, group_id);
const bool current_visibility =
omnibox_controller()->IsSuggestionGroupHidden(group_id);
omnibox_controller()->SetSuggestionGroupHidden(group_id, !current_visibility);
}
void RealboxHandler::ExecuteAction(uint8_t line,
uint8_t action_index,
const GURL& url,
base::TimeTicks match_selection_timestamp,
uint8_t mouse_button,
bool alt_key,
bool ctrl_key,
bool meta_key,
bool shift_key) {
const AutocompleteMatch* match = GetMatchWithUrl(line, url);
if (!match) {
// This can happen due to asynchronous updates changing the result while
// the web UI is referencing a stale match.
return;
}
if (action_index >= match->actions.size()) {
return;
}
const WindowOpenDisposition disposition = ui::DispositionFromClick(
/*middle_button=*/mouse_button == 1, alt_key, ctrl_key, meta_key,
shift_key);
OmniboxPopupSelection selection(
line, OmniboxPopupSelection::LineState::FOCUSED_BUTTON_ACTION,
action_index);
edit_model()->OpenSelection(selection, match_selection_timestamp,
disposition);
}
searchbox::mojom::SelectionLineState ConvertLineState(
OmniboxPopupSelection::LineState state) {
switch (state) {
case OmniboxPopupSelection::LineState::FOCUSED_BUTTON_HEADER:
return searchbox::mojom::SelectionLineState::kFocusedButtonHeader;
case OmniboxPopupSelection::LineState::NORMAL:
return searchbox::mojom::SelectionLineState::kNormal;
case OmniboxPopupSelection::LineState::KEYWORD_MODE:
return searchbox::mojom::SelectionLineState::kKeywordMode;
case OmniboxPopupSelection::LineState::FOCUSED_BUTTON_ACTION:
return searchbox::mojom::SelectionLineState::kFocusedButtonAction;
case OmniboxPopupSelection::LineState::FOCUSED_BUTTON_REMOVE_SUGGESTION:
return searchbox::mojom::SelectionLineState::
kFocusedButtonRemoveSuggestion;
default:
NOTREACHED_IN_MIGRATION();
break;
}
return searchbox::mojom::SelectionLineState::kNormal;
}
void RealboxHandler::SetInputText(const std::string& input_text) {
page_->SetInputText(input_text);
}
void RealboxHandler::SetThumbnail(const std::string& thumbnail_url) {
page_->SetThumbnail(thumbnail_url);
}
void RealboxHandler::UpdateSelection(OmniboxPopupSelection old_selection,
OmniboxPopupSelection selection) {
page_->UpdateSelection(
searchbox::mojom::OmniboxPopupSelection::New(
old_selection.line, ConvertLineState(old_selection.state),
old_selection.action_index),
searchbox::mojom::OmniboxPopupSelection::New(
selection.line, ConvertLineState(selection.state),
selection.action_index));
}
void RealboxHandler::SetLensSearchboxClientForTesting(
LensSearchboxClient* lens_searchbox_client) {
lens_searchbox_client_ = lens_searchbox_client;
static_cast<RealboxOmniboxClient*>(omnibox_controller()->client())
->SetLensSearchboxClientForTesting(lens_searchbox_client); // IN-TEST
}
OmniboxEditModel* RealboxHandler::edit_model() const {
return omnibox_controller()->edit_model();
}
const AutocompleteMatch* RealboxHandler::GetMatchWithUrl(size_t index,
const GURL& url) {
const AutocompleteResult& result = autocomplete_controller()->result();
if (index >= result.size()) {
// This can happen due to asynchronous updates changing the result while
// the web UI is referencing a stale match.
return nullptr;
}
const AutocompleteMatch& match = result.match_at(index);
if (match.destination_url != url) {
// This can happen also, for the same reason. We could search the result
// for the match with this URL, but there would be no guarantee that it's
// the same match, so for this edge case we treat result mismatch as none.
return nullptr;
}
return &match;
}