blob: b1bc99656e6bd4e48854da4c39389fcbf1b9276b [file] [log] [blame]
// 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_tracker.h"
#include <stdint.h>
#include <algorithm>
#include <vector>
#include "base/bind.h"
#include "base/location.h"
#include "base/single_thread_task_runner.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/time/time.h"
#include "components/gcm_driver/gcm_driver.h"
#include "google_apis/gaia/google_service_auth_error.h"
#include "net/base/ip_endpoint.h"
#include "services/identity/public/cpp/access_token_fetcher.h"
#include "services/identity/public/cpp/identity_manager.h"
namespace gcm {
namespace {
// Scopes needed by the OAuth2 access tokens.
const char kGCMGroupServerScope[] = "https://www.googleapis.com/auth/gcm";
const char kGCMCheckinServerScope[] =
"https://www.googleapis.com/auth/android_checkin";
// Name of the GCM account tracker for fetching access tokens.
const char kGCMAccountTrackerName[] = "gcm_account_tracker";
// Minimum token validity when sending to GCM groups server.
const int64_t kMinimumTokenValidityMs = 500;
// Token reporting interval, when no account changes are detected.
const int64_t kTokenReportingIntervalMs =
12 * 60 * 60 * 1000; // 12 hours in ms.
} // namespace
GCMAccountTracker::AccountInfo::AccountInfo(const std::string& email,
AccountState state)
: email(email), state(state) {
}
GCMAccountTracker::AccountInfo::~AccountInfo() {
}
GCMAccountTracker::GCMAccountTracker(
std::unique_ptr<AccountTracker> account_tracker,
identity::IdentityManager* identity_manager,
GCMDriver* driver)
: account_tracker_(account_tracker.release()),
identity_manager_(identity_manager),
driver_(driver),
shutdown_called_(false),
reporting_weak_ptr_factory_(this) {}
GCMAccountTracker::~GCMAccountTracker() {
DCHECK(shutdown_called_);
}
void GCMAccountTracker::Shutdown() {
shutdown_called_ = true;
driver_->RemoveConnectionObserver(this);
account_tracker_->RemoveObserver(this);
account_tracker_->Shutdown();
}
void GCMAccountTracker::Start() {
DCHECK(!shutdown_called_);
account_tracker_->AddObserver(this);
driver_->AddConnectionObserver(this);
std::vector<AccountIds> accounts = account_tracker_->GetAccounts();
for (std::vector<AccountIds>::const_iterator iter = accounts.begin();
iter != accounts.end(); ++iter) {
if (!iter->email.empty()) {
account_infos_.insert(std::make_pair(
iter->account_key, AccountInfo(iter->email, TOKEN_NEEDED)));
}
}
if (IsTokenReportingRequired())
ReportTokens();
else
ScheduleReportTokens();
}
void GCMAccountTracker::ScheduleReportTokens() {
// Shortcutting here, in case GCM Driver is not yet connected. In that case
// reporting will be scheduled/started when the connection is made.
if (!driver_->IsConnected())
return;
DVLOG(1) << "Deferring the token reporting for: "
<< GetTimeToNextTokenReporting().InSeconds() << " seconds.";
reporting_weak_ptr_factory_.InvalidateWeakPtrs();
base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&GCMAccountTracker::ReportTokens,
reporting_weak_ptr_factory_.GetWeakPtr()),
GetTimeToNextTokenReporting());
}
void GCMAccountTracker::OnAccountSignInChanged(const AccountIds& ids,
bool is_signed_in) {
if (is_signed_in)
OnAccountSignedIn(ids);
else
OnAccountSignedOut(ids);
}
void GCMAccountTracker::OnAccessTokenFetchCompleteForAccount(
std::string account_id,
GoogleServiceAuthError error,
identity::AccessTokenInfo access_token_info) {
auto iter = account_infos_.find(account_id);
DCHECK(iter != account_infos_.end());
if (iter != account_infos_.end()) {
DCHECK_EQ(GETTING_TOKEN, iter->second.state);
if (error.state() == GoogleServiceAuthError::NONE) {
DVLOG(1) << "Get token success: " << account_id;
iter->second.state = TOKEN_PRESENT;
iter->second.access_token = access_token_info.token;
iter->second.expiration_time = access_token_info.expiration_time;
} else {
DVLOG(1) << "Get token failure: " << account_id;
// Given the fetcher has a built in retry logic, consider this situation
// to be invalid refresh token, that is only fixed when user signs in.
// Once the users signs in properly the minting will retry.
iter->second.access_token.clear();
iter->second.state = ACCOUNT_REMOVED;
}
}
pending_token_requests_.erase(account_id);
ReportTokens();
}
void GCMAccountTracker::OnConnected(const net::IPEndPoint& ip_endpoint) {
// We are sure here, that GCM is running and connected. We can start reporting
// tokens if reporting is due now, or schedule reporting for later.
if (IsTokenReportingRequired())
ReportTokens();
else
ScheduleReportTokens();
}
void GCMAccountTracker::OnDisconnected() {
// We are disconnected, so no point in trying to work with tokens.
}
void GCMAccountTracker::ReportTokens() {
SanitizeTokens();
// Make sure all tokens are valid.
if (IsTokenFetchingRequired()) {
GetAllNeededTokens();
return;
}
// Wait for AccountTracker to be done with fetching the user info, as
// well as all of the pending token requests from GCMAccountTracker to be done
// before you report the results.
if (!account_tracker_->IsAllUserInfoFetched() ||
!pending_token_requests_.empty()) {
return;
}
bool account_removed = false;
// Stop tracking the accounts, that were removed, as it will be reported to
// the driver.
for (auto iter = account_infos_.begin(); iter != account_infos_.end();) {
if (iter->second.state == ACCOUNT_REMOVED) {
account_removed = true;
account_infos_.erase(iter++);
} else {
++iter;
}
}
std::vector<GCMClient::AccountTokenInfo> account_tokens;
for (auto iter = account_infos_.begin(); iter != account_infos_.end();
++iter) {
if (iter->second.state == TOKEN_PRESENT) {
GCMClient::AccountTokenInfo token_info;
token_info.account_id = iter->first;
token_info.email = iter->second.email;
token_info.access_token = iter->second.access_token;
account_tokens.push_back(token_info);
} else {
// This should not happen, as we are making a check that there are no
// pending requests above, stopping tracking of removed accounts, or start
// fetching tokens.
NOTREACHED();
}
}
// Make sure that there is something to report, otherwise bail out.
if (!account_tokens.empty() || account_removed) {
DVLOG(1) << "Reporting the tokens to driver: " << account_tokens.size();
driver_->SetAccountTokens(account_tokens);
driver_->SetLastTokenFetchTime(base::Time::Now());
ScheduleReportTokens();
} else {
DVLOG(1) << "No tokens and nothing removed. Skipping callback.";
}
}
void GCMAccountTracker::SanitizeTokens() {
for (auto iter = account_infos_.begin(); iter != account_infos_.end();
++iter) {
if (iter->second.state == TOKEN_PRESENT &&
iter->second.expiration_time <
base::Time::Now() +
base::TimeDelta::FromMilliseconds(kMinimumTokenValidityMs)) {
iter->second.access_token.clear();
iter->second.state = TOKEN_NEEDED;
iter->second.expiration_time = base::Time();
}
}
}
bool GCMAccountTracker::IsTokenReportingRequired() const {
if (GetTimeToNextTokenReporting().is_zero())
return true;
bool reporting_required = false;
for (auto iter = account_infos_.begin(); iter != account_infos_.end();
++iter) {
if (iter->second.state == ACCOUNT_REMOVED)
reporting_required = true;
}
return reporting_required;
}
bool GCMAccountTracker::IsTokenFetchingRequired() const {
bool token_needed = false;
for (auto iter = account_infos_.begin(); iter != account_infos_.end();
++iter) {
if (iter->second.state == TOKEN_NEEDED)
token_needed = true;
}
return token_needed;
}
base::TimeDelta GCMAccountTracker::GetTimeToNextTokenReporting() const {
base::TimeDelta time_till_next_reporting =
driver_->GetLastTokenFetchTime() +
base::TimeDelta::FromMilliseconds(kTokenReportingIntervalMs) -
base::Time::Now();
// Case when token fetching is overdue.
if (time_till_next_reporting < base::TimeDelta())
return base::TimeDelta();
// Case when calculated period is larger than expected, including the
// situation when the method is called before GCM driver is completely
// initialized.
if (time_till_next_reporting >
base::TimeDelta::FromMilliseconds(kTokenReportingIntervalMs)) {
return base::TimeDelta::FromMilliseconds(kTokenReportingIntervalMs);
}
return time_till_next_reporting;
}
void GCMAccountTracker::GetAllNeededTokens() {
// Only start fetching tokens if driver is running, they have a limited
// validity time and GCM connection is a good indication of network running.
// If the GetAllNeededTokens was called as part of periodic schedule, it may
// not have network. In that case the next network change will trigger token
// fetching.
if (!driver_->IsConnected())
return;
for (auto iter = account_infos_.begin(); iter != account_infos_.end();
++iter) {
if (iter->second.state == TOKEN_NEEDED)
GetToken(iter);
}
}
void GCMAccountTracker::GetToken(AccountInfos::iterator& account_iter) {
DCHECK_EQ(account_iter->second.state, TOKEN_NEEDED);
identity::ScopeSet scopes;
scopes.insert(kGCMGroupServerScope);
scopes.insert(kGCMCheckinServerScope);
// NOTE: It is safe to use base::Unretained() here as |token_fetcher| is owned
// by this object and guarantees that it will not invoke its callback after
// its destruction.
std::unique_ptr<identity::AccessTokenFetcher> token_fetcher =
identity_manager_->CreateAccessTokenFetcherForAccount(
account_iter->first, kGCMAccountTrackerName, scopes,
base::BindOnce(
&GCMAccountTracker::OnAccessTokenFetchCompleteForAccount,
base::Unretained(this), account_iter->first),
identity::AccessTokenFetcher::Mode::kImmediate);
DCHECK(pending_token_requests_.count(account_iter->first) == 0);
pending_token_requests_.emplace(account_iter->first,
std::move(token_fetcher));
account_iter->second.state = GETTING_TOKEN;
}
void GCMAccountTracker::OnAccountSignedIn(const AccountIds& ids) {
DVLOG(1) << "Account signed in: " << ids.email;
auto iter = account_infos_.find(ids.account_key);
if (iter == account_infos_.end()) {
DCHECK(!ids.email.empty());
account_infos_.insert(
std::make_pair(ids.account_key, AccountInfo(ids.email, TOKEN_NEEDED)));
} else if (iter->second.state == ACCOUNT_REMOVED) {
iter->second.state = TOKEN_NEEDED;
}
GetAllNeededTokens();
}
void GCMAccountTracker::OnAccountSignedOut(const AccountIds& ids) {
DVLOG(1) << "Account signed out: " << ids.email;
auto iter = account_infos_.find(ids.account_key);
if (iter == account_infos_.end())
return;
iter->second.access_token.clear();
iter->second.state = ACCOUNT_REMOVED;
// Delete any ongoing access token request now so that if the account is later
// re-added and a new access token request made, we do not break this class'
// invariant that there is at most one ongoing access token request per
// account.
pending_token_requests_.erase(ids.account_key);
ReportTokens();
}
} // namespace gcm