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