| // Copyright 2014 The Chromium Authors |
| // 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 <string_view> |
| #include <utility> |
| |
| #include "base/files/file_path.h" |
| #include "base/functional/bind.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/not_fatal_until.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_split.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. |
| }; |
| |
| 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[] = "[email protected]"; |
| 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(); |
| } |
| 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. |
| auto parts = base::SplitStringOnce(chrome_version, '.'); |
| if (!parts) { |
| NOTREACHED(); |
| } |
| |
| int gcm_version = 0; |
| base::StringToInt(parts->first, &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); |
| auto parts = base::SplitStringOnce(serialized_data, ','); |
| if (!parts) { |
| return false; |
| } |
| *instance_id = parts->first; |
| *extra_data = parts->second; |
| 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); |
| } |
| |
| } // namespace |
| |
| void RecordRegistrationRequestToUMA(gcm::RegistrationCacheStatus status) { |
| UMA_HISTOGRAM_ENUMERATION( |
| "GCM.RegistrationCacheStatus", status, |
| RegistrationCacheStatus::REGISTRATION_CACHE_STATUS_COUNT); |
| } |
| GCMInternalsBuilder::GCMInternalsBuilder() = default; |
| GCMInternalsBuilder::~GCMInternalsBuilder() = default; |
| |
| 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() = default; |
| |
| void GCMClientImpl::CheckinInfo::Reset() { |
| android_id = 0; |
| secret = 0; |
| accounts_set = false; |
| } |
| |
| 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() = default; |
| |
| void GCMClientImpl::Initialize( |
| const ChromeBuildInfo& chrome_build_info, |
| const base::FilePath& path, |
| 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, 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_.accounts_set = true; |
| last_checkin_time_ = result->last_checkin_time; |
| gservices_settings_.UpdateFromLoadResult(*result); |
| |
| for (const auto& [key, value] : result->registrations) { |
| std::string registration_id; |
| scoped_refptr<RegistrationInfo> registration = |
| RegistrationInfo::BuildFromString(key, value, ®istration_id); |
| // TODO(jianli): Add UMA to track the error case. |
| if (registration) |
| registrations_.emplace(std::move(registration), registration_id); |
| } |
| |
| for (const auto& [key, value] : result->instance_id_data) { |
| std::string instance_id; |
| std::string extra_data; |
| if (DeserializeInstanceIDData(value, &instance_id, &extra_data)) |
| instance_id_data_[key] = 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) { |
| DVLOG(1) << "GCM is in delayed start mode and there is no standalone " |
| "app, posting task to wipe store in " |
| << base::Milliseconds(kDestroyGCMStoreDelayMS) |
| << " milliseconds."; |
| 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(); |
| |
| DVLOG(1) << "Starting initial GCM checkin."; |
| 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_) { |
| 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()); |
| |
| 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; |
| |
| 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.com/40109289): 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, |
| 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_); |
| 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_, |
| 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/40691191): 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"; |
| 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"; |
| 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() { |
| DVLOG(2) << "Resetting GCMClientImpl cache"; |
| |
| 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 (const auto& kv : gcm_registration_info->sender_ids) { |
| if (!senders.empty()) |
| senders.append(","); |
| senders.append(kv); |
| } |
| 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); |
| CHECK(instance_id_iter != instance_id_data_.end(), |
| base::NotFatalUntil::M130); |
| |
| 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(); |
| } |
| |
| 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!"; |
| } |
| |
| 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(); |
| } |
| |
| 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"; |
| } |
| } |
| |
| 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) { |
| recorder_.RecordDataMessageReceived(app_id, data_message_stanza.from(), |
| data_message_stanza.ByteSizeLong(), |
| 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; |
| } |
| |
| recorder_.RecordDataMessageReceived(app_id, data_message_stanza.from(), |
| data_message_stanza.ByteSizeLong(), |
| 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 |