diff options
Diffstat (limited to 'chromium/components/gcm_driver/gcm_account_mapper.cc')
-rw-r--r-- | chromium/components/gcm_driver/gcm_account_mapper.cc | 386 |
1 files changed, 386 insertions, 0 deletions
diff --git a/chromium/components/gcm_driver/gcm_account_mapper.cc b/chromium/components/gcm_driver/gcm_account_mapper.cc new file mode 100644 index 00000000000..5570f8520b3 --- /dev/null +++ b/chromium/components/gcm_driver/gcm_account_mapper.cc @@ -0,0 +1,386 @@ +// 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_account_mapper.h" + +#include <utility> + +#include "base/bind.h" +#include "base/guid.h" +#include "base/metrics/histogram_functions.h" +#include "base/time/clock.h" +#include "base/time/default_clock.h" +#include "components/gcm_driver/gcm_driver_desktop.h" +#include "google_apis/gcm/engine/gcm_store.h" + +namespace gcm { + +namespace { + +const char kGCMAccountMapperSenderId[] = "745476177629"; +const char kGCMAccountMapperSendTo[] = "google.com"; +const int kGCMAddMappingMessageTTL = 30 * 60; // 0.5 hours in seconds. +const int kGCMRemoveMappingMessageTTL = 24 * 60 * 60; // 1 day in seconds. +const int kGCMUpdateIntervalHours = 24; +// Because adding an account mapping dependents on a fresh OAuth2 token, we +// allow the update to happen earlier than update due time, if it is within +// the early start time to take advantage of that token. +const int kGCMUpdateEarlyStartHours = 6; +const char kRegistrationIdMessgaeKey[] = "id"; +const char kTokenMessageKey[] = "t"; +const char kAccountMessageKey[] = "a"; +const char kRemoveAccountKey[] = "r"; +const char kRemoveAccountValue[] = "1"; +// Use to handle send to Gaia ID scenario: +const char kGCMSendToGaiaIdAppIdKey[] = "gcmb"; + + +std::string GenerateMessageID() { + return base::GenerateGUID(); +} + +} // namespace + +const char kGCMAccountMapperAppId[] = "com.google.android.gms"; + +GCMAccountMapper::GCMAccountMapper(GCMDriver* gcm_driver) + : gcm_driver_(gcm_driver), + clock_(base::DefaultClock::GetInstance()), + initialized_(false) {} + +GCMAccountMapper::~GCMAccountMapper() = default; + +void GCMAccountMapper::Initialize(const AccountMappings& account_mappings, + const DispatchMessageCallback& callback) { + DCHECK(!initialized_); + initialized_ = true; + accounts_ = account_mappings; + dispatch_message_callback_ = callback; + GetRegistration(); +} + +void GCMAccountMapper::SetAccountTokens( + const std::vector<GCMClient::AccountTokenInfo>& account_tokens) { + DVLOG(1) << "GCMAccountMapper::SetAccountTokens called with " + << account_tokens.size() << " accounts."; + + // If account mapper is not ready to handle tasks yet, save the latest + // account tokens and return. + if (!IsReady()) { + pending_account_tokens_ = account_tokens; + // If mapper is initialized, but still does not have registration ID, + // maybe the registration gave up. Retrying in case. + if (initialized_ && gcm_driver_->IsStarted()) + GetRegistration(); + return; + } + + // Start from removing the old tokens, from all of the known accounts. + for (auto iter = accounts_.begin(); iter != accounts_.end(); ++iter) { + iter->access_token.clear(); + } + + // Update the internal collection of mappings with the new tokens. + for (auto token_iter = account_tokens.begin(); + token_iter != account_tokens.end(); ++token_iter) { + AccountMapping* account_mapping = + FindMappingByAccountId(token_iter->account_id); + if (!account_mapping) { + AccountMapping new_mapping; + new_mapping.status = AccountMapping::NEW; + new_mapping.account_id = token_iter->account_id; + new_mapping.access_token = token_iter->access_token; + new_mapping.email = token_iter->email; + accounts_.push_back(new_mapping); + } else { + // Since we got a token for an account, drop the remove message and treat + // it as mapped. + if (account_mapping->status == AccountMapping::REMOVING) { + account_mapping->status = AccountMapping::MAPPED; + account_mapping->status_change_timestamp = base::Time(); + account_mapping->last_message_id.clear(); + } + + account_mapping->email = token_iter->email; + account_mapping->access_token = token_iter->access_token; + } + } + + // Decide what to do with each account (either start mapping, or start + // removing). + for (auto mappings_iter = accounts_.begin(); mappings_iter != accounts_.end(); + ++mappings_iter) { + if (mappings_iter->access_token.empty()) { + // Send a remove message if the account was not previously being removed, + // or it doesn't have a pending message, or the pending message is + // already expired, but OnSendError event was lost. + if (mappings_iter->status != AccountMapping::REMOVING || + mappings_iter->last_message_id.empty() || + IsLastStatusChangeOlderThanTTL(*mappings_iter)) { + SendRemoveMappingMessage(*mappings_iter); + } + } else { + // A message is sent for all of the mappings considered NEW, or mappings + // that are ADDING, but have expired message (OnSendError event lost), or + // for those mapped accounts that can be refreshed. + if (mappings_iter->status == AccountMapping::NEW || + (mappings_iter->status == AccountMapping::ADDING && + IsLastStatusChangeOlderThanTTL(*mappings_iter)) || + (mappings_iter->status == AccountMapping::MAPPED && + CanTriggerUpdate(mappings_iter->status_change_timestamp))) { + mappings_iter->last_message_id.clear(); + SendAddMappingMessage(*mappings_iter); + } + } + } +} + +void GCMAccountMapper::ShutdownHandler() { + initialized_ = false; + accounts_.clear(); + registration_id_.clear(); + dispatch_message_callback_.Reset(); +} + +void GCMAccountMapper::OnStoreReset() { + // TODO(crbug.com/661660): Tell server to remove the mapping. But can't use + // upstream GCM send for that since the store got reset. + ShutdownHandler(); +} + +void GCMAccountMapper::OnMessage(const std::string& app_id, + const IncomingMessage& message) { + DCHECK_EQ(app_id, kGCMAccountMapperAppId); + // TODO(fgorski): Report Send to Gaia ID failures using UMA. + + base::UmaHistogramBoolean("GCM.AccountMappingMessageReceived", true); + + if (dispatch_message_callback_.is_null()) { + DVLOG(1) << "dispatch_message_callback_ missing in GCMAccountMapper"; + return; + } + + auto it = message.data.find(kGCMSendToGaiaIdAppIdKey); + if (it == message.data.end()) { + DVLOG(1) << "Send to Gaia ID failure: Embedded app ID missing."; + return; + } + + std::string embedded_app_id = it->second; + if (embedded_app_id.empty()) { + DVLOG(1) << "Send to Gaia ID failure: Embedded app ID is empty."; + return; + } + + // Ensuring the message does not carry the embedded app ID. + IncomingMessage new_message = message; + new_message.data.erase(new_message.data.find(kGCMSendToGaiaIdAppIdKey)); + dispatch_message_callback_.Run(embedded_app_id, new_message); +} + +void GCMAccountMapper::OnMessagesDeleted(const std::string& app_id) { + // Account message does not expect messages right now. +} + +void GCMAccountMapper::OnSendError( + const std::string& app_id, + const GCMClient::SendErrorDetails& send_error_details) { + DCHECK_EQ(app_id, kGCMAccountMapperAppId); + + auto account_mapping_it = + FindMappingByMessageId(send_error_details.message_id); + + if (account_mapping_it == accounts_.end()) + return; + + if (send_error_details.result != GCMClient::TTL_EXCEEDED) { + DVLOG(1) << "Send error result different than TTL EXCEEDED: " + << send_error_details.result << ". " + << "Postponing the retry until a new batch of tokens arrives."; + return; + } + + if (account_mapping_it->status == AccountMapping::REMOVING) { + // Another message to remove mapping can be sent immediately, because TTL + // for those is one day. No need to back off. + SendRemoveMappingMessage(*account_mapping_it); + } else { + if (account_mapping_it->status == AccountMapping::ADDING) { + // There is no mapping established, so we can remove the entry. + // Getting a fresh token will trigger a new attempt. + gcm_driver_->RemoveAccountMapping(account_mapping_it->account_id); + accounts_.erase(account_mapping_it); + } else { + // Account is already MAPPED, we have to wait for another token. + account_mapping_it->last_message_id.clear(); + gcm_driver_->UpdateAccountMapping(*account_mapping_it); + } + } +} + +void GCMAccountMapper::OnSendAcknowledged(const std::string& app_id, + const std::string& message_id) { + DCHECK_EQ(app_id, kGCMAccountMapperAppId); + auto account_mapping_it = FindMappingByMessageId(message_id); + + DVLOG(1) << "OnSendAcknowledged with message ID: " << message_id; + + if (account_mapping_it == accounts_.end()) + return; + + // Here is where we advance a status of a mapping and persist or remove. + if (account_mapping_it->status == AccountMapping::REMOVING) { + // Message removing the account has been confirmed by the GCM, we can remove + // all the information related to the account (from memory and store). + gcm_driver_->RemoveAccountMapping(account_mapping_it->account_id); + accounts_.erase(account_mapping_it); + } else { + // Mapping status is ADDING only when it is a first time mapping. + DCHECK(account_mapping_it->status == AccountMapping::ADDING || + account_mapping_it->status == AccountMapping::MAPPED); + + // Account is marked as mapped with the current time. + account_mapping_it->status = AccountMapping::MAPPED; + account_mapping_it->status_change_timestamp = clock_->Now(); + // There is no pending message for the account. + account_mapping_it->last_message_id.clear(); + + gcm_driver_->UpdateAccountMapping(*account_mapping_it); + } +} + +bool GCMAccountMapper::CanHandle(const std::string& app_id) const { + return app_id.compare(kGCMAccountMapperAppId) == 0; +} + +bool GCMAccountMapper::IsReady() { + return initialized_ && gcm_driver_->IsStarted() && !registration_id_.empty(); +} + +void GCMAccountMapper::SendAddMappingMessage(AccountMapping& account_mapping) { + CreateAndSendMessage(account_mapping); +} + +void GCMAccountMapper::SendRemoveMappingMessage( + AccountMapping& account_mapping) { + // We want to persist an account that is being removed as quickly as possible + // as well as clean up the last message information. + if (account_mapping.status != AccountMapping::REMOVING) { + account_mapping.status = AccountMapping::REMOVING; + account_mapping.status_change_timestamp = clock_->Now(); + } + + account_mapping.last_message_id.clear(); + + gcm_driver_->UpdateAccountMapping(account_mapping); + + CreateAndSendMessage(account_mapping); +} + +void GCMAccountMapper::CreateAndSendMessage( + const AccountMapping& account_mapping) { + OutgoingMessage outgoing_message; + outgoing_message.id = GenerateMessageID(); + outgoing_message.data[kRegistrationIdMessgaeKey] = registration_id_; + outgoing_message.data[kAccountMessageKey] = account_mapping.email; + + if (account_mapping.status == AccountMapping::REMOVING) { + outgoing_message.time_to_live = kGCMRemoveMappingMessageTTL; + outgoing_message.data[kRemoveAccountKey] = kRemoveAccountValue; + } else { + outgoing_message.data[kTokenMessageKey] = account_mapping.access_token; + outgoing_message.time_to_live = kGCMAddMappingMessageTTL; + } + + gcm_driver_->Send(kGCMAccountMapperAppId, kGCMAccountMapperSendTo, + outgoing_message, + base::BindOnce(&GCMAccountMapper::OnSendFinished, + weak_ptr_factory_.GetWeakPtr(), + account_mapping.account_id)); +} + +void GCMAccountMapper::OnSendFinished(const CoreAccountId& account_id, + const std::string& message_id, + GCMClient::Result result) { + // TODO(fgorski): Add another attempt, in case the QUEUE is not full. + if (result != GCMClient::SUCCESS) + return; + + AccountMapping* account_mapping = FindMappingByAccountId(account_id); + DCHECK(account_mapping); + + // If we are dealing with account with status NEW, it is the first time + // mapping, and we should mark it as ADDING. + if (account_mapping->status == AccountMapping::NEW) { + account_mapping->status = AccountMapping::ADDING; + account_mapping->status_change_timestamp = clock_->Now(); + } + + account_mapping->last_message_id = message_id; + + gcm_driver_->UpdateAccountMapping(*account_mapping); +} + +void GCMAccountMapper::GetRegistration() { + DCHECK(registration_id_.empty()); + std::vector<std::string> sender_ids; + sender_ids.push_back(kGCMAccountMapperSenderId); + gcm_driver_->Register(kGCMAccountMapperAppId, sender_ids, + base::BindOnce(&GCMAccountMapper::OnRegisterFinished, + weak_ptr_factory_.GetWeakPtr())); +} + +void GCMAccountMapper::OnRegisterFinished(const std::string& registration_id, + GCMClient::Result result) { + if (result == GCMClient::SUCCESS) + registration_id_ = registration_id; + + if (IsReady()) { + if (!pending_account_tokens_.empty()) { + SetAccountTokens(pending_account_tokens_); + pending_account_tokens_.clear(); + } + } +} + +bool GCMAccountMapper::CanTriggerUpdate( + const base::Time& last_update_time) const { + return last_update_time + + base::Hours(kGCMUpdateIntervalHours - kGCMUpdateEarlyStartHours) < + clock_->Now(); +} + +bool GCMAccountMapper::IsLastStatusChangeOlderThanTTL( + const AccountMapping& account_mapping) const { + int ttl_seconds = account_mapping.status == AccountMapping::REMOVING ? + kGCMRemoveMappingMessageTTL : kGCMAddMappingMessageTTL; + return account_mapping.status_change_timestamp + base::Seconds(ttl_seconds) < + clock_->Now(); +} + +AccountMapping* GCMAccountMapper::FindMappingByAccountId( + const CoreAccountId& account_id) { + for (auto iter = accounts_.begin(); iter != accounts_.end(); ++iter) { + if (iter->account_id == account_id) + return &*iter; + } + + return nullptr; +} + +GCMAccountMapper::AccountMappings::iterator +GCMAccountMapper::FindMappingByMessageId(const std::string& message_id) { + for (auto iter = accounts_.begin(); iter != accounts_.end(); ++iter) { + if (iter->last_message_id == message_id) + return iter; + } + + return accounts_.end(); +} + +void GCMAccountMapper::SetClockForTesting(base::Clock* clock) { + clock_ = clock; +} + +} // namespace gcm |