1/*
2 * Copyright (C) 2009 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.accounts;
18
19import android.os.Bundle;
20import android.os.RemoteException;
21import android.os.Binder;
22import android.os.IBinder;
23import android.content.pm.PackageManager;
24import android.content.Context;
25import android.content.Intent;
26import android.Manifest;
27import android.util.Log;
28
29import java.util.Arrays;
30
31/**
32 * Abstract base class for creating AccountAuthenticators.
33 * In order to be an authenticator one must extend this class, provider implementations for the
34 * abstract methods and write a service that returns the result of {@link #getIBinder()}
35 * in the service's {@link android.app.Service#onBind(android.content.Intent)} when invoked
36 * with an intent with action {@link AccountManager#ACTION_AUTHENTICATOR_INTENT}. This service
37 * must specify the following intent filter and metadata tags in its AndroidManifest.xml file
38 * <pre>
39 *   &lt;intent-filter&gt;
40 *     &lt;action android:name="android.accounts.AccountAuthenticator" /&gt;
41 *   &lt;/intent-filter&gt;
42 *   &lt;meta-data android:name="android.accounts.AccountAuthenticator"
43 *             android:resource="@xml/authenticator" /&gt;
44 * </pre>
45 * The <code>android:resource</code> attribute must point to a resource that looks like:
46 * <pre>
47 * &lt;account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
48 *    android:accountType="typeOfAuthenticator"
49 *    android:icon="@drawable/icon"
50 *    android:smallIcon="@drawable/miniIcon"
51 *    android:label="@string/label"
52 *    android:accountPreferences="@xml/account_preferences"
53 * /&gt;
54 * </pre>
55 * Replace the icons and labels with your own resources. The <code>android:accountType</code>
56 * attribute must be a string that uniquely identifies your authenticator and will be the same
57 * string that user will use when making calls on the {@link AccountManager} and it also
58 * corresponds to {@link Account#type} for your accounts. One user of the android:icon is the
59 * "Account & Sync" settings page and one user of the android:smallIcon is the Contact Application's
60 * tab panels.
61 * <p>
62 * The preferences attribute points to a PreferenceScreen xml hierarchy that contains
63 * a list of PreferenceScreens that can be invoked to manage the authenticator. An example is:
64 * <pre>
65 * &lt;PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"&gt;
66 *    &lt;PreferenceCategory android:title="@string/title_fmt" /&gt;
67 *    &lt;PreferenceScreen
68 *         android:key="key1"
69 *         android:title="@string/key1_action"
70 *         android:summary="@string/key1_summary"&gt;
71 *         &lt;intent
72 *             android:action="key1.ACTION"
73 *             android:targetPackage="key1.package"
74 *             android:targetClass="key1.class" /&gt;
75 *     &lt;/PreferenceScreen&gt;
76 * &lt;/PreferenceScreen&gt;
77 * </pre>
78 *
79 * <p>
80 * The standard pattern for implementing any of the abstract methods is the following:
81 * <ul>
82 * <li> If the supplied arguments are enough for the authenticator to fully satisfy the request
83 * then it will do so and return a {@link Bundle} that contains the results.
84 * <li> If the authenticator needs information from the user to satisfy the request then it
85 * will create an {@link Intent} to an activity that will prompt the user for the information
86 * and then carry out the request. This intent must be returned in a Bundle as key
87 * {@link AccountManager#KEY_INTENT}.
88 * <p>
89 * The activity needs to return the final result when it is complete so the Intent should contain
90 * the {@link AccountAuthenticatorResponse} as {@link AccountManager#KEY_ACCOUNT_MANAGER_RESPONSE}.
91 * The activity must then call {@link AccountAuthenticatorResponse#onResult} or
92 * {@link AccountAuthenticatorResponse#onError} when it is complete.
93 * <li> If the authenticator cannot synchronously process the request and return a result then it
94 * may choose to return null and then use the AccountManagerResponse to send the result
95 * when it has completed the request.
96 * </ul>
97 * <p>
98 * The following descriptions of each of the abstract authenticator methods will not describe the
99 * possible asynchronous nature of the request handling and will instead just describe the input
100 * parameters and the expected result.
101 * <p>
102 * When writing an activity to satisfy these requests one must pass in the AccountManagerResponse
103 * and return the result via that response when the activity finishes (or whenever else  the
104 * activity author deems it is the correct time to respond).
105 * The {@link AccountAuthenticatorActivity} handles this, so one may wish to extend that when
106 * writing activities to handle these requests.
107 */
108public abstract class AbstractAccountAuthenticator {
109    private static final String TAG = "AccountAuthenticator";
110
111    private final Context mContext;
112
113    public AbstractAccountAuthenticator(Context context) {
114        mContext = context;
115    }
116
117    private class Transport extends IAccountAuthenticator.Stub {
118        public void addAccount(IAccountAuthenticatorResponse response, String accountType,
119                String authTokenType, String[] features, Bundle options)
120                throws RemoteException {
121            if (Log.isLoggable(TAG, Log.VERBOSE)) {
122                Log.v(TAG, "addAccount: accountType " + accountType
123                        + ", authTokenType " + authTokenType
124                        + ", features " + (features == null ? "[]" : Arrays.toString(features)));
125            }
126            checkBinderPermission();
127            try {
128                final Bundle result = AbstractAccountAuthenticator.this.addAccount(
129                    new AccountAuthenticatorResponse(response),
130                        accountType, authTokenType, features, options);
131                if (Log.isLoggable(TAG, Log.VERBOSE)) {
132                    result.keySet(); // force it to be unparcelled
133                    Log.v(TAG, "addAccount: result " + AccountManager.sanitizeResult(result));
134                }
135                if (result != null) {
136                    response.onResult(result);
137                }
138            } catch (Exception e) {
139                handleException(response, "addAccount", accountType, e);
140            }
141        }
142
143        public void confirmCredentials(IAccountAuthenticatorResponse response,
144                Account account, Bundle options) throws RemoteException {
145            if (Log.isLoggable(TAG, Log.VERBOSE)) {
146                Log.v(TAG, "confirmCredentials: " + account);
147            }
148            checkBinderPermission();
149            try {
150                final Bundle result = AbstractAccountAuthenticator.this.confirmCredentials(
151                    new AccountAuthenticatorResponse(response), account, options);
152                if (Log.isLoggable(TAG, Log.VERBOSE)) {
153                    result.keySet(); // force it to be unparcelled
154                    Log.v(TAG, "confirmCredentials: result "
155                            + AccountManager.sanitizeResult(result));
156                }
157                if (result != null) {
158                    response.onResult(result);
159                }
160            } catch (Exception e) {
161                handleException(response, "confirmCredentials", account.toString(), e);
162            }
163        }
164
165        public void getAuthTokenLabel(IAccountAuthenticatorResponse response,
166                String authTokenType)
167                throws RemoteException {
168            if (Log.isLoggable(TAG, Log.VERBOSE)) {
169                Log.v(TAG, "getAuthTokenLabel: authTokenType " + authTokenType);
170            }
171            checkBinderPermission();
172            try {
173                Bundle result = new Bundle();
174                result.putString(AccountManager.KEY_AUTH_TOKEN_LABEL,
175                        AbstractAccountAuthenticator.this.getAuthTokenLabel(authTokenType));
176                if (Log.isLoggable(TAG, Log.VERBOSE)) {
177                    result.keySet(); // force it to be unparcelled
178                    Log.v(TAG, "getAuthTokenLabel: result "
179                            + AccountManager.sanitizeResult(result));
180                }
181                response.onResult(result);
182            } catch (Exception e) {
183                handleException(response, "getAuthTokenLabel", authTokenType, e);
184            }
185        }
186
187        public void getAuthToken(IAccountAuthenticatorResponse response,
188                Account account, String authTokenType, Bundle loginOptions)
189                throws RemoteException {
190            if (Log.isLoggable(TAG, Log.VERBOSE)) {
191                Log.v(TAG, "getAuthToken: " + account
192                        + ", authTokenType " + authTokenType);
193            }
194            checkBinderPermission();
195            try {
196                final Bundle result = AbstractAccountAuthenticator.this.getAuthToken(
197                        new AccountAuthenticatorResponse(response), account,
198                        authTokenType, loginOptions);
199                if (Log.isLoggable(TAG, Log.VERBOSE)) {
200                    result.keySet(); // force it to be unparcelled
201                    Log.v(TAG, "getAuthToken: result " + AccountManager.sanitizeResult(result));
202                }
203                if (result != null) {
204                    response.onResult(result);
205                }
206            } catch (Exception e) {
207                handleException(response, "getAuthToken",
208                        account.toString() + "," + authTokenType, e);
209            }
210        }
211
212        public void updateCredentials(IAccountAuthenticatorResponse response, Account account,
213                String authTokenType, Bundle loginOptions) throws RemoteException {
214            if (Log.isLoggable(TAG, Log.VERBOSE)) {
215                Log.v(TAG, "updateCredentials: " + account
216                        + ", authTokenType " + authTokenType);
217            }
218            checkBinderPermission();
219            try {
220                final Bundle result = AbstractAccountAuthenticator.this.updateCredentials(
221                    new AccountAuthenticatorResponse(response), account,
222                        authTokenType, loginOptions);
223                if (Log.isLoggable(TAG, Log.VERBOSE)) {
224                    result.keySet(); // force it to be unparcelled
225                    Log.v(TAG, "updateCredentials: result "
226                            + AccountManager.sanitizeResult(result));
227                }
228                if (result != null) {
229                    response.onResult(result);
230                }
231            } catch (Exception e) {
232                handleException(response, "updateCredentials",
233                        account.toString() + "," + authTokenType, e);
234            }
235        }
236
237        public void editProperties(IAccountAuthenticatorResponse response,
238                String accountType) throws RemoteException {
239            checkBinderPermission();
240            try {
241                final Bundle result = AbstractAccountAuthenticator.this.editProperties(
242                    new AccountAuthenticatorResponse(response), accountType);
243                if (result != null) {
244                    response.onResult(result);
245                }
246            } catch (Exception e) {
247                handleException(response, "editProperties", accountType, e);
248            }
249        }
250
251        public void hasFeatures(IAccountAuthenticatorResponse response,
252                Account account, String[] features) throws RemoteException {
253            checkBinderPermission();
254            try {
255                final Bundle result = AbstractAccountAuthenticator.this.hasFeatures(
256                    new AccountAuthenticatorResponse(response), account, features);
257                if (result != null) {
258                    response.onResult(result);
259                }
260            } catch (Exception e) {
261                handleException(response, "hasFeatures", account.toString(), e);
262            }
263        }
264
265        public void getAccountRemovalAllowed(IAccountAuthenticatorResponse response,
266                Account account) throws RemoteException {
267            checkBinderPermission();
268            try {
269                final Bundle result = AbstractAccountAuthenticator.this.getAccountRemovalAllowed(
270                    new AccountAuthenticatorResponse(response), account);
271                if (result != null) {
272                    response.onResult(result);
273                }
274            } catch (Exception e) {
275                handleException(response, "getAccountRemovalAllowed", account.toString(), e);
276            }
277        }
278
279        public void getAccountCredentialsForCloning(IAccountAuthenticatorResponse response,
280                Account account) throws RemoteException {
281            checkBinderPermission();
282            try {
283                final Bundle result =
284                        AbstractAccountAuthenticator.this.getAccountCredentialsForCloning(
285                                new AccountAuthenticatorResponse(response), account);
286                if (result != null) {
287                    response.onResult(result);
288                }
289            } catch (Exception e) {
290                handleException(response, "getAccountCredentialsForCloning", account.toString(), e);
291            }
292        }
293
294        public void addAccountFromCredentials(IAccountAuthenticatorResponse response,
295                Account account,
296                Bundle accountCredentials) throws RemoteException {
297            checkBinderPermission();
298            try {
299                final Bundle result =
300                        AbstractAccountAuthenticator.this.addAccountFromCredentials(
301                                new AccountAuthenticatorResponse(response), account,
302                                accountCredentials);
303                if (result != null) {
304                    response.onResult(result);
305                }
306            } catch (Exception e) {
307                handleException(response, "addAccountFromCredentials", account.toString(), e);
308            }
309        }
310    }
311
312    private void handleException(IAccountAuthenticatorResponse response, String method,
313            String data, Exception e) throws RemoteException {
314        if (e instanceof NetworkErrorException) {
315            if (Log.isLoggable(TAG, Log.VERBOSE)) {
316                Log.v(TAG, method + "(" + data + ")", e);
317            }
318            response.onError(AccountManager.ERROR_CODE_NETWORK_ERROR, e.getMessage());
319        } else if (e instanceof UnsupportedOperationException) {
320            if (Log.isLoggable(TAG, Log.VERBOSE)) {
321                Log.v(TAG, method + "(" + data + ")", e);
322            }
323            response.onError(AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION,
324                    method + " not supported");
325        } else if (e instanceof IllegalArgumentException) {
326            if (Log.isLoggable(TAG, Log.VERBOSE)) {
327                Log.v(TAG, method + "(" + data + ")", e);
328            }
329            response.onError(AccountManager.ERROR_CODE_BAD_ARGUMENTS,
330                    method + " not supported");
331        } else {
332            Log.w(TAG, method + "(" + data + ")", e);
333            response.onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION,
334                    method + " failed");
335        }
336    }
337
338    private void checkBinderPermission() {
339        final int uid = Binder.getCallingUid();
340        final String perm = Manifest.permission.ACCOUNT_MANAGER;
341        if (mContext.checkCallingOrSelfPermission(perm) != PackageManager.PERMISSION_GRANTED) {
342            throw new SecurityException("caller uid " + uid + " lacks " + perm);
343        }
344    }
345
346    private Transport mTransport = new Transport();
347
348    /**
349     * @return the IBinder for the AccountAuthenticator
350     */
351    public final IBinder getIBinder() {
352        return mTransport.asBinder();
353    }
354
355    /**
356     * Returns a Bundle that contains the Intent of the activity that can be used to edit the
357     * properties. In order to indicate success the activity should call response.setResult()
358     * with a non-null Bundle.
359     * @param response used to set the result for the request. If the Constants.INTENT_KEY
360     *   is set in the bundle then this response field is to be used for sending future
361     *   results if and when the Intent is started.
362     * @param accountType the AccountType whose properties are to be edited.
363     * @return a Bundle containing the result or the Intent to start to continue the request.
364     *   If this is null then the request is considered to still be active and the result should
365     *   sent later using response.
366     */
367    public abstract Bundle editProperties(AccountAuthenticatorResponse response,
368            String accountType);
369
370    /**
371     * Adds an account of the specified accountType.
372     * @param response to send the result back to the AccountManager, will never be null
373     * @param accountType the type of account to add, will never be null
374     * @param authTokenType the type of auth token to retrieve after adding the account, may be null
375     * @param requiredFeatures a String array of authenticator-specific features that the added
376     * account must support, may be null
377     * @param options a Bundle of authenticator-specific options, may be null
378     * @return a Bundle result or null if the result is to be returned via the response. The result
379     * will contain either:
380     * <ul>
381     * <li> {@link AccountManager#KEY_INTENT}, or
382     * <li> {@link AccountManager#KEY_ACCOUNT_NAME} and {@link AccountManager#KEY_ACCOUNT_TYPE} of
383     * the account that was added, or
384     * <li> {@link AccountManager#KEY_ERROR_CODE} and {@link AccountManager#KEY_ERROR_MESSAGE} to
385     * indicate an error
386     * </ul>
387     * @throws NetworkErrorException if the authenticator could not honor the request due to a
388     * network error
389     */
390    public abstract Bundle addAccount(AccountAuthenticatorResponse response, String accountType,
391            String authTokenType, String[] requiredFeatures, Bundle options)
392            throws NetworkErrorException;
393
394    /**
395     * Checks that the user knows the credentials of an account.
396     * @param response to send the result back to the AccountManager, will never be null
397     * @param account the account whose credentials are to be checked, will never be null
398     * @param options a Bundle of authenticator-specific options, may be null
399     * @return a Bundle result or null if the result is to be returned via the response. The result
400     * will contain either:
401     * <ul>
402     * <li> {@link AccountManager#KEY_INTENT}, or
403     * <li> {@link AccountManager#KEY_BOOLEAN_RESULT}, true if the check succeeded, false otherwise
404     * <li> {@link AccountManager#KEY_ERROR_CODE} and {@link AccountManager#KEY_ERROR_MESSAGE} to
405     * indicate an error
406     * </ul>
407     * @throws NetworkErrorException if the authenticator could not honor the request due to a
408     * network error
409     */
410    public abstract Bundle confirmCredentials(AccountAuthenticatorResponse response,
411            Account account, Bundle options)
412            throws NetworkErrorException;
413    /**
414     * Gets the authtoken for an account.
415     * @param response to send the result back to the AccountManager, will never be null
416     * @param account the account whose credentials are to be retrieved, will never be null
417     * @param authTokenType the type of auth token to retrieve, will never be null
418     * @param options a Bundle of authenticator-specific options, may be null
419     * @return a Bundle result or null if the result is to be returned via the response. The result
420     * will contain either:
421     * <ul>
422     * <li> {@link AccountManager#KEY_INTENT}, or
423     * <li> {@link AccountManager#KEY_ACCOUNT_NAME}, {@link AccountManager#KEY_ACCOUNT_TYPE},
424     * and {@link AccountManager#KEY_AUTHTOKEN}, or
425     * <li> {@link AccountManager#KEY_ERROR_CODE} and {@link AccountManager#KEY_ERROR_MESSAGE} to
426     * indicate an error
427     * </ul>
428     * @throws NetworkErrorException if the authenticator could not honor the request due to a
429     * network error
430     */
431    public abstract Bundle getAuthToken(AccountAuthenticatorResponse response,
432            Account account, String authTokenType, Bundle options)
433            throws NetworkErrorException;
434
435    /**
436     * Ask the authenticator for a localized label for the given authTokenType.
437     * @param authTokenType the authTokenType whose label is to be returned, will never be null
438     * @return the localized label of the auth token type, may be null if the type isn't known
439     */
440    public abstract String getAuthTokenLabel(String authTokenType);
441
442    /**
443     * Update the locally stored credentials for an account.
444     * @param response to send the result back to the AccountManager, will never be null
445     * @param account the account whose credentials are to be updated, will never be null
446     * @param authTokenType the type of auth token to retrieve after updating the credentials,
447     * may be null
448     * @param options a Bundle of authenticator-specific options, may be null
449     * @return a Bundle result or null if the result is to be returned via the response. The result
450     * will contain either:
451     * <ul>
452     * <li> {@link AccountManager#KEY_INTENT}, or
453     * <li> {@link AccountManager#KEY_ACCOUNT_NAME} and {@link AccountManager#KEY_ACCOUNT_TYPE} of
454     * the account that was added, or
455     * <li> {@link AccountManager#KEY_ERROR_CODE} and {@link AccountManager#KEY_ERROR_MESSAGE} to
456     * indicate an error
457     * </ul>
458     * @throws NetworkErrorException if the authenticator could not honor the request due to a
459     * network error
460     */
461    public abstract Bundle updateCredentials(AccountAuthenticatorResponse response,
462            Account account, String authTokenType, Bundle options) throws NetworkErrorException;
463
464    /**
465     * Checks if the account supports all the specified authenticator specific features.
466     * @param response to send the result back to the AccountManager, will never be null
467     * @param account the account to check, will never be null
468     * @param features an array of features to check, will never be null
469     * @return a Bundle result or null if the result is to be returned via the response. The result
470     * will contain either:
471     * <ul>
472     * <li> {@link AccountManager#KEY_INTENT}, or
473     * <li> {@link AccountManager#KEY_BOOLEAN_RESULT}, true if the account has all the features,
474     * false otherwise
475     * <li> {@link AccountManager#KEY_ERROR_CODE} and {@link AccountManager#KEY_ERROR_MESSAGE} to
476     * indicate an error
477     * </ul>
478     * @throws NetworkErrorException if the authenticator could not honor the request due to a
479     * network error
480     */
481    public abstract Bundle hasFeatures(AccountAuthenticatorResponse response,
482            Account account, String[] features) throws NetworkErrorException;
483
484    /**
485     * Checks if the removal of this account is allowed.
486     * @param response to send the result back to the AccountManager, will never be null
487     * @param account the account to check, will never be null
488     * @return a Bundle result or null if the result is to be returned via the response. The result
489     * will contain either:
490     * <ul>
491     * <li> {@link AccountManager#KEY_INTENT}, or
492     * <li> {@link AccountManager#KEY_BOOLEAN_RESULT}, true if the removal of the account is
493     * allowed, false otherwise
494     * <li> {@link AccountManager#KEY_ERROR_CODE} and {@link AccountManager#KEY_ERROR_MESSAGE} to
495     * indicate an error
496     * </ul>
497     * @throws NetworkErrorException if the authenticator could not honor the request due to a
498     * network error
499     */
500    public Bundle getAccountRemovalAllowed(AccountAuthenticatorResponse response,
501            Account account) throws NetworkErrorException {
502        final Bundle result = new Bundle();
503        result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, true);
504        return result;
505    }
506
507    /**
508     * Returns a Bundle that contains whatever is required to clone the account on a different
509     * user. The Bundle is passed to the authenticator instance in the target user via
510     * {@link #addAccountFromCredentials(AccountAuthenticatorResponse, Account, Bundle)}.
511     * The default implementation returns null, indicating that cloning is not supported.
512     * @param response to send the result back to the AccountManager, will never be null
513     * @param account the account to clone, will never be null
514     * @return a Bundle result or null if the result is to be returned via the response.
515     * @throws NetworkErrorException
516     * @see {@link #addAccountFromCredentials(AccountAuthenticatorResponse, Account, Bundle)}
517     */
518    public Bundle getAccountCredentialsForCloning(final AccountAuthenticatorResponse response,
519            final Account account) throws NetworkErrorException {
520        new Thread(new Runnable() {
521            public void run() {
522                Bundle result = new Bundle();
523                result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false);
524                response.onResult(result);
525            }
526        }).start();
527        return null;
528    }
529
530    /**
531     * Creates an account based on credentials provided by the authenticator instance of another
532     * user on the device, who has chosen to share the account with this user.
533     * @param response to send the result back to the AccountManager, will never be null
534     * @param account the account to clone, will never be null
535     * @param accountCredentials the Bundle containing the required credentials to create the
536     * account. Contents of the Bundle are only meaningful to the authenticator. This Bundle is
537     * provided by {@link #getAccountCredentialsForCloning(AccountAuthenticatorResponse, Account)}.
538     * @return a Bundle result or null if the result is to be returned via the response.
539     * @throws NetworkErrorException
540     * @see {@link #getAccountCredentialsForCloning(AccountAuthenticatorResponse, Account)}
541     */
542    public Bundle addAccountFromCredentials(final AccountAuthenticatorResponse response,
543            Account account,
544            Bundle accountCredentials) throws NetworkErrorException {
545        new Thread(new Runnable() {
546            public void run() {
547                Bundle result = new Bundle();
548                result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false);
549                response.onResult(result);
550            }
551        }).start();
552        return null;
553    }
554}
555