blob: ce8b0eeef4c32d10b0b8e7a8460d6e1083d16c0e [file] [log] [blame]
// Copyright (c) 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/omnibox/browser/physical_web_provider.h"
#include <memory>
#include <string>
#include <utility>
#include "base/feature_list.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "components/bookmarks/browser/titled_url_index.h"
#include "components/bookmarks/browser/titled_url_node_sorter.h"
#include "components/omnibox/browser/autocomplete_provider_client.h"
#include "components/omnibox/browser/autocomplete_provider_listener.h"
#include "components/omnibox/browser/history_url_provider.h"
#include "components/omnibox/browser/omnibox_field_trial.h"
#include "components/omnibox/browser/physical_web_node.h"
#include "components/omnibox/browser/titled_url_match_utils.h"
#include "components/omnibox/browser/url_prefix.h"
#include "components/omnibox/browser/verbatim_match.h"
#include "components/physical_web/data_source/physical_web_data_source.h"
#include "components/strings/grit/components_strings.h"
#include "components/url_formatter/url_formatter.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/text_elider.h"
#include "url/gurl.h"
namespace {
// The maximum length of the page title's part of the overflow item's
// description. Longer titles will be truncated to this length. In a normal
// Physical Web match item (non-overflow item) we allow the omnibox display to
// truncate the title instead.
static const size_t kMaxTitleLengthInOverflow = 15;
// The maximum number of Physical Web URLs to retrieve from the index.
static const size_t kPhysicalWebIndexMaxMatches = 50;
} // namespace
// static
const size_t PhysicalWebProvider::kPhysicalWebMaxMatches = 1;
// static
PhysicalWebProvider* PhysicalWebProvider::Create(
AutocompleteProviderClient* client,
HistoryURLProvider* history_url_provider) {
return new PhysicalWebProvider(client, history_url_provider);
}
void PhysicalWebProvider::Start(const AutocompleteInput& input,
bool minimal_changes) {
DCHECK(kPhysicalWebMaxMatches < kMaxMatches);
Stop(false, false);
done_ = false;
matches_.clear();
had_physical_web_suggestions_ = false;
physical_web::PhysicalWebDataSource* data_source =
client_->GetPhysicalWebDataSource();
// Don't provide results in incognito mode or if the data source could not be
// created.
if (client_->IsOffTheRecord() || !data_source) {
done_ = true;
nearby_url_count_ = 0;
nearby_url_count_at_focus_ = 0;
had_physical_web_suggestions_at_focus_or_later_ = false;
return;
}
auto metadata_list = data_source->GetMetadataList();
nearby_url_count_ = metadata_list->size();
if (input.from_omnibox_focus())
nearby_url_count_at_focus_ = nearby_url_count_;
const bool empty_input_from_user =
!input.from_omnibox_focus() && input.text().empty();
if (input.from_omnibox_focus() || empty_input_from_user) {
ConstructZeroSuggestMatches(std::move(metadata_list));
if (!matches_.empty()) {
had_physical_web_suggestions_ = true;
had_physical_web_suggestions_at_focus_or_later_ = true;
}
// Don't show zero-suggest suggestions unless the PhysicalWebZeroSuggest
// omnibox experiment parameter is enabled. If the omnibox input is empty
// because the user cleared it, also require that PhysicalWebAfterTyping is
// enabled.
if (!zero_suggest_enabled_ ||
(empty_input_from_user && !after_typing_enabled_)) {
matches_.clear();
}
// In zero-suggest, Physical Web matches should never be default. If the
// omnibox input is non-empty and we have at least one Physical Web match,
// add the current URL as the default so that hitting enter after focusing
// the omnibox causes the current page to reload. If the input field is
// empty, no default match is required.
if (!matches_.empty() && !input.text().empty()) {
const base::string16 description =
(base::FeatureList::IsEnabled(omnibox::kDisplayTitleForCurrentUrl))
? input.current_title()
: base::string16();
matches_.push_back(VerbatimMatchForURL(client_, input,
input.current_url(), description,
history_url_provider_, -1));
}
} else {
ConstructQuerySuggestMatches(std::move(metadata_list), input);
if (!matches_.empty()) {
had_physical_web_suggestions_ = true;
had_physical_web_suggestions_at_focus_or_later_ = true;
}
// Don't show Physical Web suggestions after the user starts typing unless
// the PhysicalWebAfterTyping omnibox experiment parameter is enabled.
if (!after_typing_enabled_) {
matches_.clear();
}
}
done_ = true;
}
void PhysicalWebProvider::Stop(bool clear_cached_results,
bool due_to_user_inactivity) {
done_ = true;
}
void PhysicalWebProvider::AddProviderInfo(ProvidersInfo* provider_info) const {
// Record whether the provider could have provided a Physical Web suggestion,
// even if the suggestion could not be displayed due to the current field
// trial.
provider_info->push_back(metrics::OmniboxEventProto_ProviderInfo());
metrics::OmniboxEventProto_ProviderInfo& new_entry = provider_info->back();
new_entry.set_provider(AsOmniboxEventProviderType());
new_entry.set_provider_done(done_);
std::vector<uint32_t> field_trial_hashes;
OmniboxFieldTrial::GetActiveSuggestFieldTrialHashes(&field_trial_hashes);
for (size_t i = 0; i < field_trial_hashes.size(); ++i) {
if (had_physical_web_suggestions_)
new_entry.mutable_field_trial_triggered()->Add(field_trial_hashes[i]);
if (had_physical_web_suggestions_at_focus_or_later_) {
new_entry.mutable_field_trial_triggered_in_session()->Add(
field_trial_hashes[i]);
}
}
// When the user accepts an autocomplete suggestion, record the number of
// nearby Physical Web URLs at the time the provider last constructed matches.
UMA_HISTOGRAM_EXACT_LINEAR(
"Omnibox.SuggestionUsed.NearbyURLCount.AtMatchCreation",
nearby_url_count_, 50);
// On Android, it is somehow possible to accept an omnibox suggestion without
// first focusing the omnibox. In this situation, the nearby URL count at
// focus will be invalid because the omnibox was never focused. We guard
// against recording the invalid data and instead record that we hit this
// corner case.
// TODO(crbug.com/691059): Remove once the focus-less suggestion mystery is
// solved.
const bool suggestion_used_without_focus =
(nearby_url_count_at_focus_ == std::string::npos);
UMA_HISTOGRAM_BOOLEAN(
"Omnibox.PhysicalWebProvider.SuggestionUsedWithoutOmniboxFocus",
suggestion_used_without_focus);
if (!suggestion_used_without_focus) {
// When the user accepts an autocomplete suggestion, record the number of
// nearby Physical Web URLs at the time the omnibox input was last focused.
UMA_HISTOGRAM_EXACT_LINEAR("Omnibox.SuggestionUsed.NearbyURLCount.AtFocus",
nearby_url_count_at_focus_, 50);
}
}
PhysicalWebProvider::PhysicalWebProvider(
AutocompleteProviderClient* client,
HistoryURLProvider* history_url_provider)
: AutocompleteProvider(AutocompleteProvider::TYPE_PHYSICAL_WEB),
client_(client),
history_url_provider_(history_url_provider),
nearby_url_count_(std::string::npos),
nearby_url_count_at_focus_(std::string::npos),
zero_suggest_enabled_(
OmniboxFieldTrial::InPhysicalWebZeroSuggestFieldTrial()),
after_typing_enabled_(
OmniboxFieldTrial::InPhysicalWebAfterTypingFieldTrial()),
zero_suggest_base_relevance_(
OmniboxFieldTrial::GetPhysicalWebZeroSuggestBaseRelevance()),
after_typing_base_relevance_(
OmniboxFieldTrial::GetPhysicalWebAfterTypingBaseRelevance()),
had_physical_web_suggestions_(false),
had_physical_web_suggestions_at_focus_or_later_(false) {}
PhysicalWebProvider::~PhysicalWebProvider() {
}
void PhysicalWebProvider::ConstructZeroSuggestMatches(
std::unique_ptr<physical_web::MetadataList> metadata_list) {
size_t nearby_url_count = metadata_list->size();
size_t used_slots = 0;
for (size_t i = 0; i < nearby_url_count; ++i) {
const auto& metadata_item = (*metadata_list)[i];
std::string url_string = metadata_item.resolved_url.spec();
std::string title_string = metadata_item.title;
base::string16 title =
AutocompleteMatch::SanitizeString(base::UTF8ToUTF16(title_string));
// Add match items with decreasing relevance to preserve the ordering in
// the metadata list.
int relevance = zero_suggest_base_relevance_ - used_slots;
// Append an overflow item if creating a match for each metadata item would
// exceed the match limit.
const size_t remaining_slots = kPhysicalWebMaxMatches - used_slots;
const size_t remaining_metadata = nearby_url_count - i;
if ((remaining_slots == 1) && (remaining_metadata > remaining_slots)) {
AppendOverflowItem(remaining_metadata, relevance, title);
break;
}
GURL url(url_string);
AutocompleteMatch match(this, relevance, false,
AutocompleteMatchType::PHYSICAL_WEB);
match.destination_url = url;
match.contents = url_formatter::FormatUrl(
url, AutocompleteMatch::GetFormatTypes(false, false, false),
net::UnescapeRule::SPACES, nullptr, nullptr, nullptr);
match.contents_class.push_back(
ACMatchClassification(0, ACMatchClassification::URL));
match.fill_into_edit =
AutocompleteInput::FormattedStringWithEquivalentMeaning(
url, url_formatter::FormatUrl(url), client_->GetSchemeClassifier());
match.description = title;
match.description_class.push_back(
ACMatchClassification(0, ACMatchClassification::NONE));
matches_.push_back(match);
++used_slots;
}
UMA_HISTOGRAM_EXACT_LINEAR(
"Omnibox.PhysicalWebProviderMatches", matches_.size(), kMaxMatches);
}
void PhysicalWebProvider::ConstructQuerySuggestMatches(
std::unique_ptr<physical_web::MetadataList> metadata_list,
const AutocompleteInput& input) {
// Passing nullptr for the TitledUrlNodeSorter will cause the returned match
// list to be unsorted.
bookmarks::TitledUrlIndex index(nullptr);
std::vector<std::unique_ptr<PhysicalWebNode>> nodes;
for (const auto& metadata_item : *metadata_list) {
nodes.push_back(base::MakeUnique<PhysicalWebNode>(metadata_item));
index.Add(nodes.back().get());
}
std::vector<bookmarks::TitledUrlMatch> titled_url_matches;
index.GetResultsMatching(input.text(), kPhysicalWebIndexMaxMatches,
query_parser::MatchingAlgorithm::DEFAULT,
&titled_url_matches);
size_t used_slots = 0;
const base::string16 fixed_up_input(FixupUserInput(input).second);
for (auto titled_url_match : titled_url_matches) {
const int relevance = after_typing_base_relevance_ - used_slots;
matches_.push_back(bookmarks::TitledUrlMatchToAutocompleteMatch(
titled_url_match, AutocompleteMatchType::PHYSICAL_WEB, relevance, this,
client_->GetSchemeClassifier(), input, fixed_up_input));
++used_slots;
if (matches_.size() >= kPhysicalWebMaxMatches) {
break;
}
}
}
void PhysicalWebProvider::AppendOverflowItem(int additional_url_count,
int relevance,
const base::string16& title) {
std::string url_string = "chrome://physical-web";
GURL url(url_string);
AutocompleteMatch match(this, relevance, false,
AutocompleteMatchType::PHYSICAL_WEB_OVERFLOW);
match.destination_url = url;
base::string16 contents_title = gfx::TruncateString(
title, kMaxTitleLengthInOverflow, gfx::CHARACTER_BREAK);
if (contents_title.empty()) {
match.contents = l10n_util::GetPluralStringFUTF16(
IDS_PHYSICAL_WEB_OVERFLOW_EMPTY_TITLE, additional_url_count);
} else {
base::string16 contents_suffix = l10n_util::GetPluralStringFUTF16(
IDS_PHYSICAL_WEB_OVERFLOW, additional_url_count - 1);
match.contents = contents_title + base::UTF8ToUTF16(" ") + contents_suffix;
}
match.contents_class.push_back(
ACMatchClassification(0, ACMatchClassification::DIM));
match.fill_into_edit =
AutocompleteInput::FormattedStringWithEquivalentMeaning(
url, match.contents, client_->GetSchemeClassifier());
match.description =
l10n_util::GetStringUTF16(IDS_PHYSICAL_WEB_OVERFLOW_DESCRIPTION);
match.description_class.push_back(
ACMatchClassification(0, ACMatchClassification::NONE));
matches_.push_back(match);
}