diff options
Diffstat (limited to 'chromium/components/gcm_driver/gcm_client_impl.cc')
-rw-r--r-- | chromium/components/gcm_driver/gcm_client_impl.cc | 1481 |
1 files changed, 1481 insertions, 0 deletions
diff --git a/chromium/components/gcm_driver/gcm_client_impl.cc b/chromium/components/gcm_driver/gcm_client_impl.cc new file mode 100644 index 00000000000..016228a0326 --- /dev/null +++ b/chromium/components/gcm_driver/gcm_client_impl.cc @@ -0,0 +1,1481 @@ +// 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/gcm_driver/gcm_client_impl.h" + +#include <stddef.h> + +#include <memory> +#include <utility> + +#include "base/bind.h" +#include "base/files/file_path.h" +#include "base/location.h" +#include "base/logging.h" +#include "base/memory/ptr_util.h" +#include "base/metrics/histogram_functions.h" +#include "base/metrics/histogram_macros.h" +#include "base/strings/string_number_conversions.h" +#include "base/task/sequenced_task_runner.h" +#include "base/task/single_thread_task_runner.h" +#include "base/time/default_clock.h" +#include "base/timer/timer.h" +#include "components/crx_file/id_util.h" +#include "components/gcm_driver/crypto/gcm_decryption_result.h" +#include "components/gcm_driver/features.h" +#include "components/gcm_driver/gcm_account_mapper.h" +#include "components/gcm_driver/gcm_backoff_policy.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/engine/checkin_request.h" +#include "google_apis/gcm/engine/connection_factory_impl.h" +#include "google_apis/gcm/engine/gcm_registration_request_handler.h" +#include "google_apis/gcm/engine/gcm_store_impl.h" +#include "google_apis/gcm/engine/gcm_unregistration_request_handler.h" +#include "google_apis/gcm/engine/instance_id_delete_token_request_handler.h" +#include "google_apis/gcm/engine/instance_id_get_token_request_handler.h" +#include "google_apis/gcm/monitoring/gcm_stats_recorder.h" +#include "google_apis/gcm/protocol/checkin.pb.h" +#include "google_apis/gcm/protocol/mcs.pb.h" +#include "services/network/public/cpp/shared_url_loader_factory.h" +#include "url/gurl.h" + +namespace gcm { + +// It is okay to append to the enum if these states grow. DO NOT reorder, +// renumber or otherwise reuse existing values. +// Do not assign an explicit value to REGISTRATION_CACHE_STATUS_COUNT, as +// this lets the compiler keep it up to date. +enum class RegistrationCacheStatus { + REGISTRATION_NOT_FOUND = 0, + REGISTRATION_FOUND_AND_FRESH = 1, + REGISTRATION_FOUND_BUT_STALE = 2, + REGISTRATION_FOUND_BUT_SENDERS_DONT_MATCH = 3, + // NOTE: always keep this entry at the end. Add new value only immediately + // above this line. Make sure to update the corresponding histogram enum + // accordingly. + REGISTRATION_CACHE_STATUS_COUNT +}; + +namespace { + +// Indicates a message type of the received message. +enum MessageType { + UNKNOWN, // Undetermined type. + DATA_MESSAGE, // Regular data message. + DELETED_MESSAGES, // Messages were deleted on the server. + SEND_ERROR, // Error sending a message. +}; + +enum ResetStoreError { + DESTROYING_STORE_FAILED, + INFINITE_STORE_RESET, + // NOTE: always keep this entry at the end. Add new value only immediately + // above this line. Make sure to update the corresponding histogram enum + // accordingly. + RESET_STORE_ERROR_COUNT +}; + +const int kMaxRegistrationRetries = 5; +const int kMaxUnregistrationRetries = 5; +const char kDeletedCountKey[] = "total_deleted"; +const char kMessageTypeDataMessage[] = "gcm"; +const char kMessageTypeDeletedMessagesKey[] = "deleted_messages"; +const char kMessageTypeKey[] = "message_type"; +const char kMessageTypeSendErrorKey[] = "send_error"; +const char kSubtypeKey[] = "subtype"; +const char kSendMessageFromValue[] = "gcm@chrome.com"; +const int64_t kDefaultUserSerialNumber = 0LL; +const int kDestroyGCMStoreDelayMS = 5 * 60 * 1000; // 5 minutes. + +GCMClient::Result ToGCMClientResult(MCSClient::MessageSendStatus status) { + switch (status) { + case MCSClient::QUEUED: + return GCMClient::SUCCESS; + case MCSClient::MESSAGE_TOO_LARGE: + return GCMClient::INVALID_PARAMETER; + case MCSClient::QUEUE_SIZE_LIMIT_REACHED: + case MCSClient::APP_QUEUE_SIZE_LIMIT_REACHED: + case MCSClient::NO_CONNECTION_ON_ZERO_TTL: + case MCSClient::TTL_EXCEEDED: + return GCMClient::NETWORK_ERROR; + case MCSClient::SENT: + case MCSClient::SEND_STATUS_COUNT: + NOTREACHED(); + break; + } + return GCMClientImpl::UNKNOWN_ERROR; +} + +void ToCheckinProtoVersion( + const GCMClient::ChromeBuildInfo& chrome_build_info, + checkin_proto::ChromeBuildProto* android_build_info) { + checkin_proto::ChromeBuildProto_Platform platform = + checkin_proto::ChromeBuildProto_Platform_PLATFORM_LINUX; + switch (chrome_build_info.platform) { + case GCMClient::PLATFORM_WIN: + platform = checkin_proto::ChromeBuildProto_Platform_PLATFORM_WIN; + break; + case GCMClient::PLATFORM_MAC: + platform = checkin_proto::ChromeBuildProto_Platform_PLATFORM_MAC; + break; + case GCMClient::PLATFORM_LINUX: + platform = checkin_proto::ChromeBuildProto_Platform_PLATFORM_LINUX; + break; + case GCMClient::PLATFORM_IOS: + platform = checkin_proto::ChromeBuildProto_Platform_PLATFORM_IOS; + break; + case GCMClient::PLATFORM_ANDROID: + platform = checkin_proto::ChromeBuildProto_Platform_PLATFORM_ANDROID; + break; + case GCMClient::PLATFORM_CROS: + platform = checkin_proto::ChromeBuildProto_Platform_PLATFORM_CROS; + break; + case GCMClient::PLATFORM_UNSPECIFIED: + // For unknown platform, return as LINUX. + platform = checkin_proto::ChromeBuildProto_Platform_PLATFORM_LINUX; + break; + } + android_build_info->set_platform(platform); + + checkin_proto::ChromeBuildProto_Channel channel = + checkin_proto::ChromeBuildProto_Channel_CHANNEL_UNKNOWN; + switch (chrome_build_info.channel) { + case GCMClient::CHANNEL_STABLE: + channel = checkin_proto::ChromeBuildProto_Channel_CHANNEL_STABLE; + break; + case GCMClient::CHANNEL_BETA: + channel = checkin_proto::ChromeBuildProto_Channel_CHANNEL_BETA; + break; + case GCMClient::CHANNEL_DEV: + channel = checkin_proto::ChromeBuildProto_Channel_CHANNEL_DEV; + break; + case GCMClient::CHANNEL_CANARY: + channel = checkin_proto::ChromeBuildProto_Channel_CHANNEL_CANARY; + break; + case GCMClient::CHANNEL_UNKNOWN: + channel = checkin_proto::ChromeBuildProto_Channel_CHANNEL_UNKNOWN; + break; + } + android_build_info->set_channel(channel); + + android_build_info->set_chrome_version(chrome_build_info.version); +} + +MessageType DecodeMessageType(const std::string& value) { + if (kMessageTypeDeletedMessagesKey == value) + return DELETED_MESSAGES; + if (kMessageTypeSendErrorKey == value) + return SEND_ERROR; + if (kMessageTypeDataMessage == value) + return DATA_MESSAGE; + return UNKNOWN; +} + +int ConstructGCMVersion(const std::string& chrome_version) { + // Major Chrome version is passed as GCM version. + size_t pos = chrome_version.find('.'); + if (pos == std::string::npos) { + NOTREACHED(); + return 0; + } + + int gcm_version = 0; + base::StringToInt( + base::StringPiece(chrome_version.c_str(), pos), &gcm_version); + return gcm_version; +} + +std::string SerializeInstanceIDData(const std::string& instance_id, + const std::string& extra_data) { + DCHECK(!instance_id.empty() && !extra_data.empty()); + DCHECK(instance_id.find(',') == std::string::npos); + return instance_id + "," + extra_data; +} + +bool DeserializeInstanceIDData(const std::string& serialized_data, + std::string* instance_id, + std::string* extra_data) { + DCHECK(instance_id && extra_data); + std::size_t pos = serialized_data.find(','); + if (pos == std::string::npos) + return false; + *instance_id = serialized_data.substr(0, pos); + *extra_data = serialized_data.substr(pos + 1); + return !instance_id->empty() && !extra_data->empty(); +} + +bool InstanceIDUsesSubtypeForAppId(const std::string& app_id) { + // Always use subtypes with Instance ID, except for Chrome Apps/Extensions. + return !crx_file::id_util::IdIsValid(app_id); +} + +void RecordResetStoreErrorToUMA(ResetStoreError error) { + UMA_HISTOGRAM_ENUMERATION("GCM.ResetStore", error, RESET_STORE_ERROR_COUNT); +} + +} // namespace + +void RecordRegistrationRequestToUMA(gcm::RegistrationCacheStatus status) { + UMA_HISTOGRAM_ENUMERATION( + "GCM.RegistrationCacheStatus", status, + RegistrationCacheStatus::REGISTRATION_CACHE_STATUS_COUNT); +} +GCMInternalsBuilder::GCMInternalsBuilder() {} +GCMInternalsBuilder::~GCMInternalsBuilder() {} + +base::Clock* GCMInternalsBuilder::GetClock() { + return base::DefaultClock::GetInstance(); +} + +std::unique_ptr<MCSClient> GCMInternalsBuilder::BuildMCSClient( + const std::string& version, + base::Clock* clock, + ConnectionFactory* connection_factory, + GCMStore* gcm_store, + scoped_refptr<base::SequencedTaskRunner> io_task_runner, + GCMStatsRecorder* recorder) { + return std::make_unique<MCSClient>(version, clock, connection_factory, + gcm_store, std::move(io_task_runner), + recorder); +} + +std::unique_ptr<ConnectionFactory> GCMInternalsBuilder::BuildConnectionFactory( + const std::vector<GURL>& endpoints, + const net::BackoffEntry::Policy& backoff_policy, + base::RepeatingCallback<void( + mojo::PendingReceiver<network::mojom::ProxyResolvingSocketFactory>)> + get_socket_factory_callback, + scoped_refptr<base::SequencedTaskRunner> io_task_runner, + GCMStatsRecorder* recorder, + network::NetworkConnectionTracker* network_connection_tracker) { + return std::make_unique<ConnectionFactoryImpl>( + endpoints, backoff_policy, std::move(get_socket_factory_callback), + std::move(io_task_runner), recorder, network_connection_tracker); +} + +GCMClientImpl::CheckinInfo::CheckinInfo() + : android_id(0), secret(0), accounts_set(false) { +} + +GCMClientImpl::CheckinInfo::~CheckinInfo() { +} + +void GCMClientImpl::CheckinInfo::SnapshotCheckinAccounts() { + last_checkin_accounts.clear(); + for (auto iter = account_tokens.begin(); iter != account_tokens.end(); + ++iter) { + last_checkin_accounts.insert(iter->first); + } +} + +void GCMClientImpl::CheckinInfo::Reset() { + android_id = 0; + secret = 0; + accounts_set = false; + account_tokens.clear(); + last_checkin_accounts.clear(); +} + +GCMClientImpl::GCMClientImpl( + std::unique_ptr<GCMInternalsBuilder> internals_builder) + : internals_builder_(std::move(internals_builder)), + state_(UNINITIALIZED), + delegate_(nullptr), + start_mode_(DELAYED_START), + clock_(internals_builder_->GetClock()), + gcm_store_reset_(false), + network_connection_tracker_(nullptr) {} + +GCMClientImpl::~GCMClientImpl() { +} + +void GCMClientImpl::Initialize( + const ChromeBuildInfo& chrome_build_info, + const base::FilePath& path, + bool remove_account_mappings_with_email_key, + const scoped_refptr<base::SequencedTaskRunner>& blocking_task_runner, + scoped_refptr<base::SequencedTaskRunner> io_task_runner, + base::RepeatingCallback<void( + mojo::PendingReceiver<network::mojom::ProxyResolvingSocketFactory>)> + get_socket_factory_callback, + const scoped_refptr<network::SharedURLLoaderFactory>& url_loader_factory, + network::NetworkConnectionTracker* network_connection_tracker, + std::unique_ptr<Encryptor> encryptor, + GCMClient::Delegate* delegate) { + DCHECK_EQ(UNINITIALIZED, state_); + DCHECK(delegate); + DCHECK(io_task_runner); + DCHECK(io_task_runner->RunsTasksInCurrentSequence()); + + get_socket_factory_callback_ = std::move(get_socket_factory_callback); + url_loader_factory_ = url_loader_factory; + network_connection_tracker_ = network_connection_tracker; + chrome_build_info_ = chrome_build_info; + gcm_store_ = std::make_unique<GCMStoreImpl>( + path, remove_account_mappings_with_email_key, blocking_task_runner, + std::move(encryptor)); + delegate_ = delegate; + io_task_runner_ = std::move(io_task_runner); + recorder_.SetDelegate(this); + state_ = INITIALIZED; +} + +void GCMClientImpl::Start(StartMode start_mode) { + DCHECK_NE(UNINITIALIZED, state_); + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + + if (state_ == LOADED) { + // Start the GCM if not yet. + if (start_mode == IMMEDIATE_START) { + // Give up the scheduling to wipe out the store since now some one starts + // to use GCM. + destroying_gcm_store_ptr_factory_.InvalidateWeakPtrs(); + + StartGCM(); + } + return; + } + + // The delay start behavior will be abandoned when Start has been called + // once with IMMEDIATE_START behavior. + if (start_mode == IMMEDIATE_START) + start_mode_ = IMMEDIATE_START; + + // Bail out if the loading is not started or completed. + if (state_ != INITIALIZED) + return; + + // Once the loading is completed, the check-in will be initiated. + // If we're in lazy start mode, don't create a new store since none is really + // using GCM functionality yet. + gcm_store_->Load((start_mode == IMMEDIATE_START) ? GCMStore::CREATE_IF_MISSING + : GCMStore::DO_NOT_CREATE, + base::BindOnce(&GCMClientImpl::OnLoadCompleted, + weak_ptr_factory_.GetWeakPtr())); + state_ = LOADING; +} + +void GCMClientImpl::OnLoadCompleted( + std::unique_ptr<GCMStore::LoadResult> result) { + DCHECK_EQ(LOADING, state_); + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + + if (!result->success) { + if (result->store_does_not_exist) { + if (start_mode_ == IMMEDIATE_START) { + // An immediate start was requested during the delayed start that just + // completed. Perform it now. + gcm_store_->Load(GCMStore::CREATE_IF_MISSING, + base::BindOnce(&GCMClientImpl::OnLoadCompleted, + weak_ptr_factory_.GetWeakPtr())); + } else { + // In the case that the store does not exist, set |state_| back to + // INITIALIZED such that store loading could be triggered again when + // Start() is called with IMMEDIATE_START. + state_ = INITIALIZED; + } + } else { + // Otherwise, destroy the store to try again. + ResetStore(); + } + return; + } + gcm_store_reset_ = false; + + device_checkin_info_.android_id = result->device_android_id; + device_checkin_info_.secret = result->device_security_token; + device_checkin_info_.last_checkin_accounts = result->last_checkin_accounts; + // A case where there were previously no accounts reported with checkin is + // considered to be the same as when the list of accounts is empty. It enables + // scheduling a periodic checkin for devices with no signed in users + // immediately after restart, while keeping |accounts_set == false| delays the + // checkin until the list of accounts is set explicitly. + if (result->last_checkin_accounts.size() == 0) + device_checkin_info_.accounts_set = true; + last_checkin_time_ = result->last_checkin_time; + gservices_settings_.UpdateFromLoadResult(*result); + + for (auto iter = result->registrations.begin(); + iter != result->registrations.end(); + ++iter) { + std::string registration_id; + scoped_refptr<RegistrationInfo> registration = + RegistrationInfo::BuildFromString(iter->first, iter->second, + ®istration_id); + // TODO(jianli): Add UMA to track the error case. + if (registration) + registrations_.emplace(std::move(registration), registration_id); + } + + for (auto iter = result->instance_id_data.begin(); + iter != result->instance_id_data.end(); + ++iter) { + std::string instance_id; + std::string extra_data; + if (DeserializeInstanceIDData(iter->second, &instance_id, &extra_data)) + instance_id_data_[iter->first] = std::make_pair(instance_id, extra_data); + } + + load_result_ = std::move(result); + state_ = LOADED; + + // Don't initiate the GCM connection when GCM is in delayed start mode and + // not any standalone app has registered GCM yet. + if (start_mode_ == DELAYED_START && !HasStandaloneRegisteredApp()) { + // If no standalone app is using GCM and the device ID is present, schedule + // to have the store wiped out. + if (device_checkin_info_.android_id) { + io_task_runner_->PostDelayedTask( + FROM_HERE, + base::BindOnce(&GCMClientImpl::DestroyStoreWhenNotNeeded, + destroying_gcm_store_ptr_factory_.GetWeakPtr()), + base::Milliseconds(kDestroyGCMStoreDelayMS)); + } + + return; + } + + StartGCM(); +} + +void GCMClientImpl::StartGCM() { + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + + // Taking over the value of account_mappings before passing the ownership of + // load result to InitializeMCSClient. + std::vector<AccountMapping> account_mappings; + account_mappings.swap(load_result_->account_mappings); + base::Time last_token_fetch_time = load_result_->last_token_fetch_time; + + InitializeMCSClient(); + + if (device_checkin_info_.IsValid()) { + SchedulePeriodicCheckin(); + OnReady(account_mappings, last_token_fetch_time); + return; + } + + state_ = INITIAL_DEVICE_CHECKIN; + device_checkin_info_.Reset(); + StartCheckin(); +} + +void GCMClientImpl::InitializeMCSClient() { + DCHECK(network_connection_tracker_); + std::vector<GURL> endpoints; + endpoints.push_back(gservices_settings_.GetMCSMainEndpoint()); + GURL fallback_endpoint = gservices_settings_.GetMCSFallbackEndpoint(); + if (fallback_endpoint.is_valid()) + endpoints.push_back(fallback_endpoint); + connection_factory_ = internals_builder_->BuildConnectionFactory( + endpoints, GetGCMBackoffPolicy(), get_socket_factory_callback_, + io_task_runner_, &recorder_, network_connection_tracker_); + connection_factory_->SetConnectionListener(this); + mcs_client_ = internals_builder_->BuildMCSClient( + chrome_build_info_.version, clock_, connection_factory_.get(), + gcm_store_.get(), io_task_runner_, &recorder_); + + mcs_client_->Initialize( + base::BindRepeating(&GCMClientImpl::OnMCSError, + weak_ptr_factory_.GetWeakPtr()), + base::BindRepeating(&GCMClientImpl::OnMessageReceivedFromMCS, + weak_ptr_factory_.GetWeakPtr()), + base::BindRepeating(&GCMClientImpl::OnMessageSentToMCS, + weak_ptr_factory_.GetWeakPtr()), + std::move(load_result_)); +} + +void GCMClientImpl::OnFirstTimeDeviceCheckinCompleted( + const CheckinInfo& checkin_info) { + DCHECK(!device_checkin_info_.IsValid()); + + device_checkin_info_.android_id = checkin_info.android_id; + device_checkin_info_.secret = checkin_info.secret; + // If accounts were not set by now, we can consider them set (to empty list) + // to make sure periodic checkins get scheduled after initial checkin. + device_checkin_info_.accounts_set = true; + gcm_store_->SetDeviceCredentials( + checkin_info.android_id, checkin_info.secret, + base::BindOnce(&GCMClientImpl::SetDeviceCredentialsCallback, + weak_ptr_factory_.GetWeakPtr())); + + OnReady(std::vector<AccountMapping>(), base::Time()); +} + +void GCMClientImpl::OnReady(const std::vector<AccountMapping>& account_mappings, + const base::Time& last_token_fetch_time) { + state_ = READY; + StartMCSLogin(); + + delegate_->OnGCMReady(account_mappings, last_token_fetch_time); +} + +void GCMClientImpl::StartMCSLogin() { + DCHECK_EQ(READY, state_); + DCHECK(device_checkin_info_.IsValid()); + mcs_client_->Login(device_checkin_info_.android_id, + device_checkin_info_.secret); +} + +void GCMClientImpl::DestroyStoreWhenNotNeeded() { + if (state_ != LOADED || start_mode_ != DELAYED_START) + return; + + gcm_store_->Destroy(base::BindOnce(&GCMClientImpl::DestroyStoreCallback, + weak_ptr_factory_.GetWeakPtr())); +} + +void GCMClientImpl::ResetStore() { + // If already being reset, don't do it again. We want to prevent from + // resetting and loading from the store again and again. + if (gcm_store_reset_) { + RecordResetStoreErrorToUMA(INFINITE_STORE_RESET); + state_ = UNINITIALIZED; + return; + } + gcm_store_reset_ = true; + + // Destroy the GCM store to start over. + gcm_store_->Destroy(base::BindOnce(&GCMClientImpl::ResetStoreCallback, + weak_ptr_factory_.GetWeakPtr())); +} + +void GCMClientImpl::SetAccountTokens( + const std::vector<AccountTokenInfo>& account_tokens) { + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + + device_checkin_info_.account_tokens.clear(); + for (auto iter = account_tokens.begin(); iter != account_tokens.end(); + ++iter) { + device_checkin_info_.account_tokens[iter->email] = iter->access_token; + } + + bool accounts_set_before = device_checkin_info_.accounts_set; + device_checkin_info_.accounts_set = true; + + DVLOG(1) << "Set account called with: " << account_tokens.size() + << " accounts."; + + if (state_ != READY && state_ != INITIAL_DEVICE_CHECKIN) + return; + + bool account_removed = false; + for (auto iter = device_checkin_info_.last_checkin_accounts.begin(); + iter != device_checkin_info_.last_checkin_accounts.end(); ++iter) { + if (device_checkin_info_.account_tokens.find(*iter) == + device_checkin_info_.account_tokens.end()) { + account_removed = true; + } + } + + // Checkin will be forced when any of the accounts was removed during the + // current Chrome session or if there has been an account removed between the + // restarts of Chrome. If there is a checkin in progress, it will be canceled. + // We only force checkin when user signs out. When there is a new account + // signed in, the periodic checkin will take care of adding the association in + // reasonable time. + if (account_removed) { + DVLOG(1) << "Detected that account has been removed. Forcing checkin."; + checkin_request_.reset(); + StartCheckin(); + } else if (!accounts_set_before) { + SchedulePeriodicCheckin(); + DVLOG(1) << "Accounts set for the first time. Scheduled periodic checkin."; + } +} + +void GCMClientImpl::UpdateAccountMapping( + const AccountMapping& account_mapping) { + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + gcm_store_->AddAccountMapping( + account_mapping, base::BindOnce(&GCMClientImpl::DefaultStoreCallback, + weak_ptr_factory_.GetWeakPtr())); +} + +void GCMClientImpl::RemoveAccountMapping(const CoreAccountId& account_id) { + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + gcm_store_->RemoveAccountMapping( + account_id, base::BindOnce(&GCMClientImpl::DefaultStoreCallback, + weak_ptr_factory_.GetWeakPtr())); +} + +void GCMClientImpl::SetLastTokenFetchTime(const base::Time& time) { + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + gcm_store_->SetLastTokenFetchTime( + time, + base::BindOnce(&GCMClientImpl::IgnoreWriteResultCallback, + weak_ptr_factory_.GetWeakPtr(), + /*operation_suffix_for_uma=*/"SetLastTokenFetchTime")); +} + +void GCMClientImpl::UpdateHeartbeatTimer( + std::unique_ptr<base::RetainingOneShotTimer> timer) { + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + DCHECK(mcs_client_); + mcs_client_->UpdateHeartbeatTimer(std::move(timer)); +} + +void GCMClientImpl::AddInstanceIDData(const std::string& app_id, + const std::string& instance_id, + const std::string& extra_data) { + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + instance_id_data_[app_id] = std::make_pair(instance_id, extra_data); + // TODO(crbug/1028761): If this call fails, we likely leak a registration + // (the one stored in instance_id_data_ would be used for a registration but + // not persisted). + gcm_store_->AddInstanceIDData( + app_id, SerializeInstanceIDData(instance_id, extra_data), + base::BindOnce(&GCMClientImpl::IgnoreWriteResultCallback, + weak_ptr_factory_.GetWeakPtr(), + /*operation_suffix_for_uma=*/"AddInstanceIDData")); +} + +void GCMClientImpl::RemoveInstanceIDData(const std::string& app_id) { + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + instance_id_data_.erase(app_id); + gcm_store_->RemoveInstanceIDData( + app_id, + base::BindOnce(&GCMClientImpl::IgnoreWriteResultCallback, + weak_ptr_factory_.GetWeakPtr(), + /*operation_suffix_for_uma=*/"RemoveInstanceIDData")); +} + +void GCMClientImpl::GetInstanceIDData(const std::string& app_id, + std::string* instance_id, + std::string* extra_data) { + DCHECK(instance_id && extra_data); + + auto iter = instance_id_data_.find(app_id); + if (iter == instance_id_data_.end()) + return; + *instance_id = iter->second.first; + *extra_data = iter->second.second; +} + +void GCMClientImpl::AddHeartbeatInterval(const std::string& scope, + int interval_ms) { + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + DCHECK(mcs_client_); + mcs_client_->AddHeartbeatInterval(scope, interval_ms); +} + +void GCMClientImpl::RemoveHeartbeatInterval(const std::string& scope) { + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + DCHECK(mcs_client_); + mcs_client_->RemoveHeartbeatInterval(scope); +} + +void GCMClientImpl::StartCheckin() { + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + + // Make sure no checkin is in progress. + if (checkin_request_) + return; + + checkin_proto::ChromeBuildProto chrome_build_proto; + ToCheckinProtoVersion(chrome_build_info_, &chrome_build_proto); + CheckinRequest::RequestInfo request_info(device_checkin_info_.android_id, + device_checkin_info_.secret, + device_checkin_info_.account_tokens, + gservices_settings_.digest(), + chrome_build_proto); + checkin_request_ = std::make_unique<CheckinRequest>( + gservices_settings_.GetCheckinURL(), request_info, GetGCMBackoffPolicy(), + base::BindOnce(&GCMClientImpl::OnCheckinCompleted, + weak_ptr_factory_.GetWeakPtr()), + url_loader_factory_, io_task_runner_, &recorder_); + // Taking a snapshot of the accounts count here, as there might be an asynch + // update of the account tokens while checkin is in progress. + device_checkin_info_.SnapshotCheckinAccounts(); + checkin_request_->Start(); +} + +void GCMClientImpl::OnCheckinCompleted( + net::HttpStatusCode response_code, + const checkin_proto::AndroidCheckinResponse& checkin_response) { + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + + checkin_request_.reset(); + + if (response_code == net::HTTP_UNAUTHORIZED || + response_code == net::HTTP_BAD_REQUEST) { + LOG(ERROR) << "Checkin rejected. Resetting GCM Store."; + ResetStore(); + return; + } + + DCHECK(checkin_response.has_android_id()); + DCHECK(checkin_response.has_security_token()); + CheckinInfo checkin_info; + checkin_info.android_id = checkin_response.android_id(); + checkin_info.secret = checkin_response.security_token(); + + if (state_ == INITIAL_DEVICE_CHECKIN) { + OnFirstTimeDeviceCheckinCompleted(checkin_info); + } else { + // checkin_info is not expected to change after a periodic checkin as it + // would invalidate the registration IDs. + DCHECK_EQ(READY, state_); + DCHECK_EQ(device_checkin_info_.android_id, checkin_info.android_id); + DCHECK_EQ(device_checkin_info_.secret, checkin_info.secret); + } + + if (device_checkin_info_.IsValid()) { + // First update G-services settings, as something might have changed. + if (gservices_settings_.UpdateFromCheckinResponse(checkin_response)) { + gcm_store_->SetGServicesSettings( + gservices_settings_.settings_map(), gservices_settings_.digest(), + base::BindOnce(&GCMClientImpl::SetGServicesSettingsCallback, + weak_ptr_factory_.GetWeakPtr())); + } + + last_checkin_time_ = clock_->Now(); + gcm_store_->SetLastCheckinInfo( + last_checkin_time_, device_checkin_info_.last_checkin_accounts, + base::BindOnce(&GCMClientImpl::SetLastCheckinInfoCallback, + weak_ptr_factory_.GetWeakPtr())); + SchedulePeriodicCheckin(); + } +} + +void GCMClientImpl::SetGServicesSettingsCallback(bool success) { + DCHECK(success); +} + +void GCMClientImpl::SchedulePeriodicCheckin() { + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + + // Make sure no checkin is in progress. + if (checkin_request_.get() || !device_checkin_info_.accounts_set) + return; + + // There should be only one periodic checkin pending at a time. Removing + // pending periodic checkin to schedule a new one. + periodic_checkin_ptr_factory_.InvalidateWeakPtrs(); + + base::TimeDelta time_to_next_checkin = GetTimeToNextCheckin(); + if (time_to_next_checkin.is_negative()) + time_to_next_checkin = base::TimeDelta(); + + io_task_runner_->PostDelayedTask( + FROM_HERE, + base::BindOnce(&GCMClientImpl::StartCheckin, + periodic_checkin_ptr_factory_.GetWeakPtr()), + time_to_next_checkin); +} + +base::TimeDelta GCMClientImpl::GetTimeToNextCheckin() const { + return last_checkin_time_ + gservices_settings_.GetCheckinInterval() - + clock_->Now(); +} + +void GCMClientImpl::SetLastCheckinInfoCallback(bool success) { + // TODO(fgorski): This is one of the signals that store needs a rebuild. + DCHECK(success); +} + +void GCMClientImpl::SetDeviceCredentialsCallback(bool success) { + // TODO(fgorski): This is one of the signals that store needs a rebuild. + DCHECK(success); +} + +void GCMClientImpl::UpdateRegistrationCallback(bool success) { + // TODO(fgorski): This is one of the signals that store needs a rebuild. + DCHECK(success); +} + +void GCMClientImpl::DefaultStoreCallback(bool success) { + DCHECK(success); +} + +void GCMClientImpl::IgnoreWriteResultCallback( + const std::string& operation_suffix_for_uma, + bool success) { + // TODO(crbug.com/1081149): Implement proper error handling. + // TODO(fgorski): Ignoring the write result for now to make sure + // sync_intergration_tests are not broken. +} + +void GCMClientImpl::DestroyStoreCallback(bool success) { + ResetCache(); + + if (!success) { + LOG(ERROR) << "Failed to destroy GCM store"; + RecordResetStoreErrorToUMA(DESTROYING_STORE_FAILED); + state_ = UNINITIALIZED; + return; + } + + state_ = INITIALIZED; +} + +void GCMClientImpl::ResetStoreCallback(bool success) { + // Even an incomplete reset may invalidate registrations, and this might be + // the only opportunity to notify the delegate. For example a partial reset + // that deletes the "CURRENT" file will cause GCMStoreImpl to consider the DB + // to no longer exist, in which case the next load will simply create a new + // store rather than resetting it. + delegate_->OnStoreReset(); + + if (!success) { + LOG(ERROR) << "Failed to reset GCM store"; + RecordResetStoreErrorToUMA(DESTROYING_STORE_FAILED); + state_ = UNINITIALIZED; + return; + } + + state_ = INITIALIZED; + Start(start_mode_); +} + +void GCMClientImpl::Stop() { + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + // TODO(fgorski): Perhaps we should make a distinction between a Stop and a + // Shutdown. + DVLOG(1) << "Stopping the GCM Client"; + ResetCache(); + state_ = INITIALIZED; + gcm_store_->Close(); +} + +void GCMClientImpl::ResetCache() { + weak_ptr_factory_.InvalidateWeakPtrs(); + periodic_checkin_ptr_factory_.InvalidateWeakPtrs(); + device_checkin_info_.Reset(); + connection_factory_.reset(); + delegate_->OnDisconnected(); + mcs_client_.reset(); + checkin_request_.reset(); + // Delete all of the pending registration and unregistration requests. + pending_registration_requests_.clear(); + pending_unregistration_requests_.clear(); +} + +void GCMClientImpl::Register( + scoped_refptr<RegistrationInfo> registration_info) { + DCHECK_EQ(state_, READY); + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + + // Registrations should never pass as an app_id the special category used + // internally when registering with a subtype. See security note in + // GCMClientImpl::HandleIncomingMessage. + CHECK_NE(registration_info->app_id, + chrome_build_info_.product_category_for_subtypes); + + // Find and use the cached registration ID. + RegistrationInfoMap::const_iterator registrations_iter = + registrations_.find(registration_info); + if (registrations_iter != registrations_.end()) { + bool matched = true; + + // For GCM registration, we also match the sender IDs since multiple + // registrations are not supported. + const GCMRegistrationInfo* gcm_registration_info = + GCMRegistrationInfo::FromRegistrationInfo(registration_info.get()); + if (gcm_registration_info) { + const GCMRegistrationInfo* cached_gcm_registration_info = + GCMRegistrationInfo::FromRegistrationInfo( + registrations_iter->first.get()); + DCHECK(cached_gcm_registration_info); + if (gcm_registration_info->sender_ids != + cached_gcm_registration_info->sender_ids) { + matched = false; + // Senders IDs don't match existing registration. + RecordRegistrationRequestToUMA( + RegistrationCacheStatus::REGISTRATION_FOUND_BUT_SENDERS_DONT_MATCH); + } + } + + if (matched) { + // Skip registration if token is fresh. + base::TimeDelta token_invalidation_period = + features::GetTokenInvalidationInterval(); + base::TimeDelta time_since_last_validation = + clock_->Now() - registrations_iter->first->last_validated; + if (token_invalidation_period.is_zero() || + time_since_last_validation < token_invalidation_period) { + // Token is fresh, or token invalidation is disabled. + // Use cached registration. + delegate_->OnRegisterFinished(registration_info, + registrations_iter->second, SUCCESS); + RecordRegistrationRequestToUMA( + RegistrationCacheStatus::REGISTRATION_FOUND_AND_FRESH); + return; + } else { + // Token is stale. + RecordRegistrationRequestToUMA( + RegistrationCacheStatus::REGISTRATION_FOUND_BUT_STALE); + } + } + } else { + // New Registration request (no existing registration) + RecordRegistrationRequestToUMA( + RegistrationCacheStatus::REGISTRATION_NOT_FOUND); + } + + std::unique_ptr<RegistrationRequest::CustomRequestHandler> request_handler; + std::string source_to_record; + + const GCMRegistrationInfo* gcm_registration_info = + GCMRegistrationInfo::FromRegistrationInfo(registration_info.get()); + if (gcm_registration_info) { + std::string senders; + for (auto iter = gcm_registration_info->sender_ids.begin(); + iter != gcm_registration_info->sender_ids.end(); + ++iter) { + if (!senders.empty()) + senders.append(","); + senders.append(*iter); + } + request_handler = std::make_unique<GCMRegistrationRequestHandler>(senders); + source_to_record = senders; + } + + const InstanceIDTokenInfo* instance_id_token_info = + InstanceIDTokenInfo::FromRegistrationInfo(registration_info.get()); + if (instance_id_token_info) { + auto instance_id_iter = instance_id_data_.find(registration_info->app_id); + DCHECK(instance_id_iter != instance_id_data_.end()); + + request_handler = std::make_unique<InstanceIDGetTokenRequestHandler>( + instance_id_iter->second.first, + instance_id_token_info->authorized_entity, + instance_id_token_info->scope, + ConstructGCMVersion(chrome_build_info_.version), + instance_id_token_info->time_to_live); + source_to_record = instance_id_token_info->authorized_entity + "/" + + instance_id_token_info->scope; + } + + bool use_subtype = instance_id_token_info && + InstanceIDUsesSubtypeForAppId(registration_info->app_id); + std::string category = use_subtype + ? chrome_build_info_.product_category_for_subtypes + : registration_info->app_id; + std::string subtype = use_subtype ? registration_info->app_id : std::string(); + RegistrationRequest::RequestInfo request_info(device_checkin_info_.android_id, + device_checkin_info_.secret, + category, subtype); + + std::unique_ptr<RegistrationRequest> registration_request( + new RegistrationRequest( + gservices_settings_.GetRegistrationURL(), request_info, + std::move(request_handler), GetGCMBackoffPolicy(), + base::BindOnce(&GCMClientImpl::OnRegisterCompleted, + weak_ptr_factory_.GetWeakPtr(), registration_info), + kMaxRegistrationRetries, url_loader_factory_, io_task_runner_, + &recorder_, source_to_record)); + registration_request->Start(); + pending_registration_requests_.insert( + std::make_pair(registration_info, std::move(registration_request))); +} + +void GCMClientImpl::OnRegisterCompleted( + scoped_refptr<RegistrationInfo> registration_info, + RegistrationRequest::Status status, + const std::string& registration_id) { + DCHECK(delegate_); + + Result result; + PendingRegistrationRequests::const_iterator iter = + pending_registration_requests_.find(registration_info); + if (iter == pending_registration_requests_.end()) { + result = UNKNOWN_ERROR; + } else if (status == RegistrationRequest::INVALID_SENDER) { + result = INVALID_PARAMETER; + } else if (registration_id.empty()) { + // All other errors are currently treated as SERVER_ERROR (including + // REACHED_MAX_RETRIES due to the device being offline!). + result = SERVER_ERROR; + } else { + result = SUCCESS; + } + + if (result == SUCCESS) { + // Cache it. + // Note that the existing cached record has to be removed first because + // otherwise the key value in registrations_ will not be updated. For GCM + // registrations, the key consists of pair of app_id and sender_ids though + // only app_id is used in the key comparison. + registrations_.erase(registration_info); + registration_info->last_validated = clock_->Now(); + registrations_[registration_info] = registration_id; + + // Save it in the persistent store. + gcm_store_->AddRegistration( + registration_info->GetSerializedKey(), + registration_info->GetSerializedValue(registration_id), + base::BindOnce(&GCMClientImpl::UpdateRegistrationCallback, + weak_ptr_factory_.GetWeakPtr())); + } + + delegate_->OnRegisterFinished( + registration_info, + result == SUCCESS ? registration_id : std::string(), + result); + + if (iter != pending_registration_requests_.end()) + pending_registration_requests_.erase(iter); +} + +bool GCMClientImpl::ValidateRegistration( + scoped_refptr<RegistrationInfo> registration_info, + const std::string& registration_id) { + DCHECK_EQ(state_, READY); + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + + // Must have a cached registration. + RegistrationInfoMap::const_iterator registrations_iter = + registrations_.find(registration_info); + if (registrations_iter == registrations_.end()) + return false; + + // Cached registration ID must match. + const std::string& cached_registration_id = registrations_iter->second; + if (registration_id != cached_registration_id) + return false; + + // For GCM registration, we also match the sender IDs since multiple + // registrations are not supported. + const GCMRegistrationInfo* gcm_registration_info = + GCMRegistrationInfo::FromRegistrationInfo(registration_info.get()); + if (gcm_registration_info) { + const GCMRegistrationInfo* cached_gcm_registration_info = + GCMRegistrationInfo::FromRegistrationInfo( + registrations_iter->first.get()); + DCHECK(cached_gcm_registration_info); + if (gcm_registration_info->sender_ids != + cached_gcm_registration_info->sender_ids) { + return false; + } + } + + return true; +} + +void GCMClientImpl::Unregister( + scoped_refptr<RegistrationInfo> registration_info) { + DCHECK_EQ(state_, READY); + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + + std::unique_ptr<UnregistrationRequest::CustomRequestHandler> request_handler; + std::string source_to_record; + + const GCMRegistrationInfo* gcm_registration_info = + GCMRegistrationInfo::FromRegistrationInfo(registration_info.get()); + if (gcm_registration_info) { + request_handler = std::make_unique<GCMUnregistrationRequestHandler>( + registration_info->app_id); + } + + const InstanceIDTokenInfo* instance_id_token_info = + InstanceIDTokenInfo::FromRegistrationInfo(registration_info.get()); + if (instance_id_token_info) { + auto instance_id_iter = instance_id_data_.find(registration_info->app_id); + if (instance_id_iter == instance_id_data_.end()) { + // This should not be reached since we should not delete tokens when + // an InstanceID has not been created yet. + NOTREACHED(); + return; + } + + request_handler = std::make_unique<InstanceIDDeleteTokenRequestHandler>( + instance_id_iter->second.first, + instance_id_token_info->authorized_entity, + instance_id_token_info->scope, + ConstructGCMVersion(chrome_build_info_.version)); + source_to_record = instance_id_token_info->authorized_entity + "/" + + instance_id_token_info->scope; + } + + // Remove the registration/token(s) from the cache and the store. + // TODO(jianli): Remove it only when the request is successful. + if (instance_id_token_info && + instance_id_token_info->authorized_entity == "*" && + instance_id_token_info->scope == "*") { + // If authorized_entity and scope are '*', find and remove all associated + // tokens. + bool token_found = false; + for (auto iter = registrations_.begin(); + iter != registrations_.end();) { + InstanceIDTokenInfo* cached_instance_id_token_info = + InstanceIDTokenInfo::FromRegistrationInfo(iter->first.get()); + if (cached_instance_id_token_info && + cached_instance_id_token_info->app_id == registration_info->app_id) { + token_found = true; + gcm_store_->RemoveRegistration( + cached_instance_id_token_info->GetSerializedKey(), + base::BindOnce(&GCMClientImpl::UpdateRegistrationCallback, + weak_ptr_factory_.GetWeakPtr())); + registrations_.erase(iter++); + } else { + ++iter; + } + } + + // If no token is found for the Instance ID, don't need to unregister + // since the Instance ID is not sent to the server yet. + if (!token_found) { + OnUnregisterCompleted(registration_info, + UnregistrationRequest::SUCCESS); + return; + } + } else { + auto iter = registrations_.find(registration_info); + if (iter == registrations_.end()) { + delegate_->OnUnregisterFinished(registration_info, INVALID_PARAMETER); + return; + } + registrations_.erase(iter); + + gcm_store_->RemoveRegistration( + registration_info->GetSerializedKey(), + base::BindOnce(&GCMClientImpl::UpdateRegistrationCallback, + weak_ptr_factory_.GetWeakPtr())); + } + + bool use_subtype = instance_id_token_info && + InstanceIDUsesSubtypeForAppId(registration_info->app_id); + std::string category = use_subtype + ? chrome_build_info_.product_category_for_subtypes + : registration_info->app_id; + std::string subtype = use_subtype ? registration_info->app_id : std::string(); + UnregistrationRequest::RequestInfo request_info( + device_checkin_info_.android_id, device_checkin_info_.secret, category, + subtype); + + std::unique_ptr<UnregistrationRequest> unregistration_request( + new UnregistrationRequest( + gservices_settings_.GetRegistrationURL(), request_info, + std::move(request_handler), GetGCMBackoffPolicy(), + base::BindOnce(&GCMClientImpl::OnUnregisterCompleted, + weak_ptr_factory_.GetWeakPtr(), registration_info), + kMaxUnregistrationRetries, url_loader_factory_, io_task_runner_, + &recorder_, source_to_record)); + unregistration_request->Start(); + pending_unregistration_requests_.insert( + std::make_pair(registration_info, std::move(unregistration_request))); +} + +void GCMClientImpl::OnUnregisterCompleted( + scoped_refptr<RegistrationInfo> registration_info, + UnregistrationRequest::Status status) { + DVLOG(1) << "Unregister completed for app: " << registration_info->app_id + << " with " << (status ? "success." : "failure."); + + Result result; + switch (status) { + case UnregistrationRequest::SUCCESS: + result = SUCCESS; + break; + case UnregistrationRequest::INVALID_PARAMETERS: + result = INVALID_PARAMETER; + break; + default: + // All other errors are currently treated as SERVER_ERROR (including + // REACHED_MAX_RETRIES due to the device being offline!). + result = SERVER_ERROR; + break; + } + delegate_->OnUnregisterFinished(registration_info, result); + + pending_unregistration_requests_.erase(registration_info); +} + +void GCMClientImpl::OnGCMStoreDestroyed(bool success) { + DLOG_IF(ERROR, !success) << "GCM store failed to be destroyed!"; + UMA_HISTOGRAM_BOOLEAN("GCM.StoreDestroySucceeded", success); +} + +void GCMClientImpl::Send(const std::string& app_id, + const std::string& receiver_id, + const OutgoingMessage& message) { + DCHECK_EQ(state_, READY); + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + + mcs_proto::DataMessageStanza stanza; + stanza.set_ttl(message.time_to_live); + stanza.set_sent(clock_->Now().ToInternalValue() / + base::Time::kMicrosecondsPerSecond); + stanza.set_id(message.id); + stanza.set_from(kSendMessageFromValue); + stanza.set_to(receiver_id); + stanza.set_category(app_id); + + for (auto iter = message.data.begin(); iter != message.data.end(); ++iter) { + mcs_proto::AppData* app_data = stanza.add_app_data(); + app_data->set_key(iter->first); + app_data->set_value(iter->second); + } + + MCSMessage mcs_message(stanza); + DVLOG(1) << "MCS message size: " << mcs_message.size(); + mcs_client_->SendMessage(mcs_message); +} + +std::string GCMClientImpl::GetStateString() const { + switch(state_) { + case GCMClientImpl::UNINITIALIZED: + return "UNINITIALIZED"; + case GCMClientImpl::INITIALIZED: + return "INITIALIZED"; + case GCMClientImpl::LOADING: + return "LOADING"; + case GCMClientImpl::LOADED: + return "LOADED"; + case GCMClientImpl::INITIAL_DEVICE_CHECKIN: + return "INITIAL_DEVICE_CHECKIN"; + case GCMClientImpl::READY: + return "READY"; + } + NOTREACHED(); + return std::string(); +} + +void GCMClientImpl::RecordDecryptionFailure(const std::string& app_id, + GCMDecryptionResult result) { + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + recorder_.RecordDecryptionFailure(app_id, result); +} + +void GCMClientImpl::SetRecording(bool recording) { + recorder_.set_is_recording(recording); +} + +void GCMClientImpl::ClearActivityLogs() { + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + recorder_.Clear(); +} + +GCMClient::GCMStatistics GCMClientImpl::GetStatistics() const { + DCHECK(io_task_runner_->RunsTasksInCurrentSequence()); + GCMClient::GCMStatistics stats; + stats.gcm_client_created = true; + stats.is_recording = recorder_.is_recording(); + stats.gcm_client_state = GetStateString(); + stats.connection_client_created = mcs_client_ != nullptr; + stats.last_checkin = last_checkin_time_; + stats.next_checkin = + last_checkin_time_ + gservices_settings_.GetCheckinInterval(); + if (connection_factory_) + stats.connection_state = connection_factory_->GetConnectionStateString(); + if (mcs_client_) { + stats.send_queue_size = mcs_client_->GetSendQueueSize(); + stats.resend_queue_size = mcs_client_->GetResendQueueSize(); + } + if (device_checkin_info_.android_id > 0) + stats.android_id = device_checkin_info_.android_id; + if (device_checkin_info_.secret > 0) + stats.android_secret = device_checkin_info_.secret; + + recorder_.CollectActivities(&stats.recorded_activities); + + for (auto it = registrations_.begin(); it != registrations_.end(); ++it) { + stats.registered_app_ids.push_back(it->first->app_id); + } + return stats; +} + +void GCMClientImpl::OnActivityRecorded() { + delegate_->OnActivityRecorded(); +} + +void GCMClientImpl::OnConnected(const GURL& current_server, + const net::IPEndPoint& ip_endpoint) { + // TODO(gcm): expose current server in debug page. + delegate_->OnActivityRecorded(); + delegate_->OnConnected(ip_endpoint); +} + +void GCMClientImpl::OnDisconnected() { + delegate_->OnActivityRecorded(); + delegate_->OnDisconnected(); +} + +void GCMClientImpl::OnMessageReceivedFromMCS(const gcm::MCSMessage& message) { + switch (message.tag()) { + case kLoginResponseTag: + DVLOG(1) << "Login response received by GCM Client. Ignoring."; + return; + case kDataMessageStanzaTag: + DVLOG(1) << "A downstream message received. Processing..."; + HandleIncomingMessage(message); + return; + default: + NOTREACHED() << "Message with unexpected tag received by GCMClient"; + return; + } +} + +void GCMClientImpl::OnMessageSentToMCS(int64_t user_serial_number, + const std::string& app_id, + const std::string& message_id, + MCSClient::MessageSendStatus status) { + DCHECK_EQ(user_serial_number, kDefaultUserSerialNumber); + DCHECK(delegate_); + + // TTL_EXCEEDED is singled out here, because it can happen long time after the + // message was sent. That is why it comes as |OnMessageSendError| event rather + // than |OnSendFinished|. SendErrorDetails.additional_data is left empty. + // All other errors will be raised immediately, through asynchronous callback. + // It is expected that TTL_EXCEEDED will be issued for a message that was + // previously issued |OnSendFinished| with status SUCCESS. + // TODO(jianli): Consider adding UMA for this status. + if (status == MCSClient::TTL_EXCEEDED) { + SendErrorDetails send_error_details; + send_error_details.message_id = message_id; + send_error_details.result = GCMClient::TTL_EXCEEDED; + delegate_->OnMessageSendError(app_id, send_error_details); + } else if (status == MCSClient::SENT) { + delegate_->OnSendAcknowledged(app_id, message_id); + } else { + delegate_->OnSendFinished(app_id, message_id, ToGCMClientResult(status)); + } +} + +void GCMClientImpl::OnMCSError() { + // TODO(fgorski): For now it replaces the initialization method. Long term it + // should have an error or status passed in. +} + +void GCMClientImpl::HandleIncomingMessage(const gcm::MCSMessage& message) { + DCHECK(delegate_); + + const mcs_proto::DataMessageStanza& data_message_stanza = + reinterpret_cast<const mcs_proto::DataMessageStanza&>( + message.GetProtobuf()); + DCHECK_EQ(data_message_stanza.device_user_id(), kDefaultUserSerialNumber); + + // Copy all the data from the stanza to a MessageData object. When present, + // keys like kSubtypeKey or kMessageTypeKey will be filtered out later. + MessageData message_data; + for (int i = 0; i < data_message_stanza.app_data_size(); ++i) { + std::string key = data_message_stanza.app_data(i).key(); + message_data[key] = data_message_stanza.app_data(i).value(); + } + + std::string subtype; + auto subtype_iter = message_data.find(kSubtypeKey); + if (subtype_iter != message_data.end()) { + subtype = subtype_iter->second; + message_data.erase(subtype_iter); + } + + // SECURITY NOTE: Subtypes received from GCM *cannot* be trusted for + // registrations without a subtype (as the sender can pass any subtype they + // want). They can however be trusted for registrations that are known to have + // a subtype (as GCM overwrites anything passed by the sender). + // + // So a given Chrome profile always passes a fixed string called + // |product_category_for_subtypes| (of the form "com.chrome.macosx") as the + // category when registering with a subtype, and incoming subtypes are only + // trusted for that category. + // + // TODO(johnme): Remove this check if GCM starts sending the subtype in a + // field that's guaranteed to be trusted (b/18198485). + // + // (On Android, all registrations made by Chrome on behalf of third-party + // apps/extensions/websites have always had a subtype, so such a check is not + // necessary - or possible, since category is fixed to the true package name). + bool subtype_is_trusted = data_message_stanza.category() == + chrome_build_info_.product_category_for_subtypes; + bool use_subtype = subtype_is_trusted && !subtype.empty(); + std::string app_id = use_subtype ? subtype : data_message_stanza.category(); + + MessageType message_type = DATA_MESSAGE; + auto type_iter = message_data.find(kMessageTypeKey); + if (type_iter != message_data.end()) { + message_type = DecodeMessageType(type_iter->second); + message_data.erase(type_iter); + } + + switch (message_type) { + case DATA_MESSAGE: + HandleIncomingDataMessage(app_id, use_subtype, data_message_stanza, + message_data); + break; + case DELETED_MESSAGES: + HandleIncomingDeletedMessages(app_id, data_message_stanza, message_data); + break; + case SEND_ERROR: + HandleIncomingSendError(app_id, data_message_stanza, message_data); + break; + case UNKNOWN: + DVLOG(1) << "Unknown message_type received. Message ignored. " + << "App ID: " << app_id << "."; + break; + } +} + +void GCMClientImpl::HandleIncomingDataMessage( + const std::string& app_id, + bool was_subtype, + const mcs_proto::DataMessageStanza& data_message_stanza, + MessageData& message_data) { + UMA_HISTOGRAM_BOOLEAN("GCM.DataMessageReceived", true); + + bool has_collapse_key = + data_message_stanza.has_token() && !data_message_stanza.token().empty(); + UMA_HISTOGRAM_BOOLEAN("GCM.DataMessageReceivedHasCollapseKey", + has_collapse_key); + + recorder_.RecordDataMessageReceived(app_id, data_message_stanza.from(), + data_message_stanza.ByteSize(), + GCMStatsRecorder::DATA_MESSAGE); + + IncomingMessage incoming_message; + incoming_message.sender_id = data_message_stanza.from(); + incoming_message.message_id = data_message_stanza.persistent_id(); + if (data_message_stanza.has_token()) + incoming_message.collapse_key = data_message_stanza.token(); + incoming_message.data = message_data; + incoming_message.raw_data = data_message_stanza.raw_data(); + + delegate_->OnMessageReceived(app_id, incoming_message); +} + +void GCMClientImpl::HandleIncomingDeletedMessages( + const std::string& app_id, + const mcs_proto::DataMessageStanza& data_message_stanza, + MessageData& message_data) { + int deleted_count = 0; + auto count_iter = message_data.find(kDeletedCountKey); + if (count_iter != message_data.end()) { + if (!base::StringToInt(count_iter->second, &deleted_count)) + deleted_count = 0; + } + UMA_HISTOGRAM_COUNTS_1000("GCM.DeletedMessagesReceived", deleted_count); + + recorder_.RecordDataMessageReceived(app_id, data_message_stanza.from(), + data_message_stanza.ByteSize(), + GCMStatsRecorder::DELETED_MESSAGES); + delegate_->OnMessagesDeleted(app_id); +} + +void GCMClientImpl::HandleIncomingSendError( + const std::string& app_id, + const mcs_proto::DataMessageStanza& data_message_stanza, + MessageData& message_data) { + SendErrorDetails send_error_details; + send_error_details.additional_data = message_data; + send_error_details.result = SERVER_ERROR; + send_error_details.message_id = data_message_stanza.persistent_id(); + + recorder_.RecordIncomingSendError(app_id, data_message_stanza.to(), + data_message_stanza.id()); + delegate_->OnMessageSendError(app_id, send_error_details); +} + +bool GCMClientImpl::HasStandaloneRegisteredApp() const { + if (registrations_.empty()) + return false; + // Note that account mapper is not counted as a standalone app since it is + // automatically started when other app uses GCM. + return registrations_.size() > 1 || + !ExistsGCMRegistrationInMap(registrations_, kGCMAccountMapperAppId); +} + +} // namespace gcm |