diff options
author | satorux <satorux@chromium.org> | 2014-12-16 00:29:53 -0800 |
---|---|---|
committer | Commit bot <commit-bot@chromium.org> | 2014-12-16 08:30:32 +0000 |
commit | 07e69091e58db4faf9aacf6ca6a444048469694d (patch) | |
tree | 6504c786455a6fc3938fbf661e1e19ef61913720 /chromeos/timezone | |
parent | 51ca24b5b340648640e339379f290c7ddfd22516 (diff) | |
download | chromium_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/DEPS | 3 | ||||
-rw-r--r-- | chromeos/timezone/timezone_provider.cc | 61 | ||||
-rw-r--r-- | chromeos/timezone/timezone_provider.h | 66 | ||||
-rw-r--r-- | chromeos/timezone/timezone_request.cc | 431 | ||||
-rw-r--r-- | chromeos/timezone/timezone_request.h | 141 | ||||
-rw-r--r-- | chromeos/timezone/timezone_unittest.cc | 290 |
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×tamp=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 |