1// Copyright 2013 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5package org.chromium.chrome.browser.signin;
6
7import android.accounts.Account;
8import android.app.Activity;
9import android.content.Context;
10import android.preference.PreferenceManager;
11import android.util.Log;
12
13import org.chromium.base.CalledByNative;
14import org.chromium.base.ObserverList;
15import org.chromium.base.ThreadUtils;
16import org.chromium.base.VisibleForTesting;
17import org.chromium.chrome.browser.profiles.Profile;
18import org.chromium.sync.signin.AccountManagerHelper;
19import org.chromium.sync.signin.ChromeSigninController;
20
21import java.util.Arrays;
22import java.util.HashSet;
23import java.util.Set;
24import java.util.concurrent.Semaphore;
25import java.util.concurrent.TimeUnit;
26import java.util.concurrent.atomic.AtomicReference;
27
28import javax.annotation.Nullable;
29
30/**
31 * Java instance for the native OAuth2TokenService.
32 * <p/>
33 * This class forwards calls to request or invalidate access tokens made by native code to
34 * AccountManagerHelper and forwards callbacks to native code.
35 * <p/>
36 */
37public final class OAuth2TokenService {
38
39    private static final String TAG = "OAuth2TokenService";
40
41    @VisibleForTesting
42    public static final String STORED_ACCOUNTS_KEY = "google.services.stored_accounts";
43
44    /**
45     * Classes that want to listen for refresh token availability should
46     * implement this interface and register with {@link #addObserver}.
47     */
48    public interface OAuth2TokenServiceObserver {
49        void onRefreshTokenAvailable(Account account);
50        void onRefreshTokenRevoked(Account account);
51        void onRefreshTokensLoaded();
52    }
53
54    private static final String OAUTH2_SCOPE_PREFIX = "oauth2:";
55
56    private final long mNativeProfileOAuth2TokenService;
57    private final ObserverList<OAuth2TokenServiceObserver> mObservers;
58
59    private OAuth2TokenService(long nativeOAuth2Service) {
60        mNativeProfileOAuth2TokenService = nativeOAuth2Service;
61        mObservers = new ObserverList<OAuth2TokenServiceObserver>();
62    }
63
64    public static OAuth2TokenService getForProfile(Profile profile) {
65        ThreadUtils.assertOnUiThread();
66        return (OAuth2TokenService) nativeGetForProfile(profile);
67    }
68
69    @CalledByNative
70    private static OAuth2TokenService create(long nativeOAuth2Service) {
71        ThreadUtils.assertOnUiThread();
72        return new OAuth2TokenService(nativeOAuth2Service);
73    }
74
75    public void addObserver(OAuth2TokenServiceObserver observer) {
76        ThreadUtils.assertOnUiThread();
77        mObservers.addObserver(observer);
78    }
79
80    public void removeObserver(OAuth2TokenServiceObserver observer) {
81        ThreadUtils.assertOnUiThread();
82        mObservers.removeObserver(observer);
83    }
84
85    private static Account getAccountOrNullFromUsername(Context context, String username) {
86        if (username == null) {
87            Log.e(TAG, "Username is null");
88            return null;
89        }
90
91        AccountManagerHelper accountManagerHelper = AccountManagerHelper.get(context);
92        Account account = accountManagerHelper.getAccountFromName(username);
93        if (account == null) {
94            Log.e(TAG, "Account not found for provided username.");
95            return null;
96        }
97        return account;
98    }
99
100    /**
101     * Called by native to list the activite accounts in the OS.
102     */
103    @VisibleForTesting
104    @CalledByNative
105    public static String[] getSystemAccounts(Context context) {
106        AccountManagerHelper accountManagerHelper = AccountManagerHelper.get(context);
107        java.util.List<String> accountNames = accountManagerHelper.getGoogleAccountNames();
108        return accountNames.toArray(new String[accountNames.size()]);
109    }
110
111    /**
112     * Called by native to list the accounts with OAuth2 refresh tokens.
113     * This can differ from getSystemAccounts as the user add/remove accounts
114     * from the OS. validateAccounts should be called to keep these two
115     * in sync.
116     */
117    @CalledByNative
118    public static String[] getAccounts(Context context) {
119        return getStoredAccounts(context);
120    }
121
122    /**
123     * Called by native to retrieve OAuth2 tokens.
124     *
125     * @param username The native username (full address).
126     * @param scope The scope to get an auth token for (without Android-style 'oauth2:' prefix).
127     * @param nativeCallback The pointer to the native callback that should be run upon completion.
128     */
129    @CalledByNative
130    public static void getOAuth2AuthToken(
131            Context context, String username, String scope, final long nativeCallback) {
132        Account account = getAccountOrNullFromUsername(context, username);
133        if (account == null) {
134            nativeOAuth2TokenFetched(null, false, nativeCallback);
135            return;
136        }
137        String oauth2Scope = OAUTH2_SCOPE_PREFIX + scope;
138
139        AccountManagerHelper accountManagerHelper = AccountManagerHelper.get(context);
140        accountManagerHelper.getAuthTokenFromForeground(
141            null, account, oauth2Scope, new AccountManagerHelper.GetAuthTokenCallback() {
142                @Override
143                public void tokenAvailable(String token) {
144                    nativeOAuth2TokenFetched(
145                        token, token != null, nativeCallback);
146                }
147            });
148    }
149
150    /**
151     * Call this method to retrieve an OAuth2 access token for the given account and scope.
152     *
153     * @param activity the current activity. May be null.
154     * @param account the account to get the access token for.
155     * @param scope The scope to get an auth token for (without Android-style 'oauth2:' prefix).
156     * @param callback called on successful and unsuccessful fetching of auth token.
157     */
158    public static void getOAuth2AccessToken(Context context, @Nullable Activity activity,
159                                            Account account, String scope,
160                                            AccountManagerHelper.GetAuthTokenCallback callback) {
161        String oauth2Scope = OAUTH2_SCOPE_PREFIX + scope;
162        AccountManagerHelper.get(context).getAuthTokenFromForeground(
163                activity, account, oauth2Scope, callback);
164    }
165
166    /**
167     * Call this method to retrieve an OAuth2 access token for the given account and scope. This
168     * method times out after the specified timeout, and will return null if that happens.
169     *
170     * Given that this is a blocking method call, this should never be called from the UI thread.
171     *
172     * @param activity the current activity. May be null.
173     * @param account the account to get the access token for.
174     * @param scope The scope to get an auth token for (without Android-style 'oauth2:' prefix).
175     * @param timeout the timeout.
176     * @param unit the unit for |timeout|.
177     */
178    public static String getOAuth2AccessTokenWithTimeout(
179            Context context, @Nullable Activity activity, Account account, String scope,
180            long timeout, TimeUnit unit) {
181        assert !ThreadUtils.runningOnUiThread();
182        final AtomicReference<String> result = new AtomicReference<String>();
183        final Semaphore semaphore = new Semaphore(0);
184        getOAuth2AccessToken(
185                context, activity, account, scope,
186                new AccountManagerHelper.GetAuthTokenCallback() {
187                    @Override
188                    public void tokenAvailable(String token) {
189                        result.set(token);
190                        semaphore.release();
191                    }
192                });
193        try {
194            if (semaphore.tryAcquire(timeout, unit)) {
195                return result.get();
196            } else {
197                Log.d(TAG, "Failed to retrieve auth token within timeout (" +
198                        timeout + " + " + unit.name() + ")");
199                return null;
200            }
201        } catch (InterruptedException e) {
202            Log.w(TAG, "Got interrupted while waiting for auth token");
203            return null;
204        }
205    }
206
207    /**
208     * Called by native to check wether the account has an OAuth2 refresh token.
209     */
210    @CalledByNative
211    public static boolean hasOAuth2RefreshToken(Context context, String accountName) {
212        return AccountManagerHelper.get(context).hasAccountForName(accountName);
213    }
214
215    /**
216    * Called by native to invalidate an OAuth2 token.
217    */
218    @CalledByNative
219    public static void invalidateOAuth2AuthToken(Context context, String accessToken) {
220        if (accessToken != null) {
221            AccountManagerHelper.get(context).invalidateAuthToken(accessToken);
222        }
223    }
224
225    @CalledByNative
226    public void validateAccounts(Context context, boolean forceNotifications) {
227        ThreadUtils.assertOnUiThread();
228        String currentlySignedInAccount =
229                ChromeSigninController.get(context).getSignedInAccountName();
230        nativeValidateAccounts(mNativeProfileOAuth2TokenService, currentlySignedInAccount,
231                               forceNotifications);
232    }
233
234    /**
235     * Triggers a notification to all observers of the native and Java instance of the
236     * OAuth2TokenService that a refresh token is now available. This may cause observers to retry
237     * operations that require authentication.
238     */
239    public void fireRefreshTokenAvailable(Account account) {
240        ThreadUtils.assertOnUiThread();
241        assert account != null;
242        nativeFireRefreshTokenAvailableFromJava(mNativeProfileOAuth2TokenService, account.name);
243    }
244
245    @CalledByNative
246    public void notifyRefreshTokenAvailable(String accountName) {
247        assert accountName != null;
248        Account account = AccountManagerHelper.createAccountFromName(accountName);
249        for (OAuth2TokenServiceObserver observer : mObservers) {
250            observer.onRefreshTokenAvailable(account);
251        }
252    }
253
254    /**
255     * Triggers a notification to all observers of the native and Java instance of the
256     * OAuth2TokenService that a refresh token is now revoked.
257     */
258    public void fireRefreshTokenRevoked(Account account) {
259        ThreadUtils.assertOnUiThread();
260        assert account != null;
261        nativeFireRefreshTokenRevokedFromJava(mNativeProfileOAuth2TokenService, account.name);
262    }
263
264    @CalledByNative
265    public void notifyRefreshTokenRevoked(String accountName) {
266        assert accountName != null;
267        Account account = AccountManagerHelper.createAccountFromName(accountName);
268        for (OAuth2TokenServiceObserver observer : mObservers) {
269            observer.onRefreshTokenRevoked(account);
270        }
271    }
272
273    /**
274     * Triggers a notification to all observers of the native and Java instance of the
275     * OAuth2TokenService that all refresh tokens now have been loaded.
276     */
277    public void fireRefreshTokensLoaded() {
278        ThreadUtils.assertOnUiThread();
279        nativeFireRefreshTokensLoadedFromJava(mNativeProfileOAuth2TokenService);
280    }
281
282    @CalledByNative
283    public void notifyRefreshTokensLoaded() {
284        for (OAuth2TokenServiceObserver observer : mObservers) {
285            observer.onRefreshTokensLoaded();
286        }
287    }
288
289    private static String[] getStoredAccounts(Context context) {
290        Set<String> accounts =
291                PreferenceManager.getDefaultSharedPreferences(context)
292                        .getStringSet(STORED_ACCOUNTS_KEY, null);
293        return accounts == null ? new String[]{} : accounts.toArray(new String[accounts.size()]);
294    }
295
296    @CalledByNative
297    private static void saveStoredAccounts(Context context, String[] accounts) {
298        Set<String> set = new HashSet<String>(Arrays.asList(accounts));
299        PreferenceManager.getDefaultSharedPreferences(context).edit().
300                putStringSet(STORED_ACCOUNTS_KEY, set).apply();
301    }
302
303    private static native Object nativeGetForProfile(Profile profile);
304    private static native void nativeOAuth2TokenFetched(
305            String authToken, boolean result, long nativeCallback);
306    private native void nativeValidateAccounts(
307            long nativeAndroidProfileOAuth2TokenService,
308            String currentlySignedInAccount,
309            boolean forceNotifications);
310    private native void nativeFireRefreshTokenAvailableFromJava(
311            long nativeAndroidProfileOAuth2TokenService, String accountName);
312    private native void nativeFireRefreshTokenRevokedFromJava(
313            long nativeAndroidProfileOAuth2TokenService, String accountName);
314    private native void nativeFireRefreshTokensLoadedFromJava(
315            long nativeAndroidProfileOAuth2TokenService);
316}
317