summaryrefslogtreecommitdiffstats
path: root/src/com/android/email/mail/internet/AuthenticationCache.java
blob: 21508af3542292b21396b8c01568829a71c135c2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
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<Long, CacheEntry> 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<Long, CacheEntry>();
        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);
    }
}