AccountManager: add startUpdateCredentials API.

Adding startUpdateCredentials API to AccountManager and
AbstractAccountAuthenticator.

Change-Id: Id9a1ff86764f2fde01fd8482594e4ae34e1f3bd1
diff --git a/api/current.txt b/api/current.txt
index e17bf28..2a363cb 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -2733,6 +2733,7 @@
     method public final android.os.IBinder getIBinder();
     method public abstract android.os.Bundle hasFeatures(android.accounts.AccountAuthenticatorResponse, android.accounts.Account, java.lang.String[]) throws android.accounts.NetworkErrorException;
     method public android.os.Bundle startAddAccountSession(android.accounts.AccountAuthenticatorResponse, java.lang.String, java.lang.String, java.lang.String[], android.os.Bundle) throws android.accounts.NetworkErrorException;
+    method public android.os.Bundle startUpdateCredentialsSession(android.accounts.AccountAuthenticatorResponse, android.accounts.Account, java.lang.String, android.os.Bundle) throws android.accounts.NetworkErrorException;
     method public abstract android.os.Bundle updateCredentials(android.accounts.AccountAuthenticatorResponse, android.accounts.Account, java.lang.String, android.os.Bundle) throws android.accounts.NetworkErrorException;
     field public static final java.lang.String KEY_CUSTOM_TOKEN_EXPIRY = "android.accounts.expiry";
   }
@@ -2798,6 +2799,7 @@
     method public void setPassword(android.accounts.Account, java.lang.String);
     method public void setUserData(android.accounts.Account, java.lang.String, java.lang.String);
     method public android.accounts.AccountManagerFuture<android.os.Bundle> startAddAccountSession(java.lang.String, java.lang.String, java.lang.String[], android.os.Bundle, android.app.Activity, android.accounts.AccountManagerCallback<android.os.Bundle>, android.os.Handler);
+    method public android.accounts.AccountManagerFuture<android.os.Bundle> startUpdateCredentialsSession(android.accounts.Account, java.lang.String, android.os.Bundle, android.app.Activity, android.accounts.AccountManagerCallback<android.os.Bundle>, android.os.Handler);
     method public android.accounts.AccountManagerFuture<android.os.Bundle> updateCredentials(android.accounts.Account, java.lang.String, android.os.Bundle, android.app.Activity, android.accounts.AccountManagerCallback<android.os.Bundle>, android.os.Handler);
     field public static final java.lang.String ACTION_AUTHENTICATOR_INTENT = "android.accounts.AccountAuthenticator";
     field public static final java.lang.String AUTHENTICATOR_ATTRIBUTES_NAME = "account-authenticator";
diff --git a/api/system-current.txt b/api/system-current.txt
index d8b9b6f..3b7ffcb 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -2832,6 +2832,7 @@
     method public final android.os.IBinder getIBinder();
     method public abstract android.os.Bundle hasFeatures(android.accounts.AccountAuthenticatorResponse, android.accounts.Account, java.lang.String[]) throws android.accounts.NetworkErrorException;
     method public android.os.Bundle startAddAccountSession(android.accounts.AccountAuthenticatorResponse, java.lang.String, java.lang.String, java.lang.String[], android.os.Bundle) throws android.accounts.NetworkErrorException;
+    method public android.os.Bundle startUpdateCredentialsSession(android.accounts.AccountAuthenticatorResponse, android.accounts.Account, java.lang.String, android.os.Bundle) throws android.accounts.NetworkErrorException;
     method public abstract android.os.Bundle updateCredentials(android.accounts.AccountAuthenticatorResponse, android.accounts.Account, java.lang.String, android.os.Bundle) throws android.accounts.NetworkErrorException;
     field public static final java.lang.String KEY_CUSTOM_TOKEN_EXPIRY = "android.accounts.expiry";
   }
@@ -2897,6 +2898,7 @@
     method public void setPassword(android.accounts.Account, java.lang.String);
     method public void setUserData(android.accounts.Account, java.lang.String, java.lang.String);
     method public android.accounts.AccountManagerFuture<android.os.Bundle> startAddAccountSession(java.lang.String, java.lang.String, java.lang.String[], android.os.Bundle, android.app.Activity, android.accounts.AccountManagerCallback<android.os.Bundle>, android.os.Handler);
+    method public android.accounts.AccountManagerFuture<android.os.Bundle> startUpdateCredentialsSession(android.accounts.Account, java.lang.String, android.os.Bundle, android.app.Activity, android.accounts.AccountManagerCallback<android.os.Bundle>, android.os.Handler);
     method public android.accounts.AccountManagerFuture<android.os.Bundle> updateCredentials(android.accounts.Account, java.lang.String, android.os.Bundle, android.app.Activity, android.accounts.AccountManagerCallback<android.os.Bundle>, android.os.Handler);
     field public static final java.lang.String ACTION_AUTHENTICATOR_INTENT = "android.accounts.AccountAuthenticator";
     field public static final java.lang.String AUTHENTICATOR_ATTRIBUTES_NAME = "account-authenticator";
diff --git a/core/java/android/accounts/AbstractAccountAuthenticator.java b/core/java/android/accounts/AbstractAccountAuthenticator.java
index 185ceb4..041f591 100644
--- a/core/java/android/accounts/AbstractAccountAuthenticator.java
+++ b/core/java/android/accounts/AbstractAccountAuthenticator.java
@@ -119,21 +119,26 @@
     /**
      * Bundle key used for the {@link String} account type in session bundle.
      * This is used in the default implementation of
-     * {@link #startAddAccountSession}. TODO: and startUpdateCredentialsSession.
+     * {@link #startAddAccountSession} and {@link startUpdateCredentialsSession}.
      */
     private static final String KEY_AUTH_TOKEN_TYPE = "android.accounts.KEY_AUTH_TOKEN_TYPE";
     /**
      * Bundle key used for the {@link String} array of required features in
      * session bundle. This is used in the default implementation of
-     * {@link #startAddAccountSession}. TODO: and startUpdateCredentialsSession.
+     * {@link #startAddAccountSession} and {@link startUpdateCredentialsSession}.
      */
     private static final String KEY_REQUIRED_FEATURES = "android.accounts.AbstractAccountAuthenticator.KEY_REQUIRED_FEATURES";
     /**
      * Bundle key used for the {@link Bundle} options in session bundle. This is
-     * used in default implementation of {@link #startAddAccountSession}. TODO:
-     * and startUpdateCredentialsSession.
+     * used in default implementation of {@link #startAddAccountSession} and
+     * {@link startUpdateCredentialsSession}.
      */
     private static final String KEY_OPTIONS = "android.accounts.AbstractAccountAuthenticator.KEY_OPTIONS";
+    /**
+     * Bundle key used for the {@link Account} account in session bundle. This is used
+     * used in default implementation of {@link startUpdateCredentialsSession}.
+     */
+    private static final String KEY_ACCOUNT = "android.accounts.AbstractAccountAuthenticator.KEY_ACCOUNT";
 
     private final Context mContext;
 
@@ -385,6 +390,43 @@
                 handleException(response, "startAddAccountSession", accountType, e);
             }
         }
+
+        @Override
+        public void startUpdateCredentialsSession(
+                IAccountAuthenticatorResponse response,
+                Account account,
+                String authTokenType,
+                Bundle loginOptions) throws RemoteException {
+            if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                Log.v(TAG, "startUpdateCredentialsSession: "
+                        + account
+                        + ", authTokenType "
+                        + authTokenType);
+            }
+            checkBinderPermission();
+            try {
+                final Bundle result = AbstractAccountAuthenticator.this
+                        .startUpdateCredentialsSession(
+                                new AccountAuthenticatorResponse(response),
+                                account,
+                                authTokenType,
+                                loginOptions);
+                if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                    // Result may be null.
+                    if (result != null) {
+                        result.keySet(); // force it to be unparcelled
+                    }
+                    Log.v(TAG, "startUpdateCredentialsSession: result "
+                            + AccountManager.sanitizeResult(result));
+                }
+                if (result != null) {
+                    response.onResult(result);
+                }
+            } catch (Exception e) {
+                handleException(response, "startUpdateCredentialsSession",
+                        account.toString() + "," + authTokenType, e);
+            }
+        }
     }
 
     private void handleException(IAccountAuthenticatorResponse response, String method,
@@ -700,4 +742,49 @@
         }).start();
         return null;
     }
+
+    /**
+     * Asks user to re-authenticate for an account but defers updating the locally stored
+     * credentials.
+     *
+     * @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 (TODO)
+     * @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_SESSION_BUNDLE} for updating the
+     *         locally stored credentials later, and if account is
+     *         re-authenticated, {@link AccountManager#KEY_PASSWORD} and
+     *         {@link AccountManager#KEY_ACCOUNT_STATUS_TOKEN} for checking the
+     *         status of the account later, 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 Bundle startUpdateCredentialsSession(final AccountAuthenticatorResponse response,
+            final Account account, final String authTokenType, final Bundle options)
+                    throws NetworkErrorException {
+        new Thread(new Runnable() {
+            @Override
+            public void run() {
+                Bundle sessionBundle = new Bundle();
+                sessionBundle.putString(KEY_AUTH_TOKEN_TYPE, authTokenType);
+                sessionBundle.putParcelable(KEY_ACCOUNT, account);
+                sessionBundle.putBundle(KEY_OPTIONS, options);
+                Bundle result = new Bundle();
+                result.putBundle(AccountManager.KEY_ACCOUNT_SESSION_BUNDLE, sessionBundle);
+                response.onResult(result);
+            }
+
+        }).start();
+        return null;
+    }
 }
diff --git a/core/java/android/accounts/AccountManager.java b/core/java/android/accounts/AccountManager.java
index 42e5e2a..5557905 100644
--- a/core/java/android/accounts/AccountManager.java
+++ b/core/java/android/accounts/AccountManager.java
@@ -2666,9 +2666,14 @@
      *         trouble
      *         </ul>
      */
-    public AccountManagerFuture<Bundle> startAddAccountSession(final String accountType,
-            final String authTokenType, final String[] requiredFeatures, final Bundle options,
-            final Activity activity, AccountManagerCallback<Bundle> callback, Handler handler) {
+    public AccountManagerFuture<Bundle> startAddAccountSession(
+            final String accountType,
+            final String authTokenType,
+            final String[] requiredFeatures,
+            final Bundle options,
+            final Activity activity,
+            AccountManagerCallback<Bundle> callback,
+            Handler handler) {
         if (accountType == null) throw new IllegalArgumentException("accountType is null");
         final Bundle optionsIn = new Bundle();
         if (options != null) {
@@ -2679,8 +2684,89 @@
         return new AmsTask(activity, handler, callback) {
             @Override
             public void doWork() throws RemoteException {
-                mService.startAddAccountSession(mResponse, accountType, authTokenType,
-                        requiredFeatures, activity != null, optionsIn);
+                mService.startAddAccountSession(
+                        mResponse,
+                        accountType,
+                        authTokenType,
+                        requiredFeatures,
+                        activity != null,
+                        optionsIn);
+            }
+        }.start();
+    }
+
+    /**
+     * Asks the user to enter a new password for an account but not updating the
+     * saved credentials for the account until finishSession is
+     * called.
+     * <p>
+     * This method may be called from any thread, but the returned
+     * {@link AccountManagerFuture} must not be used on the main thread.
+     * <p>
+     * <b>NOTE:</b> The saved credentials for the account alone will not be
+     * updated by calling this API alone .
+     *
+     * @param account The account to update credentials for
+     * @param authTokenType The credentials entered must allow an auth token of
+     *            this type to be created (but no actual auth token is
+     *            returned); may be null
+     * @param options Authenticator-specific options for the request; may be
+     *            null or empty
+     * @param activity The {@link Activity} context to use for launching a new
+     *            authenticator-defined sub-Activity to prompt the user to enter
+     *            a password; used only to call startActivity(); if null, the
+     *            prompt will not be launched directly, but the necessary
+     *            {@link Intent} will be returned to the caller instead
+     * @param callback Callback to invoke when the request completes, null for
+     *            no callback
+     * @param handler {@link Handler} identifying the callback thread, null for
+     *            the main thread
+     * @return An {@link AccountManagerFuture} which resolves to a Bundle with
+     *         these fields if an activity was supplied and user was
+     *         successfully re-authenticated to the account (TODO: default impl
+     *         only returns escorw?):
+     *         <ul>
+     *         <li>{@link #KEY_ACCOUNT_SESSION_BUNDLE} - encrypted Bundle for
+     *         updating the local credentials on device later.
+     *         <li>{@link #KEY_PASSWORD} - optional, the password or password hash of the
+     *         account
+     *         <li>{@link #KEY_ACCOUNT_STATUS_TOKEN} - optional, token to check status of
+     *         the account
+     *         </ul>
+     *         If no activity was specified, the returned Bundle contains
+     *         {@link #KEY_INTENT} with the {@link Intent} needed to launch the
+     *         password prompt. If an error occurred,
+     *         {@link AccountManagerFuture#getResult()} throws:
+     *         <ul>
+     *         <li>{@link AuthenticatorException} if the authenticator failed to
+     *         respond
+     *         <li>{@link OperationCanceledException} if the operation was
+     *         canceled for any reason, including the user canceling the
+     *         password prompt
+     *         <li>{@link IOException} if the authenticator experienced an I/O
+     *         problem verifying the password, usually because of network
+     *         trouble
+     *         </ul>
+     */
+    public AccountManagerFuture<Bundle> startUpdateCredentialsSession(
+            final Account account,
+            final String authTokenType,
+            final Bundle options,
+            final Activity activity,
+            final AccountManagerCallback<Bundle> callback,
+            final Handler handler) {
+        if (account == null) {
+            throw new IllegalArgumentException("account is null");
+        }
+        return new AmsTask(activity, handler, callback) {
+            @Override
+            public void doWork() throws RemoteException {
+                mService.startUpdateCredentialsSession(
+                        mResponse,
+                        account,
+                        authTokenType,
+                        activity != null,
+                        options);
             }
         }.start();
     }
diff --git a/core/java/android/accounts/IAccountAuthenticator.aidl b/core/java/android/accounts/IAccountAuthenticator.aidl
index b326070..921fb19 100644
--- a/core/java/android/accounts/IAccountAuthenticator.aidl
+++ b/core/java/android/accounts/IAccountAuthenticator.aidl
@@ -90,4 +90,10 @@
      */
     void startAddAccountSession(in IAccountAuthenticatorResponse response, String accountType,
         String authTokenType, in String[] requiredFeatures, in Bundle options);
+
+    /**
+     * Prompts the user for a new password but does not write it to the IAccountManager.
+     */
+    void startUpdateCredentialsSession(in IAccountAuthenticatorResponse response, in Account account,
+        String authTokenType, in Bundle options);
 }
diff --git a/core/java/android/accounts/IAccountManager.aidl b/core/java/android/accounts/IAccountManager.aidl
index 5de311e..8489e47 100644
--- a/core/java/android/accounts/IAccountManager.aidl
+++ b/core/java/android/accounts/IAccountManager.aidl
@@ -88,4 +88,7 @@
         String authTokenType, in String[] requiredFeatures, boolean expectActivityLaunch,
         in Bundle options);
 
+    /* Update credentials in two steps. */
+    void startUpdateCredentialsSession(in IAccountManagerResponse response, in Account account,
+        String authTokenType, boolean expectActivityLaunch, in Bundle options);
 }
diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java
index 37d47c3..93eaf0e 100644
--- a/services/core/java/com/android/server/accounts/AccountManagerService.java
+++ b/services/core/java/com/android/server/accounts/AccountManagerService.java
@@ -2286,8 +2286,11 @@
     }
 
     @Override
-    public void startAddAccountSession(final IAccountManagerResponse response, final String accountType,
-            final String authTokenType, final String[] requiredFeatures,
+    public void startAddAccountSession(
+            final IAccountManagerResponse response,
+            final String accountType,
+            final String authTokenType,
+            final String[] requiredFeatures,
             final boolean expectActivityLaunch,
             final Bundle optionsIn) {
         if (Log.isLoggable(TAG, Log.VERBOSE)) {
@@ -2571,6 +2574,60 @@
     }
 
     @Override
+    public void startUpdateCredentialsSession(
+            IAccountManagerResponse response,
+            final Account account,
+            final String authTokenType,
+            final boolean expectActivityLaunch,
+            final Bundle loginOptions) {
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG,
+                    "startUpdateCredentialsSession: " + account + ", response " + response
+                            + ", authTokenType " + authTokenType + ", expectActivityLaunch "
+                            + expectActivityLaunch + ", caller's uid " + Binder.getCallingUid()
+                            + ", pid " + Binder.getCallingPid());
+        }
+        if (response == null) {
+            throw new IllegalArgumentException("response is null");
+        }
+        if (account == null) {
+            throw new IllegalArgumentException("account is null");
+        }
+        int userId = UserHandle.getCallingUserId();
+        long identityToken = clearCallingIdentity();
+        try {
+            UserAccounts accounts = getUserAccounts(userId);
+            new StartAccountSession(
+                    accounts,
+                    response,
+                    account.type,
+                    expectActivityLaunch,
+                    account.name,
+                    false /* authDetailsRequired */,
+                    true /* updateLastCredentialTime */) {
+                @Override
+                public void run() throws RemoteException {
+                    mAuthenticator.startUpdateCredentialsSession(this, account, authTokenType,
+                            loginOptions);
+                }
+
+                @Override
+                protected String toDebugString(long now) {
+                    if (loginOptions != null)
+                        loginOptions.keySet();
+                    return super.toDebugString(now)
+                            + ", startUpdateCredentialsSession"
+                            + ", " + account
+                            + ", authTokenType " + authTokenType
+                            + ", loginOptions " + loginOptions;
+                }
+            }.bind();
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+    }
+
+    @Override
     public void editProperties(IAccountManagerResponse response, final String accountType,
             final boolean expectActivityLaunch) {
         final int callingUid = Binder.getCallingUid();