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 * <intent-filter> 40 * <action android:name="android.accounts.AccountAuthenticator" /> 41 * </intent-filter> 42 * <meta-data android:name="android.accounts.AccountAuthenticator" 43 * android:resource="@xml/authenticator" /> 44 * </pre> 45 * The <code>android:resource</code> attribute must point to a resource that looks like: 46 * <pre> 47 * <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 * /> 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 * <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> 66 * <PreferenceCategory android:title="@string/title_fmt" /> 67 * <PreferenceScreen 68 * android:key="key1" 69 * android:title="@string/key1_action" 70 * android:summary="@string/key1_summary"> 71 * <intent 72 * android:action="key1.ACTION" 73 * android:targetPackage="key1.package" 74 * android:targetClass="key1.class" /> 75 * </PreferenceScreen> 76 * </PreferenceScreen> 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