// 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.
package org.chromium.chrome.browser.signin;
import android.accounts.Account;
import android.content.Context;
import android.os.StrictMode;
import android.util.Log;
import org.chromium.base.ContextUtils;
import org.chromium.base.ObserverList;
import org.chromium.base.ThreadUtils;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.components.signin.AccountManagerHelper;
import org.chromium.components.signin.ChromeSigninController;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
/**
* Java instance for the native OAuth2TokenService.
* <p/>
* This class forwards calls to request or invalidate access tokens made by native code to
* AccountManagerHelper and forwards callbacks to native code.
* <p/>
*/
public final class OAuth2TokenService
implements AccountTrackerService.OnSystemAccountsSeededListener {
private static final String TAG = "OAuth2TokenService";
@VisibleForTesting
public static final String STORED_ACCOUNTS_KEY = "google.services.stored_accounts";
/**
* Classes that want to listen for refresh token availability should
* implement this interface and register with {@link #addObserver}.
*/
public interface OAuth2TokenServiceObserver {
void onRefreshTokenAvailable(Account account);
void onRefreshTokenRevoked(Account account);
void onRefreshTokensLoaded();
}
private static final String OAUTH2_SCOPE_PREFIX = "oauth2:";
private Context mPendingValidationContext = null;
private boolean mPendingValidationForceNotifications = false;
private final long mNativeOAuth2TokenServiceDelegateAndroid;
private final ObserverList<OAuth2TokenServiceObserver> mObservers;
private OAuth2TokenService(Context context, long nativeOAuth2Service) {
mNativeOAuth2TokenServiceDelegateAndroid = nativeOAuth2Service;
mObservers = new ObserverList<OAuth2TokenServiceObserver>();
AccountTrackerService.get(context).addSystemAccountsSeededListener(this);
}
public static OAuth2TokenService getForProfile(Profile profile) {
ThreadUtils.assertOnUiThread();
return (OAuth2TokenService) nativeGetForProfile(profile);
}
@CalledByNative
private static OAuth2TokenService create(Context context, long nativeOAuth2Service) {
ThreadUtils.assertOnUiThread();
return new OAuth2TokenService(context, nativeOAuth2Service);
}
@VisibleForTesting
public void addObserver(OAuth2TokenServiceObserver observer) {
ThreadUtils.assertOnUiThread();
mObservers.addObserver(observer);
}
@VisibleForTesting
public void removeObserver(OAuth2TokenServiceObserver observer) {
ThreadUtils.assertOnUiThread();
mObservers.removeObserver(observer);
}
private static Account getAccountOrNullFromUsername(Context context, String username) {
if (username == null) {
Log.e(TAG, "Username is null");
return null;
}
AccountManagerHelper accountManagerHelper = AccountManagerHelper.get(context);
Account account = accountManagerHelper.getAccountFromName(username);
if (account == null) {
Log.e(TAG, "Account not found for provided username.");
return null;
}
return account;
}
/**
* Called by native to list the activite account names in the OS.
*/
@VisibleForTesting
@CalledByNative
public static String[] getSystemAccountNames(Context context) {
AccountManagerHelper accountManagerHelper = AccountManagerHelper.get(context);
java.util.List<String> accountNames = accountManagerHelper.getGoogleAccountNames();
return accountNames.toArray(new String[accountNames.size()]);
}
/**
* Called by native to list the accounts Id with OAuth2 refresh tokens.
* This can differ from getSystemAccountNames as the user add/remove accounts
* from the OS. validateAccounts should be called to keep these two
* in sync.
*/
@CalledByNative
public static String[] getAccounts(Context context) {
return getStoredAccounts(context);
}
/**
* Called by native to retrieve OAuth2 tokens.
*
* @param username The native username (full address).
* @param scope The scope to get an auth token for (without Android-style 'oauth2:' prefix).
* @param nativeCallback The pointer to the native callback that should be run upon completion.
*/
@CalledByNative
public static void getOAuth2AuthToken(
Context context, String username, String scope, final long nativeCallback) {
Account account = getAccountOrNullFromUsername(context, username);
if (account == null) {
ThreadUtils.postOnUiThread(new Runnable() {
@Override
public void run() {
nativeOAuth2TokenFetched(null, false, nativeCallback);
}
});
return;
}
String oauth2Scope = OAUTH2_SCOPE_PREFIX + scope;
AccountManagerHelper accountManagerHelper = AccountManagerHelper.get(context);
accountManagerHelper.getAuthToken(
account, oauth2Scope, new AccountManagerHelper.GetAuthTokenCallback() {
@Override
public void tokenAvailable(String token) {
nativeOAuth2TokenFetched(token, false, nativeCallback);
}
@Override
public void tokenUnavailable(boolean isTransientError) {
nativeOAuth2TokenFetched(null, isTransientError, nativeCallback);
}
});
}
/**
* Call this method to retrieve an OAuth2 access token for the given account and scope.
*
* @param account the account to get the access token for.
* @param scope The scope to get an auth token for (without Android-style 'oauth2:' prefix).
* @param callback called on successful and unsuccessful fetching of auth token.
*/
public static void getOAuth2AccessToken(Context context, Account account, String scope,
AccountManagerHelper.GetAuthTokenCallback callback) {
String oauth2Scope = OAUTH2_SCOPE_PREFIX + scope;
AccountManagerHelper.get(context).getAuthToken(account, oauth2Scope, callback);
}
/**
* Call this method to retrieve an OAuth2 access token for the given account and scope. This
* method times out after the specified timeout, and will return null if that happens.
*
* Given that this is a blocking method call, this should never be called from the UI thread.
*
* @param account the account to get the access token for.
* @param scope The scope to get an auth token for (without Android-style 'oauth2:' prefix).
* @param timeout the timeout.
* @param unit the unit for |timeout|.
*/
@VisibleForTesting
public static String getOAuth2AccessTokenWithTimeout(
Context context, Account account, String scope, long timeout, TimeUnit unit) {
assert !ThreadUtils.runningOnUiThread();
final AtomicReference<String> result = new AtomicReference<String>();
final Semaphore semaphore = new Semaphore(0);
getOAuth2AccessToken(
context, account, scope, new AccountManagerHelper.GetAuthTokenCallback() {
@Override
public void tokenAvailable(String token) {
result.set(token);
semaphore.release();
}
@Override
public void tokenUnavailable(boolean isTransientError) {
result.set(null);
semaphore.release();
}
});
try {
if (semaphore.tryAcquire(timeout, unit)) {
return result.get();
} else {
Log.d(TAG, "Failed to retrieve auth token within timeout ("
+ timeout + " + " + unit.name() + ")");
return null;
}
} catch (InterruptedException e) {
Log.w(TAG, "Got interrupted while waiting for auth token");
return null;
}
}
/**
* Called by native to check wether the account has an OAuth2 refresh token.
*/
@CalledByNative
public static boolean hasOAuth2RefreshToken(Context context, String accountName) {
// Temporarily allowing disk read while fixing. TODO: http://crbug.com/618096.
// This function is called in RefreshTokenIsAvailable of OAuth2TokenService which is
// expected to be called in the UI thread synchronously.
StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
try {
return AccountManagerHelper.get(context).hasAccountForName(accountName);
} finally {
StrictMode.setThreadPolicy(oldPolicy);
}
}
/**
* Called by native to invalidate an OAuth2 token.
*/
@CalledByNative
public static void invalidateOAuth2AuthToken(Context context, String accessToken) {
if (accessToken != null) {
AccountManagerHelper.get(context).invalidateAuthToken(accessToken);
}
}
/**
* Continue pending accounts validation after system accounts have been seeded into
* AccountTrackerService.
*/
@Override
public void onSystemAccountsSeedingComplete() {
if (mPendingValidationContext != null) {
validateAccountsWithSignedInAccountName(
mPendingValidationContext, mPendingValidationForceNotifications);
mPendingValidationContext = null;
mPendingValidationForceNotifications = false;
}
}
/**
* Clear pending accounts validation when system accounts in AccountTrackerService were
* refreshed.
*/
@Override
public void onSystemAccountsChanged() {
mPendingValidationContext = null;
mPendingValidationForceNotifications = false;
}
@CalledByNative
public void validateAccounts(Context context, boolean forceNotifications) {
ThreadUtils.assertOnUiThread();
if (!AccountTrackerService.get(context).checkAndSeedSystemAccounts()) {
mPendingValidationContext = context;
mPendingValidationForceNotifications = forceNotifications;
return;
}
validateAccountsWithSignedInAccountName(context, forceNotifications);
}
private void validateAccountsWithSignedInAccountName(
Context context, boolean forceNotifications) {
String currentlySignedInAccount =
ChromeSigninController.get(context).getSignedInAccountName();
if (currentlySignedInAccount != null
&& isSignedInAccountChanged(context, currentlySignedInAccount)) {
// Set currentlySignedInAccount to null for validation if signed-in account was changed
// (renamed or removed from the device), this will cause all credentials in token
// service be revoked.
// Could only get here during Chrome cold startup.
// After chrome started, SigninHelper and AccountsChangedReceiver will handle account
// change (re-signin or sign out signed-in account).
currentlySignedInAccount = null;
}
nativeValidateAccounts(mNativeOAuth2TokenServiceDelegateAndroid, currentlySignedInAccount,
forceNotifications);
}
private boolean isSignedInAccountChanged(Context context, String signedInAccountName) {
String[] accountNames = getSystemAccountNames(context);
for (String accountName : accountNames) {
if (accountName.equals(signedInAccountName)) return false;
}
return true;
}
/**
* Triggers a notification to all observers of the native and Java instance of the
* OAuth2TokenService that a refresh token is now available. This may cause observers to retry
* operations that require authentication.
*/
@VisibleForTesting
public void fireRefreshTokenAvailable(Account account) {
ThreadUtils.assertOnUiThread();
assert account != null;
nativeFireRefreshTokenAvailableFromJava(
mNativeOAuth2TokenServiceDelegateAndroid, account.name);
}
@CalledByNative
private void notifyRefreshTokenAvailable(String accountName) {
assert accountName != null;
Account account = AccountManagerHelper.createAccountFromName(accountName);
for (OAuth2TokenServiceObserver observer : mObservers) {
observer.onRefreshTokenAvailable(account);
}
}
/**
* Triggers a notification to all observers of the native and Java instance of the
* OAuth2TokenService that a refresh token is now revoked.
*/
@VisibleForTesting
public void fireRefreshTokenRevoked(Account account) {
ThreadUtils.assertOnUiThread();
assert account != null;
nativeFireRefreshTokenRevokedFromJava(
mNativeOAuth2TokenServiceDelegateAndroid, account.name);
}
@CalledByNative
public void notifyRefreshTokenRevoked(String accountName) {
assert accountName != null;
Account account = AccountManagerHelper.createAccountFromName(accountName);
for (OAuth2TokenServiceObserver observer : mObservers) {
observer.onRefreshTokenRevoked(account);
}
}
/**
* Triggers a notification to all observers of the native and Java instance of the
* OAuth2TokenService that all refresh tokens now have been loaded.
*/
@VisibleForTesting
public void fireRefreshTokensLoaded() {
ThreadUtils.assertOnUiThread();
nativeFireRefreshTokensLoadedFromJava(mNativeOAuth2TokenServiceDelegateAndroid);
}
@CalledByNative
public void notifyRefreshTokensLoaded() {
for (OAuth2TokenServiceObserver observer : mObservers) {
observer.onRefreshTokensLoaded();
}
}
private static String[] getStoredAccounts(Context context) {
Set<String> accounts =
ContextUtils.getAppSharedPreferences()
.getStringSet(STORED_ACCOUNTS_KEY, null);
return accounts == null ? new String[]{} : accounts.toArray(new String[accounts.size()]);
}
@CalledByNative
private static void saveStoredAccounts(Context context, String[] accounts) {
Set<String> set = new HashSet<String>(Arrays.asList(accounts));
ContextUtils.getAppSharedPreferences().edit()
.putStringSet(STORED_ACCOUNTS_KEY, set).apply();
}
private static native Object nativeGetForProfile(Profile profile);
private static native void nativeOAuth2TokenFetched(
String authToken, boolean isTransientError, long nativeCallback);
private native void nativeValidateAccounts(long nativeOAuth2TokenServiceDelegateAndroid,
String currentlySignedInAccount, boolean forceNotifications);
private native void nativeFireRefreshTokenAvailableFromJava(
long nativeOAuth2TokenServiceDelegateAndroid, String accountName);
private native void nativeFireRefreshTokenRevokedFromJava(
long nativeOAuth2TokenServiceDelegateAndroid, String accountName);
private native void nativeFireRefreshTokensLoadedFromJava(
long nativeOAuth2TokenServiceDelegateAndroid);
}