// 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 "base/bind.h" #include "base/guid.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_(new base::DefaultClock), initialized_(false), weak_ptr_factory_(this) { } GCMAccountMapper::~GCMAccountMapper() { } 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& 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 (AccountMappings::iterator iter = accounts_.begin(); iter != accounts_.end(); ++iter) { iter->access_token.clear(); } // Update the internal collection of mappings with the new tokens. for (std::vector::const_iterator 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 (AccountMappings::iterator 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::OnMessage(const std::string& app_id, const GCMClient::IncomingMessage& message) { DCHECK_EQ(app_id, kGCMAccountMapperAppId); // TODO(fgorski): Report Send to Gaia ID failures using UMA. if (dispatch_message_callback_.is_null()) { DVLOG(1) << "dispatch_message_callback_ missing in GCMAccountMapper"; return; } GCMClient::MessageData::const_iterator 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. GCMClient::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); AccountMappings::iterator 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); AccountMappings::iterator 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) { GCMClient::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::Bind(&GCMAccountMapper::OnSendFinished, weak_ptr_factory_.GetWeakPtr(), account_mapping.account_id)); } void GCMAccountMapper::OnSendFinished(const std::string& 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 sender_ids; sender_ids.push_back(kGCMAccountMapperSenderId); gcm_driver_->Register(kGCMAccountMapperAppId, sender_ids, base::Bind(&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::TimeDelta::FromHours(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::TimeDelta::FromSeconds(ttl_seconds) < clock_->Now(); } AccountMapping* GCMAccountMapper::FindMappingByAccountId( const std::string& account_id) { for (AccountMappings::iterator iter = accounts_.begin(); iter != accounts_.end(); ++iter) { if (iter->account_id == account_id) return &*iter; } return NULL; } GCMAccountMapper::AccountMappings::iterator GCMAccountMapper::FindMappingByMessageId(const std::string& message_id) { for (std::vector::iterator iter = accounts_.begin(); iter != accounts_.end(); ++iter) { if (iter->last_message_id == message_id) return iter; } return accounts_.end(); } void GCMAccountMapper::SetClockForTesting(scoped_ptr clock) { clock_ = clock.Pass(); } } // namespace gcm