diff --git a/api/current.txt b/api/current.txt index eda857af5eee2..ae81358044fd5 100644 --- a/api/current.txt +++ b/api/current.txt @@ -2742,6 +2742,7 @@ package android.accounts { method public android.accounts.AccountManagerFuture getAuthTokenByFeatures(java.lang.String, java.lang.String, java.lang.String[], android.app.Activity, android.os.Bundle, android.os.Bundle, android.accounts.AccountManagerCallback, android.os.Handler); method public android.accounts.AuthenticatorDescription[] getAuthenticatorTypes(); method public java.lang.String getPassword(android.accounts.Account); + method public java.lang.String getPreviousName(android.accounts.Account); method public java.lang.String getUserData(android.accounts.Account, java.lang.String); method public android.accounts.AccountManagerFuture hasFeatures(android.accounts.Account, java.lang.String[], android.accounts.AccountManagerCallback, android.os.Handler); method public void invalidateAuthToken(java.lang.String, java.lang.String); @@ -2749,6 +2750,7 @@ package android.accounts { method public java.lang.String peekAuthToken(android.accounts.Account, java.lang.String); method public android.accounts.AccountManagerFuture removeAccount(android.accounts.Account, android.accounts.AccountManagerCallback, android.os.Handler); method public void removeOnAccountsUpdatedListener(android.accounts.OnAccountsUpdateListener); + method public android.accounts.AccountManagerFuture renameAccount(android.accounts.Account, java.lang.String, android.accounts.AccountManagerCallback, android.os.Handler); method public void setAuthToken(android.accounts.Account, java.lang.String, java.lang.String); method public void setPassword(android.accounts.Account, java.lang.String); method public void setUserData(android.accounts.Account, java.lang.String, java.lang.String); diff --git a/core/java/android/accounts/AccountManager.java b/core/java/android/accounts/AccountManager.java index aab6e801ec0e9..09b484bab2ca7 100644 --- a/core/java/android/accounts/AccountManager.java +++ b/core/java/android/accounts/AccountManager.java @@ -662,6 +662,72 @@ public class AccountManager { } } + /** + * Rename the specified {@link Account}. This is equivalent to removing + * the existing account and adding a new renamed account with the old + * account's user data. + * + *

It is safe to call this method from the main thread. + * + *

This method requires the caller to hold the permission + * {@link android.Manifest.permission#AUTHENTICATE_ACCOUNTS} + * and have the same UID as the account's authenticator. + * + * @param account The {@link Account} to rename + * @param newName String name to be associated with the account. + * @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 the Account + * after the name change. If successful the account's name will be the + * specified new name. + */ + public AccountManagerFuture renameAccount( + final Account account, + final String newName, + AccountManagerCallback callback, + Handler handler) { + if (account == null) throw new IllegalArgumentException("account is null."); + if (TextUtils.isEmpty(newName)) { + throw new IllegalArgumentException("newName is empty or null."); + } + return new Future2Task(handler, callback) { + @Override + public void doWork() throws RemoteException { + mService.renameAccount(mResponse, account, newName); + } + @Override + public Account bundleToResult(Bundle bundle) throws AuthenticatorException { + String name = bundle.getString(KEY_ACCOUNT_NAME); + String type = bundle.getString(KEY_ACCOUNT_TYPE); + return new Account(name, type); + } + }.start(); + } + + /** + * Gets the previous name associated with the account or {@code null}, if + * none. This is intended so that clients of {@link + * #LOGIN_ACCOUNTS_CHANGED_ACTION} broadcasts can determine if an + * authenticator has renamed an account. + * + *

It is safe to call this method from the main thread. + * + * @param account The account to query for a previous name. + * @return The account's previous name, null if the account has never been + * renamed. + */ + public String getPreviousName(final Account account) { + if (account == null) throw new IllegalArgumentException("account is null"); + try { + return mService.getPreviousName(account); + } catch (RemoteException e) { + // will never happen + throw new RuntimeException(e); + } + } + /** * Removes an account from the AccountManager. Does nothing if the account * does not exist. Does not delete the account from the server. diff --git a/core/java/android/accounts/IAccountManager.aidl b/core/java/android/accounts/IAccountManager.aidl index a04875d3ab55e..a13378800047b 100644 --- a/core/java/android/accounts/IAccountManager.aidl +++ b/core/java/android/accounts/IAccountManager.aidl @@ -69,4 +69,10 @@ interface IAccountManager { boolean addSharedAccountAsUser(in Account account, int userId); Account[] getSharedAccountsAsUser(int userId); boolean removeSharedAccountAsUser(in Account account, int userId); + + /* Account renaming. */ + void renameAccount(in IAccountManagerResponse response, in Account accountToRename, String newName); + String getPreviousName(in Account account); + boolean renameSharedAccountAsUser(in Account accountToRename, String newName, int userId); + } diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java index 36d67eef8af2c..9a86136cddbb6 100644 --- a/services/core/java/com/android/server/accounts/AccountManagerService.java +++ b/services/core/java/com/android/server/accounts/AccountManagerService.java @@ -77,6 +77,7 @@ import com.android.internal.R; import com.android.internal.util.ArrayUtils; import com.android.internal.util.IndentingPrintWriter; import com.android.server.FgThread; + import com.google.android.collect.Lists; import com.google.android.collect.Sets; @@ -109,7 +110,7 @@ public class AccountManagerService private static final int TIMEOUT_DELAY_MS = 1000 * 60; private static final String DATABASE_NAME = "accounts.db"; - private static final int DATABASE_VERSION = 5; + private static final int DATABASE_VERSION = 6; private final Context mContext; @@ -130,6 +131,7 @@ public class AccountManagerService 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 ACCOUNTS_PREVIOUS_NAME = "previous_name"; private static final String TABLE_AUTHTOKENS = "authtokens"; private static final String AUTHTOKENS_ID = "_id"; @@ -196,6 +198,20 @@ public class AccountManagerService /** protected by the {@link #cacheLock} */ private final HashMap> authTokenCache = new HashMap>(); + /** + * protected by the {@link #cacheLock} + * + * Caches the previous names associated with an account. Previous names + * should be cached because we expect that when an Account is renamed, + * many clients will receive a LOGIN_ACCOUNTS_CHANGED broadcast and + * want to know if the accounts they care about have been renamed. + * + * The previous names are wrapped in an {@link AtomicReference} so that + * we can distinguish between those accounts with no previous names and + * those whose previous names haven't been cached (yet). + */ + private final HashMap> previousNameCache = + new HashMap>(); UserAccounts(Context context, int userId) { this.userId = userId; @@ -516,6 +532,57 @@ public class AccountManagerService } } + @Override + public String getPreviousName(Account account) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "getPreviousName: " + account + + ", caller's uid " + Binder.getCallingUid() + + ", pid " + Binder.getCallingPid()); + } + if (account == null) throw new IllegalArgumentException("account is null"); + UserAccounts accounts = getUserAccountsForCaller(); + long identityToken = clearCallingIdentity(); + try { + return readPreviousNameInternal(accounts, account); + } finally { + restoreCallingIdentity(identityToken); + } + } + + private String readPreviousNameInternal(UserAccounts accounts, Account account) { + if (account == null) { + return null; + } + synchronized (accounts.cacheLock) { + AtomicReference previousNameRef = accounts.previousNameCache.get(account); + if (previousNameRef == null) { + final SQLiteDatabase db = accounts.openHelper.getReadableDatabase(); + Cursor cursor = db.query( + TABLE_ACCOUNTS, + new String[]{ ACCOUNTS_PREVIOUS_NAME }, + ACCOUNTS_NAME + "=? AND " + ACCOUNTS_TYPE+ "=?", + new String[] { account.name, account.type }, + null, + null, + null); + try { + if (cursor.moveToNext()) { + String previousName = cursor.getString(0); + previousNameRef = new AtomicReference(previousName); + accounts.previousNameCache.put(account, previousNameRef); + return previousName; + } else { + return null; + } + } finally { + cursor.close(); + } + } else { + return previousNameRef.get(); + } + } + } + @Override public String getUserData(Account account, String key) { if (Log.isLoggable(TAG, Log.VERBOSE)) { @@ -858,6 +925,119 @@ public class AccountManagerService } } + @Override + public void renameAccount( + IAccountManagerResponse response, Account accountToRename, String newName) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "renameAccount: " + accountToRename + " -> " + newName + + ", caller's uid " + Binder.getCallingUid() + + ", pid " + Binder.getCallingPid()); + } + if (accountToRename == null) throw new IllegalArgumentException("account is null"); + checkAuthenticateAccountsPermission(accountToRename); + UserAccounts accounts = getUserAccountsForCaller(); + long identityToken = clearCallingIdentity(); + try { + Account resultingAccount = renameAccountInternal(accounts, accountToRename, newName); + Bundle result = new Bundle(); + result.putString(AccountManager.KEY_ACCOUNT_NAME, resultingAccount.name); + result.putString(AccountManager.KEY_ACCOUNT_TYPE, resultingAccount.type); + try { + response.onResult(result); + } catch (RemoteException e) { + Log.w(TAG, e.getMessage()); + } + } finally { + restoreCallingIdentity(identityToken); + } + } + + private Account renameAccountInternal( + UserAccounts accounts, Account accountToRename, String newName) { + Account resultAccount = null; + /* + * Cancel existing notifications. Let authenticators + * re-post notifications as required. But we don't know if + * the authenticators have bound their notifications to + * now stale account name data. + * + * With a rename api, we might not need to do this anymore but it + * shouldn't hurt. + */ + cancelNotification( + getSigninRequiredNotificationId(accounts, accountToRename), + new UserHandle(accounts.userId)); + synchronized(accounts.credentialsPermissionNotificationIds) { + for (Pair, Integer> pair: + accounts.credentialsPermissionNotificationIds.keySet()) { + if (accountToRename.equals(pair.first.first)) { + int id = accounts.credentialsPermissionNotificationIds.get(pair); + cancelNotification(id, new UserHandle(accounts.userId)); + } + } + } + synchronized (accounts.cacheLock) { + final SQLiteDatabase db = accounts.openHelper.getWritableDatabase(); + db.beginTransaction(); + boolean isSuccessful = false; + Account renamedAccount = new Account(newName, accountToRename.type); + try { + final ContentValues values = new ContentValues(); + values.put(ACCOUNTS_NAME, newName); + values.put(ACCOUNTS_PREVIOUS_NAME, accountToRename.name); + final long accountId = getAccountIdLocked(db, accountToRename); + if (accountId >= 0) { + final String[] argsAccountId = { String.valueOf(accountId) }; + db.update(TABLE_ACCOUNTS, values, ACCOUNTS_ID + "=?", argsAccountId); + db.setTransactionSuccessful(); + isSuccessful = true; + } + } finally { + db.endTransaction(); + if (isSuccessful) { + /* + * Database transaction was successful. Clean up cached + * data associated with the account in the user profile. + */ + insertAccountIntoCacheLocked(accounts, renamedAccount); + /* + * Extract the data and token caches before removing the + * old account to preserve the user data associated with + * the account. + */ + HashMap tmpData = accounts.userDataCache.get(accountToRename); + HashMap tmpTokens = accounts.authTokenCache.get(accountToRename); + removeAccountFromCacheLocked(accounts, accountToRename); + /* + * Update the cached data associated with the renamed + * account. + */ + accounts.userDataCache.put(renamedAccount, tmpData); + accounts.authTokenCache.put(renamedAccount, tmpTokens); + accounts.previousNameCache.put( + renamedAccount, + new AtomicReference(accountToRename.name)); + resultAccount = renamedAccount; + + if (accounts.userId == UserHandle.USER_OWNER) { + /* + * Owner's account was renamed, rename the account for + * those users with which the account was shared. + */ + List users = mUserManager.getUsers(true); + for (UserInfo user : users) { + if (!user.isPrimary() && user.isRestricted()) { + renameSharedAccountAsUser(accountToRename, newName, user.id); + } + } + } + sendAccountsChangedBroadcast(accounts.userId); + } + } + } + return resultAccount; + } + @Override public void removeAccount(IAccountManagerResponse response, Account account) { if (Log.isLoggable(TAG, Log.VERBOSE)) { @@ -2060,6 +2240,26 @@ public class AccountManagerService return true; } + @Override + public boolean renameSharedAccountAsUser(Account account, String newName, int userId) { + userId = handleIncomingUser(userId); + UserAccounts accounts = getUserAccounts(userId); + SQLiteDatabase db = accounts.openHelper.getWritableDatabase(); + final ContentValues values = new ContentValues(); + values.put(ACCOUNTS_NAME, newName); + values.put(ACCOUNTS_PREVIOUS_NAME, account.name); + int r = db.update( + TABLE_SHARED_ACCOUNTS, + values, + ACCOUNTS_NAME + "=? AND " + ACCOUNTS_TYPE+ "=?", + new String[] { account.name, account.type }); + if (r > 0) { + // Recursively rename the account. + renameAccountInternal(accounts, account, newName); + } + return r > 0; + } + @Override public boolean removeSharedAccountAsUser(Account account, int userId) { userId = handleIncomingUser(userId); @@ -2557,6 +2757,7 @@ public class AccountManagerService + ACCOUNTS_NAME + " TEXT NOT NULL, " + ACCOUNTS_TYPE + " TEXT NOT NULL, " + ACCOUNTS_PASSWORD + " TEXT, " + + ACCOUNTS_PREVIOUS_NAME + " TEXT, " + "UNIQUE(" + ACCOUNTS_NAME + "," + ACCOUNTS_TYPE + "))"); db.execSQL("CREATE TABLE " + TABLE_AUTHTOKENS + " ( " @@ -2592,6 +2793,10 @@ public class AccountManagerService + "UNIQUE(" + ACCOUNTS_NAME + "," + ACCOUNTS_TYPE + "))"); } + private void addOldAccountNameColumn(SQLiteDatabase db) { + db.execSQL("ALTER TABLE " + TABLE_ACCOUNTS + " ADD COLUMN " + ACCOUNTS_PREVIOUS_NAME); + } + private void createAccountsDeletionTrigger(SQLiteDatabase db) { db.execSQL("" + " CREATE TRIGGER " + TABLE_ACCOUNTS + "Delete DELETE ON " + TABLE_ACCOUNTS @@ -2642,6 +2847,11 @@ public class AccountManagerService oldVersion++; } + if (oldVersion == 5) { + addOldAccountNameColumn(db); + oldVersion++; + } + if (oldVersion != newVersion) { Log.e(TAG, "failed to upgrade version " + oldVersion + " to version " + newVersion); } @@ -3050,6 +3260,7 @@ public class AccountManagerService } accounts.userDataCache.remove(account); accounts.authTokenCache.remove(account); + accounts.previousNameCache.remove(account); } /**