// 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.app.Activity; import android.content.Context; import android.preference.PreferenceManager; import android.util.Log; import org.chromium.base.CalledByNative; import org.chromium.base.ObserverList; import org.chromium.base.ThreadUtils; import org.chromium.base.VisibleForTesting; import org.chromium.chrome.browser.profiles.Profile; import org.chromium.sync.signin.AccountManagerHelper; import org.chromium.sync.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; import javax.annotation.Nullable; /** * Java instance for the native OAuth2TokenService. *

* This class forwards calls to request or invalidate access tokens made by native code to * AccountManagerHelper and forwards callbacks to native code. *

*/ public final class OAuth2TokenService { 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 final long mNativeProfileOAuth2TokenService; private final ObserverList mObservers; private OAuth2TokenService(long nativeOAuth2Service) { mNativeProfileOAuth2TokenService = nativeOAuth2Service; mObservers = new ObserverList(); } public static OAuth2TokenService getForProfile(Profile profile) { ThreadUtils.assertOnUiThread(); return (OAuth2TokenService) nativeGetForProfile(profile); } @CalledByNative private static OAuth2TokenService create(long nativeOAuth2Service) { ThreadUtils.assertOnUiThread(); return new OAuth2TokenService(nativeOAuth2Service); } public void addObserver(OAuth2TokenServiceObserver observer) { ThreadUtils.assertOnUiThread(); mObservers.addObserver(observer); } 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 accounts in the OS. */ @VisibleForTesting @CalledByNative public static String[] getSystemAccounts(Context context) { AccountManagerHelper accountManagerHelper = AccountManagerHelper.get(context); java.util.List accountNames = accountManagerHelper.getGoogleAccountNames(); return accountNames.toArray(new String[accountNames.size()]); } /** * Called by native to list the accounts with OAuth2 refresh tokens. * This can differ from getSystemAccounts 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) { nativeOAuth2TokenFetched(null, false, nativeCallback); return; } String oauth2Scope = OAUTH2_SCOPE_PREFIX + scope; AccountManagerHelper accountManagerHelper = AccountManagerHelper.get(context); accountManagerHelper.getAuthTokenFromForeground( null, account, oauth2Scope, new AccountManagerHelper.GetAuthTokenCallback() { @Override public void tokenAvailable(String token) { nativeOAuth2TokenFetched( token, token != null, nativeCallback); } }); } /** * Call this method to retrieve an OAuth2 access token for the given account and scope. * * @param activity the current activity. May be null. * @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, @Nullable Activity activity, Account account, String scope, AccountManagerHelper.GetAuthTokenCallback callback) { String oauth2Scope = OAUTH2_SCOPE_PREFIX + scope; AccountManagerHelper.get(context).getAuthTokenFromForeground( activity, 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 activity the current activity. May be null. * @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|. */ public static String getOAuth2AccessTokenWithTimeout( Context context, @Nullable Activity activity, Account account, String scope, long timeout, TimeUnit unit) { assert !ThreadUtils.runningOnUiThread(); final AtomicReference result = new AtomicReference(); final Semaphore semaphore = new Semaphore(0); getOAuth2AccessToken( context, activity, account, scope, new AccountManagerHelper.GetAuthTokenCallback() { @Override public void tokenAvailable(String token) { result.set(token); 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) { return AccountManagerHelper.get(context).hasAccountForName(accountName); } /** * 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); } } @CalledByNative public void validateAccounts(Context context, boolean forceNotifications) { ThreadUtils.assertOnUiThread(); String currentlySignedInAccount = ChromeSigninController.get(context).getSignedInAccountName(); nativeValidateAccounts(mNativeProfileOAuth2TokenService, currentlySignedInAccount, forceNotifications); } /** * 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. */ public void fireRefreshTokenAvailable(Account account) { ThreadUtils.assertOnUiThread(); assert account != null; nativeFireRefreshTokenAvailableFromJava(mNativeProfileOAuth2TokenService, account.name); } @CalledByNative public 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. */ public void fireRefreshTokenRevoked(Account account) { ThreadUtils.assertOnUiThread(); assert account != null; nativeFireRefreshTokenRevokedFromJava(mNativeProfileOAuth2TokenService, 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. */ public void fireRefreshTokensLoaded() { ThreadUtils.assertOnUiThread(); nativeFireRefreshTokensLoadedFromJava(mNativeProfileOAuth2TokenService); } @CalledByNative public void notifyRefreshTokensLoaded() { for (OAuth2TokenServiceObserver observer : mObservers) { observer.onRefreshTokensLoaded(); } } private static String[] getStoredAccounts(Context context) { Set accounts = PreferenceManager.getDefaultSharedPreferences(context) .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 set = new HashSet(Arrays.asList(accounts)); PreferenceManager.getDefaultSharedPreferences(context).edit(). putStringSet(STORED_ACCOUNTS_KEY, set).apply(); } private static native Object nativeGetForProfile(Profile profile); private static native void nativeOAuth2TokenFetched( String authToken, boolean result, long nativeCallback); private native void nativeValidateAccounts( long nativeAndroidProfileOAuth2TokenService, String currentlySignedInAccount, boolean forceNotifications); private native void nativeFireRefreshTokenAvailableFromJava( long nativeAndroidProfileOAuth2TokenService, String accountName); private native void nativeFireRefreshTokenRevokedFromJava( long nativeAndroidProfileOAuth2TokenService, String accountName); private native void nativeFireRefreshTokensLoadedFromJava( long nativeAndroidProfileOAuth2TokenService); }