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