package com.android.email.mail.internet; import android.content.Context; import android.text.format.DateUtils; import com.android.email.mail.internet.OAuthAuthenticator.AuthenticationResult; import com.android.emailcommon.Logging; import com.android.emailcommon.mail.AuthenticationFailedException; import com.android.emailcommon.mail.MessagingException; import com.android.emailcommon.provider.Account; import com.android.emailcommon.provider.Credential; import com.android.emailcommon.provider.HostAuth; import com.android.mail.utils.LogUtils; import java.io.IOException; import java.util.HashMap; import java.util.Map; public class AuthenticationCache { private static AuthenticationCache sCache; // Threshold for refreshing a token. If the token is expected to expire within this amount of // time, we won't even bother attempting to use it and will simply force a refresh. private static final long EXPIRATION_THRESHOLD = 5 * DateUtils.MINUTE_IN_MILLIS; private final Map mCache; private final OAuthAuthenticator mAuthenticator; private class CacheEntry { CacheEntry(long accountId, String providerId, String accessToken, String refreshToken, long expirationTime) { mAccountId = accountId; mProviderId = providerId; mAccessToken = accessToken; mRefreshToken = refreshToken; mExpirationTime = expirationTime; } final long mAccountId; String mProviderId; String mAccessToken; String mRefreshToken; long mExpirationTime; } public static AuthenticationCache getInstance() { synchronized (AuthenticationCache.class) { if (sCache == null) { sCache = new AuthenticationCache(); } return sCache; } } private AuthenticationCache() { mCache = new HashMap(); mAuthenticator = new OAuthAuthenticator(); } // Gets an access token for the given account. This may be whatever is currently cached, or // it may query the server to get a new one if the old one is expired or nearly expired. public String retrieveAccessToken(Context context, Account account) throws MessagingException, IOException { // Currently, we always use the same OAuth info for both sending and receiving. // If we start to allow different credential objects for sending and receiving, this // will need to be updated. CacheEntry entry = null; synchronized (mCache) { entry = getEntry(context, account); } synchronized (entry) { final long actualExpiration = entry.mExpirationTime - EXPIRATION_THRESHOLD; if (System.currentTimeMillis() > actualExpiration) { // This access token is pretty close to end of life. Don't bother trying to use it, // it might just time out while we're trying to sync. Go ahead and refresh it // immediately. refreshEntry(context, entry); } return entry.mAccessToken; } } public String refreshAccessToken(Context context, Account account) throws MessagingException, IOException { CacheEntry entry = getEntry(context, account); synchronized (entry) { refreshEntry(context, entry); return entry.mAccessToken; } } private CacheEntry getEntry(Context context, Account account) { CacheEntry entry; if (account.isSaved() && !account.isTemporary()) { entry = mCache.get(account.mId); if (entry == null) { LogUtils.d(Logging.LOG_TAG, "initializing entry from database"); final HostAuth hostAuth = account.getOrCreateHostAuthRecv(context); final Credential credential = hostAuth.getOrCreateCredential(context); entry = new CacheEntry(account.mId, credential.mProviderId, credential.mAccessToken, credential.mRefreshToken, credential.mExpiration); mCache.put(account.mId, entry); } } else { // This account is temporary, just create a temporary entry. Don't store // it in the cache, it won't be findable because we don't yet have an account Id. final HostAuth hostAuth = account.getOrCreateHostAuthRecv(context); final Credential credential = hostAuth.getCredential(context); entry = new CacheEntry(account.mId, credential.mProviderId, credential.mAccessToken, credential.mRefreshToken, credential.mExpiration); } return entry; } private void refreshEntry(Context context, CacheEntry entry) throws IOException, MessagingException { LogUtils.d(Logging.LOG_TAG, "AuthenticationCache refreshEntry %d", entry.mAccountId); try { final AuthenticationResult result = mAuthenticator.requestRefresh(context, entry.mProviderId, entry.mRefreshToken); // Don't set the refresh token here, it's not returned by the refresh response, // so setting it here would make it blank. entry.mAccessToken = result.mAccessToken; entry.mExpirationTime = result.mExpiresInSeconds * DateUtils.SECOND_IN_MILLIS + System.currentTimeMillis(); saveEntry(context, entry); } catch (AuthenticationFailedException e) { // This is fatal. Clear the tokens and rethrow the exception. LogUtils.d(Logging.LOG_TAG, "authentication failed, clearning"); clearEntry(context, entry); throw e; } catch (MessagingException e) { LogUtils.d(Logging.LOG_TAG, "messaging exception"); throw e; } catch (IOException e) { LogUtils.d(Logging.LOG_TAG, "IO exception"); throw e; } } private void saveEntry(Context context, CacheEntry entry) { LogUtils.d(Logging.LOG_TAG, "saveEntry"); final Account account = Account.restoreAccountWithId(context, entry.mAccountId); final HostAuth hostAuth = account.getOrCreateHostAuthRecv(context); final Credential cred = hostAuth.getOrCreateCredential(context); cred.mProviderId = entry.mProviderId; cred.mAccessToken = entry.mAccessToken; cred.mRefreshToken = entry.mRefreshToken; cred.mExpiration = entry.mExpirationTime; cred.update(context, cred.toContentValues()); } private void clearEntry(Context context, CacheEntry entry) { LogUtils.d(Logging.LOG_TAG, "clearEntry"); entry.mAccessToken = ""; entry.mRefreshToken = ""; entry.mExpirationTime = 0; saveEntry(context, entry); mCache.remove(entry.mAccountId); } }