diff --git a/src/com/android/exchange/SyncManager.java b/src/com/android/exchange/SyncManager.java index d0ee4bfaa..015e72c58 100644 --- a/src/com/android/exchange/SyncManager.java +++ b/src/com/android/exchange/SyncManager.java @@ -194,7 +194,7 @@ public class SyncManager extends Service implements Runnable { private EasSyncStatusObserver mSyncStatusObserver; private EasAccountsUpdatedListener mAccountsUpdatedListener; - private ContentResolver mResolver; + /*package*/ ContentResolver mResolver; // The singleton SyncManager object, with its thread and stop flag protected static SyncManager INSTANCE; @@ -693,7 +693,8 @@ public class SyncManager extends Service implements Runnable { public class EasAccountsUpdatedListener implements OnAccountsUpdateListener { public void onAccountsUpdated(android.accounts.Account[] accounts) { - checkWithAccountManager(); + reconcileAccountsWithAccountManager(INSTANCE, getAccountList(), + AccountManager.get(INSTANCE).getAccountsByType(Eas.ACCOUNT_MANAGER_TYPE)); } } @@ -1218,25 +1219,52 @@ public class SyncManager extends Service implements Runnable { } } - private void checkWithAccountManager() { - android.accounts.Account[] accts = - AccountManager.get(this).getAccountsByType(Eas.ACCOUNT_MANAGER_TYPE); - List easAccounts = getAccountList(); - for (Account easAccount: easAccounts) { - String accountName = easAccount.mEmailAddress; + /** + * Compare our account list (obtained from EmailProvider) with the account list owned by + * AccountManager. If there are any orphans (an account in one list without a corresponding + * account in the other list), delete the orphan, as these must remain in sync. + * + * Note that the duplication of account information is caused by the Email application's + * incomplete integration with AccountManager. + */ + /*package*/ void reconcileAccountsWithAccountManager(Context context, + List cachedEasAccounts, android.accounts.Account[] accountManagerAccounts) { + // First, look through our cached EAS Accounts (from EmailProvider) to make sure there's a + // corresponding AccountManager account + for (Account providerAccount: cachedEasAccounts) { + String providerAccountName = providerAccount.mEmailAddress; boolean found = false; - for (android.accounts.Account acct: accts) { - if (acct.name.equalsIgnoreCase(accountName)) { + for (android.accounts.Account accountManagerAccount: accountManagerAccounts) { + if (accountManagerAccount.name.equalsIgnoreCase(providerAccountName)) { found = true; break; } } if (!found) { // This account has been deleted in the AccountManager! - log("Account deleted in AccountManager; deleting from provider: " + accountName); + log("Account deleted in AccountManager; deleting from provider: " + + providerAccountName); // TODO This will orphan downloaded attachments; need to handle this - mResolver.delete(ContentUris.withAppendedId(Account.CONTENT_URI, easAccount.mId), - null, null); + mResolver.delete(ContentUris.withAppendedId(Account.CONTENT_URI, + providerAccount.mId), null, null); + } + } + // Now, look through AccountManager accounts to make sure we have a corresponding cached EAS + // account from EmailProvider + for (android.accounts.Account accountManagerAccount: accountManagerAccounts) { + String accountManagerAccountName = accountManagerAccount.name; + boolean found = false; + for (Account cachedEasAccount: cachedEasAccounts) { + if (cachedEasAccount.mEmailAddress.equalsIgnoreCase(accountManagerAccountName)) { + found = true; + } + } + if (!found) { + // This account has been deleted from the EmailProvider database + log("Account deleted from provider; deleting from AccountManager: " + + accountManagerAccountName); + // Delete the account + AccountManager.get(context).removeAccount(accountManagerAccount, null, null); } } } diff --git a/tests/src/com/android/exchange/SyncManagerAccountTests.java b/tests/src/com/android/exchange/SyncManagerAccountTests.java new file mode 100644 index 000000000..9a1f24b59 --- /dev/null +++ b/tests/src/com/android/exchange/SyncManagerAccountTests.java @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2009 Marc Blank + * Licensed to 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 com.android.exchange; + +import com.android.email.provider.EmailContent; +import com.android.email.provider.EmailProvider; +import com.android.email.provider.ProviderTestUtils; +import com.android.email.provider.EmailContent.Account; +import com.android.exchange.SyncManager.AccountList; + +import android.accounts.AccountManager; +import android.accounts.AccountManagerFuture; +import android.accounts.AuthenticatorException; +import android.accounts.OperationCanceledException; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.Context; +import android.database.Cursor; +import android.test.ProviderTestCase2; + +import java.io.IOException; + +public class SyncManagerAccountTests extends ProviderTestCase2 { + + private static final String TEST_ACCOUNT_PREFIX = "__test"; + private static final String TEST_ACCOUNT_SUFFIX = "@android.com"; + + EmailProvider mProvider; + Context mMockContext; + + public SyncManagerAccountTests() { + super(EmailProvider.class, EmailProvider.EMAIL_AUTHORITY); + } + + @Override + public void setUp() throws Exception { + super.setUp(); + mMockContext = getMockContext(); + // Delete any test accounts we might have created earlier + deleteTemporaryAccountManagerAccounts(getContext()); + } + + @Override + public void tearDown() throws Exception { + super.tearDown(); + // Delete any test accounts we might have created earlier + deleteTemporaryAccountManagerAccounts(getContext()); + } + + private android.accounts.Account makeAccountManagerAccount(String username) { + return new android.accounts.Account(username, Eas.ACCOUNT_MANAGER_TYPE); + } + + private void createAccountManagerAccount(String username) { + final android.accounts.Account account = makeAccountManagerAccount(username); + AccountManager.get(getContext()).addAccountExplicitly(account, "password", null); + } + + private Account setupProviderAndAccountManagerAccount(String username) { + // Note that setupAccount creates the email address username@android.com, so that's what + // we need to use for the account manager + createAccountManagerAccount(username + "@android.com"); + return ProviderTestUtils.setupAccount(username, true, mMockContext); + } + + private AccountList makeSyncManagerAccountList() { + AccountList accountList = new AccountList(); + Cursor c = mMockContext.getContentResolver().query(Account.CONTENT_URI, + Account.CONTENT_PROJECTION, null, null, null); + try { + while (c.moveToNext()) { + accountList.add(new Account().restore(c)); + } + } finally { + c.close(); + } + return accountList; + } + + private void deleteAccountManagerAccount(Context context, android.accounts.Account account) { + AccountManagerFuture future = + AccountManager.get(context).removeAccount(account, null, null); + try { + future.getResult(); + } catch (OperationCanceledException e) { + } catch (AuthenticatorException e) { + } catch (IOException e) { + } + } + + private void deleteTemporaryAccountManagerAccounts(Context context) { + android.accounts.Account[] accountManagerAccounts = + AccountManager.get(context).getAccountsByType(Eas.ACCOUNT_MANAGER_TYPE); + for (android.accounts.Account accountManagerAccount: accountManagerAccounts) { + if (accountManagerAccount.name.startsWith(TEST_ACCOUNT_PREFIX) && + accountManagerAccount.name.endsWith(TEST_ACCOUNT_SUFFIX)) { + deleteAccountManagerAccount(context, accountManagerAccount); + } + } + } + + private String getTestAccountName(String name) { + return TEST_ACCOUNT_PREFIX + name; + } + + private String getTestAccountEmailAddress(String name) { + return TEST_ACCOUNT_PREFIX + name + TEST_ACCOUNT_SUFFIX; + } + + public void testReconcileAccounts() { + // Note that we can't use mMockContext for AccountManager interactions, as it isn't a fully + // functional Context. + Context context = getContext(); + + // Set up three accounts, both in AccountManager and in EmailProvider + Account firstAccount = setupProviderAndAccountManagerAccount(getTestAccountName("1")); + setupProviderAndAccountManagerAccount(getTestAccountName("2")); + setupProviderAndAccountManagerAccount(getTestAccountName("3")); + + // Check that they're set up properly + assertEquals(3, EmailContent.count(mMockContext, Account.CONTENT_URI, null, null)); + android.accounts.Account[] accountManagerAccounts = + AccountManager.get(context).getAccountsByType(Eas.ACCOUNT_MANAGER_TYPE); + assertEquals(3, accountManagerAccounts.length); + + // Delete account "2" from AccountManager + android.accounts.Account removedAccount = + makeAccountManagerAccount(getTestAccountEmailAddress("2")); + deleteAccountManagerAccount(context, removedAccount); + + // Confirm it's deleted + accountManagerAccounts = + AccountManager.get(context).getAccountsByType(Eas.ACCOUNT_MANAGER_TYPE); + assertEquals(2, accountManagerAccounts.length); + + // Run the reconciler + SyncManager syncManager = new SyncManager(); + ContentResolver resolver = mMockContext.getContentResolver(); + syncManager.mResolver = resolver; + syncManager.reconcileAccountsWithAccountManager(context, + makeSyncManagerAccountList(), accountManagerAccounts); + + // There should now be only two EmailProvider accounts + assertEquals(2, EmailContent.count(mMockContext, Account.CONTENT_URI, null, null)); + + // Ok, now we've got two of each; let's delete a provider account + resolver.delete(ContentUris.withAppendedId(Account.CONTENT_URI, firstAccount.mId), + null, null); + // ...and then there was one + assertEquals(1, EmailContent.count(mMockContext, Account.CONTENT_URI, null, null)); + + // Run the reconciler + syncManager.reconcileAccountsWithAccountManager(context, + makeSyncManagerAccountList(), accountManagerAccounts); + + // There should now be only one AccountManager account + accountManagerAccounts = + AccountManager.get(getContext()).getAccountsByType(Eas.ACCOUNT_MANAGER_TYPE); + assertEquals(1, accountManagerAccounts.length); + // ... and it should be account "3" + assertEquals(getTestAccountEmailAddress("3"), accountManagerAccounts[0].name); + } +}