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.
5 // The |FeedbackSender| object stores the user feedback to spellcheck
6 // suggestions in a |Feedback| object.
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
30 #include "chrome/browser/spellchecker/feedback_sender.h"
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
{
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
) {
71 base::StringPrintf("%" PRId64
"%" PRId64
"%" PRIuS
,
72 session_start
.ToInternalValue(),
73 base::Time::Now().ToInternalValue(),
77 // Returns a pending feedback data structure for the spellcheck |result| and
79 Misspelling
BuildFeedback(const SpellCheckResult
& result
,
80 const base::string16
& text
) {
81 size_t start
= result
.location
;
82 base::string16 context
= TrimWords(&start
,
83 start
+ result
.length
,
85 chrome::spellcheck_common::kContextWordCount
);
86 return Misspelling(context
,
89 std::vector
<base::string16
>(1, result
.replacement
),
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
=
100 suggestion_it
!= suggestions
.end();
102 base::DictionaryValue
* suggestion
= SerializeMisspelling(*suggestion_it
);
103 suggestion
->SetBoolean("isFirstInSession", is_first_feedback_batch
);
104 suggestion
->SetBoolean("isAutoCorrection", false);
105 list
->Append(suggestion
);
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");
124 // Builds feedback data from |params|. Takes ownership of |params|. The caller
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
);
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
) <=
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";
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()),
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.
190 misspelling
->action
.set_type(SpellcheckAction::TYPE_SELECT
);
191 misspelling
->action
.set_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.
201 misspelling
->action
.set_type(SpellcheckAction::TYPE_ADD_TO_DICT
);
202 misspelling
->timestamp
= base::Time::Now();
203 const std::set
<uint32
>& hashes
=
204 feedback_
.FindMisspellings(GetMisspelledString(*misspelling
));
205 for (std::set
<uint32
>::const_iterator hash_it
= hashes
.begin();
206 hash_it
!= hashes
.end();
208 Misspelling
* duplicate_misspelling
= feedback_
.GetMisspelling(*hash_it
);
209 if (!duplicate_misspelling
|| duplicate_misspelling
->action
.IsFinal())
211 duplicate_misspelling
->action
.set_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.
222 misspelling
->action
.set_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.
231 misspelling
->action
.set_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.
242 misspelling
->action
.set_type(SpellcheckAction::TYPE_MANUALLY_CORRECTED
);
243 misspelling
->action
.set_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
) {
256 if (!feedback_
.RendererHasMisspellings(renderer_process_id
))
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())
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
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();
286 if (!IsInBounds(result_it
->location
, result_it
->length
, text
.length()))
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
;
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
) {
308 language_
= language
;
312 void FeedbackSender::StartFeedbackCollection() {
313 if (timer_
.IsRunning())
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
)) {
323 base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
324 switches::kSpellingServiceFeedbackIntervalSeconds
),
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
),
336 &FeedbackSender::RequestDocumentMarkers
);
339 void FeedbackSender::StopFeedbackCollection() {
340 if (!timer_
.IsRunning())
347 void FeedbackSender::OnURLFetchComplete(const net::URLFetcher
* source
) {
348 for (ScopedVector
<net::URLFetcher
>::iterator sender_it
= senders_
.begin();
349 sender_it
!= senders_
.end();
351 if (*sender_it
== source
) {
352 senders_
.erase(sender_it
);
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());
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
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
,
377 for (std::vector
<int>::const_iterator it
= dead_renderers
.begin();
378 it
!= dead_renderers
.end();
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())
389 feedback_
.FinalizeAllMisspellings();
390 SendFeedback(feedback_
.GetAllMisspellings(),
391 renderers_sent_feedback_
.empty());
393 renderers_sent_feedback_
.clear();
394 session_start_
= base::Time::Now();
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
),
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());
425 } // namespace spellcheck