summaryrefslogtreecommitdiffstats
path: root/chromium/google_apis/gcm
diff options
context:
space:
mode:
authorJocelyn Turcotte <jocelyn.turcotte@digia.com>2014-08-08 14:30:41 +0200
committerJocelyn Turcotte <jocelyn.turcotte@digia.com>2014-08-12 13:49:54 +0200
commitab0a50979b9eb4dfa3320eff7e187e41efedf7a9 (patch)
tree498dfb8a97ff3361a9f7486863a52bb4e26bb898 /chromium/google_apis/gcm
parent4ce69f7403811819800e7c5ae1318b2647e778d1 (diff)
Update Chromium to beta version 37.0.2062.68
Change-Id: I188e3b5aff1bec75566014291b654eb19f5bc8ca Reviewed-by: Andras Becsi <andras.becsi@digia.com>
Diffstat (limited to 'chromium/google_apis/gcm')
-rw-r--r--chromium/google_apis/gcm/DEPS1
-rw-r--r--chromium/google_apis/gcm/base/encryptor.h27
-rw-r--r--chromium/google_apis/gcm/base/fake_encryptor.cc24
-rw-r--r--chromium/google_apis/gcm/base/fake_encryptor.h28
-rw-r--r--chromium/google_apis/gcm/base/mcs_message_unittest.cc6
-rw-r--r--chromium/google_apis/gcm/base/mcs_util.cc61
-rw-r--r--chromium/google_apis/gcm/base/mcs_util.h15
-rw-r--r--chromium/google_apis/gcm/base/mcs_util_unittest.cc6
-rw-r--r--chromium/google_apis/gcm/base/socket_stream.cc4
-rw-r--r--chromium/google_apis/gcm/engine/checkin_request.cc221
-rw-r--r--chromium/google_apis/gcm/engine/checkin_request.h95
-rw-r--r--chromium/google_apis/gcm/engine/checkin_request_unittest.cc380
-rw-r--r--chromium/google_apis/gcm/engine/connection_factory.cc3
-rw-r--r--chromium/google_apis/gcm/engine/connection_factory.h51
-rw-r--r--chromium/google_apis/gcm/engine/connection_factory_impl.cc508
-rw-r--r--chromium/google_apis/gcm/engine/connection_factory_impl.h108
-rw-r--r--chromium/google_apis/gcm/engine/connection_factory_impl_unittest.cc364
-rw-r--r--chromium/google_apis/gcm/engine/connection_handler.h6
-rw-r--r--chromium/google_apis/gcm/engine/connection_handler_impl.cc67
-rw-r--r--chromium/google_apis/gcm/engine/connection_handler_impl.h5
-rw-r--r--chromium/google_apis/gcm/engine/connection_handler_impl_unittest.cc99
-rw-r--r--chromium/google_apis/gcm/engine/fake_connection_factory.cc22
-rw-r--r--chromium/google_apis/gcm/engine/fake_connection_factory.h16
-rw-r--r--chromium/google_apis/gcm/engine/fake_connection_handler.cc7
-rw-r--r--chromium/google_apis/gcm/engine/fake_connection_handler.h3
-rw-r--r--chromium/google_apis/gcm/engine/gcm_store.cc21
-rw-r--r--chromium/google_apis/gcm/engine/gcm_store.h121
-rw-r--r--chromium/google_apis/gcm/engine/gcm_store_impl.cc956
-rw-r--r--chromium/google_apis/gcm/engine/gcm_store_impl.h126
-rw-r--r--chromium/google_apis/gcm/engine/gcm_store_impl_unittest.cc552
-rw-r--r--chromium/google_apis/gcm/engine/gservices_settings.cc342
-rw-r--r--chromium/google_apis/gcm/engine/gservices_settings.h82
-rw-r--r--chromium/google_apis/gcm/engine/gservices_settings_unittest.cc336
-rw-r--r--chromium/google_apis/gcm/engine/heartbeat_manager.cc119
-rw-r--r--chromium/google_apis/gcm/engine/heartbeat_manager.h82
-rw-r--r--chromium/google_apis/gcm/engine/heartbeat_manager_unittest.cc176
-rw-r--r--chromium/google_apis/gcm/engine/mcs_client.cc605
-rw-r--r--chromium/google_apis/gcm/engine/mcs_client.h150
-rw-r--r--chromium/google_apis/gcm/engine/mcs_client_unittest.cc479
-rw-r--r--chromium/google_apis/gcm/engine/registration_info.cc62
-rw-r--r--chromium/google_apis/gcm/engine/registration_info.h35
-rw-r--r--chromium/google_apis/gcm/engine/registration_request.cc267
-rw-r--r--chromium/google_apis/gcm/engine/registration_request.h128
-rw-r--r--chromium/google_apis/gcm/engine/registration_request_unittest.cc427
-rw-r--r--chromium/google_apis/gcm/engine/rmq_store.cc491
-rw-r--r--chromium/google_apis/gcm/engine/rmq_store.h102
-rw-r--r--chromium/google_apis/gcm/engine/rmq_store_unittest.cc303
-rw-r--r--chromium/google_apis/gcm/engine/unregistration_request.cc224
-rw-r--r--chromium/google_apis/gcm/engine/unregistration_request.h115
-rw-r--r--chromium/google_apis/gcm/engine/unregistration_request_unittest.cc295
-rw-r--r--chromium/google_apis/gcm/gcm.gyp89
-rw-r--r--chromium/google_apis/gcm/gcm_client.cc45
-rw-r--r--chromium/google_apis/gcm/gcm_client.h193
-rw-r--r--chromium/google_apis/gcm/gcm_client_impl.cc39
-rw-r--r--chromium/google_apis/gcm/gcm_client_impl.h39
-rw-r--r--chromium/google_apis/gcm/monitoring/fake_gcm_stats_recorder.cc107
-rw-r--r--chromium/google_apis/gcm/monitoring/fake_gcm_stats_recorder.h71
-rw-r--r--chromium/google_apis/gcm/monitoring/gcm_stats_recorder.h135
-rw-r--r--chromium/google_apis/gcm/protocol/android_checkin.proto97
-rw-r--r--chromium/google_apis/gcm/protocol/checkin.proto156
-rw-r--r--chromium/google_apis/gcm/tools/mcs_probe.cc191
61 files changed, 8070 insertions, 1815 deletions
diff --git a/chromium/google_apis/gcm/DEPS b/chromium/google_apis/gcm/DEPS
index 08ac400e0d4..047dc1564b5 100644
--- a/chromium/google_apis/gcm/DEPS
+++ b/chromium/google_apis/gcm/DEPS
@@ -7,7 +7,6 @@ include_rules = [
"+base",
"+testing",
- "+components/webdata/encryptor",
"+google", # For third_party/protobuf/src.
"+net",
"+third_party/leveldatabase",
diff --git a/chromium/google_apis/gcm/base/encryptor.h b/chromium/google_apis/gcm/base/encryptor.h
new file mode 100644
index 00000000000..898e7dba77e
--- /dev/null
+++ b/chromium/google_apis/gcm/base/encryptor.h
@@ -0,0 +1,27 @@
+// Copyright (c) 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 GOOGLE_APIS_GCM_ENCRYPTOR_H_
+#define GOOGLE_APIS_GCM_ENCRYPTOR_H_
+
+#include <string>
+#include "google_apis/gcm/base/gcm_export.h"
+
+namespace gcm {
+
+class GCM_EXPORT Encryptor {
+ public:
+ // All methods below should be thread-safe.
+ virtual bool EncryptString(const std::string& plaintext,
+ std::string* ciphertext) = 0;
+
+ virtual bool DecryptString(const std::string& ciphertext,
+ std::string* plaintext) = 0;
+
+ virtual ~Encryptor() {}
+};
+
+} // namespace gcm
+
+#endif // GOOGLE_APIS_GCM_ENCRYPTOR_H_
diff --git a/chromium/google_apis/gcm/base/fake_encryptor.cc b/chromium/google_apis/gcm/base/fake_encryptor.cc
new file mode 100644
index 00000000000..ac58081ee4c
--- /dev/null
+++ b/chromium/google_apis/gcm/base/fake_encryptor.cc
@@ -0,0 +1,24 @@
+// Copyright (c) 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 "google_apis/gcm/base/fake_encryptor.h"
+
+#include "base/base64.h"
+
+namespace gcm {
+
+FakeEncryptor::~FakeEncryptor() {}
+
+bool FakeEncryptor::EncryptString(const std::string& plaintext,
+ std::string* ciphertext) {
+ base::Base64Encode(plaintext, ciphertext);
+ return true;
+}
+
+bool FakeEncryptor::DecryptString(const std::string& ciphertext,
+ std::string* plaintext) {
+ return base::Base64Decode(ciphertext, plaintext);
+}
+
+} // namespace gcm
diff --git a/chromium/google_apis/gcm/base/fake_encryptor.h b/chromium/google_apis/gcm/base/fake_encryptor.h
new file mode 100644
index 00000000000..01ae67b0e85
--- /dev/null
+++ b/chromium/google_apis/gcm/base/fake_encryptor.h
@@ -0,0 +1,28 @@
+// Copyright (c) 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 GOOGLE_APIS_GCM_BASE_ENCRYPTOR_H_
+#define GOOGLE_APIS_GCM_BASE_ENCRYPTOR_H_
+
+#include "base/compiler_specific.h"
+#include "google_apis/gcm/base/encryptor.h"
+
+namespace gcm {
+
+// Encryptor which simply base64-encodes the plaintext to get the
+// ciphertext. Obviously, this should be used only for testing.
+class FakeEncryptor : public Encryptor {
+ public:
+ virtual ~FakeEncryptor();
+
+ virtual bool EncryptString(const std::string& plaintext,
+ std::string* ciphertext) OVERRIDE;
+
+ virtual bool DecryptString(const std::string& ciphertext,
+ std::string* plaintext) OVERRIDE;
+};
+
+} // namespace gcm
+
+#endif // GOOGLE_APIS_GCM_BASE_ENCRYPTOR_H_
diff --git a/chromium/google_apis/gcm/base/mcs_message_unittest.cc b/chromium/google_apis/gcm/base/mcs_message_unittest.cc
index 4d4ef598ad2..55ab49d130f 100644
--- a/chromium/google_apis/gcm/base/mcs_message_unittest.cc
+++ b/chromium/google_apis/gcm/base/mcs_message_unittest.cc
@@ -36,7 +36,7 @@ TEST_F(MCSMessageTest, Invalid) {
TEST_F(MCSMessageTest, InitInferTag) {
scoped_ptr<mcs_proto::LoginRequest> login_request(
- BuildLoginRequest(kAndroidId, kSecret));
+ BuildLoginRequest(kAndroidId, kSecret, ""));
scoped_ptr<google::protobuf::MessageLite> login_copy(
new mcs_proto::LoginRequest(*login_request));
MCSMessage message(*login_copy);
@@ -54,7 +54,7 @@ TEST_F(MCSMessageTest, InitInferTag) {
TEST_F(MCSMessageTest, InitWithTag) {
scoped_ptr<mcs_proto::LoginRequest> login_request(
- BuildLoginRequest(kAndroidId, kSecret));
+ BuildLoginRequest(kAndroidId, kSecret, ""));
scoped_ptr<google::protobuf::MessageLite> login_copy(
new mcs_proto::LoginRequest(*login_request));
MCSMessage message(kLoginRequestTag, *login_copy);
@@ -72,7 +72,7 @@ TEST_F(MCSMessageTest, InitWithTag) {
TEST_F(MCSMessageTest, InitPassOwnership) {
scoped_ptr<mcs_proto::LoginRequest> login_request(
- BuildLoginRequest(kAndroidId, kSecret));
+ BuildLoginRequest(kAndroidId, kSecret, ""));
scoped_ptr<google::protobuf::MessageLite> login_copy(
new mcs_proto::LoginRequest(*login_request));
MCSMessage message(kLoginRequestTag,
diff --git a/chromium/google_apis/gcm/base/mcs_util.cc b/chromium/google_apis/gcm/base/mcs_util.cc
index 736556079f8..b578e573922 100644
--- a/chromium/google_apis/gcm/base/mcs_util.cc
+++ b/chromium/google_apis/gcm/base/mcs_util.cc
@@ -8,6 +8,8 @@
#include "base/logging.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
+#include "base/time/clock.h"
+#include "base/time/time.h"
namespace gcm {
@@ -37,17 +39,21 @@ const char* kProtoNames[] = {
COMPILE_ASSERT(arraysize(kProtoNames) == kNumProtoTypes,
ProtoNamesMustIncludeAllTags);
-// TODO(zea): replace these with proper values.
-const char kLoginId[] = "login-1";
+const char kLoginId[] = "chrome-";
const char kLoginDomain[] = "mcs.android.com";
const char kLoginDeviceIdPrefix[] = "android-";
-const char kLoginSettingName[] = "new_vc";
-const char kLoginSettingValue[] = "1";
+const char kLoginSettingDefaultName[] = "new_vc";
+const char kLoginSettingDefaultValue[] = "1";
+
+// Maximum amount of time to save an unsent outgoing message for.
+const int kMaxTTLSeconds = 4 * 7 * 24 * 60 * 60; // 4 weeks.
} // namespace
-scoped_ptr<mcs_proto::LoginRequest> BuildLoginRequest(uint64 auth_id,
- uint64 auth_token) {
+scoped_ptr<mcs_proto::LoginRequest> BuildLoginRequest(
+ uint64 auth_id,
+ uint64 auth_token,
+ const std::string& version_string) {
// Create a hex encoded auth id for the device id field.
std::string auth_id_hex;
auth_id_hex = base::StringPrintf("%" PRIx64, auth_id);
@@ -58,12 +64,10 @@ scoped_ptr<mcs_proto::LoginRequest> BuildLoginRequest(uint64 auth_id,
scoped_ptr<mcs_proto::LoginRequest> login_request(
new mcs_proto::LoginRequest());
- // TODO(zea): set better values.
- login_request->set_account_id(1000000);
login_request->set_adaptive_heartbeat(false);
login_request->set_auth_service(mcs_proto::LoginRequest::ANDROID_ID);
login_request->set_auth_token(auth_token_str);
- login_request->set_id(kLoginId);
+ login_request->set_id(kLoginId + version_string);
login_request->set_domain(kLoginDomain);
login_request->set_device_id(kLoginDeviceIdPrefix + auth_id_hex);
login_request->set_network_type(1);
@@ -72,8 +76,8 @@ scoped_ptr<mcs_proto::LoginRequest> BuildLoginRequest(uint64 auth_id,
login_request->set_use_rmq2(true);
login_request->add_setting();
- login_request->mutable_setting(0)->set_name(kLoginSettingName);
- login_request->mutable_setting(0)->set_value(kLoginSettingValue);
+ login_request->mutable_setting(0)->set_name(kLoginSettingDefaultName);
+ login_request->mutable_setting(0)->set_value(kLoginSettingDefaultValue);
return login_request.Pass();
}
@@ -209,25 +213,50 @@ void SetLastStreamIdReceived(uint32 val,
if (protobuf->GetTypeName() == kProtoNames[kIqStanzaTag]) {
reinterpret_cast<mcs_proto::IqStanza*>(protobuf)->
set_last_stream_id_received(val);
- return;
+ return;
} else if (protobuf->GetTypeName() == kProtoNames[kHeartbeatPingTag]) {
reinterpret_cast<mcs_proto::HeartbeatPing*>(protobuf)->
set_last_stream_id_received(val);
- return;
+ return;
} else if (protobuf->GetTypeName() == kProtoNames[kHeartbeatAckTag]) {
reinterpret_cast<mcs_proto::HeartbeatAck*>(protobuf)->
set_last_stream_id_received(val);
- return;
+ return;
} else if (protobuf->GetTypeName() == kProtoNames[kDataMessageStanzaTag]) {
reinterpret_cast<mcs_proto::DataMessageStanza*>(protobuf)->
set_last_stream_id_received(val);
- return;
+ return;
} else if (protobuf->GetTypeName() == kProtoNames[kLoginResponseTag]) {
reinterpret_cast<mcs_proto::LoginResponse*>(protobuf)->
set_last_stream_id_received(val);
- return;
+ return;
}
NOTREACHED();
}
+bool HasTTLExpired(const google::protobuf::MessageLite& protobuf,
+ base::Clock* clock) {
+ if (protobuf.GetTypeName() != kProtoNames[kDataMessageStanzaTag])
+ return false;
+ uint64 ttl = GetTTL(protobuf);
+ uint64 sent =
+ reinterpret_cast<const mcs_proto::DataMessageStanza*>(&protobuf)->sent();
+ DCHECK(sent);
+ return ttl > 0 &&
+ clock->Now() >
+ base::Time::FromInternalValue(
+ (sent + ttl) * base::Time::kMicrosecondsPerSecond);
+}
+
+int GetTTL(const google::protobuf::MessageLite& protobuf) {
+ if (protobuf.GetTypeName() != kProtoNames[kDataMessageStanzaTag])
+ return 0;
+ const mcs_proto::DataMessageStanza* data_message =
+ reinterpret_cast<const mcs_proto::DataMessageStanza*>(&protobuf);
+ if (!data_message->has_ttl())
+ return kMaxTTLSeconds;
+ DCHECK_LE(data_message->ttl(), kMaxTTLSeconds);
+ return data_message->ttl();
+}
+
} // namespace gcm
diff --git a/chromium/google_apis/gcm/base/mcs_util.h b/chromium/google_apis/gcm/base/mcs_util.h
index 7f92564ad27..95d97f29489 100644
--- a/chromium/google_apis/gcm/base/mcs_util.h
+++ b/chromium/google_apis/gcm/base/mcs_util.h
@@ -8,6 +8,7 @@
#define GOOGLE_APIS_GCM_BASE_MCS_UTIL_H_
#include <string>
+#include <vector>
#include "base/basictypes.h"
#include "base/memory/ref_counted.h"
@@ -15,6 +16,10 @@
#include "google_apis/gcm/base/gcm_export.h"
#include "google_apis/gcm/protocol/mcs.pb.h"
+namespace base {
+class Clock;
+}
+
namespace net {
class StreamSocket;
}
@@ -52,7 +57,8 @@ enum MCSIqStanzaExtension {
// Builds a LoginRequest with the hardcoded local data.
GCM_EXPORT scoped_ptr<mcs_proto::LoginRequest> BuildLoginRequest(
uint64 auth_id,
- uint64 auth_token);
+ uint64 auth_token,
+ const std::string& version_string);
// Builds a StreamAck IqStanza message.
GCM_EXPORT scoped_ptr<mcs_proto::IqStanza> BuildStreamAck();
@@ -76,6 +82,13 @@ GCM_EXPORT void SetLastStreamIdReceived(
uint32 last_stream_id_received,
google::protobuf::MessageLite* protobuf);
+// Returns whether the TTL (time to live) for this message has expired, based
+// on the |sent| timestamps and base::TimeTicks::Now(). If |protobuf| is not
+// for a DataMessageStanza or the TTL is 0, will return false.
+GCM_EXPORT bool HasTTLExpired(const google::protobuf::MessageLite& protobuf,
+ base::Clock* clock);
+GCM_EXPORT int GetTTL(const google::protobuf::MessageLite& protobuf);
+
} // namespace gcm
#endif // GOOGLE_APIS_GCM_BASE_MCS_UTIL_H_
diff --git a/chromium/google_apis/gcm/base/mcs_util_unittest.cc b/chromium/google_apis/gcm/base/mcs_util_unittest.cc
index d25914583a4..00bac1175f9 100644
--- a/chromium/google_apis/gcm/base/mcs_util_unittest.cc
+++ b/chromium/google_apis/gcm/base/mcs_util_unittest.cc
@@ -19,11 +19,13 @@ const uint64 kAuthToken = 12345;
// Build a login request protobuf.
TEST(MCSUtilTest, BuildLoginRequest) {
scoped_ptr<mcs_proto::LoginRequest> login_request =
- BuildLoginRequest(kAuthId, kAuthToken);
- ASSERT_EQ("login-1", login_request->id());
+ BuildLoginRequest(kAuthId, kAuthToken, "1.0");
+ ASSERT_EQ("chrome-1.0", login_request->id());
ASSERT_EQ(base::Uint64ToString(kAuthToken), login_request->auth_token());
ASSERT_EQ(base::Uint64ToString(kAuthId), login_request->user());
ASSERT_EQ("android-3d5c23dac2a1fa7c", login_request->device_id());
+ ASSERT_EQ("new_vc", login_request->setting(0).name());
+ ASSERT_EQ("1", login_request->setting(0).value());
// TODO(zea): test the other fields once they have valid values.
}
diff --git a/chromium/google_apis/gcm/base/socket_stream.cc b/chromium/google_apis/gcm/base/socket_stream.cc
index 1a0b29d8d07..8c152c6c5b3 100644
--- a/chromium/google_apis/gcm/base/socket_stream.cc
+++ b/chromium/google_apis/gcm/base/socket_stream.cc
@@ -89,8 +89,8 @@ net::Error SocketInputStream::Refresh(const base::Closure& callback,
DCHECK_GT(byte_limit, 0);
if (byte_limit > read_buffer_->BytesRemaining()) {
- NOTREACHED() << "Out of buffer space, closing input stream.";
- CloseStream(net::ERR_UNEXPECTED, base::Closure());
+ LOG(ERROR) << "Out of buffer space, closing input stream.";
+ CloseStream(net::ERR_FILE_TOO_BIG, base::Closure());
return net::OK;
}
diff --git a/chromium/google_apis/gcm/engine/checkin_request.cc b/chromium/google_apis/gcm/engine/checkin_request.cc
new file mode 100644
index 00000000000..a5558471772
--- /dev/null
+++ b/chromium/google_apis/gcm/engine/checkin_request.cc
@@ -0,0 +1,221 @@
+// 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 "google_apis/gcm/engine/checkin_request.h"
+
+#include "base/bind.h"
+#include "base/message_loop/message_loop.h"
+#include "base/metrics/histogram.h"
+#include "google_apis/gcm/monitoring/gcm_stats_recorder.h"
+#include "google_apis/gcm/protocol/checkin.pb.h"
+#include "net/http/http_status_code.h"
+#include "net/url_request/url_fetcher.h"
+#include "net/url_request/url_request_status.h"
+
+namespace gcm {
+
+namespace {
+const char kRequestContentType[] = "application/x-protobuf";
+const int kRequestVersionValue = 3;
+const int kDefaultUserSerialNumber = 0;
+
+// This enum is also used in an UMA histogram (GCMCheckinRequestStatus
+// enum defined in tools/metrics/histograms/histogram.xml). Hence the entries
+// here shouldn't be deleted or re-ordered and new ones should be added to
+// the end, and update the GetCheckinRequestStatusString(...) below.
+enum CheckinRequestStatus {
+ SUCCESS, // Checkin completed successfully.
+ URL_FETCHING_FAILED, // URL fetching failed.
+ HTTP_BAD_REQUEST, // The request was malformed.
+ HTTP_UNAUTHORIZED, // The security token didn't match the android id.
+ HTTP_NOT_OK, // HTTP status was not OK.
+ RESPONSE_PARSING_FAILED, // Check in response parsing failed.
+ ZERO_ID_OR_TOKEN, // Either returned android id or security token
+ // was zero.
+ // NOTE: always keep this entry at the end. Add new status types only
+ // immediately above this line. Make sure to update the corresponding
+ // histogram enum accordingly.
+ STATUS_COUNT
+};
+
+// Returns string representation of enum CheckinRequestStatus.
+std::string GetCheckinRequestStatusString(CheckinRequestStatus status) {
+ switch (status) {
+ case SUCCESS:
+ return "SUCCESS";
+ case URL_FETCHING_FAILED:
+ return "URL_FETCHING_FAILED";
+ case HTTP_BAD_REQUEST:
+ return "HTTP_BAD_REQUEST";
+ case HTTP_UNAUTHORIZED:
+ return "HTTP_UNAUTHORIZED";
+ case HTTP_NOT_OK:
+ return "HTTP_NOT_OK";
+ case RESPONSE_PARSING_FAILED:
+ return "RESPONSE_PARSING_FAILED";
+ case ZERO_ID_OR_TOKEN:
+ return "ZERO_ID_OR_TOKEN";
+ default:
+ NOTREACHED();
+ return "UNKNOWN_STATUS";
+ }
+}
+
+// Records checkin status to both stats recorder and reports to UMA.
+void RecordCheckinStatusAndReportUMA(CheckinRequestStatus status,
+ GCMStatsRecorder* recorder,
+ bool will_retry) {
+ UMA_HISTOGRAM_ENUMERATION("GCM.CheckinRequestStatus", status, STATUS_COUNT);
+ if (status == SUCCESS)
+ recorder->RecordCheckinSuccess();
+ else {
+ recorder->RecordCheckinFailure(GetCheckinRequestStatusString(status),
+ will_retry);
+ }
+}
+
+} // namespace
+
+CheckinRequest::RequestInfo::RequestInfo(
+ uint64 android_id,
+ uint64 security_token,
+ const std::string& settings_digest,
+ const checkin_proto::ChromeBuildProto& chrome_build_proto)
+ : android_id(android_id),
+ security_token(security_token),
+ settings_digest(settings_digest),
+ chrome_build_proto(chrome_build_proto) {
+}
+
+CheckinRequest::RequestInfo::~RequestInfo() {}
+
+CheckinRequest::CheckinRequest(
+ const GURL& checkin_url,
+ const RequestInfo& request_info,
+ const net::BackoffEntry::Policy& backoff_policy,
+ const CheckinRequestCallback& callback,
+ net::URLRequestContextGetter* request_context_getter,
+ GCMStatsRecorder* recorder)
+ : request_context_getter_(request_context_getter),
+ callback_(callback),
+ backoff_entry_(&backoff_policy),
+ checkin_url_(checkin_url),
+ request_info_(request_info),
+ recorder_(recorder),
+ weak_ptr_factory_(this) {
+}
+
+CheckinRequest::~CheckinRequest() {}
+
+void CheckinRequest::Start() {
+ DCHECK(!url_fetcher_.get());
+
+ checkin_proto::AndroidCheckinRequest request;
+ request.set_id(request_info_.android_id);
+ request.set_security_token(request_info_.security_token);
+ request.set_user_serial_number(kDefaultUserSerialNumber);
+ request.set_version(kRequestVersionValue);
+ if (!request_info_.settings_digest.empty())
+ request.set_digest(request_info_.settings_digest);
+
+ checkin_proto::AndroidCheckinProto* checkin = request.mutable_checkin();
+ checkin->mutable_chrome_build()->CopyFrom(request_info_.chrome_build_proto);
+#if defined(CHROME_OS)
+ checkin->set_type(checkin_proto::DEVICE_CHROME_OS);
+#else
+ checkin->set_type(checkin_proto::DEVICE_CHROME_BROWSER);
+#endif
+
+ std::string upload_data;
+ CHECK(request.SerializeToString(&upload_data));
+
+ url_fetcher_.reset(
+ net::URLFetcher::Create(checkin_url_, net::URLFetcher::POST, this));
+ url_fetcher_->SetRequestContext(request_context_getter_);
+ url_fetcher_->SetUploadData(kRequestContentType, upload_data);
+ recorder_->RecordCheckinInitiated(request_info_.android_id);
+ request_start_time_ = base::TimeTicks::Now();
+ url_fetcher_->Start();
+}
+
+void CheckinRequest::RetryWithBackoff(bool update_backoff) {
+ if (update_backoff) {
+ backoff_entry_.InformOfRequest(false);
+ url_fetcher_.reset();
+ }
+
+ if (backoff_entry_.ShouldRejectRequest()) {
+ DVLOG(1) << "Delay GCM checkin for: "
+ << backoff_entry_.GetTimeUntilRelease().InMilliseconds()
+ << " milliseconds.";
+ recorder_->RecordCheckinDelayedDueToBackoff(
+ backoff_entry_.GetTimeUntilRelease().InMilliseconds());
+ base::MessageLoop::current()->PostDelayedTask(
+ FROM_HERE,
+ base::Bind(&CheckinRequest::RetryWithBackoff,
+ weak_ptr_factory_.GetWeakPtr(),
+ false),
+ backoff_entry_.GetTimeUntilRelease());
+ return;
+ }
+
+ Start();
+}
+
+void CheckinRequest::OnURLFetchComplete(const net::URLFetcher* source) {
+ std::string response_string;
+ checkin_proto::AndroidCheckinResponse response_proto;
+ if (!source->GetStatus().is_success()) {
+ LOG(ERROR) << "Failed to get checkin response. Fetcher failed. Retrying.";
+ RecordCheckinStatusAndReportUMA(URL_FETCHING_FAILED, recorder_, true);
+ RetryWithBackoff(true);
+ return;
+ }
+
+ net::HttpStatusCode response_status = static_cast<net::HttpStatusCode>(
+ source->GetResponseCode());
+ if (response_status == net::HTTP_BAD_REQUEST ||
+ response_status == net::HTTP_UNAUTHORIZED) {
+ // BAD_REQUEST indicates that the request was malformed.
+ // UNAUTHORIZED indicates that security token didn't match the android id.
+ LOG(ERROR) << "No point retrying the checkin with status: "
+ << response_status << ". Checkin failed.";
+ CheckinRequestStatus status = response_status == net::HTTP_BAD_REQUEST ?
+ HTTP_BAD_REQUEST : HTTP_UNAUTHORIZED;
+ RecordCheckinStatusAndReportUMA(status, recorder_, false);
+ callback_.Run(response_proto);
+ return;
+ }
+
+ if (response_status != net::HTTP_OK ||
+ !source->GetResponseAsString(&response_string) ||
+ !response_proto.ParseFromString(response_string)) {
+ LOG(ERROR) << "Failed to get checkin response. HTTP Status: "
+ << response_status << ". Retrying.";
+ CheckinRequestStatus status = response_status != net::HTTP_OK ?
+ HTTP_NOT_OK : RESPONSE_PARSING_FAILED;
+ RecordCheckinStatusAndReportUMA(status, recorder_, true);
+ RetryWithBackoff(true);
+ return;
+ }
+
+ if (!response_proto.has_android_id() ||
+ !response_proto.has_security_token() ||
+ response_proto.android_id() == 0 ||
+ response_proto.security_token() == 0) {
+ LOG(ERROR) << "Android ID or security token is 0. Retrying.";
+ RecordCheckinStatusAndReportUMA(ZERO_ID_OR_TOKEN, recorder_, true);
+ RetryWithBackoff(true);
+ return;
+ }
+
+ RecordCheckinStatusAndReportUMA(SUCCESS, recorder_, false);
+ UMA_HISTOGRAM_COUNTS("GCM.CheckinRetryCount",
+ backoff_entry_.failure_count());
+ UMA_HISTOGRAM_TIMES("GCM.CheckinCompleteTime",
+ base::TimeTicks::Now() - request_start_time_);
+ callback_.Run(response_proto);
+}
+
+} // namespace gcm
diff --git a/chromium/google_apis/gcm/engine/checkin_request.h b/chromium/google_apis/gcm/engine/checkin_request.h
new file mode 100644
index 00000000000..ae4a7d43897
--- /dev/null
+++ b/chromium/google_apis/gcm/engine/checkin_request.h
@@ -0,0 +1,95 @@
+// 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 GOOGLE_APIS_GCM_ENGINE_CHECKIN_REQUEST_H_
+#define GOOGLE_APIS_GCM_ENGINE_CHECKIN_REQUEST_H_
+
+#include <string>
+
+#include "base/basictypes.h"
+#include "base/callback.h"
+#include "base/memory/weak_ptr.h"
+#include "base/time/time.h"
+#include "google_apis/gcm/base/gcm_export.h"
+#include "google_apis/gcm/protocol/android_checkin.pb.h"
+#include "google_apis/gcm/protocol/checkin.pb.h"
+#include "net/base/backoff_entry.h"
+#include "net/url_request/url_fetcher_delegate.h"
+#include "url/gurl.h"
+
+namespace net {
+class URLRequestContextGetter;
+}
+
+namespace gcm {
+
+class GCMStatsRecorder;
+
+// Enables making check-in requests with the GCM infrastructure. When called
+// with android_id and security_token both set to 0 it is an initial check-in
+// used to obtain credentials. These should be persisted and used for subsequent
+// check-ins.
+class GCM_EXPORT CheckinRequest : public net::URLFetcherDelegate {
+ public:
+ // A callback function for the checkin request, accepting |checkin_response|
+ // protobuf.
+ typedef base::Callback<void(const checkin_proto::AndroidCheckinResponse&
+ checkin_response)> CheckinRequestCallback;
+
+ // Checkin request details.
+ struct GCM_EXPORT RequestInfo {
+ RequestInfo(uint64 android_id,
+ uint64 security_token,
+ const std::string& settings_digest,
+ const checkin_proto::ChromeBuildProto& chrome_build_proto);
+ ~RequestInfo();
+
+ // Android ID of the device.
+ uint64 android_id;
+ // Security token of the device.
+ uint64 security_token;
+ // Digest of GServices settings on the device.
+ std::string settings_digest;
+ // Information of the Chrome build of this device.
+ checkin_proto::ChromeBuildProto chrome_build_proto;
+ };
+
+ CheckinRequest(const GURL& checkin_url,
+ const RequestInfo& request_info,
+ const net::BackoffEntry::Policy& backoff_policy,
+ const CheckinRequestCallback& callback,
+ net::URLRequestContextGetter* request_context_getter,
+ GCMStatsRecorder* recorder);
+ virtual ~CheckinRequest();
+
+ void Start();
+
+ // URLFetcherDelegate implementation.
+ virtual void OnURLFetchComplete(const net::URLFetcher* source) OVERRIDE;
+
+ private:
+ // Schedules a retry attempt, informs the backoff of a previous request's
+ // failure when |update_backoff| is true.
+ void RetryWithBackoff(bool update_backoff);
+
+ net::URLRequestContextGetter* request_context_getter_;
+ CheckinRequestCallback callback_;
+
+ net::BackoffEntry backoff_entry_;
+ GURL checkin_url_;
+ scoped_ptr<net::URLFetcher> url_fetcher_;
+ const RequestInfo request_info_;
+ base::TimeTicks request_start_time_;
+
+ // Recorder that records GCM activities for debugging purpose. Not owned.
+ GCMStatsRecorder* recorder_;
+
+ base::WeakPtrFactory<CheckinRequest> weak_ptr_factory_;
+
+ DISALLOW_COPY_AND_ASSIGN(CheckinRequest);
+};
+
+} // namespace gcm
+
+#endif // GOOGLE_APIS_GCM_ENGINE_CHECKIN_REQUEST_H_
diff --git a/chromium/google_apis/gcm/engine/checkin_request_unittest.cc b/chromium/google_apis/gcm/engine/checkin_request_unittest.cc
new file mode 100644
index 00000000000..12e9a601b79
--- /dev/null
+++ b/chromium/google_apis/gcm/engine/checkin_request_unittest.cc
@@ -0,0 +1,380 @@
+// 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 <string>
+
+#include "google_apis/gcm/engine/checkin_request.h"
+#include "google_apis/gcm/monitoring/fake_gcm_stats_recorder.h"
+#include "google_apis/gcm/protocol/checkin.pb.h"
+#include "net/base/backoff_entry.h"
+#include "net/url_request/test_url_fetcher_factory.h"
+#include "net/url_request/url_request_test_util.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace gcm {
+
+namespace {
+
+const net::BackoffEntry::Policy kDefaultBackoffPolicy = {
+ // Number of initial errors (in sequence) to ignore before applying
+ // exponential back-off rules.
+ // Explicitly set to 1 to skip the delay of the first Retry, as we are not
+ // trying to test the backoff itself, but rather the fact that retry happens.
+ 1,
+
+ // Initial delay for exponential back-off in ms.
+ 15000, // 15 seconds.
+
+ // Factor by which the waiting time will be multiplied.
+ 2,
+
+ // Fuzzing percentage. ex: 10% will spread requests randomly
+ // between 90%-100% of the calculated time.
+ 0.5, // 50%.
+
+ // Maximum amount of time we are willing to delay our request in ms.
+ 1000 * 60 * 5, // 5 minutes.
+
+ // Time to keep an entry from being discarded even when it
+ // has no significant state, -1 to never discard.
+ -1,
+
+ // Don't use initial delay unless the last request was an error.
+ false,
+};
+
+}
+
+const uint64 kAndroidId = 42UL;
+const uint64 kBlankAndroidId = 999999UL;
+const uint64 kBlankSecurityToken = 999999UL;
+const char kCheckinURL[] = "http://foo.bar/checkin";
+const char kChromeVersion[] = "Version String";
+const uint64 kSecurityToken = 77;
+const char kSettingsDigest[] = "settings_digest";
+
+class CheckinRequestTest : public testing::Test {
+ public:
+ enum ResponseScenario {
+ VALID_RESPONSE, // Both android_id and security_token set in response.
+ MISSING_ANDROID_ID, // android_id is missing.
+ MISSING_SECURITY_TOKEN, // security_token is missing.
+ ANDROID_ID_IS_ZER0, // android_id is 0.
+ SECURITY_TOKEN_IS_ZERO // security_token is 0.
+ };
+
+ CheckinRequestTest();
+ virtual ~CheckinRequestTest();
+
+ void FetcherCallback(
+ const checkin_proto::AndroidCheckinResponse& response);
+
+ void CreateRequest(uint64 android_id, uint64 security_token);
+
+ void SetResponseStatusAndString(
+ net::HttpStatusCode status_code,
+ const std::string& response_data);
+
+ void CompleteFetch();
+
+ void SetResponse(ResponseScenario response_scenario);
+
+ protected:
+ bool callback_called_;
+ uint64 android_id_;
+ uint64 security_token_;
+ int checkin_device_type_;
+ base::MessageLoop message_loop_;
+ net::TestURLFetcherFactory url_fetcher_factory_;
+ scoped_refptr<net::TestURLRequestContextGetter> url_request_context_getter_;
+ checkin_proto::ChromeBuildProto chrome_build_proto_;
+ scoped_ptr<CheckinRequest> request_;
+ FakeGCMStatsRecorder recorder_;
+};
+
+CheckinRequestTest::CheckinRequestTest()
+ : callback_called_(false),
+ android_id_(kBlankAndroidId),
+ security_token_(kBlankSecurityToken),
+ checkin_device_type_(0),
+ url_request_context_getter_(new net::TestURLRequestContextGetter(
+ message_loop_.message_loop_proxy())) {
+}
+
+CheckinRequestTest::~CheckinRequestTest() {}
+
+void CheckinRequestTest::FetcherCallback(
+ const checkin_proto::AndroidCheckinResponse& checkin_response) {
+ callback_called_ = true;
+ if (checkin_response.has_android_id())
+ android_id_ = checkin_response.android_id();
+ if (checkin_response.has_security_token())
+ security_token_ = checkin_response.security_token();
+}
+
+void CheckinRequestTest::CreateRequest(uint64 android_id,
+ uint64 security_token) {
+ // First setup a chrome_build protobuf.
+ chrome_build_proto_.set_platform(
+ checkin_proto::ChromeBuildProto::PLATFORM_LINUX);
+ chrome_build_proto_.set_channel(
+ checkin_proto::ChromeBuildProto::CHANNEL_CANARY);
+ chrome_build_proto_.set_chrome_version(kChromeVersion);
+
+ CheckinRequest::RequestInfo request_info(
+ android_id,
+ security_token,
+ kSettingsDigest,
+ chrome_build_proto_);
+ // Then create a request with that protobuf and specified android_id,
+ // security_token.
+ request_.reset(new CheckinRequest(
+ GURL(kCheckinURL),
+ request_info,
+ kDefaultBackoffPolicy,
+ base::Bind(&CheckinRequestTest::FetcherCallback, base::Unretained(this)),
+ url_request_context_getter_.get(),
+ &recorder_));
+
+ // Setting android_id_ and security_token_ to blank value, not used elsewhere
+ // in the tests.
+ callback_called_ = false;
+ android_id_ = kBlankAndroidId;
+ security_token_ = kBlankSecurityToken;
+}
+
+void CheckinRequestTest::SetResponseStatusAndString(
+ net::HttpStatusCode status_code,
+ const std::string& response_data) {
+ net::TestURLFetcher* fetcher =
+ url_fetcher_factory_.GetFetcherByID(0);
+ ASSERT_TRUE(fetcher);
+ fetcher->set_response_code(status_code);
+ fetcher->SetResponseString(response_data);
+}
+
+void CheckinRequestTest::CompleteFetch() {
+ net::TestURLFetcher* fetcher =
+ url_fetcher_factory_.GetFetcherByID(0);
+ ASSERT_TRUE(fetcher);
+ fetcher->delegate()->OnURLFetchComplete(fetcher);
+}
+
+void CheckinRequestTest::SetResponse(ResponseScenario response_scenario) {
+ checkin_proto::AndroidCheckinResponse response;
+ response.set_stats_ok(true);
+
+ uint64 android_id = response_scenario == ANDROID_ID_IS_ZER0 ? 0 : kAndroidId;
+ uint64 security_token =
+ response_scenario == SECURITY_TOKEN_IS_ZERO ? 0 : kSecurityToken;
+
+ if (response_scenario != MISSING_ANDROID_ID)
+ response.set_android_id(android_id);
+
+ if (response_scenario != MISSING_SECURITY_TOKEN)
+ response.set_security_token(security_token);
+
+ std::string response_string;
+ response.SerializeToString(&response_string);
+ SetResponseStatusAndString(net::HTTP_OK, response_string);
+}
+
+TEST_F(CheckinRequestTest, FetcherDataAndURL) {
+ CreateRequest(kAndroidId, kSecurityToken);
+ request_->Start();
+
+ // Get data sent by request.
+ net::TestURLFetcher* fetcher = url_fetcher_factory_.GetFetcherByID(0);
+ ASSERT_TRUE(fetcher);
+ EXPECT_EQ(GURL(kCheckinURL), fetcher->GetOriginalURL());
+
+ checkin_proto::AndroidCheckinRequest request_proto;
+ request_proto.ParseFromString(fetcher->upload_data());
+ EXPECT_EQ(kAndroidId, static_cast<uint64>(request_proto.id()));
+ EXPECT_EQ(kSecurityToken, request_proto.security_token());
+ EXPECT_EQ(chrome_build_proto_.platform(),
+ request_proto.checkin().chrome_build().platform());
+ EXPECT_EQ(chrome_build_proto_.chrome_version(),
+ request_proto.checkin().chrome_build().chrome_version());
+ EXPECT_EQ(chrome_build_proto_.channel(),
+ request_proto.checkin().chrome_build().channel());
+
+#if defined(CHROME_OS)
+ EXPECT_EQ(checkin_proto::DEVICE_CHROME_OS, request_proto.checkin().type());
+#else
+ EXPECT_EQ(checkin_proto::DEVICE_CHROME_BROWSER,
+ request_proto.checkin().type());
+#endif
+
+ EXPECT_EQ(kSettingsDigest, request_proto.digest());
+}
+
+TEST_F(CheckinRequestTest, ResponseBodyEmpty) {
+ CreateRequest(0u, 0u);
+ request_->Start();
+
+ SetResponseStatusAndString(net::HTTP_OK, std::string());
+ CompleteFetch();
+
+ EXPECT_FALSE(callback_called_);
+
+ SetResponse(VALID_RESPONSE);
+ CompleteFetch();
+
+ EXPECT_TRUE(callback_called_);
+ EXPECT_EQ(kAndroidId, android_id_);
+ EXPECT_EQ(kSecurityToken, security_token_);
+}
+
+TEST_F(CheckinRequestTest, ResponseBodyCorrupted) {
+ CreateRequest(0u, 0u);
+ request_->Start();
+
+ SetResponseStatusAndString(net::HTTP_OK, "Corrupted response body");
+ CompleteFetch();
+
+ EXPECT_FALSE(callback_called_);
+
+ SetResponse(VALID_RESPONSE);
+ CompleteFetch();
+
+ EXPECT_TRUE(callback_called_);
+ EXPECT_EQ(kAndroidId, android_id_);
+ EXPECT_EQ(kSecurityToken, security_token_);
+}
+
+TEST_F(CheckinRequestTest, ResponseHttpStatusUnauthorized) {
+ CreateRequest(0u, 0u);
+ request_->Start();
+
+ SetResponseStatusAndString(net::HTTP_UNAUTHORIZED, std::string());
+ CompleteFetch();
+
+ EXPECT_TRUE(callback_called_);
+ EXPECT_EQ(kBlankAndroidId, android_id_);
+ EXPECT_EQ(kBlankSecurityToken, security_token_);
+}
+
+TEST_F(CheckinRequestTest, ResponseHttpStatusBadRequest) {
+ CreateRequest(0u, 0u);
+ request_->Start();
+
+ SetResponseStatusAndString(net::HTTP_BAD_REQUEST, std::string());
+ CompleteFetch();
+
+ EXPECT_TRUE(callback_called_);
+ EXPECT_EQ(kBlankAndroidId, android_id_);
+ EXPECT_EQ(kBlankSecurityToken, security_token_);
+}
+
+TEST_F(CheckinRequestTest, ResponseHttpStatusNotOK) {
+ CreateRequest(0u, 0u);
+ request_->Start();
+
+ SetResponseStatusAndString(net::HTTP_INTERNAL_SERVER_ERROR, std::string());
+ CompleteFetch();
+
+ EXPECT_FALSE(callback_called_);
+
+ SetResponse(VALID_RESPONSE);
+ CompleteFetch();
+
+ EXPECT_TRUE(callback_called_);
+ EXPECT_EQ(kAndroidId, android_id_);
+ EXPECT_EQ(kSecurityToken, security_token_);
+}
+
+TEST_F(CheckinRequestTest, ResponseMissingAndroidId) {
+ CreateRequest(0u, 0u);
+ request_->Start();
+
+ SetResponse(MISSING_ANDROID_ID);
+ CompleteFetch();
+
+ EXPECT_FALSE(callback_called_);
+
+ SetResponse(VALID_RESPONSE);
+ CompleteFetch();
+
+ EXPECT_TRUE(callback_called_);
+ EXPECT_EQ(kAndroidId, android_id_);
+ EXPECT_EQ(kSecurityToken, security_token_);
+}
+
+TEST_F(CheckinRequestTest, ResponseMissingSecurityToken) {
+ CreateRequest(0u, 0u);
+ request_->Start();
+
+ SetResponse(MISSING_SECURITY_TOKEN);
+ CompleteFetch();
+
+ EXPECT_FALSE(callback_called_);
+
+ SetResponse(VALID_RESPONSE);
+ CompleteFetch();
+
+ EXPECT_TRUE(callback_called_);
+ EXPECT_EQ(kAndroidId, android_id_);
+ EXPECT_EQ(kSecurityToken, security_token_);
+}
+
+TEST_F(CheckinRequestTest, AndroidIdEqualsZeroInResponse) {
+ CreateRequest(0u, 0u);
+ request_->Start();
+
+ SetResponse(ANDROID_ID_IS_ZER0);
+ CompleteFetch();
+
+ EXPECT_FALSE(callback_called_);
+
+ SetResponse(VALID_RESPONSE);
+ CompleteFetch();
+
+ EXPECT_TRUE(callback_called_);
+ EXPECT_EQ(kAndroidId, android_id_);
+ EXPECT_EQ(kSecurityToken, security_token_);
+}
+
+TEST_F(CheckinRequestTest, SecurityTokenEqualsZeroInResponse) {
+ CreateRequest(0u, 0u);
+ request_->Start();
+
+ SetResponse(SECURITY_TOKEN_IS_ZERO);
+ CompleteFetch();
+
+ EXPECT_FALSE(callback_called_);
+
+ SetResponse(VALID_RESPONSE);
+ CompleteFetch();
+
+ EXPECT_TRUE(callback_called_);
+ EXPECT_EQ(kAndroidId, android_id_);
+ EXPECT_EQ(kSecurityToken, security_token_);
+}
+
+TEST_F(CheckinRequestTest, SuccessfulFirstTimeCheckin) {
+ CreateRequest(0u, 0u);
+ request_->Start();
+
+ SetResponse(VALID_RESPONSE);
+ CompleteFetch();
+
+ EXPECT_TRUE(callback_called_);
+ EXPECT_EQ(kAndroidId, android_id_);
+ EXPECT_EQ(kSecurityToken, security_token_);
+}
+
+TEST_F(CheckinRequestTest, SuccessfulSubsequentCheckin) {
+ CreateRequest(kAndroidId, kSecurityToken);
+ request_->Start();
+
+ SetResponse(VALID_RESPONSE);
+ CompleteFetch();
+
+ EXPECT_TRUE(callback_called_);
+ EXPECT_EQ(kAndroidId, android_id_);
+ EXPECT_EQ(kSecurityToken, security_token_);
+}
+
+} // namespace gcm
diff --git a/chromium/google_apis/gcm/engine/connection_factory.cc b/chromium/google_apis/gcm/engine/connection_factory.cc
index 016e1e2b89c..a4bffd17076 100644
--- a/chromium/google_apis/gcm/engine/connection_factory.cc
+++ b/chromium/google_apis/gcm/engine/connection_factory.cc
@@ -6,6 +6,9 @@
namespace gcm {
+ConnectionFactory::ConnectionListener::ConnectionListener() {}
+ConnectionFactory::ConnectionListener::~ConnectionListener() {}
+
ConnectionFactory::ConnectionFactory() {}
ConnectionFactory::~ConnectionFactory() {}
diff --git a/chromium/google_apis/gcm/engine/connection_factory.h b/chromium/google_apis/gcm/engine/connection_factory.h
index 3cff48299b6..1db02a6ca13 100644
--- a/chromium/google_apis/gcm/engine/connection_factory.h
+++ b/chromium/google_apis/gcm/engine/connection_factory.h
@@ -5,10 +5,18 @@
#ifndef GOOGLE_APIS_GCM_ENGINE_CONNECTION_FACTORY_H_
#define GOOGLE_APIS_GCM_ENGINE_CONNECTION_FACTORY_H_
+#include <string>
+
#include "base/time/time.h"
#include "google_apis/gcm/base/gcm_export.h"
#include "google_apis/gcm/engine/connection_handler.h"
+class GURL;
+
+namespace net {
+class IPEndPoint;
+}
+
namespace mcs_proto {
class LoginRequest;
}
@@ -23,6 +31,35 @@ class GCM_EXPORT ConnectionFactory {
typedef base::Callback<void(mcs_proto::LoginRequest* login_request)>
BuildLoginRequestCallback;
+ // Reasons for triggering a connection reset. Note that these enums are
+ // consumed by a histogram, so ordering should not be modified.
+ enum ConnectionResetReason {
+ LOGIN_FAILURE, // Login response included an error.
+ CLOSE_COMMAND, // Received a close command.
+ HEARTBEAT_FAILURE, // Heartbeat was not acknowledged in time.
+ SOCKET_FAILURE, // net::Socket error.
+ NETWORK_CHANGE, // NetworkChangeNotifier notified of a network change.
+ // Count of total number of connection reset reasons. All new reset reasons
+ // should be added above this line.
+ CONNECTION_RESET_COUNT,
+ };
+
+ // Listener interface to be notified of endpoint connection events.
+ class GCM_EXPORT ConnectionListener {
+ public:
+ ConnectionListener();
+ virtual ~ConnectionListener();
+
+ // Notifies the listener that GCM has performed a handshake with and is now
+ // actively connected to |current_server|. |ip_endpoint| is the resolved
+ // ip address/port through which the connection is being made.
+ virtual void OnConnected(const GURL& current_server,
+ const net::IPEndPoint& ip_endpoint) = 0;
+
+ // Notifies the listener that the connection has been interrupted.
+ virtual void OnDisconnected() = 0;
+ };
+
ConnectionFactory();
virtual ~ConnectionFactory();
@@ -53,10 +90,24 @@ class GCM_EXPORT ConnectionFactory {
// connection.
virtual bool IsEndpointReachable() const = 0;
+ // Returns a debug string describing the connection state.
+ virtual std::string GetConnectionStateString() const = 0;
+
// If in backoff, the time at which the next retry will be made. Otherwise,
// a null time, indicating either no attempt to connect has been made or no
// backoff is in progress.
virtual base::TimeTicks NextRetryAttempt() const = 0;
+
+ // Manually reset the connection. This can occur if an application specific
+ // event forced a reset (e.g. server sends a close connection response).
+ // If the last connection was made within kConnectionResetWindowSecs, the old
+ // backoff is restored, else a new backoff kicks off.
+ virtual void SignalConnectionReset(ConnectionResetReason reason) = 0;
+
+ // Sets the current connection listener. Only one listener is supported at a
+ // time, and the listener must either outlive the connection factory or
+ // call SetConnectionListener(NULL) upon destruction.
+ virtual void SetConnectionListener(ConnectionListener* listener) = 0;
};
} // namespace gcm
diff --git a/chromium/google_apis/gcm/engine/connection_factory_impl.cc b/chromium/google_apis/gcm/engine/connection_factory_impl.cc
index 388b9dca5e3..d322ffa0e94 100644
--- a/chromium/google_apis/gcm/engine/connection_factory_impl.cc
+++ b/chromium/google_apis/gcm/engine/connection_factory_impl.cc
@@ -5,7 +5,10 @@
#include "google_apis/gcm/engine/connection_factory_impl.h"
#include "base/message_loop/message_loop.h"
+#include "base/metrics/histogram.h"
+#include "base/metrics/sparse_histogram.h"
#include "google_apis/gcm/engine/connection_handler_impl.h"
+#include "google_apis/gcm/monitoring/gcm_stats_recorder.h"
#include "google_apis/gcm/protocol/mcs.pb.h"
#include "net/base/net_errors.h"
#include "net/http/http_network_session.h"
@@ -22,46 +25,52 @@ namespace {
// The amount of time a Socket read should wait before timing out.
const int kReadTimeoutMs = 30000; // 30 seconds.
-// Backoff policy.
-const net::BackoffEntry::Policy kConnectionBackoffPolicy = {
- // Number of initial errors (in sequence) to ignore before applying
- // exponential back-off rules.
- 0,
-
- // Initial delay for exponential back-off in ms.
- 10000, // 10 seconds.
-
- // Factor by which the waiting time will be multiplied.
- 2,
-
- // Fuzzing percentage. ex: 10% will spread requests randomly
- // between 90%-100% of the calculated time.
- 0.2, // 20%.
-
- // Maximum amount of time we are willing to delay our request in ms.
- 1000 * 3600 * 4, // 4 hours.
-
- // Time to keep an entry from being discarded even when it
- // has no significant state, -1 to never discard.
- -1,
-
- // Don't use initial delay unless the last request was an error.
- false,
-};
+// If a connection is reset after succeeding within this window of time,
+// the previous backoff entry is restored (and the connection success is treated
+// as if it was transient).
+const int kConnectionResetWindowSecs = 10; // 10 seconds.
+
+// Decides whether the last login was within kConnectionResetWindowSecs of now
+// or not.
+bool ShouldRestorePreviousBackoff(const base::TimeTicks& login_time,
+ const base::TimeTicks& now_ticks) {
+ return !login_time.is_null() &&
+ now_ticks - login_time <=
+ base::TimeDelta::FromSeconds(kConnectionResetWindowSecs);
+}
} // namespace
ConnectionFactoryImpl::ConnectionFactoryImpl(
- const GURL& mcs_endpoint,
+ const std::vector<GURL>& mcs_endpoints,
+ const net::BackoffEntry::Policy& backoff_policy,
scoped_refptr<net::HttpNetworkSession> network_session,
- net::NetLog* net_log)
- : mcs_endpoint_(mcs_endpoint),
+ net::NetLog* net_log,
+ GCMStatsRecorder* recorder)
+ : mcs_endpoints_(mcs_endpoints),
+ next_endpoint_(0),
+ last_successful_endpoint_(0),
+ backoff_policy_(backoff_policy),
network_session_(network_session),
- net_log_(net_log),
+ bound_net_log_(
+ net::BoundNetLog::Make(net_log, net::NetLog::SOURCE_SOCKET)),
+ pac_request_(NULL),
+ connecting_(false),
+ waiting_for_backoff_(false),
+ waiting_for_network_online_(false),
+ logging_in_(false),
+ recorder_(recorder),
+ listener_(NULL),
weak_ptr_factory_(this) {
+ DCHECK_GE(mcs_endpoints_.size(), 1U);
}
ConnectionFactoryImpl::~ConnectionFactoryImpl() {
+ net::NetworkChangeNotifier::RemoveNetworkChangeObserver(this);
+ if (pac_request_) {
+ network_session_->proxy_service()->CancelPacRequest(pac_request_);
+ pac_request_ = NULL;
+ }
}
void ConnectionFactoryImpl::Initialize(
@@ -70,18 +79,18 @@ void ConnectionFactoryImpl::Initialize(
const ConnectionHandler::ProtoSentCallback& write_callback) {
DCHECK(!connection_handler_);
- backoff_entry_ = CreateBackoffEntry(&kConnectionBackoffPolicy);
+ previous_backoff_ = CreateBackoffEntry(&backoff_policy_);
+ backoff_entry_ = CreateBackoffEntry(&backoff_policy_);
request_builder_ = request_builder;
- net::NetworkChangeNotifier::AddIPAddressObserver(this);
- net::NetworkChangeNotifier::AddConnectionTypeObserver(this);
- connection_handler_.reset(
- new ConnectionHandlerImpl(
- base::TimeDelta::FromMilliseconds(kReadTimeoutMs),
- read_callback,
- write_callback,
- base::Bind(&ConnectionFactoryImpl::ConnectionHandlerCallback,
- weak_ptr_factory_.GetWeakPtr())));
+ net::NetworkChangeNotifier::AddNetworkChangeObserver(this);
+ waiting_for_network_online_ = net::NetworkChangeNotifier::IsOffline();
+ connection_handler_ = CreateConnectionHandler(
+ base::TimeDelta::FromMilliseconds(kReadTimeoutMs),
+ read_callback,
+ write_callback,
+ base::Bind(&ConnectionFactoryImpl::ConnectionHandlerCallback,
+ weak_ptr_factory_.GetWeakPtr())).Pass();
}
ConnectionHandler* ConnectionFactoryImpl::GetConnectionHandler() const {
@@ -90,21 +99,41 @@ ConnectionHandler* ConnectionFactoryImpl::GetConnectionHandler() const {
void ConnectionFactoryImpl::Connect() {
DCHECK(connection_handler_);
- DCHECK(!IsEndpointReachable());
+
+ if (connecting_ || waiting_for_backoff_)
+ return; // Connection attempt already in progress or pending.
+
+ if (IsEndpointReachable())
+ return; // Already connected.
+
+ ConnectWithBackoff();
+}
+
+void ConnectionFactoryImpl::ConnectWithBackoff() {
+ // If a canary managed to connect while a backoff expiration was pending,
+ // just cleanup the internal state.
+ if (connecting_ || logging_in_ || IsEndpointReachable()) {
+ waiting_for_backoff_ = false;
+ return;
+ }
if (backoff_entry_->ShouldRejectRequest()) {
DVLOG(1) << "Delaying MCS endpoint connection for "
<< backoff_entry_->GetTimeUntilRelease().InMilliseconds()
<< " milliseconds.";
+ waiting_for_backoff_ = true;
+ recorder_->RecordConnectionDelayedDueToBackoff(
+ backoff_entry_->GetTimeUntilRelease().InMilliseconds());
base::MessageLoop::current()->PostDelayedTask(
FROM_HERE,
- base::Bind(&ConnectionFactoryImpl::Connect,
+ base::Bind(&ConnectionFactoryImpl::ConnectWithBackoff,
weak_ptr_factory_.GetWeakPtr()),
- NextRetryAttempt() - base::TimeTicks::Now());
+ backoff_entry_->GetTimeUntilRelease());
return;
}
DVLOG(1) << "Attempting connection to MCS endpoint.";
+ waiting_for_backoff_ = false;
ConnectImpl();
}
@@ -112,54 +141,153 @@ bool ConnectionFactoryImpl::IsEndpointReachable() const {
return connection_handler_ && connection_handler_->CanSendMessage();
}
+std::string ConnectionFactoryImpl::GetConnectionStateString() const {
+ if (IsEndpointReachable())
+ return "CONNECTED";
+ if (logging_in_)
+ return "LOGGING IN";
+ if (connecting_)
+ return "CONNECTING";
+ if (waiting_for_backoff_)
+ return "WAITING FOR BACKOFF";
+ if (waiting_for_network_online_)
+ return "WAITING FOR NETWORK CHANGE";
+ return "NOT CONNECTED";
+}
+
+void ConnectionFactoryImpl::SignalConnectionReset(
+ ConnectionResetReason reason) {
+ // A failure can trigger multiple resets, so no need to do anything if a
+ // connection is already in progress.
+ if (connecting_) {
+ DVLOG(1) << "Connection in progress, ignoring reset.";
+ return;
+ }
+
+ if (listener_)
+ listener_->OnDisconnected();
+
+ UMA_HISTOGRAM_ENUMERATION("GCM.ConnectionResetReason",
+ reason,
+ CONNECTION_RESET_COUNT);
+ recorder_->RecordConnectionResetSignaled(reason);
+ if (!last_login_time_.is_null()) {
+ UMA_HISTOGRAM_CUSTOM_TIMES("GCM.ConnectionUpTime",
+ NowTicks() - last_login_time_,
+ base::TimeDelta::FromSeconds(1),
+ base::TimeDelta::FromHours(24),
+ 50);
+ // |last_login_time_| will be reset below, before attempting the new
+ // connection.
+ }
+
+ CloseSocket();
+ DCHECK(!IsEndpointReachable());
+
+ // TODO(zea): if the network is offline, don't attempt to connect.
+ // See crbug.com/396687
+
+ // Network changes get special treatment as they can trigger a one-off canary
+ // request that bypasses backoff (but does nothing if a connection is in
+ // progress). Other connection reset events can be ignored as a connection
+ // is already awaiting backoff expiration.
+ if (waiting_for_backoff_ && reason != NETWORK_CHANGE) {
+ DVLOG(1) << "Backoff expiration pending, ignoring reset.";
+ return;
+ }
+
+ if (logging_in_) {
+ // Failures prior to login completion just reuse the existing backoff entry.
+ logging_in_ = false;
+ backoff_entry_->InformOfRequest(false);
+ } else if (reason == LOGIN_FAILURE ||
+ ShouldRestorePreviousBackoff(last_login_time_, NowTicks())) {
+ // Failures due to login, or within the reset window of a login, restore
+ // the backoff entry that was saved off at login completion time.
+ backoff_entry_.swap(previous_backoff_);
+ backoff_entry_->InformOfRequest(false);
+ } else if (reason == NETWORK_CHANGE) {
+ ConnectImpl(); // Canary attempts bypass backoff without resetting it.
+ return;
+ } else {
+ // We shouldn't be in backoff in thise case.
+ DCHECK_EQ(0, backoff_entry_->failure_count());
+ }
+
+ // At this point the last login time has been consumed or deemed irrelevant,
+ // reset it.
+ last_login_time_ = base::TimeTicks();
+
+ Connect();
+}
+
+void ConnectionFactoryImpl::SetConnectionListener(
+ ConnectionListener* listener) {
+ listener_ = listener;
+}
+
base::TimeTicks ConnectionFactoryImpl::NextRetryAttempt() const {
if (!backoff_entry_)
return base::TimeTicks();
return backoff_entry_->GetReleaseTime();
}
-void ConnectionFactoryImpl::OnConnectionTypeChanged(
+void ConnectionFactoryImpl::OnNetworkChanged(
net::NetworkChangeNotifier::ConnectionType type) {
- if (type == net::NetworkChangeNotifier::CONNECTION_NONE)
+ if (type == net::NetworkChangeNotifier::CONNECTION_NONE) {
+ DVLOG(1) << "Network lost, resettion connection.";
+ waiting_for_network_online_ = true;
+
+ // Will do nothing due to |waiting_for_network_online_ == true|.
+ // TODO(zea): make the above statement actually true. See crbug.com/396687
+ SignalConnectionReset(NETWORK_CHANGE);
return;
+ }
- // TODO(zea): implement different backoff/retry policies based on connection
- // type.
- DVLOG(1) << "Connection type changed to " << type << ", resetting backoff.";
- backoff_entry_->Reset();
- // Connect(..) should be retrying with backoff already if a connection is
- // necessary, so no need to call again.
+ DVLOG(1) << "Connection type changed to " << type << ", reconnecting.";
+ waiting_for_network_online_ = false;
+ SignalConnectionReset(NETWORK_CHANGE);
}
-void ConnectionFactoryImpl::OnIPAddressChanged() {
- DVLOG(1) << "IP Address changed, resetting backoff.";
- backoff_entry_->Reset();
- // Connect(..) should be retrying with backoff already if a connection is
- // necessary, so no need to call again.
+GURL ConnectionFactoryImpl::GetCurrentEndpoint() const {
+ // Note that IsEndpointReachable() returns false anytime connecting_ is true,
+ // so while connecting this always uses |next_endpoint_|.
+ if (IsEndpointReachable())
+ return mcs_endpoints_[last_successful_endpoint_];
+ return mcs_endpoints_[next_endpoint_];
}
-void ConnectionFactoryImpl::ConnectImpl() {
- DCHECK(!IsEndpointReachable());
+net::IPEndPoint ConnectionFactoryImpl::GetPeerIP() {
+ if (!socket_handle_.socket())
+ return net::IPEndPoint();
- // TODO(zea): resolve proxies.
- net::ProxyInfo proxy_info;
- proxy_info.UseDirect();
- net::SSLConfig ssl_config;
- network_session_->ssl_config_service()->GetSSLConfig(&ssl_config);
+ net::IPEndPoint ip_endpoint;
+ int result = socket_handle_.socket()->GetPeerAddress(&ip_endpoint);
+ if (result != net::OK)
+ return net::IPEndPoint();
- int status = net::InitSocketHandleForTlsConnect(
- net::HostPortPair::FromURL(mcs_endpoint_),
- network_session_.get(),
- proxy_info,
- ssl_config,
- ssl_config,
- net::kPrivacyModeDisabled,
- net::BoundNetLog::Make(net_log_, net::NetLog::SOURCE_SOCKET),
- &socket_handle_,
- base::Bind(&ConnectionFactoryImpl::OnConnectDone,
- weak_ptr_factory_.GetWeakPtr()));
+ return ip_endpoint;
+}
+
+void ConnectionFactoryImpl::ConnectImpl() {
+ DCHECK(!IsEndpointReachable());
+ DCHECK(!socket_handle_.socket());
+
+ // TODO(zea): if the network is offline, don't attempt to connect.
+ // See crbug.com/396687
+
+ connecting_ = true;
+ GURL current_endpoint = GetCurrentEndpoint();
+ recorder_->RecordConnectionInitiated(current_endpoint.host());
+ int status = network_session_->proxy_service()->ResolveProxy(
+ current_endpoint,
+ &proxy_info_,
+ base::Bind(&ConnectionFactoryImpl::OnProxyResolveDone,
+ weak_ptr_factory_.GetWeakPtr()),
+ &pac_request_,
+ bound_net_log_);
if (status != net::ERR_IO_PENDING)
- OnConnectDone(status);
+ OnProxyResolveDone(status);
}
void ConnectionFactoryImpl::InitHandler() {
@@ -170,7 +298,7 @@ void ConnectionFactoryImpl::InitHandler() {
DCHECK(login_request.IsInitialized());
}
- connection_handler_->Init(login_request, socket_handle_.PassSocket());
+ connection_handler_->Init(login_request, socket_handle_.socket());
}
scoped_ptr<net::BackoffEntry> ConnectionFactoryImpl::CreateBackoffEntry(
@@ -178,28 +306,238 @@ scoped_ptr<net::BackoffEntry> ConnectionFactoryImpl::CreateBackoffEntry(
return scoped_ptr<net::BackoffEntry>(new net::BackoffEntry(policy));
}
+scoped_ptr<ConnectionHandler> ConnectionFactoryImpl::CreateConnectionHandler(
+ base::TimeDelta read_timeout,
+ const ConnectionHandler::ProtoReceivedCallback& read_callback,
+ const ConnectionHandler::ProtoSentCallback& write_callback,
+ const ConnectionHandler::ConnectionChangedCallback& connection_callback) {
+ return make_scoped_ptr<ConnectionHandler>(
+ new ConnectionHandlerImpl(read_timeout,
+ read_callback,
+ write_callback,
+ connection_callback));
+}
+
+base::TimeTicks ConnectionFactoryImpl::NowTicks() {
+ return base::TimeTicks::Now();
+}
+
void ConnectionFactoryImpl::OnConnectDone(int result) {
if (result != net::OK) {
+ // If the connection fails, try another proxy.
+ result = ReconsiderProxyAfterError(result);
+ // ReconsiderProxyAfterError either returns an error (in which case it is
+ // not reconsidering a proxy) or returns ERR_IO_PENDING if it is considering
+ // another proxy.
+ DCHECK_NE(result, net::OK);
+ if (result == net::ERR_IO_PENDING)
+ return; // Proxy reconsideration pending. Return.
LOG(ERROR) << "Failed to connect to MCS endpoint with error " << result;
+ UMA_HISTOGRAM_BOOLEAN("GCM.ConnectionSuccessRate", false);
+ recorder_->RecordConnectionFailure(result);
+ CloseSocket();
backoff_entry_->InformOfRequest(false);
+ UMA_HISTOGRAM_SPARSE_SLOWLY("GCM.ConnectionFailureErrorCode", result);
+
+ // If there are other endpoints available, use the next endpoint on the
+ // subsequent retry.
+ next_endpoint_++;
+ if (next_endpoint_ >= mcs_endpoints_.size())
+ next_endpoint_ = 0;
+ connecting_ = false;
Connect();
return;
}
- DVLOG(1) << "MCS endpoint connection success.";
+ UMA_HISTOGRAM_BOOLEAN("GCM.ConnectionSuccessRate", true);
+ UMA_HISTOGRAM_COUNTS("GCM.ConnectionEndpoint", next_endpoint_);
+ UMA_HISTOGRAM_BOOLEAN("GCM.ConnectedViaProxy",
+ !(proxy_info_.is_empty() || proxy_info_.is_direct()));
+ ReportSuccessfulProxyConnection();
+ recorder_->RecordConnectionSuccess();
+
+ // Reset the endpoint back to the default.
+ // TODO(zea): consider prioritizing endpoints more intelligently based on
+ // which ones succeed most for this client? Although that will affect
+ // measuring the success rate of the default endpoint vs fallback.
+ last_successful_endpoint_ = next_endpoint_;
+ next_endpoint_ = 0;
+ connecting_ = false;
+ logging_in_ = true;
+ DVLOG(1) << "MCS endpoint socket connection success, starting login.";
+ InitHandler();
+}
- // Reset the backoff.
+void ConnectionFactoryImpl::ConnectionHandlerCallback(int result) {
+ DCHECK(!connecting_);
+ if (result != net::OK) {
+ // TODO(zea): Consider how to handle errors that may require some sort of
+ // user intervention (login page, etc.).
+ UMA_HISTOGRAM_SPARSE_SLOWLY("GCM.ConnectionDisconnectErrorCode", result);
+ SignalConnectionReset(SOCKET_FAILURE);
+ return;
+ }
+
+ // Handshake complete, reset backoff. If the login failed with an error,
+ // the client should invoke SignalConnectionReset(LOGIN_FAILURE), which will
+ // restore the previous backoff.
+ DVLOG(1) << "Handshake complete.";
+ last_login_time_ = NowTicks();
+ previous_backoff_.swap(backoff_entry_);
backoff_entry_->Reset();
+ logging_in_ = false;
- InitHandler();
+ if (listener_)
+ listener_->OnConnected(GetCurrentEndpoint(), GetPeerIP());
}
-void ConnectionFactoryImpl::ConnectionHandlerCallback(int result) {
- // TODO(zea): Consider how to handle errors that may require some sort of
- // user intervention (login page, etc.).
- LOG(ERROR) << "Connection reset with error " << result;
- backoff_entry_->InformOfRequest(false);
- Connect();
+// This has largely been copied from
+// HttpStreamFactoryImpl::Job::DoResolveProxyComplete. This should be
+// refactored into some common place.
+void ConnectionFactoryImpl::OnProxyResolveDone(int status) {
+ pac_request_ = NULL;
+ DVLOG(1) << "Proxy resolution status: " << status;
+
+ DCHECK_NE(status, net::ERR_IO_PENDING);
+ if (status == net::OK) {
+ // Remove unsupported proxies from the list.
+ proxy_info_.RemoveProxiesWithoutScheme(
+ net::ProxyServer::SCHEME_DIRECT |
+ net::ProxyServer::SCHEME_HTTP | net::ProxyServer::SCHEME_HTTPS |
+ net::ProxyServer::SCHEME_SOCKS4 | net::ProxyServer::SCHEME_SOCKS5);
+
+ if (proxy_info_.is_empty()) {
+ // No proxies/direct to choose from. This happens when we don't support
+ // any of the proxies in the returned list.
+ status = net::ERR_NO_SUPPORTED_PROXIES;
+ }
+ }
+
+ if (status != net::OK) {
+ // Failed to resolve proxy. Retry later.
+ OnConnectDone(status);
+ return;
+ }
+
+ DVLOG(1) << "Resolved proxy with PAC:" << proxy_info_.ToPacString();
+
+ net::SSLConfig ssl_config;
+ network_session_->ssl_config_service()->GetSSLConfig(&ssl_config);
+ status = net::InitSocketHandleForTlsConnect(
+ net::HostPortPair::FromURL(GetCurrentEndpoint()),
+ network_session_.get(),
+ proxy_info_,
+ ssl_config,
+ ssl_config,
+ net::PRIVACY_MODE_DISABLED,
+ bound_net_log_,
+ &socket_handle_,
+ base::Bind(&ConnectionFactoryImpl::OnConnectDone,
+ weak_ptr_factory_.GetWeakPtr()));
+ if (status != net::ERR_IO_PENDING)
+ OnConnectDone(status);
+}
+
+// This has largely been copied from
+// HttpStreamFactoryImpl::Job::ReconsiderProxyAfterError. This should be
+// refactored into some common place.
+// This method reconsiders the proxy on certain errors. If it does reconsider
+// a proxy it always returns ERR_IO_PENDING and posts a call to
+// OnProxyResolveDone with the result of the reconsideration.
+int ConnectionFactoryImpl::ReconsiderProxyAfterError(int error) {
+ DCHECK(!pac_request_);
+ DCHECK_NE(error, net::OK);
+ DCHECK_NE(error, net::ERR_IO_PENDING);
+ // A failure to resolve the hostname or any error related to establishing a
+ // TCP connection could be grounds for trying a new proxy configuration.
+ //
+ // Why do this when a hostname cannot be resolved? Some URLs only make sense
+ // to proxy servers. The hostname in those URLs might fail to resolve if we
+ // are still using a non-proxy config. We need to check if a proxy config
+ // now exists that corresponds to a proxy server that could load the URL.
+ //
+ switch (error) {
+ case net::ERR_PROXY_CONNECTION_FAILED:
+ case net::ERR_NAME_NOT_RESOLVED:
+ case net::ERR_INTERNET_DISCONNECTED:
+ case net::ERR_ADDRESS_UNREACHABLE:
+ case net::ERR_CONNECTION_CLOSED:
+ case net::ERR_CONNECTION_TIMED_OUT:
+ case net::ERR_CONNECTION_RESET:
+ case net::ERR_CONNECTION_REFUSED:
+ case net::ERR_CONNECTION_ABORTED:
+ case net::ERR_TIMED_OUT:
+ case net::ERR_TUNNEL_CONNECTION_FAILED:
+ case net::ERR_SOCKS_CONNECTION_FAILED:
+ // This can happen in the case of trying to talk to a proxy using SSL, and
+ // ending up talking to a captive portal that supports SSL instead.
+ case net::ERR_PROXY_CERTIFICATE_INVALID:
+ // This can happen when trying to talk SSL to a non-SSL server (Like a
+ // captive portal).
+ case net::ERR_SSL_PROTOCOL_ERROR:
+ break;
+ case net::ERR_SOCKS_CONNECTION_HOST_UNREACHABLE:
+ // Remap the SOCKS-specific "host unreachable" error to a more
+ // generic error code (this way consumers like the link doctor
+ // know to substitute their error page).
+ //
+ // Note that if the host resolving was done by the SOCKS5 proxy, we can't
+ // differentiate between a proxy-side "host not found" versus a proxy-side
+ // "address unreachable" error, and will report both of these failures as
+ // ERR_ADDRESS_UNREACHABLE.
+ return net::ERR_ADDRESS_UNREACHABLE;
+ default:
+ return error;
+ }
+
+ net::SSLConfig ssl_config;
+ network_session_->ssl_config_service()->GetSSLConfig(&ssl_config);
+ if (proxy_info_.is_https() && ssl_config.send_client_cert) {
+ network_session_->ssl_client_auth_cache()->Remove(
+ proxy_info_.proxy_server().host_port_pair());
+ }
+
+ int status = network_session_->proxy_service()->ReconsiderProxyAfterError(
+ GetCurrentEndpoint(), error, &proxy_info_,
+ base::Bind(&ConnectionFactoryImpl::OnProxyResolveDone,
+ weak_ptr_factory_.GetWeakPtr()),
+ &pac_request_,
+ bound_net_log_);
+ if (status == net::OK || status == net::ERR_IO_PENDING) {
+ CloseSocket();
+ } else {
+ // If ReconsiderProxyAfterError() failed synchronously, it means
+ // there was nothing left to fall-back to, so fail the transaction
+ // with the last connection error we got.
+ status = error;
+ }
+
+ // If there is new proxy info, post OnProxyResolveDone to retry it. Otherwise,
+ // if there was an error falling back, fail synchronously.
+ if (status == net::OK) {
+ base::MessageLoop::current()->PostTask(
+ FROM_HERE,
+ base::Bind(&ConnectionFactoryImpl::OnProxyResolveDone,
+ weak_ptr_factory_.GetWeakPtr(), status));
+ status = net::ERR_IO_PENDING;
+ }
+ return status;
+}
+
+void ConnectionFactoryImpl::ReportSuccessfulProxyConnection() {
+ if (network_session_ && network_session_->proxy_service())
+ network_session_->proxy_service()->ReportSuccess(proxy_info_);
+}
+
+void ConnectionFactoryImpl::CloseSocket() {
+ // The connection handler needs to be reset, else it'll attempt to keep using
+ // the destroyed socket.
+ if (connection_handler_)
+ connection_handler_->Reset();
+
+ if (socket_handle_.socket() && socket_handle_.socket()->IsConnected())
+ socket_handle_.socket()->Disconnect();
+ socket_handle_.Reset();
}
} // namespace gcm
diff --git a/chromium/google_apis/gcm/engine/connection_factory_impl.h b/chromium/google_apis/gcm/engine/connection_factory_impl.h
index d807270bfdc..80fbc94ea8c 100644
--- a/chromium/google_apis/gcm/engine/connection_factory_impl.h
+++ b/chromium/google_apis/gcm/engine/connection_factory_impl.h
@@ -8,9 +8,12 @@
#include "google_apis/gcm/engine/connection_factory.h"
#include "base/memory/weak_ptr.h"
+#include "base/time/time.h"
#include "google_apis/gcm/protocol/mcs.pb.h"
#include "net/base/backoff_entry.h"
#include "net/base/network_change_notifier.h"
+#include "net/proxy/proxy_info.h"
+#include "net/proxy/proxy_service.h"
#include "net/socket/client_socket_handle.h"
#include "url/gurl.h"
@@ -22,16 +25,18 @@ class NetLog;
namespace gcm {
class ConnectionHandlerImpl;
+class GCMStatsRecorder;
class GCM_EXPORT ConnectionFactoryImpl :
public ConnectionFactory,
- public net::NetworkChangeNotifier::ConnectionTypeObserver,
- public net::NetworkChangeNotifier::IPAddressObserver {
+ public net::NetworkChangeNotifier::NetworkChangeObserver {
public:
ConnectionFactoryImpl(
- const GURL& mcs_endpoint,
+ const std::vector<GURL>& mcs_endpoints,
+ const net::BackoffEntry::Policy& backoff_policy,
scoped_refptr<net::HttpNetworkSession> network_session,
- net::NetLog* net_log);
+ net::NetLog* net_log,
+ GCMStatsRecorder* recorder);
virtual ~ConnectionFactoryImpl();
// ConnectionFactory implementation.
@@ -42,12 +47,23 @@ class GCM_EXPORT ConnectionFactoryImpl :
virtual ConnectionHandler* GetConnectionHandler() const OVERRIDE;
virtual void Connect() OVERRIDE;
virtual bool IsEndpointReachable() const OVERRIDE;
+ virtual std::string GetConnectionStateString() const OVERRIDE;
virtual base::TimeTicks NextRetryAttempt() const OVERRIDE;
+ virtual void SignalConnectionReset(ConnectionResetReason reason) OVERRIDE;
+ virtual void SetConnectionListener(ConnectionListener* listener) OVERRIDE;
- // NetworkChangeNotifier observer implementations.
- virtual void OnConnectionTypeChanged(
+ // NetworkChangeObserver implementation.
+ virtual void OnNetworkChanged(
net::NetworkChangeNotifier::ConnectionType type) OVERRIDE;
- virtual void OnIPAddressChanged() OVERRIDE;
+
+ // Returns the server to which the factory is currently connected, or if
+ // a connection is currently pending, the server to which the next connection
+ // attempt will be made.
+ GURL GetCurrentEndpoint() const;
+
+ // Returns the IPEndpoint to which the factory is currently connected. If no
+ // connection is active, returns an empty IPEndpoint.
+ net::IPEndPoint GetPeerIP();
protected:
// Implementation of Connect(..). If not in backoff, uses |login_request_|
@@ -65,32 +81,98 @@ class GCM_EXPORT ConnectionFactoryImpl :
virtual scoped_ptr<net::BackoffEntry> CreateBackoffEntry(
const net::BackoffEntry::Policy* const policy);
+ // Helper method for creating the connection handler.
+ // Virtual for testing.
+ virtual scoped_ptr<ConnectionHandler> CreateConnectionHandler(
+ base::TimeDelta read_timeout,
+ const ConnectionHandler::ProtoReceivedCallback& read_callback,
+ const ConnectionHandler::ProtoSentCallback& write_callback,
+ const ConnectionHandler::ConnectionChangedCallback& connection_callback);
+
+ // Returns the current time in Ticks.
+ // Virtual for testing.
+ virtual base::TimeTicks NowTicks();
+
// Callback for Socket connection completion.
void OnConnectDone(int result);
- private:
// ConnectionHandler callback for connection issues.
void ConnectionHandlerCallback(int result);
- // The MCS endpoint to make connections to.
- const GURL mcs_endpoint_;
+ private:
+ // Helper method for checking backoff and triggering a connection as
+ // necessary.
+ void ConnectWithBackoff();
+
+ // Proxy resolution and connection functions.
+ void OnProxyResolveDone(int status);
+ void OnProxyConnectDone(int status);
+ int ReconsiderProxyAfterError(int error);
+ void ReportSuccessfulProxyConnection();
+
+ void CloseSocket();
+
+ // The MCS endpoints to make connections to, sorted in order of priority.
+ const std::vector<GURL> mcs_endpoints_;
+ // Index to the endpoint for which a connection should be attempted next.
+ size_t next_endpoint_;
+ // Index to the endpoint that was last successfully connected.
+ size_t last_successful_endpoint_;
+
+ // The backoff policy to use.
+ const net::BackoffEntry::Policy backoff_policy_;
// ---- net:: components for establishing connections. ----
// Network session for creating new connections.
const scoped_refptr<net::HttpNetworkSession> network_session_;
// Net log to use in connection attempts.
- net::NetLog* const net_log_;
+ net::BoundNetLog bound_net_log_;
+ // The current PAC request, if one exists. Owned by the proxy service.
+ net::ProxyService::PacRequest* pac_request_;
+ // The current proxy info.
+ net::ProxyInfo proxy_info_;
// The handle to the socket for the current connection, if one exists.
net::ClientSocketHandle socket_handle_;
- // Connection attempt backoff policy.
+ // Current backoff entry.
scoped_ptr<net::BackoffEntry> backoff_entry_;
+ // Backoff entry from previous connection attempt. Updated on each login
+ // completion.
+ scoped_ptr<net::BackoffEntry> previous_backoff_;
+
+ // Whether a connection attempt is currently actively in progress.
+ bool connecting_;
+
+ // Whether the client is waiting for backoff to finish before attempting to
+ // connect. Canary jobs are able to preempt connections pending backoff
+ // expiration.
+ bool waiting_for_backoff_;
+
+ // Whether the NetworkChangeNotifier has informed the client that there is
+ // no current connection. No connection attempts will be made until the
+ // client is informed of a valid connection type.
+ bool waiting_for_network_online_;
+
+ // Whether login successfully completed after the connection was established.
+ // If a connection reset happens while attempting to log in, the current
+ // backoff entry is reused (after incrementing with a new failure).
+ bool logging_in_;
+
+ // The time of the last login completion. Used for calculating whether to
+ // restore a previous backoff entry and for measuring uptime.
+ base::TimeTicks last_login_time_;
// The current connection handler, if one exists.
- scoped_ptr<ConnectionHandlerImpl> connection_handler_;
+ scoped_ptr<ConnectionHandler> connection_handler_;
// Builder for generating new login requests.
BuildLoginRequestCallback request_builder_;
+ // Recorder that records GCM activities for debugging purpose. Not owned.
+ GCMStatsRecorder* recorder_;
+
+ // Listener for connection change events.
+ ConnectionListener* listener_;
+
base::WeakPtrFactory<ConnectionFactoryImpl> weak_ptr_factory_;
DISALLOW_COPY_AND_ASSIGN(ConnectionFactoryImpl);
diff --git a/chromium/google_apis/gcm/engine/connection_factory_impl_unittest.cc b/chromium/google_apis/gcm/engine/connection_factory_impl_unittest.cc
index 1e0ccefef26..fda42a1f93f 100644
--- a/chromium/google_apis/gcm/engine/connection_factory_impl_unittest.cc
+++ b/chromium/google_apis/gcm/engine/connection_factory_impl_unittest.cc
@@ -8,7 +8,10 @@
#include "base/message_loop/message_loop.h"
#include "base/run_loop.h"
-#include "base/time/time.h"
+#include "base/test/simple_test_tick_clock.h"
+#include "google_apis/gcm/base/mcs_util.h"
+#include "google_apis/gcm/engine/fake_connection_handler.h"
+#include "google_apis/gcm/monitoring/fake_gcm_stats_recorder.h"
#include "net/base/backoff_entry.h"
#include "net/http/http_network_session.h"
#include "testing/gtest/include/gtest/gtest.h"
@@ -19,6 +22,7 @@ namespace gcm {
namespace {
const char kMCSEndpoint[] = "http://my.server";
+const char kMCSEndpoint2[] = "http://my.alt.server";
const int kBackoffDelayMs = 1;
const int kBackoffMultiplier = 2;
@@ -50,6 +54,13 @@ const net::BackoffEntry::Policy kTestBackoffPolicy = {
false,
};
+std::vector<GURL> BuildEndpoints() {
+ std::vector<GURL> endpoints;
+ endpoints.push_back(GURL(kMCSEndpoint));
+ endpoints.push_back(GURL(kMCSEndpoint2));
+ return endpoints;
+}
+
// Helper for calculating total expected exponential backoff delay given an
// arbitrary number of failed attempts. See BackoffEntry::CalculateReleaseTime.
double CalculateBackoff(int num_attempts) {
@@ -62,15 +73,33 @@ double CalculateBackoff(int num_attempts) {
return delay;
}
-// Helper methods that should never actually be called due to real connections
-// being stubbed out.
void ReadContinuation(
scoped_ptr<google::protobuf::MessageLite> message) {
- ADD_FAILURE();
}
void WriteContinuation() {
- ADD_FAILURE();
+}
+
+class TestBackoffEntry : public net::BackoffEntry {
+ public:
+ explicit TestBackoffEntry(base::SimpleTestTickClock* tick_clock);
+ virtual ~TestBackoffEntry();
+
+ virtual base::TimeTicks ImplGetTimeNow() const OVERRIDE;
+
+ private:
+ base::SimpleTestTickClock* tick_clock_;
+};
+
+TestBackoffEntry::TestBackoffEntry(base::SimpleTestTickClock* tick_clock)
+ : BackoffEntry(&kTestBackoffPolicy),
+ tick_clock_(tick_clock) {
+}
+
+TestBackoffEntry::~TestBackoffEntry() {}
+
+base::TimeTicks TestBackoffEntry::ImplGetTimeNow() const {
+ return tick_clock_->NowTicks();
}
// A connection factory that stubs out network requests and overrides the
@@ -80,18 +109,34 @@ class TestConnectionFactoryImpl : public ConnectionFactoryImpl {
TestConnectionFactoryImpl(const base::Closure& finished_callback);
virtual ~TestConnectionFactoryImpl();
+ void InitializeFactory();
+
// Overridden stubs.
virtual void ConnectImpl() OVERRIDE;
virtual void InitHandler() OVERRIDE;
virtual scoped_ptr<net::BackoffEntry> CreateBackoffEntry(
const net::BackoffEntry::Policy* const policy) OVERRIDE;
+ virtual scoped_ptr<ConnectionHandler> CreateConnectionHandler(
+ base::TimeDelta read_timeout,
+ const ConnectionHandler::ProtoReceivedCallback& read_callback,
+ const ConnectionHandler::ProtoSentCallback& write_callback,
+ const ConnectionHandler::ConnectionChangedCallback& connection_callback)
+ OVERRIDE;
+ virtual base::TimeTicks NowTicks() OVERRIDE;
// Helpers for verifying connection attempts are made. Connection results
// must be consumed.
void SetConnectResult(int connect_result);
void SetMultipleConnectResults(int connect_result, int num_expected_attempts);
+ // Force a login handshake to be delayed.
+ void SetDelayLogin(bool delay_login);
+
+ base::SimpleTestTickClock* tick_clock() { return &tick_clock_; }
+
private:
+ // Clock for controlling delay.
+ base::SimpleTestTickClock tick_clock_;
// The result to return on the next connect attempt.
int connect_result_;
// The number of expected connection attempts;
@@ -99,17 +144,30 @@ class TestConnectionFactoryImpl : public ConnectionFactoryImpl {
// Whether all expected connection attempts have been fulfilled since an
// expectation was last set.
bool connections_fulfilled_;
+ // Whether to delay a login handshake completion or not.
+ bool delay_login_;
// Callback to invoke when all connection attempts have been made.
base::Closure finished_callback_;
+ // The current fake connection handler..
+ FakeConnectionHandler* fake_handler_;
+ FakeGCMStatsRecorder dummy_recorder_;
};
TestConnectionFactoryImpl::TestConnectionFactoryImpl(
const base::Closure& finished_callback)
- : ConnectionFactoryImpl(GURL(kMCSEndpoint), NULL, NULL),
- connect_result_(net::ERR_UNEXPECTED),
- num_expected_attempts_(0),
- connections_fulfilled_(true),
- finished_callback_(finished_callback) {
+ : ConnectionFactoryImpl(BuildEndpoints(),
+ net::BackoffEntry::Policy(),
+ NULL,
+ NULL,
+ &dummy_recorder_),
+ connect_result_(net::ERR_UNEXPECTED),
+ num_expected_attempts_(0),
+ connections_fulfilled_(true),
+ delay_login_(false),
+ finished_callback_(finished_callback),
+ fake_handler_(NULL) {
+ // Set a non-null time.
+ tick_clock_.Advance(base::TimeDelta::FromMilliseconds(1));
}
TestConnectionFactoryImpl::~TestConnectionFactoryImpl() {
@@ -118,8 +176,15 @@ TestConnectionFactoryImpl::~TestConnectionFactoryImpl() {
void TestConnectionFactoryImpl::ConnectImpl() {
ASSERT_GT(num_expected_attempts_, 0);
-
+ scoped_ptr<mcs_proto::LoginRequest> request(BuildLoginRequest(0, 0, ""));
+ GetConnectionHandler()->Init(*request, NULL);
OnConnectDone(connect_result_);
+ if (!NextRetryAttempt().is_null()) {
+ // Advance the time to the next retry time.
+ base::TimeDelta time_till_retry =
+ NextRetryAttempt() - tick_clock_.NowTicks();
+ tick_clock_.Advance(time_till_retry);
+ }
--num_expected_attempts_;
if (num_expected_attempts_ == 0) {
connect_result_ = net::ERR_UNEXPECTED;
@@ -130,12 +195,29 @@ void TestConnectionFactoryImpl::ConnectImpl() {
void TestConnectionFactoryImpl::InitHandler() {
EXPECT_NE(connect_result_, net::ERR_UNEXPECTED);
+ if (!delay_login_)
+ ConnectionHandlerCallback(net::OK);
}
scoped_ptr<net::BackoffEntry> TestConnectionFactoryImpl::CreateBackoffEntry(
const net::BackoffEntry::Policy* const policy) {
- return scoped_ptr<net::BackoffEntry>(
- new net::BackoffEntry(&kTestBackoffPolicy));
+ return scoped_ptr<net::BackoffEntry>(new TestBackoffEntry(&tick_clock_));
+}
+
+scoped_ptr<ConnectionHandler>
+TestConnectionFactoryImpl::CreateConnectionHandler(
+ base::TimeDelta read_timeout,
+ const ConnectionHandler::ProtoReceivedCallback& read_callback,
+ const ConnectionHandler::ProtoSentCallback& write_callback,
+ const ConnectionHandler::ConnectionChangedCallback& connection_callback) {
+ fake_handler_ = new FakeConnectionHandler(
+ base::Bind(&ReadContinuation),
+ base::Bind(&WriteContinuation));
+ return make_scoped_ptr<ConnectionHandler>(fake_handler_);
+}
+
+base::TimeTicks TestConnectionFactoryImpl::NowTicks() {
+ return tick_clock_.NowTicks();
}
void TestConnectionFactoryImpl::SetConnectResult(int connect_result) {
@@ -144,6 +226,10 @@ void TestConnectionFactoryImpl::SetConnectResult(int connect_result) {
connections_fulfilled_ = false;
connect_result_ = connect_result;
num_expected_attempts_ = 1;
+ fake_handler_->ExpectOutgoingMessage(
+ MCSMessage(kLoginRequestTag,
+ BuildLoginRequest(0, 0, "").PassAs<
+ const google::protobuf::MessageLite>()));
}
void TestConnectionFactoryImpl::SetMultipleConnectResults(
@@ -155,29 +241,58 @@ void TestConnectionFactoryImpl::SetMultipleConnectResults(
connections_fulfilled_ = false;
connect_result_ = connect_result;
num_expected_attempts_ = num_expected_attempts;
+ for (int i = 0 ; i < num_expected_attempts; ++i) {
+ fake_handler_->ExpectOutgoingMessage(
+ MCSMessage(kLoginRequestTag,
+ BuildLoginRequest(0, 0, "").PassAs<
+ const google::protobuf::MessageLite>()));
+ }
+}
+
+void TestConnectionFactoryImpl::SetDelayLogin(bool delay_login) {
+ delay_login_ = delay_login;
+ fake_handler_->set_fail_login(delay_login_);
}
-class ConnectionFactoryImplTest : public testing::Test {
+} // namespace
+
+class ConnectionFactoryImplTest
+ : public testing::Test,
+ public ConnectionFactory::ConnectionListener {
public:
ConnectionFactoryImplTest();
virtual ~ConnectionFactoryImplTest();
TestConnectionFactoryImpl* factory() { return &factory_; }
+ GURL& connected_server() { return connected_server_; }
void WaitForConnections();
+ // ConnectionFactory::ConnectionListener
+ virtual void OnConnected(const GURL& current_server,
+ const net::IPEndPoint& ip_endpoint) OVERRIDE;
+ virtual void OnDisconnected() OVERRIDE;
+
private:
void ConnectionsComplete();
TestConnectionFactoryImpl factory_;
base::MessageLoop message_loop_;
scoped_ptr<base::RunLoop> run_loop_;
+
+ GURL connected_server_;
};
ConnectionFactoryImplTest::ConnectionFactoryImplTest()
: factory_(base::Bind(&ConnectionFactoryImplTest::ConnectionsComplete,
base::Unretained(this))),
- run_loop_(new base::RunLoop()) {}
+ run_loop_(new base::RunLoop()) {
+ factory()->SetConnectionListener(this);
+ factory()->Initialize(
+ ConnectionFactory::BuildLoginRequestCallback(),
+ ConnectionHandler::ProtoReceivedCallback(),
+ ConnectionHandler::ProtoSentCallback());
+}
ConnectionFactoryImplTest::~ConnectionFactoryImplTest() {}
void ConnectionFactoryImplTest::WaitForConnections() {
@@ -191,73 +306,75 @@ void ConnectionFactoryImplTest::ConnectionsComplete() {
run_loop_->Quit();
}
+void ConnectionFactoryImplTest::OnConnected(
+ const GURL& current_server,
+ const net::IPEndPoint& ip_endpoint) {
+ connected_server_ = current_server;
+}
+
+void ConnectionFactoryImplTest::OnDisconnected() {
+ connected_server_ = GURL();
+}
+
// Verify building a connection handler works.
TEST_F(ConnectionFactoryImplTest, Initialize) {
- EXPECT_FALSE(factory()->IsEndpointReachable());
- factory()->Initialize(
- ConnectionFactory::BuildLoginRequestCallback(),
- base::Bind(&ReadContinuation),
- base::Bind(&WriteContinuation));
ConnectionHandler* handler = factory()->GetConnectionHandler();
ASSERT_TRUE(handler);
EXPECT_FALSE(factory()->IsEndpointReachable());
+ EXPECT_FALSE(connected_server().is_valid());
}
// An initial successful connection should not result in backoff.
TEST_F(ConnectionFactoryImplTest, ConnectSuccess) {
- factory()->Initialize(
- ConnectionFactory::BuildLoginRequestCallback(),
- ConnectionHandler::ProtoReceivedCallback(),
- ConnectionHandler::ProtoSentCallback());
factory()->SetConnectResult(net::OK);
factory()->Connect();
EXPECT_TRUE(factory()->NextRetryAttempt().is_null());
+ EXPECT_EQ(factory()->GetCurrentEndpoint(), BuildEndpoints()[0]);
+ EXPECT_TRUE(factory()->IsEndpointReachable());
+ EXPECT_TRUE(connected_server().is_valid());
}
-// A connection failure should result in backoff.
+// A connection failure should result in backoff, and attempting the fallback
+// endpoint next.
TEST_F(ConnectionFactoryImplTest, ConnectFail) {
- factory()->Initialize(
- ConnectionFactory::BuildLoginRequestCallback(),
- ConnectionHandler::ProtoReceivedCallback(),
- ConnectionHandler::ProtoSentCallback());
factory()->SetConnectResult(net::ERR_CONNECTION_FAILED);
factory()->Connect();
EXPECT_FALSE(factory()->NextRetryAttempt().is_null());
+ EXPECT_EQ(factory()->GetCurrentEndpoint(), BuildEndpoints()[1]);
+ EXPECT_FALSE(factory()->IsEndpointReachable());
+ EXPECT_FALSE(connected_server().is_valid());
}
// A connection success after a failure should reset backoff.
TEST_F(ConnectionFactoryImplTest, FailThenSucceed) {
- factory()->Initialize(
- ConnectionFactory::BuildLoginRequestCallback(),
- ConnectionHandler::ProtoReceivedCallback(),
- ConnectionHandler::ProtoSentCallback());
factory()->SetConnectResult(net::ERR_CONNECTION_FAILED);
- base::TimeTicks connect_time = base::TimeTicks::Now();
+ base::TimeTicks connect_time = factory()->tick_clock()->NowTicks();
factory()->Connect();
WaitForConnections();
+ EXPECT_FALSE(factory()->IsEndpointReachable());
+ EXPECT_FALSE(connected_server().is_valid());
base::TimeTicks retry_time = factory()->NextRetryAttempt();
EXPECT_FALSE(retry_time.is_null());
EXPECT_GE((retry_time - connect_time).InMilliseconds(), CalculateBackoff(1));
factory()->SetConnectResult(net::OK);
WaitForConnections();
EXPECT_TRUE(factory()->NextRetryAttempt().is_null());
+ EXPECT_TRUE(factory()->IsEndpointReachable());
+ EXPECT_TRUE(connected_server().is_valid());
}
// Multiple connection failures should retry with an exponentially increasing
// backoff, then reset on success.
TEST_F(ConnectionFactoryImplTest, MultipleFailuresThenSucceed) {
- factory()->Initialize(
- ConnectionFactory::BuildLoginRequestCallback(),
- ConnectionHandler::ProtoReceivedCallback(),
- ConnectionHandler::ProtoSentCallback());
-
const int kNumAttempts = 5;
factory()->SetMultipleConnectResults(net::ERR_CONNECTION_FAILED,
kNumAttempts);
- base::TimeTicks connect_time = base::TimeTicks::Now();
+ base::TimeTicks connect_time = factory()->tick_clock()->NowTicks();
factory()->Connect();
WaitForConnections();
+ EXPECT_FALSE(factory()->IsEndpointReachable());
+ EXPECT_FALSE(connected_server().is_valid());
base::TimeTicks retry_time = factory()->NextRetryAttempt();
EXPECT_FALSE(retry_time.is_null());
EXPECT_GE((retry_time - connect_time).InMilliseconds(),
@@ -266,38 +383,171 @@ TEST_F(ConnectionFactoryImplTest, MultipleFailuresThenSucceed) {
factory()->SetConnectResult(net::OK);
WaitForConnections();
EXPECT_TRUE(factory()->NextRetryAttempt().is_null());
+ EXPECT_TRUE(factory()->IsEndpointReachable());
+ EXPECT_TRUE(connected_server().is_valid());
}
-// IP events should reset backoff.
-TEST_F(ConnectionFactoryImplTest, FailThenIPEvent) {
- factory()->Initialize(
- ConnectionFactory::BuildLoginRequestCallback(),
- ConnectionHandler::ProtoReceivedCallback(),
- ConnectionHandler::ProtoSentCallback());
+// Network change events should trigger canary connections.
+TEST_F(ConnectionFactoryImplTest, FailThenNetworkChangeEvent) {
factory()->SetConnectResult(net::ERR_CONNECTION_FAILED);
factory()->Connect();
WaitForConnections();
+ base::TimeTicks initial_backoff = factory()->NextRetryAttempt();
+ EXPECT_FALSE(initial_backoff.is_null());
+
+ factory()->SetConnectResult(net::ERR_FAILED);
+ factory()->OnNetworkChanged(net::NetworkChangeNotifier::CONNECTION_WIFI);
+ WaitForConnections();
+
+ // Backoff should increase.
+ base::TimeTicks next_backoff = factory()->NextRetryAttempt();
+ EXPECT_GT(next_backoff, initial_backoff);
+ EXPECT_FALSE(factory()->IsEndpointReachable());
+}
+
+// Verify that we reconnect even if a canary succeeded then disconnected while
+// a backoff was pending.
+TEST_F(ConnectionFactoryImplTest, CanarySucceedsThenDisconnects) {
+ factory()->SetConnectResult(net::ERR_CONNECTION_FAILED);
+ factory()->Connect();
+ WaitForConnections();
+ base::TimeTicks initial_backoff = factory()->NextRetryAttempt();
+ EXPECT_FALSE(initial_backoff.is_null());
+
+ factory()->SetConnectResult(net::OK);
+ factory()->OnNetworkChanged(net::NetworkChangeNotifier::CONNECTION_ETHERNET);
+ WaitForConnections();
+ EXPECT_TRUE(factory()->IsEndpointReachable());
+ EXPECT_TRUE(connected_server().is_valid());
+
+ factory()->SetConnectResult(net::OK);
+ factory()->SignalConnectionReset(ConnectionFactory::SOCKET_FAILURE);
+ EXPECT_FALSE(factory()->IsEndpointReachable());
+ EXPECT_FALSE(connected_server().is_valid());
+ WaitForConnections();
+ EXPECT_TRUE(factory()->IsEndpointReachable());
+ EXPECT_TRUE(connected_server().is_valid());
+}
+
+// Verify that if a canary connects, but hasn't finished the handshake, a
+// pending backoff attempt doesn't interrupt the connection.
+TEST_F(ConnectionFactoryImplTest, CanarySucceedsRetryDuringLogin) {
+ factory()->SetConnectResult(net::ERR_CONNECTION_FAILED);
+ factory()->Connect();
+ WaitForConnections();
+ base::TimeTicks initial_backoff = factory()->NextRetryAttempt();
+ EXPECT_FALSE(initial_backoff.is_null());
+
+ factory()->SetDelayLogin(true);
+ factory()->SetConnectResult(net::OK);
+ factory()->OnNetworkChanged(net::NetworkChangeNotifier::CONNECTION_WIFI);
+ WaitForConnections();
+ EXPECT_FALSE(factory()->IsEndpointReachable());
+
+ // Pump the loop, to ensure the pending backoff retry has no effect.
+ base::MessageLoop::current()->PostDelayedTask(
+ FROM_HERE,
+ base::MessageLoop::QuitClosure(),
+ base::TimeDelta::FromMilliseconds(1));
+ WaitForConnections();
+}
+
+// Fail after successful connection via signal reset.
+TEST_F(ConnectionFactoryImplTest, FailViaSignalReset) {
+ factory()->SetConnectResult(net::OK);
+ factory()->Connect();
+ EXPECT_TRUE(factory()->NextRetryAttempt().is_null());
+
+ factory()->SignalConnectionReset(ConnectionFactory::SOCKET_FAILURE);
EXPECT_FALSE(factory()->NextRetryAttempt().is_null());
+ EXPECT_FALSE(factory()->IsEndpointReachable());
+}
- factory()->OnIPAddressChanged();
+TEST_F(ConnectionFactoryImplTest, IgnoreResetWhileConnecting) {
+ factory()->SetConnectResult(net::OK);
+ factory()->Connect();
EXPECT_TRUE(factory()->NextRetryAttempt().is_null());
+
+ factory()->SignalConnectionReset(ConnectionFactory::SOCKET_FAILURE);
+ base::TimeTicks retry_time = factory()->NextRetryAttempt();
+ EXPECT_FALSE(retry_time.is_null());
+ EXPECT_FALSE(factory()->IsEndpointReachable());
+
+ const int kNumAttempts = 5;
+ for (int i = 0; i < kNumAttempts; ++i)
+ factory()->SignalConnectionReset(ConnectionFactory::SOCKET_FAILURE);
+ EXPECT_EQ(retry_time, factory()->NextRetryAttempt());
+ EXPECT_FALSE(factory()->IsEndpointReachable());
}
-// Connection type events should reset backoff.
-TEST_F(ConnectionFactoryImplTest, FailThenConnectionTypeEvent) {
- factory()->Initialize(
- ConnectionFactory::BuildLoginRequestCallback(),
- ConnectionHandler::ProtoReceivedCallback(),
- ConnectionHandler::ProtoSentCallback());
+// Go into backoff due to connection failure. On successful connection, receive
+// a signal reset. The original backoff should be restored and extended, rather
+// than a new backoff starting from scratch.
+TEST_F(ConnectionFactoryImplTest, SignalResetRestoresBackoff) {
factory()->SetConnectResult(net::ERR_CONNECTION_FAILED);
+ base::TimeTicks connect_time = factory()->tick_clock()->NowTicks();
factory()->Connect();
WaitForConnections();
- EXPECT_FALSE(factory()->NextRetryAttempt().is_null());
+ base::TimeTicks retry_time = factory()->NextRetryAttempt();
+ EXPECT_FALSE(retry_time.is_null());
- factory()->OnConnectionTypeChanged(
- net::NetworkChangeNotifier::CONNECTION_WIFI);
+ factory()->SetConnectResult(net::OK);
+ connect_time = factory()->tick_clock()->NowTicks();
+ WaitForConnections();
+ EXPECT_TRUE(factory()->NextRetryAttempt().is_null());
+
+ factory()->SignalConnectionReset(ConnectionFactory::SOCKET_FAILURE);
+ EXPECT_FALSE(factory()->IsEndpointReachable());
+ EXPECT_FALSE(connected_server().is_valid());
+ EXPECT_NE(retry_time, factory()->NextRetryAttempt());
+ retry_time = factory()->NextRetryAttempt();
+ EXPECT_FALSE(retry_time.is_null());
+ EXPECT_GE((retry_time - connect_time).InMilliseconds(),
+ CalculateBackoff(2));
+
+ factory()->SetConnectResult(net::OK);
+ connect_time = factory()->tick_clock()->NowTicks();
+ factory()->tick_clock()->Advance(
+ factory()->NextRetryAttempt() - connect_time);
+ WaitForConnections();
+ EXPECT_TRUE(factory()->NextRetryAttempt().is_null());
+ EXPECT_TRUE(factory()->IsEndpointReachable());
+ EXPECT_TRUE(connected_server().is_valid());
+
+ factory()->SignalConnectionReset(ConnectionFactory::SOCKET_FAILURE);
+ EXPECT_NE(retry_time, factory()->NextRetryAttempt());
+ retry_time = factory()->NextRetryAttempt();
+ EXPECT_FALSE(retry_time.is_null());
+ EXPECT_GE((retry_time - connect_time).InMilliseconds(),
+ CalculateBackoff(3));
+ EXPECT_FALSE(factory()->IsEndpointReachable());
+ EXPECT_FALSE(connected_server().is_valid());
+}
+
+// When the network is disconnected, close the socket and suppress further
+// connection attempts until the network returns.
+// Disabled while crbug.com/396687 is being investigated.
+TEST_F(ConnectionFactoryImplTest, DISABLED_SuppressConnectWhenNoNetwork) {
+ factory()->SetConnectResult(net::OK);
+ factory()->Connect();
+ EXPECT_TRUE(factory()->NextRetryAttempt().is_null());
+ EXPECT_TRUE(factory()->IsEndpointReachable());
+
+ // Advance clock so the login window reset isn't encountered.
+ factory()->tick_clock()->Advance(base::TimeDelta::FromSeconds(11));
+
+ // Will trigger reset, but will not attempt a new connection.
+ factory()->OnNetworkChanged(net::NetworkChangeNotifier::CONNECTION_NONE);
+ EXPECT_FALSE(factory()->IsEndpointReachable());
+ EXPECT_TRUE(factory()->NextRetryAttempt().is_null());
+
+ // When the network returns, attempt to connect.
+ factory()->SetConnectResult(net::OK);
+ factory()->OnNetworkChanged(net::NetworkChangeNotifier::CONNECTION_4G);
+ WaitForConnections();
+
+ EXPECT_TRUE(factory()->IsEndpointReachable());
EXPECT_TRUE(factory()->NextRetryAttempt().is_null());
}
-} // namespace
} // namespace gcm
diff --git a/chromium/google_apis/gcm/engine/connection_handler.h b/chromium/google_apis/gcm/engine/connection_handler.h
index 5b9ea715c78..7b265f9d3bc 100644
--- a/chromium/google_apis/gcm/engine/connection_handler.h
+++ b/chromium/google_apis/gcm/engine/connection_handler.h
@@ -48,7 +48,11 @@ class GCM_EXPORT ConnectionHandler {
// Note: It is correct and expected to call Init more than once, as connection
// issues are encountered and new connections must be made.
virtual void Init(const mcs_proto::LoginRequest& login_request,
- scoped_ptr<net::StreamSocket> socket) = 0;
+ net::StreamSocket* socket) = 0;
+
+ // Resets the handler and any internal state. Should be called any time
+ // a connection reset happens externally to the handler.
+ virtual void Reset() = 0;
// Checks that a handshake has been completed and a message is not already
// in flight.
diff --git a/chromium/google_apis/gcm/engine/connection_handler_impl.cc b/chromium/google_apis/gcm/engine/connection_handler_impl.cc
index aff0dfd3651..d47e0f25b79 100644
--- a/chromium/google_apis/gcm/engine/connection_handler_impl.cc
+++ b/chromium/google_apis/gcm/engine/connection_handler_impl.cc
@@ -27,8 +27,7 @@ const int kSizePacketLenMin = 1;
const int kSizePacketLenMax = 2;
// The current MCS protocol version.
-// TODO(zea): bump to 41 once the server supports it.
-const int kMCSVersion = 38;
+const int kMCSVersion = 41;
} // namespace
@@ -38,6 +37,7 @@ ConnectionHandlerImpl::ConnectionHandlerImpl(
const ProtoSentCallback& write_callback,
const ConnectionChangedCallback& connection_callback)
: read_timeout_(read_timeout),
+ socket_(NULL),
handshake_complete_(false),
message_tag_(0),
message_size_(0),
@@ -52,7 +52,7 @@ ConnectionHandlerImpl::~ConnectionHandlerImpl() {
void ConnectionHandlerImpl::Init(
const mcs_proto::LoginRequest& login_request,
- scoped_ptr<net::StreamSocket> socket) {
+ net::StreamSocket* socket) {
DCHECK(!read_callback_.is_null());
DCHECK(!write_callback_.is_null());
DCHECK(!connection_callback_.is_null());
@@ -63,13 +63,17 @@ void ConnectionHandlerImpl::Init(
handshake_complete_ = false;
message_tag_ = 0;
message_size_ = 0;
- socket_ = socket.Pass();
- input_stream_.reset(new SocketInputStream(socket_.get()));
- output_stream_.reset(new SocketOutputStream(socket_.get()));
+ socket_ = socket;
+ input_stream_.reset(new SocketInputStream(socket_));
+ output_stream_.reset(new SocketOutputStream(socket_));
Login(login_request);
}
+void ConnectionHandlerImpl::Reset() {
+ CloseConnection();
+}
+
bool ConnectionHandlerImpl::CanSendMessage() const {
return handshake_complete_ && output_stream_.get() &&
output_stream_->GetState() == SocketOutputStream::EMPTY;
@@ -180,9 +184,9 @@ void ConnectionHandlerImpl::WaitForData(ProcessingState state) {
}
// Used to determine whether a Socket::Read is necessary.
- int min_bytes_needed = 0;
+ size_t min_bytes_needed = 0;
// Used to limit the size of the Socket::Read.
- int max_bytes_needed = 0;
+ size_t max_bytes_needed = 0;
switch(state) {
case MCS_VERSION_TAG_AND_SIZE:
@@ -210,20 +214,20 @@ void ConnectionHandlerImpl::WaitForData(ProcessingState state) {
}
DCHECK_GE(max_bytes_needed, min_bytes_needed);
- int byte_count = input_stream_->UnreadByteCount();
- if (min_bytes_needed - byte_count > 0 &&
+ size_t unread_byte_count = input_stream_->UnreadByteCount();
+ if (min_bytes_needed > unread_byte_count &&
input_stream_->Refresh(
base::Bind(&ConnectionHandlerImpl::WaitForData,
weak_ptr_factory_.GetWeakPtr(),
state),
- max_bytes_needed - byte_count) == net::ERR_IO_PENDING) {
+ max_bytes_needed - unread_byte_count) == net::ERR_IO_PENDING) {
return;
}
// Check for refresh errors.
if (input_stream_->GetState() != SocketInputStream::READY) {
// An error occurred.
- int last_error = output_stream_->last_error();
+ int last_error = input_stream_->last_error();
CloseConnection();
// If the socket stream had an error, plumb it up, else plumb up FAILED.
if (last_error == net::OK)
@@ -232,6 +236,20 @@ void ConnectionHandlerImpl::WaitForData(ProcessingState state) {
return;
}
+ // Check whether read is complete, or needs to be continued (
+ // SocketInputStream::Refresh can finish without reading all the data).
+ if (input_stream_->UnreadByteCount() < min_bytes_needed) {
+ DVLOG(1) << "Socket read finished prematurely. Waiting for "
+ << min_bytes_needed - input_stream_->UnreadByteCount()
+ << " more bytes.";
+ base::MessageLoop::current()->PostTask(
+ FROM_HERE,
+ base::Bind(&ConnectionHandlerImpl::WaitForData,
+ weak_ptr_factory_.GetWeakPtr(),
+ MCS_PROTO_BYTES));
+ return;
+ }
+
// Received enough bytes, process them.
DVLOG(1) << "Processing MCS data: state == " << state;
switch(state) {
@@ -258,7 +276,8 @@ void ConnectionHandlerImpl::OnGotVersion() {
CodedInputStream coded_input_stream(input_stream_.get());
coded_input_stream.ReadRaw(&version, 1);
}
- if (version < kMCSVersion) {
+ // TODO(zea): remove this when the server is ready.
+ if (version < kMCSVersion && version != 38) {
LOG(ERROR) << "Invalid GCM version response: " << static_cast<int>(version);
connection_callback_.Run(net::ERR_FAILED);
return;
@@ -352,18 +371,18 @@ void ConnectionHandlerImpl::OnGotMessageBytes() {
input_stream_->GetState() != SocketInputStream::READY) {
LOG(ERROR) << "Failed to extract protobuf bytes of type "
<< static_cast<unsigned int>(message_tag_);
- protobuf.reset(); // Return a null pointer to denote an error.
- read_callback_.Run(protobuf.Pass());
+ // Reset the connection.
+ connection_callback_.Run(net::ERR_FAILED);
return;
}
{
CodedInputStream coded_input_stream(input_stream_.get());
if (!protobuf->ParsePartialFromCodedStream(&coded_input_stream)) {
- NOTREACHED() << "Unable to parse GCM message of type "
- << static_cast<unsigned int>(message_tag_);
- protobuf.reset(); // Return a null pointer to denote an error.
- read_callback_.Run(protobuf.Pass());
+ LOG(ERROR) << "Unable to parse GCM message of type "
+ << static_cast<unsigned int>(message_tag_);
+ // Reset the connection.
+ connection_callback_.Run(net::ERR_FAILED);
return;
}
}
@@ -379,6 +398,7 @@ void ConnectionHandlerImpl::OnGotMessageBytes() {
} else {
handshake_complete_ = true;
DVLOG(1) << "GCM Handshake complete.";
+ connection_callback_.Run(net::OK);
}
}
read_callback_.Run(protobuf.Pass());
@@ -392,10 +412,13 @@ void ConnectionHandlerImpl::OnTimeout() {
void ConnectionHandlerImpl::CloseConnection() {
DVLOG(1) << "Closing connection.";
- read_callback_.Reset();
- write_callback_.Reset();
read_timeout_timer_.Stop();
- socket_->Disconnect();
+ if (socket_)
+ socket_->Disconnect();
+ socket_ = NULL;
+ handshake_complete_ = false;
+ message_tag_ = 0;
+ message_size_ = 0;
input_stream_.reset();
output_stream_.reset();
weak_ptr_factory_.InvalidateWeakPtrs();
diff --git a/chromium/google_apis/gcm/engine/connection_handler_impl.h b/chromium/google_apis/gcm/engine/connection_handler_impl.h
index 110cdcdddda..9f9daa94e39 100644
--- a/chromium/google_apis/gcm/engine/connection_handler_impl.h
+++ b/chromium/google_apis/gcm/engine/connection_handler_impl.h
@@ -34,7 +34,8 @@ class GCM_EXPORT ConnectionHandlerImpl : public ConnectionHandler {
// ConnectionHandler implementation.
virtual void Init(const mcs_proto::LoginRequest& login_request,
- scoped_ptr<net::StreamSocket> socket) OVERRIDE;
+ net::StreamSocket* socket) OVERRIDE;
+ virtual void Reset() OVERRIDE;
virtual bool CanSendMessage() const OVERRIDE;
virtual void SendMessage(const google::protobuf::MessageLite& message)
OVERRIDE;
@@ -96,7 +97,7 @@ class GCM_EXPORT ConnectionHandlerImpl : public ConnectionHandler {
base::OneShotTimer<ConnectionHandlerImpl> read_timeout_timer_;
// This connection's socket and the input/output streams attached to it.
- scoped_ptr<net::StreamSocket> socket_;
+ net::StreamSocket* socket_;
scoped_ptr<SocketInputStream> input_stream_;
scoped_ptr<SocketOutputStream> output_stream_;
diff --git a/chromium/google_apis/gcm/engine/connection_handler_impl_unittest.cc b/chromium/google_apis/gcm/engine/connection_handler_impl_unittest.cc
index 0cdcdc621ff..6b89644462c 100644
--- a/chromium/google_apis/gcm/engine/connection_handler_impl_unittest.cc
+++ b/chromium/google_apis/gcm/engine/connection_handler_impl_unittest.cc
@@ -27,7 +27,7 @@ typedef std::vector<net::MockWrite> WriteList;
const uint64 kAuthId = 54321;
const uint64 kAuthToken = 12345;
-const char kMCSVersion = 38; // The protocol version.
+const char kMCSVersion = 41; // The protocol version.
const int kMCSPort = 5228; // The server port.
const char kDataMsgFrom[] = "data_from";
const char kDataMsgCategory[] = "data_category";
@@ -49,12 +49,16 @@ const char kDataMsgCategoryLong2[] =
std::string EncodePacket(uint8 tag, const std::string& proto) {
std::string result;
google::protobuf::io::StringOutputStream string_output_stream(&result);
- google::protobuf::io::CodedOutputStream coded_output_stream(
+ {
+ google::protobuf::io::CodedOutputStream coded_output_stream(
&string_output_stream);
- const unsigned char tag_byte[1] = {tag};
- coded_output_stream.WriteRaw(tag_byte, 1);
- coded_output_stream.WriteVarint32(proto.size());
- coded_output_stream.WriteRaw(proto.c_str(), proto.size());
+ const unsigned char tag_byte[1] = { tag };
+ coded_output_stream.WriteRaw(tag_byte, 1);
+ coded_output_stream.WriteVarint32(proto.size());
+ coded_output_stream.WriteRaw(proto.c_str(), proto.size());
+ // ~CodedOutputStream must run before the move constructor at the
+ // return statement. http://crbug.com/338962
+ }
return result;
}
@@ -63,7 +67,8 @@ std::string EncodeHandshakeRequest() {
std::string result;
const char version_byte[1] = {kMCSVersion};
result.append(version_byte, 1);
- ScopedMessage login_request(BuildLoginRequest(kAuthId, kAuthToken));
+ ScopedMessage login_request(
+ BuildLoginRequest(kAuthId, kAuthToken, ""));
result.append(EncodePacket(kLoginRequestTag,
login_request->SerializeAsString()));
return result;
@@ -198,8 +203,9 @@ void GCMConnectionHandlerImplTest::Connect(
base::Bind(&GCMConnectionHandlerImplTest::ConnectionContinuation,
base::Unretained(this))));
EXPECT_FALSE(connection_handler()->CanSendMessage());
- connection_handler_->Init(*BuildLoginRequest(kAuthId, kAuthToken),
- socket_.Pass());
+ connection_handler_->Init(
+ *BuildLoginRequest(kAuthId, kAuthToken, ""),
+ socket_.get());
}
void GCMConnectionHandlerImplTest::ReadContinuation(
@@ -376,6 +382,7 @@ TEST_F(GCMConnectionHandlerImplTest, RecvMsg) {
WaitForMessage(); // The data message.
ASSERT_TRUE(received_message.get());
EXPECT_EQ(data_message_proto, received_message->SerializeAsString());
+ EXPECT_EQ(net::OK, last_error());
}
// Verify that if two messages arrive at once, they're treated appropriately.
@@ -413,6 +420,7 @@ TEST_F(GCMConnectionHandlerImplTest, Recv2Msgs) {
WaitForMessage(); // The second data message.
ASSERT_TRUE(received_message.get());
EXPECT_EQ(data_message_proto2, received_message->SerializeAsString());
+ EXPECT_EQ(net::OK, last_error());
}
// Receive a long (>128 bytes) message.
@@ -444,6 +452,46 @@ TEST_F(GCMConnectionHandlerImplTest, RecvLongMsg) {
WaitForMessage(); // The data message.
ASSERT_TRUE(received_message.get());
EXPECT_EQ(data_message_proto, received_message->SerializeAsString());
+ EXPECT_EQ(net::OK, last_error());
+}
+
+// Receive a long (>128 bytes) message in two synchronous parts.
+TEST_F(GCMConnectionHandlerImplTest, RecvLongMsg2Parts) {
+ std::string handshake_request = EncodeHandshakeRequest();
+ WriteList write_list(1, net::MockWrite(net::ASYNC,
+ handshake_request.c_str(),
+ handshake_request.size()));
+ std::string handshake_response = EncodeHandshakeResponse();
+
+ std::string data_message_proto =
+ BuildDataMessage(kDataMsgFromLong, kDataMsgCategoryLong);
+ std::string data_message_pkt =
+ EncodePacket(kDataMessageStanzaTag, data_message_proto);
+ DCHECK_GT(data_message_pkt.size(), 128U);
+ ReadList read_list;
+ read_list.push_back(net::MockRead(net::ASYNC,
+ handshake_response.c_str(),
+ handshake_response.size()));
+
+ int bytes_in_first_message = data_message_pkt.size() / 2;
+ read_list.push_back(net::MockRead(net::SYNCHRONOUS,
+ data_message_pkt.c_str(),
+ bytes_in_first_message));
+ read_list.push_back(net::MockRead(net::SYNCHRONOUS,
+ data_message_pkt.c_str() +
+ bytes_in_first_message,
+ data_message_pkt.size() -
+ bytes_in_first_message));
+ BuildSocket(read_list, write_list);
+
+ ScopedMessage received_message;
+ Connect(&received_message);
+ WaitForMessage(); // The login send.
+ WaitForMessage(); // The login response.
+ WaitForMessage(); // The data message.
+ ASSERT_TRUE(received_message.get());
+ EXPECT_EQ(net::OK, last_error());
+ EXPECT_EQ(data_message_proto, received_message->SerializeAsString());
}
// Receive two long (>128 bytes) message.
@@ -482,6 +530,7 @@ TEST_F(GCMConnectionHandlerImplTest, Recv2LongMsgs) {
WaitForMessage(); // The second data message.
ASSERT_TRUE(received_message.get());
EXPECT_EQ(data_message_proto2, received_message->SerializeAsString());
+ EXPECT_EQ(net::OK, last_error());
}
// Simulate a message where the end of the data does not arrive in time and the
@@ -624,5 +673,37 @@ TEST_F(GCMConnectionHandlerImplTest, SendMsgSocketDisconnected) {
EXPECT_EQ(net::ERR_CONNECTION_CLOSED, last_error());
}
+// Receive a message whose size field was corrupted and is larger than the
+// socket's buffer. Should fail gracefully.
+TEST_F(GCMConnectionHandlerImplTest, CorruptedSize) {
+ std::string handshake_request = EncodeHandshakeRequest();
+ WriteList write_list(1, net::MockWrite(net::ASYNC,
+ handshake_request.c_str(),
+ handshake_request.size()));
+ std::string handshake_response = EncodeHandshakeResponse();
+
+ // Fill a string with 9000 character zero.
+ std::string data_message_proto(9000, '0');
+ std::string data_message_pkt =
+ EncodePacket(kDataMessageStanzaTag, data_message_proto);
+ ReadList read_list;
+ read_list.push_back(net::MockRead(net::ASYNC,
+ handshake_response.c_str(),
+ handshake_response.size()));
+ read_list.push_back(net::MockRead(net::ASYNC,
+ data_message_pkt.c_str(),
+ data_message_pkt.size()));
+ BuildSocket(read_list, write_list);
+
+ ScopedMessage received_message;
+ Connect(&received_message);
+ WaitForMessage(); // The login send.
+ WaitForMessage(); // The login response.
+ received_message.reset();
+ WaitForMessage(); // The data message.
+ EXPECT_FALSE(received_message.get());
+ EXPECT_EQ(net::ERR_FILE_TOO_BIG, last_error());
+}
+
} // namespace
} // namespace gcm
diff --git a/chromium/google_apis/gcm/engine/fake_connection_factory.cc b/chromium/google_apis/gcm/engine/fake_connection_factory.cc
index 54b3423b2d5..51447b9e5a8 100644
--- a/chromium/google_apis/gcm/engine/fake_connection_factory.cc
+++ b/chromium/google_apis/gcm/engine/fake_connection_factory.cc
@@ -10,7 +10,9 @@
namespace gcm {
-FakeConnectionFactory::FakeConnectionFactory() {
+FakeConnectionFactory::FakeConnectionFactory()
+ : reconnect_pending_(false),
+ delay_reconnect_(false) {
}
FakeConnectionFactory::~FakeConnectionFactory() {
@@ -32,15 +34,31 @@ ConnectionHandler* FakeConnectionFactory::GetConnectionHandler() const {
void FakeConnectionFactory::Connect() {
mcs_proto::LoginRequest login_request;
request_builder_.Run(&login_request);
- connection_handler_->Init(login_request, scoped_ptr<net::StreamSocket>());
+ connection_handler_->Init(login_request, NULL);
}
bool FakeConnectionFactory::IsEndpointReachable() const {
return connection_handler_.get() && connection_handler_->CanSendMessage();
}
+std::string FakeConnectionFactory::GetConnectionStateString() const {
+ return "";
+}
+
base::TimeTicks FakeConnectionFactory::NextRetryAttempt() const {
return base::TimeTicks();
}
+void FakeConnectionFactory::SignalConnectionReset(
+ ConnectionResetReason reason) {
+ if (!delay_reconnect_)
+ Connect();
+ else
+ reconnect_pending_ = true;
+}
+
+void FakeConnectionFactory::SetConnectionListener(
+ ConnectionListener* listener) {
+}
+
} // namespace gcm
diff --git a/chromium/google_apis/gcm/engine/fake_connection_factory.h b/chromium/google_apis/gcm/engine/fake_connection_factory.h
index 60b10e130db..b4f0e884d5d 100644
--- a/chromium/google_apis/gcm/engine/fake_connection_factory.h
+++ b/chromium/google_apis/gcm/engine/fake_connection_factory.h
@@ -27,13 +27,29 @@ class FakeConnectionFactory : public ConnectionFactory {
virtual ConnectionHandler* GetConnectionHandler() const OVERRIDE;
virtual void Connect() OVERRIDE;
virtual bool IsEndpointReachable() const OVERRIDE;
+ virtual std::string GetConnectionStateString() const OVERRIDE;
virtual base::TimeTicks NextRetryAttempt() const OVERRIDE;
+ virtual void SignalConnectionReset(ConnectionResetReason reason) OVERRIDE;
+ virtual void SetConnectionListener(ConnectionListener* listener) OVERRIDE;
+
+ // Whether a connection reset has been triggered and is yet to run.
+ bool reconnect_pending() const { return reconnect_pending_; }
+
+ // Whether connection resets should be handled immediately or delayed until
+ // release.
+ void set_delay_reconnect(bool should_delay) {
+ delay_reconnect_ = should_delay;
+ }
private:
scoped_ptr<FakeConnectionHandler> connection_handler_;
BuildLoginRequestCallback request_builder_;
+ // Logic for handling connection resets.
+ bool reconnect_pending_;
+ bool delay_reconnect_;
+
DISALLOW_COPY_AND_ASSIGN(FakeConnectionFactory);
};
diff --git a/chromium/google_apis/gcm/engine/fake_connection_handler.cc b/chromium/google_apis/gcm/engine/fake_connection_handler.cc
index 06639331ebe..3e5d5ac2770 100644
--- a/chromium/google_apis/gcm/engine/fake_connection_handler.cc
+++ b/chromium/google_apis/gcm/engine/fake_connection_handler.cc
@@ -39,7 +39,8 @@ FakeConnectionHandler::~FakeConnectionHandler() {
}
void FakeConnectionHandler::Init(const mcs_proto::LoginRequest& login_request,
- scoped_ptr<net::StreamSocket> socket) {
+ net::StreamSocket* socket) {
+ ASSERT_GE(expected_outgoing_messages_.size(), 1U);
EXPECT_EQ(expected_outgoing_messages_.front().SerializeAsString(),
login_request.SerializeAsString());
expected_outgoing_messages_.pop_front();
@@ -48,6 +49,10 @@ void FakeConnectionHandler::Init(const mcs_proto::LoginRequest& login_request,
initialized_ = !fail_login_;
}
+void FakeConnectionHandler::Reset() {
+ initialized_ = false;
+}
+
bool FakeConnectionHandler::CanSendMessage() const {
return initialized_;
}
diff --git a/chromium/google_apis/gcm/engine/fake_connection_handler.h b/chromium/google_apis/gcm/engine/fake_connection_handler.h
index 5356b771086..5229e3fb49d 100644
--- a/chromium/google_apis/gcm/engine/fake_connection_handler.h
+++ b/chromium/google_apis/gcm/engine/fake_connection_handler.h
@@ -23,7 +23,8 @@ class FakeConnectionHandler : public ConnectionHandler {
// ConnectionHandler implementation.
virtual void Init(const mcs_proto::LoginRequest& login_request,
- scoped_ptr<net::StreamSocket> socket) OVERRIDE;
+ net::StreamSocket* socket) OVERRIDE;
+ virtual void Reset() OVERRIDE;
virtual bool CanSendMessage() const OVERRIDE;
virtual void SendMessage(const google::protobuf::MessageLite& message)
OVERRIDE;
diff --git a/chromium/google_apis/gcm/engine/gcm_store.cc b/chromium/google_apis/gcm/engine/gcm_store.cc
new file mode 100644
index 00000000000..91ad60fe852
--- /dev/null
+++ b/chromium/google_apis/gcm/engine/gcm_store.cc
@@ -0,0 +1,21 @@
+// 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 "google_apis/gcm/engine/gcm_store.h"
+
+namespace gcm {
+
+GCMStore::LoadResult::LoadResult()
+ : success(false),
+ device_android_id(0),
+ device_security_token(0) {
+}
+
+GCMStore::LoadResult::~LoadResult() {}
+
+GCMStore::GCMStore() {}
+
+GCMStore::~GCMStore() {}
+
+} // namespace gcm
diff --git a/chromium/google_apis/gcm/engine/gcm_store.h b/chromium/google_apis/gcm/engine/gcm_store.h
new file mode 100644
index 00000000000..8b9891d0ecc
--- /dev/null
+++ b/chromium/google_apis/gcm/engine/gcm_store.h
@@ -0,0 +1,121 @@
+// 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 GOOGLE_APIS_GCM_ENGINE_GCM_STORE_H_
+#define GOOGLE_APIS_GCM_ENGINE_GCM_STORE_H_
+
+#include <map>
+#include <string>
+#include <vector>
+
+#include <google/protobuf/message_lite.h>
+
+#include "base/basictypes.h"
+#include "base/callback_forward.h"
+#include "base/memory/linked_ptr.h"
+#include "base/memory/ref_counted.h"
+#include "base/memory/scoped_ptr.h"
+#include "base/time/time.h"
+#include "google_apis/gcm/base/gcm_export.h"
+#include "google_apis/gcm/engine/registration_info.h"
+
+namespace gcm {
+
+class MCSMessage;
+
+// A GCM data store interface. GCM Store will handle persistence portion of RMQ,
+// as well as store device and user checkin information.
+class GCM_EXPORT GCMStore {
+ public:
+ // Map of message id to message data for outgoing messages.
+ typedef std::map<std::string, linked_ptr<google::protobuf::MessageLite> >
+ OutgoingMessageMap;
+
+ // Container for Load(..) results.
+ struct GCM_EXPORT LoadResult {
+ LoadResult();
+ ~LoadResult();
+
+ bool success;
+ uint64 device_android_id;
+ uint64 device_security_token;
+ RegistrationInfoMap registrations;
+ std::vector<std::string> incoming_messages;
+ OutgoingMessageMap outgoing_messages;
+ std::map<std::string, std::string> gservices_settings;
+ std::string gservices_digest;
+ base::Time last_checkin_time;
+ };
+
+ typedef std::vector<std::string> PersistentIdList;
+ typedef base::Callback<void(scoped_ptr<LoadResult> result)> LoadCallback;
+ typedef base::Callback<void(bool success)> UpdateCallback;
+
+ GCMStore();
+ virtual ~GCMStore();
+
+ // Load the data from persistent store and pass the initial state back to
+ // caller.
+ virtual void Load(const LoadCallback& callback) = 0;
+
+ // Close the persistent store.
+ virtual void Close() = 0;
+
+ // Clears the GCM store of all data.
+ virtual void Destroy(const UpdateCallback& callback) = 0;
+
+ // Sets this device's messaging credentials.
+ virtual void SetDeviceCredentials(uint64 device_android_id,
+ uint64 device_security_token,
+ const UpdateCallback& callback) = 0;
+
+ // Registration info.
+ virtual void AddRegistration(const std::string& app_id,
+ const linked_ptr<RegistrationInfo>& registration,
+ const UpdateCallback& callback) = 0;
+ virtual void RemoveRegistration(const std::string& app_id,
+ const UpdateCallback& callback) = 0;
+
+ // Unacknowledged incoming message handling.
+ virtual void AddIncomingMessage(const std::string& persistent_id,
+ const UpdateCallback& callback) = 0;
+ virtual void RemoveIncomingMessage(const std::string& persistent_id,
+ const UpdateCallback& callback) = 0;
+ virtual void RemoveIncomingMessages(const PersistentIdList& persistent_ids,
+ const UpdateCallback& callback) = 0;
+
+ // Unacknowledged outgoing messages handling.
+ // Returns false if app has surpassed message limits, else returns true. Note
+ // that the message isn't persisted until |callback| is invoked with
+ // |success| == true.
+ virtual bool AddOutgoingMessage(const std::string& persistent_id,
+ const MCSMessage& message,
+ const UpdateCallback& callback) = 0;
+ virtual void OverwriteOutgoingMessage(const std::string& persistent_id,
+ const MCSMessage& message,
+ const UpdateCallback& callback) = 0;
+ virtual void RemoveOutgoingMessage(const std::string& persistent_id,
+ const UpdateCallback& callback) = 0;
+ virtual void RemoveOutgoingMessages(const PersistentIdList& persistent_ids,
+ const UpdateCallback& callback) = 0;
+
+ // Sets last device's checkin time.
+ virtual void SetLastCheckinTime(const base::Time& last_checkin_time,
+ const UpdateCallback& callback) = 0;
+
+ // G-service settings handling.
+ // Persists |settings| and |settings_digest|. It completely replaces the
+ // existing data.
+ virtual void SetGServicesSettings(
+ const std::map<std::string, std::string>& settings,
+ const std::string& settings_digest,
+ const UpdateCallback& callback) = 0;
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(GCMStore);
+};
+
+} // namespace gcm
+
+#endif // GOOGLE_APIS_GCM_ENGINE_GCM_STORE_H_
diff --git a/chromium/google_apis/gcm/engine/gcm_store_impl.cc b/chromium/google_apis/gcm/engine/gcm_store_impl.cc
new file mode 100644
index 00000000000..e27e82e6ce2
--- /dev/null
+++ b/chromium/google_apis/gcm/engine/gcm_store_impl.cc
@@ -0,0 +1,956 @@
+// 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 "google_apis/gcm/engine/gcm_store_impl.h"
+
+#include "base/basictypes.h"
+#include "base/bind.h"
+#include "base/callback.h"
+#include "base/file_util.h"
+#include "base/files/file_path.h"
+#include "base/logging.h"
+#include "base/message_loop/message_loop_proxy.h"
+#include "base/metrics/histogram.h"
+#include "base/sequenced_task_runner.h"
+#include "base/stl_util.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/string_piece.h"
+#include "base/time/time.h"
+#include "base/tracked_objects.h"
+#include "google_apis/gcm/base/encryptor.h"
+#include "google_apis/gcm/base/mcs_message.h"
+#include "google_apis/gcm/base/mcs_util.h"
+#include "google_apis/gcm/protocol/mcs.pb.h"
+#include "third_party/leveldatabase/src/include/leveldb/db.h"
+#include "third_party/leveldatabase/src/include/leveldb/write_batch.h"
+
+namespace gcm {
+
+namespace {
+
+// Limit to the number of outstanding messages per app.
+const int kMessagesPerAppLimit = 20;
+
+// ---- LevelDB keys. ----
+// Key for this device's android id.
+const char kDeviceAIDKey[] = "device_aid_key";
+// Key for this device's android security token.
+const char kDeviceTokenKey[] = "device_token_key";
+// Lowest lexicographically ordered app ids.
+// Used for prefixing app id.
+const char kRegistrationKeyStart[] = "reg1-";
+// Key guaranteed to be higher than all app ids.
+// Used for limiting iteration.
+const char kRegistrationKeyEnd[] = "reg2-";
+// Lowest lexicographically ordered incoming message key.
+// Used for prefixing messages.
+const char kIncomingMsgKeyStart[] = "incoming1-";
+// Key guaranteed to be higher than all incoming message keys.
+// Used for limiting iteration.
+const char kIncomingMsgKeyEnd[] = "incoming2-";
+// Lowest lexicographically ordered outgoing message key.
+// Used for prefixing outgoing messages.
+const char kOutgoingMsgKeyStart[] = "outgoing1-";
+// Key guaranteed to be higher than all outgoing message keys.
+// Used for limiting iteration.
+const char kOutgoingMsgKeyEnd[] = "outgoing2-";
+// Lowest lexicographically ordered G-service settings key.
+// Used for prefixing G-services settings.
+const char kGServiceSettingKeyStart[] = "gservice1-";
+// Key guaranteed to be higher than all G-services settings keys.
+// Used for limiting iteration.
+const char kGServiceSettingKeyEnd[] = "gservice2-";
+// Key for digest of the last G-services settings update.
+const char kGServiceSettingsDigestKey[] = "gservices_digest";
+// Key used to timestamp last checkin (marked with G services settings update).
+const char kLastCheckinTimeKey[] = "last_checkin_time";
+
+std::string MakeRegistrationKey(const std::string& app_id) {
+ return kRegistrationKeyStart + app_id;
+}
+
+std::string ParseRegistrationKey(const std::string& key) {
+ return key.substr(arraysize(kRegistrationKeyStart) - 1);
+}
+
+std::string MakeIncomingKey(const std::string& persistent_id) {
+ return kIncomingMsgKeyStart + persistent_id;
+}
+
+std::string MakeOutgoingKey(const std::string& persistent_id) {
+ return kOutgoingMsgKeyStart + persistent_id;
+}
+
+std::string ParseOutgoingKey(const std::string& key) {
+ return key.substr(arraysize(kOutgoingMsgKeyStart) - 1);
+}
+
+std::string MakeGServiceSettingKey(const std::string& setting_name) {
+ return kGServiceSettingKeyStart + setting_name;
+}
+
+std::string ParseGServiceSettingKey(const std::string& key) {
+ return key.substr(arraysize(kGServiceSettingKeyStart) - 1);
+}
+
+// Note: leveldb::Slice keeps a pointer to the data in |s|, which must therefore
+// outlive the slice.
+// For example: MakeSlice(MakeOutgoingKey(x)) is invalid.
+leveldb::Slice MakeSlice(const base::StringPiece& s) {
+ return leveldb::Slice(s.begin(), s.size());
+}
+
+} // namespace
+
+class GCMStoreImpl::Backend
+ : public base::RefCountedThreadSafe<GCMStoreImpl::Backend> {
+ public:
+ Backend(const base::FilePath& path,
+ scoped_refptr<base::SequencedTaskRunner> foreground_runner,
+ scoped_ptr<Encryptor> encryptor);
+
+ // Blocking implementations of GCMStoreImpl methods.
+ void Load(const LoadCallback& callback);
+ void Close();
+ void Destroy(const UpdateCallback& callback);
+ void SetDeviceCredentials(uint64 device_android_id,
+ uint64 device_security_token,
+ const UpdateCallback& callback);
+ void AddRegistration(const std::string& app_id,
+ const linked_ptr<RegistrationInfo>& registration,
+ const UpdateCallback& callback);
+ void RemoveRegistration(const std::string& app_id,
+ const UpdateCallback& callback);
+ void AddIncomingMessage(const std::string& persistent_id,
+ const UpdateCallback& callback);
+ void RemoveIncomingMessages(const PersistentIdList& persistent_ids,
+ const UpdateCallback& callback);
+ void AddOutgoingMessage(const std::string& persistent_id,
+ const MCSMessage& message,
+ const UpdateCallback& callback);
+ void RemoveOutgoingMessages(
+ const PersistentIdList& persistent_ids,
+ const base::Callback<void(bool, const AppIdToMessageCountMap&)>
+ callback);
+ void AddUserSerialNumber(const std::string& username,
+ int64 serial_number,
+ const UpdateCallback& callback);
+ void RemoveUserSerialNumber(const std::string& username,
+ const UpdateCallback& callback);
+ void SetLastCheckinTime(const base::Time& last_checkin_time,
+ const UpdateCallback& callback);
+ void SetGServicesSettings(
+ const std::map<std::string, std::string>& settings,
+ const std::string& digest,
+ const UpdateCallback& callback);
+
+ private:
+ friend class base::RefCountedThreadSafe<Backend>;
+ ~Backend();
+
+ bool LoadDeviceCredentials(uint64* android_id, uint64* security_token);
+ bool LoadRegistrations(RegistrationInfoMap* registrations);
+ bool LoadIncomingMessages(std::vector<std::string>* incoming_messages);
+ bool LoadOutgoingMessages(OutgoingMessageMap* outgoing_messages);
+ bool LoadLastCheckinTime(base::Time* last_checkin_time);
+ bool LoadGServicesSettings(std::map<std::string, std::string>* settings,
+ std::string* digest);
+
+ const base::FilePath path_;
+ scoped_refptr<base::SequencedTaskRunner> foreground_task_runner_;
+ scoped_ptr<Encryptor> encryptor_;
+
+ scoped_ptr<leveldb::DB> db_;
+};
+
+GCMStoreImpl::Backend::Backend(
+ const base::FilePath& path,
+ scoped_refptr<base::SequencedTaskRunner> foreground_task_runner,
+ scoped_ptr<Encryptor> encryptor)
+ : path_(path),
+ foreground_task_runner_(foreground_task_runner),
+ encryptor_(encryptor.Pass()) {
+}
+
+GCMStoreImpl::Backend::~Backend() {}
+
+void GCMStoreImpl::Backend::Load(const LoadCallback& callback) {
+ scoped_ptr<LoadResult> result(new LoadResult());
+ if (db_.get()) {
+ LOG(ERROR) << "Attempting to reload open database.";
+ foreground_task_runner_->PostTask(FROM_HERE,
+ base::Bind(callback,
+ base::Passed(&result)));
+ return;
+ }
+
+ leveldb::Options options;
+ options.create_if_missing = true;
+ leveldb::DB* db;
+ leveldb::Status status =
+ leveldb::DB::Open(options, path_.AsUTF8Unsafe(), &db);
+ UMA_HISTOGRAM_BOOLEAN("GCM.LoadSucceeded", status.ok());
+ if (!status.ok()) {
+ LOG(ERROR) << "Failed to open database " << path_.value() << ": "
+ << status.ToString();
+ foreground_task_runner_->PostTask(FROM_HERE,
+ base::Bind(callback,
+ base::Passed(&result)));
+ return;
+ }
+ db_.reset(db);
+
+ if (!LoadDeviceCredentials(&result->device_android_id,
+ &result->device_security_token) ||
+ !LoadRegistrations(&result->registrations) ||
+ !LoadIncomingMessages(&result->incoming_messages) ||
+ !LoadOutgoingMessages(&result->outgoing_messages) ||
+ !LoadLastCheckinTime(&result->last_checkin_time) ||
+ !LoadGServicesSettings(&result->gservices_settings,
+ &result->gservices_digest)) {
+ result->device_android_id = 0;
+ result->device_security_token = 0;
+ result->registrations.clear();
+ result->incoming_messages.clear();
+ result->outgoing_messages.clear();
+ result->gservices_settings.clear();
+ result->gservices_digest.clear();
+ result->last_checkin_time = base::Time::FromInternalValue(0LL);
+ foreground_task_runner_->PostTask(FROM_HERE,
+ base::Bind(callback,
+ base::Passed(&result)));
+ return;
+ }
+
+ // Only record histograms if GCM had already been set up for this device.
+ if (result->device_android_id != 0 && result->device_security_token != 0) {
+ int64 file_size = 0;
+ if (base::GetFileSize(path_, &file_size)) {
+ UMA_HISTOGRAM_COUNTS("GCM.StoreSizeKB",
+ static_cast<int>(file_size / 1024));
+ }
+ UMA_HISTOGRAM_COUNTS("GCM.RestoredRegistrations",
+ result->registrations.size());
+ UMA_HISTOGRAM_COUNTS("GCM.RestoredOutgoingMessages",
+ result->outgoing_messages.size());
+ UMA_HISTOGRAM_COUNTS("GCM.RestoredIncomingMessages",
+ result->incoming_messages.size());
+ }
+
+ DVLOG(1) << "Succeeded in loading " << result->registrations.size()
+ << " registrations, "
+ << result->incoming_messages.size()
+ << " unacknowledged incoming messages and "
+ << result->outgoing_messages.size()
+ << " unacknowledged outgoing messages.";
+ result->success = true;
+ foreground_task_runner_->PostTask(FROM_HERE,
+ base::Bind(callback,
+ base::Passed(&result)));
+ return;
+}
+
+void GCMStoreImpl::Backend::Close() {
+ DVLOG(1) << "Closing GCM store.";
+ db_.reset();
+}
+
+void GCMStoreImpl::Backend::Destroy(const UpdateCallback& callback) {
+ DVLOG(1) << "Destroying GCM store.";
+ db_.reset();
+ const leveldb::Status s =
+ leveldb::DestroyDB(path_.AsUTF8Unsafe(), leveldb::Options());
+ if (s.ok()) {
+ foreground_task_runner_->PostTask(FROM_HERE, base::Bind(callback, true));
+ return;
+ }
+ LOG(ERROR) << "Destroy failed: " << s.ToString();
+ foreground_task_runner_->PostTask(FROM_HERE, base::Bind(callback, false));
+}
+
+void GCMStoreImpl::Backend::SetDeviceCredentials(
+ uint64 device_android_id,
+ uint64 device_security_token,
+ const UpdateCallback& callback) {
+ DVLOG(1) << "Saving device credentials with AID " << device_android_id;
+ if (!db_.get()) {
+ LOG(ERROR) << "GCMStore db doesn't exist.";
+ foreground_task_runner_->PostTask(FROM_HERE, base::Bind(callback, false));
+ return;
+ }
+
+ leveldb::WriteOptions write_options;
+ write_options.sync = true;
+
+ std::string encrypted_token;
+ encryptor_->EncryptString(base::Uint64ToString(device_security_token),
+ &encrypted_token);
+ std::string android_id_str = base::Uint64ToString(device_android_id);
+ leveldb::Status s =
+ db_->Put(write_options,
+ MakeSlice(kDeviceAIDKey),
+ MakeSlice(android_id_str));
+ if (s.ok()) {
+ s = db_->Put(
+ write_options, MakeSlice(kDeviceTokenKey), MakeSlice(encrypted_token));
+ }
+ if (s.ok()) {
+ foreground_task_runner_->PostTask(FROM_HERE, base::Bind(callback, true));
+ return;
+ }
+ LOG(ERROR) << "LevelDB put failed: " << s.ToString();
+ foreground_task_runner_->PostTask(FROM_HERE, base::Bind(callback, false));
+}
+
+void GCMStoreImpl::Backend::AddRegistration(
+ const std::string& app_id,
+ const linked_ptr<RegistrationInfo>& registration,
+ const UpdateCallback& callback) {
+ DVLOG(1) << "Saving registration info for app: " << app_id;
+ if (!db_.get()) {
+ LOG(ERROR) << "GCMStore db doesn't exist.";
+ foreground_task_runner_->PostTask(FROM_HERE, base::Bind(callback, false));
+ return;
+ }
+ leveldb::WriteOptions write_options;
+ write_options.sync = true;
+
+ std::string key = MakeRegistrationKey(app_id);
+ std::string value = registration->SerializeAsString();
+ const leveldb::Status status = db_->Put(write_options,
+ MakeSlice(key),
+ MakeSlice(value));
+ if (status.ok()) {
+ foreground_task_runner_->PostTask(FROM_HERE, base::Bind(callback, true));
+ return;
+ }
+ LOG(ERROR) << "LevelDB put failed: " << status.ToString();
+ foreground_task_runner_->PostTask(FROM_HERE, base::Bind(callback, false));
+}
+
+void GCMStoreImpl::Backend::RemoveRegistration(const std::string& app_id,
+ const UpdateCallback& callback) {
+ if (!db_.get()) {
+ LOG(ERROR) << "GCMStore db doesn't exist.";
+ foreground_task_runner_->PostTask(FROM_HERE, base::Bind(callback, false));
+ return;
+ }
+ leveldb::WriteOptions write_options;
+ write_options.sync = true;
+
+ leveldb::Status status = db_->Delete(write_options, MakeSlice(app_id));
+ if (status.ok()) {
+ foreground_task_runner_->PostTask(FROM_HERE, base::Bind(callback, true));
+ return;
+ }
+ LOG(ERROR) << "LevelDB remove failed: " << status.ToString();
+ foreground_task_runner_->PostTask(FROM_HERE, base::Bind(callback, false));
+}
+
+void GCMStoreImpl::Backend::AddIncomingMessage(const std::string& persistent_id,
+ const UpdateCallback& callback) {
+ DVLOG(1) << "Saving incoming message with id " << persistent_id;
+ if (!db_.get()) {
+ LOG(ERROR) << "GCMStore db doesn't exist.";
+ foreground_task_runner_->PostTask(FROM_HERE, base::Bind(callback, false));
+ return;
+ }
+
+ leveldb::WriteOptions write_options;
+ write_options.sync = true;
+
+ std::string key = MakeIncomingKey(persistent_id);
+ const leveldb::Status s = db_->Put(write_options,
+ MakeSlice(key),
+ MakeSlice(persistent_id));
+ if (s.ok()) {
+ foreground_task_runner_->PostTask(FROM_HERE, base::Bind(callback, true));
+ return;
+ }
+ LOG(ERROR) << "LevelDB put failed: " << s.ToString();
+ foreground_task_runner_->PostTask(FROM_HERE, base::Bind(callback, false));
+}
+
+void GCMStoreImpl::Backend::RemoveIncomingMessages(
+ const PersistentIdList& persistent_ids,
+ const UpdateCallback& callback) {
+ if (!db_.get()) {
+ LOG(ERROR) << "GCMStore db doesn't exist.";
+ foreground_task_runner_->PostTask(FROM_HERE, base::Bind(callback, false));
+ return;
+ }
+ leveldb::WriteOptions write_options;
+ write_options.sync = true;
+
+ leveldb::Status s;
+ for (PersistentIdList::const_iterator iter = persistent_ids.begin();
+ iter != persistent_ids.end();
+ ++iter) {
+ DVLOG(1) << "Removing incoming message with id " << *iter;
+ std::string key = MakeIncomingKey(*iter);
+ s = db_->Delete(write_options, MakeSlice(key));
+ if (!s.ok())
+ break;
+ }
+ if (s.ok()) {
+ foreground_task_runner_->PostTask(FROM_HERE, base::Bind(callback, true));
+ return;
+ }
+ LOG(ERROR) << "LevelDB remove failed: " << s.ToString();
+ foreground_task_runner_->PostTask(FROM_HERE, base::Bind(callback, false));
+}
+
+void GCMStoreImpl::Backend::AddOutgoingMessage(const std::string& persistent_id,
+ const MCSMessage& message,
+ const UpdateCallback& callback) {
+ DVLOG(1) << "Saving outgoing message with id " << persistent_id;
+ if (!db_.get()) {
+ LOG(ERROR) << "GCMStore db doesn't exist.";
+ foreground_task_runner_->PostTask(FROM_HERE, base::Bind(callback, false));
+ return;
+ }
+ leveldb::WriteOptions write_options;
+ write_options.sync = true;
+
+ std::string data =
+ static_cast<char>(message.tag()) + message.SerializeAsString();
+ std::string key = MakeOutgoingKey(persistent_id);
+ const leveldb::Status s = db_->Put(write_options,
+ MakeSlice(key),
+ MakeSlice(data));
+ if (s.ok()) {
+ foreground_task_runner_->PostTask(FROM_HERE, base::Bind(callback, true));
+ return;
+ }
+ LOG(ERROR) << "LevelDB put failed: " << s.ToString();
+ foreground_task_runner_->PostTask(FROM_HERE, base::Bind(callback, false));
+}
+
+void GCMStoreImpl::Backend::RemoveOutgoingMessages(
+ const PersistentIdList& persistent_ids,
+ const base::Callback<void(bool, const AppIdToMessageCountMap&)>
+ callback) {
+ if (!db_.get()) {
+ LOG(ERROR) << "GCMStore db doesn't exist.";
+ foreground_task_runner_->PostTask(FROM_HERE,
+ base::Bind(callback,
+ false,
+ AppIdToMessageCountMap()));
+ return;
+ }
+ leveldb::ReadOptions read_options;
+ leveldb::WriteOptions write_options;
+ write_options.sync = true;
+
+ AppIdToMessageCountMap removed_message_counts;
+
+ leveldb::Status s;
+ for (PersistentIdList::const_iterator iter = persistent_ids.begin();
+ iter != persistent_ids.end();
+ ++iter) {
+ DVLOG(1) << "Removing outgoing message with id " << *iter;
+ std::string outgoing_message;
+ std::string key = MakeOutgoingKey(*iter);
+ s = db_->Get(read_options,
+ MakeSlice(key),
+ &outgoing_message);
+ if (!s.ok())
+ break;
+ mcs_proto::DataMessageStanza data_message;
+ // Skip the initial tag byte and parse the rest to extract the message.
+ if (data_message.ParseFromString(outgoing_message.substr(1))) {
+ DCHECK(!data_message.category().empty());
+ if (removed_message_counts.count(data_message.category()) != 0)
+ removed_message_counts[data_message.category()]++;
+ else
+ removed_message_counts[data_message.category()] = 1;
+ }
+ DVLOG(1) << "Removing outgoing message with id " << *iter;
+ s = db_->Delete(write_options, MakeSlice(key));
+ if (!s.ok())
+ break;
+ }
+ if (s.ok()) {
+ foreground_task_runner_->PostTask(FROM_HERE,
+ base::Bind(callback,
+ true,
+ removed_message_counts));
+ return;
+ }
+ LOG(ERROR) << "LevelDB remove failed: " << s.ToString();
+ foreground_task_runner_->PostTask(FROM_HERE,
+ base::Bind(callback,
+ false,
+ AppIdToMessageCountMap()));
+}
+
+void GCMStoreImpl::Backend::SetLastCheckinTime(
+ const base::Time& last_checkin_time,
+ const UpdateCallback& callback) {
+ leveldb::WriteOptions write_options;
+ write_options.sync = true;
+
+ int64 last_checkin_time_internal = last_checkin_time.ToInternalValue();
+ const leveldb::Status s =
+ db_->Put(write_options,
+ MakeSlice(kLastCheckinTimeKey),
+ MakeSlice(base::Int64ToString(last_checkin_time_internal)));
+
+ if (!s.ok())
+ LOG(ERROR) << "LevelDB set last checkin time failed: " << s.ToString();
+ foreground_task_runner_->PostTask(FROM_HERE, base::Bind(callback, s.ok()));
+}
+
+void GCMStoreImpl::Backend::SetGServicesSettings(
+ const std::map<std::string, std::string>& settings,
+ const std::string& settings_digest,
+ const UpdateCallback& callback) {
+ leveldb::WriteBatch write_batch;
+
+ // Remove all existing settings.
+ leveldb::ReadOptions read_options;
+ read_options.verify_checksums = true;
+ scoped_ptr<leveldb::Iterator> iter(db_->NewIterator(read_options));
+ for (iter->Seek(MakeSlice(kGServiceSettingKeyStart));
+ iter->Valid() && iter->key().ToString() < kGServiceSettingKeyEnd;
+ iter->Next()) {
+ write_batch.Delete(iter->key());
+ }
+
+ // Add the new settings.
+ for (std::map<std::string, std::string>::const_iterator iter =
+ settings.begin();
+ iter != settings.end(); ++iter) {
+ write_batch.Put(MakeSlice(MakeGServiceSettingKey(iter->first)),
+ MakeSlice(iter->second));
+ }
+
+ // Update the settings digest.
+ write_batch.Put(MakeSlice(kGServiceSettingsDigestKey),
+ MakeSlice(settings_digest));
+
+ // Write it all in a batch.
+ leveldb::WriteOptions write_options;
+ write_options.sync = true;
+
+ leveldb::Status s = db_->Write(write_options, &write_batch);
+ if (!s.ok())
+ LOG(ERROR) << "LevelDB GService Settings update failed: " << s.ToString();
+ foreground_task_runner_->PostTask(FROM_HERE, base::Bind(callback, s.ok()));
+}
+
+bool GCMStoreImpl::Backend::LoadDeviceCredentials(uint64* android_id,
+ uint64* security_token) {
+ leveldb::ReadOptions read_options;
+ read_options.verify_checksums = true;
+
+ std::string result;
+ leveldb::Status s = db_->Get(read_options, MakeSlice(kDeviceAIDKey), &result);
+ if (s.ok()) {
+ if (!base::StringToUint64(result, android_id)) {
+ LOG(ERROR) << "Failed to restore device id.";
+ return false;
+ }
+ result.clear();
+ s = db_->Get(read_options, MakeSlice(kDeviceTokenKey), &result);
+ }
+ if (s.ok()) {
+ std::string decrypted_token;
+ encryptor_->DecryptString(result, &decrypted_token);
+ if (!base::StringToUint64(decrypted_token, security_token)) {
+ LOG(ERROR) << "Failed to restore security token.";
+ return false;
+ }
+ return true;
+ }
+
+ if (s.IsNotFound()) {
+ DVLOG(1) << "No credentials found.";
+ return true;
+ }
+
+ LOG(ERROR) << "Error reading credentials from store.";
+ return false;
+}
+
+bool GCMStoreImpl::Backend::LoadRegistrations(
+ RegistrationInfoMap* registrations) {
+ leveldb::ReadOptions read_options;
+ read_options.verify_checksums = true;
+
+ scoped_ptr<leveldb::Iterator> iter(db_->NewIterator(read_options));
+ for (iter->Seek(MakeSlice(kRegistrationKeyStart));
+ iter->Valid() && iter->key().ToString() < kRegistrationKeyEnd;
+ iter->Next()) {
+ leveldb::Slice s = iter->value();
+ if (s.size() <= 1) {
+ LOG(ERROR) << "Error reading registration with key " << s.ToString();
+ return false;
+ }
+ std::string app_id = ParseRegistrationKey(iter->key().ToString());
+ linked_ptr<RegistrationInfo> registration(new RegistrationInfo);
+ if (!registration->ParseFromString(iter->value().ToString())) {
+ LOG(ERROR) << "Failed to parse registration with app id " << app_id;
+ return false;
+ }
+ DVLOG(1) << "Found registration with app id " << app_id;
+ (*registrations)[app_id] = registration;
+ }
+
+ return true;
+}
+
+bool GCMStoreImpl::Backend::LoadIncomingMessages(
+ std::vector<std::string>* incoming_messages) {
+ leveldb::ReadOptions read_options;
+ read_options.verify_checksums = true;
+
+ scoped_ptr<leveldb::Iterator> iter(db_->NewIterator(read_options));
+ for (iter->Seek(MakeSlice(kIncomingMsgKeyStart));
+ iter->Valid() && iter->key().ToString() < kIncomingMsgKeyEnd;
+ iter->Next()) {
+ leveldb::Slice s = iter->value();
+ if (s.empty()) {
+ LOG(ERROR) << "Error reading incoming message with key "
+ << iter->key().ToString();
+ return false;
+ }
+ DVLOG(1) << "Found incoming message with id " << s.ToString();
+ incoming_messages->push_back(s.ToString());
+ }
+
+ return true;
+}
+
+bool GCMStoreImpl::Backend::LoadOutgoingMessages(
+ OutgoingMessageMap* outgoing_messages) {
+ leveldb::ReadOptions read_options;
+ read_options.verify_checksums = true;
+
+ scoped_ptr<leveldb::Iterator> iter(db_->NewIterator(read_options));
+ for (iter->Seek(MakeSlice(kOutgoingMsgKeyStart));
+ iter->Valid() && iter->key().ToString() < kOutgoingMsgKeyEnd;
+ iter->Next()) {
+ leveldb::Slice s = iter->value();
+ if (s.size() <= 1) {
+ LOG(ERROR) << "Error reading incoming message with key " << s.ToString();
+ return false;
+ }
+ uint8 tag = iter->value().data()[0];
+ std::string id = ParseOutgoingKey(iter->key().ToString());
+ scoped_ptr<google::protobuf::MessageLite> message(
+ BuildProtobufFromTag(tag));
+ if (!message.get() ||
+ !message->ParseFromString(iter->value().ToString().substr(1))) {
+ LOG(ERROR) << "Failed to parse outgoing message with id " << id
+ << " and tag " << tag;
+ return false;
+ }
+ DVLOG(1) << "Found outgoing message with id " << id << " of type "
+ << base::IntToString(tag);
+ (*outgoing_messages)[id] = make_linked_ptr(message.release());
+ }
+
+ return true;
+}
+
+bool GCMStoreImpl::Backend::LoadLastCheckinTime(
+ base::Time* last_checkin_time) {
+ leveldb::ReadOptions read_options;
+ read_options.verify_checksums = true;
+
+ std::string result;
+ leveldb::Status s = db_->Get(read_options,
+ MakeSlice(kLastCheckinTimeKey),
+ &result);
+ int64 time_internal = 0LL;
+ if (s.ok() && !base::StringToInt64(result, &time_internal))
+ LOG(ERROR) << "Failed to restore last checkin time. Using default = 0.";
+
+ // In case we cannot read last checkin time, we default it to 0, as we don't
+ // want that situation to cause the whole load to fail.
+ *last_checkin_time = base::Time::FromInternalValue(time_internal);
+
+ return true;
+}
+
+bool GCMStoreImpl::Backend::LoadGServicesSettings(
+ std::map<std::string, std::string>* settings,
+ std::string* digest) {
+ leveldb::ReadOptions read_options;
+ read_options.verify_checksums = true;
+
+ // Load all of the GServices settings.
+ scoped_ptr<leveldb::Iterator> iter(db_->NewIterator(read_options));
+ for (iter->Seek(MakeSlice(kGServiceSettingKeyStart));
+ iter->Valid() && iter->key().ToString() < kGServiceSettingKeyEnd;
+ iter->Next()) {
+ std::string value = iter->value().ToString();
+ if (value.empty()) {
+ LOG(ERROR) << "Error reading GService Settings " << value;
+ return false;
+ }
+ std::string id = ParseGServiceSettingKey(iter->key().ToString());
+ (*settings)[id] = value;
+ DVLOG(1) << "Found G Service setting with key: " << id
+ << ", and value: " << value;
+ }
+
+ // Load the settings digest. It's ok if it is empty.
+ db_->Get(read_options, MakeSlice(kGServiceSettingsDigestKey), digest);
+
+ return true;
+}
+
+GCMStoreImpl::GCMStoreImpl(
+ const base::FilePath& path,
+ scoped_refptr<base::SequencedTaskRunner> blocking_task_runner,
+ scoped_ptr<Encryptor> encryptor)
+ : backend_(new Backend(path,
+ base::MessageLoopProxy::current(),
+ encryptor.Pass())),
+ blocking_task_runner_(blocking_task_runner),
+ weak_ptr_factory_(this) {
+}
+
+GCMStoreImpl::~GCMStoreImpl() {}
+
+void GCMStoreImpl::Load(const LoadCallback& callback) {
+ blocking_task_runner_->PostTask(
+ FROM_HERE,
+ base::Bind(&GCMStoreImpl::Backend::Load,
+ backend_,
+ base::Bind(&GCMStoreImpl::LoadContinuation,
+ weak_ptr_factory_.GetWeakPtr(),
+ callback)));
+}
+
+void GCMStoreImpl::Close() {
+ weak_ptr_factory_.InvalidateWeakPtrs();
+ app_message_counts_.clear();
+ blocking_task_runner_->PostTask(
+ FROM_HERE,
+ base::Bind(&GCMStoreImpl::Backend::Close, backend_));
+}
+
+void GCMStoreImpl::Destroy(const UpdateCallback& callback) {
+ blocking_task_runner_->PostTask(
+ FROM_HERE,
+ base::Bind(&GCMStoreImpl::Backend::Destroy, backend_, callback));
+}
+
+void GCMStoreImpl::SetDeviceCredentials(uint64 device_android_id,
+ uint64 device_security_token,
+ const UpdateCallback& callback) {
+ blocking_task_runner_->PostTask(
+ FROM_HERE,
+ base::Bind(&GCMStoreImpl::Backend::SetDeviceCredentials,
+ backend_,
+ device_android_id,
+ device_security_token,
+ callback));
+}
+
+void GCMStoreImpl::AddRegistration(
+ const std::string& app_id,
+ const linked_ptr<RegistrationInfo>& registration,
+ const UpdateCallback& callback) {
+ blocking_task_runner_->PostTask(
+ FROM_HERE,
+ base::Bind(&GCMStoreImpl::Backend::AddRegistration,
+ backend_,
+ app_id,
+ registration,
+ callback));
+}
+
+void GCMStoreImpl::RemoveRegistration(const std::string& app_id,
+ const UpdateCallback& callback) {
+ blocking_task_runner_->PostTask(
+ FROM_HERE,
+ base::Bind(&GCMStoreImpl::Backend::RemoveRegistration,
+ backend_,
+ app_id,
+ callback));
+}
+
+void GCMStoreImpl::AddIncomingMessage(const std::string& persistent_id,
+ const UpdateCallback& callback) {
+ blocking_task_runner_->PostTask(
+ FROM_HERE,
+ base::Bind(&GCMStoreImpl::Backend::AddIncomingMessage,
+ backend_,
+ persistent_id,
+ callback));
+}
+
+void GCMStoreImpl::RemoveIncomingMessage(const std::string& persistent_id,
+ const UpdateCallback& callback) {
+ blocking_task_runner_->PostTask(
+ FROM_HERE,
+ base::Bind(&GCMStoreImpl::Backend::RemoveIncomingMessages,
+ backend_,
+ PersistentIdList(1, persistent_id),
+ callback));
+}
+
+void GCMStoreImpl::RemoveIncomingMessages(
+ const PersistentIdList& persistent_ids,
+ const UpdateCallback& callback) {
+ blocking_task_runner_->PostTask(
+ FROM_HERE,
+ base::Bind(&GCMStoreImpl::Backend::RemoveIncomingMessages,
+ backend_,
+ persistent_ids,
+ callback));
+}
+
+bool GCMStoreImpl::AddOutgoingMessage(const std::string& persistent_id,
+ const MCSMessage& message,
+ const UpdateCallback& callback) {
+ DCHECK_EQ(message.tag(), kDataMessageStanzaTag);
+ std::string app_id = reinterpret_cast<const mcs_proto::DataMessageStanza*>(
+ &message.GetProtobuf())->category();
+ DCHECK(!app_id.empty());
+ if (app_message_counts_.count(app_id) == 0)
+ app_message_counts_[app_id] = 0;
+ if (app_message_counts_[app_id] < kMessagesPerAppLimit) {
+ app_message_counts_[app_id]++;
+
+ blocking_task_runner_->PostTask(
+ FROM_HERE,
+ base::Bind(&GCMStoreImpl::Backend::AddOutgoingMessage,
+ backend_,
+ persistent_id,
+ message,
+ base::Bind(&GCMStoreImpl::AddOutgoingMessageContinuation,
+ weak_ptr_factory_.GetWeakPtr(),
+ callback,
+ app_id)));
+ return true;
+ }
+ return false;
+}
+
+void GCMStoreImpl::OverwriteOutgoingMessage(const std::string& persistent_id,
+ const MCSMessage& message,
+ const UpdateCallback& callback) {
+ DCHECK_EQ(message.tag(), kDataMessageStanzaTag);
+ std::string app_id = reinterpret_cast<const mcs_proto::DataMessageStanza*>(
+ &message.GetProtobuf())->category();
+ DCHECK(!app_id.empty());
+ // There should already be pending messages for this app.
+ DCHECK(app_message_counts_.count(app_id));
+ // TODO(zea): consider verifying the specific message already exists.
+ blocking_task_runner_->PostTask(
+ FROM_HERE,
+ base::Bind(&GCMStoreImpl::Backend::AddOutgoingMessage,
+ backend_,
+ persistent_id,
+ message,
+ callback));
+}
+
+void GCMStoreImpl::RemoveOutgoingMessage(const std::string& persistent_id,
+ const UpdateCallback& callback) {
+ blocking_task_runner_->PostTask(
+ FROM_HERE,
+ base::Bind(&GCMStoreImpl::Backend::RemoveOutgoingMessages,
+ backend_,
+ PersistentIdList(1, persistent_id),
+ base::Bind(&GCMStoreImpl::RemoveOutgoingMessagesContinuation,
+ weak_ptr_factory_.GetWeakPtr(),
+ callback)));
+}
+
+void GCMStoreImpl::RemoveOutgoingMessages(
+ const PersistentIdList& persistent_ids,
+ const UpdateCallback& callback) {
+ blocking_task_runner_->PostTask(
+ FROM_HERE,
+ base::Bind(&GCMStoreImpl::Backend::RemoveOutgoingMessages,
+ backend_,
+ persistent_ids,
+ base::Bind(&GCMStoreImpl::RemoveOutgoingMessagesContinuation,
+ weak_ptr_factory_.GetWeakPtr(),
+ callback)));
+}
+
+void GCMStoreImpl::SetLastCheckinTime(const base::Time& last_checkin_time,
+ const UpdateCallback& callback) {
+ blocking_task_runner_->PostTask(
+ FROM_HERE,
+ base::Bind(&GCMStoreImpl::Backend::SetLastCheckinTime,
+ backend_,
+ last_checkin_time,
+ callback));
+}
+
+void GCMStoreImpl::SetGServicesSettings(
+ const std::map<std::string, std::string>& settings,
+ const std::string& digest,
+ const UpdateCallback& callback) {
+ blocking_task_runner_->PostTask(
+ FROM_HERE,
+ base::Bind(&GCMStoreImpl::Backend::SetGServicesSettings,
+ backend_,
+ settings,
+ digest,
+ callback));
+}
+
+void GCMStoreImpl::LoadContinuation(const LoadCallback& callback,
+ scoped_ptr<LoadResult> result) {
+ if (!result->success) {
+ callback.Run(result.Pass());
+ return;
+ }
+ int num_throttled_apps = 0;
+ for (OutgoingMessageMap::const_iterator
+ iter = result->outgoing_messages.begin();
+ iter != result->outgoing_messages.end(); ++iter) {
+ const mcs_proto::DataMessageStanza* data_message =
+ reinterpret_cast<mcs_proto::DataMessageStanza*>(iter->second.get());
+ DCHECK(!data_message->category().empty());
+ if (app_message_counts_.count(data_message->category()) == 0)
+ app_message_counts_[data_message->category()] = 1;
+ else
+ app_message_counts_[data_message->category()]++;
+ if (app_message_counts_[data_message->category()] == kMessagesPerAppLimit)
+ num_throttled_apps++;
+ }
+ UMA_HISTOGRAM_COUNTS("GCM.NumThrottledApps", num_throttled_apps);
+ callback.Run(result.Pass());
+}
+
+void GCMStoreImpl::AddOutgoingMessageContinuation(
+ const UpdateCallback& callback,
+ const std::string& app_id,
+ bool success) {
+ if (!success) {
+ DCHECK(app_message_counts_[app_id] > 0);
+ app_message_counts_[app_id]--;
+ }
+ callback.Run(success);
+}
+
+void GCMStoreImpl::RemoveOutgoingMessagesContinuation(
+ const UpdateCallback& callback,
+ bool success,
+ const AppIdToMessageCountMap& removed_message_counts) {
+ if (!success) {
+ callback.Run(false);
+ return;
+ }
+ for (AppIdToMessageCountMap::const_iterator iter =
+ removed_message_counts.begin();
+ iter != removed_message_counts.end(); ++iter) {
+ DCHECK_NE(app_message_counts_.count(iter->first), 0U);
+ app_message_counts_[iter->first] -= iter->second;
+ DCHECK_GE(app_message_counts_[iter->first], 0);
+ }
+ callback.Run(true);
+}
+
+} // namespace gcm
diff --git a/chromium/google_apis/gcm/engine/gcm_store_impl.h b/chromium/google_apis/gcm/engine/gcm_store_impl.h
new file mode 100644
index 00000000000..f49509a3e17
--- /dev/null
+++ b/chromium/google_apis/gcm/engine/gcm_store_impl.h
@@ -0,0 +1,126 @@
+// 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 GOOGLE_APIS_GCM_ENGINE_GCM_STORE_IMPL_H_
+#define GOOGLE_APIS_GCM_ENGINE_GCM_STORE_IMPL_H_
+
+#include "base/basictypes.h"
+#include "base/memory/ref_counted.h"
+#include "base/memory/weak_ptr.h"
+#include "google_apis/gcm/base/gcm_export.h"
+#include "google_apis/gcm/engine/gcm_store.h"
+
+namespace base {
+class FilePath;
+class SequencedTaskRunner;
+} // namespace base
+
+namespace gcm {
+
+class Encryptor;
+
+// An implementation of GCM Store that uses LevelDB for persistence.
+// It performs all blocking operations on the blocking task runner, and posts
+// all callbacks to the thread on which the GCMStoreImpl is created.
+class GCM_EXPORT GCMStoreImpl : public GCMStore {
+ public:
+ GCMStoreImpl(const base::FilePath& path,
+ scoped_refptr<base::SequencedTaskRunner> blocking_task_runner,
+ scoped_ptr<Encryptor> encryptor);
+ virtual ~GCMStoreImpl();
+
+ // Load the directory and pass the initial state back to caller.
+ virtual void Load(const LoadCallback& callback) OVERRIDE;
+
+ // Closes the GCM store.
+ virtual void Close() OVERRIDE;
+
+ // Clears the GCM store of all data and destroys any LevelDB files associated
+ // with this store.
+ // WARNING: this will permanently destroy any pending outgoing messages
+ // and require the device to re-create credentials and serial number mapping
+ // tables.
+ virtual void Destroy(const UpdateCallback& callback) OVERRIDE;
+
+ // Sets this device's messaging credentials.
+ virtual void SetDeviceCredentials(uint64 device_android_id,
+ uint64 device_security_token,
+ const UpdateCallback& callback) OVERRIDE;
+
+ // Registration info.
+ virtual void AddRegistration(const std::string& app_id,
+ const linked_ptr<RegistrationInfo>& registration,
+ const UpdateCallback& callback) OVERRIDE;
+ virtual void RemoveRegistration(const std::string& app_id,
+ const UpdateCallback& callback) OVERRIDE;
+
+ // Unacknowledged incoming message handling.
+ virtual void AddIncomingMessage(const std::string& persistent_id,
+ const UpdateCallback& callback) OVERRIDE;
+ virtual void RemoveIncomingMessage(const std::string& persistent_id,
+ const UpdateCallback& callback) OVERRIDE;
+ virtual void RemoveIncomingMessages(const PersistentIdList& persistent_ids,
+ const UpdateCallback& callback) OVERRIDE;
+
+ // Unacknowledged outgoing messages handling.
+ virtual bool AddOutgoingMessage(const std::string& persistent_id,
+ const MCSMessage& message,
+ const UpdateCallback& callback) OVERRIDE;
+ virtual void OverwriteOutgoingMessage(const std::string& persistent_id,
+ const MCSMessage& message,
+ const UpdateCallback& callback)
+ OVERRIDE;
+ virtual void RemoveOutgoingMessage(const std::string& persistent_id,
+ const UpdateCallback& callback) OVERRIDE;
+ virtual void RemoveOutgoingMessages(const PersistentIdList& persistent_ids,
+ const UpdateCallback& callback) OVERRIDE;
+
+ // Sets last device's checkin time.
+ virtual void SetLastCheckinTime(const base::Time& last_checkin_time,
+ const UpdateCallback& callback) OVERRIDE;
+
+ // G-service settings handling.
+ virtual void SetGServicesSettings(
+ const std::map<std::string, std::string>& settings,
+ const std::string& settings_digest,
+ const UpdateCallback& callback) OVERRIDE;
+
+ private:
+ typedef std::map<std::string, int> AppIdToMessageCountMap;
+
+ // Continuation to update the per-app message counts after a load.
+ void LoadContinuation(const LoadCallback& callback,
+ scoped_ptr<LoadResult> result);
+
+ // Continuation to update the per-app message counts when adding messages.
+ // In particular, if a message fails to add, the message count is decremented.
+ void AddOutgoingMessageContinuation(const UpdateCallback& callback,
+ const std::string& app_id,
+ bool success);
+
+ // Continuation to update the per-app message counts when removing messages.
+ // Note: if doing a read-then-write when removing messages proves expensive,
+ // an in-memory mapping of persisted message id to app could be maintained
+ // instead.
+ void RemoveOutgoingMessagesContinuation(
+ const UpdateCallback& callback,
+ bool success,
+ const std::map<std::string, int>& removed_message_counts);
+
+ class Backend;
+
+ // Map of App ids to their message counts.
+ AppIdToMessageCountMap app_message_counts_;
+
+ scoped_refptr<Backend> backend_;
+ scoped_refptr<base::SequencedTaskRunner> blocking_task_runner_;
+
+ base::WeakPtrFactory<GCMStoreImpl> weak_ptr_factory_;
+
+ DISALLOW_COPY_AND_ASSIGN(GCMStoreImpl);
+};
+
+} // namespace gcm
+
+#endif // GOOGLE_APIS_GCM_ENGINE_GCM_STORE_IMPL_H_
diff --git a/chromium/google_apis/gcm/engine/gcm_store_impl_unittest.cc b/chromium/google_apis/gcm/engine/gcm_store_impl_unittest.cc
new file mode 100644
index 00000000000..7b9c8936529
--- /dev/null
+++ b/chromium/google_apis/gcm/engine/gcm_store_impl_unittest.cc
@@ -0,0 +1,552 @@
+// 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 "google_apis/gcm/engine/gcm_store_impl.h"
+
+#include <string>
+#include <vector>
+
+#include "base/bind.h"
+#include "base/command_line.h"
+#include "base/files/file_path.h"
+#include "base/files/scoped_temp_dir.h"
+#include "base/memory/scoped_ptr.h"
+#include "base/message_loop/message_loop.h"
+#include "base/run_loop.h"
+#include "base/strings/string_number_conversions.h"
+#include "google_apis/gcm/base/fake_encryptor.h"
+#include "google_apis/gcm/base/mcs_message.h"
+#include "google_apis/gcm/base/mcs_util.h"
+#include "google_apis/gcm/protocol/mcs.pb.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace gcm {
+
+namespace {
+
+// Number of persistent ids to use in tests.
+const int kNumPersistentIds = 10;
+
+// Number of per-app messages in tests.
+const int kNumMessagesPerApp = 20;
+
+// App name for testing.
+const char kAppName[] = "my_app";
+
+// Category name for testing.
+const char kCategoryName[] = "my_category";
+
+const uint64 kDeviceId = 22;
+const uint64 kDeviceToken = 55;
+
+class GCMStoreImplTest : public testing::Test {
+ public:
+ GCMStoreImplTest();
+ virtual ~GCMStoreImplTest();
+
+ virtual void SetUp() OVERRIDE;
+
+ scoped_ptr<GCMStore> BuildGCMStore();
+
+ std::string GetNextPersistentId();
+
+ void PumpLoop();
+
+ void LoadCallback(scoped_ptr<GCMStore::LoadResult>* result_dst,
+ scoped_ptr<GCMStore::LoadResult> result);
+ void UpdateCallback(bool success);
+
+ protected:
+ base::MessageLoop message_loop_;
+ base::ScopedTempDir temp_directory_;
+ bool expected_success_;
+ uint64 next_persistent_id_;
+ scoped_ptr<base::RunLoop> run_loop_;
+};
+
+GCMStoreImplTest::GCMStoreImplTest()
+ : expected_success_(true),
+ next_persistent_id_(base::Time::Now().ToInternalValue()) {
+ EXPECT_TRUE(temp_directory_.CreateUniqueTempDir());
+ run_loop_.reset(new base::RunLoop());
+}
+
+GCMStoreImplTest::~GCMStoreImplTest() {}
+
+void GCMStoreImplTest::SetUp() {
+ testing::Test::SetUp();
+}
+
+scoped_ptr<GCMStore> GCMStoreImplTest::BuildGCMStore() {
+ return scoped_ptr<GCMStore>(new GCMStoreImpl(
+ temp_directory_.path(),
+ message_loop_.message_loop_proxy(),
+ make_scoped_ptr<Encryptor>(new FakeEncryptor)));
+}
+
+std::string GCMStoreImplTest::GetNextPersistentId() {
+ return base::Uint64ToString(next_persistent_id_++);
+}
+
+void GCMStoreImplTest::PumpLoop() { message_loop_.RunUntilIdle(); }
+
+void GCMStoreImplTest::LoadCallback(
+ scoped_ptr<GCMStore::LoadResult>* result_dst,
+ scoped_ptr<GCMStore::LoadResult> result) {
+ ASSERT_TRUE(result->success);
+ *result_dst = result.Pass();
+ run_loop_->Quit();
+ run_loop_.reset(new base::RunLoop());
+}
+
+void GCMStoreImplTest::UpdateCallback(bool success) {
+ ASSERT_EQ(expected_success_, success);
+}
+
+// Verify creating a new database and loading it.
+TEST_F(GCMStoreImplTest, LoadNew) {
+ scoped_ptr<GCMStore> gcm_store(BuildGCMStore());
+ scoped_ptr<GCMStore::LoadResult> load_result;
+ gcm_store->Load(base::Bind(
+ &GCMStoreImplTest::LoadCallback, base::Unretained(this), &load_result));
+ PumpLoop();
+
+ EXPECT_EQ(0U, load_result->device_android_id);
+ EXPECT_EQ(0U, load_result->device_security_token);
+ EXPECT_TRUE(load_result->incoming_messages.empty());
+ EXPECT_TRUE(load_result->outgoing_messages.empty());
+ EXPECT_TRUE(load_result->gservices_settings.empty());
+ EXPECT_EQ(base::Time::FromInternalValue(0LL), load_result->last_checkin_time);
+}
+
+TEST_F(GCMStoreImplTest, DeviceCredentials) {
+ scoped_ptr<GCMStore> gcm_store(BuildGCMStore());
+ scoped_ptr<GCMStore::LoadResult> load_result;
+ gcm_store->Load(base::Bind(
+ &GCMStoreImplTest::LoadCallback, base::Unretained(this), &load_result));
+ PumpLoop();
+
+ gcm_store->SetDeviceCredentials(
+ kDeviceId,
+ kDeviceToken,
+ base::Bind(&GCMStoreImplTest::UpdateCallback, base::Unretained(this)));
+ PumpLoop();
+
+ gcm_store = BuildGCMStore().Pass();
+ gcm_store->Load(base::Bind(
+ &GCMStoreImplTest::LoadCallback, base::Unretained(this), &load_result));
+ PumpLoop();
+
+ ASSERT_EQ(kDeviceId, load_result->device_android_id);
+ ASSERT_EQ(kDeviceToken, load_result->device_security_token);
+}
+
+TEST_F(GCMStoreImplTest, LastCheckinTime) {
+ scoped_ptr<GCMStore> gcm_store(BuildGCMStore());
+ scoped_ptr<GCMStore::LoadResult> load_result;
+ gcm_store->Load(base::Bind(
+ &GCMStoreImplTest::LoadCallback, base::Unretained(this), &load_result));
+ PumpLoop();
+
+ base::Time last_checkin_time = base::Time::Now();
+
+ gcm_store->SetLastCheckinTime(
+ last_checkin_time,
+ base::Bind(&GCMStoreImplTest::UpdateCallback, base::Unretained(this)));
+ PumpLoop();
+
+ gcm_store = BuildGCMStore().Pass();
+ gcm_store->Load(base::Bind(
+ &GCMStoreImplTest::LoadCallback, base::Unretained(this), &load_result));
+ PumpLoop();
+ ASSERT_EQ(last_checkin_time, load_result->last_checkin_time);
+}
+
+TEST_F(GCMStoreImplTest, GServicesSettings_ProtocolV2) {
+ scoped_ptr<GCMStore> gcm_store(BuildGCMStore());
+ scoped_ptr<GCMStore::LoadResult> load_result;
+ gcm_store->Load(base::Bind(
+ &GCMStoreImplTest::LoadCallback, base::Unretained(this), &load_result));
+ PumpLoop();
+
+ std::map<std::string, std::string> settings;
+ settings["checkin_interval"] = "12345";
+ settings["mcs_port"] = "438";
+ settings["checkin_url"] = "http://checkin.google.com";
+ std::string digest = "digest1";
+
+ gcm_store->SetGServicesSettings(
+ settings,
+ digest,
+ base::Bind(&GCMStoreImplTest::UpdateCallback, base::Unretained(this)));
+ PumpLoop();
+
+ gcm_store = BuildGCMStore().Pass();
+ gcm_store->Load(base::Bind(
+ &GCMStoreImplTest::LoadCallback, base::Unretained(this), &load_result));
+ PumpLoop();
+
+ ASSERT_EQ(settings, load_result->gservices_settings);
+ ASSERT_EQ(digest, load_result->gservices_digest);
+
+ // Remove some, and add some.
+ settings.clear();
+ settings["checkin_interval"] = "54321";
+ settings["registration_url"] = "http://registration.google.com";
+ digest = "digest2";
+
+ gcm_store->SetGServicesSettings(
+ settings,
+ digest,
+ base::Bind(&GCMStoreImplTest::UpdateCallback, base::Unretained(this)));
+ PumpLoop();
+
+ gcm_store = BuildGCMStore().Pass();
+ gcm_store->Load(base::Bind(
+ &GCMStoreImplTest::LoadCallback, base::Unretained(this), &load_result));
+ PumpLoop();
+
+ ASSERT_EQ(settings, load_result->gservices_settings);
+ ASSERT_EQ(digest, load_result->gservices_digest);
+}
+
+TEST_F(GCMStoreImplTest, Registrations) {
+ scoped_ptr<GCMStore> gcm_store(BuildGCMStore());
+ scoped_ptr<GCMStore::LoadResult> load_result;
+ gcm_store->Load(base::Bind(
+ &GCMStoreImplTest::LoadCallback, base::Unretained(this), &load_result));
+ PumpLoop();
+
+ // Add one registration with one sender.
+ linked_ptr<RegistrationInfo> registration1(new RegistrationInfo);
+ registration1->sender_ids.push_back("sender1");
+ registration1->registration_id = "registration1";
+ gcm_store->AddRegistration(
+ "app1",
+ registration1,
+ base::Bind(&GCMStoreImplTest::UpdateCallback, base::Unretained(this)));
+ PumpLoop();
+
+ // Add one registration with multiple senders.
+ linked_ptr<RegistrationInfo> registration2(new RegistrationInfo);
+ registration2->sender_ids.push_back("sender2_1");
+ registration2->sender_ids.push_back("sender2_2");
+ registration2->registration_id = "registration2";
+ gcm_store->AddRegistration(
+ "app2",
+ registration2,
+ base::Bind(&GCMStoreImplTest::UpdateCallback, base::Unretained(this)));
+ PumpLoop();
+
+ gcm_store = BuildGCMStore().Pass();
+ gcm_store->Load(base::Bind(
+ &GCMStoreImplTest::LoadCallback, base::Unretained(this), &load_result));
+ PumpLoop();
+
+ ASSERT_EQ(2u, load_result->registrations.size());
+ ASSERT_TRUE(load_result->registrations.find("app1") !=
+ load_result->registrations.end());
+ EXPECT_EQ(registration1->registration_id,
+ load_result->registrations["app1"]->registration_id);
+ ASSERT_EQ(1u, load_result->registrations["app1"]->sender_ids.size());
+ EXPECT_EQ(registration1->sender_ids[0],
+ load_result->registrations["app1"]->sender_ids[0]);
+ ASSERT_TRUE(load_result->registrations.find("app2") !=
+ load_result->registrations.end());
+ EXPECT_EQ(registration2->registration_id,
+ load_result->registrations["app2"]->registration_id);
+ ASSERT_EQ(2u, load_result->registrations["app2"]->sender_ids.size());
+ EXPECT_EQ(registration2->sender_ids[0],
+ load_result->registrations["app2"]->sender_ids[0]);
+ EXPECT_EQ(registration2->sender_ids[1],
+ load_result->registrations["app2"]->sender_ids[1]);
+}
+
+// Verify saving some incoming messages, reopening the directory, and then
+// removing those incoming messages.
+TEST_F(GCMStoreImplTest, IncomingMessages) {
+ scoped_ptr<GCMStore> gcm_store(BuildGCMStore());
+ scoped_ptr<GCMStore::LoadResult> load_result;
+ gcm_store->Load(base::Bind(
+ &GCMStoreImplTest::LoadCallback, base::Unretained(this), &load_result));
+ PumpLoop();
+
+ std::vector<std::string> persistent_ids;
+ for (int i = 0; i < kNumPersistentIds; ++i) {
+ persistent_ids.push_back(GetNextPersistentId());
+ gcm_store->AddIncomingMessage(
+ persistent_ids.back(),
+ base::Bind(&GCMStoreImplTest::UpdateCallback, base::Unretained(this)));
+ PumpLoop();
+ }
+
+ gcm_store = BuildGCMStore().Pass();
+ gcm_store->Load(base::Bind(
+ &GCMStoreImplTest::LoadCallback, base::Unretained(this), &load_result));
+ PumpLoop();
+
+ ASSERT_EQ(persistent_ids, load_result->incoming_messages);
+ ASSERT_TRUE(load_result->outgoing_messages.empty());
+
+ gcm_store->RemoveIncomingMessages(
+ persistent_ids,
+ base::Bind(&GCMStoreImplTest::UpdateCallback, base::Unretained(this)));
+ PumpLoop();
+
+ gcm_store = BuildGCMStore().Pass();
+ load_result->incoming_messages.clear();
+ gcm_store->Load(base::Bind(
+ &GCMStoreImplTest::LoadCallback, base::Unretained(this), &load_result));
+ PumpLoop();
+
+ ASSERT_TRUE(load_result->incoming_messages.empty());
+ ASSERT_TRUE(load_result->outgoing_messages.empty());
+}
+
+// Verify saving some outgoing messages, reopening the directory, and then
+// removing those outgoing messages.
+TEST_F(GCMStoreImplTest, OutgoingMessages) {
+ scoped_ptr<GCMStore> gcm_store(BuildGCMStore());
+ scoped_ptr<GCMStore::LoadResult> load_result;
+ gcm_store->Load(base::Bind(
+ &GCMStoreImplTest::LoadCallback, base::Unretained(this), &load_result));
+ PumpLoop();
+
+ std::vector<std::string> persistent_ids;
+ const int kNumPersistentIds = 10;
+ for (int i = 0; i < kNumPersistentIds; ++i) {
+ persistent_ids.push_back(GetNextPersistentId());
+ mcs_proto::DataMessageStanza message;
+ message.set_from(kAppName + persistent_ids.back());
+ message.set_category(kCategoryName + persistent_ids.back());
+ gcm_store->AddOutgoingMessage(
+ persistent_ids.back(),
+ MCSMessage(message),
+ base::Bind(&GCMStoreImplTest::UpdateCallback, base::Unretained(this)));
+ PumpLoop();
+ }
+
+ gcm_store = BuildGCMStore().Pass();
+ gcm_store->Load(base::Bind(
+ &GCMStoreImplTest::LoadCallback, base::Unretained(this), &load_result));
+ PumpLoop();
+
+ ASSERT_TRUE(load_result->incoming_messages.empty());
+ ASSERT_EQ(load_result->outgoing_messages.size(), persistent_ids.size());
+ for (int i = 0; i < kNumPersistentIds; ++i) {
+ std::string id = persistent_ids[i];
+ ASSERT_TRUE(load_result->outgoing_messages[id].get());
+ const mcs_proto::DataMessageStanza* message =
+ reinterpret_cast<mcs_proto::DataMessageStanza*>(
+ load_result->outgoing_messages[id].get());
+ ASSERT_EQ(message->from(), kAppName + id);
+ ASSERT_EQ(message->category(), kCategoryName + id);
+ }
+
+ gcm_store->RemoveOutgoingMessages(
+ persistent_ids,
+ base::Bind(&GCMStoreImplTest::UpdateCallback, base::Unretained(this)));
+ PumpLoop();
+
+ gcm_store = BuildGCMStore().Pass();
+ load_result->outgoing_messages.clear();
+ gcm_store->Load(base::Bind(
+ &GCMStoreImplTest::LoadCallback, base::Unretained(this), &load_result));
+ PumpLoop();
+
+ ASSERT_TRUE(load_result->incoming_messages.empty());
+ ASSERT_TRUE(load_result->outgoing_messages.empty());
+}
+
+// Verify incoming and outgoing messages don't conflict.
+TEST_F(GCMStoreImplTest, IncomingAndOutgoingMessages) {
+ scoped_ptr<GCMStore> gcm_store(BuildGCMStore());
+ scoped_ptr<GCMStore::LoadResult> load_result;
+ gcm_store->Load(base::Bind(
+ &GCMStoreImplTest::LoadCallback, base::Unretained(this), &load_result));
+ PumpLoop();
+
+ std::vector<std::string> persistent_ids;
+ const int kNumPersistentIds = 10;
+ for (int i = 0; i < kNumPersistentIds; ++i) {
+ persistent_ids.push_back(GetNextPersistentId());
+ gcm_store->AddIncomingMessage(
+ persistent_ids.back(),
+ base::Bind(&GCMStoreImplTest::UpdateCallback, base::Unretained(this)));
+ PumpLoop();
+
+ mcs_proto::DataMessageStanza message;
+ message.set_from(kAppName + persistent_ids.back());
+ message.set_category(kCategoryName + persistent_ids.back());
+ gcm_store->AddOutgoingMessage(
+ persistent_ids.back(),
+ MCSMessage(message),
+ base::Bind(&GCMStoreImplTest::UpdateCallback, base::Unretained(this)));
+ PumpLoop();
+ }
+
+ gcm_store = BuildGCMStore().Pass();
+ gcm_store->Load(base::Bind(
+ &GCMStoreImplTest::LoadCallback, base::Unretained(this), &load_result));
+ PumpLoop();
+
+ ASSERT_EQ(persistent_ids, load_result->incoming_messages);
+ ASSERT_EQ(load_result->outgoing_messages.size(), persistent_ids.size());
+ for (int i = 0; i < kNumPersistentIds; ++i) {
+ std::string id = persistent_ids[i];
+ ASSERT_TRUE(load_result->outgoing_messages[id].get());
+ const mcs_proto::DataMessageStanza* message =
+ reinterpret_cast<mcs_proto::DataMessageStanza*>(
+ load_result->outgoing_messages[id].get());
+ ASSERT_EQ(message->from(), kAppName + id);
+ ASSERT_EQ(message->category(), kCategoryName + id);
+ }
+
+ gcm_store->RemoveIncomingMessages(
+ persistent_ids,
+ base::Bind(&GCMStoreImplTest::UpdateCallback, base::Unretained(this)));
+ PumpLoop();
+ gcm_store->RemoveOutgoingMessages(
+ persistent_ids,
+ base::Bind(&GCMStoreImplTest::UpdateCallback, base::Unretained(this)));
+ PumpLoop();
+
+ gcm_store = BuildGCMStore().Pass();
+ load_result->incoming_messages.clear();
+ load_result->outgoing_messages.clear();
+ gcm_store->Load(base::Bind(
+ &GCMStoreImplTest::LoadCallback, base::Unretained(this), &load_result));
+ PumpLoop();
+
+ ASSERT_TRUE(load_result->incoming_messages.empty());
+ ASSERT_TRUE(load_result->outgoing_messages.empty());
+}
+
+// Test that per-app message limits are enforced, persisted across restarts,
+// and updated as messages are removed.
+TEST_F(GCMStoreImplTest, PerAppMessageLimits) {
+ scoped_ptr<GCMStore> gcm_store(BuildGCMStore());
+ scoped_ptr<GCMStore::LoadResult> load_result;
+ gcm_store->Load(base::Bind(&GCMStoreImplTest::LoadCallback,
+ base::Unretained(this),
+ &load_result));
+
+ // Add the initial (below app limit) messages.
+ for (int i = 0; i < kNumMessagesPerApp; ++i) {
+ mcs_proto::DataMessageStanza message;
+ message.set_from(kAppName);
+ message.set_category(kCategoryName);
+ EXPECT_TRUE(gcm_store->AddOutgoingMessage(
+ base::IntToString(i),
+ MCSMessage(message),
+ base::Bind(&GCMStoreImplTest::UpdateCallback,
+ base::Unretained(this))));
+ PumpLoop();
+ }
+
+ // Attempting to add some more should fail.
+ for (int i = 0; i < kNumMessagesPerApp; ++i) {
+ mcs_proto::DataMessageStanza message;
+ message.set_from(kAppName);
+ message.set_category(kCategoryName);
+ EXPECT_FALSE(gcm_store->AddOutgoingMessage(
+ base::IntToString(i + kNumMessagesPerApp),
+ MCSMessage(message),
+ base::Bind(&GCMStoreImplTest::UpdateCallback,
+ base::Unretained(this))));
+ PumpLoop();
+ }
+
+ // Tear down and restore the database.
+ gcm_store = BuildGCMStore().Pass();
+ gcm_store->Load(base::Bind(&GCMStoreImplTest::LoadCallback,
+ base::Unretained(this),
+ &load_result));
+ PumpLoop();
+
+ // Adding more messages should still fail.
+ for (int i = 0; i < kNumMessagesPerApp; ++i) {
+ mcs_proto::DataMessageStanza message;
+ message.set_from(kAppName);
+ message.set_category(kCategoryName);
+ EXPECT_FALSE(gcm_store->AddOutgoingMessage(
+ base::IntToString(i + kNumMessagesPerApp),
+ MCSMessage(message),
+ base::Bind(&GCMStoreImplTest::UpdateCallback,
+ base::Unretained(this))));
+ PumpLoop();
+ }
+
+ // Remove the existing messages.
+ for (int i = 0; i < kNumMessagesPerApp; ++i) {
+ gcm_store->RemoveOutgoingMessage(
+ base::IntToString(i),
+ base::Bind(&GCMStoreImplTest::UpdateCallback,
+ base::Unretained(this)));
+ PumpLoop();
+ }
+
+ // Successfully add new messages.
+ for (int i = 0; i < kNumMessagesPerApp; ++i) {
+ mcs_proto::DataMessageStanza message;
+ message.set_from(kAppName);
+ message.set_category(kCategoryName);
+ EXPECT_TRUE(gcm_store->AddOutgoingMessage(
+ base::IntToString(i + kNumMessagesPerApp),
+ MCSMessage(message),
+ base::Bind(&GCMStoreImplTest::UpdateCallback,
+ base::Unretained(this))));
+ PumpLoop();
+ }
+}
+
+// When the database is destroyed, all database updates should fail. At the
+// same time, they per-app message counts should not go up, as failures should
+// result in decrementing the counts.
+TEST_F(GCMStoreImplTest, AddMessageAfterDestroy) {
+ scoped_ptr<GCMStore> gcm_store(BuildGCMStore());
+ scoped_ptr<GCMStore::LoadResult> load_result;
+ gcm_store->Load(base::Bind(&GCMStoreImplTest::LoadCallback,
+ base::Unretained(this),
+ &load_result));
+ PumpLoop();
+ gcm_store->Destroy(base::Bind(&GCMStoreImplTest::UpdateCallback,
+ base::Unretained(this)));
+ PumpLoop();
+
+ expected_success_ = false;
+ for (int i = 0; i < kNumMessagesPerApp * 2; ++i) {
+ mcs_proto::DataMessageStanza message;
+ message.set_from(kAppName);
+ message.set_category(kCategoryName);
+ // Because all adds are failing, none should hit the per-app message limits.
+ EXPECT_TRUE(gcm_store->AddOutgoingMessage(
+ base::IntToString(i),
+ MCSMessage(message),
+ base::Bind(&GCMStoreImplTest::UpdateCallback,
+ base::Unretained(this))));
+ PumpLoop();
+ }
+}
+
+TEST_F(GCMStoreImplTest, ReloadAfterClose) {
+ scoped_ptr<GCMStore> gcm_store(BuildGCMStore());
+ scoped_ptr<GCMStore::LoadResult> load_result;
+ gcm_store->Load(base::Bind(&GCMStoreImplTest::LoadCallback,
+ base::Unretained(this),
+ &load_result));
+ PumpLoop();
+
+ gcm_store->Close();
+ PumpLoop();
+
+ gcm_store->Load(base::Bind(&GCMStoreImplTest::LoadCallback,
+ base::Unretained(this),
+ &load_result));
+ PumpLoop();
+}
+
+} // namespace
+
+} // namespace gcm
diff --git a/chromium/google_apis/gcm/engine/gservices_settings.cc b/chromium/google_apis/gcm/engine/gservices_settings.cc
new file mode 100644
index 00000000000..4e989724b7e
--- /dev/null
+++ b/chromium/google_apis/gcm/engine/gservices_settings.cc
@@ -0,0 +1,342 @@
+// 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 "google_apis/gcm/engine/gservices_settings.h"
+
+#include "base/bind.h"
+#include "base/sha1.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/string_util.h"
+#include "base/strings/stringprintf.h"
+
+namespace {
+// The expected time in seconds between periodic checkins.
+const char kCheckinIntervalKey[] = "checkin_interval";
+// The override URL to the checkin server.
+const char kCheckinURLKey[] = "checkin_url";
+// The MCS machine name to connect to.
+const char kMCSHostnameKey[] = "gcm_hostname";
+// The MCS port to connect to.
+const char kMCSSecurePortKey[] = "gcm_secure_port";
+// The URL to get MCS registration IDs.
+const char kRegistrationURLKey[] = "gcm_registration_url";
+
+const int64 kDefaultCheckinInterval = 2 * 24 * 60 * 60; // seconds = 2 days.
+const int64 kMinimumCheckinInterval = 12 * 60 * 60; // seconds = 12 hours.
+const char kDefaultCheckinURL[] = "https://android.clients.google.com/checkin";
+const char kDefaultMCSHostname[] = "mtalk.google.com";
+const int kDefaultMCSMainSecurePort = 5228;
+const int kDefaultMCSFallbackSecurePort = 443;
+const char kDefaultRegistrationURL[] =
+ "https://android.clients.google.com/c2dm/register3";
+// Settings that are to be deleted are marked with this prefix in checkin
+// response.
+const char kDeleteSettingPrefix[] = "delete_";
+// Settings digest starts with verison number followed by '-'.
+const char kDigestVersionPrefix[] = "1-";
+const char kMCSEnpointTemplate[] = "https://%s:%d";
+const int kMaxSecurePort = 65535;
+
+std::string MakeMCSEndpoint(const std::string& mcs_hostname, int port) {
+ return base::StringPrintf(kMCSEnpointTemplate, mcs_hostname.c_str(), port);
+}
+
+// Default settings can be omitted, as GServicesSettings class provides
+// reasonable defaults.
+bool CanBeOmitted(const std::string& settings_name) {
+ return settings_name == kCheckinIntervalKey ||
+ settings_name == kCheckinURLKey ||
+ settings_name == kMCSHostnameKey ||
+ settings_name == kMCSSecurePortKey ||
+ settings_name == kRegistrationURLKey;
+}
+
+bool VerifyCheckinInterval(
+ const gcm::GServicesSettings::SettingsMap& settings) {
+ gcm::GServicesSettings::SettingsMap::const_iterator iter =
+ settings.find(kCheckinIntervalKey);
+ if (iter == settings.end())
+ return CanBeOmitted(kCheckinIntervalKey);
+
+ int64 checkin_interval = kMinimumCheckinInterval;
+ if (!base::StringToInt64(iter->second, &checkin_interval)) {
+ DVLOG(1) << "Failed to parse checkin interval: " << iter->second;
+ return false;
+ }
+ if (checkin_interval == std::numeric_limits<int64>::max()) {
+ DVLOG(1) << "Checkin interval is too big: " << checkin_interval;
+ return false;
+ }
+ if (checkin_interval < kMinimumCheckinInterval) {
+ DVLOG(1) << "Checkin interval: " << checkin_interval
+ << " is less than allowed minimum: " << kMinimumCheckinInterval;
+ }
+
+ return true;
+}
+
+bool VerifyMCSEndpoint(const gcm::GServicesSettings::SettingsMap& settings) {
+ std::string mcs_hostname;
+ gcm::GServicesSettings::SettingsMap::const_iterator iter =
+ settings.find(kMCSHostnameKey);
+ if (iter == settings.end()) {
+ // Because endpoint has 2 parts (hostname and port) we are defaulting and
+ // moving on with verification.
+ if (CanBeOmitted(kMCSHostnameKey))
+ mcs_hostname = kDefaultMCSHostname;
+ else
+ return false;
+ } else if (iter->second.empty()) {
+ DVLOG(1) << "Empty MCS hostname provided.";
+ return false;
+ } else {
+ mcs_hostname = iter->second;
+ }
+
+ int mcs_secure_port = 0;
+ iter = settings.find(kMCSSecurePortKey);
+ if (iter == settings.end()) {
+ // Simlarly we might have to default the port, when only hostname is
+ // provided.
+ if (CanBeOmitted(kMCSSecurePortKey))
+ mcs_secure_port = kDefaultMCSMainSecurePort;
+ else
+ return false;
+ } else if (!base::StringToInt(iter->second, &mcs_secure_port)) {
+ DVLOG(1) << "Failed to parse MCS secure port: " << iter->second;
+ return false;
+ }
+
+ if (mcs_secure_port < 0 || mcs_secure_port > kMaxSecurePort) {
+ DVLOG(1) << "Incorrect port value: " << mcs_secure_port;
+ return false;
+ }
+
+ GURL mcs_main_endpoint(MakeMCSEndpoint(mcs_hostname, mcs_secure_port));
+ if (!mcs_main_endpoint.is_valid()) {
+ DVLOG(1) << "Invalid main MCS endpoint: "
+ << mcs_main_endpoint.possibly_invalid_spec();
+ return false;
+ }
+ GURL mcs_fallback_endpoint(
+ MakeMCSEndpoint(mcs_hostname, kDefaultMCSFallbackSecurePort));
+ if (!mcs_fallback_endpoint.is_valid()) {
+ DVLOG(1) << "Invalid fallback MCS endpoint: "
+ << mcs_fallback_endpoint.possibly_invalid_spec();
+ return false;
+ }
+
+ return true;
+}
+
+bool VerifyCheckinURL(const gcm::GServicesSettings::SettingsMap& settings) {
+ gcm::GServicesSettings::SettingsMap::const_iterator iter =
+ settings.find(kCheckinURLKey);
+ if (iter == settings.end())
+ return CanBeOmitted(kCheckinURLKey);
+
+ GURL checkin_url(iter->second);
+ if (!checkin_url.is_valid()) {
+ DVLOG(1) << "Invalid checkin URL provided: " << iter->second;
+ return false;
+ }
+
+ return true;
+}
+
+bool VerifyRegistrationURL(
+ const gcm::GServicesSettings::SettingsMap& settings) {
+ gcm::GServicesSettings::SettingsMap::const_iterator iter =
+ settings.find(kRegistrationURLKey);
+ if (iter == settings.end())
+ return CanBeOmitted(kRegistrationURLKey);
+
+ GURL registration_url(iter->second);
+ if (!registration_url.is_valid()) {
+ DVLOG(1) << "Invalid registration URL provided: " << iter->second;
+ return false;
+ }
+
+ return true;
+}
+
+bool VerifySettings(const gcm::GServicesSettings::SettingsMap& settings) {
+ return VerifyCheckinInterval(settings) && VerifyMCSEndpoint(settings) &&
+ VerifyCheckinURL(settings) && VerifyRegistrationURL(settings);
+}
+
+} // namespace
+
+namespace gcm {
+
+// static
+const base::TimeDelta GServicesSettings::MinimumCheckinInterval() {
+ return base::TimeDelta::FromSeconds(kMinimumCheckinInterval);
+}
+
+// static
+const GURL GServicesSettings::DefaultCheckinURL() {
+ return GURL(kDefaultCheckinURL);
+}
+
+// static
+std::string GServicesSettings::CalculateDigest(const SettingsMap& settings) {
+ unsigned char hash[base::kSHA1Length];
+ std::string data;
+ for (SettingsMap::const_iterator iter = settings.begin();
+ iter != settings.end();
+ ++iter) {
+ data += iter->first;
+ data += '\0';
+ data += iter->second;
+ data += '\0';
+ }
+ base::SHA1HashBytes(
+ reinterpret_cast<const unsigned char*>(&data[0]), data.size(), hash);
+ std::string digest =
+ kDigestVersionPrefix + base::HexEncode(hash, base::kSHA1Length);
+ digest = StringToLowerASCII(digest);
+ return digest;
+}
+
+GServicesSettings::GServicesSettings() : weak_ptr_factory_(this) {
+ digest_ = CalculateDigest(settings_);
+}
+
+GServicesSettings::~GServicesSettings() {
+}
+
+bool GServicesSettings::UpdateFromCheckinResponse(
+ const checkin_proto::AndroidCheckinResponse& checkin_response) {
+ if (!checkin_response.has_settings_diff()) {
+ DVLOG(1) << "Field settings_diff not set in response.";
+ return false;
+ }
+
+ bool settings_diff = checkin_response.settings_diff();
+ SettingsMap new_settings;
+ // Only reuse the existing settings, if we are given a settings difference.
+ if (settings_diff)
+ new_settings = settings_map();
+
+ for (int i = 0; i < checkin_response.setting_size(); ++i) {
+ std::string name = checkin_response.setting(i).name();
+ if (name.empty()) {
+ DVLOG(1) << "Setting name is empty";
+ return false;
+ }
+
+ if (settings_diff && name.find(kDeleteSettingPrefix) == 0) {
+ std::string setting_to_delete =
+ name.substr(arraysize(kDeleteSettingPrefix) - 1);
+ new_settings.erase(setting_to_delete);
+ DVLOG(1) << "Setting deleted: " << setting_to_delete;
+ } else {
+ std::string value = checkin_response.setting(i).value();
+ new_settings[name] = value;
+ DVLOG(1) << "New setting: '" << name << "' : '" << value << "'";
+ }
+ }
+
+ if (!VerifySettings(new_settings))
+ return false;
+
+ settings_.swap(new_settings);
+ digest_ = CalculateDigest(settings_);
+ return true;
+}
+
+void GServicesSettings::UpdateFromLoadResult(
+ const GCMStore::LoadResult& load_result) {
+ // No need to try to update settings when load_result is empty.
+ if (load_result.gservices_settings.empty())
+ return;
+ if (!VerifySettings(load_result.gservices_settings))
+ return;
+ std::string digest = CalculateDigest(load_result.gservices_settings);
+ if (digest != load_result.gservices_digest) {
+ DVLOG(1) << "G-services settings digest mismatch. "
+ << "Expected digest: " << load_result.gservices_digest
+ << ". Calculated digest is: " << digest;
+ return;
+ }
+
+ settings_ = load_result.gservices_settings;
+ digest_ = load_result.gservices_digest;
+}
+
+base::TimeDelta GServicesSettings::GetCheckinInterval() const {
+ int64 checkin_interval = kMinimumCheckinInterval;
+ SettingsMap::const_iterator iter = settings_.find(kCheckinIntervalKey);
+ if (iter == settings_.end() ||
+ !base::StringToInt64(iter->second, &checkin_interval)) {
+ checkin_interval = kDefaultCheckinInterval;
+ }
+
+ if (checkin_interval < kMinimumCheckinInterval)
+ checkin_interval = kMinimumCheckinInterval;
+
+ return base::TimeDelta::FromSeconds(checkin_interval);
+}
+
+GURL GServicesSettings::GetCheckinURL() const {
+ SettingsMap::const_iterator iter = settings_.find(kCheckinURLKey);
+ if (iter == settings_.end() || iter->second.empty())
+ return GURL(kDefaultCheckinURL);
+ return GURL(iter->second);
+}
+
+GURL GServicesSettings::GetMCSMainEndpoint() const {
+ // Get alternative hostname or use default.
+ std::string mcs_hostname;
+ SettingsMap::const_iterator iter = settings_.find(kMCSHostnameKey);
+ if (iter != settings_.end() && !iter->second.empty())
+ mcs_hostname = iter->second;
+ else
+ mcs_hostname = kDefaultMCSHostname;
+
+ // Get alternative secure port or use defualt.
+ int mcs_secure_port = 0;
+ iter = settings_.find(kMCSSecurePortKey);
+ if (iter == settings_.end() || iter->second.empty() ||
+ !base::StringToInt(iter->second, &mcs_secure_port)) {
+ mcs_secure_port = kDefaultMCSMainSecurePort;
+ }
+
+ // If constructed address makes sense use it.
+ GURL mcs_endpoint(MakeMCSEndpoint(mcs_hostname, mcs_secure_port));
+ if (mcs_endpoint.is_valid())
+ return mcs_endpoint;
+
+ // Otherwise use default settings.
+ return GURL(MakeMCSEndpoint(kDefaultMCSHostname, kDefaultMCSMainSecurePort));
+}
+
+GURL GServicesSettings::GetMCSFallbackEndpoint() const {
+ // Get alternative hostname or use default.
+ std::string mcs_hostname;
+ SettingsMap::const_iterator iter = settings_.find(kMCSHostnameKey);
+ if (iter != settings_.end() && !iter->second.empty())
+ mcs_hostname = iter->second;
+ else
+ mcs_hostname = kDefaultMCSHostname;
+
+ // If constructed address makes sense use it.
+ GURL mcs_endpoint(
+ MakeMCSEndpoint(mcs_hostname, kDefaultMCSFallbackSecurePort));
+ if (mcs_endpoint.is_valid())
+ return mcs_endpoint;
+
+ return GURL(
+ MakeMCSEndpoint(kDefaultMCSHostname, kDefaultMCSFallbackSecurePort));
+}
+
+GURL GServicesSettings::GetRegistrationURL() const {
+ SettingsMap::const_iterator iter = settings_.find(kRegistrationURLKey);
+ if (iter == settings_.end() || iter->second.empty())
+ return GURL(kDefaultRegistrationURL);
+ return GURL(iter->second);
+}
+
+} // namespace gcm
diff --git a/chromium/google_apis/gcm/engine/gservices_settings.h b/chromium/google_apis/gcm/engine/gservices_settings.h
new file mode 100644
index 00000000000..d3aeeb03d29
--- /dev/null
+++ b/chromium/google_apis/gcm/engine/gservices_settings.h
@@ -0,0 +1,82 @@
+// 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 GOOGLE_APIS_GCM_ENGINE_GSERVICES_SETTINGS_H_
+#define GOOGLE_APIS_GCM_ENGINE_GSERVICES_SETTINGS_H_
+
+#include <map>
+#include <string>
+
+#include "base/memory/weak_ptr.h"
+#include "base/time/time.h"
+#include "google_apis/gcm/base/gcm_export.h"
+#include "google_apis/gcm/engine/gcm_store.h"
+#include "google_apis/gcm/protocol/checkin.pb.h"
+#include "url/gurl.h"
+
+namespace gcm {
+
+// Class responsible for handling G-services settings. It takes care of
+// extracting them from checkin response and storing in GCMStore.
+class GCM_EXPORT GServicesSettings {
+ public:
+ typedef std::map<std::string, std::string> SettingsMap;
+
+ // Minimum periodic checkin interval in seconds.
+ static const base::TimeDelta MinimumCheckinInterval();
+
+ // Default checkin URL.
+ static const GURL DefaultCheckinURL();
+
+ // Calculates digest of provided settings.
+ static std::string CalculateDigest(const SettingsMap& settings);
+
+ GServicesSettings();
+ ~GServicesSettings();
+
+ // Updates the settings based on |checkin_response|.
+ bool UpdateFromCheckinResponse(
+ const checkin_proto::AndroidCheckinResponse& checkin_response);
+
+ // Updates the settings based on |load_result|. Returns true if update was
+ // successful, false otherwise.
+ void UpdateFromLoadResult(const GCMStore::LoadResult& load_result);
+
+ SettingsMap settings_map() const { return settings_; }
+
+ std::string digest() const { return digest_; }
+
+ // Gets the interval at which device should perform a checkin.
+ base::TimeDelta GetCheckinInterval() const;
+
+ // Gets the URL to use when checking in.
+ GURL GetCheckinURL() const;
+
+ // Gets address of main MCS endpoint.
+ GURL GetMCSMainEndpoint() const;
+
+ // Gets address of fallback MCS endpoint.
+ GURL GetMCSFallbackEndpoint() const;
+
+ // Gets the URL to use when registering or unregistering the apps.
+ GURL GetRegistrationURL() const;
+
+ private:
+ // Digest (hash) of the settings, used to check whether settings need update.
+ // It is meant to be sent with checkin request, instead of sending the whole
+ // settings table.
+ std::string digest_;
+
+ // G-services settings as provided by checkin response.
+ SettingsMap settings_;
+
+ // Factory for creating references in callbacks.
+ base::WeakPtrFactory<GServicesSettings> weak_ptr_factory_;
+
+ DISALLOW_COPY_AND_ASSIGN(GServicesSettings);
+};
+
+} // namespace gcm
+
+#endif // GOOGLE_APIS_GCM_ENGINE_GSERVICES_SETTINGS_H_
diff --git a/chromium/google_apis/gcm/engine/gservices_settings_unittest.cc b/chromium/google_apis/gcm/engine/gservices_settings_unittest.cc
new file mode 100644
index 00000000000..9e02109200c
--- /dev/null
+++ b/chromium/google_apis/gcm/engine/gservices_settings_unittest.cc
@@ -0,0 +1,336 @@
+// 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/strings/string_number_conversions.h"
+#include "google_apis/gcm/engine/gservices_settings.h"
+#include "google_apis/gcm/engine/registration_info.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace gcm {
+
+namespace {
+
+const int64 kAlternativeCheckinInterval = 16 * 60 * 60;
+const char kAlternativeCheckinURL[] = "http://alternative.url/checkin";
+const char kAlternativeMCSHostname[] = "alternative.gcm.host";
+const int kAlternativeMCSSecurePort = 7777;
+const char kAlternativeRegistrationURL[] =
+ "http://alternative.url/registration";
+
+const int64 kDefaultCheckinInterval = 2 * 24 * 60 * 60; // seconds = 2 days.
+const char kDefaultCheckinURL[] = "https://android.clients.google.com/checkin";
+const char kDefaultRegistrationURL[] =
+ "https://android.clients.google.com/c2dm/register3";
+const char kDefaultSettingsDigest[] =
+ "1-da39a3ee5e6b4b0d3255bfef95601890afd80709";
+const char kAlternativeSettingsDigest[] =
+ "1-7da4aa4eb38a8bd3e330e3751cc0899924499134";
+
+void AddSettingsToResponse(
+ checkin_proto::AndroidCheckinResponse& checkin_response,
+ const GServicesSettings::SettingsMap& settings,
+ bool settings_diff) {
+ for (GServicesSettings::SettingsMap::const_iterator iter = settings.begin();
+ iter != settings.end();
+ ++iter) {
+ checkin_proto::GservicesSetting* setting = checkin_response.add_setting();
+ setting->set_name(iter->first);
+ setting->set_value(iter->second);
+ }
+ checkin_response.set_settings_diff(settings_diff);
+}
+
+} // namespace
+
+class GServicesSettingsTest : public testing::Test {
+ public:
+ GServicesSettingsTest();
+ virtual ~GServicesSettingsTest();
+
+ void CheckAllSetToDefault();
+
+ GServicesSettings& settings() {
+ return gserivces_settings_;
+ }
+
+ private:
+ GServicesSettings gserivces_settings_;
+};
+
+GServicesSettingsTest::GServicesSettingsTest()
+ : gserivces_settings_() {
+}
+
+GServicesSettingsTest::~GServicesSettingsTest() {}
+
+void GServicesSettingsTest::CheckAllSetToDefault() {
+ EXPECT_EQ(base::TimeDelta::FromSeconds(kDefaultCheckinInterval),
+ settings().GetCheckinInterval());
+ EXPECT_EQ(GURL(kDefaultCheckinURL), settings().GetCheckinURL());
+ EXPECT_EQ(GURL("https://mtalk.google.com:5228"),
+ settings().GetMCSMainEndpoint());
+ EXPECT_EQ(GURL("https://mtalk.google.com:443"),
+ settings().GetMCSFallbackEndpoint());
+ EXPECT_EQ(GURL(kDefaultRegistrationURL), settings().GetRegistrationURL());
+}
+
+// Verifies default values of the G-services settings and settings digest.
+TEST_F(GServicesSettingsTest, DefaultSettingsAndDigest) {
+ CheckAllSetToDefault();
+ EXPECT_EQ(kDefaultSettingsDigest, settings().digest());
+ EXPECT_EQ(kDefaultSettingsDigest,
+ GServicesSettings::CalculateDigest(settings().settings_map()));
+}
+
+// Verifies digest calculation for the sample provided by protocol owners.
+TEST_F(GServicesSettingsTest, CalculateDigest) {
+ GServicesSettings::SettingsMap settings_map;
+ settings_map["android_id"] = "55XXXXXXXXXXXXXXXX0";
+ settings_map["checkin_interval"] = "86400";
+ settings_map["checkin_url"] =
+ "https://fake.address.google.com/canary/checkin";
+ settings_map["chrome_device"] = "1";
+ settings_map["device_country"] = "us";
+ settings_map["gcm_hostname"] = "fake.address.google.com";
+ settings_map["gcm_secure_port"] = "443";
+
+ EXPECT_EQ("1-33381ccd1cf5791dc0e6dfa234266fa9f1259197",
+ GServicesSettings::CalculateDigest(settings_map));
+}
+
+// Verifies that settings are not updated when load result is empty.
+TEST_F(GServicesSettingsTest, UpdateFromEmptyLoadResult) {
+ GCMStore::LoadResult result;
+ result.gservices_digest = "";
+ settings().UpdateFromLoadResult(result);
+
+ CheckAllSetToDefault();
+ EXPECT_EQ(kDefaultSettingsDigest, settings().digest());
+}
+
+// Verifies that settings are not when digest value does not match.
+TEST_F(GServicesSettingsTest, UpdateFromLoadResultWithSettingMissing) {
+ GCMStore::LoadResult result;
+ result.gservices_settings["checkin_internval"] = "100000";
+ result.gservices_digest = "digest_value";
+ settings().UpdateFromLoadResult(result);
+
+ CheckAllSetToDefault();
+ EXPECT_EQ(kDefaultSettingsDigest, settings().digest());
+}
+
+// Verifies that the settings are set correctly based on the load result.
+TEST_F(GServicesSettingsTest, UpdateFromLoadResult) {
+ GCMStore::LoadResult result;
+ result.gservices_settings["checkin_interval"] =
+ base::Int64ToString(kAlternativeCheckinInterval);
+ result.gservices_settings["checkin_url"] = kAlternativeCheckinURL;
+ result.gservices_settings["gcm_hostname"] = kAlternativeMCSHostname;
+ result.gservices_settings["gcm_secure_port"] =
+ base::IntToString(kAlternativeMCSSecurePort);
+ result.gservices_settings["gcm_registration_url"] =
+ kAlternativeRegistrationURL;
+ result.gservices_digest = kAlternativeSettingsDigest;
+ settings().UpdateFromLoadResult(result);
+
+ EXPECT_EQ(base::TimeDelta::FromSeconds(kAlternativeCheckinInterval),
+ settings().GetCheckinInterval());
+ EXPECT_EQ(GURL(kAlternativeCheckinURL), settings().GetCheckinURL());
+ EXPECT_EQ(GURL("https://alternative.gcm.host:7777"),
+ settings().GetMCSMainEndpoint());
+ EXPECT_EQ(GURL("https://alternative.gcm.host:443"),
+ settings().GetMCSFallbackEndpoint());
+ EXPECT_EQ(GURL(kAlternativeRegistrationURL), settings().GetRegistrationURL());
+ EXPECT_EQ(GServicesSettings::CalculateDigest(result.gservices_settings),
+ settings().digest());
+}
+
+// Verifies that the checkin interval is updated to minimum if the original
+// value is less than minimum.
+TEST_F(GServicesSettingsTest, CheckinResponseMinimumCheckinInterval) {
+ // Setting the checkin interval to less than minimum.
+ checkin_proto::AndroidCheckinResponse checkin_response;
+ GServicesSettings::SettingsMap new_settings;
+ new_settings["checkin_interval"] = "3600";
+ AddSettingsToResponse(checkin_response, new_settings, false);
+
+ EXPECT_TRUE(settings().UpdateFromCheckinResponse(checkin_response));
+
+ EXPECT_EQ(GServicesSettings::MinimumCheckinInterval(),
+ settings().GetCheckinInterval());
+ EXPECT_EQ(GServicesSettings::CalculateDigest(new_settings),
+ settings().digest());
+}
+
+// Verifies that default checkin interval can be selectively overwritten.
+TEST_F(GServicesSettingsTest, CheckinResponseUpdateCheckinInterval) {
+ checkin_proto::AndroidCheckinResponse checkin_response;
+ GServicesSettings::SettingsMap new_settings;
+ new_settings["checkin_interval"] = "86400";
+ AddSettingsToResponse(checkin_response, new_settings, false);
+
+ EXPECT_TRUE(settings().UpdateFromCheckinResponse(checkin_response));
+
+ // Only the checkin interval was updated:
+ EXPECT_EQ(base::TimeDelta::FromSeconds(86400),
+ settings().GetCheckinInterval());
+
+ // Other settings still set to default.
+ EXPECT_EQ(GURL("https://mtalk.google.com:5228"),
+ settings().GetMCSMainEndpoint());
+ EXPECT_EQ(GURL("https://mtalk.google.com:443"),
+ settings().GetMCSFallbackEndpoint());
+ EXPECT_EQ(GURL(kDefaultCheckinURL), settings().GetCheckinURL());
+ EXPECT_EQ(GURL(kDefaultRegistrationURL), settings().GetRegistrationURL());
+
+ EXPECT_EQ(GServicesSettings::CalculateDigest(new_settings),
+ settings().digest());
+}
+
+// Verifies that default registration URL can be selectively overwritten.
+TEST_F(GServicesSettingsTest, CheckinResponseUpdateRegistrationURL) {
+ checkin_proto::AndroidCheckinResponse checkin_response;
+ GServicesSettings::SettingsMap new_settings;
+ new_settings["gcm_registration_url"] = "https://new.registration.url";
+ AddSettingsToResponse(checkin_response, new_settings, false);
+
+ EXPECT_TRUE(settings().UpdateFromCheckinResponse(checkin_response));
+
+ // Only the registration URL was updated:
+ EXPECT_EQ(GURL("https://new.registration.url"),
+ settings().GetRegistrationURL());
+
+ // Other settings still set to default.
+ EXPECT_EQ(base::TimeDelta::FromSeconds(kDefaultCheckinInterval),
+ settings().GetCheckinInterval());
+ EXPECT_EQ(GURL("https://mtalk.google.com:5228"),
+ settings().GetMCSMainEndpoint());
+ EXPECT_EQ(GURL("https://mtalk.google.com:443"),
+ settings().GetMCSFallbackEndpoint());
+ EXPECT_EQ(GURL(kDefaultCheckinURL), settings().GetCheckinURL());
+
+ EXPECT_EQ(GServicesSettings::CalculateDigest(new_settings),
+ settings().digest());
+}
+
+// Verifies that default checkin URL can be selectively overwritten.
+TEST_F(GServicesSettingsTest, CheckinResponseUpdateCheckinURL) {
+ checkin_proto::AndroidCheckinResponse checkin_response;
+ GServicesSettings::SettingsMap new_settings;
+ new_settings["checkin_url"] = "https://new.checkin.url";
+ AddSettingsToResponse(checkin_response, new_settings, false);
+
+ EXPECT_TRUE(settings().UpdateFromCheckinResponse(checkin_response));
+
+ // Only the checkin URL was updated:
+ EXPECT_EQ(GURL("https://new.checkin.url"), settings().GetCheckinURL());
+
+ // Other settings still set to default.
+ EXPECT_EQ(base::TimeDelta::FromSeconds(kDefaultCheckinInterval),
+ settings().GetCheckinInterval());
+ EXPECT_EQ(GURL("https://mtalk.google.com:5228"),
+ settings().GetMCSMainEndpoint());
+ EXPECT_EQ(GURL("https://mtalk.google.com:443"),
+ settings().GetMCSFallbackEndpoint());
+ EXPECT_EQ(GURL(kDefaultRegistrationURL), settings().GetRegistrationURL());
+
+ EXPECT_EQ(GServicesSettings::CalculateDigest(new_settings),
+ settings().digest());
+}
+
+// Verifies that default MCS hostname can be selectively overwritten.
+TEST_F(GServicesSettingsTest, CheckinResponseUpdateMCSHostname) {
+ checkin_proto::AndroidCheckinResponse checkin_response;
+ GServicesSettings::SettingsMap new_settings;
+ new_settings["gcm_hostname"] = "new.gcm.hostname";
+ AddSettingsToResponse(checkin_response, new_settings, false);
+
+ EXPECT_TRUE(settings().UpdateFromCheckinResponse(checkin_response));
+
+ // Only the MCS endpoints were updated:
+ EXPECT_EQ(GURL("https://new.gcm.hostname:5228"),
+ settings().GetMCSMainEndpoint());
+ EXPECT_EQ(GURL("https://new.gcm.hostname:443"),
+ settings().GetMCSFallbackEndpoint());
+
+ // Other settings still set to default.
+ EXPECT_EQ(base::TimeDelta::FromSeconds(kDefaultCheckinInterval),
+ settings().GetCheckinInterval());
+ EXPECT_EQ(GURL(kDefaultCheckinURL), settings().GetCheckinURL());
+ EXPECT_EQ(GURL(kDefaultRegistrationURL), settings().GetRegistrationURL());
+
+ EXPECT_EQ(GServicesSettings::CalculateDigest(new_settings),
+ settings().digest());
+}
+
+// Verifies that default MCS secure port can be selectively overwritten.
+TEST_F(GServicesSettingsTest, CheckinResponseUpdateMCSSecurePort) {
+ checkin_proto::AndroidCheckinResponse checkin_response;
+ GServicesSettings::SettingsMap new_settings;
+ new_settings["gcm_secure_port"] = "5229";
+ AddSettingsToResponse(checkin_response, new_settings, false);
+
+ EXPECT_TRUE(settings().UpdateFromCheckinResponse(checkin_response));
+
+ // Only the main MCS endpoint was updated:
+ EXPECT_EQ(GURL("https://mtalk.google.com:5229"),
+ settings().GetMCSMainEndpoint());
+
+ // Other settings still set to default.
+ EXPECT_EQ(base::TimeDelta::FromSeconds(kDefaultCheckinInterval),
+ settings().GetCheckinInterval());
+ EXPECT_EQ(GURL(kDefaultCheckinURL), settings().GetCheckinURL());
+ EXPECT_EQ(GURL("https://mtalk.google.com:443"),
+ settings().GetMCSFallbackEndpoint());
+ EXPECT_EQ(GURL(kDefaultRegistrationURL), settings().GetRegistrationURL());
+
+ EXPECT_EQ(GServicesSettings::CalculateDigest(new_settings),
+ settings().digest());
+}
+
+// Update from checkin response should also do incremental update for both cases
+// where some settings are removed or added.
+TEST_F(GServicesSettingsTest, UpdateFromCheckinResponseSettingsDiff) {
+ checkin_proto::AndroidCheckinResponse checkin_response;
+
+ // Only the new settings will be included in the response with settings diff.
+ GServicesSettings::SettingsMap settings_diff;
+ settings_diff["new_setting_1"] = "new_setting_1_value";
+ settings_diff["new_setting_2"] = "new_setting_2_value";
+ settings_diff["gcm_secure_port"] = "5229";
+
+ // Full settings are necessary to calculate digest.
+ GServicesSettings::SettingsMap full_settings(settings_diff);
+ std::string digest = GServicesSettings::CalculateDigest(full_settings);
+
+ checkin_response.Clear();
+ AddSettingsToResponse(checkin_response, settings_diff, true);
+ EXPECT_TRUE(settings().UpdateFromCheckinResponse(checkin_response));
+ EXPECT_EQ(full_settings, settings().settings_map());
+ // Default setting overwritten by settings diff.
+ EXPECT_EQ(GURL("https://mtalk.google.com:5229"),
+ settings().GetMCSMainEndpoint());
+
+ // Setting up diff removing some of the values (including default setting).
+ settings_diff.clear();
+ settings_diff["delete_new_setting_1"] = "";
+ settings_diff["delete_gcm_secure_port"] = "";
+ settings_diff["new_setting_3"] = "new_setting_3_value";
+
+ // Updating full settings to calculate digest.
+ full_settings.erase(full_settings.find("new_setting_1"));
+ full_settings.erase(full_settings.find("gcm_secure_port"));
+ full_settings["new_setting_3"] = "new_setting_3_value";
+ digest = GServicesSettings::CalculateDigest(full_settings);
+
+ checkin_response.Clear();
+ AddSettingsToResponse(checkin_response, settings_diff, true);
+ EXPECT_TRUE(settings().UpdateFromCheckinResponse(checkin_response));
+ EXPECT_EQ(full_settings, settings().settings_map());
+ // Default setting back to norm.
+ EXPECT_EQ(GURL("https://mtalk.google.com:5228"),
+ settings().GetMCSMainEndpoint());
+}
+
+} // namespace gcm
diff --git a/chromium/google_apis/gcm/engine/heartbeat_manager.cc b/chromium/google_apis/gcm/engine/heartbeat_manager.cc
new file mode 100644
index 00000000000..5b5ae476a4b
--- /dev/null
+++ b/chromium/google_apis/gcm/engine/heartbeat_manager.cc
@@ -0,0 +1,119 @@
+// 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 "google_apis/gcm/engine/heartbeat_manager.h"
+
+#include "google_apis/gcm/protocol/mcs.pb.h"
+#include "net/base/network_change_notifier.h"
+
+namespace gcm {
+
+namespace {
+// The default heartbeat when on a mobile or unknown network .
+const int64 kCellHeartbeatDefaultMs = 1000 * 60 * 28; // 28 minutes.
+// The default heartbeat when on WiFi (also used for ethernet).
+const int64 kWifiHeartbeatDefaultMs = 1000 * 60 * 15; // 15 minutes.
+// The default heartbeat ack interval.
+const int64 kHeartbeatAckDefaultMs = 1000 * 60 * 1; // 1 minute.
+} // namespace
+
+HeartbeatManager::HeartbeatManager()
+ : waiting_for_ack_(false),
+ heartbeat_interval_ms_(0),
+ server_interval_ms_(0),
+ heartbeat_timer_(true /* retain user task */,
+ false /* not repeating */),
+ weak_ptr_factory_(this) {}
+
+HeartbeatManager::~HeartbeatManager() {}
+
+void HeartbeatManager::Start(
+ const base::Closure& send_heartbeat_callback,
+ const base::Closure& trigger_reconnect_callback) {
+ DCHECK(!send_heartbeat_callback.is_null());
+ DCHECK(!trigger_reconnect_callback.is_null());
+ send_heartbeat_callback_ = send_heartbeat_callback;
+ trigger_reconnect_callback_ = trigger_reconnect_callback;
+
+ // Kicks off the timer.
+ waiting_for_ack_ = false;
+ RestartTimer();
+}
+
+void HeartbeatManager::Stop() {
+ heartbeat_timer_.Stop();
+ waiting_for_ack_ = false;
+}
+
+void HeartbeatManager::OnHeartbeatAcked() {
+ if (!heartbeat_timer_.IsRunning())
+ return;
+
+ DCHECK(!send_heartbeat_callback_.is_null());
+ DCHECK(!trigger_reconnect_callback_.is_null());
+ waiting_for_ack_ = false;
+ RestartTimer();
+}
+
+void HeartbeatManager::UpdateHeartbeatConfig(
+ const mcs_proto::HeartbeatConfig& config) {
+ if (!config.IsInitialized() ||
+ !config.has_interval_ms() ||
+ config.interval_ms() <= 0) {
+ return;
+ }
+ DVLOG(1) << "Updating heartbeat interval to " << config.interval_ms();
+ server_interval_ms_ = config.interval_ms();
+}
+
+base::TimeTicks HeartbeatManager::GetNextHeartbeatTime() const {
+ if (heartbeat_timer_.IsRunning())
+ return heartbeat_timer_.desired_run_time();
+ else
+ return base::TimeTicks();
+}
+
+void HeartbeatManager::OnHeartbeatTriggered() {
+ if (waiting_for_ack_) {
+ LOG(WARNING) << "Lost connection to MCS, reconnecting.";
+ Stop();
+ trigger_reconnect_callback_.Run();
+ return;
+ }
+
+ waiting_for_ack_ = true;
+ RestartTimer();
+ send_heartbeat_callback_.Run();
+}
+
+void HeartbeatManager::RestartTimer() {
+ if (!waiting_for_ack_) {
+ // Recalculate the timer interval based network type.
+ if (server_interval_ms_ != 0) {
+ // If a server interval is set, it overrides any local one.
+ heartbeat_interval_ms_ = server_interval_ms_;
+ } else if (net::NetworkChangeNotifier::GetConnectionType() ==
+ net::NetworkChangeNotifier::CONNECTION_WIFI ||
+ net::NetworkChangeNotifier::GetConnectionType() ==
+ net::NetworkChangeNotifier::CONNECTION_ETHERNET) {
+ heartbeat_interval_ms_ = kWifiHeartbeatDefaultMs;
+ } else {
+ // For unknown connections, use the longer cellular heartbeat interval.
+ heartbeat_interval_ms_ = kCellHeartbeatDefaultMs;
+ }
+ DVLOG(1) << "Sending next heartbeat in "
+ << heartbeat_interval_ms_ << " ms.";
+ } else {
+ heartbeat_interval_ms_ = kHeartbeatAckDefaultMs;
+ DVLOG(1) << "Resetting timer for ack with "
+ << heartbeat_interval_ms_ << " ms interval.";
+ }
+ heartbeat_timer_.Start(FROM_HERE,
+ base::TimeDelta::FromMilliseconds(
+ heartbeat_interval_ms_),
+ base::Bind(&HeartbeatManager::OnHeartbeatTriggered,
+ weak_ptr_factory_.GetWeakPtr()));
+}
+
+} // namespace gcm
diff --git a/chromium/google_apis/gcm/engine/heartbeat_manager.h b/chromium/google_apis/gcm/engine/heartbeat_manager.h
new file mode 100644
index 00000000000..9266b98c8d4
--- /dev/null
+++ b/chromium/google_apis/gcm/engine/heartbeat_manager.h
@@ -0,0 +1,82 @@
+// 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 GOOGLE_APIS_GCM_ENGINE_HEARTBEAT_MANAGER_H_
+#define GOOGLE_APIS_GCM_ENGINE_HEARTBEAT_MANAGER_H_
+
+#include "base/callback.h"
+#include "base/logging.h"
+#include "base/memory/weak_ptr.h"
+#include "base/timer/timer.h"
+#include "google_apis/gcm/base/gcm_export.h"
+
+namespace mcs_proto {
+class HeartbeatConfig;
+}
+
+namespace gcm {
+
+// A heartbeat management class, capable of sending and handling heartbeat
+// receipt/failures and triggering reconnection as necessary.
+class GCM_EXPORT HeartbeatManager {
+ public:
+ HeartbeatManager();
+ ~HeartbeatManager();
+
+ // Start the heartbeat logic.
+ // |send_heartbeat_callback_| is the callback the HeartbeatManager uses to
+ // send new heartbeats. Only one heartbeat can be outstanding at a time.
+ void Start(const base::Closure& send_heartbeat_callback,
+ const base::Closure& trigger_reconnect_callback);
+
+ // Stop the timer. Start(..) must be called again to begin sending heartbeats
+ // afterwards.
+ void Stop();
+
+ // Reset the heartbeat timer. It is valid to call this even if no heartbeat
+ // is associated with the ack (for example if another signal is used to
+ // determine that the connection is alive).
+ void OnHeartbeatAcked();
+
+ // Updates the current heartbeat interval.
+ void UpdateHeartbeatConfig(const mcs_proto::HeartbeatConfig& config);
+
+ // Returns the next scheduled heartbeat time. A null time means
+ // no heartbeat is pending. If non-null and less than the
+ // current time (in ticks), the heartbeat has been triggered and an ack is
+ // pending.
+ base::TimeTicks GetNextHeartbeatTime() const;
+
+ protected:
+ // Helper method to send heartbeat on timer trigger.
+ void OnHeartbeatTriggered();
+
+ private:
+ // Restarts the heartbeat timer.
+ void RestartTimer();
+
+ // Whether the last heartbeat ping sent has been acknowledged or not.
+ bool waiting_for_ack_;
+
+ // The current heartbeat interval.
+ int heartbeat_interval_ms_;
+ // The most recent server-provided heartbeat interval (0 if none has been
+ // provided).
+ int server_interval_ms_;
+
+ // Timer for triggering heartbeats.
+ base::Timer heartbeat_timer_;
+
+ // Callbacks for interacting with the the connection.
+ base::Closure send_heartbeat_callback_;
+ base::Closure trigger_reconnect_callback_;
+
+ base::WeakPtrFactory<HeartbeatManager> weak_ptr_factory_;
+
+ DISALLOW_COPY_AND_ASSIGN(HeartbeatManager);
+};
+
+} // namespace gcm
+
+#endif // GOOGLE_APIS_GCM_ENGINE_HEARTBEAT_MANAGER_H_
diff --git a/chromium/google_apis/gcm/engine/heartbeat_manager_unittest.cc b/chromium/google_apis/gcm/engine/heartbeat_manager_unittest.cc
new file mode 100644
index 00000000000..2c3b668d533
--- /dev/null
+++ b/chromium/google_apis/gcm/engine/heartbeat_manager_unittest.cc
@@ -0,0 +1,176 @@
+// 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 "google_apis/gcm/engine/heartbeat_manager.h"
+
+#include "base/message_loop/message_loop.h"
+#include "base/time/time.h"
+#include "google_apis/gcm/protocol/mcs.pb.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace gcm {
+
+namespace {
+
+mcs_proto::HeartbeatConfig BuildHeartbeatConfig(int interval_ms) {
+ mcs_proto::HeartbeatConfig config;
+ config.set_interval_ms(interval_ms);
+ return config;
+}
+
+class TestHeartbeatManager : public HeartbeatManager {
+ public:
+ TestHeartbeatManager() {}
+ virtual ~TestHeartbeatManager() {}
+
+ // Bypass the heartbeat timer, and send the heartbeat now.
+ void TriggerHearbeat();
+};
+
+void TestHeartbeatManager::TriggerHearbeat() {
+ OnHeartbeatTriggered();
+}
+
+class HeartbeatManagerTest : public testing::Test {
+ public:
+ HeartbeatManagerTest();
+ virtual ~HeartbeatManagerTest() {}
+
+ TestHeartbeatManager* manager() const { return manager_.get(); }
+ int heartbeats_sent() const { return heartbeats_sent_; }
+ int reconnects_triggered() const { return reconnects_triggered_; }
+
+ // Starts the heartbeat manager.
+ void StartManager();
+
+ private:
+ // Helper functions for verifying heartbeat manager effects.
+ void SendHeartbeatClosure();
+ void TriggerReconnectClosure();
+
+ scoped_ptr<TestHeartbeatManager> manager_;
+
+ int heartbeats_sent_;
+ int reconnects_triggered_;
+
+ base::MessageLoop message_loop_;
+};
+
+HeartbeatManagerTest::HeartbeatManagerTest()
+ : manager_(new TestHeartbeatManager()),
+ heartbeats_sent_(0),
+ reconnects_triggered_(0) {
+}
+
+void HeartbeatManagerTest::StartManager() {
+ manager_->Start(base::Bind(&HeartbeatManagerTest::SendHeartbeatClosure,
+ base::Unretained(this)),
+ base::Bind(&HeartbeatManagerTest::TriggerReconnectClosure,
+ base::Unretained(this)));
+}
+
+void HeartbeatManagerTest::SendHeartbeatClosure() {
+ heartbeats_sent_++;
+}
+
+void HeartbeatManagerTest::TriggerReconnectClosure() {
+ reconnects_triggered_++;
+}
+
+// Basic initialization. No heartbeat should be pending.
+TEST_F(HeartbeatManagerTest, Init) {
+ EXPECT_TRUE(manager()->GetNextHeartbeatTime().is_null());
+}
+
+// Acknowledging a heartbeat before starting the manager should have no effect.
+TEST_F(HeartbeatManagerTest, AckBeforeStart) {
+ manager()->OnHeartbeatAcked();
+ EXPECT_TRUE(manager()->GetNextHeartbeatTime().is_null());
+}
+
+// Starting the manager should start the heartbeat timer.
+TEST_F(HeartbeatManagerTest, Start) {
+ StartManager();
+ EXPECT_GT(manager()->GetNextHeartbeatTime(), base::TimeTicks::Now());
+ EXPECT_EQ(0, heartbeats_sent());
+ EXPECT_EQ(0, reconnects_triggered());
+}
+
+// Acking the heartbeat should trigger a new heartbeat timer.
+TEST_F(HeartbeatManagerTest, AckedHeartbeat) {
+ StartManager();
+ manager()->TriggerHearbeat();
+ base::TimeTicks heartbeat = manager()->GetNextHeartbeatTime();
+ EXPECT_GT(heartbeat, base::TimeTicks::Now());
+ EXPECT_EQ(1, heartbeats_sent());
+ EXPECT_EQ(0, reconnects_triggered());
+
+ manager()->OnHeartbeatAcked();
+ EXPECT_LT(heartbeat, manager()->GetNextHeartbeatTime());
+ EXPECT_EQ(1, heartbeats_sent());
+ EXPECT_EQ(0, reconnects_triggered());
+
+ manager()->TriggerHearbeat();
+ EXPECT_EQ(2, heartbeats_sent());
+ EXPECT_EQ(0, reconnects_triggered());
+}
+
+// Trigger a heartbeat when one was outstanding should reset the connection.
+TEST_F(HeartbeatManagerTest, UnackedHeartbeat) {
+ StartManager();
+ manager()->TriggerHearbeat();
+ EXPECT_EQ(1, heartbeats_sent());
+ EXPECT_EQ(0, reconnects_triggered());
+
+ manager()->TriggerHearbeat();
+ EXPECT_EQ(1, heartbeats_sent());
+ EXPECT_EQ(1, reconnects_triggered());
+}
+
+// Updating the heartbeat interval before starting should result in the new
+// interval being used at Start time.
+TEST_F(HeartbeatManagerTest, UpdateIntervalThenStart) {
+ const int kIntervalMs = 60 * 1000; // 60 seconds.
+ manager()->UpdateHeartbeatConfig(BuildHeartbeatConfig(kIntervalMs));
+ EXPECT_TRUE(manager()->GetNextHeartbeatTime().is_null());
+ StartManager();
+ EXPECT_LE(manager()->GetNextHeartbeatTime() - base::TimeTicks::Now(),
+ base::TimeDelta::FromMilliseconds(kIntervalMs));
+}
+
+// Updating the heartbeat interval after starting should only use the new
+// interval on the next heartbeat.
+TEST_F(HeartbeatManagerTest, StartThenUpdateInterval) {
+ const int kIntervalMs = 60 * 1000; // 60 seconds.
+ StartManager();
+ base::TimeTicks heartbeat = manager()->GetNextHeartbeatTime();
+ EXPECT_GT(heartbeat - base::TimeTicks::Now(),
+ base::TimeDelta::FromMilliseconds(kIntervalMs));
+
+ // Updating the interval should not affect an outstanding heartbeat.
+ manager()->UpdateHeartbeatConfig(BuildHeartbeatConfig(kIntervalMs));
+ EXPECT_EQ(heartbeat, manager()->GetNextHeartbeatTime());
+
+ // Triggering and acking the heartbeat should result in a heartbeat being
+ // posted with the new interval.
+ manager()->TriggerHearbeat();
+ manager()->OnHeartbeatAcked();
+
+ EXPECT_LE(manager()->GetNextHeartbeatTime() - base::TimeTicks::Now(),
+ base::TimeDelta::FromMilliseconds(kIntervalMs));
+ EXPECT_NE(heartbeat, manager()->GetNextHeartbeatTime());
+}
+
+// Stopping the manager should reset the heartbeat timer.
+TEST_F(HeartbeatManagerTest, Stop) {
+ StartManager();
+ EXPECT_GT(manager()->GetNextHeartbeatTime(), base::TimeTicks::Now());
+
+ manager()->Stop();
+ EXPECT_TRUE(manager()->GetNextHeartbeatTime().is_null());
+}
+
+} // namespace
+
+} // namespace gcm
diff --git a/chromium/google_apis/gcm/engine/mcs_client.cc b/chromium/google_apis/gcm/engine/mcs_client.cc
index f0af051379f..a99a333a37e 100644
--- a/chromium/google_apis/gcm/engine/mcs_client.cc
+++ b/chromium/google_apis/gcm/engine/mcs_client.cc
@@ -4,13 +4,18 @@
#include "google_apis/gcm/engine/mcs_client.h"
+#include <set>
+
#include "base/basictypes.h"
#include "base/message_loop/message_loop.h"
+#include "base/metrics/histogram.h"
#include "base/strings/string_number_conversions.h"
+#include "base/time/clock.h"
+#include "base/time/time.h"
#include "google_apis/gcm/base/mcs_util.h"
#include "google_apis/gcm/base/socket_stream.h"
#include "google_apis/gcm/engine/connection_factory.h"
-#include "google_apis/gcm/engine/rmq_store.h"
+#include "google_apis/gcm/monitoring/gcm_stats_recorder.h"
using namespace google::protobuf::io;
@@ -20,9 +25,6 @@ namespace {
typedef scoped_ptr<google::protobuf::MessageLite> MCSProto;
-// TODO(zea): get these values from MCS settings.
-const int64 kHeartbeatDefaultSeconds = 60 * 15; // 15 minutes.
-
// The category of messages intended for the GCM client itself from MCS.
const char kMCSCategory[] = "com.google.android.gsf.gtalkservice";
@@ -30,8 +32,8 @@ const char kMCSCategory[] = "com.google.android.gsf.gtalkservice";
const char kGCMFromField[] = "gcm@android.com";
// MCS status message types.
+// TODO(zea): handle these at the GCMClient layer.
const char kIdleNotification[] = "IdleNotification";
-// TODO(zea): consume the following message types:
// const char kAlwaysShowOnIdle[] = "ShowAwayOnIdle";
// const char kPowerNotification[] = "PowerNotification";
// const char kDataActiveNotification[] = "DataActiveNotification";
@@ -64,6 +66,47 @@ bool BuildPersistentIdListFromProto(const google::protobuf::string& bytes,
} // namespace
+class CollapseKey {
+ public:
+ explicit CollapseKey(const mcs_proto::DataMessageStanza& message);
+ ~CollapseKey();
+
+ // Comparison operator for use in maps.
+ bool operator<(const CollapseKey& right) const;
+
+ // Whether the message had a valid collapse key.
+ bool IsValid() const;
+
+ std::string token() const { return token_; }
+ std::string app_id() const { return app_id_; }
+ int64 device_user_id() const { return device_user_id_; }
+
+ private:
+ const std::string token_;
+ const std::string app_id_;
+ const int64 device_user_id_;
+};
+
+CollapseKey::CollapseKey(const mcs_proto::DataMessageStanza& message)
+ : token_(message.token()),
+ app_id_(message.category()),
+ device_user_id_(message.device_user_id()) {}
+
+CollapseKey::~CollapseKey() {}
+
+bool CollapseKey::IsValid() const {
+ // Device user id is optional, but the application id and token are not.
+ return !token_.empty() && !app_id_.empty();
+}
+
+bool CollapseKey::operator<(const CollapseKey& right) const {
+ if (device_user_id_ != right.device_user_id())
+ return device_user_id_ < right.device_user_id();
+ if (app_id_ != right.app_id())
+ return app_id_ < right.app_id();
+ return token_ < right.token();
+}
+
struct ReliablePacketInfo {
ReliablePacketInfo();
~ReliablePacketInfo();
@@ -86,11 +129,38 @@ ReliablePacketInfo::ReliablePacketInfo()
}
ReliablePacketInfo::~ReliablePacketInfo() {}
-MCSClient::MCSClient(
- const base::FilePath& rmq_path,
- ConnectionFactory* connection_factory,
- scoped_refptr<base::SequencedTaskRunner> blocking_task_runner)
- : state_(UNINITIALIZED),
+int MCSClient::GetSendQueueSize() const {
+ return to_send_.size();
+}
+
+int MCSClient::GetResendQueueSize() const {
+ return to_resend_.size();
+}
+
+std::string MCSClient::GetStateString() const {
+ switch(state_) {
+ case UNINITIALIZED:
+ return "UNINITIALIZED";
+ case LOADED:
+ return "LOADED";
+ case CONNECTING:
+ return "CONNECTING";
+ case CONNECTED:
+ return "CONNECTED";
+ default:
+ NOTREACHED();
+ return std::string();
+ }
+}
+
+MCSClient::MCSClient(const std::string& version_string,
+ base::Clock* clock,
+ ConnectionFactory* connection_factory,
+ GCMStore* gcm_store,
+ GCMStatsRecorder* recorder)
+ : version_string_(version_string),
+ clock_(clock),
+ state_(UNINITIALIZED),
android_id_(0),
security_token_(0),
connection_factory_(connection_factory),
@@ -99,11 +169,8 @@ MCSClient::MCSClient(
last_server_to_device_stream_id_received_(0),
stream_id_out_(0),
stream_id_in_(0),
- rmq_store_(rmq_path, blocking_task_runner),
- heartbeat_interval_(
- base::TimeDelta::FromSeconds(kHeartbeatDefaultSeconds)),
- heartbeat_timer_(true, true),
- blocking_task_runner_(blocking_task_runner),
+ gcm_store_(gcm_store),
+ recorder_(recorder),
weak_ptr_factory_(this) {
}
@@ -111,18 +178,17 @@ MCSClient::~MCSClient() {
}
void MCSClient::Initialize(
- const InitializationCompleteCallback& initialization_callback,
+ const ErrorCallback& error_callback,
const OnMessageReceivedCallback& message_received_callback,
- const OnMessageSentCallback& message_sent_callback) {
+ const OnMessageSentCallback& message_sent_callback,
+ scoped_ptr<GCMStore::LoadResult> load_result) {
DCHECK_EQ(state_, UNINITIALIZED);
- initialization_callback_ = initialization_callback;
+
+ state_ = LOADED;
+ mcs_error_callback_ = error_callback;
message_received_callback_ = message_received_callback;
message_sent_callback_ = message_sent_callback;
- state_ = LOADING;
- rmq_store_.Load(base::Bind(&MCSClient::OnRMQLoadFinished,
- weak_ptr_factory_.GetWeakPtr()));
-
connection_factory_->Initialize(
base::Bind(&MCSClient::ResetStateAndBuildLoginRequest,
weak_ptr_factory_.GetWeakPtr()),
@@ -131,71 +197,171 @@ void MCSClient::Initialize(
base::Bind(&MCSClient::MaybeSendMessage,
weak_ptr_factory_.GetWeakPtr()));
connection_handler_ = connection_factory_->GetConnectionHandler();
+
+ stream_id_out_ = 1; // Login request is hardcoded to id 1.
+
+ android_id_ = load_result->device_android_id;
+ security_token_ = load_result->device_security_token;
+
+ if (android_id_ == 0) {
+ DVLOG(1) << "No device credentials found, assuming new client.";
+ // No need to try and load RMQ data in that case.
+ return;
+ }
+
+ // |android_id_| is non-zero, so should |security_token_|.
+ DCHECK_NE(0u, security_token_) << "Security token invalid, while android id"
+ << " is non-zero.";
+
+ DVLOG(1) << "RMQ Load finished with " << load_result->incoming_messages.size()
+ << " incoming acks pending and "
+ << load_result->outgoing_messages.size()
+ << " outgoing messages pending.";
+
+ restored_unackeds_server_ids_ = load_result->incoming_messages;
+
+ // First go through and order the outgoing messages by recency.
+ std::map<uint64, google::protobuf::MessageLite*> ordered_messages;
+ std::vector<PersistentId> expired_ttl_ids;
+ for (GCMStore::OutgoingMessageMap::iterator iter =
+ load_result->outgoing_messages.begin();
+ iter != load_result->outgoing_messages.end(); ++iter) {
+ uint64 timestamp = 0;
+ if (!base::StringToUint64(iter->first, &timestamp)) {
+ LOG(ERROR) << "Invalid restored message.";
+ // TODO(fgorski): Error: data unreadable
+ mcs_error_callback_.Run();
+ return;
+ }
+
+ // Check if the TTL has expired for this message.
+ if (HasTTLExpired(*iter->second, clock_)) {
+ expired_ttl_ids.push_back(iter->first);
+ NotifyMessageSendStatus(*iter->second, TTL_EXCEEDED);
+ continue;
+ }
+
+ ordered_messages[timestamp] = iter->second.release();
+ }
+
+ if (!expired_ttl_ids.empty()) {
+ gcm_store_->RemoveOutgoingMessages(
+ expired_ttl_ids,
+ base::Bind(&MCSClient::OnGCMUpdateFinished,
+ weak_ptr_factory_.GetWeakPtr()));
+ }
+
+ // Now go through and add the outgoing messages to the send queue in their
+ // appropriate order (oldest at front, most recent at back).
+ for (std::map<uint64, google::protobuf::MessageLite*>::iterator
+ iter = ordered_messages.begin();
+ iter != ordered_messages.end(); ++iter) {
+ ReliablePacketInfo* packet_info = new ReliablePacketInfo();
+ packet_info->protobuf.reset(iter->second);
+ packet_info->tag = GetMCSProtoTag(*iter->second);
+ packet_info->persistent_id = base::Uint64ToString(iter->first);
+ to_send_.push_back(make_linked_ptr(packet_info));
+
+ if (packet_info->tag == kDataMessageStanzaTag) {
+ mcs_proto::DataMessageStanza* data_message =
+ reinterpret_cast<mcs_proto::DataMessageStanza*>(
+ packet_info->protobuf.get());
+ CollapseKey collapse_key(*data_message);
+ if (collapse_key.IsValid())
+ collapse_key_map_[collapse_key] = packet_info;
+ }
+ }
}
void MCSClient::Login(uint64 android_id, uint64 security_token) {
DCHECK_EQ(state_, LOADED);
+ DCHECK(android_id_ == 0 || android_id_ == android_id);
+ DCHECK(security_token_ == 0 || security_token_ == security_token);
+
if (android_id != android_id_ && security_token != security_token_) {
DCHECK(android_id);
DCHECK(security_token);
- DCHECK(restored_unackeds_server_ids_.empty());
android_id_ = android_id;
security_token_ = security_token;
- rmq_store_.SetDeviceCredentials(android_id_,
- security_token_,
- base::Bind(&MCSClient::OnRMQUpdateFinished,
- weak_ptr_factory_.GetWeakPtr()));
}
+ DCHECK(android_id_ != 0 || restored_unackeds_server_ids_.empty());
+
state_ = CONNECTING;
connection_factory_->Connect();
}
-void MCSClient::SendMessage(const MCSMessage& message, bool use_rmq) {
- DCHECK_EQ(state_, CONNECTED);
+void MCSClient::SendMessage(const MCSMessage& message) {
+ int ttl = GetTTL(message.GetProtobuf());
+ DCHECK_GE(ttl, 0);
if (to_send_.size() > kMaxSendQueueSize) {
- base::MessageLoop::current()->PostTask(
- FROM_HERE,
- base::Bind(message_sent_callback_, "Message queue full."));
+ NotifyMessageSendStatus(message.GetProtobuf(), QUEUE_SIZE_LIMIT_REACHED);
return;
}
if (message.size() > kMaxMessageBytes) {
- base::MessageLoop::current()->PostTask(
- FROM_HERE,
- base::Bind(message_sent_callback_, "Message too large."));
+ NotifyMessageSendStatus(message.GetProtobuf(), MESSAGE_TOO_LARGE);
return;
}
- ReliablePacketInfo* packet_info = new ReliablePacketInfo();
+ scoped_ptr<ReliablePacketInfo> packet_info(new ReliablePacketInfo());
+ packet_info->tag = message.tag();
packet_info->protobuf = message.CloneProtobuf();
- if (use_rmq) {
- PersistentId persistent_id = GetNextPersistentId();
- DVLOG(1) << "Setting persistent id to " << persistent_id;
- packet_info->persistent_id = persistent_id;
- SetPersistentId(persistent_id,
- packet_info->protobuf.get());
- rmq_store_.AddOutgoingMessage(persistent_id,
- MCSMessage(message.tag(),
- *(packet_info->protobuf)),
- base::Bind(&MCSClient::OnRMQUpdateFinished,
- weak_ptr_factory_.GetWeakPtr()));
- } else {
- // Check that there is an active connection to the endpoint.
- if (!connection_handler_->CanSendMessage()) {
- base::MessageLoop::current()->PostTask(
- FROM_HERE,
- base::Bind(message_sent_callback_, "Unable to reach endpoint"));
+ if (ttl > 0) {
+ DCHECK_EQ(message.tag(), kDataMessageStanzaTag);
+
+ // First check if this message should replace a pending message with the
+ // same collapse key.
+ mcs_proto::DataMessageStanza* data_message =
+ reinterpret_cast<mcs_proto::DataMessageStanza*>(
+ packet_info->protobuf.get());
+ CollapseKey collapse_key(*data_message);
+ if (collapse_key.IsValid() && collapse_key_map_.count(collapse_key) > 0) {
+ ReliablePacketInfo* original_packet = collapse_key_map_[collapse_key];
+ DVLOG(1) << "Found matching collapse key, Reusing persistent id of "
+ << original_packet->persistent_id;
+ original_packet->protobuf = packet_info->protobuf.Pass();
+ SetPersistentId(original_packet->persistent_id,
+ original_packet->protobuf.get());
+ gcm_store_->OverwriteOutgoingMessage(
+ original_packet->persistent_id,
+ message,
+ base::Bind(&MCSClient::OnGCMUpdateFinished,
+ weak_ptr_factory_.GetWeakPtr()));
+
+ // The message is already queued, return.
return;
+ } else {
+ PersistentId persistent_id = GetNextPersistentId();
+ DVLOG(1) << "Setting persistent id to " << persistent_id;
+ packet_info->persistent_id = persistent_id;
+ SetPersistentId(persistent_id, packet_info->protobuf.get());
+ if (!gcm_store_->AddOutgoingMessage(
+ persistent_id,
+ MCSMessage(message.tag(), *(packet_info->protobuf)),
+ base::Bind(&MCSClient::OnGCMUpdateFinished,
+ weak_ptr_factory_.GetWeakPtr()))) {
+ NotifyMessageSendStatus(message.GetProtobuf(),
+ APP_QUEUE_SIZE_LIMIT_REACHED);
+ return;
+ }
}
+
+ if (collapse_key.IsValid())
+ collapse_key_map_[collapse_key] = packet_info.get();
+ } else if (!connection_factory_->IsEndpointReachable()) {
+ DVLOG(1) << "No active connection, dropping message.";
+ NotifyMessageSendStatus(message.GetProtobuf(), NO_CONNECTION_ON_ZERO_TTL);
+ return;
}
- to_send_.push_back(make_linked_ptr(packet_info));
- MaybeSendMessage();
-}
-void MCSClient::Destroy() {
- rmq_store_.Destroy(base::Bind(&MCSClient::OnRMQUpdateFinished,
- weak_ptr_factory_.GetWeakPtr()));
+ to_send_.push_back(make_linked_ptr(packet_info.release()));
+
+ // Notify that the messages has been succsfully queued for sending.
+ // TODO(jianli): We should report QUEUED after writing to GCM store succeeds.
+ NotifyMessageSendStatus(message.GetProtobuf(), QUEUED);
+
+ MaybeSendMessage();
}
void MCSClient::ResetStateAndBuildLoginRequest(
@@ -207,7 +373,7 @@ void MCSClient::ResetStateAndBuildLoginRequest(
last_device_to_server_stream_id_received_ = 0;
last_server_to_device_stream_id_received_ = 0;
- // TODO(zea): expire all messages older than their TTL.
+ heartbeat_manager_.Stop();
// Add any pending acknowledgments to the list of ids.
for (StreamIdToPersistentIdMap::const_iterator iter =
@@ -229,7 +395,9 @@ void MCSClient::ResetStateAndBuildLoginRequest(
acked_server_ids_.clear();
// Then build the request, consuming all pending acknowledgments.
- request->Swap(BuildLoginRequest(android_id_, security_token_).get());
+ request->Swap(BuildLoginRequest(android_id_,
+ security_token_,
+ version_string_).get());
for (PersistentIdList::const_iterator iter =
restored_unackeds_server_ids_.begin();
iter != restored_unackeds_server_ids_.end(); ++iter) {
@@ -245,74 +413,49 @@ void MCSClient::ResetStateAndBuildLoginRequest(
to_send_.push_front(to_resend_.back());
to_resend_.pop_back();
}
- DVLOG(1) << "Resetting state, with " << request->received_persistent_id_size()
- << " incoming acks pending, and " << to_send_.size()
- << " pending outgoing messages.";
-
- heartbeat_timer_.Stop();
-
- state_ = CONNECTING;
-}
-void MCSClient::SendHeartbeat() {
- SendMessage(MCSMessage(kHeartbeatPingTag, mcs_proto::HeartbeatPing()),
- false);
-}
-
-void MCSClient::OnRMQLoadFinished(const RMQStore::LoadResult& result) {
- if (!result.success) {
- state_ = UNINITIALIZED;
- LOG(ERROR) << "Failed to load/create RMQ state. Not connecting.";
- initialization_callback_.Run(false, 0, 0);
- return;
+ // Drop all TTL == 0 or expired TTL messages from the queue.
+ std::deque<MCSPacketInternal> new_to_send;
+ std::vector<PersistentId> expired_ttl_ids;
+ while (!to_send_.empty()) {
+ MCSPacketInternal packet = PopMessageForSend();
+ if (GetTTL(*packet->protobuf) > 0 &&
+ !HasTTLExpired(*packet->protobuf, clock_)) {
+ new_to_send.push_back(packet);
+ } else {
+ // If the TTL was 0 there is no persistent id, so no need to remove the
+ // message from the persistent store.
+ if (!packet->persistent_id.empty())
+ expired_ttl_ids.push_back(packet->persistent_id);
+ NotifyMessageSendStatus(*packet->protobuf, TTL_EXCEEDED);
+ }
}
- state_ = LOADED;
- stream_id_out_ = 1; // Login request is hardcoded to id 1.
- if (result.device_android_id == 0 || result.device_security_token == 0) {
- DVLOG(1) << "No device credentials found, assuming new client.";
- initialization_callback_.Run(true, 0, 0);
- return;
+ if (!expired_ttl_ids.empty()) {
+ DVLOG(1) << "Connection reset, " << expired_ttl_ids.size()
+ << " messages expired.";
+ gcm_store_->RemoveOutgoingMessages(
+ expired_ttl_ids,
+ base::Bind(&MCSClient::OnGCMUpdateFinished,
+ weak_ptr_factory_.GetWeakPtr()));
}
- android_id_ = result.device_android_id;
- security_token_ = result.device_security_token;
+ to_send_.swap(new_to_send);
- DVLOG(1) << "RMQ Load finished with " << result.incoming_messages.size()
- << " incoming acks pending and " << result.outgoing_messages.size()
- << " outgoing messages pending.";
-
- restored_unackeds_server_ids_ = result.incoming_messages;
-
- // First go through and order the outgoing messages by recency.
- std::map<uint64, google::protobuf::MessageLite*> ordered_messages;
- for (std::map<PersistentId, google::protobuf::MessageLite*>::const_iterator
- iter = result.outgoing_messages.begin();
- iter != result.outgoing_messages.end(); ++iter) {
- uint64 timestamp = 0;
- if (!base::StringToUint64(iter->first, &timestamp)) {
- LOG(ERROR) << "Invalid restored message.";
- return;
- }
- ordered_messages[timestamp] = iter->second;
- }
+ DVLOG(1) << "Resetting state, with " << request->received_persistent_id_size()
+ << " incoming acks pending, and " << to_send_.size()
+ << " pending outgoing messages.";
- // Now go through and add the outgoing messages to the send queue in their
- // appropriate order (oldest at front, most recent at back).
- for (std::map<uint64, google::protobuf::MessageLite*>::const_iterator
- iter = ordered_messages.begin();
- iter != ordered_messages.end(); ++iter) {
- ReliablePacketInfo* packet_info = new ReliablePacketInfo();
- packet_info->protobuf.reset(iter->second);
- packet_info->persistent_id = base::Uint64ToString(iter->first);
- to_send_.push_back(make_linked_ptr(packet_info));
- }
+ state_ = CONNECTING;
+}
- initialization_callback_.Run(true, android_id_, security_token_);
+void MCSClient::SendHeartbeat() {
+ SendMessage(MCSMessage(kHeartbeatPingTag, mcs_proto::HeartbeatPing()));
}
-void MCSClient::OnRMQUpdateFinished(bool success) {
- LOG_IF(ERROR, !success) << "RMQ Update failed!";
+void MCSClient::OnGCMUpdateFinished(bool success) {
+ LOG_IF(ERROR, !success) << "GCM Update failed!";
+ UMA_HISTOGRAM_BOOLEAN("GCM.StoreUpdateSucceeded", success);
// TODO(zea): Rebuild the store from scratch in case of persistence failure?
}
@@ -320,25 +463,56 @@ void MCSClient::MaybeSendMessage() {
if (to_send_.empty())
return;
- if (!connection_handler_->CanSendMessage())
+ // If the connection has been reset, do nothing. On reconnection
+ // MaybeSendMessage will be automatically invoked again.
+ // TODO(zea): consider doing TTL expiration at connection reset time, rather
+ // than reconnect time.
+ if (!connection_factory_->IsEndpointReachable())
return;
- // TODO(zea): drop messages older than their TTL.
-
+ MCSPacketInternal packet = PopMessageForSend();
+ if (HasTTLExpired(*packet->protobuf, clock_)) {
+ DCHECK(!packet->persistent_id.empty());
+ DVLOG(1) << "Dropping expired message " << packet->persistent_id << ".";
+ NotifyMessageSendStatus(*packet->protobuf, TTL_EXCEEDED);
+ gcm_store_->RemoveOutgoingMessage(
+ packet->persistent_id,
+ base::Bind(&MCSClient::OnGCMUpdateFinished,
+ weak_ptr_factory_.GetWeakPtr()));
+ base::MessageLoop::current()->PostTask(
+ FROM_HERE,
+ base::Bind(&MCSClient::MaybeSendMessage,
+ weak_ptr_factory_.GetWeakPtr()));
+ return;
+ }
DVLOG(1) << "Pending output message found, sending.";
- MCSPacketInternal packet = to_send_.front();
- to_send_.pop_front();
if (!packet->persistent_id.empty())
to_resend_.push_back(packet);
SendPacketToWire(packet.get());
}
void MCSClient::SendPacketToWire(ReliablePacketInfo* packet_info) {
- // Reset the heartbeat interval.
- heartbeat_timer_.Reset();
packet_info->stream_id = ++stream_id_out_;
DVLOG(1) << "Sending packet of type " << packet_info->protobuf->GetTypeName();
+ // Set the queued time as necessary.
+ if (packet_info->tag == kDataMessageStanzaTag) {
+ mcs_proto::DataMessageStanza* data_message =
+ reinterpret_cast<mcs_proto::DataMessageStanza*>(
+ packet_info->protobuf.get());
+ uint64 sent = data_message->sent();
+ DCHECK_GT(sent, 0U);
+ int queued = (clock_->Now().ToInternalValue() /
+ base::Time::kMicrosecondsPerSecond) - sent;
+ DVLOG(1) << "Message was queued for " << queued << " seconds.";
+ data_message->set_queued(queued);
+ recorder_->RecordDataSentToWire(
+ data_message->category(),
+ data_message->to(),
+ data_message->id(),
+ queued);
+ }
+
// Set the proper last received stream id to acknowledge received server
// packets.
DVLOG(1) << "Setting last stream id received to "
@@ -373,6 +547,9 @@ void MCSClient::HandleMCSDataMesssage(
scoped_ptr<mcs_proto::DataMessageStanza> response(
new mcs_proto::DataMessageStanza());
response->set_from(kGCMFromField);
+ response->set_sent(clock_->Now().ToInternalValue() /
+ base::Time::kMicrosecondsPerSecond);
+ response->set_ttl(0);
bool send = false;
for (int i = 0; i < data_message->app_data_size(); ++i) {
const mcs_proto::AppData& app_data = data_message->app_data(i);
@@ -390,8 +567,7 @@ void MCSClient::HandleMCSDataMesssage(
if (send) {
SendMessage(
MCSMessage(kDataMessageStanzaTag,
- response.PassAs<const google::protobuf::MessageLite>()),
- false);
+ response.PassAs<const google::protobuf::MessageLite>()));
}
}
@@ -430,9 +606,9 @@ void MCSClient::HandlePacketFromWire(
++stream_id_in_;
if (!persistent_id.empty()) {
unacked_server_ids_[stream_id_in_] = persistent_id;
- rmq_store_.AddIncomingMessage(persistent_id,
- base::Bind(&MCSClient::OnRMQUpdateFinished,
- weak_ptr_factory_.GetWeakPtr()));
+ gcm_store_->AddIncomingMessage(persistent_id,
+ base::Bind(&MCSClient::OnGCMUpdateFinished,
+ weak_ptr_factory_.GetWeakPtr()));
}
DVLOG(1) << "Received message of type " << protobuf->GetTypeName()
@@ -445,25 +621,36 @@ void MCSClient::HandlePacketFromWire(
unacked_server_ids_.size() % kUnackedMessageBeforeStreamAck == 0) {
SendMessage(MCSMessage(kIqStanzaTag,
BuildStreamAck().
- PassAs<const google::protobuf::MessageLite>()),
- false);
+ PassAs<const google::protobuf::MessageLite>()));
}
+ // The connection is alive, treat this message as a heartbeat ack.
+ heartbeat_manager_.OnHeartbeatAcked();
+
switch (tag) {
case kLoginResponseTag: {
+ DCHECK_EQ(CONNECTING, state_);
mcs_proto::LoginResponse* login_response =
reinterpret_cast<mcs_proto::LoginResponse*>(protobuf.get());
DVLOG(1) << "Received login response:";
DVLOG(1) << " Id: " << login_response->id();
DVLOG(1) << " Timestamp: " << login_response->server_timestamp();
- if (login_response->has_error()) {
+ if (login_response->has_error() && login_response->error().code() != 0) {
state_ = UNINITIALIZED;
DVLOG(1) << " Error code: " << login_response->error().code();
DVLOG(1) << " Error message: " << login_response->error().message();
- initialization_callback_.Run(false, 0, 0);
+ LOG(ERROR) << "Failed to log in to GCM, resetting connection.";
+ connection_factory_->SignalConnectionReset(
+ ConnectionFactory::LOGIN_FAILURE);
+ mcs_error_callback_.Run();
return;
}
+ if (login_response->has_heartbeat_config()) {
+ heartbeat_manager_.UpdateHeartbeatConfig(
+ login_response->heartbeat_config());
+ }
+
state_ = CONNECTED;
stream_id_in_ = 1; // To account for the login response.
DCHECK_EQ(1U, stream_id_out_);
@@ -484,29 +671,29 @@ void MCSClient::HandlePacketFromWire(
weak_ptr_factory_.GetWeakPtr()));
}
- heartbeat_timer_.Start(FROM_HERE,
- heartbeat_interval_,
- base::Bind(&MCSClient::SendHeartbeat,
- weak_ptr_factory_.GetWeakPtr()));
+ heartbeat_manager_.Start(
+ base::Bind(&MCSClient::SendHeartbeat,
+ weak_ptr_factory_.GetWeakPtr()),
+ base::Bind(&MCSClient::OnConnectionResetByHeartbeat,
+ weak_ptr_factory_.GetWeakPtr()));
return;
}
case kHeartbeatPingTag:
DCHECK_GE(stream_id_in_, 1U);
DVLOG(1) << "Received heartbeat ping, sending ack.";
SendMessage(
- MCSMessage(kHeartbeatAckTag, mcs_proto::HeartbeatAck()), false);
+ MCSMessage(kHeartbeatAckTag, mcs_proto::HeartbeatAck()));
return;
case kHeartbeatAckTag:
DCHECK_GE(stream_id_in_, 1U);
DVLOG(1) << "Received heartbeat ack.";
- // TODO(zea): add logic to reconnect if no ack received within a certain
- // timeout (with backoff).
+ // Do nothing else, all messages act as heartbeat acks.
return;
case kCloseTag:
- LOG(ERROR) << "Received close command, closing connection.";
- state_ = UNINITIALIZED;
- initialization_callback_.Run(false, 0, 0);
- // TODO(zea): should this happen in non-error cases? Reconnect?
+ LOG(ERROR) << "Received close command, resetting connection.";
+ state_ = LOADED;
+ connection_factory_->SignalConnectionReset(
+ ConnectionFactory::CLOSE_COMMAND);
return;
case kIqStanzaTag: {
DCHECK_GE(stream_id_in_, 1U);
@@ -565,58 +752,85 @@ void MCSClient::HandleStreamAck(StreamId last_stream_id_received) {
const MCSPacketInternal& outgoing_packet = to_resend_.front();
acked_outgoing_persistent_ids.push_back(outgoing_packet->persistent_id);
acked_outgoing_stream_ids.push_back(outgoing_packet->stream_id);
+ NotifyMessageSendStatus(*outgoing_packet->protobuf, SENT);
to_resend_.pop_front();
}
DVLOG(1) << "Server acked " << acked_outgoing_persistent_ids.size()
<< " outgoing messages, " << to_resend_.size()
<< " remaining unacked";
- rmq_store_.RemoveOutgoingMessages(acked_outgoing_persistent_ids,
- base::Bind(&MCSClient::OnRMQUpdateFinished,
- weak_ptr_factory_.GetWeakPtr()));
+ gcm_store_->RemoveOutgoingMessages(
+ acked_outgoing_persistent_ids,
+ base::Bind(&MCSClient::OnGCMUpdateFinished,
+ weak_ptr_factory_.GetWeakPtr()));
HandleServerConfirmedReceipt(last_stream_id_received);
}
void MCSClient::HandleSelectiveAck(const PersistentIdList& id_list) {
- // First check the to_resend_ queue. Acknowledgments should always happen
- // in the order they were sent, so if messages are present they should match
- // the acknowledge list.
- PersistentIdList::const_iterator iter = id_list.begin();
- for (; iter != id_list.end() && !to_resend_.empty(); ++iter) {
+ std::set<PersistentId> remaining_ids(id_list.begin(), id_list.end());
+
+ StreamId last_stream_id_received = -1;
+
+ // First check the to_resend_ queue. Acknowledgments are always contiguous,
+ // so if there's a pending message that hasn't been acked, all newer messages
+ // must also be unacked.
+ while(!to_resend_.empty() && !remaining_ids.empty()) {
const MCSPacketInternal& outgoing_packet = to_resend_.front();
- DCHECK_EQ(outgoing_packet->persistent_id, *iter);
+ if (remaining_ids.count(outgoing_packet->persistent_id) == 0)
+ break; // Newer message must be unacked too.
+ remaining_ids.erase(outgoing_packet->persistent_id);
+ NotifyMessageSendStatus(*outgoing_packet->protobuf, SENT);
// No need to re-acknowledge any server messages this message already
// acknowledged.
StreamId device_stream_id = outgoing_packet->stream_id;
- HandleServerConfirmedReceipt(device_stream_id);
-
+ if (device_stream_id > last_stream_id_received)
+ last_stream_id_received = device_stream_id;
to_resend_.pop_front();
}
// If the acknowledged ids aren't all there, they might be in the to_send_
- // queue (typically when a StreamAck confirms messages as part of a login
+ // queue (typically when a SelectiveAck confirms messages as part of a login
// response).
- for (; iter != id_list.end() && !to_send_.empty(); ++iter) {
+ while (!to_send_.empty() && !remaining_ids.empty()) {
const MCSPacketInternal& outgoing_packet = to_send_.front();
- DCHECK_EQ(outgoing_packet->persistent_id, *iter);
+ if (remaining_ids.count(outgoing_packet->persistent_id) == 0)
+ break; // Newer messages must be unacked too.
+ remaining_ids.erase(outgoing_packet->persistent_id);
+ NotifyMessageSendStatus(*outgoing_packet->protobuf, SENT);
// No need to re-acknowledge any server messages this message already
// acknowledged.
StreamId device_stream_id = outgoing_packet->stream_id;
- HandleServerConfirmedReceipt(device_stream_id);
-
- to_send_.pop_front();
+ if (device_stream_id > last_stream_id_received)
+ last_stream_id_received = device_stream_id;
+ PopMessageForSend();
}
- DCHECK(iter == id_list.end());
+ // Only handle the largest stream id value. All other stream ids are
+ // implicitly handled.
+ if (last_stream_id_received > 0)
+ HandleServerConfirmedReceipt(last_stream_id_received);
+
+ // At this point, all remaining acked ids are redundant.
+ PersistentIdList acked_ids;
+ if (remaining_ids.size() > 0) {
+ for (size_t i = 0; i < id_list.size(); ++i) {
+ if (remaining_ids.count(id_list[i]) > 0)
+ continue;
+ acked_ids.push_back(id_list[i]);
+ }
+ } else {
+ acked_ids = id_list;
+ }
- DVLOG(1) << "Server acked " << id_list.size()
+ DVLOG(1) << "Server acked " << acked_ids.size()
<< " messages, " << to_resend_.size() << " remaining unacked.";
- rmq_store_.RemoveOutgoingMessages(id_list,
- base::Bind(&MCSClient::OnRMQUpdateFinished,
- weak_ptr_factory_.GetWeakPtr()));
+ gcm_store_->RemoveOutgoingMessages(
+ acked_ids,
+ base::Bind(&MCSClient::OnGCMUpdateFinished,
+ weak_ptr_factory_.GetWeakPtr()));
// Resend any remaining outgoing messages, as they were not received by the
// server.
@@ -628,12 +842,6 @@ void MCSClient::HandleSelectiveAck(const PersistentIdList& id_list) {
}
void MCSClient::HandleServerConfirmedReceipt(StreamId device_stream_id) {
- // TODO(zea): use a message id the sender understands.
- base::MessageLoop::current()->PostTask(
- FROM_HERE,
- base::Bind(message_sent_callback_,
- "Message " + base::UintToString(device_stream_id) + " sent."));
-
PersistentIdList acked_incoming_ids;
for (std::map<StreamId, PersistentIdList>::iterator iter =
acked_server_ids_.begin();
@@ -647,13 +855,56 @@ void MCSClient::HandleServerConfirmedReceipt(StreamId device_stream_id) {
DVLOG(1) << "Server confirmed receipt of " << acked_incoming_ids.size()
<< " acknowledged server messages.";
- rmq_store_.RemoveIncomingMessages(acked_incoming_ids,
- base::Bind(&MCSClient::OnRMQUpdateFinished,
- weak_ptr_factory_.GetWeakPtr()));
+ gcm_store_->RemoveIncomingMessages(
+ acked_incoming_ids,
+ base::Bind(&MCSClient::OnGCMUpdateFinished,
+ weak_ptr_factory_.GetWeakPtr()));
}
MCSClient::PersistentId MCSClient::GetNextPersistentId() {
return base::Uint64ToString(base::TimeTicks::Now().ToInternalValue());
}
+void MCSClient::OnConnectionResetByHeartbeat() {
+ connection_factory_->SignalConnectionReset(
+ ConnectionFactory::HEARTBEAT_FAILURE);
+}
+
+void MCSClient::NotifyMessageSendStatus(
+ const google::protobuf::MessageLite& protobuf,
+ MessageSendStatus status) {
+ if (GetMCSProtoTag(protobuf) != kDataMessageStanzaTag)
+ return;
+
+ const mcs_proto::DataMessageStanza* data_message_stanza =
+ reinterpret_cast<const mcs_proto::DataMessageStanza*>(&protobuf);
+ recorder_->RecordNotifySendStatus(
+ data_message_stanza->category(),
+ data_message_stanza->to(),
+ data_message_stanza->id(),
+ status,
+ protobuf.ByteSize(),
+ data_message_stanza->ttl());
+ message_sent_callback_.Run(
+ data_message_stanza->device_user_id(),
+ data_message_stanza->category(),
+ data_message_stanza->id(),
+ status);
+}
+
+MCSClient::MCSPacketInternal MCSClient::PopMessageForSend() {
+ MCSPacketInternal packet = to_send_.front();
+ to_send_.pop_front();
+
+ if (packet->tag == kDataMessageStanzaTag) {
+ mcs_proto::DataMessageStanza* data_message =
+ reinterpret_cast<mcs_proto::DataMessageStanza*>(packet->protobuf.get());
+ CollapseKey collapse_key(*data_message);
+ if (collapse_key.IsValid())
+ collapse_key_map_.erase(collapse_key);
+ }
+
+ return packet;
+}
+
} // namespace gcm
diff --git a/chromium/google_apis/gcm/engine/mcs_client.h b/chromium/google_apis/gcm/engine/mcs_client.h
index 4de62cb127e..cc915e49087 100644
--- a/chromium/google_apis/gcm/engine/mcs_client.h
+++ b/chromium/google_apis/gcm/engine/mcs_client.h
@@ -13,11 +13,15 @@
#include "base/files/file_path.h"
#include "base/memory/linked_ptr.h"
#include "base/memory/weak_ptr.h"
-#include "base/timer/timer.h"
#include "google_apis/gcm/base/gcm_export.h"
#include "google_apis/gcm/base/mcs_message.h"
#include "google_apis/gcm/engine/connection_handler.h"
-#include "google_apis/gcm/engine/rmq_store.h"
+#include "google_apis/gcm/engine/gcm_store.h"
+#include "google_apis/gcm/engine/heartbeat_manager.h"
+
+namespace base {
+class Clock;
+} // namespace base
namespace google {
namespace protobuf {
@@ -31,7 +35,9 @@ class LoginRequest;
namespace gcm {
+class CollapseKey;
class ConnectionFactory;
+class GCMStatsRecorder;
struct ReliablePacketInfo;
// An MCS client. This client is in charge of all communications with an
@@ -40,48 +46,74 @@ struct ReliablePacketInfo;
// network requests are performed on.
class GCM_EXPORT MCSClient {
public:
+ // Any change made to this enum should have corresponding change in the
+ // GetStateString(...) function.
enum State {
- UNINITIALIZED, // Uninitialized.
- LOADING, // Waiting for RMQ load to finish.
- LOADED, // RMQ Load finished, waiting to connect.
- CONNECTING, // Connection in progress.
- CONNECTED, // Connected and running.
+ UNINITIALIZED, // Uninitialized.
+ LOADED, // GCM Load finished, waiting to connect.
+ CONNECTING, // Connection in progress.
+ CONNECTED, // Connected and running.
};
- // Callback for informing MCSClient status. It is valid for this to be
- // invoked more than once if a permanent error is encountered after a
- // successful login was initiated.
- typedef base::Callback<
- void(bool success,
- uint64 restored_android_id,
- uint64 restored_security_token)> InitializationCompleteCallback;
+ // Any change made to this enum should have corresponding change in the
+ // GetMessageSendStatusString(...) function in mcs_client.cc.
+ enum MessageSendStatus {
+ // Message was queued succcessfully.
+ QUEUED,
+ // Message was sent to the server and the ACK was received.
+ SENT,
+ // Message not saved, because total queue size limit reached.
+ QUEUE_SIZE_LIMIT_REACHED,
+ // Message not saved, because app queue size limit reached.
+ APP_QUEUE_SIZE_LIMIT_REACHED,
+ // Message too large to send.
+ MESSAGE_TOO_LARGE,
+ // Message not send becuase of TTL = 0 and no working connection.
+ NO_CONNECTION_ON_ZERO_TTL,
+ // Message exceeded TTL.
+ TTL_EXCEEDED,
+
+ // NOTE: always keep this entry at the end. Add new status types only
+ // immediately above this line. Make sure to update the corresponding
+ // histogram enum accordingly.
+ SEND_STATUS_COUNT
+ };
+
+ // Callback for MCSClient's error conditions.
+ // TODO(fgorski): Keeping it as a callback with intention to add meaningful
+ // error information.
+ typedef base::Callback<void()> ErrorCallback;
// Callback when a message is received.
typedef base::Callback<void(const MCSMessage& message)>
OnMessageReceivedCallback;
// Callback when a message is sent (and receipt has been acknowledged by
// the MCS endpoint).
- // TODO(zea): pass some sort of structure containing more details about
- // send failures.
- typedef base::Callback<void(const std::string& message_id)>
- OnMessageSentCallback;
+ typedef base::Callback<
+ void(int64 user_serial_number,
+ const std::string& app_id,
+ const std::string& message_id,
+ MessageSendStatus status)> OnMessageSentCallback;
- MCSClient(const base::FilePath& rmq_path,
+ MCSClient(const std::string& version_string,
+ base::Clock* clock,
ConnectionFactory* connection_factory,
- scoped_refptr<base::SequencedTaskRunner> blocking_task_runner);
+ GCMStore* gcm_store,
+ GCMStatsRecorder* recorder);
virtual ~MCSClient();
// Initialize the client. Will load any previous id/token information as well
- // as unacknowledged message information from the RMQ storage, if it exists,
+ // as unacknowledged message information from the GCM storage, if it exists,
// passing the id/token information back via |initialization_callback| along
- // with a |success == true| result. If no RMQ information is present (and
- // this is therefore a fresh client), a clean RMQ store will be created and
+ // with a |success == true| result. If no GCM information is present (and
+ // this is therefore a fresh client), a clean GCM store will be created and
// values of 0 will be returned via |initialization_callback| with
// |success == true|.
- /// If an error loading the RMQ store is encountered,
+ /// If an error loading the GCM store is encountered,
// |initialization_callback| will be invoked with |success == false|.
- void Initialize(const InitializationCompleteCallback& initialization_callback,
+ void Initialize(const ErrorCallback& initialization_callback,
const OnMessageReceivedCallback& message_received_callback,
- const OnMessageSentCallback& message_sent_callback);
+ const OnMessageSentCallback& message_sent_callback,
+ scoped_ptr<GCMStore::LoadResult> load_result);
// Logs the client into the server. Client must be initialized.
// |android_id| and |security_token| are optional if this is not a new
@@ -90,21 +122,29 @@ class GCM_EXPORT MCSClient {
// with a valid LoginResponse.
// Login failure (typically invalid id/token) will shut down the client, and
// |initialization_callback| to be invoked with |success = false|.
- void Login(uint64 android_id, uint64 security_token);
+ virtual void Login(uint64 android_id, uint64 security_token);
// Sends a message, with or without reliable message queueing (RMQ) support.
// Will asynchronously invoke the OnMessageSent callback regardless.
- // TODO(zea): support TTL.
- void SendMessage(const MCSMessage& message, bool use_rmq);
-
- // Disconnects the client and permanently destroys the persistent RMQ store.
- // WARNING: This is permanent, and the client must be recreated with new
- // credentials afterwards.
- void Destroy();
+ // Whether to use RMQ depends on whether the protobuf has |ttl| set or not.
+ // |ttl == 0| denotes the message should only be sent if the connection is
+ // open. |ttl > 0| will keep the message saved for |ttl| seconds, after which
+ // it will be dropped if it was unable to be sent. When a message is dropped,
+ // |message_sent_callback_| is invoked with a TTL expiration error.
+ virtual void SendMessage(const MCSMessage& message);
// Returns the current state of the client.
State state() const { return state_; }
+ // Returns the size of the send message queue.
+ int GetSendQueueSize() const;
+
+ // Returns the size of the resend messaage queue.
+ int GetResendQueueSize() const;
+
+ // Returns text representation of the state enum.
+ std::string GetStateString() const;
+
private:
typedef uint32 StreamId;
typedef std::string PersistentId;
@@ -122,9 +162,8 @@ class GCM_EXPORT MCSClient {
// Send a heartbeat to the MCS server.
void SendHeartbeat();
- // RMQ Store callbacks.
- void OnRMQLoadFinished(const RMQStore::LoadResult& result);
- void OnRMQUpdateFinished(bool success);
+ // GCM Store callback.
+ void OnGCMUpdateFinished(bool success);
// Attempt to send a message.
void MaybeSendMessage();
@@ -155,11 +194,28 @@ class GCM_EXPORT MCSClient {
// Virtual for testing.
virtual PersistentId GetNextPersistentId();
+ // Helper for the heartbeat manager to signal a connection reset.
+ void OnConnectionResetByHeartbeat();
+
+ // Runs the message_sent_callback_ with send |status| of the |protobuf|.
+ void NotifyMessageSendStatus(const google::protobuf::MessageLite& protobuf,
+ MessageSendStatus status);
+
+ // Pops the next message from the front of the send queue (cleaning up
+ // any associated state).
+ MCSPacketInternal PopMessageForSend();
+
+ // Local version string. Sent on login.
+ const std::string version_string_;
+
+ // Clock for enforcing TTL. Passed in for testing.
+ base::Clock* const clock_;
+
// Client state.
State state_;
// Callbacks for owner.
- InitializationCompleteCallback initialization_callback_;
+ ErrorCallback mcs_error_callback_;
OnMessageReceivedCallback message_received_callback_;
OnMessageSentCallback message_sent_callback_;
@@ -182,6 +238,9 @@ class GCM_EXPORT MCSClient {
std::deque<MCSPacketInternal> to_send_;
std::deque<MCSPacketInternal> to_resend_;
+ // Map of collapse keys to their pending messages.
+ std::map<CollapseKey, ReliablePacketInfo*> collapse_key_map_;
+
// Last device_to_server stream id acknowledged by the server.
StreamId last_device_to_server_stream_id_received_;
// Last server_to_device stream id acknowledged by this device.
@@ -209,17 +268,14 @@ class GCM_EXPORT MCSClient {
// acknowledged on the next login attempt.
PersistentIdList restored_unackeds_server_ids_;
- // The reliable message queue persistent store.
- RMQStore rmq_store_;
+ // The GCM persistent store. Not owned.
+ GCMStore* gcm_store_;
- // ----- Heartbeats -----
- // The current heartbeat interval.
- base::TimeDelta heartbeat_interval_;
- // Timer for triggering heartbeats.
- base::Timer heartbeat_timer_;
+ // Manager to handle triggering/detecting heartbeats.
+ HeartbeatManager heartbeat_manager_;
- // The task runner for blocking tasks (i.e. persisting RMQ state to disk).
- scoped_refptr<base::SequencedTaskRunner> blocking_task_runner_;
+ // Recorder that records GCM activities for debugging purpose. Not owned.
+ GCMStatsRecorder* recorder_;
base::WeakPtrFactory<MCSClient> weak_ptr_factory_;
diff --git a/chromium/google_apis/gcm/engine/mcs_client_unittest.cc b/chromium/google_apis/gcm/engine/mcs_client_unittest.cc
index 6ef140586e9..915b25d18db 100644
--- a/chromium/google_apis/gcm/engine/mcs_client_unittest.cc
+++ b/chromium/google_apis/gcm/engine/mcs_client_unittest.cc
@@ -4,14 +4,18 @@
#include "google_apis/gcm/engine/mcs_client.h"
+#include "base/command_line.h"
#include "base/files/scoped_temp_dir.h"
#include "base/message_loop/message_loop.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
-#include "components/webdata/encryptor/encryptor.h"
+#include "base/test/simple_test_clock.h"
+#include "google_apis/gcm/base/fake_encryptor.h"
#include "google_apis/gcm/base/mcs_util.h"
#include "google_apis/gcm/engine/fake_connection_factory.h"
#include "google_apis/gcm/engine/fake_connection_handler.h"
+#include "google_apis/gcm/engine/gcm_store_impl.h"
+#include "google_apis/gcm/monitoring/fake_gcm_stats_recorder.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace gcm {
@@ -30,27 +34,43 @@ const int kMessageBatchSize = 6;
// TODO(zea): get this (and other constants) directly from the mcs client.
const int kAckLimitSize = 10;
+// TTL value for reliable messages.
+const int kTTLValue = 5 * 60; // 5 minutes.
+
// Helper for building arbitrary data messages.
MCSMessage BuildDataMessage(const std::string& from,
const std::string& category,
+ const std::string& message_id,
int last_stream_id_received,
- const std::string persistent_id) {
+ const std::string& persistent_id,
+ int ttl,
+ uint64 sent,
+ int queued,
+ const std::string& token,
+ const uint64& user_id) {
mcs_proto::DataMessageStanza data_message;
+ data_message.set_id(message_id);
data_message.set_from(from);
data_message.set_category(category);
data_message.set_last_stream_id_received(last_stream_id_received);
if (!persistent_id.empty())
data_message.set_persistent_id(persistent_id);
+ data_message.set_ttl(ttl);
+ data_message.set_sent(sent);
+ data_message.set_queued(queued);
+ data_message.set_token(token);
+ data_message.set_device_user_id(user_id);
return MCSMessage(kDataMessageStanzaTag, data_message);
}
// MCSClient with overriden exposed persistent id logic.
class TestMCSClient : public MCSClient {
public:
- TestMCSClient(const base::FilePath& rmq_path,
+ TestMCSClient(base::Clock* clock,
ConnectionFactory* connection_factory,
- scoped_refptr<base::SequencedTaskRunner> blocking_task_runner)
- : MCSClient(rmq_path, connection_factory, blocking_task_runner),
+ GCMStore* gcm_store,
+ gcm::GCMStatsRecorder* recorder)
+ : MCSClient("", clock, connection_factory, gcm_store, recorder),
next_id_(0) {
}
@@ -67,10 +87,14 @@ class MCSClientTest : public testing::Test {
MCSClientTest();
virtual ~MCSClientTest();
+ virtual void SetUp() OVERRIDE;
+
void BuildMCSClient();
void InitializeClient();
+ void StoreCredentials();
void LoginClient(const std::vector<std::string>& acknowledged_ids);
+ base::SimpleTestClock* clock() { return &clock_; }
TestMCSClient* mcs_client() const { return mcs_client_.get(); }
FakeConnectionFactory* connection_factory() {
return &connection_factory_;
@@ -80,6 +104,11 @@ class MCSClientTest : public testing::Test {
uint64 restored_security_token() const { return restored_security_token_; }
MCSMessage* received_message() const { return received_message_.get(); }
std::string sent_message_id() const { return sent_message_id_;}
+ MCSClient::MessageSendStatus message_send_status() const {
+ return message_send_status_;
+ }
+
+ void SetDeviceCredentialsCallback(bool success);
FakeConnectionHandler* GetFakeHandler() const;
@@ -87,15 +116,19 @@ class MCSClientTest : public testing::Test {
void PumpLoop();
private:
- void InitializationCallback(bool success,
- uint64 restored_android_id,
- uint64 restored_security_token);
+ void ErrorCallback();
void MessageReceivedCallback(const MCSMessage& message);
- void MessageSentCallback(const std::string& message_id);
+ void MessageSentCallback(int64 user_serial_number,
+ const std::string& app_id,
+ const std::string& message_id,
+ MCSClient::MessageSendStatus status);
+
+ base::SimpleTestClock clock_;
base::ScopedTempDir temp_directory_;
base::MessageLoop message_loop_;
scoped_ptr<base::RunLoop> run_loop_;
+ scoped_ptr<GCMStore> gcm_store_;
FakeConnectionFactory connection_factory_;
scoped_ptr<TestMCSClient> mcs_client_;
@@ -104,46 +137,58 @@ class MCSClientTest : public testing::Test {
uint64 restored_security_token_;
scoped_ptr<MCSMessage> received_message_;
std::string sent_message_id_;
+ MCSClient::MessageSendStatus message_send_status_;
+
+ gcm::FakeGCMStatsRecorder recorder_;
};
MCSClientTest::MCSClientTest()
: run_loop_(new base::RunLoop()),
- init_success_(false),
+ init_success_(true),
restored_android_id_(0),
- restored_security_token_(0) {
+ restored_security_token_(0),
+ message_send_status_(MCSClient::SENT) {
EXPECT_TRUE(temp_directory_.CreateUniqueTempDir());
run_loop_.reset(new base::RunLoop());
- // On OSX, prevent the Keychain permissions popup during unit tests.
-#if defined(OS_MACOSX)
- Encryptor::UseMockKeychain(true);
-#endif
+ // Advance the clock to a non-zero time.
+ clock_.Advance(base::TimeDelta::FromSeconds(1));
}
MCSClientTest::~MCSClientTest() {}
+void MCSClientTest::SetUp() {
+ testing::Test::SetUp();
+}
+
void MCSClientTest::BuildMCSClient() {
- mcs_client_.reset(
- new TestMCSClient(temp_directory_.path(),
- &connection_factory_,
- message_loop_.message_loop_proxy()));
+ gcm_store_.reset(new GCMStoreImpl(
+ temp_directory_.path(),
+ message_loop_.message_loop_proxy(),
+ make_scoped_ptr<Encryptor>(new FakeEncryptor)));
+ mcs_client_.reset(new TestMCSClient(&clock_,
+ &connection_factory_,
+ gcm_store_.get(),
+ &recorder_));
}
void MCSClientTest::InitializeClient() {
- mcs_client_->Initialize(base::Bind(&MCSClientTest::InitializationCallback,
- base::Unretained(this)),
- base::Bind(&MCSClientTest::MessageReceivedCallback,
- base::Unretained(this)),
- base::Bind(&MCSClientTest::MessageSentCallback,
- base::Unretained(this)));
- run_loop_->Run();
+ gcm_store_->Load(base::Bind(
+ &MCSClient::Initialize,
+ base::Unretained(mcs_client_.get()),
+ base::Bind(&MCSClientTest::ErrorCallback,
+ base::Unretained(this)),
+ base::Bind(&MCSClientTest::MessageReceivedCallback,
+ base::Unretained(this)),
+ base::Bind(&MCSClientTest::MessageSentCallback, base::Unretained(this))));
+ run_loop_->RunUntilIdle();
run_loop_.reset(new base::RunLoop());
}
void MCSClientTest::LoginClient(
const std::vector<std::string>& acknowledged_ids) {
scoped_ptr<mcs_proto::LoginRequest> login_request =
- BuildLoginRequest(kAndroidId, kSecurityToken);
+ BuildLoginRequest(kAndroidId, kSecurityToken, "");
for (size_t i = 0; i < acknowledged_ids.size(); ++i)
login_request->add_received_persistent_id(acknowledged_ids[i]);
GetFakeHandler()->ExpectOutgoingMessage(
@@ -154,6 +199,15 @@ void MCSClientTest::LoginClient(
run_loop_.reset(new base::RunLoop());
}
+void MCSClientTest::StoreCredentials() {
+ gcm_store_->SetDeviceCredentials(
+ kAndroidId, kSecurityToken,
+ base::Bind(&MCSClientTest::SetDeviceCredentialsCallback,
+ base::Unretained(this)));
+ run_loop_->Run();
+ run_loop_.reset(new base::RunLoop());
+}
+
FakeConnectionHandler* MCSClientTest::GetFakeHandler() const {
return reinterpret_cast<FakeConnectionHandler*>(
connection_factory_.GetConnectionHandler());
@@ -169,13 +223,9 @@ void MCSClientTest::PumpLoop() {
run_loop_.reset(new base::RunLoop());
}
-void MCSClientTest::InitializationCallback(bool success,
- uint64 restored_android_id,
- uint64 restored_security_token) {
- init_success_ = success;
- restored_android_id_ = restored_android_id;
- restored_security_token_ = restored_security_token;
- DVLOG(1) << "Initialization callback invoked, killing loop.";
+void MCSClientTest::ErrorCallback() {
+ init_success_ = false;
+ DVLOG(1) << "Error callback invoked, killing loop.";
run_loop_->Quit();
}
@@ -185,8 +235,18 @@ void MCSClientTest::MessageReceivedCallback(const MCSMessage& message) {
run_loop_->Quit();
}
-void MCSClientTest::MessageSentCallback(const std::string& message_id) {
+void MCSClientTest::MessageSentCallback(int64 user_serial_number,
+ const std::string& app_id,
+ const std::string& message_id,
+ MCSClient::MessageSendStatus status) {
DVLOG(1) << "Message sent callback invoked, killing loop.";
+ sent_message_id_ = message_id;
+ message_send_status_ = status;
+ run_loop_->Quit();
+}
+
+void MCSClientTest::SetDeviceCredentialsCallback(bool success) {
+ ASSERT_TRUE(success);
run_loop_->Quit();
}
@@ -194,8 +254,6 @@ void MCSClientTest::MessageSentCallback(const std::string& message_id) {
TEST_F(MCSClientTest, InitializeNew) {
BuildMCSClient();
InitializeClient();
- EXPECT_EQ(0U, restored_android_id());
- EXPECT_EQ(0U, restored_security_token());
EXPECT_TRUE(init_success());
}
@@ -206,11 +264,10 @@ TEST_F(MCSClientTest, InitializeExisting) {
InitializeClient();
LoginClient(std::vector<std::string>());
- // Rebuild the client, to reload from the RMQ.
+ // Rebuild the client, to reload from the GCM store.
+ StoreCredentials();
BuildMCSClient();
InitializeClient();
- EXPECT_EQ(kAndroidId, restored_android_id());
- EXPECT_EQ(kSecurityToken, restored_security_token());
EXPECT_TRUE(init_success());
}
@@ -225,15 +282,18 @@ TEST_F(MCSClientTest, LoginSuccess) {
EXPECT_EQ(kLoginResponseTag, received_message()->tag());
}
-// Encounter a server error during the login attempt.
+// Encounter a server error during the login attempt. Should trigger a
+// reconnect.
TEST_F(MCSClientTest, FailLogin) {
BuildMCSClient();
InitializeClient();
GetFakeHandler()->set_fail_login(true);
+ connection_factory()->set_delay_reconnect(true);
LoginClient(std::vector<std::string>());
EXPECT_FALSE(connection_factory()->IsEndpointReachable());
EXPECT_FALSE(init_success());
EXPECT_FALSE(received_message());
+ EXPECT_TRUE(connection_factory()->reconnect_pending());
}
// Send a message without RMQ support.
@@ -241,11 +301,29 @@ TEST_F(MCSClientTest, SendMessageNoRMQ) {
BuildMCSClient();
InitializeClient();
LoginClient(std::vector<std::string>());
- MCSMessage message(BuildDataMessage("from", "category", 1, ""));
+ MCSMessage message(
+ BuildDataMessage("from", "category", "X", 1, "", 0, 1, 0, "", 0));
GetFakeHandler()->ExpectOutgoingMessage(message);
- mcs_client()->SendMessage(message, false);
- EXPECT_TRUE(GetFakeHandler()->
- AllOutgoingMessagesReceived());
+ mcs_client()->SendMessage(message);
+ EXPECT_TRUE(GetFakeHandler()->AllOutgoingMessagesReceived());
+}
+
+// Send a message without RMQ support while disconnected. Message send should
+// fail immediately, invoking callback.
+TEST_F(MCSClientTest, SendMessageNoRMQWhileDisconnected) {
+ BuildMCSClient();
+ InitializeClient();
+
+ EXPECT_TRUE(sent_message_id().empty());
+ MCSMessage message(
+ BuildDataMessage("from", "category", "X", 1, "", 0, 1, 0, "", 0));
+ mcs_client()->SendMessage(message);
+
+ // Message sent callback should be invoked, but no message should actually
+ // be sent.
+ EXPECT_EQ("X", sent_message_id());
+ EXPECT_EQ(MCSClient::NO_CONNECTION_ON_ZERO_TTL, message_send_status());
+ EXPECT_TRUE(GetFakeHandler()->AllOutgoingMessagesReceived());
}
// Send a message with RMQ support.
@@ -253,11 +331,11 @@ TEST_F(MCSClientTest, SendMessageRMQ) {
BuildMCSClient();
InitializeClient();
LoginClient(std::vector<std::string>());
- MCSMessage message(BuildDataMessage("from", "category", 1, "1"));
+ MCSMessage message(BuildDataMessage(
+ "from", "category", "X", 1, "1", kTTLValue, 1, 0, "", 0));
GetFakeHandler()->ExpectOutgoingMessage(message);
- mcs_client()->SendMessage(message, true);
- EXPECT_TRUE(GetFakeHandler()->
- AllOutgoingMessagesReceived());
+ mcs_client()->SendMessage(message);
+ EXPECT_TRUE(GetFakeHandler()->AllOutgoingMessagesReceived());
}
// Send a message with RMQ support while disconnected. On reconnect, the message
@@ -267,26 +345,31 @@ TEST_F(MCSClientTest, SendMessageRMQWhileDisconnected) {
InitializeClient();
LoginClient(std::vector<std::string>());
GetFakeHandler()->set_fail_send(true);
- MCSMessage message(BuildDataMessage("from", "category", 1, "1"));
+ MCSMessage message(BuildDataMessage(
+ "from", "category", "X", 1, "1", kTTLValue, 1, 0, "", 0));
// The initial (failed) send.
GetFakeHandler()->ExpectOutgoingMessage(message);
// The login request.
GetFakeHandler()->ExpectOutgoingMessage(
- MCSMessage(kLoginRequestTag,
- BuildLoginRequest(kAndroidId, kSecurityToken).
- PassAs<const google::protobuf::MessageLite>()));
+ MCSMessage(
+ kLoginRequestTag,
+ BuildLoginRequest(kAndroidId, kSecurityToken, "").
+ PassAs<const google::protobuf::MessageLite>()));
// The second (re)send.
- GetFakeHandler()->ExpectOutgoingMessage(message);
- mcs_client()->SendMessage(message, true);
- EXPECT_FALSE(GetFakeHandler()->
- AllOutgoingMessagesReceived());
+ MCSMessage message2(BuildDataMessage(
+ "from", "category", "X", 1, "1", kTTLValue, 1, kTTLValue - 1, "", 0));
+ GetFakeHandler()->ExpectOutgoingMessage(message2);
+ mcs_client()->SendMessage(message);
+ PumpLoop(); // Wait for the queuing to happen.
+ EXPECT_EQ(MCSClient::QUEUED, message_send_status());
+ EXPECT_FALSE(GetFakeHandler()->AllOutgoingMessagesReceived());
GetFakeHandler()->set_fail_send(false);
+ clock()->Advance(base::TimeDelta::FromSeconds(kTTLValue - 1));
connection_factory()->Connect();
WaitForMCSEvent(); // Wait for the login to finish.
PumpLoop(); // Wait for the send to happen.
- EXPECT_TRUE(GetFakeHandler()->
- AllOutgoingMessagesReceived());
+ EXPECT_TRUE(GetFakeHandler()->AllOutgoingMessagesReceived());
}
// Send a message with RMQ support without receiving an acknowledgement. On
@@ -296,23 +379,28 @@ TEST_F(MCSClientTest, SendMessageRMQOnRestart) {
InitializeClient();
LoginClient(std::vector<std::string>());
GetFakeHandler()->set_fail_send(true);
- MCSMessage message(BuildDataMessage("from", "category", 1, "1"));
+ MCSMessage message(BuildDataMessage(
+ "from", "category", "X", 1, "1", kTTLValue, 1, 0, "", 0));
// The initial (failed) send.
GetFakeHandler()->ExpectOutgoingMessage(message);
GetFakeHandler()->set_fail_send(false);
- mcs_client()->SendMessage(message, true);
- EXPECT_TRUE(GetFakeHandler()->
- AllOutgoingMessagesReceived());
+ mcs_client()->SendMessage(message);
+ PumpLoop();
+ EXPECT_TRUE(GetFakeHandler()->AllOutgoingMessagesReceived());
// Rebuild the client, which should resend the old message.
+ StoreCredentials();
BuildMCSClient();
InitializeClient();
+
+ clock()->Advance(base::TimeDelta::FromSeconds(kTTLValue - 1));
+ MCSMessage message2(BuildDataMessage(
+ "from", "category", "X", 1, "1", kTTLValue, 1, kTTLValue - 1, "", 0));
LoginClient(std::vector<std::string>());
- GetFakeHandler()->ExpectOutgoingMessage(message);
+ GetFakeHandler()->ExpectOutgoingMessage(message2);
PumpLoop();
- EXPECT_TRUE(GetFakeHandler()->
- AllOutgoingMessagesReceived());
+ EXPECT_TRUE(GetFakeHandler()->AllOutgoingMessagesReceived());
}
// Send messages with RMQ support, followed by receiving a stream ack. On
@@ -324,13 +412,21 @@ TEST_F(MCSClientTest, SendMessageRMQWithStreamAck) {
// Send some messages.
for (int i = 1; i <= kMessageBatchSize; ++i) {
- MCSMessage message(
- BuildDataMessage("from", "category", 1, base::IntToString(i)));
+ MCSMessage message(BuildDataMessage("from",
+ "category",
+ "X",
+ 1,
+ base::IntToString(i),
+ kTTLValue,
+ 1,
+ 0,
+ "",
+ 0));
GetFakeHandler()->ExpectOutgoingMessage(message);
- mcs_client()->SendMessage(message, true);
+ mcs_client()->SendMessage(message);
+ PumpLoop();
}
- EXPECT_TRUE(GetFakeHandler()->
- AllOutgoingMessagesReceived());
+ EXPECT_TRUE(GetFakeHandler()->AllOutgoingMessagesReceived());
// Receive the ack.
scoped_ptr<mcs_proto::IqStanza> ack = BuildStreamAck();
@@ -341,6 +437,7 @@ TEST_F(MCSClientTest, SendMessageRMQWithStreamAck) {
WaitForMCSEvent();
// Reconnect and ensure no messages are resent.
+ StoreCredentials();
BuildMCSClient();
InitializeClient();
LoginClient(std::vector<std::string>());
@@ -358,16 +455,25 @@ TEST_F(MCSClientTest, SendMessageRMQAckOnReconnect) {
std::vector<std::string> id_list;
for (int i = 1; i <= kMessageBatchSize; ++i) {
id_list.push_back(base::IntToString(i));
- MCSMessage message(
- BuildDataMessage("from", "category", 1, id_list.back()));
+ MCSMessage message(BuildDataMessage("from",
+ "category",
+ id_list.back(),
+ 1,
+ id_list.back(),
+ kTTLValue,
+ 1,
+ 0,
+ "",
+ 0));
GetFakeHandler()->ExpectOutgoingMessage(message);
- mcs_client()->SendMessage(message, true);
+ mcs_client()->SendMessage(message);
+ PumpLoop();
}
- EXPECT_TRUE(GetFakeHandler()->
- AllOutgoingMessagesReceived());
+ EXPECT_TRUE(GetFakeHandler()->AllOutgoingMessagesReceived());
// Rebuild the client, and receive an acknowledgment for the messages as
// part of the login response.
+ StoreCredentials();
BuildMCSClient();
InitializeClient();
LoginClient(std::vector<std::string>());
@@ -375,9 +481,7 @@ TEST_F(MCSClientTest, SendMessageRMQAckOnReconnect) {
GetFakeHandler()->ReceiveMessage(
MCSMessage(kIqStanzaTag,
ack.PassAs<const google::protobuf::MessageLite>()));
- WaitForMCSEvent();
- EXPECT_TRUE(GetFakeHandler()->
- AllOutgoingMessagesReceived());
+ EXPECT_TRUE(GetFakeHandler()->AllOutgoingMessagesReceived());
}
// Send messages with RMQ support. On restart, receive a SelectiveAck with
@@ -392,16 +496,25 @@ TEST_F(MCSClientTest, SendMessageRMQPartialAckOnReconnect) {
std::vector<std::string> id_list;
for (int i = 1; i <= kMessageBatchSize; ++i) {
id_list.push_back(base::IntToString(i));
- MCSMessage message(
- BuildDataMessage("from", "category", 1, id_list.back()));
+ MCSMessage message(BuildDataMessage("from",
+ "category",
+ id_list.back(),
+ 1,
+ id_list.back(),
+ kTTLValue,
+ 1,
+ 0,
+ "",
+ 0));
GetFakeHandler()->ExpectOutgoingMessage(message);
- mcs_client()->SendMessage(message, true);
+ mcs_client()->SendMessage(message);
+ PumpLoop();
}
- EXPECT_TRUE(GetFakeHandler()->
- AllOutgoingMessagesReceived());
+ EXPECT_TRUE(GetFakeHandler()->AllOutgoingMessagesReceived());
// Rebuild the client, and receive an acknowledgment for the messages as
// part of the login response.
+ StoreCredentials();
BuildMCSClient();
InitializeClient();
LoginClient(std::vector<std::string>());
@@ -414,11 +527,16 @@ TEST_F(MCSClientTest, SendMessageRMQPartialAckOnReconnect) {
id_list.begin() + kMessageBatchSize / 2,
id_list.end());
for (int i = 1; i <= kMessageBatchSize / 2; ++i) {
- MCSMessage message(
- BuildDataMessage("from",
- "category",
- 2,
- remaining_ids[i - 1]));
+ MCSMessage message(BuildDataMessage("from",
+ "category",
+ remaining_ids[i - 1],
+ 2,
+ remaining_ids[i - 1],
+ kTTLValue,
+ 1,
+ 0,
+ "",
+ 0));
GetFakeHandler()->ExpectOutgoingMessage(message);
}
scoped_ptr<mcs_proto::IqStanza> ack(BuildSelectiveAck(acked_ids));
@@ -426,8 +544,8 @@ TEST_F(MCSClientTest, SendMessageRMQPartialAckOnReconnect) {
MCSMessage(kIqStanzaTag,
ack.PassAs<const google::protobuf::MessageLite>()));
WaitForMCSEvent();
- EXPECT_TRUE(GetFakeHandler()->
- AllOutgoingMessagesReceived());
+ PumpLoop();
+ EXPECT_TRUE(GetFakeHandler()->AllOutgoingMessagesReceived());
}
// Receive some messages. On restart, the login request should contain the
@@ -441,14 +559,15 @@ TEST_F(MCSClientTest, AckOnLogin) {
std::vector<std::string> id_list;
for (int i = 1; i <= kMessageBatchSize; ++i) {
id_list.push_back(base::IntToString(i));
- MCSMessage message(
- BuildDataMessage("from", "category", i, id_list.back()));
+ MCSMessage message(BuildDataMessage(
+ "from", "category", "X", 1, id_list.back(), kTTLValue, 1, 0, "", 0));
GetFakeHandler()->ReceiveMessage(message);
WaitForMCSEvent();
PumpLoop();
}
// Restart the client.
+ StoreCredentials();
BuildMCSClient();
InitializeClient();
LoginClient(id_list);
@@ -465,20 +584,34 @@ TEST_F(MCSClientTest, AckOnSend) {
std::vector<std::string> id_list;
for (int i = 1; i <= kMessageBatchSize; ++i) {
id_list.push_back(base::IntToString(i));
- MCSMessage message(
- BuildDataMessage("from", "category", i, id_list.back()));
+ MCSMessage message(BuildDataMessage("from",
+ "category",
+ id_list.back(),
+ 1,
+ id_list.back(),
+ kTTLValue,
+ 1,
+ 0,
+ "",
+ 0));
GetFakeHandler()->ReceiveMessage(message);
- WaitForMCSEvent();
PumpLoop();
}
// Trigger a message send, which should acknowledge via stream ack.
- MCSMessage message(
- BuildDataMessage("from", "category", kMessageBatchSize + 1, "1"));
+ MCSMessage message(BuildDataMessage("from",
+ "category",
+ "X",
+ kMessageBatchSize + 1,
+ "1",
+ kTTLValue,
+ 1,
+ 0,
+ "",
+ 0));
GetFakeHandler()->ExpectOutgoingMessage(message);
- mcs_client()->SendMessage(message, true);
- EXPECT_TRUE(GetFakeHandler()->
- AllOutgoingMessagesReceived());
+ mcs_client()->SendMessage(message);
+ EXPECT_TRUE(GetFakeHandler()->AllOutgoingMessagesReceived());
}
// Receive the ack limit in messages, which should trigger an automatic
@@ -499,14 +632,21 @@ TEST_F(MCSClientTest, AckWhenLimitReachedWithHeartbeat) {
std::vector<std::string> id_list;
for (int i = 1; i <= kAckLimitSize; ++i) {
id_list.push_back(base::IntToString(i));
- MCSMessage message(
- BuildDataMessage("from", "category", i, id_list.back()));
+ MCSMessage message(BuildDataMessage("from",
+ "category",
+ id_list.back(),
+ 1,
+ id_list.back(),
+ kTTLValue,
+ 1,
+ 0,
+ "",
+ 0));
GetFakeHandler()->ReceiveMessage(message);
WaitForMCSEvent();
PumpLoop();
}
- EXPECT_TRUE(GetFakeHandler()->
- AllOutgoingMessagesReceived());
+ EXPECT_TRUE(GetFakeHandler()->AllOutgoingMessagesReceived());
// Receive a heartbeat confirming the ack (and receive the heartbeat ack).
scoped_ptr<mcs_proto::HeartbeatPing> heartbeat(
@@ -523,16 +663,135 @@ TEST_F(MCSClientTest, AckWhenLimitReachedWithHeartbeat) {
GetFakeHandler()->ReceiveMessage(
MCSMessage(kHeartbeatPingTag,
heartbeat.PassAs<const google::protobuf::MessageLite>()));
- WaitForMCSEvent();
- EXPECT_TRUE(GetFakeHandler()->
- AllOutgoingMessagesReceived());
+ PumpLoop();
+ EXPECT_TRUE(GetFakeHandler()->AllOutgoingMessagesReceived());
// Rebuild the client. Nothing should be sent on login.
+ StoreCredentials();
BuildMCSClient();
InitializeClient();
LoginClient(std::vector<std::string>());
- EXPECT_TRUE(GetFakeHandler()->
- AllOutgoingMessagesReceived());
+ EXPECT_TRUE(GetFakeHandler()->AllOutgoingMessagesReceived());
+}
+
+// If a message's TTL has expired by the time it reaches the front of the send
+// queue, it should be dropped.
+TEST_F(MCSClientTest, ExpiredTTLOnSend) {
+ BuildMCSClient();
+ InitializeClient();
+ LoginClient(std::vector<std::string>());
+ MCSMessage message(BuildDataMessage(
+ "from", "category", "X", 1, "1", kTTLValue, 1, 0, "", 0));
+
+ // Advance time to after the TTL.
+ clock()->Advance(base::TimeDelta::FromSeconds(kTTLValue + 2));
+ EXPECT_TRUE(sent_message_id().empty());
+ mcs_client()->SendMessage(message);
+
+ // No messages should be sent, but the callback should still be invoked.
+ EXPECT_EQ("X", sent_message_id());
+ EXPECT_EQ(MCSClient::TTL_EXCEEDED, message_send_status());
+ EXPECT_TRUE(GetFakeHandler()->AllOutgoingMessagesReceived());
+}
+
+TEST_F(MCSClientTest, ExpiredTTLOnRestart) {
+ BuildMCSClient();
+ InitializeClient();
+ LoginClient(std::vector<std::string>());
+ GetFakeHandler()->set_fail_send(true);
+ MCSMessage message(BuildDataMessage(
+ "from", "category", "X", 1, "1", kTTLValue, 1, 0, "", 0));
+
+ // The initial (failed) send.
+ GetFakeHandler()->ExpectOutgoingMessage(message);
+ GetFakeHandler()->set_fail_send(false);
+ mcs_client()->SendMessage(message);
+ PumpLoop();
+ EXPECT_TRUE(GetFakeHandler()->AllOutgoingMessagesReceived());
+
+ // Move the clock forward and rebuild the client, which should fail the
+ // message send on restart.
+ clock()->Advance(base::TimeDelta::FromSeconds(kTTLValue + 2));
+ StoreCredentials();
+ BuildMCSClient();
+ InitializeClient();
+ LoginClient(std::vector<std::string>());
+ PumpLoop();
+ EXPECT_EQ("X", sent_message_id());
+ EXPECT_EQ(MCSClient::TTL_EXCEEDED, message_send_status());
+ EXPECT_TRUE(GetFakeHandler()->AllOutgoingMessagesReceived());
+}
+
+// Sending two messages with the same collapse key and same app id while
+// disconnected should only send the latter of the two on reconnection.
+TEST_F(MCSClientTest, CollapseKeysSameApp) {
+ BuildMCSClient();
+ InitializeClient();
+ MCSMessage message(BuildDataMessage(
+ "from", "app", "message id 1", 1, "1", kTTLValue, 1, 0, "token", 0));
+ mcs_client()->SendMessage(message);
+
+ MCSMessage message2(BuildDataMessage(
+ "from", "app", "message id 2", 1, "1", kTTLValue, 1, 0, "token", 0));
+ mcs_client()->SendMessage(message2);
+
+ LoginClient(std::vector<std::string>());
+ GetFakeHandler()->ExpectOutgoingMessage(message2);
+ PumpLoop();
+}
+
+// Sending two messages with the same collapse key and different app id while
+// disconnected should not perform any collapsing.
+TEST_F(MCSClientTest, CollapseKeysDifferentApp) {
+ BuildMCSClient();
+ InitializeClient();
+ MCSMessage message(BuildDataMessage(
+ "from", "app", "message id 1", 1, "1", kTTLValue, 1, 0, "token", 0));
+ mcs_client()->SendMessage(message);
+
+ MCSMessage message2(BuildDataMessage("from",
+ "app 2",
+ "message id 2",
+ 1,
+ "2",
+ kTTLValue,
+ 1,
+ 0,
+ "token",
+ 0));
+ mcs_client()->SendMessage(message2);
+
+ LoginClient(std::vector<std::string>());
+ GetFakeHandler()->ExpectOutgoingMessage(message);
+ GetFakeHandler()->ExpectOutgoingMessage(message2);
+ PumpLoop();
+}
+
+// Sending two messages with the same collapse key and app id, but different
+// user, while disconnected, should not perform any collapsing.
+TEST_F(MCSClientTest, CollapseKeysDifferentUser) {
+ BuildMCSClient();
+ InitializeClient();
+ MCSMessage message(BuildDataMessage(
+ "from", "app", "message id 1", 1, "1", kTTLValue, 1, 0, "token", 0));
+ mcs_client()->SendMessage(message);
+
+ MCSMessage message2(BuildDataMessage("from",
+ "app",
+ "message id 2",
+ 1,
+ "2",
+ kTTLValue,
+ 1,
+ 0,
+ "token",
+ 1));
+ mcs_client()->SendMessage(message2);
+
+ LoginClient(std::vector<std::string>());
+ GetFakeHandler()->ExpectOutgoingMessage(message);
+ GetFakeHandler()->ExpectOutgoingMessage(message2);
+ PumpLoop();
}
} // namespace
diff --git a/chromium/google_apis/gcm/engine/registration_info.cc b/chromium/google_apis/gcm/engine/registration_info.cc
new file mode 100644
index 00000000000..6a41c76e00e
--- /dev/null
+++ b/chromium/google_apis/gcm/engine/registration_info.cc
@@ -0,0 +1,62 @@
+// 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 "google_apis/gcm/engine/registration_info.h"
+
+#include "base/strings/string_util.h"
+
+namespace gcm {
+
+RegistrationInfo::RegistrationInfo() {
+}
+
+RegistrationInfo::~RegistrationInfo() {
+}
+
+std::string RegistrationInfo::SerializeAsString() const {
+ if (sender_ids.empty() || registration_id.empty())
+ return std::string();
+
+ // Serialize as:
+ // sender1,sender2,...=reg_id
+ std::string value;
+ for (std::vector<std::string>::const_iterator iter = sender_ids.begin();
+ iter != sender_ids.end(); ++iter) {
+ DCHECK(!iter->empty() &&
+ iter->find(',') == std::string::npos &&
+ iter->find('=') == std::string::npos);
+ if (!value.empty())
+ value += ",";
+ value += *iter;
+ }
+
+ DCHECK(registration_id.find('=') == std::string::npos);
+ value += '=';
+ value += registration_id;
+ return value;
+}
+
+bool RegistrationInfo::ParseFromString(const std::string& value) {
+ if (value.empty())
+ return true;
+
+ size_t pos = value.find('=');
+ if (pos == std::string::npos)
+ return false;
+
+ std::string senders = value.substr(0, pos);
+ registration_id = value.substr(pos + 1);
+
+ Tokenize(senders, ",", &sender_ids);
+
+ if (sender_ids.empty() || registration_id.empty()) {
+ sender_ids.clear();
+ registration_id.clear();
+ return false;
+ }
+
+ return true;
+}
+
+} // namespace gcm
diff --git a/chromium/google_apis/gcm/engine/registration_info.h b/chromium/google_apis/gcm/engine/registration_info.h
new file mode 100644
index 00000000000..c06a6aa4c56
--- /dev/null
+++ b/chromium/google_apis/gcm/engine/registration_info.h
@@ -0,0 +1,35 @@
+// 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 GOOGLE_APIS_GCM_ENGINE_REGISTRATION_INFO_H_
+#define GOOGLE_APIS_GCM_ENGINE_REGISTRATION_INFO_H_
+
+#include <map>
+#include <string>
+#include <vector>
+
+#include "base/basictypes.h"
+#include "base/memory/linked_ptr.h"
+#include "google_apis/gcm/base/gcm_export.h"
+
+namespace gcm {
+
+struct GCM_EXPORT RegistrationInfo {
+ RegistrationInfo();
+ ~RegistrationInfo();
+
+ std::string SerializeAsString() const;
+ bool ParseFromString(const std::string& value);
+
+ std::vector<std::string> sender_ids;
+ std::string registration_id;
+};
+
+// Map of app id to registration info.
+typedef std::map<std::string, linked_ptr<RegistrationInfo> >
+RegistrationInfoMap;
+
+} // namespace gcm
+
+#endif // GOOGLE_APIS_GCM_ENGINE_REGISTRATION_INFO_H_
diff --git a/chromium/google_apis/gcm/engine/registration_request.cc b/chromium/google_apis/gcm/engine/registration_request.cc
new file mode 100644
index 00000000000..e72e7ba2b79
--- /dev/null
+++ b/chromium/google_apis/gcm/engine/registration_request.cc
@@ -0,0 +1,267 @@
+// 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 "google_apis/gcm/engine/registration_request.h"
+
+#include "base/bind.h"
+#include "base/message_loop/message_loop.h"
+#include "base/metrics/histogram.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/values.h"
+#include "google_apis/gcm/monitoring/gcm_stats_recorder.h"
+#include "net/base/escape.h"
+#include "net/http/http_request_headers.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"
+#include "url/gurl.h"
+
+namespace gcm {
+
+namespace {
+
+const char kRegistrationRequestContentType[] =
+ "application/x-www-form-urlencoded";
+
+// Request constants.
+const char kAppIdKey[] = "app";
+const char kDeviceIdKey[] = "device";
+const char kLoginHeader[] = "AidLogin";
+const char kSenderKey[] = "sender";
+
+// Request validation constants.
+const size_t kMaxSenders = 100;
+
+// Response constants.
+const char kErrorPrefix[] = "Error=";
+const char kTokenPrefix[] = "token=";
+const char kDeviceRegistrationError[] = "PHONE_REGISTRATION_ERROR";
+const char kAuthenticationFailed[] = "AUTHENTICATION_FAILED";
+const char kInvalidSender[] = "INVALID_SENDER";
+const char kInvalidParameters[] = "INVALID_PARAMETERS";
+
+void BuildFormEncoding(const std::string& key,
+ const std::string& value,
+ std::string* out) {
+ if (!out->empty())
+ out->append("&");
+ out->append(key + "=" + net::EscapeUrlEncodedData(value, true));
+}
+
+// Gets correct status from the error message.
+RegistrationRequest::Status GetStatusFromError(const std::string& error) {
+ // TODO(fgorski): Improve error parsing in case there is nore then just an
+ // Error=ERROR_STRING in response.
+ if (error.find(kDeviceRegistrationError) != std::string::npos)
+ return RegistrationRequest::DEVICE_REGISTRATION_ERROR;
+ if (error.find(kAuthenticationFailed) != std::string::npos)
+ return RegistrationRequest::AUTHENTICATION_FAILED;
+ if (error.find(kInvalidSender) != std::string::npos)
+ return RegistrationRequest::INVALID_SENDER;
+ if (error.find(kInvalidParameters) != std::string::npos)
+ return RegistrationRequest::INVALID_PARAMETERS;
+ return RegistrationRequest::UNKNOWN_ERROR;
+}
+
+// Indicates whether a retry attempt should be made based on the status of the
+// last request.
+bool ShouldRetryWithStatus(RegistrationRequest::Status status) {
+ return status == RegistrationRequest::UNKNOWN_ERROR ||
+ status == RegistrationRequest::AUTHENTICATION_FAILED ||
+ status == RegistrationRequest::DEVICE_REGISTRATION_ERROR ||
+ status == RegistrationRequest::HTTP_NOT_OK ||
+ status == RegistrationRequest::URL_FETCHING_FAILED ||
+ status == RegistrationRequest::RESPONSE_PARSING_FAILED;
+}
+
+void RecordRegistrationStatusToUMA(RegistrationRequest::Status status) {
+ UMA_HISTOGRAM_ENUMERATION("GCM.RegistrationRequestStatus", status,
+ RegistrationRequest::STATUS_COUNT);
+}
+
+} // namespace
+
+RegistrationRequest::RequestInfo::RequestInfo(
+ uint64 android_id,
+ uint64 security_token,
+ const std::string& app_id,
+ const std::vector<std::string>& sender_ids)
+ : android_id(android_id),
+ security_token(security_token),
+ app_id(app_id),
+ sender_ids(sender_ids) {
+}
+
+RegistrationRequest::RequestInfo::~RequestInfo() {}
+
+RegistrationRequest::RegistrationRequest(
+ const GURL& registration_url,
+ const RequestInfo& request_info,
+ const net::BackoffEntry::Policy& backoff_policy,
+ const RegistrationCallback& callback,
+ int max_retry_count,
+ scoped_refptr<net::URLRequestContextGetter> request_context_getter,
+ GCMStatsRecorder* recorder)
+ : callback_(callback),
+ request_info_(request_info),
+ registration_url_(registration_url),
+ backoff_entry_(&backoff_policy),
+ request_context_getter_(request_context_getter),
+ retries_left_(max_retry_count),
+ recorder_(recorder),
+ weak_ptr_factory_(this) {
+ DCHECK_GE(max_retry_count, 0);
+}
+
+RegistrationRequest::~RegistrationRequest() {}
+
+void RegistrationRequest::Start() {
+ DCHECK(!callback_.is_null());
+ DCHECK(request_info_.android_id != 0UL);
+ DCHECK(request_info_.security_token != 0UL);
+ DCHECK(0 < request_info_.sender_ids.size() &&
+ request_info_.sender_ids.size() <= kMaxSenders);
+
+ DCHECK(!url_fetcher_.get());
+ url_fetcher_.reset(net::URLFetcher::Create(
+ registration_url_, net::URLFetcher::POST, this));
+ url_fetcher_->SetRequestContext(request_context_getter_);
+
+ std::string android_id = base::Uint64ToString(request_info_.android_id);
+ std::string auth_header =
+ std::string(net::HttpRequestHeaders::kAuthorization) + ": " +
+ kLoginHeader + " " + android_id + ":" +
+ base::Uint64ToString(request_info_.security_token);
+ url_fetcher_->SetExtraRequestHeaders(auth_header);
+
+ std::string body;
+ BuildFormEncoding(kAppIdKey, request_info_.app_id, &body);
+ BuildFormEncoding(kDeviceIdKey, android_id, &body);
+
+ std::string senders;
+ for (std::vector<std::string>::const_iterator iter =
+ request_info_.sender_ids.begin();
+ iter != request_info_.sender_ids.end();
+ ++iter) {
+ DCHECK(!iter->empty());
+ if (!senders.empty())
+ senders.append(",");
+ senders.append(*iter);
+ }
+ BuildFormEncoding(kSenderKey, senders, &body);
+ UMA_HISTOGRAM_COUNTS("GCM.RegistrationSenderIdCount",
+ request_info_.sender_ids.size());
+
+ DVLOG(1) << "Performing registration for: " << request_info_.app_id;
+ DVLOG(1) << "Registration request: " << body;
+ url_fetcher_->SetUploadData(kRegistrationRequestContentType, body);
+ recorder_->RecordRegistrationSent(request_info_.app_id, senders);
+ request_start_time_ = base::TimeTicks::Now();
+ url_fetcher_->Start();
+}
+
+void RegistrationRequest::RetryWithBackoff(bool update_backoff) {
+ if (update_backoff) {
+ DCHECK_GT(retries_left_, 0);
+ --retries_left_;
+ url_fetcher_.reset();
+ backoff_entry_.InformOfRequest(false);
+ }
+
+ if (backoff_entry_.ShouldRejectRequest()) {
+ DVLOG(1) << "Delaying GCM registration of app: "
+ << request_info_.app_id << ", for "
+ << backoff_entry_.GetTimeUntilRelease().InMilliseconds()
+ << " milliseconds.";
+ base::MessageLoop::current()->PostDelayedTask(
+ FROM_HERE,
+ base::Bind(&RegistrationRequest::RetryWithBackoff,
+ weak_ptr_factory_.GetWeakPtr(),
+ false),
+ backoff_entry_.GetTimeUntilRelease());
+ return;
+ }
+
+ Start();
+}
+
+RegistrationRequest::Status RegistrationRequest::ParseResponse(
+ const net::URLFetcher* source, std::string* token) {
+ if (!source->GetStatus().is_success()) {
+ LOG(ERROR) << "URL fetching failed.";
+ return URL_FETCHING_FAILED;
+ }
+
+ std::string response;
+ if (!source->GetResponseAsString(&response)) {
+ LOG(ERROR) << "Failed to parse registration response as a string.";
+ return RESPONSE_PARSING_FAILED;
+ }
+
+ if (source->GetResponseCode() == net::HTTP_OK) {
+ size_t token_pos = response.find(kTokenPrefix);
+ if (token_pos != std::string::npos) {
+ *token = response.substr(token_pos + arraysize(kTokenPrefix) - 1);
+ return SUCCESS;
+ }
+ }
+
+ // If we are able to parse a meaningful known error, let's do so. Some errors
+ // will have HTTP_BAD_REQUEST, some will have HTTP_OK response code.
+ size_t error_pos = response.find(kErrorPrefix);
+ if (error_pos != std::string::npos) {
+ std::string error = response.substr(
+ error_pos + arraysize(kErrorPrefix) - 1);
+ return GetStatusFromError(error);
+ }
+
+ // If we cannot tell what the error is, but at least we know response code was
+ // not OK.
+ if (source->GetResponseCode() != net::HTTP_OK) {
+ DLOG(ERROR) << "URL fetching HTTP response code is not OK. It is "
+ << source->GetResponseCode();
+ return HTTP_NOT_OK;
+ }
+
+ return UNKNOWN_ERROR;
+}
+
+void RegistrationRequest::OnURLFetchComplete(const net::URLFetcher* source) {
+ std::string token;
+ Status status = ParseResponse(source, &token);
+ RecordRegistrationStatusToUMA(status);
+ recorder_->RecordRegistrationResponse(
+ request_info_.app_id,
+ request_info_.sender_ids,
+ status);
+
+ if (ShouldRetryWithStatus(status)) {
+ if (retries_left_ > 0) {
+ recorder_->RecordRegistrationRetryRequested(
+ request_info_.app_id,
+ request_info_.sender_ids,
+ retries_left_);
+ RetryWithBackoff(true);
+ return;
+ }
+
+ status = REACHED_MAX_RETRIES;
+ recorder_->RecordRegistrationResponse(
+ request_info_.app_id,
+ request_info_.sender_ids,
+ status);
+ RecordRegistrationStatusToUMA(status);
+ }
+
+ if (status == SUCCESS) {
+ UMA_HISTOGRAM_COUNTS("GCM.RegistrationRetryCount",
+ backoff_entry_.failure_count());
+ UMA_HISTOGRAM_TIMES("GCM.RegistrationCompleteTime",
+ base::TimeTicks::Now() - request_start_time_);
+ }
+ callback_.Run(status, token);
+}
+
+} // namespace gcm
diff --git a/chromium/google_apis/gcm/engine/registration_request.h b/chromium/google_apis/gcm/engine/registration_request.h
new file mode 100644
index 00000000000..d41f2858f6b
--- /dev/null
+++ b/chromium/google_apis/gcm/engine/registration_request.h
@@ -0,0 +1,128 @@
+// 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 GOOGLE_APIS_GCM_ENGINE_REGISTRATION_REQUEST_H_
+#define GOOGLE_APIS_GCM_ENGINE_REGISTRATION_REQUEST_H_
+
+#include <map>
+#include <vector>
+
+#include "base/basictypes.h"
+#include "base/callback.h"
+#include "base/memory/ref_counted.h"
+#include "base/memory/scoped_ptr.h"
+#include "base/memory/weak_ptr.h"
+#include "base/time/time.h"
+#include "google_apis/gcm/base/gcm_export.h"
+#include "net/base/backoff_entry.h"
+#include "net/url_request/url_fetcher_delegate.h"
+#include "url/gurl.h"
+
+namespace net {
+class URLRequestContextGetter;
+}
+
+namespace gcm {
+
+class GCMStatsRecorder;
+
+// Registration request is used to obtain registration IDs for applications that
+// want to use GCM. It requires a set of parameters to be specified to identify
+// the Chrome instance, the user, the application and a set of senders that will
+// be authorized to address the application using it's assigned registration ID.
+class GCM_EXPORT RegistrationRequest : public net::URLFetcherDelegate {
+ public:
+ // This enum is also used in an UMA histogram (GCMRegistrationRequestStatus
+ // enum defined in tools/metrics/histograms/histogram.xml). Hence the entries
+ // here shouldn't be deleted or re-ordered and new ones should be added to
+ // the end.
+ enum Status {
+ SUCCESS, // Registration completed successfully.
+ INVALID_PARAMETERS, // One of request paramteres was invalid.
+ INVALID_SENDER, // One of the provided senders was invalid.
+ AUTHENTICATION_FAILED, // Authentication failed.
+ DEVICE_REGISTRATION_ERROR, // Chrome is not properly registered.
+ UNKNOWN_ERROR, // Unknown error.
+ URL_FETCHING_FAILED, // URL fetching failed.
+ HTTP_NOT_OK, // HTTP status was not OK.
+ RESPONSE_PARSING_FAILED, // Registration response parsing failed.
+ REACHED_MAX_RETRIES, // Reached maximum number of retries.
+ // NOTE: always keep this entry at the end. Add new status types only
+ // immediately above this line. Make sure to update the corresponding
+ // histogram enum accordingly.
+ STATUS_COUNT
+ };
+
+ // Callback completing the registration request.
+ typedef base::Callback<void(Status status,
+ const std::string& registration_id)>
+ RegistrationCallback;
+
+ // Details of the of the Registration Request. Only user's android ID and
+ // its serial number are optional and can be set to 0. All other parameters
+ // have to be specified to successfully complete the call.
+ struct GCM_EXPORT RequestInfo {
+ RequestInfo(uint64 android_id,
+ uint64 security_token,
+ const std::string& app_id,
+ const std::vector<std::string>& sender_ids);
+ ~RequestInfo();
+
+ // Android ID of the device.
+ uint64 android_id;
+ // Security token of the device.
+ uint64 security_token;
+ // Application ID.
+ std::string app_id;
+ // Certificate of the application.
+ std::string cert;
+ // List of IDs of senders. Allowed up to 100.
+ std::vector<std::string> sender_ids;
+ };
+
+ RegistrationRequest(
+ const GURL& registration_url,
+ const RequestInfo& request_info,
+ const net::BackoffEntry::Policy& backoff_policy,
+ const RegistrationCallback& callback,
+ int max_retry_count,
+ scoped_refptr<net::URLRequestContextGetter> request_context_getter,
+ GCMStatsRecorder* recorder);
+ virtual ~RegistrationRequest();
+
+ void Start();
+
+ // URLFetcherDelegate implementation.
+ virtual void OnURLFetchComplete(const net::URLFetcher* source) OVERRIDE;
+
+ private:
+ // Schedules a retry attempt, informs the backoff of a previous request's
+ // failure, when |update_backoff| is true.
+ void RetryWithBackoff(bool update_backoff);
+
+ // Parse the response returned by the URL fetcher into token, and returns the
+ // status.
+ Status ParseResponse(const net::URLFetcher* source, std::string* token);
+
+ RegistrationCallback callback_;
+ RequestInfo request_info_;
+ GURL registration_url_;
+
+ net::BackoffEntry backoff_entry_;
+ scoped_refptr<net::URLRequestContextGetter> request_context_getter_;
+ scoped_ptr<net::URLFetcher> url_fetcher_;
+ int retries_left_;
+ base::TimeTicks request_start_time_;
+
+ // Recorder that records GCM activities for debugging purpose. Not owned.
+ GCMStatsRecorder* recorder_;
+
+ base::WeakPtrFactory<RegistrationRequest> weak_ptr_factory_;
+
+ DISALLOW_COPY_AND_ASSIGN(RegistrationRequest);
+};
+
+} // namespace gcm
+
+#endif // GOOGLE_APIS_GCM_ENGINE_REGISTRATION_REQUEST_H_
diff --git a/chromium/google_apis/gcm/engine/registration_request_unittest.cc b/chromium/google_apis/gcm/engine/registration_request_unittest.cc
new file mode 100644
index 00000000000..c5b8c767aed
--- /dev/null
+++ b/chromium/google_apis/gcm/engine/registration_request_unittest.cc
@@ -0,0 +1,427 @@
+// 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 <map>
+#include <string>
+#include <vector>
+
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/string_tokenizer.h"
+#include "google_apis/gcm/engine/registration_request.h"
+#include "google_apis/gcm/monitoring/fake_gcm_stats_recorder.h"
+#include "net/url_request/test_url_fetcher_factory.h"
+#include "net/url_request/url_request_status.h"
+#include "net/url_request/url_request_test_util.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace gcm {
+
+namespace {
+const uint64 kAndroidId = 42UL;
+const char kAppId[] = "TestAppId";
+const char kDeveloperId[] = "Project1";
+const char kLoginHeader[] = "AidLogin";
+const char kRegistrationURL[] = "http://foo.bar/register";
+const uint64 kSecurityToken = 77UL;
+
+// Backoff policy for testing registration request.
+const net::BackoffEntry::Policy kDefaultBackoffPolicy = {
+ // Number of initial errors (in sequence) to ignore before applying
+ // exponential back-off rules.
+ // Explicitly set to 2 to skip the delay on the first retry, as we are not
+ // trying to test the backoff itself, but rather the fact that retry happens.
+ 2,
+
+ // Initial delay for exponential back-off in ms.
+ 15000, // 15 seconds.
+
+ // Factor by which the waiting time will be multiplied.
+ 2,
+
+ // Fuzzing percentage. ex: 10% will spread requests randomly
+ // between 90%-100% of the calculated time.
+ 0.5, // 50%.
+
+ // Maximum amount of time we are willing to delay our request in ms.
+ 1000 * 60 * 5, // 5 minutes.
+
+ // Time to keep an entry from being discarded even when it
+ // has no significant state, -1 to never discard.
+ -1,
+
+ // Don't use initial delay unless the last request was an error.
+ false,
+};
+
+} // namespace
+
+class RegistrationRequestTest : public testing::Test {
+ public:
+ RegistrationRequestTest();
+ virtual ~RegistrationRequestTest();
+
+ void RegistrationCallback(RegistrationRequest::Status status,
+ const std::string& registration_id);
+
+ void CreateRequest(const std::string& sender_ids);
+ void SetResponseStatusAndString(net::HttpStatusCode status_code,
+ const std::string& response_body);
+ void CompleteFetch();
+ void set_max_retry_count(int max_retry_count) {
+ max_retry_count_ = max_retry_count;
+ }
+
+ protected:
+ int max_retry_count_;
+ RegistrationRequest::Status status_;
+ std::string registration_id_;
+ bool callback_called_;
+ std::map<std::string, std::string> extras_;
+ scoped_ptr<RegistrationRequest> request_;
+ base::MessageLoop message_loop_;
+ net::TestURLFetcherFactory url_fetcher_factory_;
+ scoped_refptr<net::TestURLRequestContextGetter> url_request_context_getter_;
+ FakeGCMStatsRecorder recorder_;
+};
+
+RegistrationRequestTest::RegistrationRequestTest()
+ : max_retry_count_(2),
+ status_(RegistrationRequest::SUCCESS),
+ callback_called_(false),
+ url_request_context_getter_(new net::TestURLRequestContextGetter(
+ message_loop_.message_loop_proxy())) {}
+
+RegistrationRequestTest::~RegistrationRequestTest() {}
+
+void RegistrationRequestTest::RegistrationCallback(
+ RegistrationRequest::Status status,
+ const std::string& registration_id) {
+ status_ = status;
+ registration_id_ = registration_id;
+ callback_called_ = true;
+}
+
+void RegistrationRequestTest::CreateRequest(const std::string& sender_ids) {
+ std::vector<std::string> senders;
+ base::StringTokenizer tokenizer(sender_ids, ",");
+ while (tokenizer.GetNext())
+ senders.push_back(tokenizer.token());
+
+ request_.reset(new RegistrationRequest(
+ GURL(kRegistrationURL),
+ RegistrationRequest::RequestInfo(kAndroidId,
+ kSecurityToken,
+ kAppId,
+ senders),
+ kDefaultBackoffPolicy,
+ base::Bind(&RegistrationRequestTest::RegistrationCallback,
+ base::Unretained(this)),
+ max_retry_count_,
+ url_request_context_getter_.get(),
+ &recorder_));
+}
+
+void RegistrationRequestTest::SetResponseStatusAndString(
+ net::HttpStatusCode status_code,
+ const std::string& response_body) {
+ net::TestURLFetcher* fetcher = url_fetcher_factory_.GetFetcherByID(0);
+ ASSERT_TRUE(fetcher);
+ fetcher->set_response_code(status_code);
+ fetcher->SetResponseString(response_body);
+}
+
+void RegistrationRequestTest::CompleteFetch() {
+ registration_id_.clear();
+ status_ = RegistrationRequest::SUCCESS;
+ callback_called_ = false;
+
+ net::TestURLFetcher* fetcher = url_fetcher_factory_.GetFetcherByID(0);
+ ASSERT_TRUE(fetcher);
+ fetcher->delegate()->OnURLFetchComplete(fetcher);
+}
+
+TEST_F(RegistrationRequestTest, RequestSuccessful) {
+ set_max_retry_count(0);
+ CreateRequest("sender1,sender2");
+ request_->Start();
+
+ SetResponseStatusAndString(net::HTTP_OK, "token=2501");
+ CompleteFetch();
+
+ EXPECT_TRUE(callback_called_);
+ EXPECT_EQ(RegistrationRequest::SUCCESS, status_);
+ EXPECT_EQ("2501", registration_id_);
+}
+
+TEST_F(RegistrationRequestTest, RequestDataAndURL) {
+ CreateRequest(kDeveloperId);
+ request_->Start();
+
+ // Get data sent by request.
+ net::TestURLFetcher* fetcher = url_fetcher_factory_.GetFetcherByID(0);
+ ASSERT_TRUE(fetcher);
+
+ EXPECT_EQ(GURL(kRegistrationURL), fetcher->GetOriginalURL());
+
+ // Verify that authorization header was put together properly.
+ net::HttpRequestHeaders headers;
+ fetcher->GetExtraRequestHeaders(&headers);
+ std::string auth_header;
+ headers.GetHeader(net::HttpRequestHeaders::kAuthorization, &auth_header);
+ base::StringTokenizer auth_tokenizer(auth_header, " :");
+ ASSERT_TRUE(auth_tokenizer.GetNext());
+ EXPECT_EQ(kLoginHeader, auth_tokenizer.token());
+ ASSERT_TRUE(auth_tokenizer.GetNext());
+ EXPECT_EQ(base::Uint64ToString(kAndroidId), auth_tokenizer.token());
+ ASSERT_TRUE(auth_tokenizer.GetNext());
+ EXPECT_EQ(base::Uint64ToString(kSecurityToken), auth_tokenizer.token());
+
+ std::map<std::string, std::string> expected_pairs;
+ expected_pairs["app"] = kAppId;
+ expected_pairs["sender"] = kDeveloperId;
+ expected_pairs["device"] = base::Uint64ToString(kAndroidId);
+
+ // Verify data was formatted properly.
+ std::string upload_data = fetcher->upload_data();
+ base::StringTokenizer data_tokenizer(upload_data, "&=");
+ while (data_tokenizer.GetNext()) {
+ std::map<std::string, std::string>::iterator iter =
+ expected_pairs.find(data_tokenizer.token());
+ ASSERT_TRUE(iter != expected_pairs.end());
+ ASSERT_TRUE(data_tokenizer.GetNext());
+ EXPECT_EQ(iter->second, data_tokenizer.token());
+ // Ensure that none of the keys appears twice.
+ expected_pairs.erase(iter);
+ }
+
+ EXPECT_EQ(0UL, expected_pairs.size());
+}
+
+TEST_F(RegistrationRequestTest, RequestRegistrationWithMultipleSenderIds) {
+ CreateRequest("sender1,sender2@gmail.com");
+ request_->Start();
+
+ net::TestURLFetcher* fetcher = url_fetcher_factory_.GetFetcherByID(0);
+ ASSERT_TRUE(fetcher);
+
+ // Verify data was formatted properly.
+ std::string upload_data = fetcher->upload_data();
+ base::StringTokenizer data_tokenizer(upload_data, "&=");
+
+ // Skip all tokens until you hit entry for senders.
+ while (data_tokenizer.GetNext() && data_tokenizer.token() != "sender")
+ continue;
+
+ ASSERT_TRUE(data_tokenizer.GetNext());
+ std::string senders(net::UnescapeURLComponent(data_tokenizer.token(),
+ net::UnescapeRule::URL_SPECIAL_CHARS));
+ base::StringTokenizer sender_tokenizer(senders, ",");
+ ASSERT_TRUE(sender_tokenizer.GetNext());
+ EXPECT_EQ("sender1", sender_tokenizer.token());
+ ASSERT_TRUE(sender_tokenizer.GetNext());
+ EXPECT_EQ("sender2@gmail.com", sender_tokenizer.token());
+}
+
+TEST_F(RegistrationRequestTest, ResponseParsing) {
+ CreateRequest("sender1,sender2");
+ request_->Start();
+
+ SetResponseStatusAndString(net::HTTP_OK, "token=2501");
+ CompleteFetch();
+
+ EXPECT_TRUE(callback_called_);
+ EXPECT_EQ(RegistrationRequest::SUCCESS, status_);
+ EXPECT_EQ("2501", registration_id_);
+}
+
+TEST_F(RegistrationRequestTest, ResponseHttpStatusNotOK) {
+ CreateRequest("sender1,sender2");
+ request_->Start();
+
+ SetResponseStatusAndString(net::HTTP_UNAUTHORIZED, "token=2501");
+ CompleteFetch();
+
+ EXPECT_FALSE(callback_called_);
+
+ SetResponseStatusAndString(net::HTTP_OK, "token=2501");
+ CompleteFetch();
+
+ EXPECT_TRUE(callback_called_);
+ EXPECT_EQ(RegistrationRequest::SUCCESS, status_);
+ EXPECT_EQ("2501", registration_id_);
+}
+
+TEST_F(RegistrationRequestTest, ResponseMissingRegistrationId) {
+ CreateRequest("sender1,sender2");
+ request_->Start();
+
+ SetResponseStatusAndString(net::HTTP_OK, "");
+ CompleteFetch();
+
+ EXPECT_FALSE(callback_called_);
+
+ SetResponseStatusAndString(net::HTTP_OK, "some error in response");
+ CompleteFetch();
+
+ EXPECT_FALSE(callback_called_);
+
+ // Ensuring a retry happened and succeeds.
+ SetResponseStatusAndString(net::HTTP_OK, "token=2501");
+ CompleteFetch();
+
+ EXPECT_TRUE(callback_called_);
+ EXPECT_EQ(RegistrationRequest::SUCCESS, status_);
+ EXPECT_EQ("2501", registration_id_);
+}
+
+TEST_F(RegistrationRequestTest, ResponseDeviceRegistrationError) {
+ CreateRequest("sender1,sender2");
+ request_->Start();
+
+ SetResponseStatusAndString(net::HTTP_OK, "Error=PHONE_REGISTRATION_ERROR");
+ CompleteFetch();
+
+ EXPECT_FALSE(callback_called_);
+
+ // Ensuring a retry happened and succeeds.
+ SetResponseStatusAndString(net::HTTP_OK, "token=2501");
+ CompleteFetch();
+
+ EXPECT_TRUE(callback_called_);
+ EXPECT_EQ(RegistrationRequest::SUCCESS, status_);
+ EXPECT_EQ("2501", registration_id_);
+}
+
+TEST_F(RegistrationRequestTest, ResponseAuthenticationError) {
+ CreateRequest("sender1,sender2");
+ request_->Start();
+
+ SetResponseStatusAndString(net::HTTP_UNAUTHORIZED,
+ "Error=AUTHENTICATION_FAILED");
+ CompleteFetch();
+
+ EXPECT_FALSE(callback_called_);
+
+ // Ensuring a retry happened and succeeds.
+ SetResponseStatusAndString(net::HTTP_OK, "token=2501");
+ CompleteFetch();
+
+ EXPECT_TRUE(callback_called_);
+ EXPECT_EQ(RegistrationRequest::SUCCESS, status_);
+ EXPECT_EQ("2501", registration_id_);
+}
+
+TEST_F(RegistrationRequestTest, ResponseInvalidParameters) {
+ CreateRequest("sender1,sender2");
+ request_->Start();
+
+ SetResponseStatusAndString(net::HTTP_OK, "Error=INVALID_PARAMETERS");
+ CompleteFetch();
+
+ EXPECT_TRUE(callback_called_);
+ EXPECT_EQ(RegistrationRequest::INVALID_PARAMETERS, status_);
+ EXPECT_EQ(std::string(), registration_id_);
+}
+
+TEST_F(RegistrationRequestTest, ResponseInvalidSender) {
+ CreateRequest("sender1,sender2");
+ request_->Start();
+
+ SetResponseStatusAndString(net::HTTP_OK, "Error=INVALID_SENDER");
+ CompleteFetch();
+
+ EXPECT_TRUE(callback_called_);
+ EXPECT_EQ(RegistrationRequest::INVALID_SENDER, status_);
+ EXPECT_EQ(std::string(), registration_id_);
+}
+
+TEST_F(RegistrationRequestTest, ResponseInvalidSenderBadRequest) {
+ CreateRequest("sender1");
+ request_->Start();
+
+ SetResponseStatusAndString(net::HTTP_BAD_REQUEST, "Error=INVALID_SENDER");
+ CompleteFetch();
+
+ EXPECT_TRUE(callback_called_);
+ EXPECT_EQ(RegistrationRequest::INVALID_SENDER, status_);
+ EXPECT_EQ(std::string(), registration_id_);
+}
+
+TEST_F(RegistrationRequestTest, RequestNotSuccessful) {
+ CreateRequest("sender1,sender2");
+ request_->Start();
+
+ net::URLRequestStatus request_status(net::URLRequestStatus::FAILED, 1);
+ SetResponseStatusAndString(net::HTTP_OK, "token=2501");
+ net::TestURLFetcher* fetcher = url_fetcher_factory_.GetFetcherByID(0);
+ ASSERT_TRUE(fetcher);
+ fetcher->set_status(request_status);
+
+ CompleteFetch();
+
+ EXPECT_FALSE(callback_called_);
+
+ // Ensuring a retry happened and succeeded.
+ SetResponseStatusAndString(net::HTTP_OK, "token=2501");
+ CompleteFetch();
+
+ EXPECT_TRUE(callback_called_);
+ EXPECT_EQ(RegistrationRequest::SUCCESS, status_);
+ EXPECT_EQ("2501", registration_id_);
+}
+
+TEST_F(RegistrationRequestTest, ResponseHttpNotOk) {
+ CreateRequest("sender1,sender2");
+ request_->Start();
+
+ SetResponseStatusAndString(net::HTTP_GATEWAY_TIMEOUT, "token=2501");
+ CompleteFetch();
+
+ EXPECT_FALSE(callback_called_);
+
+ // Ensuring a retry happened and succeeded.
+ SetResponseStatusAndString(net::HTTP_OK, "token=2501");
+ CompleteFetch();
+
+ EXPECT_TRUE(callback_called_);
+ EXPECT_EQ(RegistrationRequest::SUCCESS, status_);
+ EXPECT_EQ("2501", registration_id_);
+}
+
+TEST_F(RegistrationRequestTest, MaximumAttemptsReachedWithZeroRetries) {
+ set_max_retry_count(0);
+ CreateRequest("sender1,sender2");
+ request_->Start();
+
+ SetResponseStatusAndString(net::HTTP_GATEWAY_TIMEOUT, "token=2501");
+ CompleteFetch();
+
+ EXPECT_TRUE(callback_called_);
+ EXPECT_EQ(RegistrationRequest::REACHED_MAX_RETRIES, status_);
+ EXPECT_EQ(std::string(), registration_id_);
+}
+
+TEST_F(RegistrationRequestTest, MaximumAttemptsReached) {
+ CreateRequest("sender1,sender2");
+ request_->Start();
+
+ SetResponseStatusAndString(net::HTTP_GATEWAY_TIMEOUT, "token=2501");
+ CompleteFetch();
+
+ EXPECT_FALSE(callback_called_);
+
+ SetResponseStatusAndString(net::HTTP_GATEWAY_TIMEOUT, "token=2501");
+ CompleteFetch();
+
+ EXPECT_FALSE(callback_called_);
+
+ SetResponseStatusAndString(net::HTTP_GATEWAY_TIMEOUT, "token=2501");
+ CompleteFetch();
+
+ EXPECT_TRUE(callback_called_);
+ EXPECT_EQ(RegistrationRequest::REACHED_MAX_RETRIES, status_);
+ EXPECT_EQ(std::string(), registration_id_);
+}
+
+} // namespace gcm
diff --git a/chromium/google_apis/gcm/engine/rmq_store.cc b/chromium/google_apis/gcm/engine/rmq_store.cc
deleted file mode 100644
index eaa6dcc8059..00000000000
--- a/chromium/google_apis/gcm/engine/rmq_store.cc
+++ /dev/null
@@ -1,491 +0,0 @@
-// Copyright 2013 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 "google_apis/gcm/engine/rmq_store.h"
-
-#include "base/basictypes.h"
-#include "base/bind.h"
-#include "base/callback.h"
-#include "base/files/file_path.h"
-#include "base/logging.h"
-#include "base/message_loop/message_loop_proxy.h"
-#include "base/sequenced_task_runner.h"
-#include "base/stl_util.h"
-#include "base/strings/string_number_conversions.h"
-#include "base/strings/string_piece.h"
-#include "base/tracked_objects.h"
-#include "components/webdata/encryptor/encryptor.h"
-#include "google_apis/gcm/base/mcs_message.h"
-#include "google_apis/gcm/base/mcs_util.h"
-#include "google_apis/gcm/protocol/mcs.pb.h"
-#include "third_party/leveldatabase/src/include/leveldb/db.h"
-
-namespace gcm {
-
-namespace {
-
-// ---- LevelDB keys. ----
-// Key for this device's android id.
-const char kDeviceAIDKey[] = "device_aid_key";
-// Key for this device's android security token.
-const char kDeviceTokenKey[] = "device_token_key";
-// Lowest lexicographically ordered incoming message key.
-// Used for prefixing messages.
-const char kIncomingMsgKeyStart[] = "incoming1-";
-// Key guaranteed to be higher than all incoming message keys.
-// Used for limiting iteration.
-const char kIncomingMsgKeyEnd[] = "incoming2-";
-// Lowest lexicographically ordered outgoing message key.
-// Used for prefixing outgoing messages.
-const char kOutgoingMsgKeyStart[] = "outgoing1-";
-// Key guaranteed to be higher than all outgoing message keys.
-// Used for limiting iteration.
-const char kOutgoingMsgKeyEnd[] = "outgoing2-";
-
-std::string MakeIncomingKey(const std::string& persistent_id) {
- return kIncomingMsgKeyStart + persistent_id;
-}
-
-std::string MakeOutgoingKey(const std::string& persistent_id) {
- return kOutgoingMsgKeyStart + persistent_id;
-}
-
-std::string ParseOutgoingKey(const std::string& key) {
- return key.substr(arraysize(kOutgoingMsgKeyStart) - 1);
-}
-
-leveldb::Slice MakeSlice(const base::StringPiece& s) {
- return leveldb::Slice(s.begin(), s.size());
-}
-
-} // namespace
-
-class RMQStore::Backend : public base::RefCountedThreadSafe<RMQStore::Backend> {
- public:
- Backend(const base::FilePath& path,
- scoped_refptr<base::SequencedTaskRunner> foreground_runner);
-
- // Blocking implementations of RMQStore methods.
- void Load(const LoadCallback& callback);
- void Destroy(const UpdateCallback& callback);
- void SetDeviceCredentials(uint64 device_android_id,
- uint64 device_security_token,
- const UpdateCallback& callback);
- void AddIncomingMessage(const std::string& persistent_id,
- const UpdateCallback& callback);
- void RemoveIncomingMessages(const PersistentIdList& persistent_ids,
- const UpdateCallback& callback);
- void AddOutgoingMessage(const std::string& persistent_id,
- const MCSMessage& message,
- const UpdateCallback& callback);
- void RemoveOutgoingMessages(const PersistentIdList& persistent_ids,
- const UpdateCallback& callback);
-
- private:
- friend class base::RefCountedThreadSafe<Backend>;
- ~Backend();
-
- bool LoadDeviceCredentials(uint64* android_id, uint64* security_token);
- bool LoadIncomingMessages(std::vector<std::string>* incoming_messages);
- bool LoadOutgoingMessages(
- std::map<std::string, google::protobuf::MessageLite*>* outgoing_messages);
-
- const base::FilePath path_;
- scoped_refptr<base::SequencedTaskRunner> foreground_task_runner_;
-
- scoped_ptr<leveldb::DB> db_;
-};
-
-RMQStore::Backend::Backend(
- const base::FilePath& path,
- scoped_refptr<base::SequencedTaskRunner> foreground_task_runner)
- : path_(path),
- foreground_task_runner_(foreground_task_runner) {
-}
-
-RMQStore::Backend::~Backend() {
-}
-
-void RMQStore::Backend::Load(const LoadCallback& callback) {
- LoadResult result;
-
- leveldb::Options options;
- options.create_if_missing = true;
- leveldb::DB* db;
- leveldb::Status status = leveldb::DB::Open(options,
- path_.AsUTF8Unsafe(),
- &db);
- if (!status.ok()) {
- LOG(ERROR) << "Failed to open database " << path_.value()
- << ": " << status.ToString();
- foreground_task_runner_->PostTask(FROM_HERE,
- base::Bind(callback, result));
- return;
- }
- db_.reset(db);
-
- if (!LoadDeviceCredentials(&result.device_android_id,
- &result.device_security_token) ||
- !LoadIncomingMessages(&result.incoming_messages) ||
- !LoadOutgoingMessages(&result.outgoing_messages)) {
- result.device_android_id = 0;
- result.device_security_token = 0;
- result.incoming_messages.clear();
- STLDeleteContainerPairSecondPointers(result.outgoing_messages.begin(),
- result.outgoing_messages.end());
- result.outgoing_messages.clear();
- foreground_task_runner_->PostTask(FROM_HERE,
- base::Bind(callback, result));
- return;
- }
-
- DVLOG(1) << "Succeeded in loading " << result.incoming_messages.size()
- << " unacknowledged incoming messages and "
- << result.outgoing_messages.size()
- << " unacknowledged outgoing messages.";
- result.success = true;
- foreground_task_runner_->PostTask(FROM_HERE,
- base::Bind(callback, result));
- return;
-}
-
-void RMQStore::Backend::Destroy(const UpdateCallback& callback) {
- DVLOG(1) << "Destroying RMQ store.";
- const leveldb::Status s =
- leveldb::DestroyDB(path_.AsUTF8Unsafe(),
- leveldb::Options());
- if (s.ok()) {
- foreground_task_runner_->PostTask(FROM_HERE,
- base::Bind(callback, true));
- return;
- }
- LOG(ERROR) << "Destroy failed.";
- foreground_task_runner_->PostTask(FROM_HERE,
- base::Bind(callback, false));
-}
-
-void RMQStore::Backend::SetDeviceCredentials(uint64 device_android_id,
- uint64 device_security_token,
- const UpdateCallback& callback) {
- DVLOG(1) << "Saving device credentials with AID " << device_android_id;
- leveldb::WriteOptions write_options;
- write_options.sync = true;
-
- std::string encrypted_token;
- Encryptor::EncryptString(base::Uint64ToString(device_security_token),
- &encrypted_token);
- leveldb::Status s =
- db_->Put(write_options,
- MakeSlice(kDeviceAIDKey),
- MakeSlice(base::Uint64ToString(device_android_id)));
- if (s.ok()) {
- s = db_->Put(write_options,
- MakeSlice(kDeviceTokenKey),
- MakeSlice(encrypted_token));
- }
- if (s.ok()) {
- foreground_task_runner_->PostTask(FROM_HERE,
- base::Bind(callback, true));
- return;
- }
- LOG(ERROR) << "LevelDB put failed: " << s.ToString();
- foreground_task_runner_->PostTask(FROM_HERE,
- base::Bind(callback, false));
-}
-
-void RMQStore::Backend::AddIncomingMessage(const std::string& persistent_id,
- const UpdateCallback& callback) {
- DVLOG(1) << "Saving incoming message with id " << persistent_id;
- leveldb::WriteOptions write_options;
- write_options.sync = true;
-
- const leveldb::Status s =
- db_->Put(write_options,
- MakeSlice(MakeIncomingKey(persistent_id)),
- MakeSlice(persistent_id));
- if (s.ok()) {
- foreground_task_runner_->PostTask(FROM_HERE,
- base::Bind(callback, true));
- return;
- }
- LOG(ERROR) << "LevelDB put failed: " << s.ToString();
- foreground_task_runner_->PostTask(FROM_HERE,
- base::Bind(callback, false));
-}
-
-void RMQStore::Backend::RemoveIncomingMessages(
- const PersistentIdList& persistent_ids,
- const UpdateCallback& callback) {
- leveldb::WriteOptions write_options;
- write_options.sync = true;
-
- leveldb::Status s;
- for (PersistentIdList::const_iterator iter = persistent_ids.begin();
- iter != persistent_ids.end(); ++iter){
- DVLOG(1) << "Removing incoming message with id " << *iter;
- s = db_->Delete(write_options,
- MakeSlice(MakeIncomingKey(*iter)));
- if (!s.ok())
- break;
- }
- if (s.ok()) {
- foreground_task_runner_->PostTask(FROM_HERE,
- base::Bind(callback, true));
- return;
- }
- LOG(ERROR) << "LevelDB remove failed: " << s.ToString();
- foreground_task_runner_->PostTask(FROM_HERE,
- base::Bind(callback, false));
-}
-
-void RMQStore::Backend::AddOutgoingMessage(
- const std::string& persistent_id,
- const MCSMessage& message,
- const UpdateCallback& callback) {
- DVLOG(1) << "Saving outgoing message with id " << persistent_id;
- leveldb::WriteOptions write_options;
- write_options.sync = true;
-
- std::string data = static_cast<char>(message.tag()) +
- message.SerializeAsString();
- const leveldb::Status s =
- db_->Put(write_options,
- MakeSlice(MakeOutgoingKey(persistent_id)),
- MakeSlice(data));
- if (s.ok()) {
- foreground_task_runner_->PostTask(FROM_HERE,
- base::Bind(callback, true));
- return;
- }
- LOG(ERROR) << "LevelDB put failed: " << s.ToString();
- foreground_task_runner_->PostTask(FROM_HERE,
- base::Bind(callback, false));
-
-}
-
-void RMQStore::Backend::RemoveOutgoingMessages(
- const PersistentIdList& persistent_ids,
- const UpdateCallback& callback) {
- leveldb::WriteOptions write_options;
- write_options.sync = true;
-
- leveldb::Status s;
- for (PersistentIdList::const_iterator iter = persistent_ids.begin();
- iter != persistent_ids.end(); ++iter){
- DVLOG(1) << "Removing outgoing message with id " << *iter;
- s = db_->Delete(write_options,
- MakeSlice(MakeOutgoingKey(*iter)));
- if (!s.ok())
- break;
- }
- if (s.ok()) {
- foreground_task_runner_->PostTask(FROM_HERE,
- base::Bind(callback, true));
- return;
- }
- LOG(ERROR) << "LevelDB remove failed: " << s.ToString();
- foreground_task_runner_->PostTask(FROM_HERE,
- base::Bind(callback, false));
-}
-
-bool RMQStore::Backend::LoadDeviceCredentials(uint64* android_id,
- uint64* security_token) {
- leveldb::ReadOptions read_options;
- read_options.verify_checksums = true;
-
- std::string result;
- leveldb::Status s = db_->Get(read_options,
- MakeSlice(kDeviceAIDKey),
- &result);
- if (s.ok()) {
- if (!base::StringToUint64(result, android_id)) {
- LOG(ERROR) << "Failed to restore device id.";
- return false;
- }
- result.clear();
- s = db_->Get(read_options,
- MakeSlice(kDeviceTokenKey),
- &result);
- }
- if (s.ok()) {
- std::string decrypted_token;
- Encryptor::DecryptString(result, &decrypted_token);
- if (!base::StringToUint64(decrypted_token, security_token)) {
- LOG(ERROR) << "Failed to restore security token.";
- return false;
- }
- return true;
- }
-
- if (s.IsNotFound()) {
- DVLOG(1) << "No credentials found.";
- return true;
- }
-
- LOG(ERROR) << "Error reading credentials from store.";
- return false;
-}
-
-bool RMQStore::Backend::LoadIncomingMessages(
- std::vector<std::string>* incoming_messages) {
- leveldb::ReadOptions read_options;
- read_options.verify_checksums = true;
-
- scoped_ptr<leveldb::Iterator> iter(db_->NewIterator(read_options));
- for (iter->Seek(MakeSlice(kIncomingMsgKeyStart));
- iter->Valid() && iter->key().ToString() < kIncomingMsgKeyEnd;
- iter->Next()) {
- leveldb::Slice s = iter->value();
- if (s.empty()) {
- LOG(ERROR) << "Error reading incoming message with key "
- << iter->key().ToString();
- return false;
- }
- DVLOG(1) << "Found incoming message with id " << s.ToString();
- incoming_messages->push_back(s.ToString());
- }
-
- return true;
-}
-
-bool RMQStore::Backend::LoadOutgoingMessages(
- std::map<std::string, google::protobuf::MessageLite*>*
- outgoing_messages) {
- leveldb::ReadOptions read_options;
- read_options.verify_checksums = true;
-
- scoped_ptr<leveldb::Iterator> iter(db_->NewIterator(read_options));
- for (iter->Seek(MakeSlice(kOutgoingMsgKeyStart));
- iter->Valid() && iter->key().ToString() < kOutgoingMsgKeyEnd;
- iter->Next()) {
- leveldb::Slice s = iter->value();
- if (s.size() <= 1) {
- LOG(ERROR) << "Error reading incoming message with key " << s.ToString();
- return false;
- }
- uint8 tag = iter->value().data()[0];
- std::string id = ParseOutgoingKey(iter->key().ToString());
- scoped_ptr<google::protobuf::MessageLite> message(
- BuildProtobufFromTag(tag));
- if (!message.get() ||
- !message->ParseFromString(iter->value().ToString().substr(1))) {
- LOG(ERROR) << "Failed to parse outgoing message with id "
- << id << " and tag " << tag;
- return false;
- }
- DVLOG(1) << "Found outgoing message with id " << id << " of type "
- << base::IntToString(tag);
- (*outgoing_messages)[id] = message.release();
- }
-
- return true;
-}
-
-RMQStore::LoadResult::LoadResult()
- : success(false),
- device_android_id(0),
- device_security_token(0) {
-}
-RMQStore::LoadResult::~LoadResult() {}
-
-RMQStore::RMQStore(
- const base::FilePath& path,
- scoped_refptr<base::SequencedTaskRunner> blocking_task_runner)
- : backend_(new Backend(path, base::MessageLoopProxy::current())),
- blocking_task_runner_(blocking_task_runner) {
-}
-
-RMQStore::~RMQStore() {
-}
-
-void RMQStore::Load(const LoadCallback& callback) {
- blocking_task_runner_->PostTask(FROM_HERE,
- base::Bind(&RMQStore::Backend::Load,
- backend_,
- callback));
-}
-
-void RMQStore::Destroy(const UpdateCallback& callback) {
- blocking_task_runner_->PostTask(
- FROM_HERE,
- base::Bind(&RMQStore::Backend::Destroy,
- backend_,
- callback));
-}
-
-void RMQStore::SetDeviceCredentials(uint64 device_android_id,
- uint64 device_security_token,
- const UpdateCallback& callback) {
- blocking_task_runner_->PostTask(
- FROM_HERE,
- base::Bind(&RMQStore::Backend::SetDeviceCredentials,
- backend_,
- device_android_id,
- device_security_token,
- callback));
-}
-
-void RMQStore::AddIncomingMessage(const std::string& persistent_id,
- const UpdateCallback& callback) {
- blocking_task_runner_->PostTask(
- FROM_HERE,
- base::Bind(&RMQStore::Backend::AddIncomingMessage,
- backend_,
- persistent_id,
- callback));
-}
-
-void RMQStore::RemoveIncomingMessage(const std::string& persistent_id,
- const UpdateCallback& callback) {
- blocking_task_runner_->PostTask(
- FROM_HERE,
- base::Bind(&RMQStore::Backend::RemoveIncomingMessages,
- backend_,
- PersistentIdList(1, persistent_id),
- callback));
-}
-
-void RMQStore::RemoveIncomingMessages(const PersistentIdList& persistent_ids,
- const UpdateCallback& callback) {
- blocking_task_runner_->PostTask(
- FROM_HERE,
- base::Bind(&RMQStore::Backend::RemoveIncomingMessages,
- backend_,
- persistent_ids,
- callback));
-}
-
-void RMQStore::AddOutgoingMessage(const std::string& persistent_id,
- const MCSMessage& message,
- const UpdateCallback& callback) {
- blocking_task_runner_->PostTask(
- FROM_HERE,
- base::Bind(&RMQStore::Backend::AddOutgoingMessage,
- backend_,
- persistent_id,
- message,
- callback));
-}
-
-void RMQStore::RemoveOutgoingMessage(const std::string& persistent_id,
- const UpdateCallback& callback) {
- blocking_task_runner_->PostTask(
- FROM_HERE,
- base::Bind(&RMQStore::Backend::RemoveOutgoingMessages,
- backend_,
- PersistentIdList(1, persistent_id),
- callback));
-}
-
-void RMQStore::RemoveOutgoingMessages(const PersistentIdList& persistent_ids,
- const UpdateCallback& callback) {
- blocking_task_runner_->PostTask(
- FROM_HERE,
- base::Bind(&RMQStore::Backend::RemoveOutgoingMessages,
- backend_,
- persistent_ids,
- callback));
-}
-
-} // namespace gcm
diff --git a/chromium/google_apis/gcm/engine/rmq_store.h b/chromium/google_apis/gcm/engine/rmq_store.h
deleted file mode 100644
index d3762a199c7..00000000000
--- a/chromium/google_apis/gcm/engine/rmq_store.h
+++ /dev/null
@@ -1,102 +0,0 @@
-// Copyright 2013 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 GOOGLE_APIS_GCM_ENGINE_RMQ_STORE_H_
-#define GOOGLE_APIS_GCM_ENGINE_RMQ_STORE_H_
-
-#include <map>
-#include <string>
-#include <vector>
-
-#include "base/basictypes.h"
-#include "base/callback_forward.h"
-#include "base/memory/ref_counted.h"
-#include "google_apis/gcm/base/gcm_export.h"
-
-namespace base {
-class FilePath;
-class SequencedTaskRunner;
-} // namespace base
-
-namespace google {
-namespace protobuf {
-class MessageLite;
-} // namespace protobuf
-} // namespace google
-
-namespace gcm {
-
-class MCSMessage;
-
-// A Reliable Message Queue store.
-// Will perform all blocking operations on the blocking task runner, and will
-// post all callbacks to the thread on which the RMQStore is created.
-class GCM_EXPORT RMQStore {
- public:
- // Container for Load(..) results.
- struct GCM_EXPORT LoadResult {
- LoadResult();
- ~LoadResult();
-
- bool success;
- uint64 device_android_id;
- uint64 device_security_token;
- std::vector<std::string> incoming_messages;
- std::map<std::string, google::protobuf::MessageLite*>
- outgoing_messages;
- };
-
- typedef std::vector<std::string> PersistentIdList;
- // Note: callee receives ownership of |outgoing_messages|' values.
- typedef base::Callback<void(const LoadResult& result)> LoadCallback;
- typedef base::Callback<void(bool success)> UpdateCallback;
-
- RMQStore(const base::FilePath& path,
- scoped_refptr<base::SequencedTaskRunner> blocking_task_runner);
- ~RMQStore();
-
- // Load the directory and pass the initial state back to caller.
- void Load(const LoadCallback& callback);
-
- // Clears the RMQ store of all data and destroys any LevelDB files associated
- // with this store.
- // WARNING: this will permanently destroy any pending outgoing messages
- // and require the device to re-create credentials.
- void Destroy(const UpdateCallback& callback);
-
- // Sets this device's messaging credentials.
- void SetDeviceCredentials(uint64 device_android_id,
- uint64 device_security_token,
- const UpdateCallback& callback);
-
- // Unacknowledged incoming message handling.
- void AddIncomingMessage(const std::string& persistent_id,
- const UpdateCallback& callback);
- void RemoveIncomingMessage(const std::string& persistent_id,
- const UpdateCallback& callback);
- void RemoveIncomingMessages(const PersistentIdList& persistent_ids,
- const UpdateCallback& callback);
-
- // Unacknowledged outgoing messages handling.
- // TODO(zea): implement per-app limits on the number of outgoing messages.
- void AddOutgoingMessage(const std::string& persistent_id,
- const MCSMessage& message,
- const UpdateCallback& callback);
- void RemoveOutgoingMessage(const std::string& persistent_id,
- const UpdateCallback& callback);
- void RemoveOutgoingMessages(const PersistentIdList& persistent_ids,
- const UpdateCallback& callback);
-
- private:
- class Backend;
-
- scoped_refptr<Backend> backend_;
- scoped_refptr<base::SequencedTaskRunner> blocking_task_runner_;
-
- DISALLOW_COPY_AND_ASSIGN(RMQStore);
-};
-
-} // namespace gcm
-
-#endif // GOOGLE_APIS_GCM_ENGINE_RMQ_STORE_H_
diff --git a/chromium/google_apis/gcm/engine/rmq_store_unittest.cc b/chromium/google_apis/gcm/engine/rmq_store_unittest.cc
deleted file mode 100644
index 1fd55bcf14b..00000000000
--- a/chromium/google_apis/gcm/engine/rmq_store_unittest.cc
+++ /dev/null
@@ -1,303 +0,0 @@
-// Copyright 2013 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 "google_apis/gcm/engine/rmq_store.h"
-
-#include <string>
-#include <vector>
-
-#include "base/bind.h"
-#include "base/files/file_path.h"
-#include "base/files/scoped_temp_dir.h"
-#include "base/memory/scoped_ptr.h"
-#include "base/message_loop/message_loop.h"
-#include "base/run_loop.h"
-#include "base/strings/string_number_conversions.h"
-#include "components/webdata/encryptor/encryptor.h"
-#include "google_apis/gcm/base/mcs_message.h"
-#include "google_apis/gcm/base/mcs_util.h"
-#include "google_apis/gcm/protocol/mcs.pb.h"
-#include "testing/gtest/include/gtest/gtest.h"
-
-namespace gcm {
-
-namespace {
-
-// Number of persistent ids to use in tests.
-const int kNumPersistentIds = 10;
-
-const uint64 kDeviceId = 22;
-const uint64 kDeviceToken = 55;
-
-class RMQStoreTest : public testing::Test {
- public:
- RMQStoreTest();
- virtual ~RMQStoreTest();
-
- scoped_ptr<RMQStore> BuildRMQStore();
-
- std::string GetNextPersistentId();
-
- void PumpLoop();
-
- void LoadCallback(RMQStore::LoadResult* result_dst,
- const RMQStore::LoadResult& result);
- void UpdateCallback(bool success);
-
- private:
- base::MessageLoop message_loop_;
- base::ScopedTempDir temp_directory_;
- scoped_ptr<base::RunLoop> run_loop_;
-};
-
-RMQStoreTest::RMQStoreTest() {
- EXPECT_TRUE(temp_directory_.CreateUniqueTempDir());
- run_loop_.reset(new base::RunLoop());
-
- // On OSX, prevent the Keychain permissions popup during unit tests.
- #if defined(OS_MACOSX)
- Encryptor::UseMockKeychain(true);
- #endif
-}
-
-RMQStoreTest::~RMQStoreTest() {
-}
-
-scoped_ptr<RMQStore> RMQStoreTest::BuildRMQStore() {
- return scoped_ptr<RMQStore>(new RMQStore(temp_directory_.path(),
- message_loop_.message_loop_proxy()));
-}
-
-std::string RMQStoreTest::GetNextPersistentId() {
- return base::Uint64ToString(base::Time::Now().ToInternalValue());
-}
-
-void RMQStoreTest::PumpLoop() {
- message_loop_.RunUntilIdle();
-}
-
-void RMQStoreTest::LoadCallback(RMQStore::LoadResult* result_dst,
- const RMQStore::LoadResult& result) {
- ASSERT_TRUE(result.success);
- *result_dst = result;
- run_loop_->Quit();
- run_loop_.reset(new base::RunLoop());
-}
-
-void RMQStoreTest::UpdateCallback(bool success) {
- ASSERT_TRUE(success);
-}
-
-// Verify creating a new database and loading it.
-TEST_F(RMQStoreTest, LoadNew) {
- scoped_ptr<RMQStore> rmq_store(BuildRMQStore());
- RMQStore::LoadResult load_result;
- rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback,
- base::Unretained(this),
- &load_result));
- PumpLoop();
-
- ASSERT_EQ(0U, load_result.device_android_id);
- ASSERT_EQ(0U, load_result.device_security_token);
- ASSERT_TRUE(load_result.incoming_messages.empty());
- ASSERT_TRUE(load_result.outgoing_messages.empty());
-}
-
-TEST_F(RMQStoreTest, DeviceCredentials) {
- scoped_ptr<RMQStore> rmq_store(BuildRMQStore());
- RMQStore::LoadResult load_result;
- rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback,
- base::Unretained(this),
- &load_result));
- PumpLoop();
-
- rmq_store->SetDeviceCredentials(kDeviceId,
- kDeviceToken,
- base::Bind(&RMQStoreTest::UpdateCallback,
- base::Unretained(this)));
- PumpLoop();
-
- rmq_store = BuildRMQStore().Pass();
- rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback,
- base::Unretained(this),
- &load_result));
- PumpLoop();
-
- ASSERT_EQ(kDeviceId, load_result.device_android_id);
- ASSERT_EQ(kDeviceToken, load_result.device_security_token);
-}
-
-// Verify saving some incoming messages, reopening the directory, and then
-// removing those incoming messages.
-TEST_F(RMQStoreTest, IncomingMessages) {
- scoped_ptr<RMQStore> rmq_store(BuildRMQStore());
- RMQStore::LoadResult load_result;
- rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback,
- base::Unretained(this),
- &load_result));
- PumpLoop();
-
- std::vector<std::string> persistent_ids;
- for (int i = 0; i < kNumPersistentIds; ++i) {
- persistent_ids.push_back(GetNextPersistentId());
- rmq_store->AddIncomingMessage(persistent_ids.back(),
- base::Bind(&RMQStoreTest::UpdateCallback,
- base::Unretained(this)));
- PumpLoop();
- }
-
- rmq_store = BuildRMQStore().Pass();
- rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback,
- base::Unretained(this),
- &load_result));
- PumpLoop();
-
- ASSERT_EQ(persistent_ids, load_result.incoming_messages);
- ASSERT_TRUE(load_result.outgoing_messages.empty());
-
- rmq_store->RemoveIncomingMessages(persistent_ids,
- base::Bind(&RMQStoreTest::UpdateCallback,
- base::Unretained(this)));
- PumpLoop();
-
- rmq_store = BuildRMQStore().Pass();
- load_result.incoming_messages.clear();
- rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback,
- base::Unretained(this),
- &load_result));
- PumpLoop();
-
- ASSERT_TRUE(load_result.incoming_messages.empty());
- ASSERT_TRUE(load_result.outgoing_messages.empty());
-}
-
-// Verify saving some outgoing messages, reopening the directory, and then
-// removing those outgoing messages.
-TEST_F(RMQStoreTest, OutgoingMessages) {
- scoped_ptr<RMQStore> rmq_store(BuildRMQStore());
- RMQStore::LoadResult load_result;
- rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback,
- base::Unretained(this),
- &load_result));
- PumpLoop();
-
- std::vector<std::string> persistent_ids;
- const int kNumPersistentIds = 10;
- for (int i = 0; i < kNumPersistentIds; ++i) {
- persistent_ids.push_back(GetNextPersistentId());
- mcs_proto::DataMessageStanza message;
- message.set_from(persistent_ids.back());
- message.set_category(persistent_ids.back());
- rmq_store->AddOutgoingMessage(persistent_ids.back(),
- MCSMessage(message),
- base::Bind(&RMQStoreTest::UpdateCallback,
- base::Unretained(this)));
- PumpLoop();
- }
-
- rmq_store = BuildRMQStore().Pass();
- rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback,
- base::Unretained(this),
- &load_result));
- PumpLoop();
-
- ASSERT_TRUE(load_result.incoming_messages.empty());
- ASSERT_EQ(load_result.outgoing_messages.size(), persistent_ids.size());
- for (int i =0 ; i < kNumPersistentIds; ++i) {
- std::string id = persistent_ids[i];
- ASSERT_TRUE(load_result.outgoing_messages[id]);
- const mcs_proto::DataMessageStanza* message =
- reinterpret_cast<mcs_proto::DataMessageStanza *>(
- load_result.outgoing_messages[id]);
- ASSERT_EQ(message->from(), id);
- ASSERT_EQ(message->category(), id);
- }
-
- rmq_store->RemoveOutgoingMessages(persistent_ids,
- base::Bind(&RMQStoreTest::UpdateCallback,
- base::Unretained(this)));
- PumpLoop();
-
- rmq_store = BuildRMQStore().Pass();
- load_result.outgoing_messages.clear();
- rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback,
- base::Unretained(this),
- &load_result));
- PumpLoop();
-
- ASSERT_TRUE(load_result.incoming_messages.empty());
- ASSERT_TRUE(load_result.outgoing_messages.empty());
-}
-
-// Verify incoming and outgoing messages don't conflict.
-TEST_F(RMQStoreTest, IncomingAndOutgoingMessages) {
- scoped_ptr<RMQStore> rmq_store(BuildRMQStore());
- RMQStore::LoadResult load_result;
- rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback,
- base::Unretained(this),
- &load_result));
- PumpLoop();
-
- std::vector<std::string> persistent_ids;
- const int kNumPersistentIds = 10;
- for (int i = 0; i < kNumPersistentIds; ++i) {
- persistent_ids.push_back(GetNextPersistentId());
- rmq_store->AddIncomingMessage(persistent_ids.back(),
- base::Bind(&RMQStoreTest::UpdateCallback,
- base::Unretained(this)));
- PumpLoop();
-
- mcs_proto::DataMessageStanza message;
- message.set_from(persistent_ids.back());
- message.set_category(persistent_ids.back());
- rmq_store->AddOutgoingMessage(persistent_ids.back(),
- MCSMessage(message),
- base::Bind(&RMQStoreTest::UpdateCallback,
- base::Unretained(this)));
- PumpLoop();
- }
-
-
- rmq_store = BuildRMQStore().Pass();
- rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback,
- base::Unretained(this),
- &load_result));
- PumpLoop();
-
- ASSERT_EQ(persistent_ids, load_result.incoming_messages);
- ASSERT_EQ(load_result.outgoing_messages.size(), persistent_ids.size());
- for (int i =0 ; i < kNumPersistentIds; ++i) {
- std::string id = persistent_ids[i];
- ASSERT_TRUE(load_result.outgoing_messages[id]);
- const mcs_proto::DataMessageStanza* message =
- reinterpret_cast<mcs_proto::DataMessageStanza *>(
- load_result.outgoing_messages[id]);
- ASSERT_EQ(message->from(), id);
- ASSERT_EQ(message->category(), id);
- }
-
- rmq_store->RemoveIncomingMessages(persistent_ids,
- base::Bind(&RMQStoreTest::UpdateCallback,
- base::Unretained(this)));
- PumpLoop();
- rmq_store->RemoveOutgoingMessages(persistent_ids,
- base::Bind(&RMQStoreTest::UpdateCallback,
- base::Unretained(this)));
- PumpLoop();
-
- rmq_store = BuildRMQStore().Pass();
- load_result.incoming_messages.clear();
- load_result.outgoing_messages.clear();
- rmq_store->Load(base::Bind(&RMQStoreTest::LoadCallback,
- base::Unretained(this),
- &load_result));
- PumpLoop();
-
- ASSERT_TRUE(load_result.incoming_messages.empty());
- ASSERT_TRUE(load_result.outgoing_messages.empty());
-}
-
-} // namespace
-
-} // namespace gcm
diff --git a/chromium/google_apis/gcm/engine/unregistration_request.cc b/chromium/google_apis/gcm/engine/unregistration_request.cc
new file mode 100644
index 00000000000..2b8c97dadf1
--- /dev/null
+++ b/chromium/google_apis/gcm/engine/unregistration_request.cc
@@ -0,0 +1,224 @@
+// 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 "google_apis/gcm/engine/unregistration_request.h"
+
+#include "base/bind.h"
+#include "base/message_loop/message_loop.h"
+#include "base/metrics/histogram.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/string_piece.h"
+#include "base/values.h"
+#include "google_apis/gcm/monitoring/gcm_stats_recorder.h"
+#include "net/base/escape.h"
+#include "net/http/http_request_headers.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 gcm {
+
+namespace {
+
+const char kRequestContentType[] = "application/x-www-form-urlencoded";
+
+// Request constants.
+const char kAppIdKey[] = "app";
+const char kDeleteKey[] = "delete";
+const char kDeleteValue[] = "true";
+const char kDeviceIdKey[] = "device";
+const char kLoginHeader[] = "AidLogin";
+const char kUnregistrationCallerKey[] = "gcm_unreg_caller";
+// We are going to set the value to "false" in order to forcefully unregister
+// the application.
+const char kUnregistrationCallerValue[] = "false";
+
+// Response constants.
+const char kDeletedPrefix[] = "deleted=";
+const char kErrorPrefix[] = "Error=";
+const char kInvalidParameters[] = "INVALID_PARAMETERS";
+
+
+void BuildFormEncoding(const std::string& key,
+ const std::string& value,
+ std::string* out) {
+ if (!out->empty())
+ out->append("&");
+ out->append(key + "=" + net::EscapeUrlEncodedData(value, true));
+}
+
+UnregistrationRequest::Status ParseFetcherResponse(
+ const net::URLFetcher* source,
+ std::string request_app_id) {
+ if (!source->GetStatus().is_success()) {
+ DVLOG(1) << "Fetcher failed";
+ return UnregistrationRequest::URL_FETCHING_FAILED;
+ }
+
+ net::HttpStatusCode response_status = static_cast<net::HttpStatusCode>(
+ source->GetResponseCode());
+ if (response_status != net::HTTP_OK) {
+ DVLOG(1) << "HTTP Status code is not OK, but: " << response_status;
+ if (response_status == net::HTTP_SERVICE_UNAVAILABLE)
+ return UnregistrationRequest::SERVICE_UNAVAILABLE;
+ else if (response_status == net::HTTP_INTERNAL_SERVER_ERROR)
+ return UnregistrationRequest::INTERNAL_SERVER_ERROR;
+ return UnregistrationRequest::HTTP_NOT_OK;
+ }
+
+ std::string response;
+ if (!source->GetResponseAsString(&response)) {
+ DVLOG(1) << "Failed to get response body.";
+ return UnregistrationRequest::NO_RESPONSE_BODY;
+ }
+
+ DVLOG(1) << "Parsing unregistration response.";
+ if (response.find(kDeletedPrefix) != std::string::npos) {
+ std::string app_id = response.substr(
+ response.find(kDeletedPrefix) + arraysize(kDeletedPrefix) - 1);
+ if (app_id == request_app_id)
+ return UnregistrationRequest::SUCCESS;
+ return UnregistrationRequest::INCORRECT_APP_ID;
+ }
+
+ if (response.find(kErrorPrefix) != std::string::npos) {
+ std::string error = response.substr(
+ response.find(kErrorPrefix) + arraysize(kErrorPrefix) - 1);
+ if (error == kInvalidParameters)
+ return UnregistrationRequest::INVALID_PARAMETERS;
+ return UnregistrationRequest::UNKNOWN_ERROR;
+ }
+
+ DVLOG(1) << "Not able to parse a meaningful output from response body."
+ << response;
+ return UnregistrationRequest::RESPONSE_PARSING_FAILED;
+}
+
+} // namespace
+
+UnregistrationRequest::RequestInfo::RequestInfo(
+ uint64 android_id,
+ uint64 security_token,
+ const std::string& app_id)
+ : android_id(android_id),
+ security_token(security_token),
+ app_id(app_id) {
+}
+
+UnregistrationRequest::RequestInfo::~RequestInfo() {}
+
+UnregistrationRequest::UnregistrationRequest(
+ const GURL& registration_url,
+ const RequestInfo& request_info,
+ const net::BackoffEntry::Policy& backoff_policy,
+ const UnregistrationCallback& callback,
+ scoped_refptr<net::URLRequestContextGetter> request_context_getter,
+ GCMStatsRecorder* recorder)
+ : callback_(callback),
+ request_info_(request_info),
+ registration_url_(registration_url),
+ backoff_entry_(&backoff_policy),
+ request_context_getter_(request_context_getter),
+ recorder_(recorder),
+ weak_ptr_factory_(this) {
+}
+
+UnregistrationRequest::~UnregistrationRequest() {}
+
+void UnregistrationRequest::Start() {
+ DCHECK(!callback_.is_null());
+ DCHECK(request_info_.android_id != 0UL);
+ DCHECK(request_info_.security_token != 0UL);
+ DCHECK(!url_fetcher_.get());
+
+ url_fetcher_.reset(net::URLFetcher::Create(
+ registration_url_, net::URLFetcher::POST, this));
+ url_fetcher_->SetRequestContext(request_context_getter_);
+
+ std::string android_id = base::Uint64ToString(request_info_.android_id);
+ std::string auth_header =
+ std::string(kLoginHeader) + " " + android_id + ":" +
+ base::Uint64ToString(request_info_.security_token);
+ net::HttpRequestHeaders headers;
+ headers.SetHeader(net::HttpRequestHeaders::kAuthorization, auth_header);
+ headers.SetHeader(kAppIdKey, request_info_.app_id);
+ url_fetcher_->SetExtraRequestHeaders(headers.ToString());
+
+ std::string body;
+ BuildFormEncoding(kAppIdKey, request_info_.app_id, &body);
+ BuildFormEncoding(kDeviceIdKey, android_id, &body);
+ BuildFormEncoding(kDeleteKey, kDeleteValue, &body);
+ BuildFormEncoding(kUnregistrationCallerKey,
+ kUnregistrationCallerValue,
+ &body);
+
+ DVLOG(1) << "Unregistration request: " << body;
+ url_fetcher_->SetUploadData(kRequestContentType, body);
+
+ DVLOG(1) << "Performing unregistration for: " << request_info_.app_id;
+ recorder_->RecordUnregistrationSent(request_info_.app_id);
+ request_start_time_ = base::TimeTicks::Now();
+ url_fetcher_->Start();
+}
+
+void UnregistrationRequest::RetryWithBackoff(bool update_backoff) {
+ if (update_backoff) {
+ url_fetcher_.reset();
+ backoff_entry_.InformOfRequest(false);
+ }
+
+ if (backoff_entry_.ShouldRejectRequest()) {
+ DVLOG(1) << "Delaying GCM unregistration of app: "
+ << request_info_.app_id << ", for "
+ << backoff_entry_.GetTimeUntilRelease().InMilliseconds()
+ << " milliseconds.";
+ recorder_->RecordUnregistrationRetryDelayed(
+ request_info_.app_id,
+ backoff_entry_.GetTimeUntilRelease().InMilliseconds());
+ base::MessageLoop::current()->PostDelayedTask(
+ FROM_HERE,
+ base::Bind(&UnregistrationRequest::RetryWithBackoff,
+ weak_ptr_factory_.GetWeakPtr(),
+ false),
+ backoff_entry_.GetTimeUntilRelease());
+ return;
+ }
+
+ Start();
+}
+
+void UnregistrationRequest::OnURLFetchComplete(const net::URLFetcher* source) {
+ UnregistrationRequest::Status status =
+ ParseFetcherResponse(source, request_info_.app_id);
+
+ DVLOG(1) << "UnregistrationRequestStauts: " << status;
+ UMA_HISTOGRAM_ENUMERATION("GCM.UnregistrationRequestStatus",
+ status,
+ UNREGISTRATION_STATUS_COUNT);
+ recorder_->RecordUnregistrationResponse(request_info_.app_id, status);
+
+ if (status == URL_FETCHING_FAILED ||
+ status == SERVICE_UNAVAILABLE ||
+ status == INTERNAL_SERVER_ERROR ||
+ status == INCORRECT_APP_ID ||
+ status == RESPONSE_PARSING_FAILED) {
+ RetryWithBackoff(true);
+ return;
+ }
+
+ // status == SUCCESS || HTTP_NOT_OK || NO_RESPONSE_BODY ||
+ // INVALID_PARAMETERS || UNKNOWN_ERROR
+
+ if (status == SUCCESS) {
+ UMA_HISTOGRAM_COUNTS("GCM.UnregistrationRetryCount",
+ backoff_entry_.failure_count());
+ UMA_HISTOGRAM_TIMES("GCM.UnregistrationCompleteTime",
+ base::TimeTicks::Now() - request_start_time_);
+ }
+
+ callback_.Run(status);
+}
+
+} // namespace gcm
diff --git a/chromium/google_apis/gcm/engine/unregistration_request.h b/chromium/google_apis/gcm/engine/unregistration_request.h
new file mode 100644
index 00000000000..aec3331d18a
--- /dev/null
+++ b/chromium/google_apis/gcm/engine/unregistration_request.h
@@ -0,0 +1,115 @@
+// 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 GOOGLE_APIS_GCM_ENGINE_UNREGISTRATION_REQUEST_H_
+#define GOOGLE_APIS_GCM_ENGINE_UNREGISTRATION_REQUEST_H_
+
+#include "base/basictypes.h"
+#include "base/callback.h"
+#include "base/memory/ref_counted.h"
+#include "base/memory/scoped_ptr.h"
+#include "base/memory/weak_ptr.h"
+#include "base/time/time.h"
+#include "google_apis/gcm/base/gcm_export.h"
+#include "net/base/backoff_entry.h"
+#include "net/url_request/url_fetcher_delegate.h"
+#include "url/gurl.h"
+
+namespace net {
+class URLRequestContextGetter;
+}
+
+namespace gcm {
+
+class GCMStatsRecorder;
+
+// Unregistration request is used to revoke registration IDs for applications
+// that were uninstalled and should no longer receive GCM messages. In case an
+// attempt to unregister fails, it will retry using the backoff policy.
+// TODO(fgorski): Consider sharing code with RegistrationRequest if possible.
+class GCM_EXPORT UnregistrationRequest : public net::URLFetcherDelegate {
+ public:
+ // Outcome of the response parsing. Note that these enums are consumed by a
+ // histogram, so ordering should not be modified.
+ enum Status {
+ SUCCESS, // Unregistration completed successfully.
+ URL_FETCHING_FAILED, // URL fetching failed.
+ NO_RESPONSE_BODY, // No response body.
+ RESPONSE_PARSING_FAILED, // Failed to parse a meaningful output from
+ // response
+ // body.
+ INCORRECT_APP_ID, // App ID returned by the fetcher does not match
+ // request.
+ INVALID_PARAMETERS, // Request parameters were invalid.
+ SERVICE_UNAVAILABLE, // Unregistration service unavailable.
+ INTERNAL_SERVER_ERROR, // Internal server error happened during request.
+ HTTP_NOT_OK, // HTTP response code was not OK.
+ UNKNOWN_ERROR, // Unknown error.
+ // NOTE: Always keep this entry at the end. Add new status types only
+ // immediately above this line. Make sure to update the corresponding
+ // histogram enum accordingly.
+ UNREGISTRATION_STATUS_COUNT,
+ };
+
+ // Callback completing the unregistration request.
+ typedef base::Callback<void(Status success)> UnregistrationCallback;
+
+ // Details of the of the Unregistration Request. All parameters are mandatory.
+ struct GCM_EXPORT RequestInfo {
+ RequestInfo(uint64 android_id,
+ uint64 security_token,
+ const std::string& app_id);
+ ~RequestInfo();
+
+ // Android ID of the device.
+ uint64 android_id;
+ // Security token of the device.
+ uint64 security_token;
+ // Application ID.
+ std::string app_id;
+ };
+
+ // Creates an instance of UnregistrationRequest. |callback| will be called
+ // once registration has been revoked or there has been an error that makes
+ // further retries pointless.
+ UnregistrationRequest(
+ const GURL& registration_url,
+ const RequestInfo& request_info,
+ const net::BackoffEntry::Policy& backoff_policy,
+ const UnregistrationCallback& callback,
+ scoped_refptr<net::URLRequestContextGetter> request_context_getter,
+ GCMStatsRecorder* recorder);
+ virtual ~UnregistrationRequest();
+
+ // Starts an unregistration request.
+ void Start();
+
+ // URLFetcherDelegate implementation.
+ virtual void OnURLFetchComplete(const net::URLFetcher* source) OVERRIDE;
+
+ private:
+ // Schedules a retry attempt and informs the backoff of previous request's
+ // failure, when |update_backoff| is true.
+ void RetryWithBackoff(bool update_backoff);
+
+ UnregistrationCallback callback_;
+ RequestInfo request_info_;
+ GURL registration_url_;
+
+ net::BackoffEntry backoff_entry_;
+ scoped_refptr<net::URLRequestContextGetter> request_context_getter_;
+ scoped_ptr<net::URLFetcher> url_fetcher_;
+ base::TimeTicks request_start_time_;
+
+ // Recorder that records GCM activities for debugging purpose. Not owned.
+ GCMStatsRecorder* recorder_;
+
+ base::WeakPtrFactory<UnregistrationRequest> weak_ptr_factory_;
+
+ DISALLOW_COPY_AND_ASSIGN(UnregistrationRequest);
+};
+
+} // namespace gcm
+
+#endif // GOOGLE_APIS_GCM_ENGINE_UNREGISTRATION_REQUEST_H_
diff --git a/chromium/google_apis/gcm/engine/unregistration_request_unittest.cc b/chromium/google_apis/gcm/engine/unregistration_request_unittest.cc
new file mode 100644
index 00000000000..247150390ae
--- /dev/null
+++ b/chromium/google_apis/gcm/engine/unregistration_request_unittest.cc
@@ -0,0 +1,295 @@
+// 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 <map>
+#include <string>
+#include <vector>
+
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/string_tokenizer.h"
+#include "google_apis/gcm/engine/unregistration_request.h"
+#include "google_apis/gcm/monitoring/fake_gcm_stats_recorder.h"
+#include "net/url_request/test_url_fetcher_factory.h"
+#include "net/url_request/url_request_test_util.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace gcm {
+
+namespace {
+const uint64 kAndroidId = 42UL;
+const char kLoginHeader[] = "AidLogin";
+const char kAppId[] = "TestAppId";
+const char kDeletedAppId[] = "deleted=TestAppId";
+const char kRegistrationURL[] = "http://foo.bar/register";
+const uint64 kSecurityToken = 77UL;
+
+// Backoff policy for testing registration request.
+const net::BackoffEntry::Policy kDefaultBackoffPolicy = {
+ // Number of initial errors (in sequence) to ignore before applying
+ // exponential back-off rules.
+ // Explicitly set to 2 to skip the delay on the first retry, as we are not
+ // trying to test the backoff itself, but rather the fact that retry happens.
+ 1,
+
+ // Initial delay for exponential back-off in ms.
+ 15000, // 15 seconds.
+
+ // Factor by which the waiting time will be multiplied.
+ 2,
+
+ // Fuzzing percentage. ex: 10% will spread requests randomly
+ // between 90%-100% of the calculated time.
+ 0.5, // 50%.
+
+ // Maximum amount of time we are willing to delay our request in ms.
+ 1000 * 60 * 5, // 5 minutes.
+
+ // Time to keep an entry from being discarded even when it
+ // has no significant state, -1 to never discard.
+ -1,
+
+ // Don't use initial delay unless the last request was an error.
+ false,
+};
+} // namespace
+
+class UnregistrationRequestTest : public testing::Test {
+ public:
+ UnregistrationRequestTest();
+ virtual ~UnregistrationRequestTest();
+
+ void UnregistrationCallback(UnregistrationRequest::Status status);
+
+ void CreateRequest();
+ void SetResponseStatusAndString(net::HttpStatusCode status_code,
+ const std::string& response_body);
+ void CompleteFetch();
+
+ protected:
+ bool callback_called_;
+ UnregistrationRequest::Status status_;
+ scoped_ptr<UnregistrationRequest> request_;
+ base::MessageLoop message_loop_;
+ net::TestURLFetcherFactory url_fetcher_factory_;
+ scoped_refptr<net::TestURLRequestContextGetter> url_request_context_getter_;
+ FakeGCMStatsRecorder recorder_;
+};
+
+UnregistrationRequestTest::UnregistrationRequestTest()
+ : callback_called_(false),
+ status_(UnregistrationRequest::UNREGISTRATION_STATUS_COUNT),
+ url_request_context_getter_(new net::TestURLRequestContextGetter(
+ message_loop_.message_loop_proxy())) {}
+
+UnregistrationRequestTest::~UnregistrationRequestTest() {}
+
+void UnregistrationRequestTest::UnregistrationCallback(
+ UnregistrationRequest::Status status) {
+ callback_called_ = true;
+ status_ = status;
+}
+
+void UnregistrationRequestTest::CreateRequest() {
+ request_.reset(new UnregistrationRequest(
+ GURL(kRegistrationURL),
+ UnregistrationRequest::RequestInfo(kAndroidId,
+ kSecurityToken,
+ kAppId),
+ kDefaultBackoffPolicy,
+ base::Bind(&UnregistrationRequestTest::UnregistrationCallback,
+ base::Unretained(this)),
+ url_request_context_getter_.get(),
+ &recorder_));
+}
+
+void UnregistrationRequestTest::SetResponseStatusAndString(
+ net::HttpStatusCode status_code,
+ const std::string& response_body) {
+ net::TestURLFetcher* fetcher = url_fetcher_factory_.GetFetcherByID(0);
+ ASSERT_TRUE(fetcher);
+ fetcher->set_response_code(status_code);
+ fetcher->SetResponseString(response_body);
+}
+
+void UnregistrationRequestTest::CompleteFetch() {
+ status_ = UnregistrationRequest::UNREGISTRATION_STATUS_COUNT;
+ callback_called_ = false;
+ net::TestURLFetcher* fetcher = url_fetcher_factory_.GetFetcherByID(0);
+ ASSERT_TRUE(fetcher);
+ fetcher->delegate()->OnURLFetchComplete(fetcher);
+}
+
+TEST_F(UnregistrationRequestTest, RequestDataPassedToFetcher) {
+ CreateRequest();
+ request_->Start();
+
+ // Get data sent by request.
+ net::TestURLFetcher* fetcher = url_fetcher_factory_.GetFetcherByID(0);
+ ASSERT_TRUE(fetcher);
+
+ EXPECT_EQ(GURL(kRegistrationURL), fetcher->GetOriginalURL());
+
+ // Verify that authorization header was put together properly.
+ net::HttpRequestHeaders headers;
+ fetcher->GetExtraRequestHeaders(&headers);
+ std::string auth_header;
+ headers.GetHeader(net::HttpRequestHeaders::kAuthorization, &auth_header);
+ base::StringTokenizer auth_tokenizer(auth_header, " :");
+ ASSERT_TRUE(auth_tokenizer.GetNext());
+ EXPECT_EQ(kLoginHeader, auth_tokenizer.token());
+ ASSERT_TRUE(auth_tokenizer.GetNext());
+ EXPECT_EQ(base::Uint64ToString(kAndroidId), auth_tokenizer.token());
+ ASSERT_TRUE(auth_tokenizer.GetNext());
+ EXPECT_EQ(base::Uint64ToString(kSecurityToken), auth_tokenizer.token());
+ std::string app_id_header;
+ headers.GetHeader("app", &app_id_header);
+ EXPECT_EQ(kAppId, app_id_header);
+
+ std::map<std::string, std::string> expected_pairs;
+ expected_pairs["app"] = kAppId;
+ expected_pairs["device"] = base::Uint64ToString(kAndroidId);
+ expected_pairs["delete"] = "true";
+ expected_pairs["gcm_unreg_caller"] = "false";
+
+ // Verify data was formatted properly.
+ std::string upload_data = fetcher->upload_data();
+ base::StringTokenizer data_tokenizer(upload_data, "&=");
+ while (data_tokenizer.GetNext()) {
+ std::map<std::string, std::string>::iterator iter =
+ expected_pairs.find(data_tokenizer.token());
+ ASSERT_TRUE(iter != expected_pairs.end()) << data_tokenizer.token();
+ ASSERT_TRUE(data_tokenizer.GetNext()) << data_tokenizer.token();
+ EXPECT_EQ(iter->second, data_tokenizer.token());
+ // Ensure that none of the keys appears twice.
+ expected_pairs.erase(iter);
+ }
+
+ EXPECT_EQ(0UL, expected_pairs.size());
+}
+
+TEST_F(UnregistrationRequestTest, SuccessfulUnregistration) {
+ CreateRequest();
+ request_->Start();
+
+ SetResponseStatusAndString(net::HTTP_OK, kDeletedAppId);
+ CompleteFetch();
+
+ EXPECT_TRUE(callback_called_);
+ EXPECT_EQ(UnregistrationRequest::SUCCESS, status_);
+}
+
+TEST_F(UnregistrationRequestTest, ResponseHttpStatusNotOK) {
+ CreateRequest();
+ request_->Start();
+
+ SetResponseStatusAndString(net::HTTP_UNAUTHORIZED, "");
+ CompleteFetch();
+
+ EXPECT_TRUE(callback_called_);
+ EXPECT_EQ(UnregistrationRequest::HTTP_NOT_OK, status_);
+}
+
+TEST_F(UnregistrationRequestTest, ResponseEmpty) {
+ CreateRequest();
+ request_->Start();
+
+ SetResponseStatusAndString(net::HTTP_OK, "");
+ CompleteFetch();
+
+ EXPECT_FALSE(callback_called_);
+
+ SetResponseStatusAndString(net::HTTP_OK, kDeletedAppId);
+ CompleteFetch();
+
+ EXPECT_TRUE(callback_called_);
+ EXPECT_EQ(UnregistrationRequest::SUCCESS, status_);
+}
+
+TEST_F(UnregistrationRequestTest, InvalidParametersError) {
+ CreateRequest();
+ request_->Start();
+
+ SetResponseStatusAndString(net::HTTP_OK, "Error=INVALID_PARAMETERS");
+ CompleteFetch();
+
+ EXPECT_TRUE(callback_called_);
+ EXPECT_EQ(UnregistrationRequest::INVALID_PARAMETERS, status_);
+}
+
+TEST_F(UnregistrationRequestTest, UnkwnownError) {
+ CreateRequest();
+ request_->Start();
+
+ SetResponseStatusAndString(net::HTTP_OK, "Error=XXX");
+ CompleteFetch();
+
+ EXPECT_TRUE(callback_called_);
+ EXPECT_EQ(UnregistrationRequest::UNKNOWN_ERROR, status_);
+}
+
+TEST_F(UnregistrationRequestTest, ServiceUnavailable) {
+ CreateRequest();
+ request_->Start();
+
+ SetResponseStatusAndString(net::HTTP_SERVICE_UNAVAILABLE, "");
+ CompleteFetch();
+
+ EXPECT_FALSE(callback_called_);
+
+ SetResponseStatusAndString(net::HTTP_OK, kDeletedAppId);
+ CompleteFetch();
+
+ EXPECT_TRUE(callback_called_);
+ EXPECT_EQ(UnregistrationRequest::SUCCESS, status_);
+}
+
+TEST_F(UnregistrationRequestTest, InternalServerError) {
+ CreateRequest();
+ request_->Start();
+
+ SetResponseStatusAndString(net::HTTP_INTERNAL_SERVER_ERROR, "");
+ CompleteFetch();
+
+ EXPECT_FALSE(callback_called_);
+
+ SetResponseStatusAndString(net::HTTP_OK, kDeletedAppId);
+ CompleteFetch();
+
+ EXPECT_TRUE(callback_called_);
+ EXPECT_EQ(UnregistrationRequest::SUCCESS, status_);
+}
+
+TEST_F(UnregistrationRequestTest, IncorrectAppId) {
+ CreateRequest();
+ request_->Start();
+
+ SetResponseStatusAndString(net::HTTP_OK, "deleted=OtherTestAppId");
+ CompleteFetch();
+
+ EXPECT_FALSE(callback_called_);
+
+ SetResponseStatusAndString(net::HTTP_OK, kDeletedAppId);
+ CompleteFetch();
+
+ EXPECT_TRUE(callback_called_);
+ EXPECT_EQ(UnregistrationRequest::SUCCESS, status_);
+}
+
+TEST_F(UnregistrationRequestTest, ResponseParsingFailed) {
+ CreateRequest();
+ request_->Start();
+
+ SetResponseStatusAndString(net::HTTP_OK, "some malformed response");
+ CompleteFetch();
+
+ EXPECT_FALSE(callback_called_);
+
+ SetResponseStatusAndString(net::HTTP_OK, kDeletedAppId);
+ CompleteFetch();
+
+ EXPECT_TRUE(callback_called_);
+ EXPECT_EQ(UnregistrationRequest::SUCCESS, status_);
+}
+
+} // namespace gcm
diff --git a/chromium/google_apis/gcm/gcm.gyp b/chromium/google_apis/gcm/gcm.gyp
index f81c4ef4493..81dcf21596f 100644
--- a/chromium/google_apis/gcm/gcm.gyp
+++ b/chromium/google_apis/gcm/gcm.gyp
@@ -31,35 +31,47 @@
'dependencies': [
'../../base/base.gyp:base',
'../../base/third_party/dynamic_annotations/dynamic_annotations.gyp:dynamic_annotations',
- '../../components/components.gyp:encryptor',
'../../net/net.gyp:net',
'../../third_party/leveldatabase/leveldatabase.gyp:leveldatabase',
'../../third_party/protobuf/protobuf.gyp:protobuf_lite',
'../../url/url.gyp:url_lib',
],
'sources': [
- 'base/mcs_message.h',
'base/mcs_message.cc',
- 'base/mcs_util.h',
+ 'base/mcs_message.h',
'base/mcs_util.cc',
- 'base/socket_stream.h',
+ 'base/mcs_util.h',
'base/socket_stream.cc',
- 'engine/connection_factory.h',
+ 'base/socket_stream.h',
+ 'engine/checkin_request.cc',
+ 'engine/checkin_request.h',
'engine/connection_factory.cc',
- 'engine/connection_factory_impl.h',
+ 'engine/connection_factory.h',
'engine/connection_factory_impl.cc',
- 'engine/connection_handler.h',
+ 'engine/connection_factory_impl.h',
'engine/connection_handler.cc',
- 'engine/connection_handler_impl.h',
+ 'engine/connection_handler.h',
'engine/connection_handler_impl.cc',
- 'engine/mcs_client.h',
+ 'engine/connection_handler_impl.h',
+ 'engine/gcm_store.cc',
+ 'engine/gcm_store.h',
+ 'engine/gcm_store_impl.cc',
+ 'engine/gcm_store_impl.h',
+ 'engine/gservices_settings.cc',
+ 'engine/gservices_settings.h',
+ 'engine/heartbeat_manager.cc',
+ 'engine/heartbeat_manager.h',
'engine/mcs_client.cc',
- 'engine/rmq_store.h',
- 'engine/rmq_store.cc',
- 'gcm_client.cc',
- 'gcm_client.h',
- 'gcm_client_impl.cc',
- 'gcm_client_impl.h',
+ 'engine/mcs_client.h',
+ 'engine/registration_info.cc',
+ 'engine/registration_info.h',
+ 'engine/registration_request.cc',
+ 'engine/registration_request.h',
+ 'engine/unregistration_request.cc',
+ 'engine/unregistration_request.h',
+ 'monitoring/gcm_stats_recorder.h',
+ 'protocol/android_checkin.proto',
+ 'protocol/checkin.proto',
'protocol/mcs.proto',
],
'includes': [
@@ -67,6 +79,34 @@
],
},
+ # The test support library that is needed to test gcm.
+ {
+ 'target_name': 'gcm_test_support',
+ 'type': 'static_library',
+ 'include_dirs': [
+ '..',
+ ],
+ 'export_dependent_settings': [
+ '../../third_party/protobuf/protobuf.gyp:protobuf_lite'
+ ],
+ 'dependencies': [
+ '../../base/base.gyp:base',
+ '../../testing/gtest.gyp:gtest',
+ '../../third_party/protobuf/protobuf.gyp:protobuf_lite',
+ 'gcm',
+ ],
+ 'sources': [
+ 'base/fake_encryptor.cc',
+ 'base/fake_encryptor.h',
+ 'engine/fake_connection_factory.cc',
+ 'engine/fake_connection_factory.h',
+ 'engine/fake_connection_handler.cc',
+ 'engine/fake_connection_handler.h',
+ 'monitoring/fake_gcm_stats_recorder.cc',
+ 'monitoring/fake_gcm_stats_recorder.h',
+ ],
+ },
+
# A standalone MCS (mobile connection server) client.
{
'target_name': 'mcs_probe',
@@ -80,7 +120,8 @@
'../../net/net.gyp:net',
'../../net/net.gyp:net_test_support',
'../../third_party/protobuf/protobuf.gyp:protobuf_lite',
- 'gcm'
+ 'gcm',
+ 'gcm_test_support'
],
'sources': [
'tools/mcs_probe.cc',
@@ -101,25 +142,27 @@
'dependencies': [
'../../base/base.gyp:run_all_unittests',
'../../base/base.gyp:base',
- '../../components/components.gyp:encryptor',
'../../net/net.gyp:net',
'../../net/net.gyp:net_test_support',
'../../testing/gtest.gyp:gtest',
'../../third_party/protobuf/protobuf.gyp:protobuf_lite',
- 'gcm'
+ 'mcs_probe',
+ 'gcm',
+ 'gcm_test_support'
],
'sources': [
'base/mcs_message_unittest.cc',
'base/mcs_util_unittest.cc',
'base/socket_stream_unittest.cc',
+ 'engine/checkin_request_unittest.cc',
'engine/connection_factory_impl_unittest.cc',
'engine/connection_handler_impl_unittest.cc',
- 'engine/fake_connection_factory.h',
- 'engine/fake_connection_factory.cc',
- 'engine/fake_connection_handler.h',
- 'engine/fake_connection_handler.cc',
+ 'engine/gcm_store_impl_unittest.cc',
+ 'engine/gservices_settings_unittest.cc',
+ 'engine/heartbeat_manager_unittest.cc',
'engine/mcs_client_unittest.cc',
- 'engine/rmq_store_unittest.cc',
+ 'engine/registration_request_unittest.cc',
+ 'engine/unregistration_request_unittest.cc',
]
},
],
diff --git a/chromium/google_apis/gcm/gcm_client.cc b/chromium/google_apis/gcm/gcm_client.cc
deleted file mode 100644
index e437a305e47..00000000000
--- a/chromium/google_apis/gcm/gcm_client.cc
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright 2013 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 "google_apis/gcm/gcm_client.h"
-
-#include "base/lazy_instance.h"
-#include "google_apis/gcm/gcm_client_impl.h"
-
-namespace gcm {
-
-namespace {
-
-static base::LazyInstance<GCMClientImpl>::Leaky g_gcm_client =
- LAZY_INSTANCE_INITIALIZER;
-static GCMClient* g_gcm_client_override = NULL;
-
-} // namespace
-
-GCMClient::OutgoingMessage::OutgoingMessage()
- : time_to_live(0) {
-}
-
-GCMClient::OutgoingMessage::~OutgoingMessage() {
-}
-
-GCMClient::IncomingMessage::IncomingMessage() {
-}
-
-GCMClient::IncomingMessage::~IncomingMessage() {
-}
-
-// static
-GCMClient* GCMClient::Get() {
- if (g_gcm_client_override)
- return g_gcm_client_override;
- return g_gcm_client.Pointer();
-}
-
-// static
-void GCMClient::SetForTesting(GCMClient* client) {
- g_gcm_client_override = client;
-}
-
-} // namespace gcm
diff --git a/chromium/google_apis/gcm/gcm_client.h b/chromium/google_apis/gcm/gcm_client.h
deleted file mode 100644
index 66e17a04489..00000000000
--- a/chromium/google_apis/gcm/gcm_client.h
+++ /dev/null
@@ -1,193 +0,0 @@
-// Copyright 2013 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 GOOGLE_APIS_GCM_GCM_CLIENT_H_
-#define GOOGLE_APIS_GCM_GCM_CLIENT_H_
-
-#include <map>
-#include <string>
-#include <vector>
-
-#include "base/basictypes.h"
-#include "google_apis/gcm/base/gcm_export.h"
-
-namespace base {
-class TaskRunner;
-}
-
-namespace gcm {
-
-// Interface that encapsulates the network communications with the Google Cloud
-// Messaging server. This interface is not supposed to be thread-safe.
-class GCM_EXPORT GCMClient {
- public:
- enum Result {
- // Successful operation.
- SUCCESS,
- // Invalid parameter.
- INVALID_PARAMETER,
- // Previous asynchronous operation is still pending to finish. Certain
- // operation, like register, is only allowed one at a time.
- ASYNC_OPERATION_PENDING,
- // Network socket error.
- NETWORK_ERROR,
- // Problem at the server.
- SERVER_ERROR,
- // Exceeded the specified TTL during message sending.
- TTL_EXCEEDED,
- // Other errors.
- UNKNOWN_ERROR
- };
-
- // Message data consisting of key-value pairs.
- typedef std::map<std::string, std::string> MessageData;
-
- // Message to be delivered to the other party.
- struct GCM_EXPORT OutgoingMessage {
- OutgoingMessage();
- ~OutgoingMessage();
-
- // Message ID.
- std::string id;
- // In seconds.
- int time_to_live;
- MessageData data;
- };
-
- // Message being received from the other party.
- struct GCM_EXPORT IncomingMessage {
- IncomingMessage();
- ~IncomingMessage();
-
- MessageData data;
- };
-
- // The check-in info for the user. Returned by the server.
- struct GCM_EXPORT CheckInInfo {
- CheckInInfo() : android_id(0), secret(0) {}
- bool IsValid() const { return android_id != 0 && secret != 0; }
- void Reset() {
- android_id = 0;
- secret = 0;
- }
-
- uint64 android_id;
- uint64 secret;
- };
-
- // A delegate interface that allows the GCMClient instance to interact with
- // its caller, i.e. notifying asynchronous event.
- class Delegate {
- public:
- // Called when the user has been checked in successfully or an error occurs.
- // |checkin_info|: valid if the checkin completed successfully.
- // |result|: the type of the error if an error occured, success otherwise.
- virtual void OnCheckInFinished(const CheckInInfo& checkin_info,
- Result result) = 0;
-
- // Called when the registration completed successfully or an error occurs.
- // |app_id|: application ID.
- // |registration_id|: non-empty if the registration completed successfully.
- // |result|: the type of the error if an error occured, success otherwise.
- virtual void OnRegisterFinished(const std::string& app_id,
- const std::string& registration_id,
- Result result) = 0;
-
- // Called when the message is scheduled to send successfully or an error
- // occurs.
- // |app_id|: application ID.
- // |message_id|: ID of the message being sent.
- // |result|: the type of the error if an error occured, success otherwise.
- virtual void OnSendFinished(const std::string& app_id,
- const std::string& message_id,
- Result result) = 0;
-
- // Called when a message has been received.
- // |app_id|: application ID.
- // |message|: message received.
- virtual void OnMessageReceived(const std::string& app_id,
- const IncomingMessage& message) = 0;
-
- // Called when some messages have been deleted from the server.
- // |app_id|: application ID.
- virtual void OnMessagesDeleted(const std::string& app_id) = 0;
-
- // Called when a message failed to send to the server.
- // |app_id|: application ID.
- // |message_id|: ID of the message being sent.
- // |result|: the type of the error if an error occured, success otherwise.
- virtual void OnMessageSendError(const std::string& app_id,
- const std::string& message_id,
- Result result) = 0;
-
- // Returns the checkin info associated with this user. The delegate class
- // is expected to persist the checkin info that is provided by
- // OnCheckInFinished.
- virtual CheckInInfo GetCheckInInfo() const = 0;
-
- // Called when the loading from the persistent store is done. The loading
- // is triggered asynchronously when GCMClient is created.
- virtual void OnLoadingCompleted() = 0;
-
- // Returns a task runner for file operations that may block. This is used
- // in writing to or reading from the persistent store.
- virtual base::TaskRunner* GetFileTaskRunner() = 0;
- };
-
- // Returns the single instance. Multiple profiles share the same client
- // that makes use of the same MCS connection.
- static GCMClient* Get();
-
- // Passes a mocked instance for testing purpose.
- static void SetForTesting(GCMClient* client);
-
- // Checks in the user to use GCM. If the device has not been checked in, it
- // will be done first.
- // |username|: the username (email address) used to check in with the server.
- // |delegate|: the delegate whose methods will be called asynchronously in
- // response to events and messages.
- virtual void CheckIn(const std::string& username, Delegate* delegate) = 0;
-
- // Registers the application for GCM. Delegate::OnRegisterFinished will be
- // called asynchronously upon completion.
- // |username|: the username (email address) passed in CheckIn.
- // |app_id|: application ID.
- // |cert|: SHA-1 of public key of the application, in base16 format.
- // |sender_ids|: list of IDs of the servers that are allowed to send the
- // messages to the application. These IDs are assigned by the
- // Google API Console.
- virtual void Register(const std::string& username,
- const std::string& app_id,
- const std::string& cert,
- const std::vector<std::string>& sender_ids) = 0;
-
- // Unregisters the application from GCM when it is uninstalled.
- // Delegate::OnUnregisterFinished will be called asynchronously upon
- // completion.
- // |username|: the username (email address) passed in CheckIn.
- // |app_id|: application ID.
- virtual void Unregister(const std::string& username,
- const std::string& app_id) = 0;
-
- // Sends a message to a given receiver. Delegate::OnSendFinished will be
- // called asynchronously upon completion.
- // |username|: the username (email address) passed in CheckIn.
- // |app_id|: application ID.
- // |receiver_id|: registration ID of the receiver party.
- // |message|: message to be sent.
- virtual void Send(const std::string& username,
- const std::string& app_id,
- const std::string& receiver_id,
- const OutgoingMessage& message) = 0;
-
- // Returns true if the loading from the persistent store is still in progress.
- virtual bool IsLoading() const = 0;
-
- protected:
- virtual ~GCMClient() {}
-};
-
-} // namespace gcm
-
-#endif // GOOGLE_APIS_GCM_GCM_CLIENT_H_
diff --git a/chromium/google_apis/gcm/gcm_client_impl.cc b/chromium/google_apis/gcm/gcm_client_impl.cc
deleted file mode 100644
index 76900cda747..00000000000
--- a/chromium/google_apis/gcm/gcm_client_impl.cc
+++ /dev/null
@@ -1,39 +0,0 @@
-// Copyright 2013 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 "google_apis/gcm/gcm_client_impl.h"
-
-namespace gcm {
-
-GCMClientImpl::GCMClientImpl() {
-}
-
-GCMClientImpl::~GCMClientImpl() {
-}
-
-void GCMClientImpl::CheckIn(const std::string& username,
- Delegate* delegate) {
-}
-
-void GCMClientImpl::Register(const std::string& username,
- const std::string& app_id,
- const std::string& cert,
- const std::vector<std::string>& sender_ids) {
-}
-
-void GCMClientImpl::Unregister(const std::string& username,
- const std::string& app_id) {
-}
-
-void GCMClientImpl::Send(const std::string& username,
- const std::string& app_id,
- const std::string& receiver_id,
- const OutgoingMessage& message) {
-}
-
-bool GCMClientImpl::IsLoading() const {
- return false;
-}
-
-} // namespace gcm
diff --git a/chromium/google_apis/gcm/gcm_client_impl.h b/chromium/google_apis/gcm/gcm_client_impl.h
deleted file mode 100644
index 46f910e546a..00000000000
--- a/chromium/google_apis/gcm/gcm_client_impl.h
+++ /dev/null
@@ -1,39 +0,0 @@
-// Copyright 2013 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 GOOGLE_APIS_GCM_GCM_CLIENT_IMPL_H_
-#define GOOGLE_APIS_GCM_GCM_CLIENT_IMPL_H_
-
-#include "base/compiler_specific.h"
-#include "google_apis/gcm/gcm_client.h"
-
-namespace gcm {
-
-class GCMClientImpl : public GCMClient {
- public:
- GCMClientImpl();
- virtual ~GCMClientImpl();
-
- // Overridden from GCMClient:
- virtual void CheckIn(const std::string& username,
- Delegate* delegate) OVERRIDE;
- virtual void Register(const std::string& username,
- const std::string& app_id,
- const std::string& cert,
- const std::vector<std::string>& sender_ids) OVERRIDE;
- virtual void Unregister(const std::string& username,
- const std::string& app_id) OVERRIDE;
- virtual void Send(const std::string& username,
- const std::string& app_id,
- const std::string& receiver_id,
- const OutgoingMessage& message) OVERRIDE;
- virtual bool IsLoading() const OVERRIDE;
-
- private:
- DISALLOW_COPY_AND_ASSIGN(GCMClientImpl);
-};
-
-} // namespace gcm
-
-#endif // GOOGLE_APIS_GCM_GCM_CLIENT_IMPL_H_
diff --git a/chromium/google_apis/gcm/monitoring/fake_gcm_stats_recorder.cc b/chromium/google_apis/gcm/monitoring/fake_gcm_stats_recorder.cc
new file mode 100644
index 00000000000..5ed8a1df49c
--- /dev/null
+++ b/chromium/google_apis/gcm/monitoring/fake_gcm_stats_recorder.cc
@@ -0,0 +1,107 @@
+// 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 "google_apis/gcm/monitoring/fake_gcm_stats_recorder.h"
+
+namespace gcm {
+
+FakeGCMStatsRecorder::FakeGCMStatsRecorder() {
+}
+
+FakeGCMStatsRecorder::~FakeGCMStatsRecorder() {
+}
+
+void FakeGCMStatsRecorder::RecordCheckinInitiated(uint64 android_id) {
+}
+
+void FakeGCMStatsRecorder::RecordCheckinDelayedDueToBackoff(int64 delay_msec) {
+}
+
+void FakeGCMStatsRecorder::RecordCheckinSuccess() {
+}
+
+void FakeGCMStatsRecorder::RecordCheckinFailure(std::string status,
+ bool will_retry) {
+}
+
+void FakeGCMStatsRecorder::RecordConnectionInitiated(const std::string& host) {
+}
+
+void FakeGCMStatsRecorder::RecordConnectionDelayedDueToBackoff(
+ int64 delay_msec) {
+}
+
+void FakeGCMStatsRecorder::RecordConnectionSuccess() {
+}
+
+void FakeGCMStatsRecorder::RecordConnectionFailure(int network_error) {
+}
+
+void FakeGCMStatsRecorder::RecordConnectionResetSignaled(
+ ConnectionFactory::ConnectionResetReason reason) {
+}
+
+void FakeGCMStatsRecorder::RecordRegistrationSent(
+ const std::string& app_id,
+ const std::string& sender_ids) {
+}
+
+void FakeGCMStatsRecorder::RecordRegistrationResponse(
+ const std::string& app_id,
+ const std::vector<std::string>& sender_ids,
+ RegistrationRequest::Status status) {
+}
+
+void FakeGCMStatsRecorder::RecordRegistrationRetryRequested(
+ const std::string& app_id,
+ const std::vector<std::string>& sender_ids,
+ int retries_left) {
+}
+
+void FakeGCMStatsRecorder::RecordUnregistrationSent(
+ const std::string& app_id) {
+}
+
+void FakeGCMStatsRecorder::RecordUnregistrationResponse(
+ const std::string& app_id,
+ UnregistrationRequest::Status status) {
+}
+
+void FakeGCMStatsRecorder::RecordUnregistrationRetryDelayed(
+ const std::string& app_id,
+ int64 delay_msec) {
+}
+
+void FakeGCMStatsRecorder::RecordDataMessageReceived(
+ const std::string& app_id,
+ const std::string& from,
+ int message_byte_size,
+ bool to_registered_app,
+ ReceivedMessageType message_type) {
+}
+
+void FakeGCMStatsRecorder::RecordDataSentToWire(
+ const std::string& app_id,
+ const std::string& receiver_id,
+ const std::string& message_id,
+ int queued) {
+}
+
+void FakeGCMStatsRecorder::RecordNotifySendStatus(
+ const std::string& app_id,
+ const std::string& receiver_id,
+ const std::string& message_id,
+ gcm::MCSClient::MessageSendStatus status,
+ int byte_size,
+ int ttl) {
+}
+
+void FakeGCMStatsRecorder::RecordIncomingSendError(
+ const std::string& app_id,
+ const std::string& receiver_id,
+ const std::string& message_id) {
+}
+
+
+} // namespace gcm
diff --git a/chromium/google_apis/gcm/monitoring/fake_gcm_stats_recorder.h b/chromium/google_apis/gcm/monitoring/fake_gcm_stats_recorder.h
new file mode 100644
index 00000000000..1ada61107c3
--- /dev/null
+++ b/chromium/google_apis/gcm/monitoring/fake_gcm_stats_recorder.h
@@ -0,0 +1,71 @@
+// 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 GOOGLE_APIS_GCM_MONITORING_FAKE_GCM_STATS_RECODER_H_
+#define GOOGLE_APIS_GCM_MONITORING_FAKE_GCM_STATS_RECODER_H_
+
+#include "google_apis/gcm/monitoring/gcm_stats_recorder.h"
+
+namespace gcm {
+
+// The fake version of GCMStatsRecorder that does nothing.
+class FakeGCMStatsRecorder : public GCMStatsRecorder {
+ public:
+ FakeGCMStatsRecorder();
+ virtual ~FakeGCMStatsRecorder();
+
+ virtual void RecordCheckinInitiated(uint64 android_id) OVERRIDE;
+ virtual void RecordCheckinDelayedDueToBackoff(int64 delay_msec) OVERRIDE;
+ virtual void RecordCheckinSuccess() OVERRIDE;
+ virtual void RecordCheckinFailure(std::string status,
+ bool will_retry) OVERRIDE;
+ virtual void RecordConnectionInitiated(const std::string& host) OVERRIDE;
+ virtual void RecordConnectionDelayedDueToBackoff(int64 delay_msec) OVERRIDE;
+ virtual void RecordConnectionSuccess() OVERRIDE;
+ virtual void RecordConnectionFailure(int network_error) OVERRIDE;
+ virtual void RecordConnectionResetSignaled(
+ ConnectionFactory::ConnectionResetReason reason) OVERRIDE;
+ virtual void RecordRegistrationSent(const std::string& app_id,
+ const std::string& sender_ids) OVERRIDE;
+ virtual void RecordRegistrationResponse(
+ const std::string& app_id,
+ const std::vector<std::string>& sender_ids,
+ RegistrationRequest::Status status) OVERRIDE;
+ virtual void RecordRegistrationRetryRequested(
+ const std::string& app_id,
+ const std::vector<std::string>& sender_ids,
+ int retries_left) OVERRIDE;
+ virtual void RecordUnregistrationSent(const std::string& app_id) OVERRIDE;
+ virtual void RecordUnregistrationResponse(
+ const std::string& app_id,
+ UnregistrationRequest::Status status) OVERRIDE;
+ virtual void RecordUnregistrationRetryDelayed(const std::string& app_id,
+ int64 delay_msec) OVERRIDE;
+ virtual void RecordDataMessageReceived(
+ const std::string& app_id,
+ const std::string& from,
+ int message_byte_size,
+ bool to_registered_app,
+ ReceivedMessageType message_type) OVERRIDE;
+ virtual void RecordDataSentToWire(const std::string& app_id,
+ const std::string& receiver_id,
+ const std::string& message_id,
+ int queued) OVERRIDE;
+ virtual void RecordNotifySendStatus(const std::string& app_id,
+ const std::string& receiver_id,
+ const std::string& message_id,
+ MCSClient::MessageSendStatus status,
+ int byte_size,
+ int ttl) OVERRIDE;
+ virtual void RecordIncomingSendError(const std::string& app_id,
+ const std::string& receiver_id,
+ const std::string& message_id) OVERRIDE;
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(FakeGCMStatsRecorder);
+};
+
+} // namespace gcm
+
+#endif // GOOGLE_APIS_GCM_MONITORING_FAKE_GCM_STATS_RECODER_H_
diff --git a/chromium/google_apis/gcm/monitoring/gcm_stats_recorder.h b/chromium/google_apis/gcm/monitoring/gcm_stats_recorder.h
new file mode 100644
index 00000000000..71915693fda
--- /dev/null
+++ b/chromium/google_apis/gcm/monitoring/gcm_stats_recorder.h
@@ -0,0 +1,135 @@
+// 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 GOOGLE_APIS_GCM_MONITORING_GCM_STATS_RECORDER_H_
+#define GOOGLE_APIS_GCM_MONITORING_GCM_STATS_RECORDER_H_
+
+#include <string>
+#include <vector>
+
+#include "base/time/time.h"
+#include "google_apis/gcm/base/gcm_export.h"
+#include "google_apis/gcm/engine/connection_factory.h"
+#include "google_apis/gcm/engine/mcs_client.h"
+#include "google_apis/gcm/engine/registration_request.h"
+#include "google_apis/gcm/engine/unregistration_request.h"
+
+namespace gcm {
+
+// Defines the interface to record GCM internal stats and activities for
+// debugging purpose.
+class GCM_EXPORT GCMStatsRecorder {
+ public:
+ // Type of a received message
+ enum ReceivedMessageType {
+ // Data message.
+ DATA_MESSAGE,
+ // Message that indicates some messages have been deleted on the server.
+ DELETED_MESSAGES,
+ };
+
+ // A delegate interface that allows the GCMStatsRecorderImpl instance to
+ // interact with its container.
+ class Delegate {
+ public:
+ // Called when the GCMStatsRecorderImpl is recording activities and a new
+ // activity has just been recorded.
+ virtual void OnActivityRecorded() = 0;
+ };
+
+ GCMStatsRecorder() {}
+ virtual ~GCMStatsRecorder() {}
+
+ // Records that a check-in has been initiated.
+ virtual void RecordCheckinInitiated(uint64 android_id) = 0;
+
+ // Records that a check-in has been delayed due to backoff.
+ virtual void RecordCheckinDelayedDueToBackoff(int64 delay_msec) = 0;
+
+ // Records that a check-in request has succeeded.
+ virtual void RecordCheckinSuccess() = 0;
+
+ // Records that a check-in request has failed. If a retry will be tempted then
+ // will_retry should be true.
+ virtual void RecordCheckinFailure(std::string status, bool will_retry) = 0;
+
+ // Records that a connection to MCS has been initiated.
+ virtual void RecordConnectionInitiated(const std::string& host) = 0;
+
+ // Records that a connection has been delayed due to backoff.
+ virtual void RecordConnectionDelayedDueToBackoff(int64 delay_msec) = 0;
+
+ // Records that connection has been successfully established.
+ virtual void RecordConnectionSuccess() = 0;
+
+ // Records that connection has failed with a network error code.
+ virtual void RecordConnectionFailure(int network_error) = 0;
+
+ // Records that connection reset has been signaled.
+ virtual void RecordConnectionResetSignaled(
+ ConnectionFactory::ConnectionResetReason reason) = 0;
+
+ // Records that a registration request has been sent. This could be initiated
+ // directly from API, or from retry logic.
+ virtual void RecordRegistrationSent(const std::string& app_id,
+ const std::string& sender_ids) = 0;
+
+ // Records that a registration response has been received from server.
+ virtual void RecordRegistrationResponse(
+ const std::string& app_id,
+ const std::vector<std::string>& sender_ids,
+ RegistrationRequest::Status status) = 0;
+
+ // Records that a registration retry has been requested. The actual retry
+ // action may not occur until some time later according to backoff logic.
+ virtual void RecordRegistrationRetryRequested(
+ const std::string& app_id,
+ const std::vector<std::string>& sender_ids,
+ int retries_left) = 0;
+
+ // Records that an unregistration request has been sent. This could be
+ // initiated directly from API, or from retry logic.
+ virtual void RecordUnregistrationSent(const std::string& app_id) = 0;
+
+ // Records that an unregistration response has been received from server.
+ virtual void RecordUnregistrationResponse(
+ const std::string& app_id,
+ UnregistrationRequest::Status status) = 0;
+
+ // Records that an unregistration retry has been requested and delayed due to
+ // backoff logic.
+ virtual void RecordUnregistrationRetryDelayed(const std::string& app_id,
+ int64 delay_msec) = 0;
+
+ // Records that a data message has been received. If this message is not
+ // sent to a registered app, to_registered_app shoudl be false. If it
+ // indicates that a message has been dropped on the server, is_message_dropped
+ // should be true.
+ virtual void RecordDataMessageReceived(const std::string& app_id,
+ const std::string& from,
+ int message_byte_size,
+ bool to_registered_app,
+ ReceivedMessageType message_type) = 0;
+
+ // Records that an outgoing data message was sent over the wire.
+ virtual void RecordDataSentToWire(const std::string& app_id,
+ const std::string& receiver_id,
+ const std::string& message_id,
+ int queued) = 0;
+ // Records that the MCS client sent a 'send status' notification to callback.
+ virtual void RecordNotifySendStatus(const std::string& app_id,
+ const std::string& receiver_id,
+ const std::string& message_id,
+ MCSClient::MessageSendStatus status,
+ int byte_size,
+ int ttl) = 0;
+ // Records that a 'send error' message was received.
+ virtual void RecordIncomingSendError(const std::string& app_id,
+ const std::string& receiver_id,
+ const std::string& message_id) = 0;
+};
+
+} // namespace gcm
+
+#endif // GOOGLE_APIS_GCM_MONITORING_GCM_STATS_RECORDER_H_
diff --git a/chromium/google_apis/gcm/protocol/android_checkin.proto b/chromium/google_apis/gcm/protocol/android_checkin.proto
new file mode 100644
index 00000000000..a4520513888
--- /dev/null
+++ b/chromium/google_apis/gcm/protocol/android_checkin.proto
@@ -0,0 +1,97 @@
+// 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.
+//
+// Logging information for Android "checkin" events (automatic, periodic
+// requests made by Android devices to the server).
+
+syntax = "proto2";
+
+option optimize_for = LITE_RUNTIME;
+option retain_unknown_fields = true;
+package checkin_proto;
+
+// Build characteristics unique to the Chrome browser, and Chrome OS
+message ChromeBuildProto {
+ enum Platform {
+ PLATFORM_WIN = 1;
+ PLATFORM_MAC = 2;
+ PLATFORM_LINUX = 3;
+ PLATFORM_CROS = 4;
+ PLATFORM_IOS = 5;
+ // Just a placeholder. Likely don't need it due to the presence of the
+ // Android GCM on phone/tablet devices.
+ PLATFORM_ANDROID = 6;
+ }
+
+ enum Channel {
+ CHANNEL_STABLE = 1;
+ CHANNEL_BETA = 2;
+ CHANNEL_DEV = 3;
+ CHANNEL_CANARY = 4;
+ CHANNEL_UNKNOWN = 5; // for tip of tree or custom builds
+ }
+
+ // The platform of the device.
+ optional Platform platform = 1;
+
+ // The Chrome instance's version.
+ optional string chrome_version = 2;
+
+ // The Channel (build type) of Chrome.
+ optional Channel channel = 3;
+}
+
+// Information sent by the device in a "checkin" request.
+message AndroidCheckinProto {
+ // Miliseconds since the Unix epoch of the device's last successful checkin.
+ optional int64 last_checkin_msec = 2;
+
+ // The current MCC+MNC of the mobile device's current cell.
+ optional string cell_operator = 6;
+
+ // The MCC+MNC of the SIM card (different from operator if the
+ // device is roaming, for instance).
+ optional string sim_operator = 7;
+
+ // The device's current roaming state (reported starting in eclair builds).
+ // Currently one of "{,not}mobile-{,not}roaming", if it is present at all.
+ optional string roaming = 8;
+
+ // For devices supporting multiple user profiles (which may be
+ // supported starting in jellybean), the ordinal number of the
+ // profile that is checking in. This is 0 for the primary profile
+ // (which can't be changed without wiping the device), and 1,2,3,...
+ // for additional profiles (which can be added and deleted freely).
+ optional int32 user_number = 9;
+
+ // Class of device. Indicates the type of build proto
+ // (IosBuildProto/ChromeBuildProto/AndroidBuildProto)
+ // That is included in this proto
+ optional DeviceType type = 12 [default = DEVICE_ANDROID_OS];
+
+ // For devices running MCS on Chrome, build-specific characteristics
+ // of the browser. There are no hardware aspects (except for ChromeOS).
+ // This will only be populated for Chrome builds/ChromeOS devices
+ optional checkin_proto.ChromeBuildProto chrome_build = 13;
+
+ // Note: Some of the Android specific optional fields were skipped to limit
+ // the protobuf definition.
+ // Next 14
+}
+
+// enum values correspond to the type of device.
+// Used in the AndroidCheckinProto and Device proto.
+enum DeviceType {
+ // Android Device
+ DEVICE_ANDROID_OS = 1;
+
+ // Apple IOS device
+ DEVICE_IOS_OS = 2;
+
+ // Chrome browser - Not Chrome OS. No hardware records.
+ DEVICE_CHROME_BROWSER = 3;
+
+ // Chrome OS
+ DEVICE_CHROME_OS = 4;
+}
diff --git a/chromium/google_apis/gcm/protocol/checkin.proto b/chromium/google_apis/gcm/protocol/checkin.proto
new file mode 100644
index 00000000000..10ad628f733
--- /dev/null
+++ b/chromium/google_apis/gcm/protocol/checkin.proto
@@ -0,0 +1,156 @@
+// 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.
+//
+// Request and reply to the "checkin server" devices poll every few hours.
+
+syntax = "proto2";
+
+option optimize_for = LITE_RUNTIME;
+option retain_unknown_fields = true;
+
+package checkin_proto;
+
+import "android_checkin.proto";
+
+// A concrete name/value pair sent to the device's Gservices database.
+message GservicesSetting {
+ required bytes name = 1;
+ required bytes value = 2;
+}
+
+// Devices send this every few hours to tell us how they're doing.
+message AndroidCheckinRequest {
+ // IMEI (used by GSM phones) is sent and stored as 15 decimal
+ // digits; the 15th is a check digit.
+ optional string imei = 1; // IMEI, reported but not logged.
+
+ // MEID (used by CDMA phones) is sent and stored as 14 hexadecimal
+ // digits (no check digit).
+ optional string meid = 10; // MEID, reported but not logged.
+
+ // MAC address (used by non-phone devices). 12 hexadecimal digits;
+ // no separators (eg "0016E6513AC2", not "00:16:E6:51:3A:C2").
+ repeated string mac_addr = 9; // MAC address, reported but not logged.
+
+ // An array parallel to mac_addr, describing the type of interface.
+ // Currently accepted values: "wifi", "ethernet", "bluetooth". If
+ // not present, "wifi" is assumed.
+ repeated string mac_addr_type = 19;
+
+ // Serial number (a manufacturer-defined unique hardware
+ // identifier). Alphanumeric, case-insensitive.
+ optional string serial_number = 16;
+
+ // Older CDMA networks use an ESN (8 hex digits) instead of an MEID.
+ optional string esn = 17; // ESN, reported but not logged
+
+ optional int64 id = 2; // Android device ID, not logged
+ optional int64 logging_id = 7; // Pseudonymous logging ID for Sawmill
+ optional string digest = 3; // Digest of device provisioning, not logged.
+ optional string locale = 6; // Current locale in standard (xx_XX) format
+ required AndroidCheckinProto checkin = 4;
+
+ // DEPRECATED, see AndroidCheckinProto.requested_group
+ optional string desired_build = 5;
+
+ // Blob of data from the Market app to be passed to Market API server
+ optional string market_checkin = 8;
+
+ // SID cookies of any google accounts stored on the phone. Not logged.
+ repeated string account_cookie = 11;
+
+ // Time zone. Not currently logged.
+ optional string time_zone = 12;
+
+ // Security token used to validate the checkin request.
+ // Required for android IDs issued to Froyo+ devices, not for legacy IDs.
+ optional fixed64 security_token = 13;
+
+ // Version of checkin protocol.
+ //
+ // There are currently two versions:
+ //
+ // - version field missing: android IDs are assigned based on
+ // hardware identifiers. unsecured in the sense that you can
+ // "unregister" someone's phone by sending a registration request
+ // with their IMEI/MEID/MAC.
+ //
+ // - version=2: android IDs are assigned randomly. The device is
+ // sent a security token that must be included in all future
+ // checkins for that android id.
+ //
+ // - version=3: same as version 2, but the 'fragment' field is
+ // provided, and the device understands incremental updates to the
+ // gservices table (ie, only returning the keys whose values have
+ // changed.)
+ //
+ // (version=1 was skipped to avoid confusion with the "missing"
+ // version field that is effectively version 1.)
+ optional int32 version = 14;
+
+ // OTA certs accepted by device (base-64 SHA-1 of cert files). Not
+ // logged.
+ repeated string ota_cert = 15;
+
+ // Honeycomb and newer devices send configuration data with their checkin.
+ // optional DeviceConfigurationProto device_configuration = 18;
+
+ // A single CheckinTask on the device may lead to multiple checkin
+ // requests if there is too much log data to upload in a single
+ // request. For version 3 and up, this field will be filled in with
+ // the number of the request, starting with 0.
+ optional int32 fragment = 20;
+
+ // For devices supporting multiple users, the name of the current
+ // profile (they all check in independently, just as if they were
+ // multiple physical devices). This may not be set, even if the
+ // device is using multiuser. (checkin.user_number should be set to
+ // the ordinal of the user.)
+ optional string user_name = 21;
+
+ // For devices supporting multiple user profiles, the serial number
+ // for the user checking in. Not logged. May not be set, even if
+ // the device supportes multiuser. checkin.user_number is the
+ // ordinal of the user (0, 1, 2, ...), which may be reused if users
+ // are deleted and re-created. user_serial_number is never reused
+ // (unless the device is wiped).
+ optional int32 user_serial_number = 22;
+
+ // NEXT TAG: 23
+}
+
+// The response to the device.
+message AndroidCheckinResponse {
+ required bool stats_ok = 1; // Whether statistics were recorded properly.
+ optional int64 time_msec = 3; // Time of day from server (Java epoch).
+ // repeated AndroidIntentProto intent = 2;
+
+ // Provisioning is sent if the request included an obsolete digest.
+ //
+ // For version <= 2, 'digest' contains the digest that should be
+ // sent back to the server on the next checkin, and 'setting'
+ // contains the entire gservices table (which replaces the entire
+ // current table on the device).
+ //
+ // for version >= 3, 'digest' will be absent. If 'settings_diff'
+ // is false, then 'setting' contains the entire table, as in version
+ // 2. If 'settings_diff' is true, then 'delete_setting' contains
+ // the keys to delete, and 'setting' contains only keys to be added
+ // or for which the value has changed. All other keys in the
+ // current table should be left untouched. If 'settings_diff' is
+ // absent, don't touch the existing gservices table.
+ //
+ optional string digest = 4;
+ optional bool settings_diff = 9;
+ repeated string delete_setting = 10;
+ repeated GservicesSetting setting = 5;
+
+ optional bool market_ok = 6; // If Market got the market_checkin data OK.
+
+ optional fixed64 android_id = 7; // From the request, or newly assigned
+ optional fixed64 security_token = 8; // The associated security token
+
+ optional string version_info = 11;
+ // NEXT TAG: 12
+}
diff --git a/chromium/google_apis/gcm/tools/mcs_probe.cc b/chromium/google_apis/gcm/tools/mcs_probe.cc
index bc4ad7cad3a..1bb0bc14dfc 100644
--- a/chromium/google_apis/gcm/tools/mcs_probe.cc
+++ b/chromium/google_apis/gcm/tools/mcs_probe.cc
@@ -8,6 +8,7 @@
#include <cstddef>
#include <cstdio>
#include <string>
+#include <vector>
#include "base/at_exit.h"
#include "base/command_line.h"
@@ -20,11 +21,17 @@
#include "base/strings/string_number_conversions.h"
#include "base/threading/thread.h"
#include "base/threading/worker_pool.h"
+#include "base/time/default_clock.h"
#include "base/values.h"
+#include "google_apis/gcm/base/fake_encryptor.h"
#include "google_apis/gcm/base/mcs_message.h"
#include "google_apis/gcm/base/mcs_util.h"
+#include "google_apis/gcm/engine/checkin_request.h"
#include "google_apis/gcm/engine/connection_factory_impl.h"
+#include "google_apis/gcm/engine/gcm_store_impl.h"
+#include "google_apis/gcm/engine/gservices_settings.h"
#include "google_apis/gcm/engine/mcs_client.h"
+#include "google_apis/gcm/monitoring/fake_gcm_stats_recorder.h"
#include "net/base/host_mapping_rules.h"
#include "net/base/net_log_logger.h"
#include "net/cert/cert_verifier.h"
@@ -48,6 +55,35 @@
namespace gcm {
namespace {
+const net::BackoffEntry::Policy kDefaultBackoffPolicy = {
+ // Number of initial errors (in sequence) to ignore before applying
+ // exponential back-off rules.
+ 0,
+
+ // Initial delay for exponential back-off in ms.
+ 15000, // 15 seconds.
+
+ // Factor by which the waiting time will be multiplied.
+ 2,
+
+ // Fuzzing percentage. ex: 10% will spread requests randomly
+ // between 90%-100% of the calculated time.
+ 0.5, // 50%.
+
+ // Maximum amount of time we are willing to delay our request in ms.
+ 1000 * 60 * 5, // 5 minutes.
+
+ // Time to keep an entry from being discarded even when it
+ // has no significant state, -1 to never discard.
+ -1,
+
+ // Don't use initial delay unless the last request was an error.
+ false,
+};
+
+// Default values used to communicate with the check-in server.
+const char kChromeVersion[] = "Chrome MCS Probe";
+
// The default server to communicate with.
const char kMCSServerHost[] = "mtalk.google.com";
const uint16 kMCSServerPort = 5228;
@@ -82,8 +118,14 @@ void MessageReceivedCallback(const MCSMessage& message) {
}
}
-void MessageSentCallback(const std::string& local_id) {
- LOG(INFO) << "Message sent. Status: " << local_id;
+void MessageSentCallback(int64 user_serial_number,
+ const std::string& app_id,
+ const std::string& message_id,
+ MCSClient::MessageSendStatus status) {
+ LOG(INFO) << "Message sent. Serial number: " << user_serial_number
+ << " Application ID: " << app_id
+ << " Message ID: " << message_id
+ << " Message send status: " << status;
}
// Needed to use a real host resolver.
@@ -164,16 +206,22 @@ class MCSProbe {
uint64 secret() const { return secret_; }
private:
+ void CheckIn();
void InitializeNetworkState();
void BuildNetworkSession();
- void InitializationCallback(bool success,
- uint64 restored_android_id,
- uint64 restored_security_token);
+ void LoadCallback(scoped_ptr<GCMStore::LoadResult> load_result);
+ void UpdateCallback(bool success);
+ void ErrorCallback();
+ void OnCheckInCompleted(
+ const checkin_proto::AndroidCheckinResponse& checkin_response);
+ void StartMCSLogin();
+
+ base::DefaultClock clock_;
CommandLine command_line_;
- base::FilePath rmq_path_;
+ base::FilePath gcm_store_path_;
uint64 android_id_;
uint64 secret_;
std::string server_host_;
@@ -195,7 +243,10 @@ class MCSProbe {
scoped_refptr<net::HttpNetworkSession> network_session_;
scoped_ptr<net::ProxyService> proxy_service_;
+ FakeGCMStatsRecorder recorder_;
+ scoped_ptr<GCMStore> gcm_store_;
scoped_ptr<MCSClient> mcs_client_;
+ scoped_ptr<CheckinRequest> checkin_request_;
scoped_ptr<ConnectionFactoryImpl> connection_factory_;
@@ -208,14 +259,14 @@ MCSProbe::MCSProbe(
const CommandLine& command_line,
scoped_refptr<net::URLRequestContextGetter> url_request_context_getter)
: command_line_(command_line),
- rmq_path_(base::FilePath(FILE_PATH_LITERAL("gcm_rmq_store"))),
+ gcm_store_path_(base::FilePath(FILE_PATH_LITERAL("gcm_store"))),
android_id_(0),
secret_(0),
server_port_(0),
url_request_context_getter_(url_request_context_getter),
file_thread_("FileThread") {
if (command_line.HasSwitch(kRMQFileName)) {
- rmq_path_ = command_line.GetSwitchValuePath(kRMQFileName);
+ gcm_store_path_ = command_line.GetSwitchValuePath(kRMQFileName);
}
if (command_line.HasSwitch(kAndroidIdSwitch)) {
base::StringToUint64(command_line.GetSwitchValueASCII(kAndroidIdSwitch),
@@ -244,22 +295,64 @@ void MCSProbe::Start() {
file_thread_.Start();
InitializeNetworkState();
BuildNetworkSession();
+ std::vector<GURL> endpoints(1,
+ GURL("https://" +
+ net::HostPortPair(server_host_,
+ server_port_).ToString()));
connection_factory_.reset(
- new ConnectionFactoryImpl(GURL("https://" + net::HostPortPair(
- server_host_, server_port_).ToString()),
+ new ConnectionFactoryImpl(endpoints,
+ kDefaultBackoffPolicy,
network_session_,
- &net_log_));
- mcs_client_.reset(new MCSClient(rmq_path_,
+ &net_log_,
+ &recorder_));
+ gcm_store_.reset(
+ new GCMStoreImpl(gcm_store_path_,
+ file_thread_.message_loop_proxy(),
+ make_scoped_ptr<Encryptor>(new FakeEncryptor)));
+ mcs_client_.reset(new MCSClient("probe",
+ &clock_,
connection_factory_.get(),
- file_thread_.message_loop_proxy()));
+ gcm_store_.get(),
+ &recorder_));
run_loop_.reset(new base::RunLoop());
- mcs_client_->Initialize(base::Bind(&MCSProbe::InitializationCallback,
- base::Unretained(this)),
- base::Bind(&MessageReceivedCallback),
- base::Bind(&MessageSentCallback));
+ gcm_store_->Load(base::Bind(&MCSProbe::LoadCallback,
+ base::Unretained(this)));
run_loop_->Run();
}
+void MCSProbe::LoadCallback(scoped_ptr<GCMStore::LoadResult> load_result) {
+ DCHECK(load_result->success);
+ if (android_id_ != 0 && secret_ != 0) {
+ DVLOG(1) << "Presetting MCS id " << android_id_;
+ load_result->device_android_id = android_id_;
+ load_result->device_security_token = secret_;
+ gcm_store_->SetDeviceCredentials(android_id_,
+ secret_,
+ base::Bind(&MCSProbe::UpdateCallback,
+ base::Unretained(this)));
+ } else {
+ android_id_ = load_result->device_android_id;
+ secret_ = load_result->device_security_token;
+ DVLOG(1) << "Loaded MCS id " << android_id_;
+ }
+ mcs_client_->Initialize(
+ base::Bind(&MCSProbe::ErrorCallback, base::Unretained(this)),
+ base::Bind(&MessageReceivedCallback),
+ base::Bind(&MessageSentCallback),
+ load_result.Pass());
+
+ if (!android_id_ || !secret_) {
+ DVLOG(1) << "Checkin to generate new MCS credentials.";
+ CheckIn();
+ return;
+ }
+
+ StartMCSLogin();
+}
+
+void MCSProbe::UpdateCallback(bool success) {
+}
+
void MCSProbe::InitializeNetworkState() {
FILE* log_file = NULL;
if (command_line_.HasSwitch(kLogFileSwitch)) {
@@ -317,7 +410,6 @@ void MCSProbe::BuildNetworkSession() {
session_params.network_delegate = NULL; // TODO(zea): implement?
session_params.host_mapping_rules = host_mapping_rules_.get();
session_params.ignore_certificate_errors = true;
- session_params.http_pipelining_enabled = false;
session_params.testing_fixed_http_port = 0;
session_params.testing_fixed_https_port = 0;
session_params.net_log = &net_log_;
@@ -326,16 +418,59 @@ void MCSProbe::BuildNetworkSession() {
network_session_ = new net::HttpNetworkSession(session_params);
}
-void MCSProbe::InitializationCallback(bool success,
- uint64 restored_android_id,
- uint64 restored_security_token) {
- LOG(INFO) << "Initialization " << (success ? "success!" : "failure!");
- if (restored_android_id && restored_security_token) {
- android_id_ = restored_android_id;
- secret_ = restored_security_token;
- }
- if (success)
- mcs_client_->Login(android_id_, secret_);
+void MCSProbe::ErrorCallback() {
+ LOG(INFO) << "MCS error happened";
+}
+
+void MCSProbe::CheckIn() {
+ LOG(INFO) << "Check-in request initiated.";
+ checkin_proto::ChromeBuildProto chrome_build_proto;
+ chrome_build_proto.set_platform(
+ checkin_proto::ChromeBuildProto::PLATFORM_LINUX);
+ chrome_build_proto.set_channel(
+ checkin_proto::ChromeBuildProto::CHANNEL_CANARY);
+ chrome_build_proto.set_chrome_version(kChromeVersion);
+
+ CheckinRequest::RequestInfo request_info(
+ 0, 0, std::string(), chrome_build_proto);
+
+ checkin_request_.reset(new CheckinRequest(
+ GServicesSettings::DefaultCheckinURL(),
+ request_info,
+ kDefaultBackoffPolicy,
+ base::Bind(&MCSProbe::OnCheckInCompleted, base::Unretained(this)),
+ url_request_context_getter_.get(),
+ &recorder_));
+ checkin_request_->Start();
+}
+
+void MCSProbe::OnCheckInCompleted(
+ const checkin_proto::AndroidCheckinResponse& checkin_response) {
+ bool success = checkin_response.has_android_id() &&
+ checkin_response.android_id() != 0UL &&
+ checkin_response.has_security_token() &&
+ checkin_response.security_token() != 0UL;
+ LOG(INFO) << "Check-in request completion "
+ << (success ? "success!" : "failure!");
+
+ if (!success)
+ return;
+
+ android_id_ = checkin_response.android_id();
+ secret_ = checkin_response.security_token();
+
+ gcm_store_->SetDeviceCredentials(android_id_,
+ secret_,
+ base::Bind(&MCSProbe::UpdateCallback,
+ base::Unretained(this)));
+
+ StartMCSLogin();
+}
+
+void MCSProbe::StartMCSLogin() {
+ LOG(INFO) << "MCS login initiated.";
+
+ mcs_client_->Login(android_id_, secret_);
}
int MCSProbeMain(int argc, char* argv[]) {