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
280    private void handleException(IAccountAuthenticatorResponse response, String method,
281            String data, Exception e) throws RemoteException {
282        if (e instanceof NetworkErrorException) {
283            if (Log.isLoggable(TAG, Log.VERBOSE)) {
284                Log.v(TAG, method + "(" + data + ")", e);
285            }
286            response.onError(AccountManager.ERROR_CODE_NETWORK_ERROR, e.getMessage());
287        } else if (e instanceof UnsupportedOperationException) {
288            if (Log.isLoggable(TAG, Log.VERBOSE)) {
289                Log.v(TAG, method + "(" + data + ")", e);
290            }
291            response.onError(AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION,
292                    method + " not supported");
293        } else if (e instanceof IllegalArgumentException) {
294            if (Log.isLoggable(TAG, Log.VERBOSE)) {
295                Log.v(TAG, method + "(" + data + ")", e);
296            }
297            response.onError(AccountManager.ERROR_CODE_BAD_ARGUMENTS,
298                    method + " not supported");
299        } else {
300            Log.w(TAG, method + "(" + data + ")", e);
301            response.onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION,
302                    method + " failed");
303        }
304    }
305
306    private void checkBinderPermission() {
307        final int uid = Binder.getCallingUid();
308        final String perm = Manifest.permission.ACCOUNT_MANAGER;
309        if (mContext.checkCallingOrSelfPermission(perm) != PackageManager.PERMISSION_GRANTED) {
310            throw new SecurityException("caller uid " + uid + " lacks " + perm);
311        }
312    }
313
314    private Transport mTransport = new Transport();
315
316    /**
317     * @return the IBinder for the AccountAuthenticator
318     */
319    public final IBinder getIBinder() {
320        return mTransport.asBinder();
321    }
322
323    /**
324     * Returns a Bundle that contains the Intent of the activity that can be used to edit the
325     * properties. In order to indicate success the activity should call response.setResult()
326     * with a non-null Bundle.
327     * @param response used to set the result for the request. If the Constants.INTENT_KEY
328     *   is set in the bundle then this response field is to be used for sending future
329     *   results if and when the Intent is started.
330     * @param accountType the AccountType whose properties are to be edited.
331     * @return a Bundle containing the result or the Intent to start to continue the request.
332     *   If this is null then the request is considered to still be active and the result should
333     *   sent later using response.
334     */
335    public abstract Bundle editProperties(AccountAuthenticatorResponse response,
336            String accountType);
337
338    /**
339     * Adds an account of the specified accountType.
340     * @param response to send the result back to the AccountManager, will never be null
341     * @param accountType the type of account to add, will never be null
342     * @param authTokenType the type of auth token to retrieve after adding the account, may be null
343     * @param requiredFeatures a String array of authenticator-specific features that the added
344     * account must support, may be null
345     * @param options a Bundle of authenticator-specific options, may be null
346     * @return a Bundle result or null if the result is to be returned via the response. The result
347     * will contain either:
348     * <ul>
349     * <li> {@link AccountManager#KEY_INTENT}, or
350     * <li> {@link AccountManager#KEY_ACCOUNT_NAME} and {@link AccountManager#KEY_ACCOUNT_TYPE} of
351     * the account that was added, or
352     * <li> {@link AccountManager#KEY_ERROR_CODE} and {@link AccountManager#KEY_ERROR_MESSAGE} to
353     * indicate an error
354     * </ul>
355     * @throws NetworkErrorException if the authenticator could not honor the request due to a
356     * network error
357     */
358    public abstract Bundle addAccount(AccountAuthenticatorResponse response, String accountType,
359            String authTokenType, String[] requiredFeatures, Bundle options)
360            throws NetworkErrorException;
361
362    /**
363     * Checks that the user knows the credentials of an account.
364     * @param response to send the result back to the AccountManager, will never be null
365     * @param account the account whose credentials are to be checked, will never be null
366     * @param options a Bundle of authenticator-specific options, may be null
367     * @return a Bundle result or null if the result is to be returned via the response. The result
368     * will contain either:
369     * <ul>
370     * <li> {@link AccountManager#KEY_INTENT}, or
371     * <li> {@link AccountManager#KEY_BOOLEAN_RESULT}, true if the check succeeded, false otherwise
372     * <li> {@link AccountManager#KEY_ERROR_CODE} and {@link AccountManager#KEY_ERROR_MESSAGE} to
373     * indicate an error
374     * </ul>
375     * @throws NetworkErrorException if the authenticator could not honor the request due to a
376     * network error
377     */
378    public abstract Bundle confirmCredentials(AccountAuthenticatorResponse response,
379            Account account, Bundle options)
380            throws NetworkErrorException;
381    /**
382     * Gets the authtoken for an account.
383     * @param response to send the result back to the AccountManager, will never be null
384     * @param account the account whose credentials are to be retrieved, will never be null
385     * @param authTokenType the type of auth token to retrieve, will never be null
386     * @param options a Bundle of authenticator-specific options, may be null
387     * @return a Bundle result or null if the result is to be returned via the response. The result
388     * will contain either:
389     * <ul>
390     * <li> {@link AccountManager#KEY_INTENT}, or
391     * <li> {@link AccountManager#KEY_ACCOUNT_NAME}, {@link AccountManager#KEY_ACCOUNT_TYPE},
392     * and {@link AccountManager#KEY_AUTHTOKEN}, or
393     * <li> {@link AccountManager#KEY_ERROR_CODE} and {@link AccountManager#KEY_ERROR_MESSAGE} to
394     * indicate an error
395     * </ul>
396     * @throws NetworkErrorException if the authenticator could not honor the request due to a
397     * network error
398     */
399    public abstract Bundle getAuthToken(AccountAuthenticatorResponse response,
400            Account account, String authTokenType, Bundle options)
401            throws NetworkErrorException;
402
403    /**
404     * Ask the authenticator for a localized label for the given authTokenType.
405     * @param authTokenType the authTokenType whose label is to be returned, will never be null
406     * @return the localized label of the auth token type, may be null if the type isn't known
407     */
408    public abstract String getAuthTokenLabel(String authTokenType);
409
410    /**
411     * Update the locally stored credentials for an account.
412     * @param response to send the result back to the AccountManager, will never be null
413     * @param account the account whose credentials are to be updated, will never be null
414     * @param authTokenType the type of auth token to retrieve after updating the credentials,
415     * may be null
416     * @param options a Bundle of authenticator-specific options, may be null
417     * @return a Bundle result or null if the result is to be returned via the response. The result
418     * will contain either:
419     * <ul>
420     * <li> {@link AccountManager#KEY_INTENT}, or
421     * <li> {@link AccountManager#KEY_ACCOUNT_NAME} and {@link AccountManager#KEY_ACCOUNT_TYPE} of
422     * the account that was added, or
423     * <li> {@link AccountManager#KEY_ERROR_CODE} and {@link AccountManager#KEY_ERROR_MESSAGE} to
424     * indicate an error
425     * </ul>
426     * @throws NetworkErrorException if the authenticator could not honor the request due to a
427     * network error
428     */
429    public abstract Bundle updateCredentials(AccountAuthenticatorResponse response,
430            Account account, String authTokenType, Bundle options) throws NetworkErrorException;
431
432    /**
433     * Checks if the account supports all the specified authenticator specific features.
434     * @param response to send the result back to the AccountManager, will never be null
435     * @param account the account to check, will never be null
436     * @param features an array of features to check, will never be null
437     * @return a Bundle result or null if the result is to be returned via the response. The result
438     * will contain either:
439     * <ul>
440     * <li> {@link AccountManager#KEY_INTENT}, or
441     * <li> {@link AccountManager#KEY_BOOLEAN_RESULT}, true if the account has all the features,
442     * false otherwise
443     * <li> {@link AccountManager#KEY_ERROR_CODE} and {@link AccountManager#KEY_ERROR_MESSAGE} to
444     * indicate an error
445     * </ul>
446     * @throws NetworkErrorException if the authenticator could not honor the request due to a
447     * network error
448     */
449    public abstract Bundle hasFeatures(AccountAuthenticatorResponse response,
450            Account account, String[] features) throws NetworkErrorException;
451
452    /**
453     * Checks if the removal of this account is allowed.
454     * @param response to send the result back to the AccountManager, will never be null
455     * @param account the account to check, will never be null
456     * @return a Bundle result or null if the result is to be returned via the response. The result
457     * will contain either:
458     * <ul>
459     * <li> {@link AccountManager#KEY_INTENT}, or
460     * <li> {@link AccountManager#KEY_BOOLEAN_RESULT}, true if the removal of the account is
461     * allowed, false otherwise
462     * <li> {@link AccountManager#KEY_ERROR_CODE} and {@link AccountManager#KEY_ERROR_MESSAGE} to
463     * indicate an error
464     * </ul>
465     * @throws NetworkErrorException if the authenticator could not honor the request due to a
466     * network error
467     */
468    public Bundle getAccountRemovalAllowed(AccountAuthenticatorResponse response,
469            Account account) throws NetworkErrorException {
470        final Bundle result = new Bundle();
471        result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, true);
472        return result;
473    }
474}
475