1// Copyright 2013 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5package org.chromium.chrome.browser.signin; 6 7import android.accounts.Account; 8import android.app.Activity; 9import android.content.Context; 10import android.preference.PreferenceManager; 11import android.util.Log; 12 13import org.chromium.base.CalledByNative; 14import org.chromium.base.ObserverList; 15import org.chromium.base.ThreadUtils; 16import org.chromium.base.VisibleForTesting; 17import org.chromium.chrome.browser.profiles.Profile; 18import org.chromium.sync.signin.AccountManagerHelper; 19import org.chromium.sync.signin.ChromeSigninController; 20 21import java.util.Arrays; 22import java.util.HashSet; 23import java.util.Set; 24import java.util.concurrent.Semaphore; 25import java.util.concurrent.TimeUnit; 26import java.util.concurrent.atomic.AtomicReference; 27 28import javax.annotation.Nullable; 29 30/** 31 * Java instance for the native OAuth2TokenService. 32 * <p/> 33 * This class forwards calls to request or invalidate access tokens made by native code to 34 * AccountManagerHelper and forwards callbacks to native code. 35 * <p/> 36 */ 37public final class OAuth2TokenService { 38 39 private static final String TAG = "OAuth2TokenService"; 40 41 @VisibleForTesting 42 public static final String STORED_ACCOUNTS_KEY = "google.services.stored_accounts"; 43 44 /** 45 * Classes that want to listen for refresh token availability should 46 * implement this interface and register with {@link #addObserver}. 47 */ 48 public interface OAuth2TokenServiceObserver { 49 void onRefreshTokenAvailable(Account account); 50 void onRefreshTokenRevoked(Account account); 51 void onRefreshTokensLoaded(); 52 } 53 54 private static final String OAUTH2_SCOPE_PREFIX = "oauth2:"; 55 56 private final long mNativeProfileOAuth2TokenService; 57 private final ObserverList<OAuth2TokenServiceObserver> mObservers; 58 59 private OAuth2TokenService(long nativeOAuth2Service) { 60 mNativeProfileOAuth2TokenService = nativeOAuth2Service; 61 mObservers = new ObserverList<OAuth2TokenServiceObserver>(); 62 } 63 64 public static OAuth2TokenService getForProfile(Profile profile) { 65 ThreadUtils.assertOnUiThread(); 66 return (OAuth2TokenService) nativeGetForProfile(profile); 67 } 68 69 @CalledByNative 70 private static OAuth2TokenService create(long nativeOAuth2Service) { 71 ThreadUtils.assertOnUiThread(); 72 return new OAuth2TokenService(nativeOAuth2Service); 73 } 74 75 public void addObserver(OAuth2TokenServiceObserver observer) { 76 ThreadUtils.assertOnUiThread(); 77 mObservers.addObserver(observer); 78 } 79 80 public void removeObserver(OAuth2TokenServiceObserver observer) { 81 ThreadUtils.assertOnUiThread(); 82 mObservers.removeObserver(observer); 83 } 84 85 private static Account getAccountOrNullFromUsername(Context context, String username) { 86 if (username == null) { 87 Log.e(TAG, "Username is null"); 88 return null; 89 } 90 91 AccountManagerHelper accountManagerHelper = AccountManagerHelper.get(context); 92 Account account = accountManagerHelper.getAccountFromName(username); 93 if (account == null) { 94 Log.e(TAG, "Account not found for provided username."); 95 return null; 96 } 97 return account; 98 } 99 100 /** 101 * Called by native to list the activite accounts in the OS. 102 */ 103 @VisibleForTesting 104 @CalledByNative 105 public static String[] getSystemAccounts(Context context) { 106 AccountManagerHelper accountManagerHelper = AccountManagerHelper.get(context); 107 java.util.List<String> accountNames = accountManagerHelper.getGoogleAccountNames(); 108 return accountNames.toArray(new String[accountNames.size()]); 109 } 110 111 /** 112 * Called by native to list the accounts with OAuth2 refresh tokens. 113 * This can differ from getSystemAccounts as the user add/remove accounts 114 * from the OS. validateAccounts should be called to keep these two 115 * in sync. 116 */ 117 @CalledByNative 118 public static String[] getAccounts(Context context) { 119 return getStoredAccounts(context); 120 } 121 122 /** 123 * Called by native to retrieve OAuth2 tokens. 124 * 125 * @param username The native username (full address). 126 * @param scope The scope to get an auth token for (without Android-style 'oauth2:' prefix). 127 * @param nativeCallback The pointer to the native callback that should be run upon completion. 128 */ 129 @CalledByNative 130 public static void getOAuth2AuthToken( 131 Context context, String username, String scope, final long nativeCallback) { 132 Account account = getAccountOrNullFromUsername(context, username); 133 if (account == null) { 134 nativeOAuth2TokenFetched(null, false, nativeCallback); 135 return; 136 } 137 String oauth2Scope = OAUTH2_SCOPE_PREFIX + scope; 138 139 AccountManagerHelper accountManagerHelper = AccountManagerHelper.get(context); 140 accountManagerHelper.getAuthTokenFromForeground( 141 null, account, oauth2Scope, new AccountManagerHelper.GetAuthTokenCallback() { 142 @Override 143 public void tokenAvailable(String token) { 144 nativeOAuth2TokenFetched( 145 token, token != null, nativeCallback); 146 } 147 }); 148 } 149 150 /** 151 * Call this method to retrieve an OAuth2 access token for the given account and scope. 152 * 153 * @param activity the current activity. May be null. 154 * @param account the account to get the access token for. 155 * @param scope The scope to get an auth token for (without Android-style 'oauth2:' prefix). 156 * @param callback called on successful and unsuccessful fetching of auth token. 157 */ 158 public static void getOAuth2AccessToken(Context context, @Nullable Activity activity, 159 Account account, String scope, 160 AccountManagerHelper.GetAuthTokenCallback callback) { 161 String oauth2Scope = OAUTH2_SCOPE_PREFIX + scope; 162 AccountManagerHelper.get(context).getAuthTokenFromForeground( 163 activity, account, oauth2Scope, callback); 164 } 165 166 /** 167 * Call this method to retrieve an OAuth2 access token for the given account and scope. This 168 * method times out after the specified timeout, and will return null if that happens. 169 * 170 * Given that this is a blocking method call, this should never be called from the UI thread. 171 * 172 * @param activity the current activity. May be null. 173 * @param account the account to get the access token for. 174 * @param scope The scope to get an auth token for (without Android-style 'oauth2:' prefix). 175 * @param timeout the timeout. 176 * @param unit the unit for |timeout|. 177 */ 178 public static String getOAuth2AccessTokenWithTimeout( 179 Context context, @Nullable Activity activity, Account account, String scope, 180 long timeout, TimeUnit unit) { 181 assert !ThreadUtils.runningOnUiThread(); 182 final AtomicReference<String> result = new AtomicReference<String>(); 183 final Semaphore semaphore = new Semaphore(0); 184 getOAuth2AccessToken( 185 context, activity, account, scope, 186 new AccountManagerHelper.GetAuthTokenCallback() { 187 @Override 188 public void tokenAvailable(String token) { 189 result.set(token); 190 semaphore.release(); 191 } 192 }); 193 try { 194 if (semaphore.tryAcquire(timeout, unit)) { 195 return result.get(); 196 } else { 197 Log.d(TAG, "Failed to retrieve auth token within timeout (" + 198 timeout + " + " + unit.name() + ")"); 199 return null; 200 } 201 } catch (InterruptedException e) { 202 Log.w(TAG, "Got interrupted while waiting for auth token"); 203 return null; 204 } 205 } 206 207 /** 208 * Called by native to check wether the account has an OAuth2 refresh token. 209 */ 210 @CalledByNative 211 public static boolean hasOAuth2RefreshToken(Context context, String accountName) { 212 return AccountManagerHelper.get(context).hasAccountForName(accountName); 213 } 214 215 /** 216 * Called by native to invalidate an OAuth2 token. 217 */ 218 @CalledByNative 219 public static void invalidateOAuth2AuthToken(Context context, String accessToken) { 220 if (accessToken != null) { 221 AccountManagerHelper.get(context).invalidateAuthToken(accessToken); 222 } 223 } 224 225 @CalledByNative 226 public void validateAccounts(Context context, boolean forceNotifications) { 227 ThreadUtils.assertOnUiThread(); 228 String currentlySignedInAccount = 229 ChromeSigninController.get(context).getSignedInAccountName(); 230 nativeValidateAccounts(mNativeProfileOAuth2TokenService, currentlySignedInAccount, 231 forceNotifications); 232 } 233 234 /** 235 * Triggers a notification to all observers of the native and Java instance of the 236 * OAuth2TokenService that a refresh token is now available. This may cause observers to retry 237 * operations that require authentication. 238 */ 239 public void fireRefreshTokenAvailable(Account account) { 240 ThreadUtils.assertOnUiThread(); 241 assert account != null; 242 nativeFireRefreshTokenAvailableFromJava(mNativeProfileOAuth2TokenService, account.name); 243 } 244 245 @CalledByNative 246 public void notifyRefreshTokenAvailable(String accountName) { 247 assert accountName != null; 248 Account account = AccountManagerHelper.createAccountFromName(accountName); 249 for (OAuth2TokenServiceObserver observer : mObservers) { 250 observer.onRefreshTokenAvailable(account); 251 } 252 } 253 254 /** 255 * Triggers a notification to all observers of the native and Java instance of the 256 * OAuth2TokenService that a refresh token is now revoked. 257 */ 258 public void fireRefreshTokenRevoked(Account account) { 259 ThreadUtils.assertOnUiThread(); 260 assert account != null; 261 nativeFireRefreshTokenRevokedFromJava(mNativeProfileOAuth2TokenService, account.name); 262 } 263 264 @CalledByNative 265 public void notifyRefreshTokenRevoked(String accountName) { 266 assert accountName != null; 267 Account account = AccountManagerHelper.createAccountFromName(accountName); 268 for (OAuth2TokenServiceObserver observer : mObservers) { 269 observer.onRefreshTokenRevoked(account); 270 } 271 } 272 273 /** 274 * Triggers a notification to all observers of the native and Java instance of the 275 * OAuth2TokenService that all refresh tokens now have been loaded. 276 */ 277 public void fireRefreshTokensLoaded() { 278 ThreadUtils.assertOnUiThread(); 279 nativeFireRefreshTokensLoadedFromJava(mNativeProfileOAuth2TokenService); 280 } 281 282 @CalledByNative 283 public void notifyRefreshTokensLoaded() { 284 for (OAuth2TokenServiceObserver observer : mObservers) { 285 observer.onRefreshTokensLoaded(); 286 } 287 } 288 289 private static String[] getStoredAccounts(Context context) { 290 Set<String> accounts = 291 PreferenceManager.getDefaultSharedPreferences(context) 292 .getStringSet(STORED_ACCOUNTS_KEY, null); 293 return accounts == null ? new String[]{} : accounts.toArray(new String[accounts.size()]); 294 } 295 296 @CalledByNative 297 private static void saveStoredAccounts(Context context, String[] accounts) { 298 Set<String> set = new HashSet<String>(Arrays.asList(accounts)); 299 PreferenceManager.getDefaultSharedPreferences(context).edit(). 300 putStringSet(STORED_ACCOUNTS_KEY, set).apply(); 301 } 302 303 private static native Object nativeGetForProfile(Profile profile); 304 private static native void nativeOAuth2TokenFetched( 305 String authToken, boolean result, long nativeCallback); 306 private native void nativeValidateAccounts( 307 long nativeAndroidProfileOAuth2TokenService, 308 String currentlySignedInAccount, 309 boolean forceNotifications); 310 private native void nativeFireRefreshTokenAvailableFromJava( 311 long nativeAndroidProfileOAuth2TokenService, String accountName); 312 private native void nativeFireRefreshTokenRevokedFromJava( 313 long nativeAndroidProfileOAuth2TokenService, String accountName); 314 private native void nativeFireRefreshTokensLoadedFromJava( 315 long nativeAndroidProfileOAuth2TokenService); 316} 317