diff options
Diffstat (limited to 'core/java/android/accounts')
29 files changed, 5025 insertions, 314 deletions
diff --git a/core/java/android/accounts/AbstractAccountAuthenticator.java b/core/java/android/accounts/AbstractAccountAuthenticator.java new file mode 100644 index 0000000..be2bdbe --- /dev/null +++ b/core/java/android/accounts/AbstractAccountAuthenticator.java @@ -0,0 +1,424 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.accounts; + +import android.os.Bundle; +import android.os.RemoteException; +import android.os.Binder; +import android.os.IBinder; +import android.content.pm.PackageManager; +import android.content.Context; +import android.content.Intent; +import android.Manifest; + +/** + * Abstract base class for creating AccountAuthenticators. + * In order to be an authenticator one must extend this class, provider implementations for the + * abstract methods and write a service that returns the result of {@link #getIBinder()} + * in the service's {@link android.app.Service#onBind(android.content.Intent)} when invoked + * with an intent with action {@link AccountManager#ACTION_AUTHENTICATOR_INTENT}. This service + * must specify the following intent filter and metadata tags in its AndroidManifest.xml file + * <pre> + * <intent-filter> + * <action android:name="android.accounts.AccountAuthenticator" /> + * </intent-filter> + * <meta-data android:name="android.accounts.AccountAuthenticator" + * android:resource="@xml/authenticator" /> + * </pre> + * The <code>android:resource</code> attribute must point to a resource that looks like: + * <pre> + * <account-authenticator xmlns:android="http://schemas.android.com/apk/res/android" + * android:accountType="typeOfAuthenticator" + * android:icon="@drawable/icon" + * android:smallIcon="@drawable/miniIcon" + * android:label="@string/label" + * android:accountPreferences="@xml/account_preferences" + * /> + * </pre> + * Replace the icons and labels with your own resources. The <code>android:accountType</code> + * attribute must be a string that uniquely identifies your authenticator and will be the same + * string that user will use when making calls on the {@link AccountManager} and it also + * corresponds to {@link Account#type} for your accounts. One user of the android:icon is the + * "Account & Sync" settings page and one user of the android:smallIcon is the Contact Application's + * tab panels. + * <p> + * The preferences attribute points to an PreferenceScreen xml hierarchy that contains + * a list of PreferenceScreens that can be invoked to manage the authenticator. An example is: + * <pre> + * <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> + * <PreferenceCategory android:title="@string/title_fmt" /> + * <PreferenceScreen + * android:key="key1" + * android:title="@string/key1_action" + * android:summary="@string/key1_summary"> + * <intent + * android:action="key1.ACTION" + * android:targetPackage="key1.package" + * android:targetClass="key1.class" /> + * </PreferenceScreen> + * </PreferenceScreen> + * </pre> + * + * <p> + * The standard pattern for implementing any of the abstract methods is the following: + * <ul> + * <li> If the supplied arguments are enough for the authenticator to fully satisfy the request + * then it will do so and return a {@link Bundle} that contains the results. + * <li> If the authenticator needs information from the user to satisfy the request then it + * will create an {@link Intent} to an activity that will prompt the user for the information + * and then carry out the request. This intent must be returned in a Bundle as key + * {@link AccountManager#KEY_INTENT}. + * <p> + * The activity needs to return the final result when it is complete so the Intent should contain + * the {@link AccountAuthenticatorResponse} as {@link AccountManager#KEY_ACCOUNT_MANAGER_RESPONSE}. + * The activity must then call {@link AccountAuthenticatorResponse#onResult} or + * {@link AccountAuthenticatorResponse#onError} when it is complete. + * <li> If the authenticator cannot synchronously process the request and return a result then it + * may choose to return null and then use the AccountManagerResponse to send the result + * when it has completed the request. + * </ul> + * <p> + * The following descriptions of each of the abstract authenticator methods will not describe the + * possible asynchronous nature of the request handling and will instead just describe the input + * parameters and the expected result. + * <p> + * When writing an activity to satisfy these requests one must pass in the AccountManagerResponse + * and return the result via that response when the activity finishes (or whenever else the + * activity author deems it is the correct time to respond). + * The {@link AccountAuthenticatorActivity} handles this, so one may wish to extend that when + * writing activities to handle these requests. + */ +public abstract class AbstractAccountAuthenticator { + private final Context mContext; + + public AbstractAccountAuthenticator(Context context) { + mContext = context; + } + + private class Transport extends IAccountAuthenticator.Stub { + public void addAccount(IAccountAuthenticatorResponse response, String accountType, + String authTokenType, String[] requiredFeatures, Bundle options) + throws RemoteException { + checkBinderPermission(); + try { + final Bundle result = AbstractAccountAuthenticator.this.addAccount( + new AccountAuthenticatorResponse(response), + accountType, authTokenType, requiredFeatures, options); + if (result != null) { + response.onResult(result); + } + } catch (NetworkErrorException e) { + response.onError(AccountManager.ERROR_CODE_NETWORK_ERROR, e.getMessage()); + } catch (UnsupportedOperationException e) { + response.onError(AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION, + "addAccount not supported"); + } + } + + public void confirmCredentials(IAccountAuthenticatorResponse response, + Account account, Bundle options) throws RemoteException { + checkBinderPermission(); + try { + final Bundle result = AbstractAccountAuthenticator.this.confirmCredentials( + new AccountAuthenticatorResponse(response), account, options); + if (result != null) { + response.onResult(result); + } + } catch (NetworkErrorException e) { + response.onError(AccountManager.ERROR_CODE_NETWORK_ERROR, e.getMessage()); + } catch (UnsupportedOperationException e) { + response.onError(AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION, + "confirmCredentials not supported"); + } + } + + public void getAuthTokenLabel(IAccountAuthenticatorResponse response, + String authTokenType) + throws RemoteException { + checkBinderPermission(); + try { + Bundle result = new Bundle(); + result.putString(AccountManager.KEY_AUTH_TOKEN_LABEL, + AbstractAccountAuthenticator.this.getAuthTokenLabel(authTokenType)); + response.onResult(result); + } catch (IllegalArgumentException e) { + response.onError(AccountManager.ERROR_CODE_BAD_ARGUMENTS, + "unknown authTokenType"); + } catch (UnsupportedOperationException e) { + response.onError(AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION, + "getAuthTokenTypeLabel not supported"); + } + } + + public void getAuthToken(IAccountAuthenticatorResponse response, + Account account, String authTokenType, Bundle loginOptions) + throws RemoteException { + checkBinderPermission(); + try { + final Bundle result = AbstractAccountAuthenticator.this.getAuthToken( + new AccountAuthenticatorResponse(response), account, + authTokenType, loginOptions); + if (result != null) { + response.onResult(result); + } + } catch (UnsupportedOperationException e) { + response.onError(AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION, + "getAuthToken not supported"); + } catch (NetworkErrorException e) { + response.onError(AccountManager.ERROR_CODE_NETWORK_ERROR, e.getMessage()); + } + } + + public void updateCredentials(IAccountAuthenticatorResponse response, Account account, + String authTokenType, Bundle loginOptions) throws RemoteException { + checkBinderPermission(); + try { + final Bundle result = AbstractAccountAuthenticator.this.updateCredentials( + new AccountAuthenticatorResponse(response), account, + authTokenType, loginOptions); + if (result != null) { + response.onResult(result); + } + } catch (NetworkErrorException e) { + response.onError(AccountManager.ERROR_CODE_NETWORK_ERROR, e.getMessage()); + } catch (UnsupportedOperationException e) { + response.onError(AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION, + "updateCredentials not supported"); + } + } + + public void editProperties(IAccountAuthenticatorResponse response, + String accountType) throws RemoteException { + checkBinderPermission(); + try { + final Bundle result = AbstractAccountAuthenticator.this.editProperties( + new AccountAuthenticatorResponse(response), accountType); + if (result != null) { + response.onResult(result); + } + } catch (UnsupportedOperationException e) { + response.onError(AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION, + "editProperties not supported"); + } + } + + public void hasFeatures(IAccountAuthenticatorResponse response, + Account account, String[] features) throws RemoteException { + checkBinderPermission(); + try { + final Bundle result = AbstractAccountAuthenticator.this.hasFeatures( + new AccountAuthenticatorResponse(response), account, features); + if (result != null) { + response.onResult(result); + } + } catch (UnsupportedOperationException e) { + response.onError(AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION, + "hasFeatures not supported"); + } catch (NetworkErrorException e) { + response.onError(AccountManager.ERROR_CODE_NETWORK_ERROR, e.getMessage()); + } + } + + public void getAccountRemovalAllowed(IAccountAuthenticatorResponse response, + Account account) throws RemoteException { + checkBinderPermission(); + try { + final Bundle result = AbstractAccountAuthenticator.this.getAccountRemovalAllowed( + new AccountAuthenticatorResponse(response), account); + if (result != null) { + response.onResult(result); + } + } catch (UnsupportedOperationException e) { + response.onError(AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION, + "getAccountRemovalAllowed not supported"); + } catch (NetworkErrorException e) { + response.onError(AccountManager.ERROR_CODE_NETWORK_ERROR, e.getMessage()); + } + } + } + + private void checkBinderPermission() { + final int uid = Binder.getCallingUid(); + final String perm = Manifest.permission.ACCOUNT_MANAGER; + if (mContext.checkCallingOrSelfPermission(perm) != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("caller uid " + uid + " lacks " + perm); + } + } + + private Transport mTransport = new Transport(); + + /** + * @return the IBinder for the AccountAuthenticator + */ + public final IBinder getIBinder() { + return mTransport.asBinder(); + } + + /** + * Returns a Bundle that contains the Intent of the activity that can be used to edit the + * properties. In order to indicate success the activity should call response.setResult() + * with a non-null Bundle. + * @param response used to set the result for the request. If the Constants.INTENT_KEY + * is set in the bundle then this response field is to be used for sending future + * results if and when the Intent is started. + * @param accountType the AccountType whose properties are to be edited. + * @return a Bundle containing the result or the Intent to start to continue the request. + * If this is null then the request is considered to still be active and the result should + * sent later using response. + */ + public abstract Bundle editProperties(AccountAuthenticatorResponse response, + String accountType); + + /** + * Adds an account of the specified accountType. + * @param response to send the result back to the AccountManager, will never be null + * @param accountType the type of account to add, will never be null + * @param authTokenType the type of auth token to retrieve after adding the account, may be null + * @param requiredFeatures a String array of authenticator-specific features that the added + * account must support, may be null + * @param options a Bundle of authenticator-specific options, may be null + * @return a Bundle result or null if the result is to be returned via the response. The result + * will contain either: + * <ul> + * <li> {@link AccountManager#KEY_INTENT}, or + * <li> {@link AccountManager#KEY_ACCOUNT_NAME} and {@link AccountManager#KEY_ACCOUNT_TYPE} of + * the account that was added, plus {@link AccountManager#KEY_AUTHTOKEN} if an authTokenType + * was supplied, or + * <li> {@link AccountManager#KEY_ERROR_CODE} and {@link AccountManager#KEY_ERROR_MESSAGE} to + * indicate an error + * </ul> + * @throws NetworkErrorException if the authenticator could not honor the request due to a + * network error + */ + public abstract Bundle addAccount(AccountAuthenticatorResponse response, String accountType, + String authTokenType, String[] requiredFeatures, Bundle options) + throws NetworkErrorException; + + /** + * Checks that the user knows the credentials of an account. + * @param response to send the result back to the AccountManager, will never be null + * @param account the account whose credentials are to be checked, will never be null + * @param options a Bundle of authenticator-specific options, may be null + * @return a Bundle result or null if the result is to be returned via the response. The result + * will contain either: + * <ul> + * <li> {@link AccountManager#KEY_INTENT}, or + * <li> {@link AccountManager#KEY_BOOLEAN_RESULT}, true if the check succeeded, false otherwise + * <li> {@link AccountManager#KEY_ERROR_CODE} and {@link AccountManager#KEY_ERROR_MESSAGE} to + * indicate an error + * </ul> + * @throws NetworkErrorException if the authenticator could not honor the request due to a + * network error + */ + public abstract Bundle confirmCredentials(AccountAuthenticatorResponse response, + Account account, Bundle options) + throws NetworkErrorException; + /** + * Gets the authtoken for an account. + * @param response to send the result back to the AccountManager, will never be null + * @param account the account whose credentials are to be retrieved, will never be null + * @param authTokenType the type of auth token to retrieve, will never be null + * @param options a Bundle of authenticator-specific options, may be null + * @return a Bundle result or null if the result is to be returned via the response. The result + * will contain either: + * <ul> + * <li> {@link AccountManager#KEY_INTENT}, or + * <li> {@link AccountManager#KEY_ACCOUNT_NAME}, {@link AccountManager#KEY_ACCOUNT_TYPE}, + * and {@link AccountManager#KEY_AUTHTOKEN}, or + * <li> {@link AccountManager#KEY_ERROR_CODE} and {@link AccountManager#KEY_ERROR_MESSAGE} to + * indicate an error + * </ul> + * @throws NetworkErrorException if the authenticator could not honor the request due to a + * network error + */ + public abstract Bundle getAuthToken(AccountAuthenticatorResponse response, + Account account, String authTokenType, Bundle options) + throws NetworkErrorException; + + /** + * Ask the authenticator for a localized label for the given authTokenType. + * @param authTokenType the authTokenType whose label is to be returned, will never be null + * @return the localized label of the auth token type, may be null if the type isn't known + */ + public abstract String getAuthTokenLabel(String authTokenType); + + /** + * Update the locally stored credentials for an account. + * @param response to send the result back to the AccountManager, will never be null + * @param account the account whose credentials are to be updated, will never be null + * @param authTokenType the type of auth token to retrieve after updating the credentials, + * may be null + * @param options a Bundle of authenticator-specific options, may be null + * @return a Bundle result or null if the result is to be returned via the response. The result + * will contain either: + * <ul> + * <li> {@link AccountManager#KEY_INTENT}, or + * <li> {@link AccountManager#KEY_ACCOUNT_NAME} and {@link AccountManager#KEY_ACCOUNT_TYPE} of + * the account that was added, plus {@link AccountManager#KEY_AUTHTOKEN} if an authTokenType + * was supplied, or + * <li> {@link AccountManager#KEY_ERROR_CODE} and {@link AccountManager#KEY_ERROR_MESSAGE} to + * indicate an error + * </ul> + * @throws NetworkErrorException if the authenticator could not honor the request due to a + * network error + */ + public abstract Bundle updateCredentials(AccountAuthenticatorResponse response, + Account account, String authTokenType, Bundle options) throws NetworkErrorException; + + /** + * Checks if the account supports all the specified authenticator specific features. + * @param response to send the result back to the AccountManager, will never be null + * @param account the account to check, will never be null + * @param features an array of features to check, will never be null + * @return a Bundle result or null if the result is to be returned via the response. The result + * will contain either: + * <ul> + * <li> {@link AccountManager#KEY_INTENT}, or + * <li> {@link AccountManager#KEY_BOOLEAN_RESULT}, true if the account has all the features, + * false otherwise + * <li> {@link AccountManager#KEY_ERROR_CODE} and {@link AccountManager#KEY_ERROR_MESSAGE} to + * indicate an error + * </ul> + * @throws NetworkErrorException if the authenticator could not honor the request due to a + * network error + */ + public abstract Bundle hasFeatures(AccountAuthenticatorResponse response, + Account account, String[] features) throws NetworkErrorException; + + /** + * Checks if the removal of this account is allowed. + * @param response to send the result back to the AccountManager, will never be null + * @param account the account to check, will never be null + * @return a Bundle result or null if the result is to be returned via the response. The result + * will contain either: + * <ul> + * <li> {@link AccountManager#KEY_INTENT}, or + * <li> {@link AccountManager#KEY_BOOLEAN_RESULT}, true if the removal of the account is + * allowed, false otherwise + * <li> {@link AccountManager#KEY_ERROR_CODE} and {@link AccountManager#KEY_ERROR_MESSAGE} to + * indicate an error + * </ul> + * @throws NetworkErrorException if the authenticator could not honor the request due to a + * network error + */ + public Bundle getAccountRemovalAllowed(AccountAuthenticatorResponse response, + Account account) throws NetworkErrorException { + final Bundle result = new Bundle(); + result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, true); + return result; + } +} diff --git a/core/java/android/accounts/Account.aidl b/core/java/android/accounts/Account.aidl new file mode 100644 index 0000000..8752d99 --- /dev/null +++ b/core/java/android/accounts/Account.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.accounts; + +parcelable Account; diff --git a/core/java/android/accounts/Account.java b/core/java/android/accounts/Account.java new file mode 100644 index 0000000..7b83a30 --- /dev/null +++ b/core/java/android/accounts/Account.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.accounts; + +import android.os.Parcelable; +import android.os.Parcel; +import android.text.TextUtils; + +/** + * Value type that represents an Account in the {@link AccountManager}. This object is + * {@link Parcelable} and also overrides {@link #equals} and {@link #hashCode}, making it + * suitable for use as the key of a {@link java.util.Map} + */ +public class Account implements Parcelable { + public final String name; + public final String type; + + public boolean equals(Object o) { + if (o == this) return true; + if (!(o instanceof Account)) return false; + final Account other = (Account)o; + return name.equals(other.name) && type.equals(other.type); + } + + public int hashCode() { + int result = 17; + result = 31 * result + name.hashCode(); + result = 31 * result + type.hashCode(); + return result; + } + + public Account(String name, String type) { + if (TextUtils.isEmpty(name)) { + throw new IllegalArgumentException("the name must not be empty: " + name); + } + if (TextUtils.isEmpty(type)) { + throw new IllegalArgumentException("the type must not be empty: " + type); + } + this.name = name; + this.type = type; + } + + public Account(Parcel in) { + this.name = in.readString(); + this.type = in.readString(); + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(name); + dest.writeString(type); + } + + public static final Creator<Account> CREATOR = new Creator<Account>() { + public Account createFromParcel(Parcel source) { + return new Account(source); + } + + public Account[] newArray(int size) { + return new Account[size]; + } + }; + + public String toString() { + return "Account {name=" + name + ", type=" + type + "}"; + } +} diff --git a/core/java/android/accounts/AccountAuthenticatorActivity.java b/core/java/android/accounts/AccountAuthenticatorActivity.java new file mode 100644 index 0000000..5cce6da --- /dev/null +++ b/core/java/android/accounts/AccountAuthenticatorActivity.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.accounts; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; + +/** + * Base class for implementing an Activity that is used to help implement an + * AbstractAccountAuthenticator. If the AbstractAccountAuthenticator needs to use an activity + * to handle the request then it can have the activity extend AccountAuthenticatorActivity. + * The AbstractAccountAuthenticator passes in the response to the intent using the following: + * <pre> + * intent.putExtra(Constants.ACCOUNT_AUTHENTICATOR_RESPONSE_KEY, response); + * </pre> + * The activity then sets the result that is to be handed to the response via + * {@link #setAccountAuthenticatorResult(android.os.Bundle)}. + * This result will be sent as the result of the request when the activity finishes. If this + * is never set or if it is set to null then error {@link AccountManager#ERROR_CODE_CANCELED} + * will be called on the response. + */ +public class AccountAuthenticatorActivity extends Activity { + private AccountAuthenticatorResponse mAccountAuthenticatorResponse = null; + private Bundle mResultBundle = null; + + /** + * Set the result that is to be sent as the result of the request that caused this + * Activity to be launched. If result is null or this method is never called then + * the request will be canceled. + * @param result this is returned as the result of the AbstractAccountAuthenticator request + */ + public final void setAccountAuthenticatorResult(Bundle result) { + mResultBundle = result; + } + + /** + * Retreives the AccountAuthenticatorResponse from either the intent of the icicle, if the + * icicle is non-zero. + * @param icicle the save instance data of this Activity, may be null + */ + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + + mAccountAuthenticatorResponse = + getIntent().getParcelableExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE); + + if (mAccountAuthenticatorResponse != null) { + mAccountAuthenticatorResponse.onRequestContinued(); + } + } + + /** + * Sends the result or a Constants.ERROR_CODE_CANCELED error if a result isn't present. + */ + public void finish() { + if (mAccountAuthenticatorResponse != null) { + // send the result bundle back if set, otherwise send an error. + if (mResultBundle != null) { + mAccountAuthenticatorResponse.onResult(mResultBundle); + } else { + mAccountAuthenticatorResponse.onError(AccountManager.ERROR_CODE_CANCELED, + "canceled"); + } + mAccountAuthenticatorResponse = null; + } + super.finish(); + } +} diff --git a/core/java/android/accounts/AccountAuthenticatorCache.java b/core/java/android/accounts/AccountAuthenticatorCache.java new file mode 100644 index 0000000..d6c76a2 --- /dev/null +++ b/core/java/android/accounts/AccountAuthenticatorCache.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.accounts; + +import android.content.pm.PackageManager; +import android.content.pm.RegisteredServicesCache; +import android.content.pm.XmlSerializerAndParser; +import android.content.res.TypedArray; +import android.content.Context; +import android.util.AttributeSet; +import android.text.TextUtils; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlSerializer; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; + +/** + * A cache of services that export the {@link IAccountAuthenticator} interface. This cache + * is built by interrogating the {@link PackageManager} and is updated as packages are added, + * removed and changed. The authenticators are referred to by their account type and + * are made available via the {@link RegisteredServicesCache#getServiceInfo} method. + * @hide + */ +/* package private */ class AccountAuthenticatorCache + extends RegisteredServicesCache<AuthenticatorDescription> { + private static final String TAG = "Account"; + private static final MySerializer sSerializer = new MySerializer(); + + public AccountAuthenticatorCache(Context context) { + super(context, AccountManager.ACTION_AUTHENTICATOR_INTENT, + AccountManager.AUTHENTICATOR_META_DATA_NAME, + AccountManager.AUTHENTICATOR_ATTRIBUTES_NAME, sSerializer); + } + + public AuthenticatorDescription parseServiceAttributes(String packageName, AttributeSet attrs) { + TypedArray sa = mContext.getResources().obtainAttributes(attrs, + com.android.internal.R.styleable.AccountAuthenticator); + try { + final String accountType = + sa.getString(com.android.internal.R.styleable.AccountAuthenticator_accountType); + final int labelId = sa.getResourceId( + com.android.internal.R.styleable.AccountAuthenticator_label, 0); + final int iconId = sa.getResourceId( + com.android.internal.R.styleable.AccountAuthenticator_icon, 0); + final int smallIconId = sa.getResourceId( + com.android.internal.R.styleable.AccountAuthenticator_smallIcon, 0); + final int prefId = sa.getResourceId( + com.android.internal.R.styleable.AccountAuthenticator_accountPreferences, 0); + if (TextUtils.isEmpty(accountType)) { + return null; + } + return new AuthenticatorDescription(accountType, packageName, labelId, iconId, + smallIconId, prefId); + } finally { + sa.recycle(); + } + } + + private static class MySerializer implements XmlSerializerAndParser<AuthenticatorDescription> { + public void writeAsXml(AuthenticatorDescription item, XmlSerializer out) + throws IOException { + out.attribute(null, "type", item.type); + } + + public AuthenticatorDescription createFromXml(XmlPullParser parser) + throws IOException, XmlPullParserException { + return AuthenticatorDescription.newKey(parser.getAttributeValue(null, "type")); + } + } +} diff --git a/core/java/android/accounts/AccountAuthenticatorResponse.java b/core/java/android/accounts/AccountAuthenticatorResponse.java new file mode 100644 index 0000000..7c09fbf --- /dev/null +++ b/core/java/android/accounts/AccountAuthenticatorResponse.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.accounts; + +import android.os.Bundle; +import android.os.Parcelable; +import android.os.Parcel; +import android.os.RemoteException; + +/** + * Object used to communicate responses back to the AccountManager + */ +public class AccountAuthenticatorResponse implements Parcelable { + private IAccountAuthenticatorResponse mAccountAuthenticatorResponse; + + /** + * @hide + */ + /* package private */ AccountAuthenticatorResponse(IAccountAuthenticatorResponse response) { + mAccountAuthenticatorResponse = response; + } + + public AccountAuthenticatorResponse(Parcel parcel) { + mAccountAuthenticatorResponse = + IAccountAuthenticatorResponse.Stub.asInterface(parcel.readStrongBinder()); + } + + public void onResult(Bundle result) { + try { + mAccountAuthenticatorResponse.onResult(result); + } catch (RemoteException e) { + // this should never happen + } + } + + public void onRequestContinued() { + try { + mAccountAuthenticatorResponse.onRequestContinued(); + } catch (RemoteException e) { + // this should never happen + } + } + + public void onError(int errorCode, String errorMessage) { + try { + mAccountAuthenticatorResponse.onError(errorCode, errorMessage); + } catch (RemoteException e) { + // this should never happen + } + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeStrongBinder(mAccountAuthenticatorResponse.asBinder()); + } + + public static final Creator<AccountAuthenticatorResponse> CREATOR = + new Creator<AccountAuthenticatorResponse>() { + public AccountAuthenticatorResponse createFromParcel(Parcel source) { + return new AccountAuthenticatorResponse(source); + } + + public AccountAuthenticatorResponse[] newArray(int size) { + return new AccountAuthenticatorResponse[size]; + } + }; +} diff --git a/core/java/android/accounts/AccountManager.java b/core/java/android/accounts/AccountManager.java new file mode 100644 index 0000000..9765496 --- /dev/null +++ b/core/java/android/accounts/AccountManager.java @@ -0,0 +1,1367 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.accounts; + +import android.app.Activity; +import android.content.Intent; +import android.content.Context; +import android.content.IntentFilter; +import android.content.BroadcastReceiver; +import android.database.SQLException; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.RemoteException; +import android.os.Parcelable; +import android.util.Log; + +import java.io.IOException; +import java.util.concurrent.Callable; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.TimeUnit; +import java.util.HashMap; +import java.util.Map; + +import com.google.android.collect.Maps; + +/** + * A class that helps with interactions with the AccountManager Service. It provides + * methods to allow for account, password, and authtoken management for all accounts on the + * device. One accesses the {@link AccountManager} by calling: + * <pre> + * AccountManager accountManager = AccountManager.get(context); + * </pre> + * + * <p> + * The AccountManager Service provides storage for the accounts known to the system, + * provides methods to manage them, and allows the registration of authenticators to + * which operations such as addAccount and getAuthToken are delegated. + * <p> + * Many of the calls take an {@link AccountManagerCallback} and {@link Handler} as parameters. + * These calls return immediately but run asynchronously. If a callback is provided then + * {@link AccountManagerCallback#run} will be invoked wen the request completes, successfully + * or not. An {@link AccountManagerFuture} is returned by these requests and also passed into the + * callback. The result if retrieved by calling {@link AccountManagerFuture#getResult()} which + * either returns the result or throws an exception as appropriate. + * <p> + * The asynchronous request can be made blocking by not providing a callback and instead + * calling {@link AccountManagerFuture#getResult()} on the future that is returned. This will + * cause the running thread to block until the result is returned. Keep in mind that one + * should not block the main thread in this way. Instead one should either use a callback, + * thus making the call asynchronous, or make the blocking call on a separate thread. + * <p> + * If one wants to ensure that the callback is invoked from a specific handler then they should + * pass the handler to the request. This makes it easier to ensure thread-safety by running + * all of one's logic from a single handler. + */ +public class AccountManager { + private static final String TAG = "AccountManager"; + + public static final int ERROR_CODE_REMOTE_EXCEPTION = 1; + public static final int ERROR_CODE_NETWORK_ERROR = 3; + public static final int ERROR_CODE_CANCELED = 4; + public static final int ERROR_CODE_INVALID_RESPONSE = 5; + public static final int ERROR_CODE_UNSUPPORTED_OPERATION = 6; + public static final int ERROR_CODE_BAD_ARGUMENTS = 7; + public static final int ERROR_CODE_BAD_REQUEST = 8; + + public static final String KEY_ACCOUNTS = "accounts"; + public static final String KEY_AUTHENTICATOR_TYPES = "authenticator_types"; + public static final String KEY_USERDATA = "userdata"; + public static final String KEY_AUTHTOKEN = "authtoken"; + public static final String KEY_PASSWORD = "password"; + public static final String KEY_ACCOUNT_NAME = "authAccount"; + public static final String KEY_ACCOUNT_TYPE = "accountType"; + public static final String KEY_ERROR_CODE = "errorCode"; + public static final String KEY_ERROR_MESSAGE = "errorMessage"; + public static final String KEY_INTENT = "intent"; + public static final String KEY_BOOLEAN_RESULT = "booleanResult"; + public static final String KEY_ACCOUNT_AUTHENTICATOR_RESPONSE = "accountAuthenticatorResponse"; + public static final String KEY_ACCOUNT_MANAGER_RESPONSE = "accountManagerResponse"; + public static final String KEY_AUTH_FAILED_MESSAGE = "authFailedMessage"; + public static final String KEY_AUTH_TOKEN_LABEL = "authTokenLabelKey"; + public static final String ACTION_AUTHENTICATOR_INTENT = + "android.accounts.AccountAuthenticator"; + public static final String AUTHENTICATOR_META_DATA_NAME = + "android.accounts.AccountAuthenticator"; + public static final String AUTHENTICATOR_ATTRIBUTES_NAME = "account-authenticator"; + + private final Context mContext; + private final IAccountManager mService; + private final Handler mMainHandler; + /** + * Action sent as a broadcast Intent by the AccountsService + * when accounts are added to and/or removed from the device's + * database. + */ + public static final String LOGIN_ACCOUNTS_CHANGED_ACTION = + "android.accounts.LOGIN_ACCOUNTS_CHANGED"; + + /** + * @hide + */ + public AccountManager(Context context, IAccountManager service) { + mContext = context; + mService = service; + mMainHandler = new Handler(mContext.getMainLooper()); + } + + /** + * @hide used for testing only + */ + public AccountManager(Context context, IAccountManager service, Handler handler) { + mContext = context; + mService = service; + mMainHandler = handler; + } + + /** + * Retrieve an AccountManager instance that is associated with the context that is passed in. + * Certain calls such as {@link #addOnAccountsUpdatedListener} use this context internally, + * so the caller must take care to use a {@link Context} whose lifetime is associated with + * the listener registration. + * @param context The {@link Context} to use when necessary + * @return an {@link AccountManager} instance that is associated with context + */ + public static AccountManager get(Context context) { + return (AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE); + } + + /** + * Get the password that is associated with the account. Returns null if the account does + * not exist. + * <p> + * Requires that the caller has permission + * {@link android.Manifest.permission#AUTHENTICATE_ACCOUNTS} and is running + * with the same UID as the Authenticator for the account. + */ + public String getPassword(final Account account) { + try { + return mService.getPassword(account); + } catch (RemoteException e) { + // will never happen + throw new RuntimeException(e); + } + } + + /** + * Get the user data named by "key" that is associated with the account. + * Returns null if the account does not exist or if it does not have a value for key. + * <p> + * Requires that the caller has permission + * {@link android.Manifest.permission#AUTHENTICATE_ACCOUNTS} and is running + * with the same UID as the Authenticator for the account. + */ + public String getUserData(final Account account, final String key) { + try { + return mService.getUserData(account, key); + } catch (RemoteException e) { + // will never happen + throw new RuntimeException(e); + } + } + + /** + * Query the AccountManager Service for an array that contains a + * {@link AuthenticatorDescription} for each registered authenticator. + * @return an array that contains all the authenticators known to the AccountManager service. + * This array will be empty if there are no authenticators and will never return null. + * <p> + * No permission is required to make this call. + */ + public AuthenticatorDescription[] getAuthenticatorTypes() { + try { + return mService.getAuthenticatorTypes(); + } catch (RemoteException e) { + // will never happen + throw new RuntimeException(e); + } + } + + /** + * Query the AccountManager Service for all accounts. + * @return an array that contains all the accounts known to the AccountManager service. + * This array will be empty if there are no accounts and will never return null. + * <p> + * Requires that the caller has permission {@link android.Manifest.permission#GET_ACCOUNTS} + */ + public Account[] getAccounts() { + try { + return mService.getAccounts(null); + } catch (RemoteException e) { + // won't ever happen + throw new RuntimeException(e); + } + } + + /** + * Query the AccountManager for the set of accounts that have a given type. If null + * is passed as the type than all accounts are returned. + * @param type the account type by which to filter, or null to get all accounts + * @return an array that contains the accounts that match the specified type. This array + * will be empty if no accounts match. It will never return null. + * <p> + * Requires that the caller has permission {@link android.Manifest.permission#GET_ACCOUNTS} + */ + public Account[] getAccountsByType(String type) { + try { + return mService.getAccounts(type); + } catch (RemoteException e) { + // won't ever happen + throw new RuntimeException(e); + } + } + + /** + * Add an account to the AccountManager's set of known accounts. + * <p> + * Requires that the caller has permission + * {@link android.Manifest.permission#AUTHENTICATE_ACCOUNTS} and is running + * with the same UID as the Authenticator for the account. + * @param account The account to add + * @param password The password to associate with the account. May be null. + * @param userdata A bundle of key/value pairs to set as the account's userdata. May be null. + * @return true if the account was sucessfully added, false otherwise, for example, + * if the account already exists or if the account is null + */ + public boolean addAccountExplicitly(Account account, String password, Bundle userdata) { + try { + return mService.addAccount(account, password, userdata); + } catch (RemoteException e) { + // won't ever happen + throw new RuntimeException(e); + } + } + + /** + * Removes the given account. If this account does not exist then this call has no effect. + * <p> + * This call returns immediately but runs asynchronously and the result is accessed via the + * {@link AccountManagerFuture} that is returned. This future is also passed as the sole + * parameter to the {@link AccountManagerCallback}. If the caller wished to use this + * method asynchronously then they will generally pass in a callback object that will get + * invoked with the {@link AccountManagerFuture}. If they wish to use it synchronously then + * they will generally pass null for the callback and instead call + * {@link android.accounts.AccountManagerFuture#getResult()} on this method's return value, + * which will then block until the request completes. + * <p> + * Requires that the caller has permission {@link android.Manifest.permission#MANAGE_ACCOUNTS}. + * + * @param account The {@link Account} to remove + * @param callback A callback to invoke when the request completes. If null then + * no callback is invoked. + * @param handler The {@link Handler} to use to invoke the callback. If null then the + * main thread's {@link Handler} is used. + * @return an {@link AccountManagerFuture} that represents the future result of the call. + * The future result is a {@link Boolean} that is true if the account is successfully removed + * or false if the authenticator refuses to remove the account. + */ + public AccountManagerFuture<Boolean> removeAccount(final Account account, + AccountManagerCallback<Boolean> callback, Handler handler) { + return new Future2Task<Boolean>(handler, callback) { + public void doWork() throws RemoteException { + mService.removeAccount(mResponse, account); + } + public Boolean bundleToResult(Bundle bundle) throws AuthenticatorException { + if (!bundle.containsKey(KEY_BOOLEAN_RESULT)) { + throw new AuthenticatorException("no result in response"); + } + return bundle.getBoolean(KEY_BOOLEAN_RESULT); + } + }.start(); + } + + /** + * Removes the given authtoken. If this authtoken does not exist for the given account type + * then this call has no effect. + * <p> + * Requires that the caller has permission {@link android.Manifest.permission#MANAGE_ACCOUNTS}. + * @param accountType the account type of the authtoken to invalidate + * @param authToken the authtoken to invalidate + */ + public void invalidateAuthToken(final String accountType, final String authToken) { + try { + mService.invalidateAuthToken(accountType, authToken); + } catch (RemoteException e) { + // won't ever happen + throw new RuntimeException(e); + } + } + + /** + * Gets the authtoken named by "authTokenType" for the specified account if it is cached + * by the AccountManager. If no authtoken is cached then null is returned rather than + * asking the authenticaticor to generate one. If the account or the + * authtoken do not exist then null is returned. + * <p> + * Requires that the caller has permission + * {@link android.Manifest.permission#AUTHENTICATE_ACCOUNTS} and is running + * with the same UID as the Authenticator for the account. + * @param account the account whose authtoken is to be retrieved, must not be null + * @param authTokenType the type of authtoken to retrieve + * @return an authtoken for the given account and authTokenType, if one is cached by the + * AccountManager, null otherwise. + */ + public String peekAuthToken(final Account account, final String authTokenType) { + if (account == null) { + Log.e(TAG, "peekAuthToken: the account must not be null"); + return null; + } + if (authTokenType == null) { + return null; + } + try { + return mService.peekAuthToken(account, authTokenType); + } catch (RemoteException e) { + // won't ever happen + throw new RuntimeException(e); + } + } + + /** + * Sets the password for the account. The password may be null. If the account does not exist + * then this call has no affect. + * <p> + * Requires that the caller has permission + * {@link android.Manifest.permission#AUTHENTICATE_ACCOUNTS} and is running + * with the same UID as the Authenticator for the account. + * @param account the account whose password is to be set. Must not be null. + * @param password the password to set for the account. May be null. + */ + public void setPassword(final Account account, final String password) { + if (account == null) { + Log.e(TAG, "the account must not be null"); + return; + } + try { + mService.setPassword(account, password); + } catch (RemoteException e) { + // won't ever happen + throw new RuntimeException(e); + } + } + + /** + * Sets the password for account to null. If the account does not exist then this call + * has no effect. + * <p> + * Requires that the caller has permission {@link android.Manifest.permission#MANAGE_ACCOUNTS}. + * @param account the account whose password is to be cleared. Must not be null. + */ + public void clearPassword(final Account account) { + if (account == null) { + Log.e(TAG, "the account must not be null"); + return; + } + try { + mService.clearPassword(account); + } catch (RemoteException e) { + // won't ever happen + throw new RuntimeException(e); + } + } + + /** + * Sets account's userdata named "key" to the specified value. If the account does not + * exist then this call has no effect. + * <p> + * Requires that the caller has permission + * {@link android.Manifest.permission#AUTHENTICATE_ACCOUNTS} and is running + * with the same UID as the Authenticator for the account. + * @param account the account whose userdata is to be set. Must not be null. + * @param key the key of the userdata to set. Must not be null. + * @param value the value to set. May be null. + */ + public void setUserData(final Account account, final String key, final String value) { + if (account == null) { + Log.e(TAG, "the account must not be null"); + return; + } + if (key == null) { + Log.e(TAG, "the key must not be null"); + return; + } + try { + mService.setUserData(account, key, value); + } catch (RemoteException e) { + // won't ever happen + throw new RuntimeException(e); + } + } + + /** + * Sets the authtoken named by "authTokenType" to the value specified by authToken. + * If the account does not exist then this call has no effect. + * <p> + * Requires that the caller has permission + * {@link android.Manifest.permission#AUTHENTICATE_ACCOUNTS} and is running + * with the same UID as the Authenticator for the account. + * @param account the account whose authtoken is to be set. Must not be null. + * @param authTokenType the type of the authtoken to set. Must not be null. + * @param authToken the authToken to set. May be null. + */ + public void setAuthToken(Account account, final String authTokenType, final String authToken) { + try { + mService.setAuthToken(account, authTokenType, authToken); + } catch (RemoteException e) { + // won't ever happen + throw new RuntimeException(e); + } + } + + /** + * Convenience method that makes a blocking call to + * {@link #getAuthToken(Account, String, boolean, AccountManagerCallback, Handler)} + * then extracts and returns the value of {@link #KEY_AUTHTOKEN} from its result. + * <p> + * Requires that the caller has permission {@link android.Manifest.permission#USE_CREDENTIALS}. + * @param account the account whose authtoken is to be retrieved, must not be null + * @param authTokenType the type of authtoken to retrieve + * @param notifyAuthFailure if true, cause the AccountManager to put up a "sign-on" notification + * for the account if no authtoken is cached by the AccountManager and the the authenticator + * does not have valid credentials to get an authtoken. + * @return an authtoken for the given account and authTokenType, if one is cached by the + * AccountManager, null otherwise. + * @throws AuthenticatorException if the authenticator is not present, unreachable or returns + * an invalid response. + * @throws OperationCanceledException if the request is canceled for any reason + * @throws java.io.IOException if the authenticator experiences an IOException while attempting + * to communicate with its backend server. + */ + public String blockingGetAuthToken(Account account, String authTokenType, + boolean notifyAuthFailure) + throws OperationCanceledException, IOException, AuthenticatorException { + Bundle bundle = getAuthToken(account, authTokenType, notifyAuthFailure, null /* callback */, + null /* handler */).getResult(); + return bundle.getString(KEY_AUTHTOKEN); + } + + /** + * Request that an authtoken of the specified type be returned for an account. + * If the Account Manager has a cached authtoken of the requested type then it will + * service the request itself. Otherwise it will pass the request on to the authenticator. + * The authenticator can try to service this request with information it already has stored + * in the AccountManager but may need to launch an activity to prompt the + * user to enter credentials. If it is able to retrieve the authtoken it will be returned + * in the result. + * <p> + * If the authenticator needs to prompt the user for credentials it will return an intent to + * the activity that will do the prompting. If an activity is supplied then that activity + * will be used to launch the intent and the result will come from it. Otherwise a result will + * be returned that contains the intent. + * <p> + * This call returns immediately but runs asynchronously and the result is accessed via the + * {@link AccountManagerFuture} that is returned. This future is also passed as the sole + * parameter to the {@link AccountManagerCallback}. If the caller wished to use this + * method asynchronously then they will generally pass in a callback object that will get + * invoked with the {@link AccountManagerFuture}. If they wish to use it synchronously then + * they will generally pass null for the callback and instead call + * {@link android.accounts.AccountManagerFuture#getResult()} on this method's return value, + * which will then block until the request completes. + * <p> + * Requires that the caller has permission {@link android.Manifest.permission#USE_CREDENTIALS}. + * + * @param account The account whose credentials are to be updated. + * @param authTokenType the auth token to retrieve as part of updating the credentials. + * May be null. + * @param options authenticator specific options for the request + * @param activity If the authenticator returns a {@link #KEY_INTENT} in the result then + * the intent will be started with this activity. If activity is null then the result will + * be returned as-is. + * @param callback A callback to invoke when the request completes. If null then + * no callback is invoked. + * @param handler The {@link Handler} to use to invoke the callback. If null then the + * main thread's {@link Handler} is used. + * @return an {@link AccountManagerFuture} that represents the future result of the call. + * The future result is a {@link Bundle} that contains: + * <ul> + * <li> {@link #KEY_ACCOUNT_NAME}, {@link #KEY_ACCOUNT_TYPE} and {@link #KEY_AUTHTOKEN} + * </ul> + * If the user presses "back" then the request will be canceled. + */ + public AccountManagerFuture<Bundle> getAuthToken( + final Account account, final String authTokenType, final Bundle options, + final Activity activity, AccountManagerCallback<Bundle> callback, Handler handler) { + if (activity == null) throw new IllegalArgumentException("activity is null"); + if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null"); + return new AmsTask(activity, handler, callback) { + public void doWork() throws RemoteException { + mService.getAuthToken(mResponse, account, authTokenType, + false /* notifyOnAuthFailure */, true /* expectActivityLaunch */, + options); + } + }.start(); + } + + /** + * Request that an authtoken of the specified type be returned for an account. + * If the Account Manager has a cached authtoken of the requested type then it will + * service the request itself. Otherwise it will pass the request on to the authenticator. + * The authenticator can try to service this request with information it already has stored + * in the AccountManager but may need to launch an activity to prompt the + * user to enter credentials. If it is able to retrieve the authtoken it will be returned + * in the result. + * <p> + * If the authenticator needs to prompt the user for credentials it will return an intent for + * an activity that will do the prompting. If an intent is returned and notifyAuthFailure + * is true then a notification will be created that launches this intent. + * <p> + * This call returns immediately but runs asynchronously and the result is accessed via the + * {@link AccountManagerFuture} that is returned. This future is also passed as the sole + * parameter to the {@link AccountManagerCallback}. If the caller wished to use this + * method asynchronously then they will generally pass in a callback object that will get + * invoked with the {@link AccountManagerFuture}. If they wish to use it synchronously then + * they will generally pass null for the callback and instead call + * {@link android.accounts.AccountManagerFuture#getResult()} on this method's return value, + * which will then block until the request completes. + * <p> + * Requires that the caller has permission {@link android.Manifest.permission#USE_CREDENTIALS}. + * + * @param account The account whose credentials are to be updated. + * @param authTokenType the auth token to retrieve as part of updating the credentials. + * May be null. + * @param notifyAuthFailure if true and the authenticator returns a {@link #KEY_INTENT} in the + * result then a "sign-on needed" notification will be created that will launch this intent. + * @param callback A callback to invoke when the request completes. If null then + * no callback is invoked. + * @param handler The {@link Handler} to use to invoke the callback. If null then the + * main thread's {@link Handler} is used. + * @return an {@link AccountManagerFuture} that represents the future result of the call. + * The future result is a {@link Bundle} that contains either: + * <ul> + * <li> {@link #KEY_INTENT}, which is to be used to prompt the user for the credentials + * <li> {@link #KEY_ACCOUNT_NAME}, {@link #KEY_ACCOUNT_TYPE} and {@link #KEY_AUTHTOKEN} + * if the authenticator is able to retrieve the auth token + * </ul> + * If the user presses "back" then the request will be canceled. + */ + public AccountManagerFuture<Bundle> getAuthToken( + final Account account, final String authTokenType, final boolean notifyAuthFailure, + AccountManagerCallback<Bundle> callback, Handler handler) { + if (account == null) throw new IllegalArgumentException("account is null"); + if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null"); + return new AmsTask(null, handler, callback) { + public void doWork() throws RemoteException { + mService.getAuthToken(mResponse, account, authTokenType, + notifyAuthFailure, false /* expectActivityLaunch */, null /* options */); + } + }.start(); + } + + /** + * Request that an account be added with the given accountType. This request + * is processed by the authenticator for the account type. If no authenticator is registered + * in the system then {@link AuthenticatorException} is thrown. + * <p> + * This call returns immediately but runs asynchronously and the result is accessed via the + * {@link AccountManagerFuture} that is returned. This future is also passed as the sole + * parameter to the {@link AccountManagerCallback}. If the caller wished to use this + * method asynchronously then they will generally pass in a callback object that will get + * invoked with the {@link AccountManagerFuture}. If they wish to use it synchronously then + * they will generally pass null for the callback and instead call + * {@link android.accounts.AccountManagerFuture#getResult()} on this method's return value, + * which will then block until the request completes. + * <p> + * Requires that the caller has permission {@link android.Manifest.permission#MANAGE_ACCOUNTS}. + * + * @param accountType The type of account to add. This must not be null. + * @param authTokenType The account that is added should be able to service this auth token + * type. This may be null. + * @param requiredFeatures The account that is added should support these features. + * This array may be null or empty. + * @param addAccountOptions A bundle of authenticator-specific options that is passed on + * to the authenticator. This may be null. + * @param activity If the authenticator returns a {@link #KEY_INTENT} in the result then + * the intent will be started with this activity. If activity is null then the result will + * be returned as-is. + * @param callback A callback to invoke when the request completes. If null then + * no callback is invoked. + * @param handler The {@link Handler} to use to invoke the callback. If null then the + * main thread's {@link Handler} is used. + * @return an {@link AccountManagerFuture} that represents the future result of the call. + * The future result is a {@link Bundle} that contains either: + * <ul> + * <li> {@link #KEY_INTENT}, or + * <li> {@link #KEY_ACCOUNT_NAME}, {@link #KEY_ACCOUNT_TYPE} + * and {@link #KEY_AUTHTOKEN} (if an authTokenType was specified). + * </ul> + */ + public AccountManagerFuture<Bundle> addAccount(final String accountType, + final String authTokenType, final String[] requiredFeatures, + final Bundle addAccountOptions, + final Activity activity, AccountManagerCallback<Bundle> callback, Handler handler) { + return new AmsTask(activity, handler, callback) { + public void doWork() throws RemoteException { + if (accountType == null) { + Log.e(TAG, "the account must not be null"); + // to unblock caller waiting on Future.get() + set(new Bundle()); + return; + } + mService.addAcount(mResponse, accountType, authTokenType, + requiredFeatures, activity != null, addAccountOptions); + } + }.start(); + } + + public AccountManagerFuture<Account[]> getAccountsByTypeAndFeatures( + final String type, final String[] features, + AccountManagerCallback<Account[]> callback, Handler handler) { + return new Future2Task<Account[]>(handler, callback) { + public void doWork() throws RemoteException { + if (type == null) { + Log.e(TAG, "Type is null"); + set(new Account[0]); + return; + } + mService.getAccountsByFeatures(mResponse, type, features); + } + public Account[] bundleToResult(Bundle bundle) throws AuthenticatorException { + if (!bundle.containsKey(KEY_ACCOUNTS)) { + throw new AuthenticatorException("no result in response"); + } + final Parcelable[] parcelables = bundle.getParcelableArray(KEY_ACCOUNTS); + Account[] descs = new Account[parcelables.length]; + for (int i = 0; i < parcelables.length; i++) { + descs[i] = (Account) parcelables[i]; + } + return descs; + } + }.start(); + } + + /** + * Requests that the authenticator checks that the user knows the credentials for the account. + * This is typically done by returning an intent to an activity that prompts the user to + * enter the credentials. This request + * is processed by the authenticator for the account. If no matching authenticator is + * registered in the system then {@link AuthenticatorException} is thrown. + * <p> + * This call returns immediately but runs asynchronously and the result is accessed via the + * {@link AccountManagerFuture} that is returned. This future is also passed as the sole + * parameter to the {@link AccountManagerCallback}. If the caller wished to use this + * method asynchronously then they will generally pass in a callback object that will get + * invoked with the {@link AccountManagerFuture}. If they wish to use it synchronously then + * they will generally pass null for the callback and instead call + * {@link android.accounts.AccountManagerFuture#getResult()} on this method's return value, + * which will then block until the request completes. + * <p> + * Requires that the caller has permission {@link android.Manifest.permission#MANAGE_ACCOUNTS}. + * + * @param account The account whose credentials are to be checked + * @param options authenticator specific options for the request + * @param activity If the authenticator returns a {@link #KEY_INTENT} in the result then + * the intent will be started with this activity. If activity is null then the result will + * be returned as-is. + * @param callback A callback to invoke when the request completes. If null then + * no callback is invoked. + * @param handler The {@link Handler} to use to invoke the callback. If null then the + * main thread's {@link Handler} is used. + * @return an {@link AccountManagerFuture} that represents the future result of the call. + * The future result is a {@link Bundle} that contains either: + * <ul> + * <li> {@link #KEY_INTENT}, which is to be used to prompt the user for the credentials + * <li> {@link #KEY_ACCOUNT_NAME} and {@link #KEY_ACCOUNT_TYPE} if the user enters the correct + * credentials + * </ul> + * If the user presses "back" then the request will be canceled. + */ + public AccountManagerFuture<Bundle> confirmCredentials(final Account account, + final Bundle options, + final Activity activity, + final AccountManagerCallback<Bundle> callback, + final Handler handler) { + return new AmsTask(activity, handler, callback) { + public void doWork() throws RemoteException { + mService.confirmCredentials(mResponse, account, options, activity != null); + } + }.start(); + } + + /** + * Requests that the authenticator update the the credentials for a user. This is typically + * done by returning an intent to an activity that will prompt the user to update the stored + * credentials for the account. This request + * is processed by the authenticator for the account. If no matching authenticator is + * registered in the system then {@link AuthenticatorException} is thrown. + * <p> + * This call returns immediately but runs asynchronously and the result is accessed via the + * {@link AccountManagerFuture} that is returned. This future is also passed as the sole + * parameter to the {@link AccountManagerCallback}. If the caller wished to use this + * method asynchronously then they will generally pass in a callback object that will get + * invoked with the {@link AccountManagerFuture}. If they wish to use it synchronously then + * they will generally pass null for the callback and instead call + * {@link android.accounts.AccountManagerFuture#getResult()} on this method's return value, + * which will then block until the request completes. + * <p> + * Requires that the caller has permission {@link android.Manifest.permission#MANAGE_ACCOUNTS}. + * + * @param account The account whose credentials are to be updated. + * @param authTokenType the auth token to retrieve as part of updating the credentials. + * May be null. + * @param options authenticator specific options for the request + * @param activity If the authenticator returns a {@link #KEY_INTENT} in the result then + * the intent will be started with this activity. If activity is null then the result will + * be returned as-is. + * @param callback A callback to invoke when the request completes. If null then + * no callback is invoked. + * @param handler The {@link Handler} to use to invoke the callback. If null then the + * main thread's {@link Handler} is used. + * @return an {@link AccountManagerFuture} that represents the future result of the call. + * The future result is a {@link Bundle} that contains either: + * <ul> + * <li> {@link #KEY_INTENT}, which is to be used to prompt the user for the credentials + * <li> {@link #KEY_ACCOUNT_NAME} and {@link #KEY_ACCOUNT_TYPE} if the user enters the correct + * credentials, and optionally a {@link #KEY_AUTHTOKEN} if an authTokenType was provided. + * </ul> + * If the user presses "back" then the request will be canceled. + */ + public AccountManagerFuture<Bundle> updateCredentials(final Account account, + final String authTokenType, + final Bundle options, final Activity activity, + final AccountManagerCallback<Bundle> callback, + final Handler handler) { + return new AmsTask(activity, handler, callback) { + public void doWork() throws RemoteException { + mService.updateCredentials(mResponse, account, authTokenType, activity != null, + options); + } + }.start(); + } + + /** + * Request that the properties for an authenticator be updated. This is typically done by + * returning an intent to an activity that will allow the user to make changes. This request + * is processed by the authenticator for the account. If no matching authenticator is + * registered in the system then {@link AuthenticatorException} is thrown. + * <p> + * This call returns immediately but runs asynchronously and the result is accessed via the + * {@link AccountManagerFuture} that is returned. This future is also passed as the sole + * parameter to the {@link AccountManagerCallback}. If the caller wished to use this + * method asynchronously then they will generally pass in a callback object that will get + * invoked with the {@link AccountManagerFuture}. If they wish to use it synchronously then + * they will generally pass null for the callback and instead call + * {@link android.accounts.AccountManagerFuture#getResult()} on this method's return value, + * which will then block until the request completes. + * <p> + * Requires that the caller has permission {@link android.Manifest.permission#MANAGE_ACCOUNTS}. + * + * @param accountType The account type of the authenticator whose properties are to be edited. + * @param activity If the authenticator returns a {@link #KEY_INTENT} in the result then + * the intent will be started with this activity. If activity is null then the result will + * be returned as-is. + * @param callback A callback to invoke when the request completes. If null then + * no callback is invoked. + * @param handler The {@link Handler} to use to invoke the callback. If null then the + * main thread's {@link Handler} is used. + * @return an {@link AccountManagerFuture} that represents the future result of the call. + * The future result is a {@link Bundle} that contains either: + * <ul> + * <li> {@link #KEY_INTENT}, which is to be used to prompt the user for the credentials + * <li> nothing, returned if the edit completes successfully + * </ul> + * If the user presses "back" then the request will be canceled. + */ + public AccountManagerFuture<Bundle> editProperties(final String accountType, + final Activity activity, final AccountManagerCallback<Bundle> callback, + final Handler handler) { + return new AmsTask(activity, handler, callback) { + public void doWork() throws RemoteException { + mService.editProperties(mResponse, accountType, activity != null); + } + }.start(); + } + + private void ensureNotOnMainThread() { + final Looper looper = Looper.myLooper(); + if (looper != null && looper == mContext.getMainLooper()) { + // We really want to throw an exception here, but GTalkService exercises this + // path quite a bit and needs some serious rewrite in order to work properly. + //noinspection ThrowableInstanceNeverThrow +// Log.e(TAG, "calling this from your main thread can lead to deadlock and/or ANRs", +// new Exception()); + // TODO remove the log and throw this exception when the callers are fixed +// throw new IllegalStateException( +// "calling this from your main thread can lead to deadlock"); + } + } + + private void postToHandler(Handler handler, final AccountManagerCallback<Bundle> callback, + final AccountManagerFuture<Bundle> future) { + handler = handler == null ? mMainHandler : handler; + handler.post(new Runnable() { + public void run() { + callback.run(future); + } + }); + } + + private void postToHandler(Handler handler, final OnAccountsUpdateListener listener, + final Account[] accounts) { + final Account[] accountsCopy = new Account[accounts.length]; + // send a copy to make sure that one doesn't + // change what another sees + System.arraycopy(accounts, 0, accountsCopy, 0, accountsCopy.length); + handler = (handler == null) ? mMainHandler : handler; + handler.post(new Runnable() { + public void run() { + try { + listener.onAccountsUpdated(accountsCopy); + } catch (SQLException e) { + // Better luck next time. If the problem was disk-full, + // the STORAGE_OK intent will re-trigger the update. + Log.e(TAG, "Can't update accounts", e); + } + } + }); + } + + private abstract class AmsTask extends FutureTask<Bundle> implements AccountManagerFuture<Bundle> { + final IAccountManagerResponse mResponse; + final Handler mHandler; + final AccountManagerCallback<Bundle> mCallback; + final Activity mActivity; + public AmsTask(Activity activity, Handler handler, AccountManagerCallback<Bundle> callback) { + super(new Callable<Bundle>() { + public Bundle call() throws Exception { + throw new IllegalStateException("this should never be called"); + } + }); + + mHandler = handler; + mCallback = callback; + mActivity = activity; + mResponse = new Response(); + } + + public final AccountManagerFuture<Bundle> start() { + try { + doWork(); + } catch (RemoteException e) { + setException(e); + } + return this; + } + + public abstract void doWork() throws RemoteException; + + private Bundle internalGetResult(Long timeout, TimeUnit unit) + throws OperationCanceledException, IOException, AuthenticatorException { + ensureNotOnMainThread(); + try { + if (timeout == null) { + return get(); + } else { + return get(timeout, unit); + } + } catch (CancellationException e) { + throw new OperationCanceledException(); + } catch (TimeoutException e) { + // fall through and cancel + } catch (InterruptedException e) { + // fall through and cancel + } catch (ExecutionException e) { + final Throwable cause = e.getCause(); + if (cause instanceof IOException) { + throw (IOException) cause; + } else if (cause instanceof UnsupportedOperationException) { + throw new AuthenticatorException(cause); + } else if (cause instanceof AuthenticatorException) { + throw (AuthenticatorException) cause; + } else if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } else if (cause instanceof Error) { + throw (Error) cause; + } else { + throw new IllegalStateException(cause); + } + } finally { + cancel(true /* interrupt if running */); + } + throw new OperationCanceledException(); + } + + public Bundle getResult() + throws OperationCanceledException, IOException, AuthenticatorException { + return internalGetResult(null, null); + } + + public Bundle getResult(long timeout, TimeUnit unit) + throws OperationCanceledException, IOException, AuthenticatorException { + return internalGetResult(timeout, unit); + } + + protected void done() { + if (mCallback != null) { + postToHandler(mHandler, mCallback, this); + } + } + + /** Handles the responses from the AccountManager */ + private class Response extends IAccountManagerResponse.Stub { + public void onResult(Bundle bundle) { + Intent intent = bundle.getParcelable("intent"); + if (intent != null && mActivity != null) { + // since the user provided an Activity we will silently start intents + // that we see + mActivity.startActivity(intent); + // leave the Future running to wait for the real response to this request + } else if (bundle.getBoolean("retry")) { + try { + doWork(); + } catch (RemoteException e) { + // this will only happen if the system process is dead, which means + // we will be dying ourselves + } + } else { + set(bundle); + } + } + + public void onError(int code, String message) { + if (code == ERROR_CODE_CANCELED) { + // the authenticator indicated that this request was canceled, do so now + cancel(true /* mayInterruptIfRunning */); + return; + } + setException(convertErrorToException(code, message)); + } + } + + } + + private abstract class BaseFutureTask<T> extends FutureTask<T> { + final public IAccountManagerResponse mResponse; + final Handler mHandler; + + public BaseFutureTask(Handler handler) { + super(new Callable<T>() { + public T call() throws Exception { + throw new IllegalStateException("this should never be called"); + } + }); + mHandler = handler; + mResponse = new Response(); + } + + public abstract void doWork() throws RemoteException; + + public abstract T bundleToResult(Bundle bundle) throws AuthenticatorException; + + protected void postRunnableToHandler(Runnable runnable) { + Handler handler = (mHandler == null) ? mMainHandler : mHandler; + handler.post(runnable); + } + + protected void startTask() { + try { + doWork(); + } catch (RemoteException e) { + setException(e); + } + } + + protected class Response extends IAccountManagerResponse.Stub { + public void onResult(Bundle bundle) { + try { + T result = bundleToResult(bundle); + if (result == null) { + return; + } + set(result); + return; + } catch (ClassCastException e) { + // we will set the exception below + } catch (AuthenticatorException e) { + // we will set the exception below + } + onError(ERROR_CODE_INVALID_RESPONSE, "no result in response"); + } + + public void onError(int code, String message) { + if (code == ERROR_CODE_CANCELED) { + cancel(true /* mayInterruptIfRunning */); + return; + } + setException(convertErrorToException(code, message)); + } + } + } + + private abstract class Future2Task<T> + extends BaseFutureTask<T> implements AccountManagerFuture<T> { + final AccountManagerCallback<T> mCallback; + public Future2Task(Handler handler, AccountManagerCallback<T> callback) { + super(handler); + mCallback = callback; + } + + protected void done() { + if (mCallback != null) { + postRunnableToHandler(new Runnable() { + public void run() { + mCallback.run(Future2Task.this); + } + }); + } + } + + public Future2Task<T> start() { + startTask(); + return this; + } + + private T internalGetResult(Long timeout, TimeUnit unit) + throws OperationCanceledException, IOException, AuthenticatorException { + ensureNotOnMainThread(); + try { + if (timeout == null) { + return get(); + } else { + return get(timeout, unit); + } + } catch (InterruptedException e) { + // fall through and cancel + } catch (TimeoutException e) { + // fall through and cancel + } catch (CancellationException e) { + // fall through and cancel + } catch (ExecutionException e) { + final Throwable cause = e.getCause(); + if (cause instanceof IOException) { + throw (IOException) cause; + } else if (cause instanceof UnsupportedOperationException) { + throw new AuthenticatorException(cause); + } else if (cause instanceof AuthenticatorException) { + throw (AuthenticatorException) cause; + } else if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } else if (cause instanceof Error) { + throw (Error) cause; + } else { + throw new IllegalStateException(cause); + } + } finally { + cancel(true /* interrupt if running */); + } + throw new OperationCanceledException(); + } + + public T getResult() + throws OperationCanceledException, IOException, AuthenticatorException { + return internalGetResult(null, null); + } + + public T getResult(long timeout, TimeUnit unit) + throws OperationCanceledException, IOException, AuthenticatorException { + return internalGetResult(timeout, unit); + } + + } + + private Exception convertErrorToException(int code, String message) { + if (code == ERROR_CODE_NETWORK_ERROR) { + return new IOException(message); + } + + if (code == ERROR_CODE_UNSUPPORTED_OPERATION) { + return new UnsupportedOperationException(message); + } + + if (code == ERROR_CODE_INVALID_RESPONSE) { + return new AuthenticatorException(message); + } + + if (code == ERROR_CODE_BAD_ARGUMENTS) { + return new IllegalArgumentException(message); + } + + return new AuthenticatorException(message); + } + + private class GetAuthTokenByTypeAndFeaturesTask + extends AmsTask implements AccountManagerCallback<Bundle> { + GetAuthTokenByTypeAndFeaturesTask(final String accountType, final String authTokenType, + final String[] features, Activity activityForPrompting, + final Bundle addAccountOptions, final Bundle loginOptions, + AccountManagerCallback<Bundle> callback, Handler handler) { + super(activityForPrompting, handler, callback); + if (accountType == null) throw new IllegalArgumentException("account type is null"); + mAccountType = accountType; + mAuthTokenType = authTokenType; + mFeatures = features; + mAddAccountOptions = addAccountOptions; + mLoginOptions = loginOptions; + mMyCallback = this; + } + volatile AccountManagerFuture<Bundle> mFuture = null; + final String mAccountType; + final String mAuthTokenType; + final String[] mFeatures; + final Bundle mAddAccountOptions; + final Bundle mLoginOptions; + final AccountManagerCallback<Bundle> mMyCallback; + + public void doWork() throws RemoteException { + getAccountsByTypeAndFeatures(mAccountType, mFeatures, + new AccountManagerCallback<Account[]>() { + public void run(AccountManagerFuture<Account[]> future) { + Account[] accounts; + try { + accounts = future.getResult(); + } catch (OperationCanceledException e) { + setException(e); + return; + } catch (IOException e) { + setException(e); + return; + } catch (AuthenticatorException e) { + setException(e); + return; + } + + if (accounts.length == 0) { + if (mActivity != null) { + // no accounts, add one now. pretend that the user directly + // made this request + mFuture = addAccount(mAccountType, mAuthTokenType, mFeatures, + mAddAccountOptions, mActivity, mMyCallback, mHandler); + } else { + // send result since we can't prompt to add an account + Bundle result = new Bundle(); + result.putString(KEY_ACCOUNT_NAME, null); + result.putString(KEY_ACCOUNT_TYPE, null); + result.putString(KEY_AUTHTOKEN, null); + try { + mResponse.onResult(result); + } catch (RemoteException e) { + // this will never happen + } + // we are done + } + } else if (accounts.length == 1) { + // have a single account, return an authtoken for it + if (mActivity == null) { + mFuture = getAuthToken(accounts[0], mAuthTokenType, + false /* notifyAuthFailure */, mMyCallback, mHandler); + } else { + mFuture = getAuthToken(accounts[0], + mAuthTokenType, mLoginOptions, + mActivity, mMyCallback, mHandler); + } + } else { + if (mActivity != null) { + IAccountManagerResponse chooseResponse = + new IAccountManagerResponse.Stub() { + public void onResult(Bundle value) throws RemoteException { + Account account = new Account( + value.getString(KEY_ACCOUNT_NAME), + value.getString(KEY_ACCOUNT_TYPE)); + mFuture = getAuthToken(account, mAuthTokenType, mLoginOptions, + mActivity, mMyCallback, mHandler); + } + + public void onError(int errorCode, String errorMessage) + throws RemoteException { + mResponse.onError(errorCode, errorMessage); + } + }; + // have many accounts, launch the chooser + Intent intent = new Intent(); + intent.setClassName("android", + "android.accounts.ChooseAccountActivity"); + intent.putExtra(KEY_ACCOUNTS, accounts); + intent.putExtra(KEY_ACCOUNT_MANAGER_RESPONSE, + new AccountManagerResponse(chooseResponse)); + mActivity.startActivity(intent); + // the result will arrive via the IAccountManagerResponse + } else { + // send result since we can't prompt to select an account + Bundle result = new Bundle(); + result.putString(KEY_ACCOUNTS, null); + try { + mResponse.onResult(result); + } catch (RemoteException e) { + // this will never happen + } + // we are done + } + } + }}, mHandler); + } + + public void run(AccountManagerFuture<Bundle> future) { + try { + set(future.getResult()); + } catch (OperationCanceledException e) { + cancel(true /* mayInterruptIfRUnning */); + } catch (IOException e) { + setException(e); + } catch (AuthenticatorException e) { + setException(e); + } + } + } + + /** + * Convenience method that combines the functionality of {@link #getAccountsByTypeAndFeatures}, + * {@link #getAuthToken(Account, String, Bundle, Activity, AccountManagerCallback, Handler)}, + * and {@link #addAccount}. It first gets the list of accounts that match accountType and the + * feature set. If there are none then {@link #addAccount} is invoked with the authTokenType + * feature set, and addAccountOptions. If there is exactly one then + * {@link #getAuthToken(Account, String, Bundle, Activity, AccountManagerCallback, Handler)} is + * called with that account. If there are more than one then a chooser activity is launched + * to prompt the user to select one of them and then the authtoken is retrieved for it, + * <p> + * This call returns immediately but runs asynchronously and the result is accessed via the + * {@link AccountManagerFuture} that is returned. This future is also passed as the sole + * parameter to the {@link AccountManagerCallback}. If the caller wished to use this + * method asynchronously then they will generally pass in a callback object that will get + * invoked with the {@link AccountManagerFuture}. If they wish to use it synchronously then + * they will generally pass null for the callback and instead call + * {@link android.accounts.AccountManagerFuture#getResult()} on this method's return value, + * which will then block until the request completes. + * <p> + * Requires that the caller has permission {@link android.Manifest.permission#MANAGE_ACCOUNTS}. + * + * @param accountType the accountType to query; this must be non-null + * @param authTokenType the type of authtoken to retrieve; this must be non-null + * @param features a filter for the accounts. See {@link #getAccountsByTypeAndFeatures}. + * @param activityForPrompting The activity used to start any account management + * activities that are required to fulfill this request. This may be null. + * @param addAccountOptions authenticator-specific options used if an account needs to be added + * @param getAuthTokenOptions authenticator-specific options passed to getAuthToken + * @param callback A callback to invoke when the request completes. If null then + * no callback is invoked. + * @param handler The {@link Handler} to use to invoke the callback. If null then the + * main thread's {@link Handler} is used. + * @return an {@link AccountManagerFuture} that represents the future result of the call. + * The future result is a {@link Bundle} that contains either: + * <ul> + * <li> {@link #KEY_INTENT}, if no activity is supplied yet an activity needs to launched to + * fulfill the request. + * <li> {@link #KEY_ACCOUNT_NAME}, {@link #KEY_ACCOUNT_TYPE} and {@link #KEY_AUTHTOKEN} if the + * request completes successfully. + * </ul> + * If the user presses "back" then the request will be canceled. + */ + public AccountManagerFuture<Bundle> getAuthTokenByFeatures( + final String accountType, final String authTokenType, final String[] features, + final Activity activityForPrompting, final Bundle addAccountOptions, + final Bundle getAuthTokenOptions, + final AccountManagerCallback<Bundle> callback, final Handler handler) { + if (accountType == null) throw new IllegalArgumentException("account type is null"); + if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null"); + final GetAuthTokenByTypeAndFeaturesTask task = + new GetAuthTokenByTypeAndFeaturesTask(accountType, authTokenType, features, + activityForPrompting, addAccountOptions, getAuthTokenOptions, callback, handler); + task.start(); + return task; + } + + private final HashMap<OnAccountsUpdateListener, Handler> mAccountsUpdatedListeners = + Maps.newHashMap(); + + /** + * BroadcastReceiver that listens for the LOGIN_ACCOUNTS_CHANGED_ACTION intent + * so that it can read the updated list of accounts and send them to the listener + * in mAccountsUpdatedListeners. + */ + private final BroadcastReceiver mAccountsChangedBroadcastReceiver = new BroadcastReceiver() { + public void onReceive(final Context context, final Intent intent) { + final Account[] accounts = getAccounts(); + // send the result to the listeners + synchronized (mAccountsUpdatedListeners) { + for (Map.Entry<OnAccountsUpdateListener, Handler> entry : + mAccountsUpdatedListeners.entrySet()) { + postToHandler(entry.getValue(), entry.getKey(), accounts); + } + } + } + }; + + /** + * Add a {@link OnAccountsUpdateListener} to this instance of the {@link AccountManager}. + * The listener is guaranteed to be invoked on the thread of the Handler that is passed + * in or the main thread's Handler if handler is null. + * <p> + * You must remove this listener before the context that was used to retrieve this + * {@link AccountManager} instance goes away. This generally means when the Activity + * or Service you are running is stopped. + * @param listener the listener to add + * @param handler the Handler whose thread will be used to invoke the listener. If null + * the AccountManager context's main thread will be used. + * @param updateImmediately if true then the listener will be invoked as a result of this + * call. + * @throws IllegalArgumentException if listener is null + * @throws IllegalStateException if listener was already added + */ + public void addOnAccountsUpdatedListener(final OnAccountsUpdateListener listener, + Handler handler, boolean updateImmediately) { + if (listener == null) { + throw new IllegalArgumentException("the listener is null"); + } + synchronized (mAccountsUpdatedListeners) { + if (mAccountsUpdatedListeners.containsKey(listener)) { + throw new IllegalStateException("this listener is already added"); + } + final boolean wasEmpty = mAccountsUpdatedListeners.isEmpty(); + + mAccountsUpdatedListeners.put(listener, handler); + + if (wasEmpty) { + // Register a broadcast receiver to monitor account changes + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(LOGIN_ACCOUNTS_CHANGED_ACTION); + // To recover from disk-full. + intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_OK); + mContext.registerReceiver(mAccountsChangedBroadcastReceiver, intentFilter); + } + } + + if (updateImmediately) { + postToHandler(handler, listener, getAccounts()); + } + } + + /** + * Remove an {@link OnAccountsUpdateListener} that was previously registered with + * {@link #addOnAccountsUpdatedListener}. + * @param listener the listener to remove + * @throws IllegalArgumentException if listener is null + * @throws IllegalStateException if listener was not already added + */ + public void removeOnAccountsUpdatedListener(OnAccountsUpdateListener listener) { + if (listener == null) { + Log.e(TAG, "Missing listener"); + return; + } + synchronized (mAccountsUpdatedListeners) { + if (!mAccountsUpdatedListeners.containsKey(listener)) { + Log.e(TAG, "Listener was not previously added"); + return; + } + mAccountsUpdatedListeners.remove(listener); + if (mAccountsUpdatedListeners.isEmpty()) { + mContext.unregisterReceiver(mAccountsChangedBroadcastReceiver); + } + } + } +} diff --git a/core/java/android/accounts/AccountManagerCallback.java b/core/java/android/accounts/AccountManagerCallback.java new file mode 100644 index 0000000..4aa7169 --- /dev/null +++ b/core/java/android/accounts/AccountManagerCallback.java @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.accounts; + +public interface AccountManagerCallback<V> { + void run(AccountManagerFuture<V> future); +}
\ No newline at end of file diff --git a/core/java/android/accounts/AccountManagerFuture.java b/core/java/android/accounts/AccountManagerFuture.java new file mode 100644 index 0000000..a1ab00c --- /dev/null +++ b/core/java/android/accounts/AccountManagerFuture.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.accounts; + +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; +import java.io.IOException; + +/** + * A <tt>AccountManagerFuture</tt> represents the result of an asynchronous + * {@link AccountManager} call. Methods are provided to check if the computation is + * complete, to wait for its completion, and to retrieve the result of + * the computation. The result can only be retrieved using method + * <tt>get</tt> when the computation has completed, blocking if + * necessary until it is ready. Cancellation is performed by the + * <tt>cancel</tt> method. Additional methods are provided to + * determine if the task completed normally or was cancelled. Once a + * computation has completed, the computation cannot be cancelled. + * If you would like to use a <tt>Future</tt> for the sake + * of cancellability but not provide a usable result, you can + * declare types of the form <tt>Future<?></tt> and + * return <tt>null</tt> as a result of the underlying task. + */ +public interface AccountManagerFuture<V> { + /** + * Attempts to cancel execution of this task. This attempt will + * fail if the task has already completed, has already been cancelled, + * or could not be cancelled for some other reason. If successful, + * and this task has not started when <tt>cancel</tt> is called, + * this task should never run. If the task has already started, + * then the <tt>mayInterruptIfRunning</tt> parameter determines + * whether the thread executing this task should be interrupted in + * an attempt to stop the task. + * + * <p>After this method returns, subsequent calls to {@link #isDone} will + * always return <tt>true</tt>. Subsequent calls to {@link #isCancelled} + * will always return <tt>true</tt> if this method returned <tt>true</tt>. + * + * @param mayInterruptIfRunning <tt>true</tt> if the thread executing this + * task should be interrupted; otherwise, in-progress tasks are allowed + * to complete + * @return <tt>false</tt> if the task could not be cancelled, + * typically because it has already completed normally; + * <tt>true</tt> otherwise + */ + boolean cancel(boolean mayInterruptIfRunning); + + /** + * Returns <tt>true</tt> if this task was cancelled before it completed + * normally. + * + * @return <tt>true</tt> if this task was cancelled before it completed + */ + boolean isCancelled(); + + /** + * Returns <tt>true</tt> if this task completed. + * + * Completion may be due to normal termination, an exception, or + * cancellation -- in all of these cases, this method will return + * <tt>true</tt>. + * + * @return <tt>true</tt> if this task completed + */ + boolean isDone(); + + /** + * Accessor for the future result the {@link AccountManagerFuture} represents. This + * call will block until the result is available. In order to check if the result is + * available without blocking, one may call {@link #isDone()} and {@link #isCancelled()}. + * If the request that generated this result fails or is canceled then an exception + * will be thrown rather than the call returning normally. + * @return the actual result + * @throws android.accounts.OperationCanceledException if the request was canceled for any + * reason + * @throws android.accounts.AuthenticatorException if there was an error communicating with + * the authenticator or if the authenticator returned an invalid response + * @throws java.io.IOException if the authenticator returned an error response that indicates + * that it encountered an IOException while communicating with the authentication server + */ + V getResult() throws OperationCanceledException, IOException, AuthenticatorException; + + /** + * Accessor for the future result the {@link AccountManagerFuture} represents. This + * call will block until the result is available. In order to check if the result is + * available without blocking, one may call {@link #isDone()} and {@link #isCancelled()}. + * If the request that generated this result fails or is canceled then an exception + * will be thrown rather than the call returning normally. If a timeout is specified then + * the request will automatically be canceled if it does not complete in that amount of time. + * @param timeout the maximum time to wait + * @param unit the time unit of the timeout argument. This must not be null. + * @return the actual result + * @throws android.accounts.OperationCanceledException if the request was canceled for any + * reason + * @throws android.accounts.AuthenticatorException if there was an error communicating with + * the authenticator or if the authenticator returned an invalid response + * @throws java.io.IOException if the authenticator returned an error response that indicates + * that it encountered an IOException while communicating with the authentication server + */ + V getResult(long timeout, TimeUnit unit) + throws OperationCanceledException, IOException, AuthenticatorException; +}
\ No newline at end of file diff --git a/core/java/android/accounts/AccountManagerResponse.java b/core/java/android/accounts/AccountManagerResponse.java new file mode 100644 index 0000000..1cd6a74 --- /dev/null +++ b/core/java/android/accounts/AccountManagerResponse.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.accounts; + +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.RemoteException; + +/** + * Used by Account Authenticators to return a response. + */ +public class AccountManagerResponse implements Parcelable { + private IAccountManagerResponse mResponse; + + /** @hide */ + public AccountManagerResponse(IAccountManagerResponse response) { + mResponse = response; + } + + /** @hide */ + public AccountManagerResponse(Parcel parcel) { + mResponse = + IAccountManagerResponse.Stub.asInterface(parcel.readStrongBinder()); + } + + public void onResult(Bundle result) { + try { + mResponse.onResult(result); + } catch (RemoteException e) { + // this should never happen + } + } + + public void onError(int errorCode, String errorMessage) { + try { + mResponse.onError(errorCode, errorMessage); + } catch (RemoteException e) { + // this should never happen + } + } + + /** @hide */ + public int describeContents() { + return 0; + } + + /** @hide */ + public void writeToParcel(Parcel dest, int flags) { + dest.writeStrongBinder(mResponse.asBinder()); + } + + /** @hide */ + public static final Creator<AccountManagerResponse> CREATOR = + new Creator<AccountManagerResponse>() { + public AccountManagerResponse createFromParcel(Parcel source) { + return new AccountManagerResponse(source); + } + + public AccountManagerResponse[] newArray(int size) { + return new AccountManagerResponse[size]; + } + }; +} diff --git a/core/java/android/accounts/AccountManagerService.java b/core/java/android/accounts/AccountManagerService.java new file mode 100644 index 0000000..800ad749 --- /dev/null +++ b/core/java/android/accounts/AccountManagerService.java @@ -0,0 +1,1704 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.accounts; + +import android.content.BroadcastReceiver; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.content.pm.RegisteredServicesCache; +import android.content.pm.PackageInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.RegisteredServicesCacheListener; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.os.Bundle; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.os.SystemClock; +import android.os.Binder; +import android.os.SystemProperties; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.util.Log; +import android.util.Pair; +import android.app.PendingIntent; +import android.app.NotificationManager; +import android.app.Notification; +import android.Manifest; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.HashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import com.android.internal.telephony.TelephonyIntents; +import com.android.internal.R; + +/** + * A system service that provides account, password, and authtoken management for all + * accounts on the device. Some of these calls are implemented with the help of the corresponding + * {@link IAccountAuthenticator} services. This service is not accessed by users directly, + * instead one uses an instance of {@link AccountManager}, which can be accessed as follows: + * AccountManager accountManager = + * (AccountManager)context.getSystemService(Context.ACCOUNT_SERVICE) + * @hide + */ +public class AccountManagerService + extends IAccountManager.Stub + implements RegisteredServicesCacheListener<AuthenticatorDescription> { + private static final String GOOGLE_ACCOUNT_TYPE = "com.google"; + + private static final String NO_BROADCAST_FLAG = "nobroadcast"; + + private static final String TAG = "AccountManagerService"; + + private static final int TIMEOUT_DELAY_MS = 1000 * 60; + private static final String DATABASE_NAME = "accounts.db"; + private static final int DATABASE_VERSION = 4; + + private final Context mContext; + + private HandlerThread mMessageThread; + private final MessageHandler mMessageHandler; + + // Messages that can be sent on mHandler + private static final int MESSAGE_TIMED_OUT = 3; + private static final int MESSAGE_CONNECTED = 7; + private static final int MESSAGE_DISCONNECTED = 8; + + private final AccountAuthenticatorCache mAuthenticatorCache; + private final AuthenticatorBindHelper mBindHelper; + private final DatabaseHelper mOpenHelper; + private final SimWatcher mSimWatcher; + + private static final String TABLE_ACCOUNTS = "accounts"; + private static final String ACCOUNTS_ID = "_id"; + private static final String ACCOUNTS_NAME = "name"; + private static final String ACCOUNTS_TYPE = "type"; + private static final String ACCOUNTS_TYPE_COUNT = "count(type)"; + private static final String ACCOUNTS_PASSWORD = "password"; + + private static final String TABLE_AUTHTOKENS = "authtokens"; + private static final String AUTHTOKENS_ID = "_id"; + private static final String AUTHTOKENS_ACCOUNTS_ID = "accounts_id"; + private static final String AUTHTOKENS_TYPE = "type"; + private static final String AUTHTOKENS_AUTHTOKEN = "authtoken"; + + private static final String TABLE_GRANTS = "grants"; + private static final String GRANTS_ACCOUNTS_ID = "accounts_id"; + private static final String GRANTS_AUTH_TOKEN_TYPE = "auth_token_type"; + private static final String GRANTS_GRANTEE_UID = "uid"; + + private static final String TABLE_EXTRAS = "extras"; + private static final String EXTRAS_ID = "_id"; + private static final String EXTRAS_ACCOUNTS_ID = "accounts_id"; + private static final String EXTRAS_KEY = "key"; + private static final String EXTRAS_VALUE = "value"; + + private static final String TABLE_META = "meta"; + private static final String META_KEY = "key"; + private static final String META_VALUE = "value"; + + private static final String[] ACCOUNT_NAME_TYPE_PROJECTION = + new String[]{ACCOUNTS_ID, ACCOUNTS_NAME, ACCOUNTS_TYPE}; + private static final String[] ACCOUNT_TYPE_COUNT_PROJECTION = + new String[] { ACCOUNTS_TYPE, ACCOUNTS_TYPE_COUNT}; + private static final Intent ACCOUNTS_CHANGED_INTENT; + + private static final String COUNT_OF_MATCHING_GRANTS = "" + + "SELECT COUNT(*) FROM " + TABLE_GRANTS + ", " + TABLE_ACCOUNTS + + " WHERE " + GRANTS_ACCOUNTS_ID + "=" + ACCOUNTS_ID + + " AND " + GRANTS_GRANTEE_UID + "=?" + + " AND " + GRANTS_AUTH_TOKEN_TYPE + "=?" + + " AND " + ACCOUNTS_NAME + "=?" + + " AND " + ACCOUNTS_TYPE + "=?"; + + private final LinkedHashMap<String, Session> mSessions = new LinkedHashMap<String, Session>(); + private final AtomicInteger mNotificationIds = new AtomicInteger(1); + + private final HashMap<Pair<Pair<Account, String>, Integer>, Integer> + mCredentialsPermissionNotificationIds = + new HashMap<Pair<Pair<Account, String>, Integer>, Integer>(); + private final HashMap<Account, Integer> mSigninRequiredNotificationIds = + new HashMap<Account, Integer>(); + private static AtomicReference<AccountManagerService> sThis = + new AtomicReference<AccountManagerService>(); + + private static final boolean isDebuggableMonkeyBuild = + SystemProperties.getBoolean("ro.monkey", false) + && SystemProperties.getBoolean("ro.debuggable", false); + private static final Account[] EMPTY_ACCOUNT_ARRAY = new Account[]{}; + + static { + ACCOUNTS_CHANGED_INTENT = new Intent(AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION); + ACCOUNTS_CHANGED_INTENT.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); + } + + /** + * This should only be called by system code. One should only call this after the service + * has started. + * @return a reference to the AccountManagerService instance + * @hide + */ + public static AccountManagerService getSingleton() { + return sThis.get(); + } + + public class AuthTokenKey { + public final Account mAccount; + public final String mAuthTokenType; + private final int mHashCode; + + public AuthTokenKey(Account account, String authTokenType) { + mAccount = account; + mAuthTokenType = authTokenType; + mHashCode = computeHashCode(); + } + + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof AuthTokenKey)) { + return false; + } + AuthTokenKey other = (AuthTokenKey)o; + if (!mAccount.equals(other.mAccount)) { + return false; + } + return (mAuthTokenType == null) + ? other.mAuthTokenType == null + : mAuthTokenType.equals(other.mAuthTokenType); + } + + private int computeHashCode() { + int result = 17; + result = 31 * result + mAccount.hashCode(); + result = 31 * result + ((mAuthTokenType == null) ? 0 : mAuthTokenType.hashCode()); + return result; + } + + public int hashCode() { + return mHashCode; + } + } + + public AccountManagerService(Context context) { + mContext = context; + + mOpenHelper = new DatabaseHelper(mContext); + + mMessageThread = new HandlerThread("AccountManagerService"); + mMessageThread.start(); + mMessageHandler = new MessageHandler(mMessageThread.getLooper()); + + mAuthenticatorCache = new AccountAuthenticatorCache(mContext); + mAuthenticatorCache.setListener(this, null /* Handler */); + mBindHelper = new AuthenticatorBindHelper(mContext, mAuthenticatorCache, mMessageHandler, + MESSAGE_CONNECTED, MESSAGE_DISCONNECTED); + + mSimWatcher = new SimWatcher(mContext); + sThis.set(this); + } + + public void onServiceChanged(AuthenticatorDescription desc, boolean removed) { + boolean accountDeleted = false; + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + Cursor cursor = db.query(TABLE_ACCOUNTS, + new String[]{ACCOUNTS_ID, ACCOUNTS_TYPE, ACCOUNTS_NAME}, + ACCOUNTS_TYPE + "=?", new String[]{desc.type}, null, null, null); + try { + while (cursor.moveToNext()) { + final long accountId = cursor.getLong(0); + final String accountType = cursor.getString(1); + final String accountName = cursor.getString(2); + Log.d(TAG, "deleting account " + accountName + " because type " + + accountType + " no longer has a registered authenticator"); + db.delete(TABLE_ACCOUNTS, ACCOUNTS_ID + "=" + accountId, null); + accountDeleted = true; + } + } finally { + cursor.close(); + if (accountDeleted) { + sendAccountsChangedBroadcast(); + } + } + } + + public String getPassword(Account account) { + checkAuthenticateAccountsPermission(account); + + long identityToken = clearCallingIdentity(); + try { + return readPasswordFromDatabase(account); + } finally { + restoreCallingIdentity(identityToken); + } + } + + private String readPasswordFromDatabase(Account account) { + if (account == null) { + return null; + } + + SQLiteDatabase db = mOpenHelper.getReadableDatabase(); + Cursor cursor = db.query(TABLE_ACCOUNTS, new String[]{ACCOUNTS_PASSWORD}, + ACCOUNTS_NAME + "=? AND " + ACCOUNTS_TYPE+ "=?", + new String[]{account.name, account.type}, null, null, null); + try { + if (cursor.moveToNext()) { + return cursor.getString(0); + } + return null; + } finally { + cursor.close(); + } + } + + public String getUserData(Account account, String key) { + checkAuthenticateAccountsPermission(account); + long identityToken = clearCallingIdentity(); + try { + return readUserDataFromDatabase(account, key); + } finally { + restoreCallingIdentity(identityToken); + } + } + + private String readUserDataFromDatabase(Account account, String key) { + if (account == null) { + return null; + } + + SQLiteDatabase db = mOpenHelper.getReadableDatabase(); + Cursor cursor = db.query(TABLE_EXTRAS, new String[]{EXTRAS_VALUE}, + EXTRAS_ACCOUNTS_ID + + "=(select _id FROM accounts WHERE name=? AND type=?) AND " + + EXTRAS_KEY + "=?", + new String[]{account.name, account.type, key}, null, null, null); + try { + if (cursor.moveToNext()) { + return cursor.getString(0); + } + return null; + } finally { + cursor.close(); + } + } + + public AuthenticatorDescription[] getAuthenticatorTypes() { + long identityToken = clearCallingIdentity(); + try { + Collection<AccountAuthenticatorCache.ServiceInfo<AuthenticatorDescription>> + authenticatorCollection = mAuthenticatorCache.getAllServices(); + AuthenticatorDescription[] types = + new AuthenticatorDescription[authenticatorCollection.size()]; + int i = 0; + for (AccountAuthenticatorCache.ServiceInfo<AuthenticatorDescription> authenticator + : authenticatorCollection) { + types[i] = authenticator.type; + i++; + } + return types; + } finally { + restoreCallingIdentity(identityToken); + } + } + + public Account[] getAccountsByType(String accountType) { + SQLiteDatabase db = mOpenHelper.getReadableDatabase(); + + final String selection = accountType == null ? null : (ACCOUNTS_TYPE + "=?"); + final String[] selectionArgs = accountType == null ? null : new String[]{accountType}; + Cursor cursor = db.query(TABLE_ACCOUNTS, ACCOUNT_NAME_TYPE_PROJECTION, + selection, selectionArgs, null, null, null); + try { + int i = 0; + Account[] accounts = new Account[cursor.getCount()]; + while (cursor.moveToNext()) { + accounts[i] = new Account(cursor.getString(1), cursor.getString(2)); + i++; + } + return accounts; + } finally { + cursor.close(); + } + } + + public boolean addAccount(Account account, String password, Bundle extras) { + checkAuthenticateAccountsPermission(account); + + // fails if the account already exists + long identityToken = clearCallingIdentity(); + try { + return insertAccountIntoDatabase(account, password, extras); + } finally { + restoreCallingIdentity(identityToken); + } + } + + private boolean insertAccountIntoDatabase(Account account, String password, Bundle extras) { + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.beginTransaction(); + try { + if (account == null) { + return false; + } + boolean noBroadcast = false; + if (account.type.equals(GOOGLE_ACCOUNT_TYPE)) { + // Look for the 'nobroadcast' flag and remove it since we don't want it to persist + // in the db. + noBroadcast = extras.getBoolean(NO_BROADCAST_FLAG, false); + extras.remove(NO_BROADCAST_FLAG); + } + + long numMatches = DatabaseUtils.longForQuery(db, + "select count(*) from " + TABLE_ACCOUNTS + + " WHERE " + ACCOUNTS_NAME + "=? AND " + ACCOUNTS_TYPE+ "=?", + new String[]{account.name, account.type}); + if (numMatches > 0) { + return false; + } + ContentValues values = new ContentValues(); + values.put(ACCOUNTS_NAME, account.name); + values.put(ACCOUNTS_TYPE, account.type); + values.put(ACCOUNTS_PASSWORD, password); + long accountId = db.insert(TABLE_ACCOUNTS, ACCOUNTS_NAME, values); + if (accountId < 0) { + return false; + } + if (extras != null) { + for (String key : extras.keySet()) { + final String value = extras.getString(key); + if (insertExtra(db, accountId, key, value) < 0) { + return false; + } + } + } + db.setTransactionSuccessful(); + if (!noBroadcast) { + sendAccountsChangedBroadcast(); + } + return true; + } finally { + db.endTransaction(); + } + } + + private long insertExtra(SQLiteDatabase db, long accountId, String key, String value) { + ContentValues values = new ContentValues(); + values.put(EXTRAS_KEY, key); + values.put(EXTRAS_ACCOUNTS_ID, accountId); + values.put(EXTRAS_VALUE, value); + return db.insert(TABLE_EXTRAS, EXTRAS_KEY, values); + } + + public void removeAccount(IAccountManagerResponse response, Account account) { + checkManageAccountsPermission(); + long identityToken = clearCallingIdentity(); + try { + new RemoveAccountSession(response, account).bind(); + } finally { + restoreCallingIdentity(identityToken); + } + } + + private class RemoveAccountSession extends Session { + final Account mAccount; + public RemoveAccountSession(IAccountManagerResponse response, Account account) { + super(response, account.type, false /* expectActivityLaunch */); + mAccount = account; + } + + protected String toDebugString(long now) { + return super.toDebugString(now) + ", removeAccount" + + ", account " + mAccount; + } + + public void run() throws RemoteException { + mAuthenticator.getAccountRemovalAllowed(this, mAccount); + } + + public void onResult(Bundle result) { + if (result != null && result.containsKey(AccountManager.KEY_BOOLEAN_RESULT) + && !result.containsKey(AccountManager.KEY_INTENT)) { + final boolean removalAllowed = result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT); + if (removalAllowed) { + removeAccount(mAccount); + } + IAccountManagerResponse response = getResponseAndClose(); + if (response != null) { + Bundle result2 = new Bundle(); + result2.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, removalAllowed); + try { + response.onResult(result2); + } catch (RemoteException e) { + // ignore + } + } + } + super.onResult(result); + } + } + + private void removeAccount(Account account) { + final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.delete(TABLE_ACCOUNTS, ACCOUNTS_NAME + "=? AND " + ACCOUNTS_TYPE+ "=?", + new String[]{account.name, account.type}); + sendAccountsChangedBroadcast(); + } + + public void invalidateAuthToken(String accountType, String authToken) { + checkManageAccountsPermission(); + long identityToken = clearCallingIdentity(); + try { + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.beginTransaction(); + try { + invalidateAuthToken(db, accountType, authToken); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } finally { + restoreCallingIdentity(identityToken); + } + } + + private void invalidateAuthToken(SQLiteDatabase db, String accountType, String authToken) { + if (authToken == null || accountType == null) { + return; + } + Cursor cursor = db.rawQuery( + "SELECT " + TABLE_AUTHTOKENS + "." + AUTHTOKENS_ID + + ", " + TABLE_ACCOUNTS + "." + ACCOUNTS_NAME + + ", " + TABLE_AUTHTOKENS + "." + AUTHTOKENS_TYPE + + " FROM " + TABLE_ACCOUNTS + + " JOIN " + TABLE_AUTHTOKENS + + " ON " + TABLE_ACCOUNTS + "." + ACCOUNTS_ID + + " = " + AUTHTOKENS_ACCOUNTS_ID + + " WHERE " + AUTHTOKENS_AUTHTOKEN + " = ? AND " + + TABLE_ACCOUNTS + "." + ACCOUNTS_TYPE + " = ?", + new String[]{authToken, accountType}); + try { + while (cursor.moveToNext()) { + long authTokenId = cursor.getLong(0); + String accountName = cursor.getString(1); + String authTokenType = cursor.getString(2); + db.delete(TABLE_AUTHTOKENS, AUTHTOKENS_ID + "=" + authTokenId, null); + } + } finally { + cursor.close(); + } + } + + private boolean saveAuthTokenToDatabase(Account account, String type, String authToken) { + if (account == null || type == null) { + return false; + } + cancelNotification(getSigninRequiredNotificationId(account)); + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.beginTransaction(); + try { + long accountId = getAccountId(db, account); + if (accountId < 0) { + return false; + } + db.delete(TABLE_AUTHTOKENS, + AUTHTOKENS_ACCOUNTS_ID + "=" + accountId + " AND " + AUTHTOKENS_TYPE + "=?", + new String[]{type}); + ContentValues values = new ContentValues(); + values.put(AUTHTOKENS_ACCOUNTS_ID, accountId); + values.put(AUTHTOKENS_TYPE, type); + values.put(AUTHTOKENS_AUTHTOKEN, authToken); + if (db.insert(TABLE_AUTHTOKENS, AUTHTOKENS_AUTHTOKEN, values) >= 0) { + db.setTransactionSuccessful(); + return true; + } + return false; + } finally { + db.endTransaction(); + } + } + + public String readAuthTokenFromDatabase(Account account, String authTokenType) { + if (account == null || authTokenType == null) { + return null; + } + SQLiteDatabase db = mOpenHelper.getReadableDatabase(); + Cursor cursor = db.query(TABLE_AUTHTOKENS, new String[]{AUTHTOKENS_AUTHTOKEN}, + AUTHTOKENS_ACCOUNTS_ID + "=(select _id FROM accounts WHERE name=? AND type=?) AND " + + AUTHTOKENS_TYPE + "=?", + new String[]{account.name, account.type, authTokenType}, + null, null, null); + try { + if (cursor.moveToNext()) { + return cursor.getString(0); + } + return null; + } finally { + cursor.close(); + } + } + + public String peekAuthToken(Account account, String authTokenType) { + checkAuthenticateAccountsPermission(account); + long identityToken = clearCallingIdentity(); + try { + return readAuthTokenFromDatabase(account, authTokenType); + } finally { + restoreCallingIdentity(identityToken); + } + } + + public void setAuthToken(Account account, String authTokenType, String authToken) { + checkAuthenticateAccountsPermission(account); + long identityToken = clearCallingIdentity(); + try { + saveAuthTokenToDatabase(account, authTokenType, authToken); + } finally { + restoreCallingIdentity(identityToken); + } + } + + public void setPassword(Account account, String password) { + checkAuthenticateAccountsPermission(account); + long identityToken = clearCallingIdentity(); + try { + setPasswordInDB(account, password); + } finally { + restoreCallingIdentity(identityToken); + } + } + + private void setPasswordInDB(Account account, String password) { + if (account == null) { + return; + } + ContentValues values = new ContentValues(); + values.put(ACCOUNTS_PASSWORD, password); + mOpenHelper.getWritableDatabase().update(TABLE_ACCOUNTS, values, + ACCOUNTS_NAME + "=? AND " + ACCOUNTS_TYPE+ "=?", + new String[]{account.name, account.type}); + sendAccountsChangedBroadcast(); + } + + private void sendAccountsChangedBroadcast() { + mContext.sendBroadcast(ACCOUNTS_CHANGED_INTENT); + } + + public void clearPassword(Account account) { + checkManageAccountsPermission(); + long identityToken = clearCallingIdentity(); + try { + setPasswordInDB(account, null); + } finally { + restoreCallingIdentity(identityToken); + } + } + + public void setUserData(Account account, String key, String value) { + checkAuthenticateAccountsPermission(account); + long identityToken = clearCallingIdentity(); + if (account == null) { + return; + } + if (account.type.equals(GOOGLE_ACCOUNT_TYPE) && key.equals("broadcast")) { + sendAccountsChangedBroadcast(); + return; + } + try { + writeUserdataIntoDatabase(account, key, value); + } finally { + restoreCallingIdentity(identityToken); + } + } + + private void writeUserdataIntoDatabase(Account account, String key, String value) { + if (account == null || key == null) { + return; + } + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.beginTransaction(); + try { + long accountId = getAccountId(db, account); + if (accountId < 0) { + return; + } + long extrasId = getExtrasId(db, accountId, key); + if (extrasId < 0 ) { + extrasId = insertExtra(db, accountId, key, value); + if (extrasId < 0) { + return; + } + } else { + ContentValues values = new ContentValues(); + values.put(EXTRAS_VALUE, value); + if (1 != db.update(TABLE_EXTRAS, values, EXTRAS_ID + "=" + extrasId, null)) { + return; + } + + } + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + + private void onResult(IAccountManagerResponse response, Bundle result) { + try { + response.onResult(result); + } catch (RemoteException e) { + // if the caller is dead then there is no one to care about remote + // exceptions + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "failure while notifying response", e); + } + } + } + + public void getAuthToken(IAccountManagerResponse response, final Account account, + final String authTokenType, final boolean notifyOnAuthFailure, + final boolean expectActivityLaunch, final Bundle loginOptions) { + checkBinderPermission(Manifest.permission.USE_CREDENTIALS); + final int callerUid = Binder.getCallingUid(); + final boolean permissionGranted = permissionIsGranted(account, authTokenType, callerUid); + + long identityToken = clearCallingIdentity(); + try { + // if the caller has permission, do the peek. otherwise go the more expensive + // route of starting a Session + if (permissionGranted) { + String authToken = readAuthTokenFromDatabase(account, authTokenType); + if (authToken != null) { + Bundle result = new Bundle(); + result.putString(AccountManager.KEY_AUTHTOKEN, authToken); + result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name); + result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type); + onResult(response, result); + return; + } + } + + new Session(response, account.type, expectActivityLaunch) { + protected String toDebugString(long now) { + if (loginOptions != null) loginOptions.keySet(); + return super.toDebugString(now) + ", getAuthToken" + + ", " + account + + ", authTokenType " + authTokenType + + ", loginOptions " + loginOptions + + ", notifyOnAuthFailure " + notifyOnAuthFailure; + } + + public void run() throws RemoteException { + // If the caller doesn't have permission then create and return the + // "grant permission" intent instead of the "getAuthToken" intent. + if (!permissionGranted) { + mAuthenticator.getAuthTokenLabel(this, authTokenType); + } else { + mAuthenticator.getAuthToken(this, account, authTokenType, loginOptions); + } + } + + public void onResult(Bundle result) { + if (result != null) { + if (result.containsKey(AccountManager.KEY_AUTH_TOKEN_LABEL)) { + Intent intent = newGrantCredentialsPermissionIntent(account, callerUid, + new AccountAuthenticatorResponse(this), + authTokenType, + result.getString(AccountManager.KEY_AUTH_TOKEN_LABEL)); + Bundle bundle = new Bundle(); + bundle.putParcelable(AccountManager.KEY_INTENT, intent); + onResult(bundle); + return; + } + String authToken = result.getString(AccountManager.KEY_AUTHTOKEN); + if (authToken != null) { + String name = result.getString(AccountManager.KEY_ACCOUNT_NAME); + String type = result.getString(AccountManager.KEY_ACCOUNT_TYPE); + if (TextUtils.isEmpty(type) || TextUtils.isEmpty(name)) { + onError(AccountManager.ERROR_CODE_INVALID_RESPONSE, + "the type and name should not be empty"); + return; + } + saveAuthTokenToDatabase(new Account(name, type), + authTokenType, authToken); + } + + Intent intent = result.getParcelable(AccountManager.KEY_INTENT); + if (intent != null && notifyOnAuthFailure) { + doNotification( + account, result.getString(AccountManager.KEY_AUTH_FAILED_MESSAGE), + intent); + } + } + super.onResult(result); + } + }.bind(); + } finally { + restoreCallingIdentity(identityToken); + } + } + + private void createNoCredentialsPermissionNotification(Account account, Intent intent) { + int uid = intent.getIntExtra( + GrantCredentialsPermissionActivity.EXTRAS_REQUESTING_UID, -1); + String authTokenType = intent.getStringExtra( + GrantCredentialsPermissionActivity.EXTRAS_AUTH_TOKEN_TYPE); + String authTokenLabel = intent.getStringExtra( + GrantCredentialsPermissionActivity.EXTRAS_AUTH_TOKEN_LABEL); + + Notification n = new Notification(android.R.drawable.stat_sys_warning, null, + 0 /* when */); + final String titleAndSubtitle = + mContext.getString(R.string.permission_request_notification_with_subtitle, + account.name); + final int index = titleAndSubtitle.indexOf('\n'); + final String title = titleAndSubtitle.substring(0, index); + final String subtitle = titleAndSubtitle.substring(index + 1); + n.setLatestEventInfo(mContext, + title, subtitle, + PendingIntent.getActivity(mContext, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT)); + ((NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE)) + .notify(getCredentialPermissionNotificationId(account, authTokenType, uid), n); + } + + private Intent newGrantCredentialsPermissionIntent(Account account, int uid, + AccountAuthenticatorResponse response, String authTokenType, String authTokenLabel) { + RegisteredServicesCache.ServiceInfo<AuthenticatorDescription> serviceInfo = + mAuthenticatorCache.getServiceInfo( + AuthenticatorDescription.newKey(account.type)); + if (serviceInfo == null) { + throw new IllegalArgumentException("unknown account type: " + account.type); + } + + final Context authContext; + try { + authContext = mContext.createPackageContext( + serviceInfo.type.packageName, 0); + } catch (PackageManager.NameNotFoundException e) { + throw new IllegalArgumentException("unknown account type: " + account.type); + } + + Intent intent = new Intent(mContext, GrantCredentialsPermissionActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addCategory( + String.valueOf(getCredentialPermissionNotificationId(account, authTokenType, uid))); + intent.putExtra(GrantCredentialsPermissionActivity.EXTRAS_ACCOUNT, account); + intent.putExtra(GrantCredentialsPermissionActivity.EXTRAS_AUTH_TOKEN_LABEL, authTokenLabel); + intent.putExtra(GrantCredentialsPermissionActivity.EXTRAS_AUTH_TOKEN_TYPE, authTokenType); + intent.putExtra(GrantCredentialsPermissionActivity.EXTRAS_RESPONSE, response); + intent.putExtra(GrantCredentialsPermissionActivity.EXTRAS_ACCOUNT_TYPE_LABEL, + authContext.getString(serviceInfo.type.labelId)); + intent.putExtra(GrantCredentialsPermissionActivity.EXTRAS_PACKAGES, + mContext.getPackageManager().getPackagesForUid(uid)); + intent.putExtra(GrantCredentialsPermissionActivity.EXTRAS_REQUESTING_UID, uid); + return intent; + } + + private Integer getCredentialPermissionNotificationId(Account account, String authTokenType, + int uid) { + Integer id; + synchronized(mCredentialsPermissionNotificationIds) { + final Pair<Pair<Account, String>, Integer> key = + new Pair<Pair<Account, String>, Integer>( + new Pair<Account, String>(account, authTokenType), uid); + id = mCredentialsPermissionNotificationIds.get(key); + if (id == null) { + id = mNotificationIds.incrementAndGet(); + mCredentialsPermissionNotificationIds.put(key, id); + } + } + return id; + } + + private Integer getSigninRequiredNotificationId(Account account) { + Integer id; + synchronized(mSigninRequiredNotificationIds) { + id = mSigninRequiredNotificationIds.get(account); + if (id == null) { + id = mNotificationIds.incrementAndGet(); + mSigninRequiredNotificationIds.put(account, id); + } + } + return id; + } + + + public void addAcount(final IAccountManagerResponse response, final String accountType, + final String authTokenType, final String[] requiredFeatures, + final boolean expectActivityLaunch, final Bundle options) { + checkManageAccountsPermission(); + long identityToken = clearCallingIdentity(); + try { + new Session(response, accountType, expectActivityLaunch) { + public void run() throws RemoteException { + mAuthenticator.addAccount(this, mAccountType, authTokenType, requiredFeatures, + options); + } + + protected String toDebugString(long now) { + return super.toDebugString(now) + ", addAccount" + + ", accountType " + accountType + + ", requiredFeatures " + + (requiredFeatures != null + ? TextUtils.join(",", requiredFeatures) + : null); + } + }.bind(); + } finally { + restoreCallingIdentity(identityToken); + } + } + + public void confirmCredentials(IAccountManagerResponse response, + final Account account, final Bundle options, final boolean expectActivityLaunch) { + checkManageAccountsPermission(); + long identityToken = clearCallingIdentity(); + try { + new Session(response, account.type, expectActivityLaunch) { + public void run() throws RemoteException { + mAuthenticator.confirmCredentials(this, account, options); + } + protected String toDebugString(long now) { + return super.toDebugString(now) + ", confirmCredentials" + + ", " + account; + } + }.bind(); + } finally { + restoreCallingIdentity(identityToken); + } + } + + public void updateCredentials(IAccountManagerResponse response, final Account account, + final String authTokenType, final boolean expectActivityLaunch, + final Bundle loginOptions) { + checkManageAccountsPermission(); + long identityToken = clearCallingIdentity(); + try { + new Session(response, account.type, expectActivityLaunch) { + public void run() throws RemoteException { + mAuthenticator.updateCredentials(this, account, authTokenType, loginOptions); + } + protected String toDebugString(long now) { + if (loginOptions != null) loginOptions.keySet(); + return super.toDebugString(now) + ", updateCredentials" + + ", " + account + + ", authTokenType " + authTokenType + + ", loginOptions " + loginOptions; + } + }.bind(); + } finally { + restoreCallingIdentity(identityToken); + } + } + + public void editProperties(IAccountManagerResponse response, final String accountType, + final boolean expectActivityLaunch) { + checkManageAccountsPermission(); + long identityToken = clearCallingIdentity(); + try { + new Session(response, accountType, expectActivityLaunch) { + public void run() throws RemoteException { + mAuthenticator.editProperties(this, mAccountType); + } + protected String toDebugString(long now) { + return super.toDebugString(now) + ", editProperties" + + ", accountType " + accountType; + } + }.bind(); + } finally { + restoreCallingIdentity(identityToken); + } + } + + private class GetAccountsByTypeAndFeatureSession extends Session { + private final String[] mFeatures; + private volatile Account[] mAccountsOfType = null; + private volatile ArrayList<Account> mAccountsWithFeatures = null; + private volatile int mCurrentAccount = 0; + + public GetAccountsByTypeAndFeatureSession(IAccountManagerResponse response, + String type, String[] features) { + super(response, type, false /* expectActivityLaunch */); + mFeatures = features; + } + + public void run() throws RemoteException { + mAccountsOfType = getAccountsByType(mAccountType); + // check whether each account matches the requested features + mAccountsWithFeatures = new ArrayList<Account>(mAccountsOfType.length); + mCurrentAccount = 0; + + checkAccount(); + } + + public void checkAccount() { + if (mCurrentAccount >= mAccountsOfType.length) { + sendResult(); + return; + } + + try { + mAuthenticator.hasFeatures(this, mAccountsOfType[mCurrentAccount], mFeatures); + } catch (RemoteException e) { + onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION, "remote exception"); + } + } + + public void onResult(Bundle result) { + mNumResults++; + if (result == null) { + onError(AccountManager.ERROR_CODE_INVALID_RESPONSE, "null bundle"); + return; + } + if (result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT, false)) { + mAccountsWithFeatures.add(mAccountsOfType[mCurrentAccount]); + } + mCurrentAccount++; + checkAccount(); + } + + public void sendResult() { + IAccountManagerResponse response = getResponseAndClose(); + if (response != null) { + try { + Account[] accounts = new Account[mAccountsWithFeatures.size()]; + for (int i = 0; i < accounts.length; i++) { + accounts[i] = mAccountsWithFeatures.get(i); + } + Bundle result = new Bundle(); + result.putParcelableArray(AccountManager.KEY_ACCOUNTS, accounts); + response.onResult(result); + } catch (RemoteException e) { + // if the caller is dead then there is no one to care about remote exceptions + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "failure while notifying response", e); + } + } + } + } + + + protected String toDebugString(long now) { + return super.toDebugString(now) + ", getAccountsByTypeAndFeatures" + + ", " + (mFeatures != null ? TextUtils.join(",", mFeatures) : null); + } + } + + public Account[] getAccounts(String type) { + checkReadAccountsPermission(); + long identityToken = clearCallingIdentity(); + try { + return getAccountsByType(type); + } finally { + restoreCallingIdentity(identityToken); + } + } + + public void getAccountsByFeatures(IAccountManagerResponse response, + String type, String[] features) { + checkReadAccountsPermission(); + if (features != null && type == null) { + if (response != null) { + try { + response.onError(AccountManager.ERROR_CODE_BAD_ARGUMENTS, "type is null"); + } catch (RemoteException e) { + // ignore this + } + } + return; + } + long identityToken = clearCallingIdentity(); + try { + if (features == null || features.length == 0) { + getAccountsByType(type); + return; + } + new GetAccountsByTypeAndFeatureSession(response, type, features).bind(); + } finally { + restoreCallingIdentity(identityToken); + } + } + + private long getAccountId(SQLiteDatabase db, Account account) { + Cursor cursor = db.query(TABLE_ACCOUNTS, new String[]{ACCOUNTS_ID}, + "name=? AND type=?", new String[]{account.name, account.type}, null, null, null); + try { + if (cursor.moveToNext()) { + return cursor.getLong(0); + } + return -1; + } finally { + cursor.close(); + } + } + + private long getExtrasId(SQLiteDatabase db, long accountId, String key) { + Cursor cursor = db.query(TABLE_EXTRAS, new String[]{EXTRAS_ID}, + EXTRAS_ACCOUNTS_ID + "=" + accountId + " AND " + EXTRAS_KEY + "=?", + new String[]{key}, null, null, null); + try { + if (cursor.moveToNext()) { + return cursor.getLong(0); + } + return -1; + } finally { + cursor.close(); + } + } + + private abstract class Session extends IAccountAuthenticatorResponse.Stub + implements AuthenticatorBindHelper.Callback, IBinder.DeathRecipient { + IAccountManagerResponse mResponse; + final String mAccountType; + final boolean mExpectActivityLaunch; + final long mCreationTime; + + public int mNumResults = 0; + private int mNumRequestContinued = 0; + private int mNumErrors = 0; + + + IAccountAuthenticator mAuthenticator = null; + + public Session(IAccountManagerResponse response, String accountType, + boolean expectActivityLaunch) { + super(); + if (response == null) throw new IllegalArgumentException("response is null"); + if (accountType == null) throw new IllegalArgumentException("accountType is null"); + mResponse = response; + mAccountType = accountType; + mExpectActivityLaunch = expectActivityLaunch; + mCreationTime = SystemClock.elapsedRealtime(); + synchronized (mSessions) { + mSessions.put(toString(), this); + } + try { + response.asBinder().linkToDeath(this, 0 /* flags */); + } catch (RemoteException e) { + mResponse = null; + binderDied(); + } + } + + IAccountManagerResponse getResponseAndClose() { + if (mResponse == null) { + // this session has already been closed + return null; + } + IAccountManagerResponse response = mResponse; + close(); // this clears mResponse so we need to save the response before this call + return response; + } + + private void close() { + synchronized (mSessions) { + if (mSessions.remove(toString()) == null) { + // the session was already closed, so bail out now + return; + } + } + if (mResponse != null) { + // stop listening for response deaths + mResponse.asBinder().unlinkToDeath(this, 0 /* flags */); + + // clear this so that we don't accidentally send any further results + mResponse = null; + } + cancelTimeout(); + unbind(); + } + + public void binderDied() { + mResponse = null; + close(); + } + + protected String toDebugString() { + return toDebugString(SystemClock.elapsedRealtime()); + } + + protected String toDebugString(long now) { + return "Session: expectLaunch " + mExpectActivityLaunch + + ", connected " + (mAuthenticator != null) + + ", stats (" + mNumResults + "/" + mNumRequestContinued + + "/" + mNumErrors + ")" + + ", lifetime " + ((now - mCreationTime) / 1000.0); + } + + void bind() { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "initiating bind to authenticator type " + mAccountType); + } + if (!mBindHelper.bind(mAccountType, this)) { + Log.d(TAG, "bind attempt failed for " + toDebugString()); + onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION, "bind failure"); + } + } + + private void unbind() { + if (mAuthenticator != null) { + mAuthenticator = null; + mBindHelper.unbind(this); + } + } + + public void scheduleTimeout() { + mMessageHandler.sendMessageDelayed( + mMessageHandler.obtainMessage(MESSAGE_TIMED_OUT, this), TIMEOUT_DELAY_MS); + } + + public void cancelTimeout() { + mMessageHandler.removeMessages(MESSAGE_TIMED_OUT, this); + } + + public void onConnected(IBinder service) { + mAuthenticator = IAccountAuthenticator.Stub.asInterface(service); + try { + run(); + } catch (RemoteException e) { + onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION, + "remote exception"); + } + } + + public abstract void run() throws RemoteException; + + public void onDisconnected() { + mAuthenticator = null; + IAccountManagerResponse response = getResponseAndClose(); + if (response != null) { + onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION, + "disconnected"); + } + } + + public void onTimedOut() { + IAccountManagerResponse response = getResponseAndClose(); + if (response != null) { + onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION, + "timeout"); + } + } + + public void onResult(Bundle result) { + mNumResults++; + if (result != null && !TextUtils.isEmpty(result.getString(AccountManager.KEY_AUTHTOKEN))) { + String accountName = result.getString(AccountManager.KEY_ACCOUNT_NAME); + String accountType = result.getString(AccountManager.KEY_ACCOUNT_TYPE); + if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) { + Account account = new Account(accountName, accountType); + cancelNotification(getSigninRequiredNotificationId(account)); + } + } + IAccountManagerResponse response; + if (mExpectActivityLaunch && result != null + && result.containsKey(AccountManager.KEY_INTENT)) { + response = mResponse; + } else { + response = getResponseAndClose(); + } + if (response != null) { + try { + if (result == null) { + response.onError(AccountManager.ERROR_CODE_INVALID_RESPONSE, + "null bundle returned"); + } else { + response.onResult(result); + } + } catch (RemoteException e) { + // if the caller is dead then there is no one to care about remote exceptions + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "failure while notifying response", e); + } + } + } + } + + public void onRequestContinued() { + mNumRequestContinued++; + } + + public void onError(int errorCode, String errorMessage) { + mNumErrors++; + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "Session.onError: " + errorCode + ", " + errorMessage); + } + IAccountManagerResponse response = getResponseAndClose(); + if (response != null) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "Session.onError: responding"); + } + try { + response.onError(errorCode, errorMessage); + } catch (RemoteException e) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "Session.onError: caught RemoteException while responding", e); + } + } + } else { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "Session.onError: already closed"); + } + } + } + } + + private class MessageHandler extends Handler { + MessageHandler(Looper looper) { + super(looper); + } + + public void handleMessage(Message msg) { + if (mBindHelper.handleMessage(msg)) { + return; + } + switch (msg.what) { + case MESSAGE_TIMED_OUT: + Session session = (Session)msg.obj; + session.onTimedOut(); + break; + + default: + throw new IllegalStateException("unhandled message: " + msg.what); + } + } + } + + private class DatabaseHelper extends SQLiteOpenHelper { + public DatabaseHelper(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL("CREATE TABLE " + TABLE_ACCOUNTS + " ( " + + ACCOUNTS_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + + ACCOUNTS_NAME + " TEXT NOT NULL, " + + ACCOUNTS_TYPE + " TEXT NOT NULL, " + + ACCOUNTS_PASSWORD + " TEXT, " + + "UNIQUE(" + ACCOUNTS_NAME + "," + ACCOUNTS_TYPE + "))"); + + db.execSQL("CREATE TABLE " + TABLE_AUTHTOKENS + " ( " + + AUTHTOKENS_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + + AUTHTOKENS_ACCOUNTS_ID + " INTEGER NOT NULL, " + + AUTHTOKENS_TYPE + " TEXT NOT NULL, " + + AUTHTOKENS_AUTHTOKEN + " TEXT, " + + "UNIQUE (" + AUTHTOKENS_ACCOUNTS_ID + "," + AUTHTOKENS_TYPE + "))"); + + createGrantsTable(db); + + db.execSQL("CREATE TABLE " + TABLE_EXTRAS + " ( " + + EXTRAS_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + + EXTRAS_ACCOUNTS_ID + " INTEGER, " + + EXTRAS_KEY + " TEXT NOT NULL, " + + EXTRAS_VALUE + " TEXT, " + + "UNIQUE(" + EXTRAS_ACCOUNTS_ID + "," + EXTRAS_KEY + "))"); + + db.execSQL("CREATE TABLE " + TABLE_META + " ( " + + META_KEY + " TEXT PRIMARY KEY NOT NULL, " + + META_VALUE + " TEXT)"); + + createAccountsDeletionTrigger(db); + } + + private void createAccountsDeletionTrigger(SQLiteDatabase db) { + db.execSQL("" + + " CREATE TRIGGER " + TABLE_ACCOUNTS + "Delete DELETE ON " + TABLE_ACCOUNTS + + " BEGIN" + + " DELETE FROM " + TABLE_AUTHTOKENS + + " WHERE " + AUTHTOKENS_ACCOUNTS_ID + "=OLD." + ACCOUNTS_ID + " ;" + + " DELETE FROM " + TABLE_EXTRAS + + " WHERE " + EXTRAS_ACCOUNTS_ID + "=OLD." + ACCOUNTS_ID + " ;" + + " DELETE FROM " + TABLE_GRANTS + + " WHERE " + GRANTS_ACCOUNTS_ID + "=OLD." + ACCOUNTS_ID + " ;" + + " END"); + } + + private void createGrantsTable(SQLiteDatabase db) { + db.execSQL("CREATE TABLE " + TABLE_GRANTS + " ( " + + GRANTS_ACCOUNTS_ID + " INTEGER NOT NULL, " + + GRANTS_AUTH_TOKEN_TYPE + " STRING NOT NULL, " + + GRANTS_GRANTEE_UID + " INTEGER NOT NULL, " + + "UNIQUE (" + GRANTS_ACCOUNTS_ID + "," + GRANTS_AUTH_TOKEN_TYPE + + "," + GRANTS_GRANTEE_UID + "))"); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + Log.e(TAG, "upgrade from version " + oldVersion + " to version " + newVersion); + + if (oldVersion == 1) { + // no longer need to do anything since the work is done + // when upgrading from version 2 + oldVersion++; + } + + if (oldVersion == 2) { + createGrantsTable(db); + db.execSQL("DROP TRIGGER " + TABLE_ACCOUNTS + "Delete"); + createAccountsDeletionTrigger(db); + oldVersion++; + } + + if (oldVersion == 3) { + db.execSQL("UPDATE " + TABLE_ACCOUNTS + " SET " + ACCOUNTS_TYPE + + " = 'com.google' WHERE " + ACCOUNTS_TYPE + " == 'com.google.GAIA'"); + oldVersion++; + } + } + + @Override + public void onOpen(SQLiteDatabase db) { + if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "opened database " + DATABASE_NAME); + } + } + + private void setMetaValue(String key, String value) { + ContentValues values = new ContentValues(); + values.put(META_KEY, key); + values.put(META_VALUE, value); + mOpenHelper.getWritableDatabase().replace(TABLE_META, META_KEY, values); + } + + private String getMetaValue(String key) { + Cursor c = mOpenHelper.getReadableDatabase().query(TABLE_META, + new String[]{META_VALUE}, META_KEY + "=?", new String[]{key}, null, null, null); + try { + if (c.moveToNext()) { + return c.getString(0); + } + return null; + } finally { + c.close(); + } + } + + private class SimWatcher extends BroadcastReceiver { + public SimWatcher(Context context) { + // Re-scan the SIM card when the SIM state changes, and also if + // the disk recovers from a full state (we may have failed to handle + // things properly while the disk was full). + final IntentFilter filter = new IntentFilter(); + filter.addAction(TelephonyIntents.ACTION_SIM_STATE_CHANGED); + filter.addAction(Intent.ACTION_DEVICE_STORAGE_OK); + context.registerReceiver(this, filter); + } + + /** + * Compare the IMSI to the one stored in the login service's + * database. If they differ, erase all passwords and + * authtokens (and store the new IMSI). + */ + @Override + public void onReceive(Context context, Intent intent) { + // Check IMSI on every update; nothing happens if the IMSI is missing or unchanged. + String imsi = ((TelephonyManager) context.getSystemService( + Context.TELEPHONY_SERVICE)).getSubscriberId(); + if (TextUtils.isEmpty(imsi)) return; + + String storedImsi = getMetaValue("imsi"); + + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "current IMSI=" + imsi + "; stored IMSI=" + storedImsi); + } + + if (!imsi.equals(storedImsi) && !TextUtils.isEmpty(storedImsi)) { + Log.w(TAG, "wiping all passwords and authtokens because IMSI changed (" + + "stored=" + storedImsi + ", current=" + imsi + ")"); + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.beginTransaction(); + try { + db.execSQL("DELETE from " + TABLE_AUTHTOKENS); + db.execSQL("UPDATE " + TABLE_ACCOUNTS + " SET " + ACCOUNTS_PASSWORD + " = ''"); + sendAccountsChangedBroadcast(); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + setMetaValue("imsi", imsi); + } + } + + public IBinder onBind(Intent intent) { + return asBinder(); + } + + /** + * Searches array of arguments for the specified string + * @param args array of argument strings + * @param value value to search for + * @return true if the value is contained in the array + */ + private static boolean scanArgs(String[] args, String value) { + if (args != null) { + for (String arg : args) { + if (value.equals(arg)) { + return true; + } + } + } + return false; + } + + protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) { + final boolean isCheckinRequest = scanArgs(args, "--checkin") || scanArgs(args, "-c"); + + if (isCheckinRequest) { + // This is a checkin request. *Only* upload the account types and the count of each. + SQLiteDatabase db = mOpenHelper.getReadableDatabase(); + + Cursor cursor = db.query(TABLE_ACCOUNTS, ACCOUNT_TYPE_COUNT_PROJECTION, + null, null, ACCOUNTS_TYPE, null, null); + try { + while (cursor.moveToNext()) { + // print type,count + fout.println(cursor.getString(0) + "," + cursor.getString(1)); + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + } else { + synchronized (mSessions) { + final long now = SystemClock.elapsedRealtime(); + fout.println("AccountManagerService: " + mSessions.size() + " sessions"); + for (Session session : mSessions.values()) { + fout.println(" " + session.toDebugString(now)); + } + } + + fout.println(); + mAuthenticatorCache.dump(fd, fout, args); + } + } + + private void doNotification(Account account, CharSequence message, Intent intent) { + long identityToken = clearCallingIdentity(); + try { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "doNotification: " + message + " intent:" + intent); + } + + if (intent.getComponent() != null && + GrantCredentialsPermissionActivity.class.getName().equals( + intent.getComponent().getClassName())) { + createNoCredentialsPermissionNotification(account, intent); + } else { + final Integer notificationId = getSigninRequiredNotificationId(account); + intent.addCategory(String.valueOf(notificationId)); + Notification n = new Notification(android.R.drawable.stat_sys_warning, null, + 0 /* when */); + final String notificationTitleFormat = + mContext.getText(R.string.notification_title).toString(); + n.setLatestEventInfo(mContext, + String.format(notificationTitleFormat, account.name), + message, PendingIntent.getActivity( + mContext, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT)); + ((NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE)) + .notify(notificationId, n); + } + } finally { + restoreCallingIdentity(identityToken); + } + } + + private void cancelNotification(int id) { + long identityToken = clearCallingIdentity(); + try { + ((NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE)) + .cancel(id); + } finally { + restoreCallingIdentity(identityToken); + } + } + + private void checkBinderPermission(String permission) { + final int uid = Binder.getCallingUid(); + if (mContext.checkCallingOrSelfPermission(permission) != + PackageManager.PERMISSION_GRANTED) { + String msg = "caller uid " + uid + " lacks " + permission; + Log.w(TAG, msg); + throw new SecurityException(msg); + } + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "caller uid " + uid + " has " + permission); + } + } + + private boolean inSystemImage(int callerUid) { + String[] packages = mContext.getPackageManager().getPackagesForUid(callerUid); + for (String name : packages) { + try { + PackageInfo packageInfo = + mContext.getPackageManager().getPackageInfo(name, 0 /* flags */); + if ((packageInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { + return true; + } + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } + return false; + } + + private boolean permissionIsGranted(Account account, String authTokenType, int callerUid) { + final boolean fromAuthenticator = account != null + && hasAuthenticatorUid(account.type, callerUid); + final boolean hasExplicitGrants = account != null + && hasExplicitlyGrantedPermission(account, authTokenType); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "checkGrantsOrCallingUidAgainstAuthenticator: caller uid " + + callerUid + ", account " + account + + ": is authenticator? " + fromAuthenticator + + ", has explicit permission? " + hasExplicitGrants); + } + return fromAuthenticator || hasExplicitGrants || inSystemImage(callerUid); + } + + private boolean hasAuthenticatorUid(String accountType, int callingUid) { + for (RegisteredServicesCache.ServiceInfo<AuthenticatorDescription> serviceInfo : + mAuthenticatorCache.getAllServices()) { + if (serviceInfo.type.type.equals(accountType)) { + return (serviceInfo.uid == callingUid) || + (mContext.getPackageManager().checkSignatures(serviceInfo.uid, callingUid) + == PackageManager.SIGNATURE_MATCH); + } + } + return false; + } + + private boolean hasExplicitlyGrantedPermission(Account account, String authTokenType) { + if (Binder.getCallingUid() == android.os.Process.SYSTEM_UID) { + return true; + } + SQLiteDatabase db = mOpenHelper.getReadableDatabase(); + String[] args = {String.valueOf(Binder.getCallingUid()), authTokenType, + account.name, account.type}; + final boolean permissionGranted = + DatabaseUtils.longForQuery(db, COUNT_OF_MATCHING_GRANTS, args) != 0; + if (!permissionGranted && isDebuggableMonkeyBuild) { + // TODO: Skip this check when running automated tests. Replace this + // with a more general solution. + Log.w(TAG, "no credentials permission for usage of " + account + ", " + + authTokenType + " by uid " + Binder.getCallingUid() + + " but ignoring since this is a monkey build"); + return true; + } + return permissionGranted; + } + + private void checkCallingUidAgainstAuthenticator(Account account) { + final int uid = Binder.getCallingUid(); + if (account == null || !hasAuthenticatorUid(account.type, uid)) { + String msg = "caller uid " + uid + " is different than the authenticator's uid"; + Log.w(TAG, msg); + throw new SecurityException(msg); + } + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "caller uid " + uid + " is the same as the authenticator's uid"); + } + } + + private void checkAuthenticateAccountsPermission(Account account) { + checkBinderPermission(Manifest.permission.AUTHENTICATE_ACCOUNTS); + checkCallingUidAgainstAuthenticator(account); + } + + private void checkReadAccountsPermission() { + checkBinderPermission(Manifest.permission.GET_ACCOUNTS); + } + + private void checkManageAccountsPermission() { + checkBinderPermission(Manifest.permission.MANAGE_ACCOUNTS); + } + + /** + * Allow callers with the given uid permission to get credentials for account/authTokenType. + * <p> + * Although this is public it can only be accessed via the AccountManagerService object + * which is in the system. This means we don't need to protect it with permissions. + * @hide + */ + public void grantAppPermission(Account account, String authTokenType, int uid) { + if (account == null || authTokenType == null) { + return; + } + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.beginTransaction(); + try { + long accountId = getAccountId(db, account); + if (accountId >= 0) { + ContentValues values = new ContentValues(); + values.put(GRANTS_ACCOUNTS_ID, accountId); + values.put(GRANTS_AUTH_TOKEN_TYPE, authTokenType); + values.put(GRANTS_GRANTEE_UID, uid); + db.insert(TABLE_GRANTS, GRANTS_ACCOUNTS_ID, values); + db.setTransactionSuccessful(); + } + } finally { + db.endTransaction(); + } + cancelNotification(getCredentialPermissionNotificationId(account, authTokenType, uid)); + } + + /** + * Don't allow callers with the given uid permission to get credentials for + * account/authTokenType. + * <p> + * Although this is public it can only be accessed via the AccountManagerService object + * which is in the system. This means we don't need to protect it with permissions. + * @hide + */ + public void revokeAppPermission(Account account, String authTokenType, int uid) { + if (account == null || authTokenType == null) { + return; + } + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.beginTransaction(); + try { + long accountId = getAccountId(db, account); + if (accountId >= 0) { + db.delete(TABLE_GRANTS, + GRANTS_ACCOUNTS_ID + "=? AND " + GRANTS_AUTH_TOKEN_TYPE + "=? AND " + + GRANTS_GRANTEE_UID + "=?", + new String[]{String.valueOf(accountId), authTokenType, + String.valueOf(uid)}); + db.setTransactionSuccessful(); + } + } finally { + db.endTransaction(); + } + cancelNotification(getCredentialPermissionNotificationId(account, authTokenType, uid)); + } +} diff --git a/core/java/android/accounts/AccountMonitor.java b/core/java/android/accounts/AccountMonitor.java deleted file mode 100644 index f21385e..0000000 --- a/core/java/android/accounts/AccountMonitor.java +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.accounts; - -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.ServiceConnection; -import android.database.SQLException; -import android.os.IBinder; -import android.os.Process; -import android.os.RemoteException; -import android.util.Log; - -/** - * A helper class that calls back on the provided - * AccountMonitorListener with the set of current accounts both when - * it gets created and whenever the set changes. It does this by - * binding to the AccountsService and registering to receive the - * intent broadcast when the set of accounts is changed. The - * connection to the accounts service is only made when it needs to - * fetch the current list of accounts (that is, when the - * AccountMonitor is first created, and when the intent is received). - */ -public class AccountMonitor extends BroadcastReceiver implements ServiceConnection { - private final Context mContext; - private final AccountMonitorListener mListener; - private boolean mClosed = false; - private int pending = 0; - - // This thread runs in the background and runs the code to update accounts - // in the listener. - private class AccountUpdater extends Thread { - private IBinder mService; - - public AccountUpdater(IBinder service) { - mService = service; - } - - @Override - public void run() { - Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); - IAccountsService accountsService = IAccountsService.Stub.asInterface(mService); - String[] accounts = null; - do { - try { - accounts = accountsService.getAccounts(); - } catch (RemoteException e) { - // if the service was killed then the system will restart it and when it does we - // will get another onServiceConnected, at which point we will do a notify. - Log.w("AccountMonitor", "Remote exception when getting accounts", e); - return; - } - - synchronized (AccountMonitor.this) { - --pending; - if (pending == 0) { - break; - } - } - } while (true); - - mContext.unbindService(AccountMonitor.this); - - try { - mListener.onAccountsUpdated(accounts); - } catch (SQLException e) { - // Better luck next time. If the problem was disk-full, - // the STORAGE_OK intent will re-trigger the update. - Log.e("AccountMonitor", "Can't update accounts", e); - } - } - } - - /** - * Initializes the AccountMonitor and initiates a bind to the - * AccountsService to get the initial account list. For 1.0, - * the "list" is always a single account. - * - * @param context the context we are running in - * @param listener the user to notify when the account set changes - */ - public AccountMonitor(Context context, AccountMonitorListener listener) { - if (listener == null) { - throw new IllegalArgumentException("listener is null"); - } - - mContext = context; - mListener = listener; - - // Register a broadcast receiver to monitor account changes - IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(AccountsServiceConstants.LOGIN_ACCOUNTS_CHANGED_ACTION); - intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_OK); // To recover from disk-full. - mContext.registerReceiver(this, intentFilter); - - // Send the listener the initial state now. - notifyListener(); - } - - @Override - public void onReceive(Context context, Intent intent) { - notifyListener(); - } - - public void onServiceConnected(ComponentName className, IBinder service) { - // Create a background thread to update the accounts. - new AccountUpdater(service).start(); - } - - public void onServiceDisconnected(ComponentName className) { - } - - private synchronized void notifyListener() { - if (pending == 0) { - // initiate the bind - if (!mContext.bindService(AccountsServiceConstants.SERVICE_INTENT, - this, Context.BIND_AUTO_CREATE)) { - // This is normal if GLS isn't part of this build. - Log.w("AccountMonitor", - "Couldn't connect to " + - AccountsServiceConstants.SERVICE_INTENT + - " (Missing service?)"); - } - } else { - // already bound. bindService will not trigger another - // call to onServiceConnected, so instead we make sure - // that the existing background thread will call - // getAccounts() after this function returns, by - // incrementing pending. - // - // Yes, this else clause contains only a comment. - } - ++pending; - } - - /** - * calls close() - * @throws Throwable - */ - @Override - protected void finalize() throws Throwable { - close(); - super.finalize(); - } - - /** - * Unregisters the account receiver. Consecutive calls to this - * method are harmless, but also do nothing. Once this call is - * made no more notifications will occur. - */ - public synchronized void close() { - if (!mClosed) { - mContext.unregisterReceiver(this); - mClosed = true; - } - } -} diff --git a/core/java/android/accounts/AccountsException.java b/core/java/android/accounts/AccountsException.java new file mode 100644 index 0000000..b997390 --- /dev/null +++ b/core/java/android/accounts/AccountsException.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.accounts; + +public class AccountsException extends Exception { + public AccountsException() { + super(); + } + public AccountsException(String message) { + super(message); + } + public AccountsException(String message, Throwable cause) { + super(message, cause); + } + public AccountsException(Throwable cause) { + super(cause); + } +}
\ No newline at end of file diff --git a/core/java/android/accounts/AccountsServiceConstants.java b/core/java/android/accounts/AccountsServiceConstants.java deleted file mode 100644 index b882e7b..0000000 --- a/core/java/android/accounts/AccountsServiceConstants.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (C) 2008 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.accounts; - -import android.content.Intent; - -/** - * Miscellaneous constants used by the AccountsService and its - * clients. - */ -// TODO: These constants *could* come directly from the -// IAccountsService interface, but that's not possible since the -// aidl compiler doesn't let you define constants (yet.) -public class AccountsServiceConstants { - /** This class is never instantiated. */ - private AccountsServiceConstants() { - } - - /** - * Action sent as a broadcast Intent by the AccountsService - * when accounts are added to and/or removed from the device's - * database, or when the primary account is changed. - */ - public static final String LOGIN_ACCOUNTS_CHANGED_ACTION = - "android.accounts.LOGIN_ACCOUNTS_CHANGED"; - - /** - * Action sent as a broadcast Intent by the AccountsService - * when it starts up and no accounts are available (so some should be added). - */ - public static final String LOGIN_ACCOUNTS_MISSING_ACTION = - "android.accounts.LOGIN_ACCOUNTS_MISSING"; - - /** - * Action on the intent used to bind to the IAccountsService interface. This - * is used for services that have multiple interfaces (allowing - * them to differentiate the interface intended, and return the proper - * Binder.) - */ - private static final String ACCOUNTS_SERVICE_ACTION = "android.accounts.IAccountsService"; - - /* - * The intent uses a component in addition to the action to ensure the actual - * accounts service is bound to (a malicious third-party app could - * theoretically have a service with the same action). - */ - /** The intent used to bind to the accounts service. */ - public static final Intent SERVICE_INTENT = - new Intent() - .setClassName("com.google.android.googleapps", - "com.google.android.googleapps.GoogleLoginService") - .setAction(ACCOUNTS_SERVICE_ACTION); - - /** - * Checks whether the intent is to bind to the accounts service. - * - * @param bindIntent The Intent used to bind to the service. - * @return Whether the intent is to bind to the accounts service. - */ - public static final boolean isForAccountsService(Intent bindIntent) { - String otherAction = bindIntent.getAction(); - return otherAction != null && otherAction.equals(ACCOUNTS_SERVICE_ACTION); - } -} diff --git a/core/java/android/accounts/AuthenticatorBindHelper.java b/core/java/android/accounts/AuthenticatorBindHelper.java new file mode 100644 index 0000000..2ca1f0e --- /dev/null +++ b/core/java/android/accounts/AuthenticatorBindHelper.java @@ -0,0 +1,258 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.accounts; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Map; + +import com.google.android.collect.Lists; +import com.google.android.collect.Maps; + +/** + * A helper object that simplifies binding to Account Authenticators. It uses the + * {@link AccountAuthenticatorCache} to find the component name of the authenticators, + * allowing the user to bind by account name. It also allows multiple, simultaneous binds + * to the same authenticator, with each bind call guaranteed to return either + * {@link Callback#onConnected} or {@link Callback#onDisconnected} if the bind() call + * itself succeeds, even if the authenticator is already bound internally. + * @hide + */ +public class AuthenticatorBindHelper { + private static final String TAG = "Accounts"; + private final Handler mHandler; + private final Context mContext; + private final int mMessageWhatConnected; + private final int mMessageWhatDisconnected; + private final Map<String, MyServiceConnection> mServiceConnections = Maps.newHashMap(); + private final Map<String, ArrayList<Callback>> mServiceUsers = Maps.newHashMap(); + private final AccountAuthenticatorCache mAuthenticatorCache; + + public AuthenticatorBindHelper(Context context, + AccountAuthenticatorCache authenticatorCache, Handler handler, + int messageWhatConnected, int messageWhatDisconnected) { + mContext = context; + mHandler = handler; + mAuthenticatorCache = authenticatorCache; + mMessageWhatConnected = messageWhatConnected; + mMessageWhatDisconnected = messageWhatDisconnected; + } + + public interface Callback { + void onConnected(IBinder service); + void onDisconnected(); + } + + public boolean bind(String authenticatorType, Callback callback) { + // if the authenticator is connecting or connected then return true + synchronized (mServiceConnections) { + if (mServiceConnections.containsKey(authenticatorType)) { + MyServiceConnection connection = mServiceConnections.get(authenticatorType); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "service connection already exists for " + authenticatorType); + } + mServiceUsers.get(authenticatorType).add(callback); + if (connection.mService != null) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "the service is connected, scheduling a connected message for " + + authenticatorType); + } + connection.scheduleCallbackConnectedMessage(callback); + } else { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "the service is *not* connected, waiting for for " + + authenticatorType); + } + } + return true; + } + + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "there is no service connection for " + authenticatorType); + } + + // otherwise find the component name for the authenticator and initiate a bind + // if no authenticator or the bind fails then return false, otherwise return true + AccountAuthenticatorCache.ServiceInfo<AuthenticatorDescription> authenticatorInfo = + mAuthenticatorCache.getServiceInfo( + AuthenticatorDescription.newKey(authenticatorType)); + if (authenticatorInfo == null) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "there is no authenticator for " + authenticatorType + + ", bailing out"); + } + return false; + } + + MyServiceConnection connection = new MyServiceConnection(authenticatorType); + + Intent intent = new Intent(); + intent.setAction("android.accounts.AccountAuthenticator"); + intent.setComponent(authenticatorInfo.componentName); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "performing bindService to " + authenticatorInfo.componentName); + } + if (!mContext.bindService(intent, connection, Context.BIND_AUTO_CREATE)) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "bindService to " + authenticatorInfo.componentName + " failed"); + } + return false; + } + + mServiceConnections.put(authenticatorType, connection); + mServiceUsers.put(authenticatorType, Lists.newArrayList(callback)); + return true; + } + } + + public void unbind(Callback callbackToUnbind) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "unbinding callback " + callbackToUnbind); + } + synchronized (mServiceConnections) { + for (Map.Entry<String, ArrayList<Callback>> entry : mServiceUsers.entrySet()) { + final String authenticatorType = entry.getKey(); + final ArrayList<Callback> serviceUsers = entry.getValue(); + for (Callback callback : serviceUsers) { + if (callback == callbackToUnbind) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "found callback in service" + authenticatorType); + } + serviceUsers.remove(callbackToUnbind); + if (serviceUsers.isEmpty()) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "there are no more callbacks for service " + + authenticatorType + ", unbinding service"); + } + unbindFromServiceLocked(authenticatorType); + } else { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "leaving service " + authenticatorType + + " around since there are still callbacks using it"); + } + } + return; + } + } + } + Log.e(TAG, "did not find callback " + callbackToUnbind + " in any of the services"); + } + } + + /** + * You must synchronized on mServiceConnections before calling this + */ + private void unbindFromServiceLocked(String authenticatorType) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "unbindService from " + authenticatorType); + } + mContext.unbindService(mServiceConnections.get(authenticatorType)); + mServiceUsers.remove(authenticatorType); + mServiceConnections.remove(authenticatorType); + } + + private class ConnectedMessagePayload { + public final IBinder mService; + public final Callback mCallback; + public ConnectedMessagePayload(IBinder service, Callback callback) { + mService = service; + mCallback = callback; + } + } + + private class MyServiceConnection implements ServiceConnection { + private final String mAuthenticatorType; + private IBinder mService = null; + + public MyServiceConnection(String authenticatorType) { + mAuthenticatorType = authenticatorType; + } + + public void onServiceConnected(ComponentName name, IBinder service) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "onServiceConnected for account type " + mAuthenticatorType); + } + // post a message for each service user to tell them that the service is connected + synchronized (mServiceConnections) { + mService = service; + for (Callback callback : mServiceUsers.get(mAuthenticatorType)) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "the service became connected, scheduling a connected " + + "message for " + mAuthenticatorType); + } + scheduleCallbackConnectedMessage(callback); + } + } + } + + private void scheduleCallbackConnectedMessage(Callback callback) { + final ConnectedMessagePayload payload = + new ConnectedMessagePayload(mService, callback); + mHandler.obtainMessage(mMessageWhatConnected, payload).sendToTarget(); + } + + public void onServiceDisconnected(ComponentName name) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "onServiceDisconnected for account type " + mAuthenticatorType); + } + // post a message for each service user to tell them that the service is disconnected, + // and unbind from the service. + synchronized (mServiceConnections) { + final ArrayList<Callback> callbackList = mServiceUsers.get(mAuthenticatorType); + if (callbackList != null) { + for (Callback callback : callbackList) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "the service became disconnected, scheduling a " + + "disconnected message for " + + mAuthenticatorType); + } + mHandler.obtainMessage(mMessageWhatDisconnected, callback).sendToTarget(); + } + unbindFromServiceLocked(mAuthenticatorType); + } + } + } + } + + boolean handleMessage(Message message) { + if (message.what == mMessageWhatConnected) { + ConnectedMessagePayload payload = (ConnectedMessagePayload)message.obj; + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "notifying callback " + payload.mCallback + " that it is connected"); + } + payload.mCallback.onConnected(payload.mService); + return true; + } else if (message.what == mMessageWhatDisconnected) { + Callback callback = (Callback)message.obj; + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "notifying callback " + callback + " that it is disconnected"); + } + callback.onDisconnected(); + return true; + } else { + return false; + } + } +} diff --git a/core/java/android/accounts/AuthenticatorDescription.aidl b/core/java/android/accounts/AuthenticatorDescription.aidl new file mode 100644 index 0000000..136361c --- /dev/null +++ b/core/java/android/accounts/AuthenticatorDescription.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.accounts; + +parcelable AuthenticatorDescription; diff --git a/core/java/android/accounts/AuthenticatorDescription.java b/core/java/android/accounts/AuthenticatorDescription.java new file mode 100644 index 0000000..91c94e6 --- /dev/null +++ b/core/java/android/accounts/AuthenticatorDescription.java @@ -0,0 +1,117 @@ +package android.accounts; + +import android.os.Parcelable; +import android.os.Parcel; + +/** + * A {@link Parcelable} value type that contains information about an account authenticator. + */ +public class AuthenticatorDescription implements Parcelable { + /** The string that uniquely identifies an authenticator */ + final public String type; + + /** A resource id of a label for the authenticator that is suitable for displaying */ + final public int labelId; + + /** A resource id of a icon for the authenticator */ + final public int iconId; + + /** A resource id of a smaller icon for the authenticator */ + final public int smallIconId; + + /** + * A resource id for a hierarchy of PreferenceScreen to be added to the settings page for the + * account. See {@link AbstractAccountAuthenticator} for an example. + */ + final public int accountPreferencesId; + + /** The package name that can be used to lookup the resources from above. */ + final public String packageName; + + /** A constructor for a full AuthenticatorDescription */ + public AuthenticatorDescription(String type, String packageName, int labelId, int iconId, + int smallIconId, int prefId) { + if (type == null) throw new IllegalArgumentException("type cannot be null"); + if (packageName == null) throw new IllegalArgumentException("packageName cannot be null"); + this.type = type; + this.packageName = packageName; + this.labelId = labelId; + this.iconId = iconId; + this.smallIconId = smallIconId; + this.accountPreferencesId = prefId; + } + + /** + * A factory method for creating an AuthenticatorDescription that can be used as a key + * to identify the authenticator by its type. + */ + + public static AuthenticatorDescription newKey(String type) { + if (type == null) throw new IllegalArgumentException("type cannot be null"); + return new AuthenticatorDescription(type); + } + + private AuthenticatorDescription(String type) { + this.type = type; + this.packageName = null; + this.labelId = 0; + this.iconId = 0; + this.smallIconId = 0; + this.accountPreferencesId = 0; + } + + private AuthenticatorDescription(Parcel source) { + this.type = source.readString(); + this.packageName = source.readString(); + this.labelId = source.readInt(); + this.iconId = source.readInt(); + this.smallIconId = source.readInt(); + this.accountPreferencesId = source.readInt(); + } + + /** @inheritDoc */ + public int describeContents() { + return 0; + } + + /** Returns the hashcode of the type only. */ + public int hashCode() { + return type.hashCode(); + } + + /** Compares the type only, suitable for key comparisons. */ + public boolean equals(Object o) { + if (o == this) return true; + if (!(o instanceof AuthenticatorDescription)) return false; + final AuthenticatorDescription other = (AuthenticatorDescription) o; + return type.equals(other.type); + } + + public String toString() { + return "AuthenticatorDescription {type=" + type + "}"; + } + + /** @inhericDoc */ + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(type); + dest.writeString(packageName); + dest.writeInt(labelId); + dest.writeInt(iconId); + dest.writeInt(smallIconId); + dest.writeInt(accountPreferencesId); + } + + /** Used to create the object from a parcel. */ + public static final Creator<AuthenticatorDescription> CREATOR = + new Creator<AuthenticatorDescription>() { + /** @inheritDoc */ + public AuthenticatorDescription createFromParcel(Parcel source) { + return new AuthenticatorDescription(source); + } + + /** @inheritDoc */ + public AuthenticatorDescription[] newArray(int size) { + return new AuthenticatorDescription[size]; + } + }; +} diff --git a/core/java/android/accounts/AuthenticatorException.java b/core/java/android/accounts/AuthenticatorException.java new file mode 100644 index 0000000..f778d7d --- /dev/null +++ b/core/java/android/accounts/AuthenticatorException.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.accounts; + +public class AuthenticatorException extends AccountsException { + public AuthenticatorException() { + super(); + } + public AuthenticatorException(String message) { + super(message); + } + public AuthenticatorException(String message, Throwable cause) { + super(message, cause); + } + public AuthenticatorException(Throwable cause) { + super(cause); + } +} diff --git a/core/java/android/accounts/ChooseAccountActivity.java b/core/java/android/accounts/ChooseAccountActivity.java new file mode 100644 index 0000000..4a0018e --- /dev/null +++ b/core/java/android/accounts/ChooseAccountActivity.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.accounts; + +import android.app.ListActivity; +import android.os.Bundle; +import android.os.Parcelable; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import android.view.View; +import android.util.Log; + +/** + * @hide + */ +public class ChooseAccountActivity extends ListActivity { + private static final String TAG = "AccountManager"; + private Parcelable[] mAccounts = null; + private AccountManagerResponse mAccountManagerResponse = null; + private Bundle mResult; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (savedInstanceState == null) { + mAccounts = getIntent().getParcelableArrayExtra(AccountManager.KEY_ACCOUNTS); + mAccountManagerResponse = + getIntent().getParcelableExtra(AccountManager.KEY_ACCOUNT_MANAGER_RESPONSE); + } else { + mAccounts = savedInstanceState.getParcelableArray(AccountManager.KEY_ACCOUNTS); + mAccountManagerResponse = + savedInstanceState.getParcelable(AccountManager.KEY_ACCOUNT_MANAGER_RESPONSE); + } + + String[] mAccountNames = new String[mAccounts.length]; + for (int i = 0; i < mAccounts.length; i++) { + mAccountNames[i] = ((Account) mAccounts[i]).name; + } + + // Use an existing ListAdapter that will map an array + // of strings to TextViews + setListAdapter(new ArrayAdapter<String>(this, + android.R.layout.simple_list_item_1, mAccountNames)); + getListView().setTextFilterEnabled(true); + } + + protected void onListItemClick(ListView l, View v, int position, long id) { + Account account = (Account) mAccounts[position]; + Log.d(TAG, "selected account " + account); + Bundle bundle = new Bundle(); + bundle.putString(AccountManager.KEY_ACCOUNT_NAME, account.name); + bundle.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type); + mResult = bundle; + finish(); + } + + public void finish() { + if (mAccountManagerResponse != null) { + if (mResult != null) { + mAccountManagerResponse.onResult(mResult); + } else { + mAccountManagerResponse.onError(AccountManager.ERROR_CODE_CANCELED, "canceled"); + } + } + super.finish(); + } +} diff --git a/core/java/android/accounts/GrantCredentialsPermissionActivity.java b/core/java/android/accounts/GrantCredentialsPermissionActivity.java new file mode 100644 index 0000000..e3ed2e9 --- /dev/null +++ b/core/java/android/accounts/GrantCredentialsPermissionActivity.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.accounts; + +import android.app.Activity; +import android.os.Bundle; +import android.widget.TextView; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import android.view.View; +import android.view.LayoutInflater; +import android.view.ViewGroup; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import com.android.internal.R; + +/** + * @hide + */ +public class GrantCredentialsPermissionActivity extends Activity implements View.OnClickListener { + public static final String EXTRAS_ACCOUNT = "account"; + public static final String EXTRAS_AUTH_TOKEN_LABEL = "authTokenLabel"; + public static final String EXTRAS_AUTH_TOKEN_TYPE = "authTokenType"; + public static final String EXTRAS_RESPONSE = "response"; + public static final String EXTRAS_ACCOUNT_TYPE_LABEL = "accountTypeLabel"; + public static final String EXTRAS_PACKAGES = "application"; + public static final String EXTRAS_REQUESTING_UID = "uid"; + private Account mAccount; + private String mAuthTokenType; + private int mUid; + private Bundle mResultBundle = null; + + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + getWindow().setContentView(R.layout.grant_credentials_permission); + mAccount = getIntent().getExtras().getParcelable(EXTRAS_ACCOUNT); + mAuthTokenType = getIntent().getExtras().getString(EXTRAS_AUTH_TOKEN_TYPE); + mUid = getIntent().getExtras().getInt(EXTRAS_REQUESTING_UID); + final String accountTypeLabel = + getIntent().getExtras().getString(EXTRAS_ACCOUNT_TYPE_LABEL); + final String[] packages = getIntent().getExtras().getStringArray(EXTRAS_PACKAGES); + + findViewById(R.id.allow).setOnClickListener(this); + findViewById(R.id.deny).setOnClickListener(this); + + TextView messageView = (TextView) getWindow().findViewById(R.id.message); + String authTokenLabel = getIntent().getExtras().getString(EXTRAS_AUTH_TOKEN_LABEL); + if (authTokenLabel.length() == 0) { + CharSequence grantCredentialsPermissionFormat = getResources().getText( + R.string.grant_credentials_permission_message_desc); + messageView.setText(String.format(grantCredentialsPermissionFormat.toString(), + mAccount.name, accountTypeLabel)); + } else { + CharSequence grantCredentialsPermissionFormat = getResources().getText( + R.string.grant_credentials_permission_message_with_authtokenlabel_desc); + messageView.setText(String.format(grantCredentialsPermissionFormat.toString(), + authTokenLabel, mAccount.name, accountTypeLabel)); + } + + String[] packageLabels = new String[packages.length]; + final PackageManager pm = getPackageManager(); + for (int i = 0; i < packages.length; i++) { + try { + packageLabels[i] = + pm.getApplicationLabel(pm.getApplicationInfo(packages[i], 0)).toString(); + } catch (PackageManager.NameNotFoundException e) { + packageLabels[i] = packages[i]; + } + } + ((ListView) findViewById(R.id.packages_list)).setAdapter( + new PackagesArrayAdapter(this, packageLabels)); + } + + public void onClick(View v) { + switch (v.getId()) { + case R.id.allow: + AccountManagerService.getSingleton().grantAppPermission(mAccount, mAuthTokenType, + mUid); + Intent result = new Intent(); + result.putExtra("retry", true); + setResult(RESULT_OK, result); + setAccountAuthenticatorResult(result.getExtras()); + break; + + case R.id.deny: + AccountManagerService.getSingleton().revokeAppPermission(mAccount, mAuthTokenType, + mUid); + setResult(RESULT_CANCELED); + break; + } + finish(); + } + + public final void setAccountAuthenticatorResult(Bundle result) { + mResultBundle = result; + } + + /** + * Sends the result or a Constants.ERROR_CODE_CANCELED error if a result isn't present. + */ + public void finish() { + Intent intent = getIntent(); + AccountAuthenticatorResponse accountAuthenticatorResponse = + intent.getParcelableExtra(EXTRAS_RESPONSE); + if (accountAuthenticatorResponse != null) { + // send the result bundle back if set, otherwise send an error. + if (mResultBundle != null) { + accountAuthenticatorResponse.onResult(mResultBundle); + } else { + accountAuthenticatorResponse.onError(AccountManager.ERROR_CODE_CANCELED, "canceled"); + } + } + super.finish(); + } + + private static class PackagesArrayAdapter extends ArrayAdapter<String> { + protected LayoutInflater mInflater; + private static final int mResource = R.layout.simple_list_item_1; + + public PackagesArrayAdapter(Context context, String[] items) { + super(context, mResource, items); + mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + static class ViewHolder { + TextView label; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + // A ViewHolder keeps references to children views to avoid unneccessary calls + // to findViewById() on each row. + ViewHolder holder; + + // When convertView is not null, we can reuse it directly, there is no need + // to reinflate it. We only inflate a new View when the convertView supplied + // by ListView is null. + if (convertView == null) { + convertView = mInflater.inflate(mResource, null); + + // Creates a ViewHolder and store references to the two children views + // we want to bind data to. + holder = new ViewHolder(); + holder.label = (TextView) convertView.findViewById(R.id.text1); + + convertView.setTag(holder); + } else { + // Get the ViewHolder back to get fast access to the TextView + // and the ImageView. + holder = (ViewHolder) convertView.getTag(); + } + + holder.label.setText(getItem(position)); + + return convertView; + } + } +} diff --git a/core/java/android/accounts/IAccountAuthenticator.aidl b/core/java/android/accounts/IAccountAuthenticator.aidl new file mode 100644 index 0000000..8860710 --- /dev/null +++ b/core/java/android/accounts/IAccountAuthenticator.aidl @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.accounts; + +import android.accounts.IAccountAuthenticatorResponse; +import android.accounts.Account; +import android.os.Bundle; + +/** + * Service that allows the interaction with an authentication server. + * @hide + */ +oneway interface IAccountAuthenticator { + /** + * prompts the user for account information and adds the result to the IAccountManager + */ + void addAccount(in IAccountAuthenticatorResponse response, String accountType, + String authTokenType, in String[] requiredFeatures, in Bundle options); + + /** + * prompts the user for the credentials of the account + */ + void confirmCredentials(in IAccountAuthenticatorResponse response, in Account account, + in Bundle options); + + /** + * gets the password by either prompting the user or querying the IAccountManager + */ + void getAuthToken(in IAccountAuthenticatorResponse response, in Account account, + String authTokenType, in Bundle options); + + /** + * Gets the user-visible label of the given authtoken type. + */ + void getAuthTokenLabel(in IAccountAuthenticatorResponse response, String authTokenType); + + /** + * prompts the user for a new password and writes it to the IAccountManager + */ + void updateCredentials(in IAccountAuthenticatorResponse response, in Account account, + String authTokenType, in Bundle options); + + /** + * launches an activity that lets the user edit and set the properties for an authenticator + */ + void editProperties(in IAccountAuthenticatorResponse response, String accountType); + + /** + * returns a Bundle where the boolean value BOOLEAN_RESULT_KEY is set if the account has the + * specified features + */ + void hasFeatures(in IAccountAuthenticatorResponse response, in Account account, + in String[] features); + + /** + * Gets whether or not the account is allowed to be removed. + */ + void getAccountRemovalAllowed(in IAccountAuthenticatorResponse response, in Account account); +} diff --git a/core/java/android/accounts/IAccountAuthenticatorResponse.aidl b/core/java/android/accounts/IAccountAuthenticatorResponse.aidl new file mode 100644 index 0000000..0c75e50 --- /dev/null +++ b/core/java/android/accounts/IAccountAuthenticatorResponse.aidl @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.accounts; +import android.os.Bundle; + +/** + * The interface used to return responses from an {@link IAccountAuthenticator} + * @hide + */ +oneway interface IAccountAuthenticatorResponse { + void onResult(in Bundle value); + void onRequestContinued(); + void onError(int errorCode, String errorMessage); +} diff --git a/core/java/android/accounts/IAccountManager.aidl b/core/java/android/accounts/IAccountManager.aidl new file mode 100644 index 0000000..0e318c0 --- /dev/null +++ b/core/java/android/accounts/IAccountManager.aidl @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.accounts; + +import android.accounts.IAccountManagerResponse; +import android.accounts.Account; +import android.accounts.AuthenticatorDescription; +import android.os.Bundle; + + +/** + * Central application service that provides account management. + * @hide + */ +interface IAccountManager { + String getPassword(in Account account); + String getUserData(in Account account, String key); + AuthenticatorDescription[] getAuthenticatorTypes(); + Account[] getAccounts(String accountType); + void getAccountsByFeatures(in IAccountManagerResponse response, String accountType, in String[] features); + boolean addAccount(in Account account, String password, in Bundle extras); + void removeAccount(in IAccountManagerResponse response, in Account account); + void invalidateAuthToken(String accountType, String authToken); + String peekAuthToken(in Account account, String authTokenType); + void setAuthToken(in Account account, String authTokenType, String authToken); + void setPassword(in Account account, String password); + void clearPassword(in Account account); + void setUserData(in Account account, String key, String value); + + void getAuthToken(in IAccountManagerResponse response, in Account account, + String authTokenType, boolean notifyOnAuthFailure, boolean expectActivityLaunch, + in Bundle options); + void addAcount(in IAccountManagerResponse response, String accountType, + String authTokenType, in String[] requiredFeatures, boolean expectActivityLaunch, + in Bundle options); + void updateCredentials(in IAccountManagerResponse response, in Account account, + String authTokenType, boolean expectActivityLaunch, in Bundle options); + void editProperties(in IAccountManagerResponse response, String accountType, + boolean expectActivityLaunch); + void confirmCredentials(in IAccountManagerResponse response, in Account account, + in Bundle options, boolean expectActivityLaunch); +} diff --git a/core/java/android/accounts/IAccountManagerResponse.aidl b/core/java/android/accounts/IAccountManagerResponse.aidl new file mode 100644 index 0000000..ca1203d --- /dev/null +++ b/core/java/android/accounts/IAccountManagerResponse.aidl @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.accounts; +import android.os.Bundle; + +/** + * The interface used to return responses for asynchronous calls to the {@link IAccountManager} + * @hide + */ +oneway interface IAccountManagerResponse { + void onResult(in Bundle value); + void onError(int errorCode, String errorMessage); +} diff --git a/core/java/android/accounts/IAccountsService.aidl b/core/java/android/accounts/IAccountsService.aidl deleted file mode 100644 index dda513c..0000000 --- a/core/java/android/accounts/IAccountsService.aidl +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (C) 2008 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.accounts; - -/** - * Central application service that allows querying the list of accounts. - */ -interface IAccountsService { - /** - * Gets the list of Accounts the user has previously logged - * in to. Accounts are of the form "username@domain". - * <p> - * This method will return an empty array if the device doesn't - * know about any accounts (yet). - * - * @return The accounts. The array will be zero-length if the - * AccountsService doesn't know about any accounts yet. - */ - String[] getAccounts(); - - /** - * This is an interim solution for bypassing a forgotten gesture on the - * unlock screen (it is hidden, please make sure it stays this way!). This - * will be *removed* when the unlock screen design supports additional - * authenticators. - * <p> - * The user will be presented with username and password fields that are - * called as parameters to this method. If true is returned, the user is - * able to define a new gesture and get back into the system. If false, the - * user can try again. - * - * @param username The username entered. - * @param password The password entered. - * @return Whether to allow the user to bypass the lock screen and define a - * new gesture. - * @hide (The package is already hidden, but just in case someone - * unhides that, this should not be revealed.) - */ - boolean shouldUnlock(String username, String password); -} diff --git a/core/java/android/accounts/NetworkErrorException.java b/core/java/android/accounts/NetworkErrorException.java new file mode 100644 index 0000000..07f4ce9 --- /dev/null +++ b/core/java/android/accounts/NetworkErrorException.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.accounts; + +public class NetworkErrorException extends AccountsException { + public NetworkErrorException() { + super(); + } + public NetworkErrorException(String message) { + super(message); + } + public NetworkErrorException(String message, Throwable cause) { + super(message, cause); + } + public NetworkErrorException(Throwable cause) { + super(cause); + } +}
\ No newline at end of file diff --git a/core/java/android/accounts/AccountMonitorListener.java b/core/java/android/accounts/OnAccountsUpdateListener.java index d0bd9a9..38b371d 100644 --- a/core/java/android/accounts/AccountMonitorListener.java +++ b/core/java/android/accounts/OnAccountsUpdateListener.java @@ -19,11 +19,11 @@ package android.accounts; /** * An interface that contains the callback used by the AccountMonitor */ -public interface AccountMonitorListener { +public interface OnAccountsUpdateListener { /** * This invoked when the AccountMonitor starts up and whenever the account * set changes. - * @param currentAccounts the current accounts + * @param accounts the current accounts */ - void onAccountsUpdated(String[] currentAccounts); + void onAccountsUpdated(Account[] accounts); } diff --git a/core/java/android/accounts/OperationCanceledException.java b/core/java/android/accounts/OperationCanceledException.java new file mode 100644 index 0000000..896d194 --- /dev/null +++ b/core/java/android/accounts/OperationCanceledException.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.accounts; + +public class OperationCanceledException extends AccountsException { + public OperationCanceledException() { + super(); + } + public OperationCanceledException(String message) { + super(message); + } + public OperationCanceledException(String message, Throwable cause) { + super(message, cause); + } + public OperationCanceledException(Throwable cause) { + super(cause); + } +} diff --git a/core/java/android/accounts/package.html b/core/java/android/accounts/package.html deleted file mode 100755 index c9f96a6..0000000 --- a/core/java/android/accounts/package.html +++ /dev/null @@ -1,5 +0,0 @@ -<body> - -{@hide} - -</body> |