| // 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/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), |
| 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<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 (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<GCMClient::AccountTokenInfo>::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::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. |
| |
| if (dispatch_message_callback_.is_null()) { |
| DVLOG(1) << "dispatch_message_callback_ missing in GCMAccountMapper"; |
| return; |
| } |
| |
| 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. |
| 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) { |
| 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<std::string> 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 nullptr; |
| } |
| |
| GCMAccountMapper::AccountMappings::iterator |
| GCMAccountMapper::FindMappingByMessageId(const std::string& message_id) { |
| for (std::vector<AccountMapping>::iterator 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 |