| // Copyright 2013 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/signin/core/browser/oauth2_token_service_delegate_android.h" |
| |
| #include "base/android/jni_android.h" |
| #include "base/android/jni_array.h" |
| #include "base/android/jni_string.h" |
| #include "base/bind.h" |
| #include "base/feature_list.h" |
| #include "base/logging.h" |
| #include "base/macros.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/stl_util.h" |
| #include "components/signin/core/browser/account_consistency_method.h" |
| #include "components/signin/core/browser/account_info.h" |
| #include "google_apis/gaia/gaia_auth_util.h" |
| #include "google_apis/gaia/oauth2_access_token_fetcher.h" |
| #include "jni/OAuth2TokenService_jni.h" |
| |
| using base::android::AttachCurrentThread; |
| using base::android::ConvertJavaStringToUTF8; |
| using base::android::ConvertUTF8ToJavaString; |
| using base::android::JavaParamRef; |
| using base::android::JavaRef; |
| using base::android::ScopedJavaLocalRef; |
| |
| namespace { |
| |
| // Callback from FetchOAuth2TokenWithUsername(). |
| // Arguments: |
| // - the error, or NONE if the token fetch was successful. |
| // - the OAuth2 access token. |
| // - the expiry time of the token (may be null, indicating that the expiry |
| // time is unknown. |
| typedef base::Callback< |
| void(const GoogleServiceAuthError&, const std::string&, const base::Time&)> |
| FetchOAuth2TokenCallback; |
| |
| class AndroidAccessTokenFetcher : public OAuth2AccessTokenFetcher { |
| public: |
| AndroidAccessTokenFetcher(OAuth2AccessTokenConsumer* consumer, |
| const std::string& account_id); |
| ~AndroidAccessTokenFetcher() override; |
| |
| // Overrides from OAuth2AccessTokenFetcher: |
| void Start(const std::string& client_id, |
| const std::string& client_secret, |
| const std::vector<std::string>& scopes) override; |
| void CancelRequest() override; |
| |
| // Handles an access token response. |
| void OnAccessTokenResponse(const GoogleServiceAuthError& error, |
| const std::string& access_token, |
| const base::Time& expiration_time); |
| |
| private: |
| std::string CombineScopes(const std::vector<std::string>& scopes); |
| |
| std::string account_id_; |
| bool request_was_cancelled_; |
| base::WeakPtrFactory<AndroidAccessTokenFetcher> weak_factory_; |
| |
| DISALLOW_COPY_AND_ASSIGN(AndroidAccessTokenFetcher); |
| }; |
| |
| AndroidAccessTokenFetcher::AndroidAccessTokenFetcher( |
| OAuth2AccessTokenConsumer* consumer, |
| const std::string& account_id) |
| : OAuth2AccessTokenFetcher(consumer), |
| account_id_(account_id), |
| request_was_cancelled_(false), |
| weak_factory_(this) {} |
| |
| AndroidAccessTokenFetcher::~AndroidAccessTokenFetcher() {} |
| |
| void AndroidAccessTokenFetcher::Start(const std::string& client_id, |
| const std::string& client_secret, |
| const std::vector<std::string>& scopes) { |
| JNIEnv* env = AttachCurrentThread(); |
| std::string scope = CombineScopes(scopes); |
| ScopedJavaLocalRef<jstring> j_username = |
| ConvertUTF8ToJavaString(env, account_id_); |
| ScopedJavaLocalRef<jstring> j_scope = ConvertUTF8ToJavaString(env, scope); |
| std::unique_ptr<FetchOAuth2TokenCallback> heap_callback( |
| new FetchOAuth2TokenCallback( |
| base::Bind(&AndroidAccessTokenFetcher::OnAccessTokenResponse, |
| weak_factory_.GetWeakPtr()))); |
| |
| // Call into Java to get a new token. |
| Java_OAuth2TokenService_getAccessTokenFromNative( |
| env, j_username, j_scope, |
| reinterpret_cast<intptr_t>(heap_callback.release())); |
| } |
| |
| void AndroidAccessTokenFetcher::CancelRequest() { |
| request_was_cancelled_ = true; |
| } |
| |
| void AndroidAccessTokenFetcher::OnAccessTokenResponse( |
| const GoogleServiceAuthError& error, |
| const std::string& access_token, |
| const base::Time& expiration_time) { |
| if (request_was_cancelled_) { |
| // Ignore the callback if the request was cancelled. |
| return; |
| } |
| if (error.state() == GoogleServiceAuthError::NONE) { |
| FireOnGetTokenSuccess(OAuth2AccessTokenConsumer::TokenResponse( |
| access_token, expiration_time, std::string())); |
| } else { |
| FireOnGetTokenFailure(error); |
| } |
| } |
| |
| // static |
| std::string AndroidAccessTokenFetcher::CombineScopes( |
| const std::vector<std::string>& scopes) { |
| // The Android AccountManager supports multiple scopes separated by a space: |
| // https://code.google.com/p/google-api-java-client/wiki/OAuth2#Android |
| std::string scope; |
| for (std::vector<std::string>::const_iterator it = scopes.begin(); |
| it != scopes.end(); ++it) { |
| if (!scope.empty()) |
| scope += " "; |
| scope += *it; |
| } |
| return scope; |
| } |
| |
| } // namespace |
| |
| bool OAuth2TokenServiceDelegateAndroid:: |
| disable_interaction_with_system_accounts_ = false; |
| |
| OAuth2TokenServiceDelegateAndroid::OAuth2TokenServiceDelegateAndroid( |
| AccountTrackerService* account_tracker_service) |
| : account_tracker_service_(account_tracker_service), |
| fire_refresh_token_loaded_(RT_LOAD_NOT_START) { |
| DVLOG(1) << "OAuth2TokenServiceDelegateAndroid::ctor"; |
| DCHECK(account_tracker_service_); |
| JNIEnv* env = AttachCurrentThread(); |
| base::android::ScopedJavaLocalRef<jobject> local_java_ref = |
| Java_OAuth2TokenService_create(env, reinterpret_cast<intptr_t>(this), |
| account_tracker_service_->GetJavaObject()); |
| java_ref_.Reset(env, local_java_ref.obj()); |
| |
| if (account_tracker_service_->GetMigrationState() == |
| AccountTrackerService::MIGRATION_IN_PROGRESS) { |
| std::vector<std::string> accounts = GetAccounts(); |
| std::vector<std::string> accounts_id; |
| for (auto account_name : accounts) { |
| AccountInfo account_info = |
| account_tracker_service_->FindAccountInfoByEmail(account_name); |
| DCHECK(!account_info.gaia.empty()); |
| accounts_id.push_back(account_info.gaia); |
| } |
| ScopedJavaLocalRef<jobjectArray> java_accounts( |
| base::android::ToJavaArrayOfStrings(env, accounts_id)); |
| Java_OAuth2TokenService_saveStoredAccounts(env, java_accounts); |
| } |
| |
| if (!disable_interaction_with_system_accounts_) { |
| Java_OAuth2TokenService_updateAccountList(AttachCurrentThread(), java_ref_); |
| } |
| } |
| |
| OAuth2TokenServiceDelegateAndroid::~OAuth2TokenServiceDelegateAndroid() {} |
| |
| ScopedJavaLocalRef<jobject> OAuth2TokenServiceDelegateAndroid::GetJavaObject() { |
| return ScopedJavaLocalRef<jobject>(java_ref_); |
| } |
| |
| bool OAuth2TokenServiceDelegateAndroid::RefreshTokenIsAvailable( |
| const std::string& account_id) const { |
| DVLOG(1) << "OAuth2TokenServiceDelegateAndroid::RefreshTokenIsAvailable" |
| << " account= " << account_id; |
| std::string account_name = MapAccountIdToAccountName(account_id); |
| if (account_name.empty()) { |
| // This corresponds to the case when the account with id |account_id| is not |
| // present on the device and thus was not seeded. |
| DVLOG(1) << "OAuth2TokenServiceDelegateAndroid::RefreshTokenIsAvailable" |
| << " cannot find account name for account id " << account_id; |
| return false; |
| } |
| JNIEnv* env = AttachCurrentThread(); |
| ScopedJavaLocalRef<jstring> j_account_id = |
| ConvertUTF8ToJavaString(env, account_name); |
| jboolean refresh_token_is_available = |
| Java_OAuth2TokenService_hasOAuth2RefreshToken(env, j_account_id); |
| return refresh_token_is_available == JNI_TRUE; |
| } |
| |
| GoogleServiceAuthError OAuth2TokenServiceDelegateAndroid::GetAuthError( |
| const std::string& account_id) const { |
| auto it = errors_.find(account_id); |
| return (it == errors_.end()) ? GoogleServiceAuthError::AuthErrorNone() |
| : it->second; |
| } |
| |
| void OAuth2TokenServiceDelegateAndroid::UpdateAuthError( |
| const std::string& account_id, |
| const GoogleServiceAuthError& error) { |
| DVLOG(1) << "OAuth2TokenServiceDelegateAndroid::UpdateAuthError" |
| << " account=" << account_id << " error=" << error.ToString(); |
| |
| if (error.IsTransientError()) |
| return; |
| |
| auto it = errors_.find(account_id); |
| if (error.state() == GoogleServiceAuthError::NONE) { |
| if (it == errors_.end()) |
| return; |
| errors_.erase(it); |
| } else { |
| if (it != errors_.end() && it->second == error) |
| return; |
| errors_[account_id] = error; |
| } |
| FireAuthErrorChanged(account_id, error); |
| } |
| |
| std::vector<std::string> OAuth2TokenServiceDelegateAndroid::GetAccounts() { |
| std::vector<std::string> accounts; |
| JNIEnv* env = AttachCurrentThread(); |
| ScopedJavaLocalRef<jobjectArray> j_accounts = |
| Java_OAuth2TokenService_getAccounts(env); |
| // TODO(fgorski): We may decide to filter out some of the accounts. |
| base::android::AppendJavaStringArrayToStringVector(env, j_accounts, |
| &accounts); |
| return accounts; |
| } |
| |
| std::vector<std::string> |
| OAuth2TokenServiceDelegateAndroid::GetSystemAccountNames() { |
| std::vector<std::string> account_names; |
| JNIEnv* env = AttachCurrentThread(); |
| ScopedJavaLocalRef<jobjectArray> j_accounts = |
| Java_OAuth2TokenService_getSystemAccountNames(env); |
| base::android::AppendJavaStringArrayToStringVector(env, j_accounts, |
| &account_names); |
| return account_names; |
| } |
| |
| OAuth2AccessTokenFetcher* |
| OAuth2TokenServiceDelegateAndroid::CreateAccessTokenFetcher( |
| const std::string& account_id, |
| scoped_refptr<network::SharedURLLoaderFactory> url_factory, |
| OAuth2AccessTokenConsumer* consumer) { |
| DVLOG(1) << "OAuth2TokenServiceDelegateAndroid::CreateAccessTokenFetcher" |
| << " account= " << account_id; |
| ValidateAccountId(account_id); |
| std::string account_name = MapAccountIdToAccountName(account_id); |
| DCHECK(!account_name.empty()) |
| << "Cannot find account name for account id " << account_id; |
| return new AndroidAccessTokenFetcher(consumer, account_name); |
| } |
| |
| void OAuth2TokenServiceDelegateAndroid::InvalidateAccessToken( |
| const std::string& account_id, |
| const std::string& client_id, |
| const OAuth2TokenService::ScopeSet& scopes, |
| const std::string& access_token) { |
| ValidateAccountId(account_id); |
| JNIEnv* env = AttachCurrentThread(); |
| ScopedJavaLocalRef<jstring> j_access_token = |
| ConvertUTF8ToJavaString(env, access_token); |
| Java_OAuth2TokenService_invalidateAccessToken(env, j_access_token); |
| } |
| |
| void OAuth2TokenServiceDelegateAndroid::UpdateAccountList( |
| JNIEnv* env, |
| const JavaParamRef<jobject>& obj, |
| const JavaParamRef<jstring>& j_current_acc) { |
| std::string signed_in_account_name; |
| DVLOG(1) << "OAuth2TokenServiceDelegateAndroid::UpdateAccountList from java"; |
| if (j_current_acc) |
| signed_in_account_name = ConvertJavaStringToUTF8(env, j_current_acc); |
| if (!signed_in_account_name.empty()) |
| signed_in_account_name = gaia::CanonicalizeEmail(signed_in_account_name); |
| |
| // Clear any auth errors so that client can retry to get access tokens. |
| errors_.clear(); |
| |
| UpdateAccountList(MapAccountNameToAccountId(signed_in_account_name)); |
| } |
| |
| void OAuth2TokenServiceDelegateAndroid::UpdateAccountList( |
| const std::string& signed_in_account_id) { |
| std::vector<std::string> curr_ids; |
| for (const std::string& curr_name : GetSystemAccountNames()) { |
| std::string curr_id(MapAccountNameToAccountId(curr_name)); |
| if (!curr_id.empty()) |
| curr_ids.push_back(curr_id); |
| } |
| |
| std::vector<std::string> prev_ids; |
| for (const std::string& prev_id : GetAccounts()) { |
| if (ValidateAccountId(prev_id)) |
| prev_ids.push_back(prev_id); |
| } |
| |
| DVLOG(1) << "OAuth2TokenServiceDelegateAndroid::UpdateAccountList:" |
| << " sigined_in_account_id=" << signed_in_account_id |
| << " prev_ids=" << prev_ids.size() |
| << " curr_ids=" << curr_ids.size(); |
| |
| std::vector<std::string> refreshed_ids; |
| std::vector<std::string> revoked_ids; |
| bool keep_accounts = UpdateAccountList( |
| signed_in_account_id, prev_ids, curr_ids, &refreshed_ids, &revoked_ids); |
| |
| ScopedBatchChange batch(this); |
| JNIEnv* env = AttachCurrentThread(); |
| ScopedJavaLocalRef<jobjectArray> java_accounts; |
| if (keep_accounts) { |
| java_accounts = base::android::ToJavaArrayOfStrings(env, curr_ids); |
| } else { |
| DCHECK(!base::FeatureList::IsEnabled(signin::kMiceFeature)); |
| java_accounts = |
| base::android::ToJavaArrayOfStrings(env, std::vector<std::string>()); |
| } |
| |
| // Save the current accounts in the token service before calling |
| // FireRefreshToken* methods. |
| Java_OAuth2TokenService_saveStoredAccounts(env, java_accounts); |
| |
| for (const std::string& refreshed_id : refreshed_ids) |
| FireRefreshTokenAvailable(refreshed_id); |
| for (const std::string& revoked_id : revoked_ids) |
| FireRefreshTokenRevoked(revoked_id); |
| if (fire_refresh_token_loaded_ == RT_WAIT_FOR_VALIDATION) { |
| fire_refresh_token_loaded_ = RT_LOADED; |
| FireRefreshTokensLoaded(); |
| } else if (fire_refresh_token_loaded_ == RT_LOAD_NOT_START) { |
| fire_refresh_token_loaded_ = RT_HAS_BEEN_VALIDATED; |
| } |
| |
| // Clear accounts no longer exist on device from AccountTrackerService. |
| std::vector<AccountInfo> accounts_info = |
| account_tracker_service_->GetAccounts(); |
| for (const AccountInfo& info : accounts_info) { |
| if (!base::ContainsValue(curr_ids, info.account_id)) |
| account_tracker_service_->RemoveAccount(info.account_id); |
| } |
| |
| // No need to wait for SigninManager to finish migration if not signed in. |
| if (account_tracker_service_->GetMigrationState() == |
| AccountTrackerService::MIGRATION_IN_PROGRESS && |
| signed_in_account_id.empty()) { |
| account_tracker_service_->SetMigrationDone(); |
| } |
| |
| if (!last_update_accounts_time_.is_null()) { |
| base::TimeDelta sample = base::Time::Now() - last_update_accounts_time_; |
| UmaHistogramLongTimes("Signin.AndroidTimeBetweenUpdateAccountList", sample); |
| } |
| last_update_accounts_time_ = base::Time::Now(); |
| } |
| |
| bool OAuth2TokenServiceDelegateAndroid::UpdateAccountList( |
| const std::string& signed_in_id, |
| const std::vector<std::string>& prev_ids, |
| const std::vector<std::string>& curr_ids, |
| std::vector<std::string>* refreshed_ids, |
| std::vector<std::string>* revoked_ids) { |
| bool keep_accounts = base::FeatureList::IsEnabled(signin::kMiceFeature) || |
| base::ContainsValue(curr_ids, signed_in_id); |
| if (keep_accounts) { |
| // Revoke token for ids that have been removed from the device. |
| for (const std::string& prev_id : prev_ids) { |
| if (prev_id == signed_in_id) |
| continue; |
| if (!base::ContainsValue(curr_ids, prev_id)) { |
| DVLOG(1) << "OAuth2TokenServiceDelegateAndroid::UpdateAccountList:" |
| << "revoked=" << prev_id; |
| revoked_ids->push_back(prev_id); |
| } |
| } |
| |
| if (!signed_in_id.empty()) { |
| // Always fire the primary signed in account first. |
| DVLOG(1) << "OAuth2TokenServiceDelegateAndroid::UpdateAccountList:" |
| << "refreshed=" << signed_in_id; |
| refreshed_ids->push_back(signed_in_id); |
| } |
| for (const std::string& curr_id : curr_ids) { |
| if (curr_id == signed_in_id) |
| continue; |
| DVLOG(1) << "OAuth2TokenServiceDelegateAndroid::UpdateAccountList:" |
| << "refreshed=" << curr_id; |
| refreshed_ids->push_back(curr_id); |
| } |
| } else { |
| // Revoke all ids. |
| if (base::ContainsValue(prev_ids, signed_in_id)) { |
| DVLOG(1) << "OAuth2TokenServiceDelegateAndroid::UpdateAccountList:" |
| << "revoked=" << signed_in_id; |
| revoked_ids->push_back(signed_in_id); |
| } |
| for (const std::string& prev_id : prev_ids) { |
| if (prev_id == signed_in_id) |
| continue; |
| DVLOG(1) << "OAuth2TokenServiceDelegateAndroid::UpdateAccountList:" |
| << "revoked=" << prev_id; |
| revoked_ids->push_back(prev_id); |
| } |
| } |
| return keep_accounts; |
| } |
| |
| void OAuth2TokenServiceDelegateAndroid::FireRefreshTokenAvailable( |
| const std::string& account_id) { |
| DCHECK(!account_id.empty()); |
| DVLOG(1) << "OAuth2TokenServiceDelegateAndroid::FireRefreshTokenAvailable id=" |
| << account_id; |
| std::string account_name = MapAccountIdToAccountName(account_id); |
| DCHECK(!account_name.empty()) |
| << "Cannot find account name for account id " << account_id; |
| JNIEnv* env = AttachCurrentThread(); |
| ScopedJavaLocalRef<jstring> j_account_name = |
| ConvertUTF8ToJavaString(env, account_name); |
| Java_OAuth2TokenService_notifyRefreshTokenAvailable(env, java_ref_, |
| j_account_name); |
| OAuth2TokenServiceDelegate::FireRefreshTokenAvailable(account_id); |
| } |
| |
| void OAuth2TokenServiceDelegateAndroid::FireRefreshTokenRevoked( |
| const std::string& account_id) { |
| DCHECK(!account_id.empty()); |
| DVLOG(1) << "OAuth2TokenServiceDelegateAndroid::FireRefreshTokenRevoked id=" |
| << account_id; |
| std::string account_name = MapAccountIdToAccountName(account_id); |
| if (!account_name.empty()) { |
| JNIEnv* env = AttachCurrentThread(); |
| ScopedJavaLocalRef<jstring> j_account_name = |
| ConvertUTF8ToJavaString(env, account_name); |
| Java_OAuth2TokenService_notifyRefreshTokenRevoked(env, java_ref_, |
| j_account_name); |
| } else { |
| // Current prognosis is that we have an unmigrated account which is due for |
| // deletion. Record a histogram to debug this. |
| UMA_HISTOGRAM_ENUMERATION("OAuth2Login.AccountRevoked.MigrationState", |
| account_tracker_service_->GetMigrationState(), |
| AccountTrackerService::NUM_MIGRATION_STATES); |
| bool is_email_id = account_id.find('@') != std::string::npos; |
| UMA_HISTOGRAM_BOOLEAN("OAuth2Login.AccountRevoked.IsEmailId", is_email_id); |
| } |
| OAuth2TokenServiceDelegate::FireRefreshTokenRevoked(account_id); |
| } |
| |
| void OAuth2TokenServiceDelegateAndroid::FireRefreshTokensLoaded() { |
| DVLOG(1) << "OAuth2TokenServiceDelegateAndroid::FireRefreshTokensLoaded"; |
| |
| DCHECK_EQ(LOAD_CREDENTIALS_IN_PROGRESS, load_credentials_state()); |
| set_load_credentials_state(LOAD_CREDENTIALS_FINISHED_WITH_SUCCESS); |
| |
| JNIEnv* env = AttachCurrentThread(); |
| Java_OAuth2TokenService_notifyRefreshTokensLoaded(env, java_ref_); |
| OAuth2TokenServiceDelegate::FireRefreshTokensLoaded(); |
| } |
| |
| void OAuth2TokenServiceDelegateAndroid::RevokeAllCredentials() { |
| DVLOG(1) << "OAuth2TokenServiceDelegateAndroid::RevokeAllCredentials"; |
| ScopedBatchChange batch(this); |
| std::vector<std::string> accounts_to_revoke = GetAccounts(); |
| |
| // Clear accounts in the token service before calling |
| // |FireRefreshTokenRevoked|. |
| JNIEnv* env = AttachCurrentThread(); |
| ScopedJavaLocalRef<jobjectArray> java_accounts( |
| base::android::ToJavaArrayOfStrings(env, std::vector<std::string>())); |
| Java_OAuth2TokenService_saveStoredAccounts(env, java_accounts); |
| |
| for (const std::string& account : accounts_to_revoke) |
| FireRefreshTokenRevoked(account); |
| } |
| |
| void OAuth2TokenServiceDelegateAndroid::LoadCredentials( |
| const std::string& primary_account_id) { |
| DCHECK_EQ(LOAD_CREDENTIALS_NOT_STARTED, load_credentials_state()); |
| set_load_credentials_state(LOAD_CREDENTIALS_IN_PROGRESS); |
| if (primary_account_id.empty() && |
| !base::FeatureList::IsEnabled(signin::kMiceFeature)) { |
| FireRefreshTokensLoaded(); |
| return; |
| } |
| if (fire_refresh_token_loaded_ == RT_HAS_BEEN_VALIDATED) { |
| fire_refresh_token_loaded_ = RT_LOADED; |
| FireRefreshTokensLoaded(); |
| } else if (fire_refresh_token_loaded_ == RT_LOAD_NOT_START) { |
| fire_refresh_token_loaded_ = RT_WAIT_FOR_VALIDATION; |
| } |
| } |
| |
| void OAuth2TokenServiceDelegateAndroid::ReloadAccountsFromSystem( |
| const std::string& primary_account_id) { |
| // UpdateAccountList() effectively synchronizes the accounts in the Token |
| // Service with those present at the system level. |
| UpdateAccountList(primary_account_id); |
| } |
| |
| std::string OAuth2TokenServiceDelegateAndroid::MapAccountIdToAccountName( |
| const std::string& account_id) const { |
| return account_tracker_service_->GetAccountInfo(account_id).email; |
| } |
| |
| std::string OAuth2TokenServiceDelegateAndroid::MapAccountNameToAccountId( |
| const std::string& account_name) const { |
| std::string account_id = |
| account_tracker_service_->FindAccountInfoByEmail(account_name).account_id; |
| DCHECK(!account_id.empty() || account_name.empty()) |
| << "Can't find account id, account_name=" << account_name; |
| return account_id; |
| } |
| |
| // Called from Java when fetching of an OAuth2 token is finished. The |
| // |authToken| param is only valid when |result| is true. |
| void JNI_OAuth2TokenService_OAuth2TokenFetched( |
| JNIEnv* env, |
| const JavaParamRef<jstring>& authToken, |
| jboolean isTransientError, |
| jlong nativeCallback) { |
| std::string token; |
| if (authToken) |
| token = ConvertJavaStringToUTF8(env, authToken); |
| std::unique_ptr<FetchOAuth2TokenCallback> heap_callback( |
| reinterpret_cast<FetchOAuth2TokenCallback*>(nativeCallback)); |
| GoogleServiceAuthError err = GoogleServiceAuthError::AuthErrorNone(); |
| if (!authToken) { |
| err = |
| isTransientError |
| ? GoogleServiceAuthError(GoogleServiceAuthError::CONNECTION_FAILED) |
| : GoogleServiceAuthError::FromInvalidGaiaCredentialsReason( |
| GoogleServiceAuthError::InvalidGaiaCredentialsReason:: |
| CREDENTIALS_REJECTED_BY_SERVER); |
| } |
| heap_callback->Run(err, token, base::Time()); |
| } |