text: // 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 "chrome/browser/services/gcm/gcm_account_tracker.h"
  
  #include <algorithm>
  #include <vector>
  
  #include "base/time/time.h"
  #include "google_apis/gaia/google_service_auth_error.h"
  
  namespace gcm {
  
  namespace {
  const char kGCMGroupServerScope[] = "https://www.googleapis.com/auth/gcm";
  const char kGCMAccountTrackerName[] = "gcm_account_tracker";
  }  // namespace
  
  GCMAccountTracker::AccountInfo::AccountInfo(const std::string& email,
                                              AccountState state)
      : email(email), state(state) {
  }
  
  GCMAccountTracker::AccountInfo::~AccountInfo() {
  }
  
  GCMAccountTracker::GCMAccountTracker(
      scoped_ptr<gaia::AccountTracker> account_tracker,
      const UpdateAccountsCallback& callback)
      : OAuth2TokenService::Consumer(kGCMAccountTrackerName),
        account_tracker_(account_tracker.release()),
        callback_(callback),
        shutdown_called_(false) {
    DCHECK(!callback_.is_null());
  }
  
  GCMAccountTracker::~GCMAccountTracker() {
    DCHECK(shutdown_called_);
  }
  
  void GCMAccountTracker::Shutdown() {
    Stop();
    shutdown_called_ = true;
    account_tracker_->Shutdown();
  }
  
  void GCMAccountTracker::Start() {
    DCHECK(!shutdown_called_);
    account_tracker_->AddObserver(this);
  
    std::vector<gaia::AccountIds> accounts = account_tracker_->GetAccounts();
    if (accounts.empty()) {
      CompleteCollectingTokens();
      return;
    }
  
    for (std::vector<gaia::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)));
      }
    }
  
    GetAllNeededTokens();
  }
  
  void GCMAccountTracker::Stop() {
    DCHECK(!shutdown_called_);
    account_tracker_->RemoveObserver(this);
    pending_token_requests_.clear();
  }
  
  void GCMAccountTracker::OnAccountAdded(const gaia::AccountIds& ids) {
    DVLOG(1) << "Account added: " << ids.email;
    // We listen for the account signing in, which happens after account is added.
  }
  
  void GCMAccountTracker::OnAccountRemoved(const gaia::AccountIds& ids) {
    DVLOG(1) << "Account removed: " << ids.email;
    // We listen for the account signing out, which happens before account is
    // removed.
  }
  
  void GCMAccountTracker::OnAccountSignInChanged(const gaia::AccountIds& ids,
                                                 bool is_signed_in) {
    if (is_signed_in)
      OnAccountSignedIn(ids);
    else
      OnAccountSignedOut(ids);
  }
  
  void GCMAccountTracker::OnGetTokenSuccess(
      const OAuth2TokenService::Request* request,
      const std::string& access_token,
      const base::Time& expiration_time) {
    DCHECK(request);
    DCHECK(!request->GetAccountId().empty());
    DVLOG(1) << "Get token success: " << request->GetAccountId();
  
    AccountInfos::iterator iter = account_infos_.find(request->GetAccountId());
    DCHECK(iter != account_infos_.end());
    if (iter != account_infos_.end()) {
      DCHECK(iter->second.state == GETTING_TOKEN ||
             iter->second.state == ACCOUNT_REMOVED);
      // If OnAccountSignedOut(..) was called most recently, account is kept in
      // ACCOUNT_REMOVED state.
      if (iter->second.state == GETTING_TOKEN) {
        iter->second.state = TOKEN_PRESENT;
        iter->second.access_token = access_token;
      }
    }
  
    DeleteTokenRequest(request);
    CompleteCollectingTokens();
  }
  
  void GCMAccountTracker::OnGetTokenFailure(
      const OAuth2TokenService::Request* request,
      const GoogleServiceAuthError& error) {
    DCHECK(request);
    DCHECK(!request->GetAccountId().empty());
    DVLOG(1) << "Get token failure: " << request->GetAccountId();
  
    AccountInfos::iterator iter = account_infos_.find(request->GetAccountId());
    DCHECK(iter != account_infos_.end());
    if (iter != account_infos_.end()) {
      DCHECK(iter->second.state == GETTING_TOKEN ||
             iter->second.state == ACCOUNT_REMOVED);
      // If OnAccountSignedOut(..) was called most recently, account is kept in
      // ACCOUNT_REMOVED state.
      if (iter->second.state == GETTING_TOKEN)
        iter->second.state = TOKEN_NEEDED;
    }
  
    DeleteTokenRequest(request);
    CompleteCollectingTokens();
  }
  
  void GCMAccountTracker::CompleteCollectingTokens() {
    DCHECK(!callback_.is_null());
    // Wait for gaia::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;
    std::map<std::string, std::string> account_tokens;
    for (AccountInfos::iterator iter = account_infos_.begin();
         iter != account_infos_.end();) {
      switch (iter->second.state) {
        case ACCOUNT_REMOVED:
          // We only mark accounts as removed when there was an account that was
          // explicitly signed out.
          account_removed = true;
          // We also stop tracking the account, now that it will be reported as
          // removed.
          account_infos_.erase(iter++);
          break;
  
        case TOKEN_PRESENT:
          account_tokens[iter->second.email] = iter->second.access_token;
          ++iter;
          break;
  
        case GETTING_TOKEN:
          // This should not happen, as we are making a check that there are no
          // pending requests above.
          NOTREACHED();
          ++iter;
          break;
  
        case TOKEN_NEEDED:
          // We failed to fetch an access token for the account, but it has not
          // been signed out (perhaps there is a network issue). We don't report
          // it, but next time there is a sign-in change we will update its state.
          ++iter;
          break;
      }
    }
  
    // Make sure that there is something to report, otherwise bail out.
    if (!account_tokens.empty() || account_removed) {
      DVLOG(1) << "Calling callback: " << account_tokens.size();
      callback_.Run(account_tokens);
    } else {
      DVLOG(1) << "No tokens and nothing removed. Skipping callback.";
    }
  }
  
  void GCMAccountTracker::DeleteTokenRequest(
      const OAuth2TokenService::Request* request) {
    ScopedVector<OAuth2TokenService::Request>::iterator iter = std::find(
        pending_token_requests_.begin(), pending_token_requests_.end(), request);
    if (iter != pending_token_requests_.end())
      pending_token_requests_.erase(iter);
  }
  
  void GCMAccountTracker::GetAllNeededTokens() {
    for (AccountInfos::iterator 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(GetTokenService());
    DCHECK_EQ(account_iter->second.state, TOKEN_NEEDED);
  
    OAuth2TokenService::ScopeSet scopes;
    scopes.insert(kGCMGroupServerScope);
    scoped_ptr<OAuth2TokenService::Request> request =
        GetTokenService()->StartRequest(account_iter->first, scopes, this);
  
    pending_token_requests_.push_back(request.release());
    account_iter->second.state = GETTING_TOKEN;
  }
  
  void GCMAccountTracker::OnAccountSignedIn(const gaia::AccountIds& ids) {
    DVLOG(1) << "Account signed in: " << ids.email;
    AccountInfos::iterator 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 gaia::AccountIds& ids) {
    DVLOG(1) << "Account signed out: " << ids.email;
    AccountInfos::iterator iter = account_infos_.find(ids.account_key);
    if (iter == account_infos_.end())
      return;
  
    iter->second.access_token.clear();
    iter->second.state = ACCOUNT_REMOVED;
    CompleteCollectingTokens();
  }
  
  OAuth2TokenService* GCMAccountTracker::GetTokenService() {
    DCHECK(account_tracker_->identity_provider());
    return account_tracker_->identity_provider()->GetTokenService();
  }
  
  }  // namespace gcm
  