Pin Chrome's shortcut to the Win10 Start menu on install and OS upgrade.
[chromium-blink-merge.git] / chrome / browser / spellchecker / feedback_sender.cc
blobf18a3881968a81661bcbeccc2b2653db82bfc804
1 // Copyright (c) 2013 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 //
5 // The |FeedbackSender| object stores the user feedback to spellcheck
6 // suggestions in a |Feedback| object.
7 //
8 // When spelling service returns spellcheck results, these results first arrive
9 // in |FeedbackSender| to assign hash identifiers for each
10 // misspelling-suggestion pair. If the spelling service identifies the same
11 // misspelling as already displayed to the user, then |FeedbackSender| reuses
12 // the same hash identifiers to avoid duplication. It detects the duplicates by
13 // comparing misspelling offsets in text. Spelling service can return duplicates
14 // because we request spellcheck for whole paragraphs, as context around a
15 // misspelled word is important to the spellcheck algorithm.
17 // All feedback is initially pending. When a user acts upon a misspelling such
18 // that the misspelling is no longer displayed (red squiggly line goes away),
19 // then the feedback for this misspelling is finalized. All finalized feedback
20 // is erased after being sent to the spelling service. Pending feedback is kept
21 // around for |kSessionHours| hours and then finalized even if user did not act
22 // on the misspellings.
24 // |FeedbackSender| periodically requests a list of hashes of all remaining
25 // misspellings in renderers. When a renderer responds with a list of hashes,
26 // |FeedbackSender| uses the list to determine which misspellings are no longer
27 // displayed to the user and sends the current state of user feedback to the
28 // spelling service.
30 #include "chrome/browser/spellchecker/feedback_sender.h"
32 #include <algorithm>
33 #include <iterator>
35 #include "base/command_line.h"
36 #include "base/hash.h"
37 #include "base/json/json_writer.h"
38 #include "base/location.h"
39 #include "base/metrics/field_trial.h"
40 #include "base/single_thread_task_runner.h"
41 #include "base/stl_util.h"
42 #include "base/strings/string_number_conversions.h"
43 #include "base/strings/stringprintf.h"
44 #include "base/thread_task_runner_handle.h"
45 #include "base/values.h"
46 #include "chrome/browser/spellchecker/word_trimmer.h"
47 #include "chrome/common/chrome_switches.h"
48 #include "chrome/common/spellcheck_common.h"
49 #include "chrome/common/spellcheck_marker.h"
50 #include "chrome/common/spellcheck_messages.h"
51 #include "content/public/browser/render_process_host.h"
52 #include "google_apis/google_api_keys.h"
53 #include "net/base/load_flags.h"
54 #include "net/url_request/url_fetcher.h"
55 #include "net/url_request/url_request_context_getter.h"
57 namespace spellcheck {
59 namespace {
61 // The default URL where feedback data is sent.
62 const char kFeedbackServiceURL[] = "https://www.googleapis.com/rpc";
64 // The minimum number of seconds between sending batches of feedback.
65 const int kMinIntervalSeconds = 5;
67 // Returns a hash of |session_start|, the current timestamp, and
68 // |suggestion_index|.
69 uint32 BuildHash(const base::Time& session_start, size_t suggestion_index) {
70 return base::Hash(
71 base::StringPrintf("%" PRId64 "%" PRId64 "%" PRIuS,
72 session_start.ToInternalValue(),
73 base::Time::Now().ToInternalValue(),
74 suggestion_index));
77 // Returns a pending feedback data structure for the spellcheck |result| and
78 // |text|.
79 Misspelling BuildFeedback(const SpellCheckResult& result,
80 const base::string16& text) {
81 size_t start = result.location;
82 base::string16 context = TrimWords(&start,
83 result.length,
84 text,
85 chrome::spellcheck_common::kContextWordCount);
86 return Misspelling(context,
87 start,
88 result.length,
89 std::vector<base::string16>(1, result.replacement),
90 result.hash);
93 // Builds suggestion info from |suggestions|. The caller owns the result.
94 base::ListValue* BuildSuggestionInfo(
95 const std::vector<Misspelling>& suggestions,
96 bool is_first_feedback_batch) {
97 base::ListValue* list = new base::ListValue;
98 for (std::vector<Misspelling>::const_iterator suggestion_it =
99 suggestions.begin();
100 suggestion_it != suggestions.end();
101 ++suggestion_it) {
102 base::DictionaryValue* suggestion = suggestion_it->Serialize();
103 suggestion->SetBoolean("isFirstInSession", is_first_feedback_batch);
104 suggestion->SetBoolean("isAutoCorrection", false);
105 list->Append(suggestion);
107 return list;
110 // Builds feedback parameters from |suggestion_info|, |language|, and |country|.
111 // Takes ownership of |suggestion_list|. The caller owns the result.
112 base::DictionaryValue* BuildParams(base::ListValue* suggestion_info,
113 const std::string& language,
114 const std::string& country) {
115 base::DictionaryValue* params = new base::DictionaryValue;
116 params->Set("suggestionInfo", suggestion_info);
117 params->SetString("key", google_apis::GetAPIKey());
118 params->SetString("language", language);
119 params->SetString("originCountry", country);
120 params->SetString("clientName", "Chrome");
121 return params;
124 // Builds feedback data from |params|. Takes ownership of |params|. The caller
125 // owns the result.
126 base::Value* BuildFeedbackValue(base::DictionaryValue* params,
127 const std::string& api_version) {
128 base::DictionaryValue* result = new base::DictionaryValue;
129 result->Set("params", params);
130 result->SetString("method", "spelling.feedback");
131 result->SetString("apiVersion", api_version);
132 return result;
135 // Returns true if the misspelling location is within text bounds.
136 bool IsInBounds(int misspelling_location,
137 int misspelling_length,
138 size_t text_length) {
139 return misspelling_location >= 0 && misspelling_length > 0 &&
140 static_cast<size_t>(misspelling_location) < text_length &&
141 static_cast<size_t>(misspelling_location + misspelling_length) <=
142 text_length;
145 // Returns the feedback API version.
146 std::string GetApiVersion() {
147 // This guard is temporary.
148 // TODO(rouslan): Remove the guard. http://crbug.com/247726
149 if (base::FieldTrialList::FindFullName(kFeedbackFieldTrialName) ==
150 kFeedbackFieldTrialEnabledGroupName &&
151 base::CommandLine::ForCurrentProcess()->HasSwitch(
152 switches::kEnableSpellingFeedbackFieldTrial)) {
153 return "v2-internal";
155 return "v2";
158 } // namespace
160 FeedbackSender::FeedbackSender(net::URLRequestContextGetter* request_context,
161 const std::string& language,
162 const std::string& country)
163 : request_context_(request_context),
164 api_version_(GetApiVersion()),
165 language_(language),
166 country_(country),
167 misspelling_counter_(0),
168 session_start_(base::Time::Now()),
169 feedback_service_url_(kFeedbackServiceURL) {
170 // The command-line switch is for testing and temporary.
171 // TODO(rouslan): Remove the command-line switch when testing is complete.
172 // http://crbug.com/247726
173 if (base::CommandLine::ForCurrentProcess()->HasSwitch(
174 switches::kSpellingServiceFeedbackUrl)) {
175 feedback_service_url_ =
176 GURL(base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
177 switches::kSpellingServiceFeedbackUrl));
181 FeedbackSender::~FeedbackSender() {
184 void FeedbackSender::SelectedSuggestion(uint32 hash, int suggestion_index) {
185 Misspelling* misspelling = feedback_.GetMisspelling(hash);
186 // GetMisspelling() returns null for flushed feedback. Feedback is flushed
187 // when the session expires every |kSessionHours| hours.
188 if (!misspelling)
189 return;
190 misspelling->action.type = SpellcheckAction::TYPE_SELECT;
191 misspelling->action.index = suggestion_index;
192 misspelling->timestamp = base::Time::Now();
195 void FeedbackSender::AddedToDictionary(uint32 hash) {
196 Misspelling* misspelling = feedback_.GetMisspelling(hash);
197 // GetMisspelling() returns null for flushed feedback. Feedback is flushed
198 // when the session expires every |kSessionHours| hours.
199 if (!misspelling)
200 return;
201 misspelling->action.type = SpellcheckAction::TYPE_ADD_TO_DICT;
202 misspelling->timestamp = base::Time::Now();
203 const std::set<uint32>& hashes =
204 feedback_.FindMisspellings(misspelling->GetMisspelledString());
205 for (std::set<uint32>::const_iterator hash_it = hashes.begin();
206 hash_it != hashes.end();
207 ++hash_it) {
208 Misspelling* duplicate_misspelling = feedback_.GetMisspelling(*hash_it);
209 if (!duplicate_misspelling || duplicate_misspelling->action.IsFinal())
210 continue;
211 duplicate_misspelling->action.type = SpellcheckAction::TYPE_ADD_TO_DICT;
212 duplicate_misspelling->timestamp = misspelling->timestamp;
216 void FeedbackSender::RecordInDictionary(uint32 hash) {
217 Misspelling* misspelling = feedback_.GetMisspelling(hash);
218 // GetMisspelling() returns null for flushed feedback. Feedback is flushed
219 // when the session expires every |kSessionHours| hours.
220 if (!misspelling)
221 return;
222 misspelling->action.type = SpellcheckAction::TYPE_IN_DICTIONARY;
225 void FeedbackSender::IgnoredSuggestions(uint32 hash) {
226 Misspelling* misspelling = feedback_.GetMisspelling(hash);
227 // GetMisspelling() returns null for flushed feedback. Feedback is flushed
228 // when the session expires every |kSessionHours| hours.
229 if (!misspelling)
230 return;
231 misspelling->action.type = SpellcheckAction::TYPE_PENDING_IGNORE;
232 misspelling->timestamp = base::Time::Now();
235 void FeedbackSender::ManuallyCorrected(uint32 hash,
236 const base::string16& correction) {
237 Misspelling* misspelling = feedback_.GetMisspelling(hash);
238 // GetMisspelling() returns null for flushed feedback. Feedback is flushed
239 // when the session expires every |kSessionHours| hours.
240 if (!misspelling)
241 return;
242 misspelling->action.type = SpellcheckAction::TYPE_MANUALLY_CORRECTED;
243 misspelling->action.value = correction;
244 misspelling->timestamp = base::Time::Now();
247 void FeedbackSender::OnReceiveDocumentMarkers(
248 int renderer_process_id,
249 const std::vector<uint32>& markers) {
250 if ((base::Time::Now() - session_start_).InHours() >=
251 chrome::spellcheck_common::kSessionHours) {
252 FlushFeedback();
253 return;
256 if (!feedback_.RendererHasMisspellings(renderer_process_id))
257 return;
259 feedback_.FinalizeRemovedMisspellings(renderer_process_id, markers);
260 SendFeedback(feedback_.GetMisspellingsInRenderer(renderer_process_id),
261 !renderers_sent_feedback_.count(renderer_process_id));
262 renderers_sent_feedback_.insert(renderer_process_id);
263 feedback_.EraseFinalizedMisspellings(renderer_process_id);
266 void FeedbackSender::OnSpellcheckResults(
267 int renderer_process_id,
268 const base::string16& text,
269 const std::vector<SpellCheckMarker>& markers,
270 std::vector<SpellCheckResult>* results) {
271 // Don't collect feedback if not going to send it.
272 if (!timer_.IsRunning())
273 return;
275 // Generate a map of marker offsets to marker hashes. This map helps to
276 // efficiently lookup feedback data based on the position of the misspelling
277 // in text.
278 typedef std::map<size_t, uint32> MarkerMap;
279 MarkerMap marker_map;
280 for (size_t i = 0; i < markers.size(); ++i)
281 marker_map[markers[i].offset] = markers[i].hash;
283 for (std::vector<SpellCheckResult>::iterator result_it = results->begin();
284 result_it != results->end();
285 ++result_it) {
286 if (!IsInBounds(result_it->location, result_it->length, text.length()))
287 continue;
288 MarkerMap::const_iterator marker_it = marker_map.find(result_it->location);
289 if (marker_it != marker_map.end() &&
290 feedback_.HasMisspelling(marker_it->second)) {
291 // If the renderer already has a marker for this spellcheck result, then
292 // set the hash of the spellcheck result to be the same as the marker.
293 result_it->hash = marker_it->second;
294 } else {
295 // If the renderer does not yet have a marker for this spellcheck result,
296 // then generate a new hash for the spellcheck result.
297 result_it->hash = BuildHash(session_start_, ++misspelling_counter_);
299 // Save the feedback data for the spellcheck result.
300 feedback_.AddMisspelling(renderer_process_id,
301 BuildFeedback(*result_it, text));
305 void FeedbackSender::OnLanguageCountryChange(const std::string& language,
306 const std::string& country) {
307 FlushFeedback();
308 language_ = language;
309 country_ = country;
312 void FeedbackSender::StartFeedbackCollection() {
313 if (timer_.IsRunning())
314 return;
316 int interval_seconds = chrome::spellcheck_common::kFeedbackIntervalSeconds;
317 // This command-line switch is for testing and temporary.
318 // TODO(rouslan): Remove the command-line switch when testing is complete.
319 // http://crbug.com/247726
320 if (base::CommandLine::ForCurrentProcess()->HasSwitch(
321 switches::kSpellingServiceFeedbackIntervalSeconds)) {
322 base::StringToInt(
323 base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
324 switches::kSpellingServiceFeedbackIntervalSeconds),
325 &interval_seconds);
326 if (interval_seconds < kMinIntervalSeconds)
327 interval_seconds = kMinIntervalSeconds;
328 static const int kSessionSeconds =
329 chrome::spellcheck_common::kSessionHours * 60 * 60;
330 if (interval_seconds > kSessionSeconds)
331 interval_seconds = kSessionSeconds;
333 timer_.Start(FROM_HERE,
334 base::TimeDelta::FromSeconds(interval_seconds),
335 this,
336 &FeedbackSender::RequestDocumentMarkers);
339 void FeedbackSender::StopFeedbackCollection() {
340 if (!timer_.IsRunning())
341 return;
343 FlushFeedback();
344 timer_.Stop();
347 void FeedbackSender::OnURLFetchComplete(const net::URLFetcher* source) {
348 for (ScopedVector<net::URLFetcher>::iterator sender_it = senders_.begin();
349 sender_it != senders_.end();
350 ++sender_it) {
351 if (*sender_it == source) {
352 senders_.erase(sender_it);
353 return;
356 delete source;
359 void FeedbackSender::RequestDocumentMarkers() {
360 // Request document markers from all the renderers that are still alive.
361 std::set<int> alive_renderers;
362 for (content::RenderProcessHost::iterator it(
363 content::RenderProcessHost::AllHostsIterator());
364 !it.IsAtEnd();
365 it.Advance()) {
366 alive_renderers.insert(it.GetCurrentValue()->GetID());
367 it.GetCurrentValue()->Send(new SpellCheckMsg_RequestDocumentMarkers());
370 // Asynchronously send out the feedback for all the renderers that are no
371 // longer alive.
372 std::vector<int> known_renderers = feedback_.GetRendersWithMisspellings();
373 std::sort(known_renderers.begin(), known_renderers.end());
374 std::vector<int> dead_renderers =
375 base::STLSetDifference<std::vector<int> >(known_renderers,
376 alive_renderers);
377 for (std::vector<int>::const_iterator it = dead_renderers.begin();
378 it != dead_renderers.end();
379 ++it) {
380 base::ThreadTaskRunnerHandle::Get()->PostTask(
381 FROM_HERE, base::Bind(&FeedbackSender::OnReceiveDocumentMarkers,
382 AsWeakPtr(), *it, std::vector<uint32>()));
386 void FeedbackSender::FlushFeedback() {
387 if (feedback_.Empty())
388 return;
389 feedback_.FinalizeAllMisspellings();
390 SendFeedback(feedback_.GetAllMisspellings(),
391 renderers_sent_feedback_.empty());
392 feedback_.Clear();
393 renderers_sent_feedback_.clear();
394 session_start_ = base::Time::Now();
395 timer_.Reset();
398 void FeedbackSender::SendFeedback(const std::vector<Misspelling>& feedback_data,
399 bool is_first_feedback_batch) {
400 scoped_ptr<base::Value> feedback_value(BuildFeedbackValue(
401 BuildParams(BuildSuggestionInfo(feedback_data, is_first_feedback_batch),
402 language_,
403 country_),
404 api_version_));
405 std::string feedback;
406 base::JSONWriter::Write(*feedback_value, &feedback);
408 // The tests use this identifier to mock the URL fetcher.
409 static const int kUrlFetcherId = 0;
410 net::URLFetcher* sender =
411 net::URLFetcher::Create(kUrlFetcherId, feedback_service_url_,
412 net::URLFetcher::POST, this).release();
413 sender->SetLoadFlags(net::LOAD_DO_NOT_SEND_COOKIES |
414 net::LOAD_DO_NOT_SAVE_COOKIES);
415 sender->SetUploadData("application/json", feedback);
416 senders_.push_back(sender);
418 // Request context is NULL in testing.
419 if (request_context_.get()) {
420 sender->SetRequestContext(request_context_.get());
421 sender->Start();
425 } // namespace spellcheck