summaryrefslogtreecommitdiffstats
path: root/chromeos/timezone
diff options
context:
space:
mode:
authorsatorux <satorux@chromium.org>2014-12-16 00:29:53 -0800
committerCommit bot <commit-bot@chromium.org>2014-12-16 08:30:32 +0000
commit07e69091e58db4faf9aacf6ca6a444048469694d (patch)
tree6504c786455a6fc3938fbf661e1e19ef61913720 /chromeos/timezone
parent51ca24b5b340648640e339379f290c7ddfd22516 (diff)
downloadchromium_src-07e69091e58db4faf9aacf6ca6a444048469694d.zip
chromium_src-07e69091e58db4faf9aacf6ca6a444048469694d.tar.gz
chromium_src-07e69091e58db4faf9aacf6ca6a444048469694d.tar.bz2
Move chrome/browser/chromeos/timezone to chromeos/timezone
In favor of less things to have in chrome/browser/chromeos. BUG=437703 TEST=everything builds as before Review URL: https://codereview.chromium.org/801533005 Cr-Commit-Position: refs/heads/master@{#308543}
Diffstat (limited to 'chromeos/timezone')
-rw-r--r--chromeos/timezone/DEPS3
-rw-r--r--chromeos/timezone/timezone_provider.cc61
-rw-r--r--chromeos/timezone/timezone_provider.h66
-rw-r--r--chromeos/timezone/timezone_request.cc431
-rw-r--r--chromeos/timezone/timezone_request.h141
-rw-r--r--chromeos/timezone/timezone_unittest.cc290
6 files changed, 992 insertions, 0 deletions
diff --git a/chromeos/timezone/DEPS b/chromeos/timezone/DEPS
new file mode 100644
index 0000000..9a26362
--- /dev/null
+++ b/chromeos/timezone/DEPS
@@ -0,0 +1,3 @@
+include_rules = [
+ "+google_apis",
+]
diff --git a/chromeos/timezone/timezone_provider.cc b/chromeos/timezone/timezone_provider.cc
new file mode 100644
index 0000000..d252fa6
--- /dev/null
+++ b/chromeos/timezone/timezone_provider.cc
@@ -0,0 +1,61 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chromeos/timezone/timezone_provider.h"
+
+#include <algorithm>
+#include <iterator>
+
+#include "base/bind.h"
+#include "base/logging.h"
+#include "base/time/time.h"
+#include "chromeos/geolocation/geoposition.h"
+#include "net/url_request/url_request_context_getter.h"
+#include "url/gurl.h"
+
+namespace chromeos {
+
+TimeZoneProvider::TimeZoneProvider(
+ net::URLRequestContextGetter* url_context_getter,
+ const GURL& url)
+ : url_context_getter_(url_context_getter), url_(url) {
+}
+
+TimeZoneProvider::~TimeZoneProvider() {
+ DCHECK(thread_checker_.CalledOnValidThread());
+}
+
+void TimeZoneProvider::RequestTimezone(
+ const Geoposition& position,
+ bool sensor,
+ base::TimeDelta timeout,
+ TimeZoneRequest::TimeZoneResponseCallback callback) {
+ TimeZoneRequest* request(new TimeZoneRequest(
+ url_context_getter_.get(), url_, position, sensor, timeout));
+ requests_.push_back(request);
+
+ // TimeZoneProvider owns all requests. It is safe to pass unretained "this"
+ // because destruction of TimeZoneProvider cancels all requests.
+ TimeZoneRequest::TimeZoneResponseCallback callback_tmp(
+ base::Bind(&TimeZoneProvider::OnTimezoneResponse,
+ base::Unretained(this),
+ request,
+ callback));
+ request->MakeRequest(callback_tmp);
+}
+
+void TimeZoneProvider::OnTimezoneResponse(
+ TimeZoneRequest* request,
+ TimeZoneRequest::TimeZoneResponseCallback callback,
+ scoped_ptr<TimeZoneResponseData> timezone,
+ bool server_error) {
+ ScopedVector<TimeZoneRequest>::iterator new_end =
+ std::remove(requests_.begin(), requests_.end(), request);
+ DCHECK_EQ(std::distance(new_end, requests_.end()), 1);
+ requests_.erase(new_end, requests_.end());
+
+ callback.Run(timezone.Pass(), server_error);
+}
+
+} // namespace chromeos
diff --git a/chromeos/timezone/timezone_provider.h b/chromeos/timezone/timezone_provider.h
new file mode 100644
index 0000000..616ff96
--- /dev/null
+++ b/chromeos/timezone/timezone_provider.h
@@ -0,0 +1,66 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROMEOS_TIMEZONE_TIMEZONE_PROVIDER_H_
+#define CHROMEOS_TIMEZONE_TIMEZONE_PROVIDER_H_
+
+#include "base/macros.h"
+#include "base/memory/ref_counted.h"
+#include "base/memory/scoped_ptr.h"
+#include "base/memory/scoped_vector.h"
+#include "base/memory/weak_ptr.h"
+#include "base/threading/thread_checker.h"
+#include "base/time/time.h"
+#include "chromeos/timezone/timezone_request.h"
+#include "url/gurl.h"
+
+namespace net {
+class URLRequestContextGetter;
+}
+
+namespace chromeos {
+
+struct Geoposition;
+
+// This class implements Google TimeZone API.
+//
+// Note: this should probably be a singleton to monitor requests rate.
+// But as it is used only from WizardController, it can be owned by it for now.
+class CHROMEOS_EXPORT TimeZoneProvider {
+ public:
+ TimeZoneProvider(net::URLRequestContextGetter* url_context_getter,
+ const GURL& url);
+ virtual ~TimeZoneProvider();
+
+ // Initiates new request (See TimeZoneRequest for parameters description.)
+ void RequestTimezone(const Geoposition& position,
+ bool sensor,
+ base::TimeDelta timeout,
+ TimeZoneRequest::TimeZoneResponseCallback callback);
+
+ private:
+ friend class TestTimeZoneAPIURLFetcherCallback;
+
+ // Deletes request from requests_.
+ void OnTimezoneResponse(TimeZoneRequest* request,
+ TimeZoneRequest::TimeZoneResponseCallback callback,
+ scoped_ptr<TimeZoneResponseData> timezone,
+ bool server_error);
+
+ scoped_refptr<net::URLRequestContextGetter> url_context_getter_;
+ const GURL url_;
+
+ // Requests in progress.
+ // TimeZoneProvider owns all requests, so this vector is deleted on destroy.
+ ScopedVector<TimeZoneRequest> requests_;
+
+ // Creation and destruction should happen on the same thread.
+ base::ThreadChecker thread_checker_;
+
+ DISALLOW_COPY_AND_ASSIGN(TimeZoneProvider);
+};
+
+} // namespace chromeos
+
+#endif // CHROMEOS_TIMEZONE_TIMEZONE_PROVIDER_H_
diff --git a/chromeos/timezone/timezone_request.cc b/chromeos/timezone/timezone_request.cc
new file mode 100644
index 0000000..c39de0b
--- /dev/null
+++ b/chromeos/timezone/timezone_request.cc
@@ -0,0 +1,431 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chromeos/timezone/timezone_request.h"
+
+#include <string>
+
+#include "base/json/json_reader.h"
+#include "base/metrics/histogram.h"
+#include "base/metrics/sparse_histogram.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/stringprintf.h"
+#include "base/time/time.h"
+#include "base/values.h"
+#include "chromeos/geolocation/geoposition.h"
+#include "google_apis/google_api_keys.h"
+#include "net/base/escape.h"
+#include "net/base/load_flags.h"
+#include "net/http/http_status_code.h"
+#include "net/url_request/url_fetcher.h"
+#include "net/url_request/url_request_context_getter.h"
+#include "net/url_request/url_request_status.h"
+
+namespace chromeos {
+
+namespace {
+
+const char kDefaultTimezoneProviderUrl[] =
+ "https://maps.googleapis.com/maps/api/timezone/json?";
+
+const char kKeyString[] = "key";
+// Language parameter is unsupported for now.
+// const char kLanguageString[] = "language";
+const char kLocationString[] = "location";
+const char kSensorString[] = "sensor";
+const char kTimestampString[] = "timestamp";
+
+const char kDstOffsetString[] = "dstOffset";
+const char kRawOffsetString[] = "rawOffset";
+const char kTimeZoneIdString[] = "timeZoneId";
+const char kTimeZoneNameString[] = "timeZoneName";
+const char kStatusString[] = "status";
+const char kErrorMessageString[] = "error_message";
+
+// Sleep between timezone request retry on HTTP error.
+const unsigned int kResolveTimeZoneRetrySleepOnServerErrorSeconds = 5;
+
+// Sleep between timezone request retry on bad server response.
+const unsigned int kResolveTimeZoneRetrySleepBadResponseSeconds = 10;
+
+struct StatusString2Enum {
+ const char* string;
+ TimeZoneResponseData::Status value;
+};
+
+const StatusString2Enum statusString2Enum[] = {
+ {"OK", TimeZoneResponseData::OK},
+ {"INVALID_REQUEST", TimeZoneResponseData::INVALID_REQUEST},
+ {"OVER_QUERY_LIMIT", TimeZoneResponseData::OVER_QUERY_LIMIT},
+ {"REQUEST_DENIED", TimeZoneResponseData::REQUEST_DENIED},
+ {"UNKNOWN_ERROR", TimeZoneResponseData::UNKNOWN_ERROR},
+ {"ZERO_RESULTS", TimeZoneResponseData::ZERO_RESULTS}, };
+
+enum TimeZoneRequestEvent {
+ // NOTE: Do not renumber these as that would confuse interpretation of
+ // previously logged data. When making changes, also update the enum list
+ // in tools/metrics/histograms/histograms.xml to keep it in sync.
+ TIMEZONE_REQUEST_EVENT_REQUEST_START = 0,
+ TIMEZONE_REQUEST_EVENT_RESPONSE_SUCCESS = 1,
+ TIMEZONE_REQUEST_EVENT_RESPONSE_NOT_OK = 2,
+ TIMEZONE_REQUEST_EVENT_RESPONSE_EMPTY = 3,
+ TIMEZONE_REQUEST_EVENT_RESPONSE_MALFORMED = 4,
+
+ // NOTE: Add entries only immediately above this line.
+ TIMEZONE_REQUEST_EVENT_COUNT = 5
+};
+
+enum TimeZoneRequestResult {
+ // NOTE: Do not renumber these as that would confuse interpretation of
+ // previously logged data. When making changes, also update the enum list
+ // in tools/metrics/histograms/histograms.xml to keep it in sync.
+ TIMEZONE_REQUEST_RESULT_SUCCESS = 0,
+ TIMEZONE_REQUEST_RESULT_FAILURE = 1,
+ TIMEZONE_REQUEST_RESULT_SERVER_ERROR = 2,
+ TIMEZONE_REQUEST_RESULT_CANCELLED = 3,
+
+ // NOTE: Add entries only immediately above this line.
+ TIMEZONE_REQUEST_RESULT_COUNT = 4
+};
+
+// Too many requests (more than 1) mean there is a problem in implementation.
+void RecordUmaEvent(TimeZoneRequestEvent event) {
+ UMA_HISTOGRAM_ENUMERATION(
+ "TimeZone.TimeZoneRequest.Event", event, TIMEZONE_REQUEST_EVENT_COUNT);
+}
+
+void RecordUmaResponseCode(int code) {
+ UMA_HISTOGRAM_SPARSE_SLOWLY("TimeZone.TimeZoneRequest.ResponseCode", code);
+}
+
+// Slow timezone resolve leads to bad user experience.
+void RecordUmaResponseTime(base::TimeDelta elapsed, bool success) {
+ if (success) {
+ UMA_HISTOGRAM_TIMES("TimeZone.TimeZoneRequest.ResponseSuccessTime",
+ elapsed);
+ } else {
+ UMA_HISTOGRAM_TIMES("TimeZone.TimeZoneRequest.ResponseFailureTime",
+ elapsed);
+ }
+}
+
+void RecordUmaResult(TimeZoneRequestResult result, unsigned retries) {
+ UMA_HISTOGRAM_ENUMERATION(
+ "TimeZone.TimeZoneRequest.Result", result, TIMEZONE_REQUEST_RESULT_COUNT);
+ UMA_HISTOGRAM_SPARSE_SLOWLY("TimeZone.TimeZoneRequest.Retries", retries);
+}
+
+// Creates the request url to send to the server.
+GURL TimeZoneRequestURL(const GURL& url,
+ const Geoposition& geoposition,
+ bool sensor) {
+ std::string query(url.query());
+ query += base::StringPrintf(
+ "%s=%f,%f", kLocationString, geoposition.latitude, geoposition.longitude);
+ if (url == DefaultTimezoneProviderURL()) {
+ std::string api_key = google_apis::GetAPIKey();
+ if (!api_key.empty()) {
+ query += "&";
+ query += kKeyString;
+ query += "=";
+ query += net::EscapeQueryParamValue(api_key, true);
+ }
+ }
+ if (!geoposition.timestamp.is_null()) {
+ query += base::StringPrintf(
+ "&%s=%ld", kTimestampString, geoposition.timestamp.ToTimeT());
+ }
+ query += "&";
+ query += kSensorString;
+ query += "=";
+ query += (sensor ? "true" : "false");
+
+ GURL::Replacements replacements;
+ replacements.SetQueryStr(query);
+ return url.ReplaceComponents(replacements);
+}
+
+void PrintTimeZoneError(const GURL& server_url,
+ const std::string& message,
+ TimeZoneResponseData* timezone) {
+ timezone->status = TimeZoneResponseData::REQUEST_ERROR;
+ timezone->error_message =
+ base::StringPrintf("TimeZone provider at '%s' : %s.",
+ server_url.GetOrigin().spec().c_str(),
+ message.c_str());
+ LOG(WARNING) << "TimeZoneRequest::GetTimeZoneFromResponse() : "
+ << timezone->error_message;
+}
+
+// Parses the server response body. Returns true if parsing was successful.
+// Sets |*timezone| to the parsed TimeZone if a valid timezone was received,
+// otherwise leaves it unchanged.
+bool ParseServerResponse(const GURL& server_url,
+ const std::string& response_body,
+ TimeZoneResponseData* timezone) {
+ DCHECK(timezone);
+
+ if (response_body.empty()) {
+ PrintTimeZoneError(server_url, "Server returned empty response", timezone);
+ RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_EMPTY);
+ return false;
+ }
+ VLOG(1) << "TimeZoneRequest::ParseServerResponse() : Parsing response "
+ << response_body;
+
+ // Parse the response, ignoring comments.
+ std::string error_msg;
+ scoped_ptr<base::Value> response_value(base::JSONReader::ReadAndReturnError(
+ response_body, base::JSON_PARSE_RFC, NULL, &error_msg));
+ if (response_value == NULL) {
+ PrintTimeZoneError(server_url, "JSONReader failed: " + error_msg, timezone);
+ RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_MALFORMED);
+ return false;
+ }
+
+ const base::DictionaryValue* response_object = NULL;
+ if (!response_value->GetAsDictionary(&response_object)) {
+ PrintTimeZoneError(server_url,
+ "Unexpected response type : " +
+ base::StringPrintf("%u", response_value->GetType()),
+ timezone);
+ RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_MALFORMED);
+ return false;
+ }
+
+ std::string status;
+
+ if (!response_object->GetStringWithoutPathExpansion(kStatusString, &status)) {
+ PrintTimeZoneError(server_url, "Missing status attribute.", timezone);
+ RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_MALFORMED);
+ return false;
+ }
+
+ bool found = false;
+ for (size_t i = 0; i < arraysize(statusString2Enum); ++i) {
+ if (status != statusString2Enum[i].string)
+ continue;
+
+ timezone->status = statusString2Enum[i].value;
+ found = true;
+ break;
+ }
+
+ if (!found) {
+ PrintTimeZoneError(
+ server_url, "Bad status attribute value: '" + status + "'", timezone);
+ RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_MALFORMED);
+ return false;
+ }
+
+ const bool status_ok = (timezone->status == TimeZoneResponseData::OK);
+
+ if (!response_object->GetDoubleWithoutPathExpansion(kDstOffsetString,
+ &timezone->dstOffset) &&
+ status_ok) {
+ PrintTimeZoneError(server_url, "Missing dstOffset attribute.", timezone);
+ RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_MALFORMED);
+ return false;
+ }
+
+ if (!response_object->GetDoubleWithoutPathExpansion(kRawOffsetString,
+ &timezone->rawOffset) &&
+ status_ok) {
+ PrintTimeZoneError(server_url, "Missing rawOffset attribute.", timezone);
+ RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_MALFORMED);
+ return false;
+ }
+
+ if (!response_object->GetStringWithoutPathExpansion(kTimeZoneIdString,
+ &timezone->timeZoneId) &&
+ status_ok) {
+ PrintTimeZoneError(server_url, "Missing timeZoneId attribute.", timezone);
+ RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_MALFORMED);
+ return false;
+ }
+
+ if (!response_object->GetStringWithoutPathExpansion(
+ kTimeZoneNameString, &timezone->timeZoneName) &&
+ status_ok) {
+ PrintTimeZoneError(server_url, "Missing timeZoneName attribute.", timezone);
+ RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_MALFORMED);
+ return false;
+ }
+
+ // "error_message" field is optional. Ignore result.
+ response_object->GetStringWithoutPathExpansion(kErrorMessageString,
+ &timezone->error_message);
+
+ return true;
+}
+
+// Attempts to extract a position from the response. Detects and indicates
+// various failure cases.
+scoped_ptr<TimeZoneResponseData> GetTimeZoneFromResponse(
+ bool http_success,
+ int status_code,
+ const std::string& response_body,
+ const GURL& server_url) {
+ scoped_ptr<TimeZoneResponseData> timezone(new TimeZoneResponseData);
+
+ // HttpPost can fail for a number of reasons. Most likely this is because
+ // we're offline, or there was no response.
+ if (!http_success) {
+ PrintTimeZoneError(server_url, "No response received", timezone.get());
+ RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_EMPTY);
+ return timezone.Pass();
+ }
+ if (status_code != net::HTTP_OK) {
+ std::string message = "Returned error code ";
+ message += base::IntToString(status_code);
+ PrintTimeZoneError(server_url, message, timezone.get());
+ RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_NOT_OK);
+ return timezone.Pass();
+ }
+
+ if (!ParseServerResponse(server_url, response_body, timezone.get()))
+ return timezone.Pass();
+
+ RecordUmaEvent(TIMEZONE_REQUEST_EVENT_RESPONSE_SUCCESS);
+ return timezone.Pass();
+}
+
+} // namespace
+
+TimeZoneResponseData::TimeZoneResponseData()
+ : dstOffset(0), rawOffset(0), status(ZERO_RESULTS) {
+}
+
+GURL DefaultTimezoneProviderURL() {
+ return GURL(kDefaultTimezoneProviderUrl);
+}
+
+TimeZoneRequest::TimeZoneRequest(
+ net::URLRequestContextGetter* url_context_getter,
+ const GURL& service_url,
+ const Geoposition& geoposition,
+ bool sensor,
+ base::TimeDelta retry_timeout)
+ : url_context_getter_(url_context_getter),
+ service_url_(service_url),
+ geoposition_(geoposition),
+ sensor_(sensor),
+ retry_timeout_abs_(base::Time::Now() + retry_timeout),
+ retry_sleep_on_server_error_(base::TimeDelta::FromSeconds(
+ kResolveTimeZoneRetrySleepOnServerErrorSeconds)),
+ retry_sleep_on_bad_response_(base::TimeDelta::FromSeconds(
+ kResolveTimeZoneRetrySleepBadResponseSeconds)),
+ retries_(0) {
+}
+
+TimeZoneRequest::~TimeZoneRequest() {
+ DCHECK(thread_checker_.CalledOnValidThread());
+
+ // If callback is not empty, request is cancelled.
+ if (!callback_.is_null()) {
+ RecordUmaResponseTime(base::Time::Now() - request_started_at_, false);
+ RecordUmaResult(TIMEZONE_REQUEST_RESULT_CANCELLED, retries_);
+ }
+}
+
+void TimeZoneRequest::StartRequest() {
+ DCHECK(thread_checker_.CalledOnValidThread());
+ RecordUmaEvent(TIMEZONE_REQUEST_EVENT_REQUEST_START);
+ request_started_at_ = base::Time::Now();
+ ++retries_;
+
+ url_fetcher_.reset(
+ net::URLFetcher::Create(request_url_, net::URLFetcher::GET, this));
+ url_fetcher_->SetRequestContext(url_context_getter_.get());
+ url_fetcher_->SetLoadFlags(net::LOAD_BYPASS_CACHE |
+ net::LOAD_DISABLE_CACHE |
+ net::LOAD_DO_NOT_SAVE_COOKIES |
+ net::LOAD_DO_NOT_SEND_COOKIES |
+ net::LOAD_DO_NOT_SEND_AUTH_DATA);
+ url_fetcher_->Start();
+}
+
+void TimeZoneRequest::MakeRequest(TimeZoneResponseCallback callback) {
+ callback_ = callback;
+ request_url_ =
+ TimeZoneRequestURL(service_url_, geoposition_, false /* sensor */);
+ StartRequest();
+}
+
+void TimeZoneRequest::Retry(bool server_error) {
+ const base::TimeDelta delay(server_error ? retry_sleep_on_server_error_
+ : retry_sleep_on_bad_response_);
+ timezone_request_scheduled_.Start(
+ FROM_HERE, delay, this, &TimeZoneRequest::StartRequest);
+}
+
+void TimeZoneRequest::OnURLFetchComplete(const net::URLFetcher* source) {
+ DCHECK_EQ(url_fetcher_.get(), source);
+
+ net::URLRequestStatus status = source->GetStatus();
+ int response_code = source->GetResponseCode();
+ RecordUmaResponseCode(response_code);
+
+ std::string data;
+ source->GetResponseAsString(&data);
+ scoped_ptr<TimeZoneResponseData> timezone = GetTimeZoneFromResponse(
+ status.is_success(), response_code, data, source->GetURL());
+ const bool server_error =
+ !status.is_success() || (response_code >= 500 && response_code < 600);
+ url_fetcher_.reset();
+
+ DVLOG(1) << "TimeZoneRequest::OnURLFetchComplete(): timezone={"
+ << timezone->ToStringForDebug() << "}";
+
+ const base::Time now = base::Time::Now();
+ const bool retry_timeout = (now >= retry_timeout_abs_);
+
+ const bool success = (timezone->status == TimeZoneResponseData::OK);
+ if (!success && !retry_timeout) {
+ Retry(server_error);
+ return;
+ }
+ RecordUmaResponseTime(base::Time::Now() - request_started_at_, success);
+
+ const TimeZoneRequestResult result =
+ (server_error ? TIMEZONE_REQUEST_RESULT_SERVER_ERROR
+ : (success ? TIMEZONE_REQUEST_RESULT_SUCCESS
+ : TIMEZONE_REQUEST_RESULT_FAILURE));
+ RecordUmaResult(result, retries_);
+
+ TimeZoneResponseCallback callback = callback_;
+
+ // Empty callback is used to identify "completed or not yet started request".
+ callback_.Reset();
+
+ // callback.Run() usually destroys TimeZoneRequest, because this is the way
+ // callback is implemented in TimeZoneProvider.
+ callback.Run(timezone.Pass(), server_error);
+ // "this" is already destroyed here.
+}
+
+std::string TimeZoneResponseData::ToStringForDebug() const {
+ static const char* const status2string[] = {
+ "OK",
+ "INVALID_REQUEST",
+ "OVER_QUERY_LIMIT",
+ "REQUEST_DENIED",
+ "UNKNOWN_ERROR",
+ "ZERO_RESULTS",
+ "REQUEST_ERROR"
+ };
+
+ return base::StringPrintf(
+ "dstOffset=%f, rawOffset=%f, timeZoneId='%s', timeZoneName='%s', "
+ "error_message='%s', status=%u (%s)",
+ dstOffset,
+ rawOffset,
+ timeZoneId.c_str(),
+ timeZoneName.c_str(),
+ error_message.c_str(),
+ (unsigned)status,
+ (status < arraysize(status2string) ? status2string[status] : "unknown"));
+}
+
+} // namespace chromeos
diff --git a/chromeos/timezone/timezone_request.h b/chromeos/timezone/timezone_request.h
new file mode 100644
index 0000000..66af10c
--- /dev/null
+++ b/chromeos/timezone/timezone_request.h
@@ -0,0 +1,141 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROMEOS_TIMEZONE_TIMEZONE_REQUEST_H_
+#define CHROMEOS_TIMEZONE_TIMEZONE_REQUEST_H_
+
+#include "base/basictypes.h"
+#include "base/callback.h"
+#include "base/compiler_specific.h"
+#include "base/memory/ref_counted.h"
+#include "base/memory/scoped_ptr.h"
+#include "base/threading/thread_checker.h"
+#include "base/timer/timer.h"
+#include "chromeos/chromeos_export.h"
+#include "chromeos/geolocation/geoposition.h"
+#include "net/url_request/url_fetcher.h"
+#include "net/url_request/url_fetcher_delegate.h"
+#include "url/gurl.h"
+
+namespace net {
+class URLRequestContextGetter;
+}
+
+namespace chromeos {
+
+struct CHROMEOS_EXPORT TimeZoneResponseData {
+ enum Status {
+ OK,
+ INVALID_REQUEST,
+ OVER_QUERY_LIMIT,
+ REQUEST_DENIED,
+ UNKNOWN_ERROR,
+ ZERO_RESULTS,
+ REQUEST_ERROR // local problem
+ };
+
+ TimeZoneResponseData();
+
+ std::string ToStringForDebug() const;
+
+ double dstOffset;
+ double rawOffset;
+ std::string timeZoneId;
+ std::string timeZoneName;
+ std::string error_message;
+ Status status;
+};
+
+// Returns default timezone service URL.
+CHROMEOS_EXPORT GURL DefaultTimezoneProviderURL();
+
+// Takes Geoposition and sends it to a server to get local timezone information.
+// It performs formatting of the request and interpretation of the response.
+// If error occurs, request is retried until timeout.
+// Zero timeout indicates single request.
+// Request is owned and destroyed by caller (usually TimeZoneProvider).
+// If request is destroyed while callback has not beed called yet, request
+// is silently cancelled.
+class CHROMEOS_EXPORT TimeZoneRequest : private net::URLFetcherDelegate {
+ public:
+ // Called when a new geo timezone information is available.
+ // The second argument indicates whether there was a server error or not.
+ // It is true when there was a server or network error - either no response
+ // or a 500 error code.
+ typedef base::Callback<void(scoped_ptr<TimeZoneResponseData> /* timezone */,
+ bool /* server_error */)>
+ TimeZoneResponseCallback;
+
+ // |url| is the server address to which the request wil be sent.
+ // |geoposition| is the location to query timezone for.
+ // |sensor| if this location was determined using hardware sensor.
+ // |retry_timeout| retry request on error until timeout.
+ TimeZoneRequest(net::URLRequestContextGetter* url_context_getter,
+ const GURL& service_url,
+ const Geoposition& geoposition,
+ bool sensor,
+ base::TimeDelta retry_timeout);
+
+ virtual ~TimeZoneRequest();
+
+ // Initiates request.
+ // Note: if request object is destroyed before callback is called,
+ // request will be silently cancelled.
+ void MakeRequest(TimeZoneResponseCallback callback);
+
+ void set_retry_sleep_on_server_error_for_testing(
+ const base::TimeDelta value) {
+ retry_sleep_on_server_error_ = value;
+ }
+
+ void set_retry_sleep_on_bad_response_for_testing(
+ const base::TimeDelta value) {
+ retry_sleep_on_bad_response_ = value;
+ }
+
+ private:
+ // net::URLFetcherDelegate
+ virtual void OnURLFetchComplete(const net::URLFetcher* source) override;
+
+ // Start new request.
+ void StartRequest();
+
+ // Schedules retry.
+ void Retry(bool server_error);
+
+ scoped_refptr<net::URLRequestContextGetter> url_context_getter_;
+ const GURL service_url_;
+ Geoposition geoposition_;
+ const bool sensor_;
+
+ TimeZoneResponseCallback callback_;
+
+ GURL request_url_;
+ scoped_ptr<net::URLFetcher> url_fetcher_;
+
+ // When request was actually started.
+ base::Time request_started_at_;
+
+ // Absolute time, when it is passed no more retry requests are allowed.
+ base::Time retry_timeout_abs_;
+
+ // Pending retry.
+ base::OneShotTimer<TimeZoneRequest> timezone_request_scheduled_;
+
+ base::TimeDelta retry_sleep_on_server_error_;
+
+ base::TimeDelta retry_sleep_on_bad_response_;
+
+ // Number of retry attempts.
+ unsigned retries_;
+
+ // Creation and destruction should happen on the same thread.
+ base::ThreadChecker thread_checker_;
+
+ DISALLOW_COPY_AND_ASSIGN(TimeZoneRequest);
+};
+
+} // namespace chromeos
+
+#endif // CHROMEOS_TIMEZONE_TIMEZONE_REQUEST_H_
diff --git a/chromeos/timezone/timezone_unittest.cc b/chromeos/timezone/timezone_unittest.cc
new file mode 100644
index 0000000..3987313
--- /dev/null
+++ b/chromeos/timezone/timezone_unittest.cc
@@ -0,0 +1,290 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/message_loop/message_loop.h"
+#include "base/run_loop.h"
+#include "chromeos/geolocation/geoposition.h"
+#include "chromeos/timezone/timezone_provider.h"
+#include "net/http/http_response_headers.h"
+#include "net/http/http_status_code.h"
+#include "net/url_request/test_url_fetcher_factory.h"
+#include "net/url_request/url_fetcher_impl.h"
+#include "net/url_request/url_request_status.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace {
+
+const int kRequestRetryIntervalMilliSeconds = 200;
+
+// This should be different from default to prevent TimeZoneRequest
+// from modifying it.
+const char kTestTimeZoneProviderUrl[] =
+ "https://localhost/maps/api/timezone/json?";
+
+const char kSimpleResponseBody[] =
+ "{\n"
+ " \"dstOffset\" : 0.0,\n"
+ " \"rawOffset\" : -28800.0,\n"
+ " \"status\" : \"OK\",\n"
+ " \"timeZoneId\" : \"America/Los_Angeles\",\n"
+ " \"timeZoneName\" : \"Pacific Standard Time\"\n"
+ "}";
+
+struct SimpleRequest {
+ SimpleRequest()
+ : url("https://localhost/maps/api/timezone/"
+ "json?location=39.603481,-119.682251&timestamp=1331161200&sensor="
+ "false"),
+ http_response(kSimpleResponseBody) {
+ position.latitude = 39.6034810;
+ position.longitude = -119.6822510;
+ position.accuracy = 1;
+ position.error_code = 0;
+ position.timestamp = base::Time::FromTimeT(1331161200);
+ position.status = chromeos::Geoposition::STATUS_NONE;
+ EXPECT_EQ(
+ "latitude=39.603481, longitude=-119.682251, accuracy=1.000000, "
+ "error_code=0, error_message='', status=0 (NONE)",
+ position.ToString());
+
+ timezone.dstOffset = 0;
+ timezone.rawOffset = -28800;
+ timezone.timeZoneId = "America/Los_Angeles";
+ timezone.timeZoneName = "Pacific Standard Time";
+ timezone.error_message.erase();
+ timezone.status = chromeos::TimeZoneResponseData::OK;
+ EXPECT_EQ(
+ "dstOffset=0.000000, rawOffset=-28800.000000, "
+ "timeZoneId='America/Los_Angeles', timeZoneName='Pacific Standard "
+ "Time', error_message='', status=0 (OK)",
+ timezone.ToStringForDebug());
+ }
+
+ GURL url;
+ chromeos::Geoposition position;
+ std::string http_response;
+ chromeos::TimeZoneResponseData timezone;
+};
+
+} // anonymous namespace
+
+namespace chromeos {
+
+// This is helper class for net::FakeURLFetcherFactory.
+class TestTimeZoneAPIURLFetcherCallback {
+ public:
+ TestTimeZoneAPIURLFetcherCallback(const GURL& url,
+ const size_t require_retries,
+ const std::string& response,
+ TimeZoneProvider* provider)
+ : url_(url),
+ require_retries_(require_retries),
+ response_(response),
+ factory_(NULL),
+ attempts_(0),
+ provider_(provider) {}
+
+ scoped_ptr<net::FakeURLFetcher> CreateURLFetcher(
+ const GURL& url,
+ net::URLFetcherDelegate* delegate,
+ const std::string& response_data,
+ net::HttpStatusCode response_code,
+ net::URLRequestStatus::Status status) {
+ EXPECT_EQ(provider_->requests_.size(), 1U);
+
+ TimeZoneRequest* timezone_request = provider_->requests_[0];
+
+ const base::TimeDelta base_retry_interval =
+ base::TimeDelta::FromMilliseconds(kRequestRetryIntervalMilliSeconds);
+ timezone_request->set_retry_sleep_on_server_error_for_testing(
+ base_retry_interval);
+ timezone_request->set_retry_sleep_on_bad_response_for_testing(
+ base_retry_interval);
+
+ ++attempts_;
+ if (attempts_ > require_retries_) {
+ response_code = net::HTTP_OK;
+ status = net::URLRequestStatus::SUCCESS;
+ factory_->SetFakeResponse(url, response_, response_code, status);
+ }
+ scoped_ptr<net::FakeURLFetcher> fetcher(new net::FakeURLFetcher(
+ url, delegate, response_, response_code, status));
+ scoped_refptr<net::HttpResponseHeaders> download_headers =
+ new net::HttpResponseHeaders(std::string());
+ download_headers->AddHeader("Content-Type: application/json");
+ fetcher->set_response_headers(download_headers);
+ return fetcher.Pass();
+ }
+
+ void Initialize(net::FakeURLFetcherFactory* factory) {
+ factory_ = factory;
+ factory_->SetFakeResponse(url_,
+ std::string(),
+ net::HTTP_INTERNAL_SERVER_ERROR,
+ net::URLRequestStatus::FAILED);
+ }
+
+ size_t attempts() const { return attempts_; }
+
+ private:
+ const GURL url_;
+ // Respond with OK on required retry attempt.
+ const size_t require_retries_;
+ std::string response_;
+ net::FakeURLFetcherFactory* factory_;
+ size_t attempts_;
+ TimeZoneProvider* provider_;
+
+ DISALLOW_COPY_AND_ASSIGN(TestTimeZoneAPIURLFetcherCallback);
+};
+
+// This implements fake TimeZone API remote endpoint.
+// Response data is served to TimeZoneProvider via
+// net::FakeURLFetcher.
+class TimeZoneAPIFetcherFactory {
+ public:
+ TimeZoneAPIFetcherFactory(const GURL& url,
+ const std::string& response,
+ const size_t require_retries,
+ TimeZoneProvider* provider) {
+ url_callback_.reset(new TestTimeZoneAPIURLFetcherCallback(
+ url, require_retries, response, provider));
+ net::URLFetcherImpl::set_factory(NULL);
+ fetcher_factory_.reset(new net::FakeURLFetcherFactory(
+ NULL,
+ base::Bind(&TestTimeZoneAPIURLFetcherCallback::CreateURLFetcher,
+ base::Unretained(url_callback_.get()))));
+ url_callback_->Initialize(fetcher_factory_.get());
+ }
+
+ size_t attempts() const { return url_callback_->attempts(); }
+
+ private:
+ scoped_ptr<TestTimeZoneAPIURLFetcherCallback> url_callback_;
+ scoped_ptr<net::FakeURLFetcherFactory> fetcher_factory_;
+
+ DISALLOW_COPY_AND_ASSIGN(TimeZoneAPIFetcherFactory);
+};
+
+class TimeZoneReceiver {
+ public:
+ TimeZoneReceiver() : server_error_(false) {}
+
+ void OnRequestDone(scoped_ptr<TimeZoneResponseData> timezone,
+ bool server_error) {
+ timezone_ = timezone.Pass();
+ server_error_ = server_error;
+
+ message_loop_runner_->Quit();
+ }
+
+ void WaitUntilRequestDone() {
+ message_loop_runner_.reset(new base::RunLoop);
+ message_loop_runner_->Run();
+ }
+
+ const TimeZoneResponseData* timezone() const { return timezone_.get(); }
+ bool server_error() const { return server_error_; }
+
+ private:
+ scoped_ptr<TimeZoneResponseData> timezone_;
+ bool server_error_;
+ scoped_ptr<base::RunLoop> message_loop_runner_;
+};
+
+class TimeZoneTest : public testing::Test {
+ private:
+ base::MessageLoop message_loop_;
+};
+
+TEST_F(TimeZoneTest, ResponseOK) {
+ TimeZoneProvider provider(NULL, GURL(kTestTimeZoneProviderUrl));
+ const SimpleRequest simple_request;
+
+ TimeZoneAPIFetcherFactory url_factory(simple_request.url,
+ simple_request.http_response,
+ 0 /* require_retries */,
+ &provider);
+
+ TimeZoneReceiver receiver;
+
+ provider.RequestTimezone(simple_request.position,
+ false,
+ base::TimeDelta::FromSeconds(1),
+ base::Bind(&TimeZoneReceiver::OnRequestDone,
+ base::Unretained(&receiver)));
+ receiver.WaitUntilRequestDone();
+
+ EXPECT_EQ(simple_request.timezone.ToStringForDebug(),
+ receiver.timezone()->ToStringForDebug());
+ EXPECT_FALSE(receiver.server_error());
+ EXPECT_EQ(1U, url_factory.attempts());
+}
+
+TEST_F(TimeZoneTest, ResponseOKWithRetries) {
+ TimeZoneProvider provider(NULL, GURL(kTestTimeZoneProviderUrl));
+ const SimpleRequest simple_request;
+
+ TimeZoneAPIFetcherFactory url_factory(simple_request.url,
+ simple_request.http_response,
+ 3 /* require_retries */,
+ &provider);
+
+ TimeZoneReceiver receiver;
+
+ provider.RequestTimezone(simple_request.position,
+ false,
+ base::TimeDelta::FromSeconds(1),
+ base::Bind(&TimeZoneReceiver::OnRequestDone,
+ base::Unretained(&receiver)));
+ receiver.WaitUntilRequestDone();
+ EXPECT_EQ(simple_request.timezone.ToStringForDebug(),
+ receiver.timezone()->ToStringForDebug());
+ EXPECT_FALSE(receiver.server_error());
+ EXPECT_EQ(4U, url_factory.attempts());
+}
+
+TEST_F(TimeZoneTest, InvalidResponse) {
+ TimeZoneProvider provider(NULL, GURL(kTestTimeZoneProviderUrl));
+ const SimpleRequest simple_request;
+
+ TimeZoneAPIFetcherFactory url_factory(simple_request.url,
+ "invalid JSON string",
+ 0 /* require_retries */,
+ &provider);
+
+ TimeZoneReceiver receiver;
+
+ const int timeout_seconds = 1;
+ size_t expected_retries = static_cast<size_t>(
+ timeout_seconds * 1000 / kRequestRetryIntervalMilliSeconds);
+ ASSERT_GE(expected_retries, 2U);
+
+ provider.RequestTimezone(simple_request.position,
+ false,
+ base::TimeDelta::FromSeconds(timeout_seconds),
+ base::Bind(&TimeZoneReceiver::OnRequestDone,
+ base::Unretained(&receiver)));
+ receiver.WaitUntilRequestDone();
+ EXPECT_EQ(
+ "dstOffset=0.000000, rawOffset=0.000000, timeZoneId='', timeZoneName='', "
+ "error_message='TimeZone provider at 'https://localhost/' : JSONReader "
+ "failed: Line: 1, column: 1, Unexpected token..', status=6 "
+ "(REQUEST_ERROR)",
+ receiver.timezone()->ToStringForDebug());
+ EXPECT_FALSE(receiver.server_error());
+ EXPECT_GE(url_factory.attempts(), 2U);
+ if (url_factory.attempts() > expected_retries + 1) {
+ LOG(WARNING) << "TimeZoneTest::InvalidResponse: Too many attempts ("
+ << url_factory.attempts() << "), no more then "
+ << expected_retries + 1 << " expected.";
+ }
+ if (url_factory.attempts() < expected_retries - 1) {
+ LOG(WARNING) << "TimeZoneTest::InvalidResponse: Too less attempts ("
+ << url_factory.attempts() << "), greater then "
+ << expected_retries - 1 << " expected.";
+ }
+}
+
+} // namespace chromeos