blob: 00640e3def2d6d13d8a0b8ce06d3d1ae9983a3d6 [file] [log] [blame]
// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef COMPONENTS_OMNIBOX_BROWSER_AUTOCOMPLETE_RESULT_H_
#define COMPONENTS_OMNIBOX_BROWSER_AUTOCOMPLETE_RESULT_H_
#include <stddef.h>
#include <map>
#include <optional>
#include <utility>
#include <vector>
#include "base/gtest_prod_util.h"
#include "build/build_config.h"
#include "components/omnibox/browser/actions/omnibox_action.h"
#include "components/omnibox/browser/autocomplete_match.h"
#include "components/omnibox/browser/match_compare.h"
#include "components/omnibox/browser/omnibox_metrics_provider.h"
#include "components/omnibox/browser/search_suggestion_parser.h"
#include "components/omnibox/browser/suggestion_group_util.h"
#include "third_party/omnibox_proto/groups.pb.h"
#include "url/gurl.h"
#if BUILDFLAG(IS_ANDROID)
#include "base/android/jni_array.h"
#include "base/android/scoped_java_ref.h"
#endif
class AutocompleteInput;
class AutocompleteProvider;
class AutocompleteProviderClient;
class OmniboxTriggeredFeatureService;
class TemplateURLService;
// All matches from all providers for a particular query. This also tracks
// what the default match should be if the user doesn't manually select another
// match.
class AutocompleteResult {
public:
typedef ACMatches::const_iterator const_iterator;
typedef ACMatches::iterator iterator;
using MatchDedupComparator = ACMatchKey<std::string, // URL
AutocompleteMatchDedupeType>;
// Max number of matches we'll show from the various providers. This limit
// may be different for zero suggest and non zero suggest. Does not take into
// account the boost conditionally provided by the
// omnibox::kDynamicMaxAutocomplete feature.
static size_t GetMaxMatches(
bool is_zero_suggest = false,
AutocompleteInput::FeaturedKeywordMode featured_keyword_mode =
AutocompleteInput::FeaturedKeywordMode::kFalse);
// Defaults to GetMaxMatches if omnibox::kDynamicMaxAutocomplete is disabled;
// otherwise returns the boosted dynamic limit.
static size_t GetDynamicMaxMatches();
AutocompleteResult();
~AutocompleteResult();
AutocompleteResult(const AutocompleteResult&) = delete;
AutocompleteResult& operator=(const AutocompleteResult&) = delete;
#if BUILDFLAG(IS_ANDROID)
// Returns a corresponding Java object, creating it if necessary.
// NOTE: Android specific methods are defined in autocomplete_match_android.cc
base::android::ScopedJavaLocalRef<jobject> GetOrCreateJavaObject(
JNIEnv* env) const;
// Notify the Java object that its native counterpart is about to be
// destroyed.
void DestroyJavaObject() const;
// Construct an array of AutocompleteMatch objects arranged in the exact same
// order as |matches_|.
base::android::ScopedJavaLocalRef<jobjectArray> BuildJavaMatches(
JNIEnv* env) const;
// Group suggestions in specified range by search vs url.
// The range used is [first_index, last_index), which contains all the
// elements between first_index and last_index, including the element pointed
// by first_index, but not the element pointed by last_index.
void GroupSuggestionsBySearchVsURL(JNIEnv* env,
int first_index,
int last_index);
// Compares the set of AutocompleteMatch references held by Java with the
// AutocompleteMatch objects held by this instance of the AutocompleteResult
// and returns true if the two sets are same.
// The |match_index|, when different than -1 (|kNoMatchIndex|), specifies the
// index of a match of particular interest; this index helps identify cases
// where an action is planned on suggestion at an index that falls outside of
// bounds of valid AutocompleteResult indices, where every other aspect of the
// AutocompleteResult is correct.
bool VerifyCoherency(JNIEnv* env,
const base::android::JavaParamRef<jlongArray>& matches,
jint match_index,
jint verification_point);
#endif
// Moves matches from |old_matches| to provide a consistent result set.
// |old_matches| is mutated during this, and should not be used afterwards.
void TransferOldMatches(const AutocompleteInput& input,
AutocompleteResult* old_matches);
// Adds a new set of matches to the result set. Does not re-sort.
void AppendMatches(const ACMatches& matches);
// Modifies |matches| such that any duplicate matches are coalesced into
// representative "best" matches. The erased matches are moved into the
// |duplicate_matches| members of their representative matches.
void DeduplicateMatches(const AutocompleteInput& input,
TemplateURLService* template_url_service);
// See `Sort()`. `SortAndCull()` also groups and culls the suggestions.
// TODO(manukh): `Sort()` without grouping and culling is only needed
// temporarily for ML ranking without changing the search v URL balance.
// After we remove that restriction, they should be re-merged into 1
// function, because calling `Sort()` without calling `SortAndCull()`
// afterwards is probably invalid.
void SortAndCull(const AutocompleteInput& input,
TemplateURLService* template_url_service,
OmniboxTriggeredFeatureService* triggered_feature_service,
bool is_lens_active,
bool can_show_contextual_suggestions,
bool mia_enabled,
std::optional<AutocompleteMatch> default_match_to_preserve =
std::nullopt);
// Removes duplicates, puts the list in sorted order. Sets the default match
// to the best match and updates the alternate nav URL.
//
// `default_match_to_preserve` can be used to prevent the default match from
// being surprisingly swapped out during the asynchronous pass. If it has a
// value, this method searches the results for that match, and promotes it to
// the top. But we don't add back that match if it doesn't already exist.
//
// On desktop, it filters the matches to be either all tail suggestions
// (except for the first match) or no tail suggestions.
//
// TODO(manukh): `Sort()` is useful for determining the default suggestion
// accurately. It includes more code than absolutely necessary for
// determining the default suggestion to help avoid accidentally leaving the
// results in an invalid state. But it really shouldn't be called except if
// `SortAndCull()` is guaranteed to be called soon after. The 2 should be
// re-merged into 1 function once this is no longer needed.
void Sort(const AutocompleteInput& input,
TemplateURLService* template_url_service,
std::optional<AutocompleteMatch> default_match_to_preserve);
// Ensures that matches belonging to suggestion groups, i.e., those with a
// suggestion_group_id value and a corresponding suggestion group info, are
// grouped together at the bottom of result set based on the order in which
// the groups should appear in the result set. This is done for two reasons:
//
// 1) Certain groups of remote zero-prefix matches need to appear under a
// header as specified in omnibox::GroupConfig. GroupConfigs are
// uniquely identified by the group IDs in |suggestion_groups_map_|. It is
// also possible for zero-prefix matches to mix and match while belonging to
// the same groups (e.g., bad server data or mixing of local and remote
// suggestions from different providers). Hence, after mixing, deduping, and
// sorting the matches, we group the ones with the same group ID and demote
// them to the bottom of the result set based on a predetermined order. This
// ensures matches without group IDs or omnibox::GroupConfig to appear at
// the top of the result set, and two, there are no interleaving of groups or
// headers;
//
// 2) Certain groups of non-zero-prefix matches, such as those produced by the
// HistoryClusterProvider, must appear at the bottom of the result set.
// Specifying a group ID (and a corresponding suggestion group info) for those
// matches ensures that would happen.
//
// Called after matches are deduped and sorted and before they are culled.
void GroupAndDemoteMatchesInGroups();
// Filter and remove OmniboxActions according to Platform-specific rules.
void TrimOmniboxActions(bool is_zero_suggest);
// Split some `actions` on matches out to become their own matches.
void SplitActionsToSuggestions();
// Sets |action| in matches that have Pedal-triggering text.
void AttachPedalsToMatches(const AutocompleteInput& input,
const AutocompleteProviderClient& client);
#if BUILDFLAG(IS_IOS) || BUILDFLAG(IS_ANDROID)
// Attaches AIM action to the highest-scoring eligible match in the result
// set, if no other actions are present.
void AttachAimAction(TemplateURLService* template_url_service,
AutocompleteProviderClient* client);
#endif
// Sets a takeover action on all matches to issue a contextual search.
void AttachContextualSearchFulfillmentActionToMatches();
// Sets a takeover action on all matches to open Lens.
void AttachContextualSearchOpenLensActionToMatches();
// Sets |has_tab_match| in matches whose URL matches an open tab's URL.
// Also, fixes up the description if not using another UI element to
// annotate (e.g. tab switch button). |input| can be null; if provided,
// the match can be more precise (e.g. scheme presence).
void ConvertOpenTabMatches(AutocompleteProviderClient* client,
const AutocompleteInput* input);
// Returns true if at least one match was copied from the last result.
bool HasCopiedMatches() const;
// Vector-style accessors/operators.
size_t size() const;
bool empty() const;
const_iterator begin() const;
iterator begin();
const_iterator end() const;
iterator end();
// Returns the match at the given index.
const AutocompleteMatch& match_at(size_t index) const;
AutocompleteMatch* match_at(size_t index);
// Returns the default match if it exists, or nullptr otherwise.
const AutocompleteMatch* default_match() const;
// Returns the first match in |matches| which might be chosen as default.
// If the page is not the fake box, the scores are not demoted by type.
static ACMatches::const_iterator FindTopMatch(const AutocompleteInput& input,
const ACMatches& matches);
static ACMatches::iterator FindTopMatch(const AutocompleteInput& input,
ACMatches* matches);
// If the top match is a Search Entity, and it was deduplicated with a
// non-entity match, splits off the non-entity match from the list of
// duplicates and returns true. Otherwise returns false.
// The non-entity duplicate is promoted to the top, unless the entity match
// has Action in Suggest where it remains at the top.
static bool UndedupTopSearchEntityMatch(ACMatches* matches);
// Just a helper function to encapsulate the logic of deciding how many
// matches to keep, with respect to configured maximums, URL limits,
// and relevancies.
static size_t CalculateNumMatches(
bool is_zero_suggest,
AutocompleteInput::FeaturedKeywordMode featured_keyword_mode,
const ACMatches& matches,
const CompareWithDemoteByType<AutocompleteMatch>& comparing_object);
// Determines how many matches to keep depending on how many URLs would be
// shown. Increases the match limit if there are TYPE_UNSCOPED_EXTENSION
// suggestions available so they don't replace other match types.
// CalculateNumMatches defers to CalculateNumMatchesPerUrlCount if
// all of the following are true:
// 1) not in zero suggest.
// 2) not in exact featured keyword mode.
// 3) `kDynamicMaxAutocomplete` feature is enabled.
static size_t CalculateNumMatchesPerUrlCount(
const ACMatches& matches,
const CompareWithDemoteByType<AutocompleteMatch>& comparing_object);
const omnibox::GroupConfigMap& suggestion_groups_map() const {
return suggestion_groups_map_;
}
const SessionData& session() const { return session_; }
bool zero_prefix_enabled_in_session() const {
return session_.zero_prefix_enabled;
}
void set_zero_prefix_enabled_in_session(bool enabled) {
session_.zero_prefix_enabled = enabled;
}
size_t num_zero_prefix_suggestions_shown_in_session() const {
return session_.num_zero_prefix_suggestions_shown;
}
void set_num_zero_prefix_suggestions_shown_in_session(size_t number) {
session_.num_zero_prefix_suggestions_shown = number;
}
const std::vector<int64_t>& gws_event_id_hashes_in_session() const {
return session_.gws_event_id_hashes;
}
void add_gws_event_id_hash_in_session(int64_t gws_event_id_hash) {
session_.gws_event_id_hashes.push_back(gws_event_id_hash);
}
void clear_gws_event_id_hashes_in_session() {
session_.gws_event_id_hashes.clear();
}
std::pair<bool, bool> contextual_suggestions_shown_in_session() {
return {session_.contextual_search_suggestions_shown_in_session,
session_.lens_action_shown_in_session};
}
std::pair<bool, bool> suggestions_shown_in_session(bool is_zero_suggest) {
if (is_zero_suggest) {
return {session_.zero_prefix_search_suggestions_shown_in_session,
session_.zero_prefix_url_suggestions_shown_in_session};
} else {
return {session_.typed_search_suggestions_shown_in_session,
session_.typed_url_suggestions_shown_in_session};
}
}
void set_suggestions_shown_in_session(bool is_zero_suggest,
const AutocompleteMatch& match) {
bool is_search = OmniboxMetricsProvider::GetClientSummarizedResultType(
match.GetOmniboxEventResultType()) ==
ClientSummarizedResultType::kSearch;
if (is_zero_suggest) {
session_.zero_prefix_suggestions_shown_in_session |= true;
if (is_search) {
session_.zero_prefix_search_suggestions_shown_in_session |= true;
} else {
session_.zero_prefix_url_suggestions_shown_in_session |= true;
}
} else {
session_.typed_suggestions_shown_in_session |= true;
if (is_search) {
session_.typed_search_suggestions_shown_in_session |= true;
} else {
session_.typed_url_suggestions_shown_in_session |= true;
}
}
if (match.takeover_action) {
switch (match.takeover_action->ActionId()) {
case OmniboxActionId::CONTEXTUAL_SEARCH_FULFILLMENT:
session_.contextual_search_suggestions_shown_in_session |= true;
break;
case OmniboxActionId::CONTEXTUAL_SEARCH_OPEN_LENS:
session_.lens_action_shown_in_session |= true;
break;
default:
break;
}
}
}
// Clears this result set - i.e., `matches_` and `suggestion_groups_map_`.
void ClearMatches();
// Clears this result set and the session data - i.e., `matches_`,
// `suggestion_groups_map_` and `session_`. Called when
// AutocompleteController::Stop() with `clear_result=true` is called.
void Reset();
#if DCHECK_IS_ON()
// Does a data integrity check on this result.
void Validate() const;
#endif // DCHECK_IS_ON()
// Returns a URL to offer the user as an alternative navigation when they
// open |match| after typing in |input|.
static GURL ComputeAlternateNavUrl(
const AutocompleteInput& input,
const AutocompleteMatch& match,
AutocompleteProviderClient* provider_client);
// Gets common prefix from SEARCH_SUGGEST_TAIL matches
std::u16string GetCommonPrefix();
// Estimates dynamic memory usage.
// See base/trace_event/memory_usage_estimator.h for more info.
size_t EstimateMemoryUsage() const;
// Get a list of comparators used for deduping for the matches in this result.
// This is only used for logging.
std::vector<MatchDedupComparator> GetMatchDedupComparators() const;
// Returns the header string associated with |suggestion_group_id|.
// Returns an empty string if |suggestion_group_id| is not found in
// |suggestion_groups_map_|.
std::u16string GetHeaderForSuggestionGroup(
omnibox::GroupId suggestion_group_id) const;
// Returns the section associated with |suggestion_group_id|.
// Returns omnibox::SECTION_DEFAULT if |suggestion_group_id| is not found in
// |suggestion_groups_map_|.
omnibox::GroupSection GetSectionForSuggestionGroup(
omnibox::GroupId suggestion_group_id) const;
// Returns the side type associated with `suggestion_group_id`.
// Returns omnibox::DEFAULT_PRIMARY if `suggestion_group_id` is not found in
// `suggestion_groups_map_`.
omnibox::GroupConfig_SideType GetSideTypeForSuggestionGroup(
omnibox::GroupId suggestion_group_id) const;
// Returns the render type associated with `suggestion_group_id`.
// Returns omnibox::DEFAULT_VERTICAL if `suggestion_group_id` is not found in
// `suggestion_groups_map_`.
omnibox::GroupConfig_RenderType GetRenderTypeForSuggestionGroup(
omnibox::GroupId suggestion_group_id) const;
// Updates |suggestion_groups_map_| with the suggestion groups information
// from |suggeston_groups_map|. Followed by GroupAndDemoteMatchesInGroups()
// which sorts the matches based on the order in which their groups should
// appear while preserving the existing order of matches within the same
// group.
void MergeSuggestionGroupsMap(
const omnibox::GroupConfigMap& suggeston_groups_map);
// Erase matches where `predicate` returns true. In other words, this
// preserves only those matches for which `predicate` returns false.
template <typename UnaryPredicate>
size_t EraseMatchesWhere(UnaryPredicate predicate) {
return std::erase_if(matches_, predicate);
}
// This method implements a stateful stable partition. Matches which are
// search types, and their submatches regardless of type, are shifted
// earlier in the range, while non-search types and their submatches
// are shifted later. For grouping purposes, the starter pack suggestions
// (while technically navigation suggestions) are grouped before search types.
static void GroupSuggestionsBySearchVsURL(iterator begin, iterator end);
// This value should be comfortably larger than any max-autocomplete-matches
// under consideration.
static constexpr size_t kMaxAutocompletePositionValue = 30;
private:
friend class AutocompleteController;
friend class AutocompleteResultForTesting;
friend class AutocompleteProviderTest;
friend class HistoryURLProviderTest;
FRIEND_TEST_ALL_PREFIXES(AutocompleteResultTest, Desktop_TwoColumnRealbox);
FRIEND_TEST_ALL_PREFIXES(AutocompleteResultTest, Android_TrimOmniboxActions);
FRIEND_TEST_ALL_PREFIXES(AutocompleteResultTest, SwapMatches);
typedef std::map<AutocompleteProvider*, ACMatches> ProviderToMatches;
#if BUILDFLAG(IS_ANDROID)
// iterator::difference_type is not defined in the STL that we compile with on
// Android.
typedef int matches_difference_type;
#else
typedef ACMatches::iterator::difference_type matches_difference_type;
#endif
// Swaps this result set - i.e., `matches_` and `suggestion_groups_map_` -
// with `other`. Called in AutocompleteController and tests only.
void SwapMatchesWith(AutocompleteResult* other);
// Copies the result set - i.e., `matches_` and `suggestion_groups_map_` -
// from `other`. Called in AutocompleteController and tests only.
void CopyMatchesFrom(const AutocompleteResult& other);
// Modifies |matches| such that any duplicate matches are coalesced into
// representative "best" matches. The erased matches are moved into the
// |duplicate_matches| members of their representative matches.
static void DeduplicateMatches(ACMatches* matches,
const AutocompleteInput& input,
TemplateURLService* template_url_service);
// Returns true if |matches| contains a match with the same destination as
// |match|.
static bool HasMatchByDestination(const AutocompleteMatch& match,
const ACMatches& matches);
// If there are both tail and non-tail suggestions (ignoring one default
// match), remove the tail suggestions. If the only default matches are tail
// suggestions, remove the non-tail suggestions.
static void MaybeCullTailSuggestions(
ACMatches* matches,
const CompareWithDemoteByType<AutocompleteMatch>& comparing_object);
// Populates |provider_to_matches| from |matches_|. This AutocompleteResult
// should not be used after the 'move' version.
void BuildProviderToMatchesCopy(ProviderToMatches* provider_to_matches) const;
void BuildProviderToMatchesMove(ProviderToMatches* provider_to_matches);
// Moves matches into this result. |old_matches| gives the matches from the
// last result, and |new_matches| the results from this result. |old_matches|
// should not be used afterwards.
void MergeMatchesByProvider(ACMatches* old_matches,
const ACMatches& new_matches);
// Returns a tuple encompassing all attributes relevant to determining whether
// a match should be deduplicated with another match.
static MatchDedupComparator GetMatchComparisonFields(
const AutocompleteMatch& match);
// This method reduces the number of navigation suggestions to that of
// |max_url_matches| but will allow more if there are no other types to
// replace them.
void LimitNumberOfURLsShown(
size_t max_matches,
size_t max_url_count,
const CompareWithDemoteByType<AutocompleteMatch>& comparing_object);
// If we have SearchProvider search suggestions, demote OnDeviceProvider
// search suggestions, since, which in general have lower quality than
// SearchProvider search suggestions. The demotion can happen in two ways,
// controlled by Finch (1. decrease-relevances or 2. remove-suggestions):
// 1. Decrease the on device search suggestion relevances that they will
// always be shown after SearchProvider search suggestions.
// 2. Set the relevances of OnDeviceProvider search suggestions to 0, such
// that they will be removed from result list later.
void DemoteOnDeviceSearchSuggestions();
// The current result set. Cleared on `ClearMatches()` or `Reset()`.
ACMatches matches_;
// The map of suggestion group IDs to suggestion group information for the
// current result set. Cleared along with `matches_` on `ClearMatches()` or
// `Reset()`.
omnibox::GroupConfigMap suggestion_groups_map_;
// The session data irrespective of the current result set. Cleared on
// `Reset()`.
// TODO(crbug.com/40218651): This is a bandaid solution for storing the
// session data. It relies on `ClearMatches()`, `SwapMatchesWith()`, and
// `CopyMatchesFrom()` to only modify the current result set and not the
// session data; and for `Reset()` to be called once during the autocomplete
// session. Ideally, this should be replaced with a more general solution such
// as changing the OmniboxTriggeredFeatureService so that it defines an
// autocomplete session to start when the omnibox is focused and to end when
// the popup closes.
SessionData session_;
#if BUILDFLAG(IS_ANDROID)
// Corresponding Java object.
// This object should be ignored when AutocompleteResult is copied or moved.
// This object should never be accessed directly. To acquire a reference to
// java object, call the GetOrCreateJavaObject().
// Note that this object is lazily constructed to avoid creating Java matches
// for throw away AutocompleteMatch objects, eg. during Classify() or
// QualifyPartialUrlQuery() calls.
// See AutocompleteControllerAndroid for more details.
mutable base::android::ScopedJavaGlobalRef<jobject> java_result_;
#endif
// For LOG debugging.
friend std::ostream& operator<<(std::ostream& os,
const AutocompleteResult& result);
};
#endif // COMPONENTS_OMNIBOX_BROWSER_AUTOCOMPLETE_RESULT_H_