1 package com.android.email.mail.internet; 2 3 import android.content.Context; 4 import android.text.format.DateUtils; 5 6 import com.android.email.mail.internet.OAuthAuthenticator.AuthenticationResult; 7 import com.android.emailcommon.Logging; 8 import com.android.emailcommon.mail.AuthenticationFailedException; 9 import com.android.emailcommon.mail.MessagingException; 10 import com.android.emailcommon.provider.Account; 11 import com.android.emailcommon.provider.Credential; 12 import com.android.emailcommon.provider.HostAuth; 13 import com.android.mail.utils.LogUtils; 14 15 import java.io.IOException; 16 import java.util.HashMap; 17 import java.util.Map; 18 19 public class AuthenticationCache { 20 private static AuthenticationCache sCache; 21 22 // Threshold for refreshing a token. If the token is expected to expire within this amount of 23 // time, we won't even bother attempting to use it and will simply force a refresh. 24 private static final long EXPIRATION_THRESHOLD = 5 * DateUtils.MINUTE_IN_MILLIS; 25 26 private final Map<Long, CacheEntry> mCache; 27 private final OAuthAuthenticator mAuthenticator; 28 29 private class CacheEntry { CacheEntry(long accountId, String providerId, String accessToken, String refreshToken, long expirationTime)30 CacheEntry(long accountId, String providerId, String accessToken, String refreshToken, 31 long expirationTime) { 32 mAccountId = accountId; 33 mProviderId = providerId; 34 mAccessToken = accessToken; 35 mRefreshToken = refreshToken; 36 mExpirationTime = expirationTime; 37 } 38 39 final long mAccountId; 40 String mProviderId; 41 String mAccessToken; 42 String mRefreshToken; 43 long mExpirationTime; 44 } 45 getInstance()46 public static AuthenticationCache getInstance() { 47 synchronized (AuthenticationCache.class) { 48 if (sCache == null) { 49 sCache = new AuthenticationCache(); 50 } 51 return sCache; 52 } 53 } 54 AuthenticationCache()55 private AuthenticationCache() { 56 mCache = new HashMap<Long, CacheEntry>(); 57 mAuthenticator = new OAuthAuthenticator(); 58 } 59 60 // Gets an access token for the given account. This may be whatever is currently cached, or 61 // it may query the server to get a new one if the old one is expired or nearly expired. retrieveAccessToken(Context context, Account account)62 public String retrieveAccessToken(Context context, Account account) throws 63 MessagingException, IOException { 64 // Currently, we always use the same OAuth info for both sending and receiving. 65 // If we start to allow different credential objects for sending and receiving, this 66 // will need to be updated. 67 CacheEntry entry = null; 68 synchronized (mCache) { 69 entry = getEntry(context, account); 70 } 71 synchronized (entry) { 72 final long actualExpiration = entry.mExpirationTime - EXPIRATION_THRESHOLD; 73 if (System.currentTimeMillis() > actualExpiration) { 74 // This access token is pretty close to end of life. Don't bother trying to use it, 75 // it might just time out while we're trying to sync. Go ahead and refresh it 76 // immediately. 77 refreshEntry(context, entry); 78 } 79 return entry.mAccessToken; 80 } 81 } 82 refreshAccessToken(Context context, Account account)83 public String refreshAccessToken(Context context, Account account) throws 84 MessagingException, IOException { 85 CacheEntry entry = getEntry(context, account); 86 synchronized (entry) { 87 refreshEntry(context, entry); 88 return entry.mAccessToken; 89 } 90 } 91 getEntry(Context context, Account account)92 private CacheEntry getEntry(Context context, Account account) { 93 CacheEntry entry; 94 if (account.isSaved() && !account.isTemporary()) { 95 entry = mCache.get(account.mId); 96 if (entry == null) { 97 LogUtils.d(Logging.LOG_TAG, "initializing entry from database"); 98 final HostAuth hostAuth = account.getOrCreateHostAuthRecv(context); 99 final Credential credential = hostAuth.getOrCreateCredential(context); 100 entry = new CacheEntry(account.mId, credential.mProviderId, credential.mAccessToken, 101 credential.mRefreshToken, credential.mExpiration); 102 mCache.put(account.mId, entry); 103 } 104 } else { 105 // This account is temporary, just create a temporary entry. Don't store 106 // it in the cache, it won't be findable because we don't yet have an account Id. 107 final HostAuth hostAuth = account.getOrCreateHostAuthRecv(context); 108 final Credential credential = hostAuth.getCredential(context); 109 entry = new CacheEntry(account.mId, credential.mProviderId, credential.mAccessToken, 110 credential.mRefreshToken, credential.mExpiration); 111 } 112 return entry; 113 } 114 refreshEntry(Context context, CacheEntry entry)115 private void refreshEntry(Context context, CacheEntry entry) throws 116 IOException, MessagingException { 117 LogUtils.d(Logging.LOG_TAG, "AuthenticationCache refreshEntry %d", entry.mAccountId); 118 try { 119 final AuthenticationResult result = mAuthenticator.requestRefresh(context, 120 entry.mProviderId, entry.mRefreshToken); 121 // Don't set the refresh token here, it's not returned by the refresh response, 122 // so setting it here would make it blank. 123 entry.mAccessToken = result.mAccessToken; 124 entry.mExpirationTime = result.mExpiresInSeconds * DateUtils.SECOND_IN_MILLIS + 125 System.currentTimeMillis(); 126 saveEntry(context, entry); 127 } catch (AuthenticationFailedException e) { 128 // This is fatal. Clear the tokens and rethrow the exception. 129 LogUtils.d(Logging.LOG_TAG, "authentication failed, clearning"); 130 clearEntry(context, entry); 131 throw e; 132 } catch (MessagingException e) { 133 LogUtils.d(Logging.LOG_TAG, "messaging exception"); 134 throw e; 135 } catch (IOException e) { 136 LogUtils.d(Logging.LOG_TAG, "IO exception"); 137 throw e; 138 } 139 } 140 saveEntry(Context context, CacheEntry entry)141 private void saveEntry(Context context, CacheEntry entry) { 142 LogUtils.d(Logging.LOG_TAG, "saveEntry"); 143 144 final Account account = Account.restoreAccountWithId(context, entry.mAccountId); 145 final HostAuth hostAuth = account.getOrCreateHostAuthRecv(context); 146 final Credential cred = hostAuth.getOrCreateCredential(context); 147 cred.mProviderId = entry.mProviderId; 148 cred.mAccessToken = entry.mAccessToken; 149 cred.mRefreshToken = entry.mRefreshToken; 150 cred.mExpiration = entry.mExpirationTime; 151 cred.update(context, cred.toContentValues()); 152 } 153 clearEntry(Context context, CacheEntry entry)154 private void clearEntry(Context context, CacheEntry entry) { 155 LogUtils.d(Logging.LOG_TAG, "clearEntry"); 156 entry.mAccessToken = ""; 157 entry.mRefreshToken = ""; 158 entry.mExpirationTime = 0; 159 saveEntry(context, entry); 160 mCache.remove(entry.mAccountId); 161 } 162 } 163