Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / chromeos / timezone / timezone_request.cc
blob7c284cd43788476a84b410a65078c08742ab7c77
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.
5 #include "chromeos/timezone/timezone_request.h"
7 #include <string>
9 #include "base/json/json_reader.h"
10 #include "base/metrics/histogram.h"
11 #include "base/metrics/sparse_histogram.h"
12 #include "base/strings/string_number_conversions.h"
13 #include "base/strings/stringprintf.h"
14 #include "base/time/time.h"
15 #include "base/values.h"
16 #include "google_apis/google_api_keys.h"
17 #include "net/base/escape.h"
18 #include "net/base/load_flags.h"
19 #include "net/http/http_status_code.h"
20 #include "net/url_request/url_request_context_getter.h"
21 #include "net/url_request/url_request_status.h"
23 namespace chromeos {
25 namespace {
27 const char kDefaultTimezoneProviderUrl[] =
28 "https://maps.googleapis.com/maps/api/timezone/json?";
30 const char kKeyString[] = "key";
31 // Language parameter is unsupported for now.
32 // const char kLanguageString[] = "language";
33 const char kLocationString[] = "location";
34 const char kSensorString[] = "sensor";
35 const char kTimestampString[] = "timestamp";
37 const char kDstOffsetString[] = "dstOffset";
38 const char kRawOffsetString[] = "rawOffset";
39 const char kTimeZoneIdString[] = "timeZoneId";
40 const char kTimeZoneNameString[] = "timeZoneName";
41 const char kStatusString[] = "status";
42 const char kErrorMessageString[] = "error_message";
44 // Sleep between timezone request retry on HTTP error.
45 const unsigned int kResolveTimeZoneRetrySleepOnServerErrorSeconds = 5;
47 // Sleep between timezone request retry on bad server response.
48 const unsigned int kResolveTimeZoneRetrySleepBadResponseSeconds = 10;
50 struct StatusString2Enum {
51 const char* string;
52 TimeZoneResponseData::Status value;
55 const StatusString2Enum statusString2Enum[] = {
56 {"OK", TimeZoneResponseData::OK},
57 {"INVALID_REQUEST", TimeZoneResponseData::INVALID_REQUEST},
58 {"OVER_QUERY_LIMIT", TimeZoneResponseData::OVER_QUERY_LIMIT},
59 {"REQUEST_DENIED", TimeZoneResponseData::REQUEST_DENIED},
60 {"UNKNOWN_ERROR", TimeZoneResponseData::UNKNOWN_ERROR},
61 {"ZERO_RESULTS", TimeZoneResponseData::ZERO_RESULTS}, };
63 enum TimeZoneRequestEvent {
64 // NOTE: Do not renumber these as that would confuse interpretation of
65 // previously logged data. When making changes, also update the enum list
66 // in tools/metrics/histograms/histograms.xml to keep it in sync.
67 TIMEZONE_REQUEST_EVENT_REQUEST_START = 0,
68 TIMEZONE_REQUEST_EVENT_RESPONSE_SUCCESS = 1,
69 TIMEZONE_REQUEST_EVENT_RESPONSE_NOT_OK = 2,
70 TIMEZONE_REQUEST_EVENT_RESPONSE_EMPTY = 3,
71 TIMEZONE_REQUEST_EVENT_RESPONSE_MALFORMED = 4,
73 // NOTE: Add entries only immediately above this line.
74 TIMEZONE_REQUEST_EVENT_COUNT = 5
77 enum TimeZoneRequestResult {
78 // NOTE: Do not renumber these as that would confuse interpretation of
79 // previously logged data. When making changes, also update the enum list
80 // in tools/metrics/histograms/histograms.xml to keep it in sync.
81 TIMEZONE_REQUEST_RESULT_SUCCESS = 0,
82 TIMEZONE_REQUEST_RESULT_FAILURE = 1,
83 TIMEZONE_REQUEST_RESULT_SERVER_ERROR = 2,
84 TIMEZONE_REQUEST_RESULT_CANCELLED = 3,
86 // NOTE: Add entries only immediately above this line.
87 TIMEZONE_REQUEST_RESULT_COUNT = 4
90 // Too many requests (more than 1) mean there is a problem in implementation.
91 void RecordUmaEvent(TimeZoneRequestEvent event) {
92 UMA_HISTOGRAM_ENUMERATION(
93 "TimeZone.TimeZoneRequest.Event", event, TIMEZONE_REQUEST_EVENT_COUNT);
96 void RecordUmaResponseCode(int code) {
97 UMA_HISTOGRAM_SPARSE_SLOWLY("TimeZone.TimeZoneRequest.ResponseCode", code);
100 // Slow timezone resolve leads to bad user experience.
101 void RecordUmaResponseTime(base::TimeDelta elapsed, bool success) {
102 if (success) {
103 UMA_HISTOGRAM_TIMES("TimeZone.TimeZoneRequest.ResponseSuccessTime",
104 elapsed);
105 } else {
106 UMA_HISTOGRAM_TIMES("TimeZone.TimeZoneRequest.ResponseFailureTime",
107 elapsed);
111 void RecordUmaResult(TimeZoneRequestResult result, unsigned retries) {
112 UMA_HISTOGRAM_ENUMERATION(
113 "TimeZone.TimeZoneRequest.Result", result, TIMEZONE_REQUEST_RESULT_COUNT);
114 UMA_HISTOGRAM_SPARSE_SLOWLY("TimeZone.TimeZoneRequest.Retries", retries);
117 // Creates the request url to send to the server.
118 // |sensor| if this location was determined using hardware sensor.
119 GURL TimeZoneRequestURL(const GURL& url,
120 const Geoposition& geoposition,
121 bool sensor) {
122 std::string query(url.query());
123 query += base::StringPrintf(
124 "%s=%f,%f", kLocationString, geoposition.latitude, geoposition.longitude);
125 if (url == DefaultTimezoneProviderURL()) {
126 std::string api_key = google_apis::GetAPIKey();
127 if (!api_key.empty()) {
128 query += "&";
129 query += kKeyString;
130 query += "=";
131 query += net::EscapeQueryParamValue(api_key, true);
134 if (!geoposition.timestamp.is_null()) {
135 query += base::StringPrintf(
136 "&%s=%ld", kTimestampString, geoposition.timestamp.ToTimeT());
138 query += "&";
139 query += kSensorString;
140 query += "=";
141 query += (sensor ? "true" : "false");
143 GURL::Replacements replacements;
144 replacements.SetQueryStr(query);
145 return url.ReplaceComponents(replacements);
148 void PrintTimeZoneError(const GURL& server_url,
149 const std::string& message,
150 TimeZoneResponseData* timezone) {
151 timezone->status = TimeZoneResponseData::REQUEST_ERROR;
152 timezone->error_message =
153 base::StringPrintf("TimeZone provider at '%s' : %s.",
154 server_url.GetOrigin().spec().c_str(),
155 message.c_str());
156 LOG(WARNING) << "TimeZoneRequest::GetTimeZoneFromResponse() : "
157 << timezone->error_message;
160 // Parses the server response body. Returns true if parsing was successful.
161 // Sets |*timezone| to the parsed TimeZone if a valid timezone was received,
162 // otherwise leaves it unchanged.
163 bool ParseServerResponse(const GURL& server_url,
164 const std::string& response_body,
165 TimeZoneResponseData* timezone) {
166 DCHECK(timezone);
168 if (response_body.empty()) {
169 PrintTimeZoneError(server_url, "Server returned empty response", timezone);
170 RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_EMPTY);
171 return false;
173 VLOG(1) << "TimeZoneRequest::ParseServerResponse() : Parsing response "
174 << response_body;
176 // Parse the response, ignoring comments.
177 std::string error_msg;
178 scoped_ptr<base::Value> response_value = base::JSONReader::ReadAndReturnError(
179 response_body, base::JSON_PARSE_RFC, NULL, &error_msg);
180 if (response_value == NULL) {
181 PrintTimeZoneError(server_url, "JSONReader failed: " + error_msg, timezone);
182 RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_MALFORMED);
183 return false;
186 const base::DictionaryValue* response_object = NULL;
187 if (!response_value->GetAsDictionary(&response_object)) {
188 PrintTimeZoneError(server_url,
189 "Unexpected response type : " +
190 base::StringPrintf("%u", response_value->GetType()),
191 timezone);
192 RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_MALFORMED);
193 return false;
196 std::string status;
198 if (!response_object->GetStringWithoutPathExpansion(kStatusString, &status)) {
199 PrintTimeZoneError(server_url, "Missing status attribute.", timezone);
200 RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_MALFORMED);
201 return false;
204 bool found = false;
205 for (size_t i = 0; i < arraysize(statusString2Enum); ++i) {
206 if (status != statusString2Enum[i].string)
207 continue;
209 timezone->status = statusString2Enum[i].value;
210 found = true;
211 break;
214 if (!found) {
215 PrintTimeZoneError(
216 server_url, "Bad status attribute value: '" + status + "'", timezone);
217 RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_MALFORMED);
218 return false;
221 const bool status_ok = (timezone->status == TimeZoneResponseData::OK);
223 if (!response_object->GetDoubleWithoutPathExpansion(kDstOffsetString,
224 &timezone->dstOffset) &&
225 status_ok) {
226 PrintTimeZoneError(server_url, "Missing dstOffset attribute.", timezone);
227 RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_MALFORMED);
228 return false;
231 if (!response_object->GetDoubleWithoutPathExpansion(kRawOffsetString,
232 &timezone->rawOffset) &&
233 status_ok) {
234 PrintTimeZoneError(server_url, "Missing rawOffset attribute.", timezone);
235 RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_MALFORMED);
236 return false;
239 if (!response_object->GetStringWithoutPathExpansion(kTimeZoneIdString,
240 &timezone->timeZoneId) &&
241 status_ok) {
242 PrintTimeZoneError(server_url, "Missing timeZoneId attribute.", timezone);
243 RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_MALFORMED);
244 return false;
247 if (!response_object->GetStringWithoutPathExpansion(
248 kTimeZoneNameString, &timezone->timeZoneName) &&
249 status_ok) {
250 PrintTimeZoneError(server_url, "Missing timeZoneName attribute.", timezone);
251 RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_MALFORMED);
252 return false;
255 // "error_message" field is optional. Ignore result.
256 response_object->GetStringWithoutPathExpansion(kErrorMessageString,
257 &timezone->error_message);
259 return true;
262 // Attempts to extract a position from the response. Detects and indicates
263 // various failure cases.
264 scoped_ptr<TimeZoneResponseData> GetTimeZoneFromResponse(
265 bool http_success,
266 int status_code,
267 const std::string& response_body,
268 const GURL& server_url) {
269 scoped_ptr<TimeZoneResponseData> timezone(new TimeZoneResponseData);
271 // HttpPost can fail for a number of reasons. Most likely this is because
272 // we're offline, or there was no response.
273 if (!http_success) {
274 PrintTimeZoneError(server_url, "No response received", timezone.get());
275 RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_EMPTY);
276 return timezone.Pass();
278 if (status_code != net::HTTP_OK) {
279 std::string message = "Returned error code ";
280 message += base::IntToString(status_code);
281 PrintTimeZoneError(server_url, message, timezone.get());
282 RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_NOT_OK);
283 return timezone.Pass();
286 if (!ParseServerResponse(server_url, response_body, timezone.get()))
287 return timezone.Pass();
289 RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_SUCCESS);
290 return timezone.Pass();
293 } // namespace
295 TimeZoneResponseData::TimeZoneResponseData()
296 : dstOffset(0), rawOffset(0), status(ZERO_RESULTS) {
299 GURL DefaultTimezoneProviderURL() {
300 return GURL(kDefaultTimezoneProviderUrl);
303 TimeZoneRequest::TimeZoneRequest(
304 net::URLRequestContextGetter* url_context_getter,
305 const GURL& service_url,
306 const Geoposition& geoposition,
307 base::TimeDelta retry_timeout)
308 : url_context_getter_(url_context_getter),
309 service_url_(service_url),
310 geoposition_(geoposition),
311 retry_timeout_abs_(base::Time::Now() + retry_timeout),
312 retry_sleep_on_server_error_(base::TimeDelta::FromSeconds(
313 kResolveTimeZoneRetrySleepOnServerErrorSeconds)),
314 retry_sleep_on_bad_response_(base::TimeDelta::FromSeconds(
315 kResolveTimeZoneRetrySleepBadResponseSeconds)),
316 retries_(0) {
319 TimeZoneRequest::~TimeZoneRequest() {
320 DCHECK(thread_checker_.CalledOnValidThread());
322 // If callback is not empty, request is cancelled.
323 if (!callback_.is_null()) {
324 RecordUmaResponseTime(base::Time::Now() - request_started_at_, false);
325 RecordUmaResult(TIMEZONE_REQUEST_RESULT_CANCELLED, retries_);
329 void TimeZoneRequest::StartRequest() {
330 DCHECK(thread_checker_.CalledOnValidThread());
331 RecordUmaEvent(TIMEZONE_REQUEST_EVENT_REQUEST_START);
332 request_started_at_ = base::Time::Now();
333 ++retries_;
335 url_fetcher_ =
336 net::URLFetcher::Create(request_url_, net::URLFetcher::GET, this);
337 url_fetcher_->SetRequestContext(url_context_getter_.get());
338 url_fetcher_->SetLoadFlags(net::LOAD_BYPASS_CACHE |
339 net::LOAD_DISABLE_CACHE |
340 net::LOAD_DO_NOT_SAVE_COOKIES |
341 net::LOAD_DO_NOT_SEND_COOKIES |
342 net::LOAD_DO_NOT_SEND_AUTH_DATA);
343 url_fetcher_->Start();
346 void TimeZoneRequest::MakeRequest(TimeZoneResponseCallback callback) {
347 callback_ = callback;
348 request_url_ =
349 TimeZoneRequestURL(service_url_, geoposition_, false /* sensor */);
350 StartRequest();
353 void TimeZoneRequest::Retry(bool server_error) {
354 const base::TimeDelta delay(server_error ? retry_sleep_on_server_error_
355 : retry_sleep_on_bad_response_);
356 timezone_request_scheduled_.Start(
357 FROM_HERE, delay, this, &TimeZoneRequest::StartRequest);
360 void TimeZoneRequest::OnURLFetchComplete(const net::URLFetcher* source) {
361 DCHECK_EQ(url_fetcher_.get(), source);
363 net::URLRequestStatus status = source->GetStatus();
364 int response_code = source->GetResponseCode();
365 RecordUmaResponseCode(response_code);
367 std::string data;
368 source->GetResponseAsString(&data);
369 scoped_ptr<TimeZoneResponseData> timezone = GetTimeZoneFromResponse(
370 status.is_success(), response_code, data, source->GetURL());
371 const bool server_error =
372 !status.is_success() || (response_code >= 500 && response_code < 600);
373 url_fetcher_.reset();
375 DVLOG(1) << "TimeZoneRequest::OnURLFetchComplete(): timezone={"
376 << timezone->ToStringForDebug() << "}";
378 const base::Time now = base::Time::Now();
379 const bool retry_timeout = (now >= retry_timeout_abs_);
381 const bool success = (timezone->status == TimeZoneResponseData::OK);
382 if (!success && !retry_timeout) {
383 Retry(server_error);
384 return;
386 RecordUmaResponseTime(base::Time::Now() - request_started_at_, success);
388 const TimeZoneRequestResult result =
389 (server_error ? TIMEZONE_REQUEST_RESULT_SERVER_ERROR
390 : (success ? TIMEZONE_REQUEST_RESULT_SUCCESS
391 : TIMEZONE_REQUEST_RESULT_FAILURE));
392 RecordUmaResult(result, retries_);
394 TimeZoneResponseCallback callback = callback_;
396 // Empty callback is used to identify "completed or not yet started request".
397 callback_.Reset();
399 // callback.Run() usually destroys TimeZoneRequest, because this is the way
400 // callback is implemented in TimeZoneProvider.
401 callback.Run(timezone.Pass(), server_error);
402 // "this" is already destroyed here.
405 std::string TimeZoneResponseData::ToStringForDebug() const {
406 static const char* const status2string[] = {
407 "OK",
408 "INVALID_REQUEST",
409 "OVER_QUERY_LIMIT",
410 "REQUEST_DENIED",
411 "UNKNOWN_ERROR",
412 "ZERO_RESULTS",
413 "REQUEST_ERROR"
416 return base::StringPrintf(
417 "dstOffset=%f, rawOffset=%f, timeZoneId='%s', timeZoneName='%s', "
418 "error_message='%s', status=%u (%s)",
419 dstOffset,
420 rawOffset,
421 timeZoneId.c_str(),
422 timeZoneName.c_str(),
423 error_message.c_str(),
424 (unsigned)status,
425 (status < arraysize(status2string) ? status2string[status] : "unknown"));
428 } // namespace chromeos