Home | History | Annotate | Download | only in internet
      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 {
     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 
     46     public static AuthenticationCache getInstance() {
     47         synchronized (AuthenticationCache.class) {
     48             if (sCache == null) {
     49                 sCache = new AuthenticationCache();
     50             }
     51             return sCache;
     52         }
     53     }
     54 
     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.
     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 
     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 
     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 
    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 
    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 
    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