summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorCarlos Valdivia <carlosvaldivia@google.com>2015-05-22 14:11:35 -0700
committerCarlos Valdivia <carlosvaldivia@google.com>2015-06-02 09:58:18 -0700
commit91979be8804232a04da2bf36cdd857ee7da04479 (patch)
tree7782f12bc06cfb0b43cc470acc85d7eb966eee3d
parent8fa8b95c9d38183497f08efc6bdf4d2c56a4116c (diff)
downloadframeworks_base-91979be8804232a04da2bf36cdd857ee7da04479.zip
frameworks_base-91979be8804232a04da2bf36cdd857ee7da04479.tar.gz
frameworks_base-91979be8804232a04da2bf36cdd857ee7da04479.tar.bz2
System Health: Support expiring tokens
In the past android:customTokens=true authenticators were required to handle their own token caching. This is detrimental for battery when high traffic authenticators are constantly spinning up processes to start services to do file io to check their own caches. This change allows authenticator implementers to optionally let the framework do some of the work for them by providing the framework with a expiration time. The AccountManagerService will make a best effort to re-use the cached token if possible. Bug: 21530782 Change-Id: I16a7edba36a220e3891e55cf61c725c2be863323
-rw-r--r--api/current.txt1
-rw-r--r--api/system-current.txt1
-rw-r--r--core/java/android/accounts/AbstractAccountAuthenticator.java61
-rw-r--r--services/core/java/com/android/server/accounts/AccountManagerService.java203
-rw-r--r--services/core/java/com/android/server/accounts/TokenCache.java163
5 files changed, 398 insertions, 31 deletions
diff --git a/api/current.txt b/api/current.txt
index d38e75a..418cbe5 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -2716,6 +2716,7 @@ package android.accounts {
method public final android.os.IBinder getIBinder();
method public abstract android.os.Bundle hasFeatures(android.accounts.AccountAuthenticatorResponse, android.accounts.Account, java.lang.String[]) throws android.accounts.NetworkErrorException;
method public abstract android.os.Bundle updateCredentials(android.accounts.AccountAuthenticatorResponse, android.accounts.Account, java.lang.String, android.os.Bundle) throws android.accounts.NetworkErrorException;
+ field public static final java.lang.String KEY_CUSTOM_TOKEN_EXPIRY = "android.accounts.expiry";
}
public class Account implements android.os.Parcelable {
diff --git a/api/system-current.txt b/api/system-current.txt
index 44d8c8e..b4ed5e9 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -2797,6 +2797,7 @@ package android.accounts {
method public final android.os.IBinder getIBinder();
method public abstract android.os.Bundle hasFeatures(android.accounts.AccountAuthenticatorResponse, android.accounts.Account, java.lang.String[]) throws android.accounts.NetworkErrorException;
method public abstract android.os.Bundle updateCredentials(android.accounts.AccountAuthenticatorResponse, android.accounts.Account, java.lang.String, android.os.Bundle) throws android.accounts.NetworkErrorException;
+ field public static final java.lang.String KEY_CUSTOM_TOKEN_EXPIRY = "android.accounts.expiry";
}
public class Account implements android.os.Parcelable {
diff --git a/core/java/android/accounts/AbstractAccountAuthenticator.java b/core/java/android/accounts/AbstractAccountAuthenticator.java
index dbc9051..3e4a66d 100644
--- a/core/java/android/accounts/AbstractAccountAuthenticator.java
+++ b/core/java/android/accounts/AbstractAccountAuthenticator.java
@@ -108,6 +108,14 @@ import java.util.Arrays;
public abstract class AbstractAccountAuthenticator {
private static final String TAG = "AccountAuthenticator";
+ /**
+ * Bundle key used for the {@code long} expiration time (in millis from the unix epoch) of the
+ * associated auth token.
+ *
+ * @see #getAuthToken
+ */
+ public static final String KEY_CUSTOM_TOKEN_EXPIRY = "android.accounts.expiry";
+
private final Context mContext;
public AbstractAccountAuthenticator(Context context) {
@@ -115,6 +123,7 @@ public abstract class AbstractAccountAuthenticator {
}
private class Transport extends IAccountAuthenticator.Stub {
+ @Override
public void addAccount(IAccountAuthenticatorResponse response, String accountType,
String authTokenType, String[] features, Bundle options)
throws RemoteException {
@@ -140,6 +149,7 @@ public abstract class AbstractAccountAuthenticator {
}
}
+ @Override
public void confirmCredentials(IAccountAuthenticatorResponse response,
Account account, Bundle options) throws RemoteException {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
@@ -162,6 +172,7 @@ public abstract class AbstractAccountAuthenticator {
}
}
+ @Override
public void getAuthTokenLabel(IAccountAuthenticatorResponse response,
String authTokenType)
throws RemoteException {
@@ -184,6 +195,7 @@ public abstract class AbstractAccountAuthenticator {
}
}
+ @Override
public void getAuthToken(IAccountAuthenticatorResponse response,
Account account, String authTokenType, Bundle loginOptions)
throws RemoteException {
@@ -209,6 +221,7 @@ public abstract class AbstractAccountAuthenticator {
}
}
+ @Override
public void updateCredentials(IAccountAuthenticatorResponse response, Account account,
String authTokenType, Bundle loginOptions) throws RemoteException {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
@@ -234,6 +247,7 @@ public abstract class AbstractAccountAuthenticator {
}
}
+ @Override
public void editProperties(IAccountAuthenticatorResponse response,
String accountType) throws RemoteException {
checkBinderPermission();
@@ -248,6 +262,7 @@ public abstract class AbstractAccountAuthenticator {
}
}
+ @Override
public void hasFeatures(IAccountAuthenticatorResponse response,
Account account, String[] features) throws RemoteException {
checkBinderPermission();
@@ -262,6 +277,7 @@ public abstract class AbstractAccountAuthenticator {
}
}
+ @Override
public void getAccountRemovalAllowed(IAccountAuthenticatorResponse response,
Account account) throws RemoteException {
checkBinderPermission();
@@ -276,6 +292,7 @@ public abstract class AbstractAccountAuthenticator {
}
}
+ @Override
public void getAccountCredentialsForCloning(IAccountAuthenticatorResponse response,
Account account) throws RemoteException {
checkBinderPermission();
@@ -291,6 +308,7 @@ public abstract class AbstractAccountAuthenticator {
}
}
+ @Override
public void addAccountFromCredentials(IAccountAuthenticatorResponse response,
Account account,
Bundle accountCredentials) throws RemoteException {
@@ -410,21 +428,42 @@ public abstract class AbstractAccountAuthenticator {
public abstract Bundle confirmCredentials(AccountAuthenticatorResponse response,
Account account, Bundle options)
throws NetworkErrorException;
+
/**
- * Gets the authtoken for an account.
+ * Gets an authtoken for an account.
+ *
+ * If not {@code null}, the resultant {@link Bundle} will contain different sets of keys
+ * depending on whether a token was successfully issued and, if not, whether one
+ * could be issued via some {@link android.app.Activity}.
+ * <p>
+ * If a token cannot be provided without some additional activity, the Bundle should contain
+ * {@link AccountManager#KEY_INTENT} with an associated {@link Intent}. On the other hand, if
+ * there is no such activity, then a Bundle containing
+ * {@link AccountManager#KEY_ERROR_CODE} and {@link AccountManager#KEY_ERROR_MESSAGE} should be
+ * returned.
+ * <p>
+ * If a token can be successfully issued, the implementation should return the
+ * {@link AccountManager#KEY_ACCOUNT_NAME} and {@link AccountManager#KEY_ACCOUNT_TYPE} of the
+ * account associated with the token as well as the {@link AccountManager#KEY_AUTHTOKEN}. In
+ * addition {@link AbstractAccountAuthenticator} implementations that declare themselves
+ * {@code android:customTokens=true} may also provide a non-negative {@link
+ * #KEY_CUSTOM_TOKEN_EXPIRY} long value containing the expiration timestamp of the expiration
+ * time (in millis since the unix epoch).
+ * <p>
+ * Implementers should assume that tokens will be cached on the basis of account and
+ * authTokenType. The system may ignore the contents of the supplied options Bundle when
+ * determining to re-use a cached token. Furthermore, implementers should assume a supplied
+ * expiration time will be treated as non-binding advice.
+ * <p>
+ * Finally, note that for android:customTokens=false authenticators, tokens are cached
+ * indefinitely until some client calls {@link
+ * AccountManager#invalidateAuthToken(String,String)}.
+ *
* @param response to send the result back to the AccountManager, will never be null
* @param account the account whose credentials are to be retrieved, will never be null
* @param authTokenType the type of auth token to retrieve, will never be null
* @param options a Bundle of authenticator-specific options, may be null
- * @return a Bundle result or null if the result is to be returned via the response. The result
- * will contain either:
- * <ul>
- * <li> {@link AccountManager#KEY_INTENT}, or
- * <li> {@link AccountManager#KEY_ACCOUNT_NAME}, {@link AccountManager#KEY_ACCOUNT_TYPE},
- * and {@link AccountManager#KEY_AUTHTOKEN}, or
- * <li> {@link AccountManager#KEY_ERROR_CODE} and {@link AccountManager#KEY_ERROR_MESSAGE} to
- * indicate an error
- * </ul>
+ * @return a Bundle result or null if the result is to be returned via the response.
* @throws NetworkErrorException if the authenticator could not honor the request due to a
* network error
*/
@@ -518,6 +557,7 @@ public abstract class AbstractAccountAuthenticator {
public Bundle getAccountCredentialsForCloning(final AccountAuthenticatorResponse response,
final Account account) throws NetworkErrorException {
new Thread(new Runnable() {
+ @Override
public void run() {
Bundle result = new Bundle();
result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false);
@@ -543,6 +583,7 @@ public abstract class AbstractAccountAuthenticator {
Account account,
Bundle accountCredentials) throws NetworkErrorException {
new Thread(new Runnable() {
+ @Override
public void run() {
Bundle result = new Bundle();
result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false);
diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java
index d214a20..3315c89 100644
--- a/services/core/java/com/android/server/accounts/AccountManagerService.java
+++ b/services/core/java/com/android/server/accounts/AccountManagerService.java
@@ -17,6 +17,7 @@
package com.android.server.accounts;
import android.Manifest;
+import android.accounts.AbstractAccountAuthenticator;
import android.accounts.Account;
import android.accounts.AccountAndUser;
import android.accounts.AccountAuthenticatorResponse;
@@ -49,6 +50,7 @@ import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.RegisteredServicesCache;
import android.content.pm.RegisteredServicesCacheListener;
import android.content.pm.ResolveInfo;
+import android.content.pm.Signature;
import android.content.pm.UserInfo;
import android.database.Cursor;
import android.database.DatabaseUtils;
@@ -84,6 +86,11 @@ import com.google.android.collect.Sets;
import java.io.File;
import java.io.FileDescriptor;
import java.io.PrintWriter;
+import java.lang.ref.WeakReference;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
@@ -93,6 +100,7 @@ import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
+import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
@@ -166,6 +174,10 @@ public class AccountManagerService
private static final String[] ACCOUNT_TYPE_COUNT_PROJECTION =
new String[] { ACCOUNTS_TYPE, ACCOUNTS_TYPE_COUNT};
private static final Intent ACCOUNTS_CHANGED_INTENT;
+ static {
+ ACCOUNTS_CHANGED_INTENT = new Intent(AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION);
+ ACCOUNTS_CHANGED_INTENT.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+ }
private static final String COUNT_OF_MATCHING_GRANTS = ""
+ "SELECT COUNT(*) FROM " + TABLE_GRANTS + ", " + TABLE_ACCOUNTS
@@ -177,6 +189,7 @@ public class AccountManagerService
private static final String SELECTION_AUTHTOKENS_BY_ACCOUNT =
AUTHTOKENS_ACCOUNTS_ID + "=(select _id FROM accounts WHERE name=? AND type=?)";
+
private static final String[] COLUMNS_AUTHTOKENS_TYPE_AND_AUTHTOKEN = {AUTHTOKENS_TYPE,
AUTHTOKENS_AUTHTOKEN};
@@ -205,6 +218,10 @@ public class AccountManagerService
/** protected by the {@link #cacheLock} */
private final HashMap<Account, HashMap<String, String>> authTokenCache =
new HashMap<Account, HashMap<String, String>>();
+
+ /** protected by the {@link #cacheLock} */
+ private final HashMap<Account, WeakReference<TokenCache>> accountTokenCaches = new HashMap<>();
+
/**
* protected by the {@link #cacheLock}
*
@@ -237,12 +254,6 @@ public class AccountManagerService
new AtomicReference<AccountManagerService>();
private static final Account[] EMPTY_ACCOUNT_ARRAY = new Account[]{};
- static {
- ACCOUNTS_CHANGED_INTENT = new Intent(AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION);
- ACCOUNTS_CHANGED_INTENT.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
- }
-
-
/**
* This should only be called by system code. One should only call this after the service
* has started.
@@ -425,6 +436,7 @@ public class AccountManagerService
final Account account = new Account(accountName, accountType);
accounts.userDataCache.remove(account);
accounts.authTokenCache.remove(account);
+ accounts.accountTokenCaches.remove(account);
} else {
ArrayList<String> accountNames = accountNamesByType.get(accountType);
if (accountNames == null) {
@@ -1337,9 +1349,10 @@ public class AccountManagerService
@Override
public void invalidateAuthToken(String accountType, String authToken) {
+ int callerUid = Binder.getCallingUid();
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "invalidateAuthToken: accountType " + accountType
- + ", caller's uid " + Binder.getCallingUid()
+ + ", caller's uid " + callerUid
+ ", pid " + Binder.getCallingPid());
}
if (accountType == null) throw new IllegalArgumentException("accountType is null");
@@ -1353,6 +1366,7 @@ public class AccountManagerService
db.beginTransaction();
try {
invalidateAuthTokenLocked(accounts, db, accountType, authToken);
+ invalidateCustomTokenLocked(accounts, accountType, authToken);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
@@ -1363,6 +1377,26 @@ public class AccountManagerService
}
}
+ private void invalidateCustomTokenLocked(
+ UserAccounts accounts,
+ String accountType,
+ String authToken) {
+ if (authToken == null || accountType == null) {
+ return;
+ }
+ // Also wipe out cached token in memory.
+ for (Account a : accounts.accountTokenCaches.keySet()) {
+ if (a.type.equals(accountType)) {
+ WeakReference<TokenCache> tokenCacheRef =
+ accounts.accountTokenCaches.get(a);
+ TokenCache cache = null;
+ if (tokenCacheRef != null && (cache = tokenCacheRef.get()) != null) {
+ cache.remove(authToken);
+ }
+ }
+ }
+ }
+
private void invalidateAuthTokenLocked(UserAccounts accounts, SQLiteDatabase db,
String accountType, String authToken) {
if (authToken == null || accountType == null) {
@@ -1385,14 +1419,41 @@ public class AccountManagerService
String accountName = cursor.getString(1);
String authTokenType = cursor.getString(2);
db.delete(TABLE_AUTHTOKENS, AUTHTOKENS_ID + "=" + authTokenId, null);
- writeAuthTokenIntoCacheLocked(accounts, db, new Account(accountName, accountType),
- authTokenType, null);
+ writeAuthTokenIntoCacheLocked(
+ accounts,
+ db,
+ new Account(accountName, accountType),
+ authTokenType,
+ null);
}
} finally {
cursor.close();
}
}
+ private void saveCachedToken(
+ UserAccounts accounts,
+ Account account,
+ String callerPkg,
+ byte[] callerSigDigest,
+ String tokenType,
+ String token,
+ long expiryMillis) {
+
+ if (account == null || tokenType == null || callerPkg == null || callerSigDigest == null) {
+ return;
+ }
+ cancelNotification(getSigninRequiredNotificationId(accounts, account),
+ new UserHandle(accounts.userId));
+ synchronized (accounts.cacheLock) {
+ TokenCache cache = getTokenCacheForAccountLocked(accounts, account);
+ if (cache != null) {
+ cache.put(token, tokenType, callerPkg, callerSigDigest, expiryMillis);
+ }
+ return;
+ }
+ }
+
private boolean saveAuthTokenToDatabase(UserAccounts accounts, Account account, String type,
String authToken) {
if (account == null || type == null) {
@@ -1510,6 +1571,7 @@ public class AccountManagerService
db.update(TABLE_ACCOUNTS, values, ACCOUNTS_ID + "=?", argsAccountId);
db.delete(TABLE_AUTHTOKENS, AUTHTOKENS_ACCOUNTS_ID + "=?", argsAccountId);
accounts.authTokenCache.remove(account);
+ accounts.accountTokenCaches.remove(account);
db.setTransactionSuccessful();
String action = (password == null || password.length() == 0) ?
@@ -1673,9 +1735,14 @@ public class AccountManagerService
}
@Override
- public void getAuthToken(IAccountManagerResponse response, final Account account,
- final String authTokenType, final boolean notifyOnAuthFailure,
- final boolean expectActivityLaunch, Bundle loginOptionsIn) {
+ public void getAuthToken(
+ IAccountManagerResponse response,
+ final Account account,
+ final String authTokenType,
+ final boolean notifyOnAuthFailure,
+ final boolean expectActivityLaunch,
+ final Bundle loginOptions) {
+
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "getAuthToken: " + account
+ ", response " + response
@@ -1707,19 +1774,33 @@ public class AccountManagerService
final RegisteredServicesCache.ServiceInfo<AuthenticatorDescription> authenticatorInfo;
authenticatorInfo = mAuthenticatorCache.getServiceInfo(
AuthenticatorDescription.newKey(account.type), accounts.userId);
+
final boolean customTokens =
- authenticatorInfo != null && authenticatorInfo.type.customTokens;
+ authenticatorInfo != null && authenticatorInfo.type.customTokens;
// skip the check if customTokens
final int callerUid = Binder.getCallingUid();
final boolean permissionGranted = customTokens ||
permissionIsGranted(account, authTokenType, callerUid);
- final Bundle loginOptions = (loginOptionsIn == null) ? new Bundle() :
- loginOptionsIn;
+ // Get the calling package. We will use it for the purpose of caching.
+ final String callerPkg = loginOptions.getString(AccountManager.KEY_ANDROID_PACKAGE_NAME);
+ List<String> callerOwnedPackageNames = Arrays.asList(mPackageManager.getPackagesForUid(callerUid));
+ if (callerPkg == null || !callerOwnedPackageNames.contains(callerPkg)) {
+ String msg = String.format(
+ "Uid %s is attempting to illegally masquerade as package %s!",
+ callerUid,
+ callerPkg);
+ throw new SecurityException(msg);
+ }
+
// let authenticator know the identity of the caller
loginOptions.putInt(AccountManager.KEY_CALLER_UID, callerUid);
loginOptions.putInt(AccountManager.KEY_CALLER_PID, Binder.getCallingPid());
+
+ // Distill the caller's package signatures into a single digest.
+ final byte[] callerPkgSigDigest = calculatePackageSignatureDigest(callerPkg);
+
if (notifyOnAuthFailure) {
loginOptions.putBoolean(AccountManager.KEY_NOTIFY_ON_FAILURE, true);
}
@@ -1740,6 +1821,28 @@ public class AccountManagerService
}
}
+ if (customTokens) {
+ /*
+ * Look up tokens in the new cache only if the loginOptions don't have parameters
+ * outside of those expected to be injected by the AccountManager, e.g.
+ * ANDORID_PACKAGE_NAME.
+ */
+ String token = readCachedTokenInternal(
+ accounts,
+ account,
+ authTokenType,
+ callerPkg,
+ callerPkgSigDigest);
+ if (token != null) {
+ Bundle result = new Bundle();
+ result.putString(AccountManager.KEY_AUTHTOKEN, token);
+ result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
+ result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type);
+ onResult(response, result);
+ return;
+ }
+ }
+
new Session(accounts, response, account.type, expectActivityLaunch,
false /* stripAuthTokenFromResult */, account.name,
false /* authDetailsRequired */) {
@@ -1786,9 +1889,26 @@ public class AccountManagerService
"the type and name should not be empty");
return;
}
+ Account resultAccount = new Account(name, type);
if (!customTokens) {
- saveAuthTokenToDatabase(mAccounts, new Account(name, type),
- authTokenType, authToken);
+ saveAuthTokenToDatabase(
+ mAccounts,
+ resultAccount,
+ authTokenType,
+ authToken);
+ }
+ long expiryMillis = result.getLong(
+ AbstractAccountAuthenticator.KEY_CUSTOM_TOKEN_EXPIRY, 0L);
+ if (customTokens
+ && expiryMillis > System.currentTimeMillis()) {
+ saveCachedToken(
+ mAccounts,
+ account,
+ callerPkg,
+ callerPkgSigDigest,
+ authTokenType,
+ authToken,
+ expiryMillis);
}
}
@@ -1807,6 +1927,25 @@ public class AccountManagerService
}
}
+ private byte[] calculatePackageSignatureDigest(String callerPkg) {
+ MessageDigest digester;
+ try {
+ digester = MessageDigest.getInstance("SHA-256");
+ PackageInfo pkgInfo = mPackageManager.getPackageInfo(
+ callerPkg, PackageManager.GET_SIGNATURES);
+ for (Signature sig : pkgInfo.signatures) {
+ digester.update(sig.toByteArray());
+ }
+ } catch (NoSuchAlgorithmException x) {
+ Log.wtf(TAG, "SHA-256 should be available", x);
+ digester = null;
+ } catch (NameNotFoundException e) {
+ Log.w(TAG, "Could not find packageinfo for: " + callerPkg);
+ digester = null;
+ }
+ return (digester == null) ? null : digester.digest();
+ }
+
private void createNoCredentialsPermissionNotification(Account account, Intent intent,
int userId) {
int uid = intent.getIntExtra(
@@ -2745,13 +2884,13 @@ public class AccountManagerService
if (result != null) {
boolean isSuccessfulConfirmCreds = result.getBoolean(
AccountManager.KEY_BOOLEAN_RESULT, false);
- boolean isSuccessfulUpdateCreds =
+ boolean isSuccessfulUpdateCreds =
result.containsKey(AccountManager.KEY_ACCOUNT_NAME)
&& result.containsKey(AccountManager.KEY_ACCOUNT_TYPE);
- // We should only update lastAuthenticated time, if
+ // We should only update lastAuthenticated time, if
// mUpdateLastAuthenticatedTime is true and the confirmRequest
// or updateRequest was successful
- boolean needUpdate = mUpdateLastAuthenticatedTime
+ boolean needUpdate = mUpdateLastAuthenticatedTime
&& (isSuccessfulConfirmCreds || isSuccessfulUpdateCreds);
if (needUpdate || mAuthDetailsRequired) {
boolean accountPresent = isAccountPresentForCaller(mAccountName, mAccountType);
@@ -3398,7 +3537,6 @@ public class AccountManagerService
return;
}
}
-
String msg = "caller uid " + uid + " lacks any of " + TextUtils.join(",", permissions);
Log.w(TAG, " " + msg);
throw new SecurityException(msg);
@@ -3796,6 +3934,18 @@ public class AccountManagerService
}
}
+ protected String readCachedTokenInternal(
+ UserAccounts accounts,
+ Account account,
+ String tokenType,
+ String callingPackage,
+ byte[] pkgSigDigest) {
+ synchronized (accounts.cacheLock) {
+ TokenCache cache = getTokenCacheForAccountLocked(accounts, account);
+ return cache.get(tokenType, callingPackage, pkgSigDigest);
+ }
+ }
+
protected void writeAuthTokenIntoCacheLocked(UserAccounts accounts, final SQLiteDatabase db,
Account account, String key, String value) {
HashMap<String, String> authTokensForAccount = accounts.authTokenCache.get(account);
@@ -3877,6 +4027,17 @@ public class AccountManagerService
return authTokensForAccount;
}
+ protected TokenCache getTokenCacheForAccountLocked(UserAccounts accounts, Account account) {
+ WeakReference<TokenCache> cacheRef = accounts.accountTokenCaches.get(account);
+ TokenCache cache;
+ if (cacheRef == null || (cache = cacheRef.get()) == null) {
+ cache = new TokenCache();
+ cacheRef = new WeakReference<>(cache);
+ accounts.accountTokenCaches.put(account, cacheRef);
+ }
+ return cache;
+ }
+
private Context getContextForUser(UserHandle user) {
try {
return mContext.createPackageContextAsUser(mContext.getPackageName(), 0, user);
diff --git a/services/core/java/com/android/server/accounts/TokenCache.java b/services/core/java/com/android/server/accounts/TokenCache.java
new file mode 100644
index 0000000..70a7010
--- /dev/null
+++ b/services/core/java/com/android/server/accounts/TokenCache.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.accounts;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * TokenCaches manage tokens associated with an account in memory.
+ */
+/* default */ class TokenCache {
+
+ private static class Value {
+ public final String token;
+ public final long expiryEpochMillis;
+
+ public Value(String token, long expiryEpochMillis) {
+ this.token = token;
+ this.expiryEpochMillis = expiryEpochMillis;
+ }
+ }
+
+ private static class Key {
+ public final String packageName;
+ public final String tokenType;
+ public final byte[] sigDigest;
+
+ public Key(String tokenType, String packageName, byte[] sigDigest) {
+ this.tokenType = tokenType;
+ this.packageName = packageName;
+ this.sigDigest = sigDigest;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o != null && o instanceof Key) {
+ Key cacheKey = (Key) o;
+ return Objects.equals(packageName, cacheKey.packageName)
+ && Objects.equals(tokenType, cacheKey.tokenType)
+ && Arrays.equals(sigDigest, cacheKey.sigDigest);
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return packageName.hashCode() ^ tokenType.hashCode() ^ Arrays.hashCode(sigDigest);
+ }
+ }
+
+ /**
+ * Map associating basic token lookup information with with actual tokens (and optionally their
+ * expiration times).
+ */
+ private HashMap<Key, Value> mCachedTokens = new HashMap<>();
+
+ /**
+ * Map associated tokens with an Evictor that will manage evicting the token from the cache.
+ * This reverse lookup is needed because very little information is given at token invalidation
+ * time.
+ */
+ private HashMap<String, Evictor> mTokenEvictors = new HashMap<>();
+
+ private class Evictor {
+ private final String mToken;
+ private final List<Key> mKeys;
+
+ public Evictor(String token) {
+ mKeys = new ArrayList<>();
+ mToken = token;
+ }
+
+ public void add(Key k) {
+ mKeys.add(k);
+ }
+
+ public void evict() {
+ for (Key k : mKeys) {
+ mCachedTokens.remove(k);
+ }
+ // Clear out the evictor reference.
+ mTokenEvictors.remove(mToken);
+ }
+ }
+
+ /**
+ * Caches the specified token until the specified expiryMillis. The token will be associated
+ * with the given token type, package name, and digest of signatures.
+ *
+ * @param token
+ * @param tokenType
+ * @param packageName
+ * @param sigDigest
+ * @param expiryMillis
+ */
+ public void put(
+ String token,
+ String tokenType,
+ String packageName,
+ byte[] sigDigest,
+ long expiryMillis) {
+ if (token == null || System.currentTimeMillis() > expiryMillis) {
+ return;
+ }
+ Key k = new Key(tokenType, packageName, sigDigest);
+ // Prep evictor. No token should be cached without a corresponding evictor.
+ Evictor evictor = mTokenEvictors.get(token);
+ if (evictor == null) {
+ evictor = new Evictor(token);
+ }
+ evictor.add(k);
+ mTokenEvictors.put(token, evictor);
+ // Then cache values.
+ Value v = new Value(token, expiryMillis);
+ mCachedTokens.put(k, v);
+ }
+
+ /**
+ * Evicts the specified token from the cache. This should be called as part of a token
+ * invalidation workflow.
+ */
+ public void remove(String token) {
+ Evictor evictor = mTokenEvictors.get(token);
+ if (evictor == null) {
+ // This condition is expected if the token isn't cached.
+ return;
+ }
+ evictor.evict();
+ }
+
+ /**
+ * Gets a token from the cache if possible.
+ */
+ public String get(String tokenType, String packageName, byte[] sigDigest) {
+ Key k = new Key(tokenType, packageName, sigDigest);
+ Value v = mCachedTokens.get(k);
+ long currentTime = System.currentTimeMillis();
+ if (v != null && currentTime < v.expiryEpochMillis) {
+ return v.token;
+ } else if (v != null) {
+ remove(v.token);
+ }
+ return null;
+ }
+}