diff --git a/emailcommon/src/com/android/emailcommon/utility/AccountReconciler.java b/emailcommon/src/com/android/emailcommon/utility/AccountReconciler.java index 6958d205c..3b158f08a 100644 --- a/emailcommon/src/com/android/emailcommon/utility/AccountReconciler.java +++ b/emailcommon/src/com/android/emailcommon/utility/AccountReconciler.java @@ -18,6 +18,7 @@ package com.android.emailcommon.utility; import com.android.emailcommon.Logging; import com.android.emailcommon.provider.EmailContent.Account; +import com.google.common.annotations.VisibleForTesting; import android.accounts.AccountManager; import android.accounts.AccountManagerFuture; @@ -32,6 +33,12 @@ import java.io.IOException; import java.util.List; public class AccountReconciler { + // AccountManager accounts with a name beginning with this constant are ignored for purposes + // of reconcilation. This is for unit test purposes only; the caller may NOT be in the same + // package as this class, so we make the constant public. + @VisibleForTesting + public static final String ACCOUNT_MANAGER_ACCOUNT_TEST_PREFIX = " _"; + /** * 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 @@ -89,6 +96,9 @@ public class AccountReconciler { found = true; } } + if (accountManagerAccountName.startsWith(ACCOUNT_MANAGER_ACCOUNT_TEST_PREFIX)) { + found = true; + } if (!found) { // This account has been deleted from the EmailProvider database Log.d(Logging.LOG_TAG, diff --git a/src/com/android/email/provider/EmailProvider.java b/src/com/android/email/provider/EmailProvider.java index a5b62606c..5cce40dce 100644 --- a/src/com/android/email/provider/EmailProvider.java +++ b/src/com/android/email/provider/EmailProvider.java @@ -20,6 +20,7 @@ import com.android.email.Email; import com.android.email.provider.ContentCache.CacheToken; import com.android.email.service.AttachmentDownloadService; import com.android.emailcommon.AccountManagerTypes; +import com.android.emailcommon.CalendarProviderStub; import com.android.emailcommon.Logging; import com.android.emailcommon.provider.EmailContent; import com.android.emailcommon.provider.EmailContent.Account; @@ -58,6 +59,7 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteOpenHelper; import android.net.Uri; +import android.provider.ContactsContract; import android.util.Log; import java.io.File; @@ -137,7 +139,9 @@ public class EmailProvider extends ContentProvider { // Account's policy when the Account is deleted // Version 20: Add new policies to Policy table // Version 21: Add lastSeenMessageKey column to Mailbox table - public static final int DATABASE_VERSION = 21; + // Version 22: Upgrade path for IMAP/POP accounts to integrate with AccountManager + + public static final int DATABASE_VERSION = 22; // Any changes to the database format *must* include update-in-place code. // Original version: 2 @@ -1070,6 +1074,10 @@ public class EmailProvider extends ContentProvider { upgradeFromVersion20ToVersion21(db); oldVersion = 21; } + if (oldVersion == 21) { + upgradeFromVersion21ToVersion22(db, mContext); + oldVersion = 22; + } } @Override @@ -2005,4 +2013,61 @@ public class EmailProvider extends ContentProvider { Log.w(TAG, "Exception upgrading EmailProvider.db from 20 to 21 " + e); } } + + /** + * Upgrade the database from v21 to v22 + * This entails creating AccountManager accounts for all pop3 and imap accounts + */ + + private static final String[] RECV_PROJECTION = + new String[] {AccountColumns.HOST_AUTH_KEY_RECV}; + private static final int EMAIL_AND_RECV_COLUMN_RECV = 0; + + static private void createAccountManagerAccount(Context context, HostAuth hostAuth) { + AccountManager accountManager = AccountManager.get(context); + android.accounts.Account amAccount = + new android.accounts.Account(hostAuth.mLogin, AccountManagerTypes.TYPE_POP_IMAP); + accountManager.addAccountExplicitly(amAccount, hostAuth.mPassword, null); + ContentResolver.setIsSyncable(amAccount, EmailContent.AUTHORITY, 1); + ContentResolver.setSyncAutomatically(amAccount, EmailContent.AUTHORITY, true); + ContentResolver.setIsSyncable(amAccount, ContactsContract.AUTHORITY, 0); + ContentResolver.setIsSyncable(amAccount, CalendarProviderStub.AUTHORITY, 0); + } + + @VisibleForTesting + static void upgradeFromVersion21ToVersion22(SQLiteDatabase db, Context accountManagerContext) { + try { + // Loop through accounts, looking for pop/imap accounts + Cursor accountCursor = db.query(Account.TABLE_NAME, RECV_PROJECTION, null, + null, null, null, null); + try { + String[] hostAuthArgs = new String[1]; + while (accountCursor.moveToNext()) { + hostAuthArgs[0] = accountCursor.getString(EMAIL_AND_RECV_COLUMN_RECV); + // Get the "receive" HostAuth for this account + Cursor hostAuthCursor = db.query(HostAuth.TABLE_NAME, + HostAuth.CONTENT_PROJECTION, HostAuth.RECORD_ID + "=?", hostAuthArgs, + null, null, null); + try { + if (hostAuthCursor.moveToFirst()) { + HostAuth hostAuth = new HostAuth(); + hostAuth.restore(hostAuthCursor); + String protocol = hostAuth.mProtocol; + // If this is a pop3 or imap account, create the account manager account + if ("imap".equals(protocol) || "pop3".equals(protocol)) { + createAccountManagerAccount(accountManagerContext, hostAuth); + } + } + } finally { + hostAuthCursor.close(); + } + } + } finally { + accountCursor.close(); + } + } catch (SQLException e) { + // Shouldn't be needed unless we're debugging and interrupt the process + Log.w(TAG, "Exception upgrading EmailProvider.db from 20 to 21 " + e); + } + } } diff --git a/tests/src/com/android/email/provider/ProviderTests.java b/tests/src/com/android/email/provider/ProviderTests.java index 44ff72325..0ce3da7ce 100644 --- a/tests/src/com/android/email/provider/ProviderTests.java +++ b/tests/src/com/android/email/provider/ProviderTests.java @@ -16,6 +16,7 @@ package com.android.email.provider; +import com.android.emailcommon.AccountManagerTypes; import com.android.emailcommon.provider.EmailContent; import com.android.emailcommon.provider.EmailContent.Account; import com.android.emailcommon.provider.EmailContent.AccountColumns; @@ -28,9 +29,13 @@ import com.android.emailcommon.provider.EmailContent.Message; import com.android.emailcommon.provider.EmailContent.MessageColumns; import com.android.emailcommon.provider.HostAuth; import com.android.emailcommon.provider.Mailbox; +import com.android.emailcommon.utility.AccountReconciler; import com.android.emailcommon.utility.TextUtilities; import com.android.emailcommon.utility.Utility; +import android.accounts.AccountManager; +import android.accounts.AuthenticatorException; +import android.accounts.OperationCanceledException; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; @@ -2229,4 +2234,105 @@ public class ProviderTests extends ProviderTestCase2 { assertEquals(Message.MAILBOX_KEY + "=" + out.mId, Message.buildMessageListSelection(c, out.mId)); } + + /** + * Determine whether a list of AccountManager accounts includes a given EmailProvider account + * @param amAccountList a list of AccountManager accounts + * @param account an EmailProvider account + * @param context the caller's context (our test provider's context) + * @return whether or not the EmailProvider account is represented in AccountManager + */ + private boolean amAccountListHasAccount(android.accounts.Account[] amAccountList, + Account account, Context context) { + HostAuth hostAuth = HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv); + if (hostAuth == null) return false; + String login = hostAuth.mLogin; + for (android.accounts.Account amAccount: amAccountList) { + if (amAccount.name.equals(login)) { + return true; + } + } + return false; + } + + /** + * Remove a single pop/imap account from the AccountManager + * @param accountManager our AccountManager + * @param name the name of the test account to remove + */ + private void removeAccountManagerAccount(AccountManager accountManager, String name) { + try { + accountManager.removeAccount( + new android.accounts.Account(name, AccountManagerTypes.TYPE_POP_IMAP), + null, null).getResult(); + } catch (OperationCanceledException e) { + } catch (AuthenticatorException e) { + } catch (IOException e) { + } + } + + /** + * Remove all test accounts from the AccountManager + * @param accountManager the AccountManager + */ + private void cleanupTestAccountManagerAccounts(AccountManager accountManager) { + android.accounts.Account[] amAccountList = + accountManager.getAccountsByType(AccountManagerTypes.TYPE_POP_IMAP); + for (android.accounts.Account account: amAccountList) { + if (account.name.startsWith(AccountReconciler.ACCOUNT_MANAGER_ACCOUNT_TEST_PREFIX)) { + removeAccountManagerAccount(accountManager, account.name); + } + } + } + + /** Verifies updating the DB from v21 to v22 works as expected */ + public void testUpgradeFromVersion21ToVersion22() { + String imapTestLogin = + AccountReconciler.ACCOUNT_MANAGER_ACCOUNT_TEST_PREFIX + "imap.host.com"; + String pop3TestLogin = + AccountReconciler.ACCOUNT_MANAGER_ACCOUNT_TEST_PREFIX + "pop3.host.com"; + AccountManager accountManager = AccountManager.get(mContext); + + // Create provider accounts (one of each type) + Account a1 = createAccount(mMockContext, "exchange", + ProviderTestUtils.setupHostAuth("eas", "exchange.host.com", true, mMockContext), + null); + HostAuth h2 = + ProviderTestUtils.setupHostAuth("imap", "imap.host.com", false, mMockContext); + h2.mLogin = imapTestLogin; + h2.save(mMockContext); + Account a2 = createAccount(mMockContext, "imap", h2, + ProviderTestUtils.setupHostAuth("smtp", "smtp.host.com", true, mMockContext)); + HostAuth h3 = + ProviderTestUtils.setupHostAuth("pop3", "pop3.host.com", false, mMockContext); + h3.mLogin = pop3TestLogin; + h3.save(mMockContext); + Account a3 = createAccount(mMockContext, "pop3", h3, + ProviderTestUtils.setupHostAuth("smtp", "smtp.host.com", true, mMockContext)); + + // Get the current list of AccountManager accounts (we have to use the real context here), + // whereas we use the mock context for EmailProvider (this is because the mock context + // doesn't implement AccountManager hooks) + android.accounts.Account[] amAccountList = + accountManager.getAccountsByType(AccountManagerTypes.TYPE_POP_IMAP); + // There shouldn't be AccountManager accounts for these + assertFalse(amAccountListHasAccount(amAccountList, a1, mMockContext)); + assertFalse(amAccountListHasAccount(amAccountList, a2, mMockContext)); + assertFalse(amAccountListHasAccount(amAccountList, a3, mMockContext)); + + amAccountList = null; + try { + // Upgrade the database + SQLiteDatabase db = getProvider().getDatabase(mMockContext); + EmailProvider.upgradeFromVersion21ToVersion22(db, getContext()); + + // The pop3 and imap account should now be in account manager + amAccountList = accountManager.getAccountsByType(AccountManagerTypes.TYPE_POP_IMAP); + assertFalse(amAccountListHasAccount(amAccountList, a1, mMockContext)); + assertTrue(amAccountListHasAccount(amAccountList, a2, mMockContext)); + assertTrue(amAccountListHasAccount(amAccountList, a3, mMockContext)); + } finally { + cleanupTestAccountManagerAccounts(accountManager); + } + } }