| // 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 "components/invalidation/impl/gcm_network_channel.h" | 
 |  | 
 | #include <utility> | 
 |  | 
 | #include "base/base64url.h" | 
 | #include "base/bind.h" | 
 | #include "base/i18n/time_formatting.h" | 
 | #include "base/location.h" | 
 | #include "base/metrics/histogram_macros.h" | 
 | #include "base/sha1.h" | 
 | #include "base/single_thread_task_runner.h" | 
 | #include "base/strings/string_number_conversions.h" | 
 | #include "base/strings/string_util.h" | 
 | #include "base/threading/thread_task_runner_handle.h" | 
 | #include "build/build_config.h" | 
 | #include "components/data_use_measurement/core/data_use_user_data.h" | 
 | #include "components/invalidation/impl/gcm_network_channel_delegate.h" | 
 | #include "google_apis/gaia/google_service_auth_error.h" | 
 | #include "net/base/load_flags.h" | 
 | #include "net/http/http_status_code.h" | 
 | #include "net/traffic_annotation/network_traffic_annotation.h" | 
 | #include "services/network/public/cpp/resource_request.h" | 
 | #include "services/network/public/cpp/shared_url_loader_factory.h" | 
 | #include "services/network/public/cpp/simple_url_loader.h" | 
 |  | 
 | #if !defined(OS_ANDROID) | 
 | // channel_common.proto defines ANDROID constant that conflicts with Android | 
 | // build. At the same time TiclInvalidationService is not used on Android so it | 
 | // is safe to exclude these protos from Android build. | 
 | #include "google/cacheinvalidation/android_channel.pb.h" | 
 | #include "google/cacheinvalidation/channel_common.pb.h" | 
 | #include "google/cacheinvalidation/types.pb.h" | 
 | #endif | 
 |  | 
 | namespace syncer { | 
 |  | 
 | namespace { | 
 |  | 
 | const char kCacheInvalidationEndpointUrl[] = | 
 |     "https://clients4.google.com/invalidation/android/request/"; | 
 | const char kCacheInvalidationPackageName[] = "com.google.chrome.invalidations"; | 
 |  | 
 | // Register backoff policy. | 
 | const net::BackoffEntry::Policy kRegisterBackoffPolicy = { | 
 |   // Number of initial errors (in sequence) to ignore before applying | 
 |   // exponential back-off rules. | 
 |   0, | 
 |  | 
 |   // Initial delay for exponential back-off in ms. | 
 |   2000, // 2 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, | 
 | }; | 
 |  | 
 | // Incoming message status values for UMA_HISTOGRAM. | 
 | enum IncomingMessageStatus { | 
 |   INCOMING_MESSAGE_SUCCESS, | 
 |   MESSAGE_EMPTY,     // GCM message's content is missing or empty. | 
 |   INVALID_ENCODING,  // Base64Decode failed. | 
 |   INVALID_PROTO,     // Parsing protobuf failed. | 
 |  | 
 |   // This enum is used in UMA_HISTOGRAM_ENUMERATION. Insert new values above | 
 |   // this line. | 
 |   INCOMING_MESSAGE_STATUS_COUNT | 
 | }; | 
 |  | 
 | // Outgoing message status values for UMA_HISTOGRAM. | 
 | enum OutgoingMessageStatus { | 
 |   OUTGOING_MESSAGE_SUCCESS, | 
 |   MESSAGE_DISCARDED,     // New message started before old one was sent. | 
 |   ACCESS_TOKEN_FAILURE,  // Requeting access token failed. | 
 |   POST_FAILURE,          // HTTP Post failed. | 
 |  | 
 |   // This enum is used in UMA_HISTOGRAM_ENUMERATION. Insert new values above | 
 |   // this line. | 
 |   OUTGOING_MESSAGE_STATUS_COUNT | 
 | }; | 
 |  | 
 | const char kIncomingMessageStatusHistogram[] = | 
 |     "GCMInvalidations.IncomingMessageStatus"; | 
 | const char kOutgoingMessageStatusHistogram[] = | 
 |     "GCMInvalidations.OutgoingMessageStatus"; | 
 |  | 
 | void RecordIncomingMessageStatus(IncomingMessageStatus status) { | 
 |   UMA_HISTOGRAM_ENUMERATION(kIncomingMessageStatusHistogram, | 
 |                             status, | 
 |                             INCOMING_MESSAGE_STATUS_COUNT); | 
 | } | 
 |  | 
 | void RecordOutgoingMessageStatus(OutgoingMessageStatus status) { | 
 |   UMA_HISTOGRAM_ENUMERATION(kOutgoingMessageStatusHistogram, | 
 |                             status, | 
 |                             OUTGOING_MESSAGE_STATUS_COUNT); | 
 | } | 
 |  | 
 | }  // namespace | 
 |  | 
 | GCMNetworkChannel::GCMNetworkChannel( | 
 |     scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory, | 
 |     network::NetworkConnectionTracker* network_connection_tracker, | 
 |     std::unique_ptr<GCMNetworkChannelDelegate> delegate) | 
 |     : url_loader_factory_(std::move(url_loader_factory)), | 
 |       network_connection_tracker_(network_connection_tracker), | 
 |       delegate_(std::move(delegate)), | 
 |       register_backoff_entry_(new net::BackoffEntry(&kRegisterBackoffPolicy)), | 
 |       gcm_channel_online_(false), | 
 |       http_channel_online_(false), | 
 |       diagnostic_info_(this), | 
 |       weak_factory_(this) { | 
 |   network_connection_tracker_->AddNetworkConnectionObserver(this); | 
 |   delegate_->Initialize( | 
 |       base::Bind(&GCMNetworkChannel::OnConnectionStateChanged, | 
 |                  weak_factory_.GetWeakPtr()), | 
 |       base::Bind(&GCMNetworkChannel::OnStoreReset, weak_factory_.GetWeakPtr())); | 
 |   Register(); | 
 | } | 
 |  | 
 | GCMNetworkChannel::~GCMNetworkChannel() { | 
 |   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); | 
 |   network_connection_tracker_->RemoveNetworkConnectionObserver(this); | 
 | } | 
 |  | 
 | void GCMNetworkChannel::Register() { | 
 |   delegate_->Register(base::Bind(&GCMNetworkChannel::OnRegisterComplete, | 
 |                                  weak_factory_.GetWeakPtr())); | 
 | } | 
 |  | 
 | void GCMNetworkChannel::OnRegisterComplete( | 
 |     const std::string& registration_id, | 
 |     gcm::GCMClient::Result result) { | 
 |   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); | 
 |   if (result == gcm::GCMClient::SUCCESS) { | 
 |     DCHECK(!registration_id.empty()); | 
 |     DVLOG(2) << "Got registration_id"; | 
 |     register_backoff_entry_->Reset(); | 
 |     registration_id_ = registration_id; | 
 |     if (!cached_message_.empty()) | 
 |       RequestAccessToken(); | 
 |   } else { | 
 |     DVLOG(2) << "Register failed: " << result; | 
 |     // Retry in case of transient error. | 
 |     switch (result) { | 
 |       case gcm::GCMClient::NETWORK_ERROR: | 
 |       case gcm::GCMClient::SERVER_ERROR: | 
 |       case gcm::GCMClient::TTL_EXCEEDED: | 
 |       case gcm::GCMClient::UNKNOWN_ERROR: { | 
 |         register_backoff_entry_->InformOfRequest(false); | 
 |         base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( | 
 |             FROM_HERE, | 
 |             base::BindOnce(&GCMNetworkChannel::Register, | 
 |                            weak_factory_.GetWeakPtr()), | 
 |             register_backoff_entry_->GetTimeUntilRelease()); | 
 |         break; | 
 |       } | 
 |       default: | 
 |         break; | 
 |     } | 
 |   } | 
 |   diagnostic_info_.registration_id_ = registration_id_; | 
 |   diagnostic_info_.registration_result_ = result; | 
 | } | 
 |  | 
 | void GCMNetworkChannel::SendMessage(const std::string& message) { | 
 |   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); | 
 |   DCHECK(!message.empty()); | 
 |   DVLOG(2) << "SendMessage"; | 
 |   diagnostic_info_.sent_messages_count_++; | 
 |   if (!cached_message_.empty()) { | 
 |     RecordOutgoingMessageStatus(MESSAGE_DISCARDED); | 
 |   } | 
 |   cached_message_ = message; | 
 |  | 
 |   if (!registration_id_.empty()) { | 
 |     RequestAccessToken(); | 
 |   } | 
 | } | 
 |  | 
 | void GCMNetworkChannel::RequestAccessToken() { | 
 |   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); | 
 |   delegate_->RequestToken(base::Bind(&GCMNetworkChannel::OnGetTokenComplete, | 
 |                                      weak_factory_.GetWeakPtr())); | 
 | } | 
 |  | 
 | void GCMNetworkChannel::OnGetTokenComplete( | 
 |     const GoogleServiceAuthError& error, | 
 |     const std::string& token) { | 
 |   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); | 
 |   if (cached_message_.empty() || registration_id_.empty()) { | 
 |     // Nothing to do. | 
 |     return; | 
 |   } | 
 |  | 
 |   if (error.state() != GoogleServiceAuthError::NONE) { | 
 |     // Requesting access token failed. Persistent errors will be reported by | 
 |     // token service. Just drop this request, cacheinvalidations will retry | 
 |     // sending message and at that time we'll retry requesting access token. | 
 |     DVLOG(1) << "RequestAccessToken failed: " << error.ToString(); | 
 |     RecordOutgoingMessageStatus(ACCESS_TOKEN_FAILURE); | 
 |     // Message won't get sent. Notify that http channel doesn't work. | 
 |     UpdateHttpChannelState(false); | 
 |     cached_message_.clear(); | 
 |     return; | 
 |   } | 
 |   DCHECK(!token.empty()); | 
 |   // Save access token in case POST fails and we need to invalidate it. | 
 |   access_token_ = token; | 
 |  | 
 |   DVLOG(2) << "Got access token, sending message"; | 
 |   net::NetworkTrafficAnnotationTag traffic_annotation = | 
 |       net::DefineNetworkTrafficAnnotation("invalidation_service", R"( | 
 |         semantics { | 
 |           sender: "Invalidation service" | 
 |           description: | 
 |             "Chromium uses cacheinvalidation library to receive push " | 
 |             "notifications from the server about sync items (bookmarks, " | 
 |             "passwords, preferences, etc.) modified on other clients. It uses " | 
 |             "GCMClient to receive incoming messages. This request is used for " | 
 |             "client-to-server communications." | 
 |           trigger: | 
 |             "The first message is sent to register client with the server on " | 
 |             "Chromium startup. It is then sent periodically to confirm that " | 
 |             "the client is still online. After receiving notification about " | 
 |             "server changes, the client sends this request to acknowledge that " | 
 |             "the notification is processed." | 
 |           data: | 
 |             "Different in each use case:\n" | 
 |             "- Initial registration: Doesn't contain user data.\n" | 
 |             "- Updating the set of subscriptions: Contains server generated " | 
 |             "client_token and ObjectIds identifying subscriptions. ObjectId " | 
 |             "is not unique to user.\n" | 
 |             "- Invalidation acknowledgement: Contains client_token, ObjectId " | 
 |             "and server version for corresponding subscription. Version is not " | 
 |             "related to sync data, it is an internal concept of invalidations " | 
 |             "protocol." | 
 |           destination: GOOGLE_OWNED_SERVICE | 
 |         } | 
 |         policy { | 
 |           cookies_allowed: NO | 
 |           setting: "This feature cannot be disabled." | 
 |           policy_exception_justification: | 
 |             "Not implemented. Disabling InvalidationService might break " | 
 |             "features that depend on it. It makes sense to control top level " | 
 |             "features that use InvalidationService." | 
 |         })"); | 
 |  | 
 |   auto resource_request = std::make_unique<network::ResourceRequest>(); | 
 |   resource_request->url = BuildUrl(registration_id_); | 
 |   resource_request->load_flags = | 
 |       net::LOAD_DO_NOT_SEND_COOKIES | net::LOAD_DO_NOT_SAVE_COOKIES; | 
 |   resource_request->method = "POST"; | 
 |   resource_request->headers.SetHeader(net::HttpRequestHeaders::kAuthorization, | 
 |                                       "Bearer " + access_token_); | 
 |   if (!echo_token_.empty()) { | 
 |     resource_request->headers.SetHeader("echo-token", echo_token_); | 
 |   } | 
 |   simple_url_loader_ = network::SimpleURLLoader::Create( | 
 |       std::move(resource_request), traffic_annotation); | 
 |   simple_url_loader_->AttachStringForUpload(cached_message_, | 
 |                                             "application/x-protobuffer"); | 
 |   // TODO(https://crbug.com/808498): Re-add data use measurement once | 
 |   // SimpleURLLoader supports it. | 
 |   // ID=data_use_measurement::DataUseUserData::INVALIDATION | 
 |   simple_url_loader_->DownloadToStringOfUnboundedSizeUntilCrashAndDie( | 
 |       url_loader_factory_.get(), | 
 |       base::BindOnce(&GCMNetworkChannel::OnSimpleLoaderComplete, | 
 |                      base::Unretained(this))); | 
 |   // Clear message to prevent accidentally resending it in the future. | 
 |   cached_message_.clear(); | 
 | } | 
 |  | 
 | void GCMNetworkChannel::OnSimpleLoaderComplete( | 
 |     std::unique_ptr<std::string> response_body) { | 
 |   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); | 
 |  | 
 |   int net_error = simple_url_loader_->NetError(); | 
 |   bool is_success = (net_error == net::OK); | 
 |   int response_code = -1; | 
 |   if (simple_url_loader_->ResponseInfo() && | 
 |       simple_url_loader_->ResponseInfo()->headers) { | 
 |     response_code = | 
 |         simple_url_loader_->ResponseInfo()->headers->response_code(); | 
 |   } | 
 |   simple_url_loader_.reset(); | 
 |   diagnostic_info_.last_post_response_code_ = | 
 |       (response_code / 100 != 2 || is_success) ? response_code : net_error; | 
 |  | 
 |   if (response_code == net::HTTP_UNAUTHORIZED) { | 
 |     DVLOG(1) << "SimpleURLLoader failure: HTTP_UNAUTHORIZED"; | 
 |     delegate_->InvalidateToken(access_token_); | 
 |   } | 
 |  | 
 |   if (!response_body) { | 
 |     DVLOG(1) << "SimpleURLLoader failure"; | 
 |     RecordOutgoingMessageStatus(POST_FAILURE); | 
 |     // POST failed. Notify that http channel doesn't work. | 
 |     UpdateHttpChannelState(false); | 
 |     return; | 
 |   } | 
 |  | 
 |   RecordOutgoingMessageStatus(OUTGOING_MESSAGE_SUCCESS); | 
 |   // Successfully sent message. Http channel works. | 
 |   UpdateHttpChannelState(true); | 
 |   DVLOG(2) << "SimpleURLLoader success"; | 
 | } | 
 |  | 
 | void GCMNetworkChannel::OnIncomingMessage(const std::string& message, | 
 |                                           const std::string& echo_token) { | 
 | #if !defined(OS_ANDROID) | 
 |   if (!echo_token.empty()) | 
 |     echo_token_ = echo_token; | 
 |   diagnostic_info_.last_message_empty_echo_token_ = echo_token.empty(); | 
 |   diagnostic_info_.last_message_received_time_ = base::Time::Now(); | 
 |  | 
 |   if (message.empty()) { | 
 |     RecordIncomingMessageStatus(MESSAGE_EMPTY); | 
 |     return; | 
 |   } | 
 |   std::string data; | 
 |   if (!base::Base64UrlDecode( | 
 |           message, base::Base64UrlDecodePolicy::IGNORE_PADDING, &data)) { | 
 |     RecordIncomingMessageStatus(INVALID_ENCODING); | 
 |     return; | 
 |   } | 
 |   ipc::invalidation::AddressedAndroidMessage android_message; | 
 |   if (!android_message.ParseFromString(data) || | 
 |       !android_message.has_message()) { | 
 |     RecordIncomingMessageStatus(INVALID_PROTO); | 
 |     return; | 
 |   } | 
 |   DVLOG(2) << "Deliver incoming message"; | 
 |   RecordIncomingMessageStatus(INCOMING_MESSAGE_SUCCESS); | 
 |   UpdateGcmChannelState(true); | 
 |   DeliverIncomingMessage(android_message.message()); | 
 | #else | 
 |   // This code shouldn't be invoked on Android. | 
 |   NOTREACHED(); | 
 | #endif | 
 | } | 
 |  | 
 | void GCMNetworkChannel::OnConnectionStateChanged(bool online) { | 
 |   UpdateGcmChannelState(online); | 
 | } | 
 |  | 
 | void GCMNetworkChannel::OnStoreReset() { | 
 |   // TODO(crbug.com/661660): Tell server the registration ID is no longer valid. | 
 |   registration_id_.clear(); | 
 | } | 
 |  | 
 | void GCMNetworkChannel::OnConnectionChanged( | 
 |     network::mojom::ConnectionType connection_type) { | 
 |   // Network connection is restored. Let's notify cacheinvalidations so it has | 
 |   // chance to retry. | 
 |   NotifyNetworkStatusChange(connection_type != | 
 |                             network::mojom::ConnectionType::CONNECTION_NONE); | 
 | } | 
 |  | 
 | void GCMNetworkChannel::UpdateGcmChannelState(bool online) { | 
 |   if (gcm_channel_online_ == online) | 
 |     return; | 
 |   gcm_channel_online_ = online; | 
 |   InvalidatorState channel_state = TRANSIENT_INVALIDATION_ERROR; | 
 |   if (gcm_channel_online_ && http_channel_online_) | 
 |     channel_state = INVALIDATIONS_ENABLED; | 
 |   NotifyChannelStateChange(channel_state); | 
 | } | 
 |  | 
 | void GCMNetworkChannel::UpdateHttpChannelState(bool online) { | 
 |   if (http_channel_online_ == online) | 
 |     return; | 
 |   http_channel_online_ = online; | 
 |   InvalidatorState channel_state = TRANSIENT_INVALIDATION_ERROR; | 
 |   if (gcm_channel_online_ && http_channel_online_) | 
 |     channel_state = INVALIDATIONS_ENABLED; | 
 |   NotifyChannelStateChange(channel_state); | 
 | } | 
 |  | 
 | GURL GCMNetworkChannel::BuildUrl(const std::string& registration_id) { | 
 |   DCHECK(!registration_id.empty()); | 
 |  | 
 | #if !defined(OS_ANDROID) | 
 |   ipc::invalidation::EndpointId endpoint_id; | 
 |   endpoint_id.set_c2dm_registration_id(registration_id); | 
 |   endpoint_id.set_client_key(std::string()); | 
 |   endpoint_id.set_package_name(kCacheInvalidationPackageName); | 
 |   endpoint_id.mutable_channel_version()->set_major_version( | 
 |       ipc::invalidation::INITIAL); | 
 |   std::string endpoint_id_buffer; | 
 |   endpoint_id.SerializeToString(&endpoint_id_buffer); | 
 |  | 
 |   ipc::invalidation::NetworkEndpointId network_endpoint_id; | 
 |   network_endpoint_id.set_network_address( | 
 |       ipc::invalidation::NetworkEndpointId_NetworkAddress_ANDROID); | 
 |   network_endpoint_id.set_client_address(endpoint_id_buffer); | 
 |   std::string network_endpoint_id_buffer; | 
 |   network_endpoint_id.SerializeToString(&network_endpoint_id_buffer); | 
 |  | 
 |   std::string base64URLPiece; | 
 |   base::Base64UrlEncode( | 
 |       network_endpoint_id_buffer, base::Base64UrlEncodePolicy::OMIT_PADDING, | 
 |       &base64URLPiece); | 
 |  | 
 |   std::string url(kCacheInvalidationEndpointUrl); | 
 |   url += base64URLPiece; | 
 |   return GURL(url); | 
 | #else | 
 |   // This code shouldn't be invoked on Android. | 
 |   NOTREACHED(); | 
 |   return GURL(); | 
 | #endif | 
 | } | 
 |  | 
 | void GCMNetworkChannel::SetMessageReceiver( | 
 |     invalidation::MessageCallback* incoming_receiver) { | 
 |   delegate_->SetMessageReceiver(base::Bind( | 
 |       &GCMNetworkChannel::OnIncomingMessage, weak_factory_.GetWeakPtr())); | 
 |   SyncNetworkChannel::SetMessageReceiver(incoming_receiver); | 
 | } | 
 |  | 
 | void GCMNetworkChannel::RequestDetailedStatus( | 
 |     base::Callback<void(const base::DictionaryValue&)> callback) { | 
 |   callback.Run(*diagnostic_info_.CollectDebugData()); | 
 | } | 
 |  | 
 | void GCMNetworkChannel::UpdateCredentials(const std::string& email, | 
 |                                           const std::string& token) { | 
 |   // Do nothing. We get access token by requesting it for every message. | 
 | } | 
 |  | 
 | int GCMNetworkChannel::GetInvalidationClientType() { | 
 | #if defined(OS_IOS) | 
 |   return ipc::invalidation::ClientType::CHROME_SYNC_GCM_IOS; | 
 | #else | 
 |   return ipc::invalidation::ClientType::CHROME_SYNC_GCM_DESKTOP; | 
 | #endif | 
 | } | 
 |  | 
 | void GCMNetworkChannel::ResetRegisterBackoffEntryForTest( | 
 |     const net::BackoffEntry::Policy* policy) { | 
 |   register_backoff_entry_.reset(new net::BackoffEntry(policy)); | 
 | } | 
 |  | 
 | GCMNetworkChannelDiagnostic::GCMNetworkChannelDiagnostic( | 
 |     GCMNetworkChannel* parent) | 
 |     : parent_(parent), | 
 |       last_message_empty_echo_token_(false), | 
 |       last_post_response_code_(0), | 
 |       registration_result_(gcm::GCMClient::UNKNOWN_ERROR), | 
 |       sent_messages_count_(0) {} | 
 |  | 
 | std::unique_ptr<base::DictionaryValue> | 
 | GCMNetworkChannelDiagnostic::CollectDebugData() const { | 
 |   std::unique_ptr<base::DictionaryValue> status(new base::DictionaryValue); | 
 |   status->SetString("GCMNetworkChannel.Channel", "GCM"); | 
 |   std::string reg_id_hash = base::SHA1HashString(registration_id_); | 
 |   status->SetString("GCMNetworkChannel.HashedRegistrationID", | 
 |                     base::HexEncode(reg_id_hash.c_str(), reg_id_hash.size())); | 
 |   status->SetString("GCMNetworkChannel.RegistrationResult", | 
 |                     GCMClientResultToString(registration_result_)); | 
 |   status->SetBoolean("GCMNetworkChannel.HadLastMessageEmptyEchoToken", | 
 |                      last_message_empty_echo_token_); | 
 |   status->SetString( | 
 |       "GCMNetworkChannel.LastMessageReceivedTime", | 
 |       base::TimeFormatShortDateAndTime(last_message_received_time_)); | 
 |   status->SetInteger("GCMNetworkChannel.LastPostResponseCode", | 
 |                      last_post_response_code_); | 
 |   status->SetInteger("GCMNetworkChannel.SentMessages", sent_messages_count_); | 
 |   status->SetInteger("GCMNetworkChannel.ReceivedMessages", | 
 |                      parent_->GetReceivedMessagesCount()); | 
 |   return status; | 
 | } | 
 |  | 
 | std::string GCMNetworkChannelDiagnostic::GCMClientResultToString( | 
 |     const gcm::GCMClient::Result result) const { | 
 | #define ENUM_CASE(x) case x: return #x; break; | 
 |   switch (result) { | 
 |     ENUM_CASE(gcm::GCMClient::SUCCESS); | 
 |     ENUM_CASE(gcm::GCMClient::NETWORK_ERROR); | 
 |     ENUM_CASE(gcm::GCMClient::SERVER_ERROR); | 
 |     ENUM_CASE(gcm::GCMClient::TTL_EXCEEDED); | 
 |     ENUM_CASE(gcm::GCMClient::UNKNOWN_ERROR); | 
 |     ENUM_CASE(gcm::GCMClient::INVALID_PARAMETER); | 
 |     ENUM_CASE(gcm::GCMClient::ASYNC_OPERATION_PENDING); | 
 |     ENUM_CASE(gcm::GCMClient::GCM_DISABLED); | 
 |   } | 
 |   NOTREACHED(); | 
 |   return ""; | 
 | } | 
 |  | 
 | }  // namespace syncer |