blob: bf78f09b6b2868b8e3ab94733083f8914cf7aa6d [file] [log] [blame]
sdefresne70948d62015-08-11 10:46:351// Copyright 2015 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
Robbie Gibsonf4e78b82019-02-20 18:00:195#include "components/omnibox/browser/clipboard_provider.h"
sdefresne70948d62015-08-11 10:46:356
mpearsonea5016a2017-06-01 22:20:327#include <algorithm>
Robbie Gibson71b0c522019-02-13 00:05:588#include <memory>
9#include <utility>
mpearsonea5016a2017-06-01 22:20:3210
Robbie Gibson71b0c522019-02-13 00:05:5811#include "base/bind.h"
gcomanici8cabc77f2017-04-27 20:04:5412#include "base/feature_list.h"
sdefresne70948d62015-08-11 10:46:3513#include "base/logging.h"
Robbie Gibson71b0c522019-02-13 00:05:5814#include "base/memory/ref_counted_memory.h"
15#include "base/memory/weak_ptr.h"
Robbie Gibson1a9405b2019-02-21 12:20:4516#include "base/metrics/field_trial_params.h"
Ilya Sherman1edb6f182017-12-12 04:00:4217#include "base/metrics/histogram_functions.h"
mpearsonab3761492017-03-31 17:29:5118#include "base/metrics/histogram_macros.h"
Robbie Gibsone6915ce12018-12-26 12:08:1019#include "base/optional.h"
sdefresne70948d62015-08-11 10:46:3520#include "base/strings/utf_string_conversions.h"
Robbie Gibson71b0c522019-02-13 00:05:5821#include "base/task/post_task.h"
22#include "base/task/task_traits.h"
sdefresne70948d62015-08-11 10:46:3523#include "components/omnibox/browser/autocomplete_input.h"
Robbie Gibson71b0c522019-02-13 00:05:5824#include "components/omnibox/browser/autocomplete_match.h"
sdefresne70948d62015-08-11 10:46:3525#include "components/omnibox/browser/autocomplete_provider_client.h"
Robbie Gibson71b0c522019-02-13 00:05:5826#include "components/omnibox/browser/autocomplete_provider_listener.h"
sdefresne70948d62015-08-11 10:46:3527#include "components/omnibox/browser/verbatim_match.h"
Tomasz Wiszkowskid938a1112019-03-06 18:01:5728#include "components/omnibox/common/omnibox_features.h"
sdefresne70948d62015-08-11 10:46:3529#include "components/open_from_clipboard/clipboard_recent_content.h"
Robbie Gibsone6915ce12018-12-26 12:08:1030#include "components/search_engines/template_url_service.h"
thakisfe8fa0a2017-02-23 19:46:3631#include "components/strings/grit/components_strings.h"
sdefresne70948d62015-08-11 10:46:3532#include "components/url_formatter/url_formatter.h"
sdefresne70948d62015-08-11 10:46:3533#include "ui/base/l10n/l10n_util.h"
Robbie Gibson71b0c522019-02-13 00:05:5834#include "ui/gfx/image/image_util.h"
sdefresne70948d62015-08-11 10:46:3535
Robbie Gibsonf4e78b82019-02-20 18:00:1936ClipboardProvider::ClipboardProvider(AutocompleteProviderClient* client,
37 AutocompleteProviderListener* listener,
38 HistoryURLProvider* history_url_provider,
39 ClipboardRecentContent* clipboard_content)
40 : AutocompleteProvider(AutocompleteProvider::TYPE_CLIPBOARD),
sdefresne70948d62015-08-11 10:46:3541 client_(client),
Robbie Gibson71b0c522019-02-13 00:05:5842 listener_(listener),
mpearson931028c2016-07-01 18:55:1143 clipboard_content_(clipboard_content),
mpearsonea5016a2017-06-01 22:20:3244 history_url_provider_(history_url_provider),
Robbie Gibson71b0c522019-02-13 00:05:5845 current_url_suggested_times_(0),
Robbie Gibson1a9405b2019-02-21 12:20:4546 field_trial_triggered_(false),
47 field_trial_triggered_in_session_(false),
Robbie Gibson71b0c522019-02-13 00:05:5848 callback_weak_ptr_factory_(this) {
sdefresne70948d62015-08-11 10:46:3549 DCHECK(clipboard_content_);
50}
51
Robbie Gibsonf4e78b82019-02-20 18:00:1952ClipboardProvider::~ClipboardProvider() {}
sdefresne70948d62015-08-11 10:46:3553
Robbie Gibsonf4e78b82019-02-20 18:00:1954void ClipboardProvider::Start(const AutocompleteInput& input,
55 bool minimal_changes) {
sdefresne70948d62015-08-11 10:46:3556 matches_.clear();
Robbie Gibson1a9405b2019-02-21 12:20:4557 field_trial_triggered_ = false;
jif3986ea502016-07-13 13:43:4258
59 // If the user started typing, do not offer clipboard based match.
Robbie Gibson71b0c522019-02-13 00:05:5860 if (!input.from_omnibox_focus()) {
sdefresne70948d62015-08-11 10:46:3561 return;
Robbie Gibsone13f2e62019-01-09 11:00:2462 }
Robbie Gibson71b0c522019-02-13 00:05:5863
64 // Image matched was kicked off asynchronously, so proceed when that ends.
65 if (CreateImageMatch(input)) {
66 return;
67 }
68 base::Optional<AutocompleteMatch> optional_match = CreateURLMatch(input);
Robbie Gibsone6915ce12018-12-26 12:08:1069 if (!optional_match) {
70 optional_match = CreateTextMatch(input);
71 }
72 // The clipboard does not contain any suggestions
73 if (!optional_match) {
mpearsonea5016a2017-06-01 22:20:3274 return;
75 }
sdefresne70948d62015-08-11 10:46:3576
Robbie Gibson50dce212019-02-19 16:42:2577 AddCreatedMatchWithTracking(input, std::move(optional_match).value(),
78 clipboard_content_->GetClipboardContentAge());
Robbie Gibson71b0c522019-02-13 00:05:5879}
80
Robbie Gibsonf4e78b82019-02-20 18:00:1981void ClipboardProvider::Stop(bool clear_cached_results,
82 bool due_to_user_inactivity) {
Robbie Gibson71b0c522019-02-13 00:05:5883 callback_weak_ptr_factory_.InvalidateWeakPtrs();
84 AutocompleteProvider::Stop(clear_cached_results, due_to_user_inactivity);
85}
86
Robbie Gibson1a9405b2019-02-21 12:20:4587void ClipboardProvider::ResetSession() {
88 field_trial_triggered_ = false;
89 field_trial_triggered_in_session_ = false;
90}
91
Robbie Gibsonf4e78b82019-02-20 18:00:1992void ClipboardProvider::AddCreatedMatchWithTracking(
Robbie Gibson71b0c522019-02-13 00:05:5893 const AutocompleteInput& input,
Robbie Gibson50dce212019-02-19 16:42:2594 const AutocompleteMatch& match,
95 const base::TimeDelta clipboard_contents_age) {
Robbie Gibsond2a2eee2019-01-25 14:38:1096 // Record the number of times the currently-offered URL has been suggested.
97 // This only works over this run of Chrome; if the URL was in the clipboard
98 // on a previous run, those offerings will not be counted.
Robbie Gibson71b0c522019-02-13 00:05:5899 if (match.destination_url == current_url_suggested_) {
Robbie Gibsond2a2eee2019-01-25 14:38:10100 current_url_suggested_times_++;
101 } else {
Robbie Gibson71b0c522019-02-13 00:05:58102 current_url_suggested_ = match.destination_url;
Robbie Gibsond2a2eee2019-01-25 14:38:10103 current_url_suggested_times_ = 1;
104 }
105
106 base::UmaHistogramSparse(
107 "Omnibox.ClipboardSuggestionShownNumTimes",
108 std::min(current_url_suggested_times_, static_cast<size_t>(20)));
109
jif3986ea502016-07-13 13:43:42110 // If the omnibox is not empty, add a default match.
111 // This match will be opened when the user presses "Enter".
112 if (!input.text().empty()) {
gcomanici8cabc77f2017-04-27 20:04:54113 const base::string16 description =
114 (base::FeatureList::IsEnabled(omnibox::kDisplayTitleForCurrentUrl))
115 ? input.current_title()
116 : base::string16();
117 AutocompleteMatch verbatim_match =
118 VerbatimMatchForURL(client_, input, input.current_url(), description,
119 history_url_provider_, -1);
sdefresne70948d62015-08-11 10:46:35120 matches_.push_back(verbatim_match);
jif3986ea502016-07-13 13:43:42121 }
mpearsonab3761492017-03-31 17:29:51122 UMA_HISTOGRAM_BOOLEAN("Omnibox.ClipboardSuggestionShownWithCurrentURL",
123 !matches_.empty());
mpearsond3dcdb02017-04-08 01:11:03124 UMA_HISTOGRAM_LONG_TIMES_100("Omnibox.ClipboardSuggestionShownAge",
Robbie Gibson50dce212019-02-19 16:42:25125 clipboard_contents_age);
Robbie Gibsone6915ce12018-12-26 12:08:10126
manuk21aa24d2019-05-01 15:08:16127 matches_.push_back(match);
Robbie Gibsone6915ce12018-12-26 12:08:10128}
129
Robbie Gibsonf4e78b82019-02-20 18:00:19130base::Optional<AutocompleteMatch> ClipboardProvider::CreateURLMatch(
Robbie Gibsone6915ce12018-12-26 12:08:10131 const AutocompleteInput& input) {
132 // The clipboard does not contain a URL worth suggesting.
133 base::Optional<GURL> optional_gurl =
134 clipboard_content_->GetRecentURLFromClipboard();
135 if (!optional_gurl) {
Robbie Gibsone6915ce12018-12-26 12:08:10136 return base::nullopt;
137 }
138 GURL url = std::move(optional_gurl).value();
139
140 // The URL on the page is the same as the URL in the clipboard. Don't
141 // bother suggesting it.
142 if (url == input.current_url())
143 return base::nullopt;
144
145 DCHECK(url.is_valid());
146
jif3986ea502016-07-13 13:43:42147 // Add the clipboard match. The relevance is 800 to beat ZeroSuggest results.
Robbie Gibson3e74f7d2019-01-21 14:03:00148 AutocompleteMatch match(this, 800, false,
149 AutocompleteMatchType::CLIPBOARD_URL);
sdefresne70948d62015-08-11 10:46:35150 match.destination_url = url;
Tommy C. Lic68f2e42017-12-19 20:43:45151 // Because the user did not type a related input to get this clipboard
Tommy C. Li21da43522018-11-20 16:35:28152 // suggestion, preserve the subdomain so the user has extra context.
153 auto format_types = AutocompleteMatch::GetFormatTypes(false, true);
tommycli72014f62017-06-29 21:42:16154 match.contents.assign(url_formatter::FormatUrl(
155 url, format_types, net::UnescapeRule::SPACES, nullptr, nullptr, nullptr));
manuk21aa24d2019-05-01 15:08:16156 if (!match.contents.empty())
157 match.contents_class.push_back({0, ACMatchClassification::URL});
sdefresne70948d62015-08-11 10:46:35158
159 match.description.assign(l10n_util::GetStringUTF16(IDS_LINK_FROM_CLIPBOARD));
manuk21aa24d2019-05-01 15:08:16160 if (!match.description.empty())
161 match.description_class.push_back({0, ACMatchClassification::NONE});
sdefresne70948d62015-08-11 10:46:35162
Robbie Gibsone6915ce12018-12-26 12:08:10163 return match;
164}
165
Robbie Gibsonf4e78b82019-02-20 18:00:19166base::Optional<AutocompleteMatch> ClipboardProvider::CreateTextMatch(
Robbie Gibsone6915ce12018-12-26 12:08:10167 const AutocompleteInput& input) {
168 // Only try text match if feature is enabled
Robbie Gibson80d732e2019-01-10 10:42:49169 if (!base::FeatureList::IsEnabled(
170 omnibox::kEnableClipboardProviderTextSuggestions)) {
Robbie Gibsone6915ce12018-12-26 12:08:10171 return base::nullopt;
172 }
173
174 base::Optional<base::string16> optional_text =
175 clipboard_content_->GetRecentTextFromClipboard();
176 if (!optional_text) {
177 return base::nullopt;
178 }
179 base::string16 text = std::move(optional_text).value();
180
Robbie Gibson146bab02019-04-02 22:29:34181 // The clipboard can contain the empty string, which shouldn't be suggested.
182 if (text.empty()) {
183 return base::nullopt;
184 }
185
Robbie Gibsond2a2eee2019-01-25 14:38:10186 // The text in the clipboard is a url. We don't want to prompt the user to
187 // search for a url.
188 if (GURL(text).is_valid())
189 return base::nullopt;
190
Robbie Gibsone6915ce12018-12-26 12:08:10191 // Add the clipboard match. The relevance is 800 to beat ZeroSuggest results.
Robbie Gibson3e74f7d2019-01-21 14:03:00192 AutocompleteMatch match(this, 800, false,
193 AutocompleteMatchType::CLIPBOARD_TEXT);
Robbie Gibsone6915ce12018-12-26 12:08:10194 TemplateURLService* url_service = client_->GetTemplateURLService();
195 const TemplateURL* default_url = url_service->GetDefaultSearchProvider();
196 DCHECK(!default_url->url().empty());
197 DCHECK(default_url->url_ref().IsValid(url_service->search_terms_data()));
198 TemplateURLRef::SearchTermsArgs search_args(text);
199 GURL result(default_url->url_ref().ReplaceSearchTerms(
200 search_args, url_service->search_terms_data()));
Robbie Gibsond2a2eee2019-01-25 14:38:10201
Robbie Gibsone6915ce12018-12-26 12:08:10202 match.destination_url = result;
203 match.contents.assign(l10n_util::GetStringFUTF16(
204 IDS_COPIED_TEXT_FROM_CLIPBOARD, AutocompleteMatch::SanitizeString(text)));
manuk21aa24d2019-05-01 15:08:16205 if (!match.contents.empty())
206 match.contents_class.push_back({0, ACMatchClassification::NONE});
Robbie Gibsone6915ce12018-12-26 12:08:10207
208 match.description.assign(l10n_util::GetStringUTF16(IDS_TEXT_FROM_CLIPBOARD));
manuk21aa24d2019-05-01 15:08:16209 if (!match.description.empty())
210 match.description_class.push_back({0, ACMatchClassification::NONE});
Robbie Gibsone6915ce12018-12-26 12:08:10211
Robbie Gibson71b0c522019-02-13 00:05:58212 match.keyword = default_url->keyword();
213 match.transition = ui::PAGE_TRANSITION_GENERATED;
214
Robbie Gibson1a9405b2019-02-21 12:20:45215 // Some users may be in a counterfactual study arm in which we perform all
216 // necessary work but do not forward the autocomplete matches.
217 bool in_counterfactual_group = base::GetFieldTrialParamByFeatureAsBool(
218 omnibox::kEnableClipboardProviderTextSuggestions,
219 "ClipboardProviderTextSuggestionsCounterfactualArm", false);
220 field_trial_triggered_ = true;
221 field_trial_triggered_in_session_ = true;
222 if (in_counterfactual_group) {
223 return base::nullopt;
224 }
Robbie Gibsone6915ce12018-12-26 12:08:10225 return match;
sdefresne70948d62015-08-11 10:46:35226}
mpearsonea5016a2017-06-01 22:20:32227
Robbie Gibsonf4e78b82019-02-20 18:00:19228bool ClipboardProvider::CreateImageMatch(const AutocompleteInput& input) {
Robbie Gibsone13f2e62019-01-09 11:00:24229 // Only try image match if feature is enabled
Robbie Gibson80d732e2019-01-10 10:42:49230 if (!base::FeatureList::IsEnabled(
231 omnibox::kEnableClipboardProviderImageSuggestions)) {
Robbie Gibson71b0c522019-02-13 00:05:58232 return false;
Robbie Gibsone13f2e62019-01-09 11:00:24233 }
234
235 // Make sure current provider supports image search
236 TemplateURLService* url_service = client_->GetTemplateURLService();
237 const TemplateURL* default_url = url_service->GetDefaultSearchProvider();
238
239 if (!default_url || default_url->image_url().empty() ||
240 !default_url->image_url_ref().IsValid(url_service->search_terms_data())) {
Robbie Gibson71b0c522019-02-13 00:05:58241 return false;
Robbie Gibsone13f2e62019-01-09 11:00:24242 }
243
Robbie Gibson71b0c522019-02-13 00:05:58244 base::Optional<gfx::Image> optional_image =
245 clipboard_content_->GetRecentImageFromClipboard();
246 if (!optional_image) {
247 return false;
Robbie Gibsone13f2e62019-01-09 11:00:24248 }
249
Robbie Gibson50dce212019-02-19 16:42:25250 // We want to get the age here because the contents of the clipboard could
251 // change after this point. We want the age of the image we actually use, not
252 // the age of whatever's on the clipboard when the histogram is created (i.e
253 // when the match is created).
254 base::TimeDelta clipboard_contents_age =
255 clipboard_content_->GetClipboardContentAge();
Robbie Gibson71b0c522019-02-13 00:05:58256 done_ = false;
257 PostTaskAndReplyWithResult(
258 FROM_HERE,
Robbie Gibsonf4e78b82019-02-20 18:00:19259 base::BindOnce(&ClipboardProvider::EncodeClipboardImage,
Robbie Gibson71b0c522019-02-13 00:05:58260 optional_image.value()),
Robbie Gibsonf4e78b82019-02-20 18:00:19261 base::BindOnce(&ClipboardProvider::ConstructImageMatchCallback,
Robbie Gibson71b0c522019-02-13 00:05:58262 callback_weak_ptr_factory_.GetWeakPtr(), input,
Robbie Gibson50dce212019-02-19 16:42:25263 url_service, clipboard_contents_age));
Robbie Gibson71b0c522019-02-13 00:05:58264 return true;
265}
266
Robbie Gibsonf4e78b82019-02-20 18:00:19267scoped_refptr<base::RefCountedMemory> ClipboardProvider::EncodeClipboardImage(
268 gfx::Image image) {
Robbie Gibson71b0c522019-02-13 00:05:58269 gfx::Image resized_image = gfx::ResizedImageForSearchByImage(image);
270 return resized_image.As1xPNGBytes();
271}
272
Robbie Gibsonf4e78b82019-02-20 18:00:19273void ClipboardProvider::ConstructImageMatchCallback(
Robbie Gibson71b0c522019-02-13 00:05:58274 const AutocompleteInput& input,
275 TemplateURLService* url_service,
Robbie Gibson50dce212019-02-19 16:42:25276 base::TimeDelta clipboard_contents_age,
Robbie Gibson71b0c522019-02-13 00:05:58277 scoped_refptr<base::RefCountedMemory> image_bytes) {
278 const TemplateURL* default_url = url_service->GetDefaultSearchProvider();
Robbie Gibsone13f2e62019-01-09 11:00:24279 // Add the clipboard match. The relevance is 800 to beat ZeroSuggest results.
Robbie Gibson3e74f7d2019-01-21 14:03:00280 AutocompleteMatch match(this, 800, false,
281 AutocompleteMatchType::CLIPBOARD_IMAGE);
Robbie Gibsone13f2e62019-01-09 11:00:24282
283 match.description.assign(l10n_util::GetStringUTF16(IDS_IMAGE_FROM_CLIPBOARD));
manuk21aa24d2019-05-01 15:08:16284 if (!match.description.empty())
285 match.description_class.push_back({0, ACMatchClassification::NONE});
Robbie Gibsone13f2e62019-01-09 11:00:24286
Robbie Gibson71b0c522019-02-13 00:05:58287 TemplateURLRef::SearchTermsArgs search_args(base::ASCIIToUTF16(""));
288 search_args.image_thumbnail_content.assign(image_bytes->front_as<char>(),
289 image_bytes->size());
290 TemplateURLRef::PostContent post_content;
291 GURL result(default_url->image_url_ref().ReplaceSearchTerms(
292 search_args, url_service->search_terms_data(), &post_content));
293 match.destination_url = result;
294 match.post_content =
295 std::make_unique<TemplateURLRef::PostContent>(post_content);
296
297 match.transition = ui::PAGE_TRANSITION_GENERATED;
298
Robbie Gibson1a9405b2019-02-21 12:20:45299 // Some users may be in a counterfactual study arm in which we perform all
300 // necessary work but do not forward the autocomplete matches.
301 bool in_counterfactual_group = base::GetFieldTrialParamByFeatureAsBool(
302 omnibox::kEnableClipboardProviderImageSuggestions,
303 "ClipboardProviderImageSuggestionsCounterfactualArm", false);
304 if (!in_counterfactual_group) {
305 AddCreatedMatchWithTracking(input, match, clipboard_contents_age);
306 listener_->OnProviderUpdate(true);
307 }
308 field_trial_triggered_ = true;
309 field_trial_triggered_in_session_ = true;
Robbie Gibson71b0c522019-02-13 00:05:58310 done_ = true;
Robbie Gibsone13f2e62019-01-09 11:00:24311}
312
Robbie Gibsonf4e78b82019-02-20 18:00:19313void ClipboardProvider::AddProviderInfo(ProvidersInfo* provider_info) const {
mpearsonea5016a2017-06-01 22:20:32314 // If a URL wasn't suggested on this most recent focus event, don't bother
315 // setting |times_returned_results_in_session|, as in effect this URL has
316 // never been suggested during the current session. (For the purpose of
317 // this provider, we define a session as intervals between when a URL
318 // clipboard suggestion changes.)
319 if (current_url_suggested_times_ == 0)
320 return;
321 provider_info->push_back(metrics::OmniboxEventProto_ProviderInfo());
322 metrics::OmniboxEventProto_ProviderInfo& new_entry = provider_info->back();
323 new_entry.set_provider(AsOmniboxEventProviderType());
Robbie Gibson1a9405b2019-02-21 12:20:45324 new_entry.set_provider_done(done_);
mpearsonea5016a2017-06-01 22:20:32325 new_entry.set_times_returned_results_in_session(current_url_suggested_times_);
Robbie Gibson1a9405b2019-02-21 12:20:45326
327 if (field_trial_triggered_ || field_trial_triggered_in_session_) {
328 std::vector<uint32_t> field_trial_hashes;
329 OmniboxFieldTrial::GetActiveSuggestFieldTrialHashes(&field_trial_hashes);
330 for (uint32_t trial : field_trial_hashes) {
331 if (field_trial_triggered_) {
332 new_entry.mutable_field_trial_triggered()->Add(trial);
333 }
334 if (field_trial_triggered_in_session_) {
335 new_entry.mutable_field_trial_triggered_in_session()->Add(trial);
336 }
337 }
338 }
mpearsonea5016a2017-06-01 22:20:32339}