Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(108)

Side by Side Diff: components/suggestions/suggestions_service.cc

Issue 2568133005: [SuggestionsService] Split SuggestionsService interface from impl (Closed)
Patch Set: Moved impl to dedicated file. Created 4 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 // Copyright 2014 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 #include "components/suggestions/suggestions_service.h"
6
7 #include <memory>
8 #include <utility>
9
10 #include "base/feature_list.h"
11 #include "base/location.h"
12 #include "base/metrics/histogram_macros.h"
13 #include "base/metrics/sparse_histogram.h"
14 #include "base/strings/string_number_conversions.h"
15 #include "base/strings/string_util.h"
16 #include "base/strings/stringprintf.h"
17 #include "base/threading/thread_task_runner_handle.h"
18 #include "build/build_config.h"
19 #include "components/data_use_measurement/core/data_use_user_data.h"
20 #include "components/google/core/browser/google_util.h"
21 #include "components/pref_registry/pref_registry_syncable.h"
22 #include "components/signin/core/browser/signin_manager_base.h"
23 #include "components/suggestions/blacklist_store.h"
24 #include "components/suggestions/image_manager.h"
25 #include "components/suggestions/suggestions_store.h"
26 #include "components/sync/driver/sync_service.h"
27 #include "components/variations/net/variations_http_headers.h"
28 #include "google_apis/gaia/gaia_constants.h"
29 #include "google_apis/gaia/oauth2_token_service.h"
30 #include "net/base/escape.h"
31 #include "net/base/load_flags.h"
32 #include "net/base/net_errors.h"
33 #include "net/base/url_util.h"
34 #include "net/http/http_response_headers.h"
35 #include "net/http/http_status_code.h"
36 #include "net/http/http_util.h"
37 #include "net/url_request/url_fetcher.h"
38 #include "net/url_request/url_request_status.h"
39
40 using base::TimeDelta;
41 using base::TimeTicks;
42
43 namespace suggestions {
44
45 namespace {
46
47 // Establishes the different sync states that matter to SuggestionsService.
48 // There are three different concepts in the sync service: initialized, sync
49 // enabled and history sync enabled.
50 enum SyncState {
51 // State: Sync service is not initialized, yet not disabled. History sync
52 // state is unknown (since not initialized).
53 // Behavior: Does not issue a server request, but serves from cache if
54 // available.
55 NOT_INITIALIZED_ENABLED,
56
57 // State: Sync service is initialized, sync is enabled and history sync is
58 // enabled.
59 // Behavior: Update suggestions from the server. Serve from cache on timeout.
60 INITIALIZED_ENABLED_HISTORY,
61
62 // State: Sync service is disabled or history sync is disabled.
63 // Behavior: Do not issue a server request. Clear the cache. Serve empty
64 // suggestions.
65 SYNC_OR_HISTORY_SYNC_DISABLED,
66 };
67
68 SyncState GetSyncState(syncer::SyncService* sync) {
69 if (!sync || !sync->CanSyncStart())
70 return SYNC_OR_HISTORY_SYNC_DISABLED;
71 if (!sync->IsSyncActive() || !sync->ConfigurationDone())
72 return NOT_INITIALIZED_ENABLED;
73 return sync->GetActiveDataTypes().Has(syncer::HISTORY_DELETE_DIRECTIVES)
74 ? INITIALIZED_ENABLED_HISTORY
75 : SYNC_OR_HISTORY_SYNC_DISABLED;
76 }
77
78 // Used to UMA log the state of the last response from the server.
79 enum SuggestionsResponseState {
80 RESPONSE_EMPTY,
81 RESPONSE_INVALID,
82 RESPONSE_VALID,
83 RESPONSE_STATE_SIZE
84 };
85
86 // Will log the supplied response |state|.
87 void LogResponseState(SuggestionsResponseState state) {
88 UMA_HISTOGRAM_ENUMERATION("Suggestions.ResponseState", state,
89 RESPONSE_STATE_SIZE);
90 }
91
92 // Default delay used when scheduling a request.
93 const int kDefaultSchedulingDelaySec = 1;
94
95 // Multiplier on the delay used when re-scheduling a failed request.
96 const int kSchedulingBackoffMultiplier = 2;
97
98 // Maximum valid delay for scheduling a request. Candidate delays larger than
99 // this are rejected. This means the maximum backoff is at least 5 / 2 minutes.
100 const int kSchedulingMaxDelaySec = 5 * 60;
101
102 const char kDefaultGoogleBaseURL[] = "https://www.google.com/";
103
104 GURL GetGoogleBaseURL() {
105 GURL url(google_util::CommandLineGoogleBaseURL());
106 if (url.is_valid())
107 return url;
108 return GURL(kDefaultGoogleBaseURL);
109 }
110
111 // Format strings for the various suggestions URLs. They all have two string
112 // params: The Google base URL and the device type.
113 // TODO(mathp): Put this in TemplateURL.
114 const char kSuggestionsURLFormat[] =
115 "%schromesuggestions?t=%s";
116 const char kSuggestionsBlacklistURLPrefixFormat[] =
117 "%schromesuggestions/blacklist?t=%s&url=";
118 const char kSuggestionsBlacklistClearURLFormat[] =
119 "%schromesuggestions/blacklist/clear?t=%s";
120
121 const char kSuggestionsBlacklistURLParam[] = "url";
122
123 #if defined(OS_ANDROID) || defined(OS_IOS)
124 const char kDeviceType[] = "2";
125 #else
126 const char kDeviceType[] = "1";
127 #endif
128
129 // Format string for OAuth2 authentication headers.
130 const char kAuthorizationHeaderFormat[] = "Authorization: Bearer %s";
131
132 const char kFaviconURL[] =
133 "https://s2.googleusercontent.com/s2/favicons?domain_url=%s&alt=s&sz=32";
134
135 // The default expiry timeout is 168 hours.
136 const int64_t kDefaultExpiryUsec = 168 * base::Time::kMicrosecondsPerHour;
137
138 } // namespace
139
140 // Helper class for fetching OAuth2 access tokens.
141 // To get a token, call |GetAccessToken|. Does not support multiple concurrent
142 // token requests, i.e. check |HasPendingRequest| first.
143 class SuggestionsService::AccessTokenFetcher
144 : public OAuth2TokenService::Consumer {
145 public:
146 using TokenCallback = base::Callback<void(const std::string&)>;
147
148 AccessTokenFetcher(const SigninManagerBase* signin_manager,
149 OAuth2TokenService* token_service)
150 : OAuth2TokenService::Consumer("suggestions_service"),
151 signin_manager_(signin_manager),
152 token_service_(token_service) {}
153
154 void GetAccessToken(const TokenCallback& callback) {
155 callback_ = callback;
156 std::string account_id;
157 // |signin_manager_| can be null in unit tests.
158 if (signin_manager_)
159 account_id = signin_manager_->GetAuthenticatedAccountId();
160 OAuth2TokenService::ScopeSet scopes;
161 scopes.insert(GaiaConstants::kChromeSyncOAuth2Scope);
162 token_request_ = token_service_->StartRequest(account_id, scopes, this);
163 }
164
165 bool HasPendingRequest() const {
166 return !!token_request_.get();
167 }
168
169 private:
170 void OnGetTokenSuccess(const OAuth2TokenService::Request* request,
171 const std::string& access_token,
172 const base::Time& expiration_time) override {
173 DCHECK_EQ(request, token_request_.get());
174 callback_.Run(access_token);
175 token_request_.reset(nullptr);
176 }
177
178 void OnGetTokenFailure(const OAuth2TokenService::Request* request,
179 const GoogleServiceAuthError& error) override {
180 DCHECK_EQ(request, token_request_.get());
181 LOG(WARNING) << "Token error: " << error.ToString();
182 callback_.Run(std::string());
183 token_request_.reset(nullptr);
184 }
185
186 const SigninManagerBase* signin_manager_;
187 OAuth2TokenService* token_service_;
188
189 TokenCallback callback_;
190 std::unique_ptr<OAuth2TokenService::Request> token_request_;
191 };
192
193 SuggestionsService::SuggestionsService(
194 const SigninManagerBase* signin_manager,
195 OAuth2TokenService* token_service,
196 syncer::SyncService* sync_service,
197 net::URLRequestContextGetter* url_request_context,
198 std::unique_ptr<SuggestionsStore> suggestions_store,
199 std::unique_ptr<ImageManager> thumbnail_manager,
200 std::unique_ptr<BlacklistStore> blacklist_store)
201 : sync_service_(sync_service),
202 sync_service_observer_(this),
203 url_request_context_(url_request_context),
204 suggestions_store_(std::move(suggestions_store)),
205 thumbnail_manager_(std::move(thumbnail_manager)),
206 blacklist_store_(std::move(blacklist_store)),
207 scheduling_delay_(TimeDelta::FromSeconds(kDefaultSchedulingDelaySec)),
208 token_fetcher_(new AccessTokenFetcher(signin_manager, token_service)),
209 weak_ptr_factory_(this) {
210 // |sync_service_| is null if switches::kDisableSync is set (tests use that).
211 if (sync_service_)
212 sync_service_observer_.Add(sync_service_);
213 // Immediately get the current sync state, so we'll flush the cache if
214 // necessary.
215 OnStateChanged();
216 }
217
218 SuggestionsService::~SuggestionsService() {}
219
220 bool SuggestionsService::FetchSuggestionsData() {
221 DCHECK(thread_checker_.CalledOnValidThread());
222 // If sync state allows, issue a network request to refresh the suggestions.
223 if (GetSyncState(sync_service_) != INITIALIZED_ENABLED_HISTORY)
224 return false;
225 IssueRequestIfNoneOngoing(BuildSuggestionsURL());
226 return true;
227 }
228
229 SuggestionsProfile SuggestionsService::GetSuggestionsDataFromCache() const {
230 SuggestionsProfile suggestions;
231 // In case of empty cache or error, |suggestions| stays empty.
232 suggestions_store_->LoadSuggestions(&suggestions);
233 thumbnail_manager_->Initialize(suggestions);
234 blacklist_store_->FilterSuggestions(&suggestions);
235 return suggestions;
236 }
237
238 std::unique_ptr<SuggestionsService::ResponseCallbackList::Subscription>
239 SuggestionsService::AddCallback(const ResponseCallback& callback) {
240 return callback_list_.Add(callback);
241 }
242
243 void SuggestionsService::GetPageThumbnail(const GURL& url,
244 const BitmapCallback& callback) {
245 thumbnail_manager_->GetImageForURL(url, callback);
246 }
247
248 void SuggestionsService::GetPageThumbnailWithURL(
249 const GURL& url,
250 const GURL& thumbnail_url,
251 const BitmapCallback& callback) {
252 thumbnail_manager_->AddImageURL(url, thumbnail_url);
253 GetPageThumbnail(url, callback);
254 }
255
256 bool SuggestionsService::BlacklistURL(const GURL& candidate_url) {
257 DCHECK(thread_checker_.CalledOnValidThread());
258
259 if (!blacklist_store_->BlacklistUrl(candidate_url))
260 return false;
261
262 callback_list_.Notify(GetSuggestionsDataFromCache());
263
264 // Blacklist uploads are scheduled on any request completion, so only schedule
265 // an upload if there is no ongoing request.
266 if (!pending_request_.get())
267 ScheduleBlacklistUpload();
268
269 return true;
270 }
271
272 bool SuggestionsService::UndoBlacklistURL(const GURL& url) {
273 DCHECK(thread_checker_.CalledOnValidThread());
274 TimeDelta time_delta;
275 if (blacklist_store_->GetTimeUntilURLReadyForUpload(url, &time_delta) &&
276 time_delta > TimeDelta::FromSeconds(0) &&
277 blacklist_store_->RemoveUrl(url)) {
278 // The URL was not yet candidate for upload to the server and could be
279 // removed from the blacklist.
280 callback_list_.Notify(GetSuggestionsDataFromCache());
281 return true;
282 }
283 return false;
284 }
285
286 void SuggestionsService::ClearBlacklist() {
287 DCHECK(thread_checker_.CalledOnValidThread());
288 blacklist_store_->ClearBlacklist();
289 callback_list_.Notify(GetSuggestionsDataFromCache());
290 IssueRequestIfNoneOngoing(BuildSuggestionsBlacklistClearURL());
291 }
292
293 // static
294 bool SuggestionsService::GetBlacklistedUrl(const net::URLFetcher& request,
295 GURL* url) {
296 bool is_blacklist_request = base::StartsWith(
297 request.GetOriginalURL().spec(), BuildSuggestionsBlacklistURLPrefix(),
298 base::CompareCase::SENSITIVE);
299 if (!is_blacklist_request) return false;
300
301 // Extract the blacklisted URL from the blacklist request.
302 std::string blacklisted;
303 if (!net::GetValueForKeyInQuery(
304 request.GetOriginalURL(),
305 kSuggestionsBlacklistURLParam,
306 &blacklisted)) {
307 return false;
308 }
309
310 GURL blacklisted_url(blacklisted);
311 blacklisted_url.Swap(url);
312 return true;
313 }
314
315 // static
316 void SuggestionsService::RegisterProfilePrefs(
317 user_prefs::PrefRegistrySyncable* registry) {
318 SuggestionsStore::RegisterProfilePrefs(registry);
319 BlacklistStore::RegisterProfilePrefs(registry);
320 }
321
322 // static
323 GURL SuggestionsService::BuildSuggestionsURL() {
324 return GURL(base::StringPrintf(kSuggestionsURLFormat,
325 GetGoogleBaseURL().spec().c_str(),
326 kDeviceType));
327 }
328
329 // static
330 std::string SuggestionsService::BuildSuggestionsBlacklistURLPrefix() {
331 return base::StringPrintf(kSuggestionsBlacklistURLPrefixFormat,
332 GetGoogleBaseURL().spec().c_str(), kDeviceType);
333 }
334
335 // static
336 GURL SuggestionsService::BuildSuggestionsBlacklistURL(
337 const GURL& candidate_url) {
338 return GURL(BuildSuggestionsBlacklistURLPrefix() +
339 net::EscapeQueryParamValue(candidate_url.spec(), true));
340 }
341
342 // static
343 GURL SuggestionsService::BuildSuggestionsBlacklistClearURL() {
344 return GURL(base::StringPrintf(kSuggestionsBlacklistClearURLFormat,
345 GetGoogleBaseURL().spec().c_str(),
346 kDeviceType));
347 }
348
349 void SuggestionsService::OnStateChanged() {
350 switch (GetSyncState(sync_service_)) {
351 case SYNC_OR_HISTORY_SYNC_DISABLED:
352 // Cancel any ongoing request, to stop interacting with the server.
353 pending_request_.reset(nullptr);
354 suggestions_store_->ClearSuggestions();
355 callback_list_.Notify(SuggestionsProfile());
356 break;
357 case NOT_INITIALIZED_ENABLED:
358 // Keep the cache (if any), but don't refresh.
359 break;
360 case INITIALIZED_ENABLED_HISTORY:
361 // If we have any observers, issue a network request to refresh the
362 // suggestions in the cache.
363 if (!callback_list_.empty())
364 IssueRequestIfNoneOngoing(BuildSuggestionsURL());
365 break;
366 }
367 }
368
369 void SuggestionsService::SetDefaultExpiryTimestamp(
370 SuggestionsProfile* suggestions,
371 int64_t default_timestamp_usec) {
372 for (int i = 0; i < suggestions->suggestions_size(); ++i) {
373 ChromeSuggestion* suggestion = suggestions->mutable_suggestions(i);
374 // Do not set expiry if the server has already provided a more specific
375 // expiry time for this suggestion.
376 if (!suggestion->has_expiry_ts()) {
377 suggestion->set_expiry_ts(default_timestamp_usec);
378 }
379 }
380 }
381
382 void SuggestionsService::IssueRequestIfNoneOngoing(const GURL& url) {
383 // If there is an ongoing request, let it complete.
384 if (pending_request_.get()) {
385 return;
386 }
387 // If there is an ongoing token request, also wait for that.
388 if (token_fetcher_->HasPendingRequest()) {
389 return;
390 }
391 token_fetcher_->GetAccessToken(
392 base::Bind(&SuggestionsService::IssueSuggestionsRequest,
393 base::Unretained(this), url));
394 }
395
396 void SuggestionsService::IssueSuggestionsRequest(
397 const GURL& url,
398 const std::string& access_token) {
399 if (access_token.empty()) {
400 UpdateBlacklistDelay(false);
401 ScheduleBlacklistUpload();
402 return;
403 }
404 pending_request_ = CreateSuggestionsRequest(url, access_token);
405 pending_request_->Start();
406 last_request_started_time_ = TimeTicks::Now();
407 }
408
409 std::unique_ptr<net::URLFetcher> SuggestionsService::CreateSuggestionsRequest(
410 const GURL& url,
411 const std::string& access_token) {
412 std::unique_ptr<net::URLFetcher> request =
413 net::URLFetcher::Create(0, url, net::URLFetcher::GET, this);
414 data_use_measurement::DataUseUserData::AttachToFetcher(
415 request.get(), data_use_measurement::DataUseUserData::SUGGESTIONS);
416 int load_flags = net::LOAD_DISABLE_CACHE | net::LOAD_DO_NOT_SEND_COOKIES |
417 net::LOAD_DO_NOT_SAVE_COOKIES;
418
419 request->SetLoadFlags(load_flags);
420 request->SetRequestContext(url_request_context_);
421 // Add Chrome experiment state to the request headers.
422 net::HttpRequestHeaders headers;
423 variations::AppendVariationHeaders(request->GetOriginalURL(), false, false,
424 &headers);
425 request->SetExtraRequestHeaders(headers.ToString());
426 if (!access_token.empty()) {
427 request->AddExtraRequestHeader(
428 base::StringPrintf(kAuthorizationHeaderFormat, access_token.c_str()));
429 }
430 return request;
431 }
432
433 void SuggestionsService::OnURLFetchComplete(const net::URLFetcher* source) {
434 DCHECK(thread_checker_.CalledOnValidThread());
435 DCHECK_EQ(pending_request_.get(), source);
436
437 // The fetcher will be deleted when the request is handled.
438 std::unique_ptr<const net::URLFetcher> request(std::move(pending_request_));
439
440 const net::URLRequestStatus& request_status = request->GetStatus();
441 if (request_status.status() != net::URLRequestStatus::SUCCESS) {
442 // This represents network errors (i.e. the server did not provide a
443 // response).
444 UMA_HISTOGRAM_SPARSE_SLOWLY("Suggestions.FailedRequestErrorCode",
445 -request_status.error());
446 DVLOG(1) << "Suggestions server request failed with error: "
447 << request_status.error() << ": "
448 << net::ErrorToString(request_status.error());
449 UpdateBlacklistDelay(false);
450 ScheduleBlacklistUpload();
451 return;
452 }
453
454 const int response_code = request->GetResponseCode();
455 UMA_HISTOGRAM_SPARSE_SLOWLY("Suggestions.FetchResponseCode", response_code);
456 if (response_code != net::HTTP_OK) {
457 // A non-200 response code means that server has no (longer) suggestions for
458 // this user. Aggressively clear the cache.
459 suggestions_store_->ClearSuggestions();
460 UpdateBlacklistDelay(false);
461 ScheduleBlacklistUpload();
462 return;
463 }
464
465 const TimeDelta latency = TimeTicks::Now() - last_request_started_time_;
466 UMA_HISTOGRAM_MEDIUM_TIMES("Suggestions.FetchSuccessLatency", latency);
467
468 // Handle a successful blacklisting.
469 GURL blacklisted_url;
470 if (GetBlacklistedUrl(*source, &blacklisted_url)) {
471 blacklist_store_->RemoveUrl(blacklisted_url);
472 }
473
474 std::string suggestions_data;
475 bool success = request->GetResponseAsString(&suggestions_data);
476 DCHECK(success);
477
478 // Parse the received suggestions and update the cache, or take proper action
479 // in the case of invalid response.
480 SuggestionsProfile suggestions;
481 if (suggestions_data.empty()) {
482 LogResponseState(RESPONSE_EMPTY);
483 suggestions_store_->ClearSuggestions();
484 } else if (suggestions.ParseFromString(suggestions_data)) {
485 LogResponseState(RESPONSE_VALID);
486 int64_t now_usec =
487 (base::Time::NowFromSystemTime() - base::Time::UnixEpoch())
488 .ToInternalValue();
489 SetDefaultExpiryTimestamp(&suggestions, now_usec + kDefaultExpiryUsec);
490 PopulateExtraData(&suggestions);
491 suggestions_store_->StoreSuggestions(suggestions);
492 } else {
493 LogResponseState(RESPONSE_INVALID);
494 }
495
496 callback_list_.Notify(GetSuggestionsDataFromCache());
497
498 UpdateBlacklistDelay(true);
499 ScheduleBlacklistUpload();
500 }
501
502 void SuggestionsService::PopulateExtraData(SuggestionsProfile* suggestions) {
503 for (int i = 0; i < suggestions->suggestions_size(); ++i) {
504 suggestions::ChromeSuggestion* s = suggestions->mutable_suggestions(i);
505 if (!s->has_favicon_url() || s->favicon_url().empty()) {
506 s->set_favicon_url(base::StringPrintf(kFaviconURL, s->url().c_str()));
507 }
508 }
509 }
510
511 void SuggestionsService::Shutdown() {
512 // Cancel pending request.
513 pending_request_.reset(nullptr);
514 }
515
516 void SuggestionsService::ScheduleBlacklistUpload() {
517 DCHECK(thread_checker_.CalledOnValidThread());
518 TimeDelta time_delta;
519 if (blacklist_store_->GetTimeUntilReadyForUpload(&time_delta)) {
520 // Blacklist cache is not empty: schedule.
521 base::Closure blacklist_cb =
522 base::Bind(&SuggestionsService::UploadOneFromBlacklist,
523 weak_ptr_factory_.GetWeakPtr());
524 base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
525 FROM_HERE, blacklist_cb, time_delta + scheduling_delay_);
526 }
527 }
528
529 void SuggestionsService::UploadOneFromBlacklist() {
530 DCHECK(thread_checker_.CalledOnValidThread());
531
532 GURL blacklisted_url;
533 if (blacklist_store_->GetCandidateForUpload(&blacklisted_url)) {
534 // Issue a blacklisting request. Even if this request ends up not being sent
535 // because of an ongoing request, a blacklist request is later scheduled.
536 IssueRequestIfNoneOngoing(BuildSuggestionsBlacklistURL(blacklisted_url));
537 return;
538 }
539
540 // Even though there's no candidate for upload, the blacklist might not be
541 // empty.
542 ScheduleBlacklistUpload();
543 }
544
545 void SuggestionsService::UpdateBlacklistDelay(bool last_request_successful) {
546 DCHECK(thread_checker_.CalledOnValidThread());
547
548 if (last_request_successful) {
549 scheduling_delay_ = TimeDelta::FromSeconds(kDefaultSchedulingDelaySec);
550 } else {
551 TimeDelta candidate_delay =
552 scheduling_delay_ * kSchedulingBackoffMultiplier;
553 if (candidate_delay < TimeDelta::FromSeconds(kSchedulingMaxDelaySec))
554 scheduling_delay_ = candidate_delay;
555 }
556 }
557
558 } // namespace suggestions
OLDNEW
« no previous file with comments | « components/suggestions/suggestions_service.h ('k') | components/suggestions/suggestions_service_impl.h » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698