Simplify account reconciliation.

- Rather than handle by type, do them all at once.
- Simplify when reconciliation happens.

Bug: 9056861

Change-Id: If264678c82c63090246ef8ff857c8e46f6672c85
This commit is contained in:
Yu Ping Hu 2013-07-26 14:18:39 -07:00
parent 3d2f8b4be8
commit afe097f318
6 changed files with 99 additions and 219 deletions

View File

@ -21,42 +21,64 @@ import android.accounts.AccountManagerFuture;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.content.Context;
import android.net.Uri;
import android.database.Cursor;
import com.android.email.NotificationController;
import com.android.email.R;
import com.android.emailcommon.Logging;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.EmailContent;
import com.android.emailcommon.provider.Mailbox;
import com.android.mail.utils.LogUtils;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
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
static final String ACCOUNT_MANAGER_ACCOUNT_TEST_PREFIX = " _";
/**
* Get all AccountManager accounts for all email types.
* @param context Our {@link Context}.
* @return A list of all {@link android.accounts.Account}s created by our app.
*/
private static List<android.accounts.Account> getAllAmAccounts(final Context context) {
final AccountManager am = AccountManager.get(context);
final ImmutableList.Builder<android.accounts.Account> builder = ImmutableList.builder();
// TODO: Consider getting the types programmatically, in case we add more types.
builder.addAll(Arrays.asList(am.getAccountsByType(
context.getString(R.string.account_manager_type_legacy_imap))));
builder.addAll(Arrays.asList(am.getAccountsByType(
context.getString(R.string.account_manager_type_pop3))));
builder.addAll(Arrays.asList(am.getAccountsByType(
context.getString(R.string.account_manager_type_exchange))));
return builder.build();
}
/**
* Checks two account lists to see if there is any reconciling to be done. Can be done on the
* UI thread.
* @param context the app context
* @param emailProviderAccounts accounts as reported in the Email provider
* @param accountManagerAccounts accounts as reported by the system account manager, for the
* particular protocol types that match emailProviderAccounts
* Get a all {@link Account} objects from the {@link EmailProvider}.
* @param context Our {@link Context}.
* @return A list of all {@link Account}s from the {@link EmailProvider}.
*/
public static boolean accountsNeedReconciling(
final Context context,
List<Account> emailProviderAccounts,
android.accounts.Account[] accountManagerAccounts) {
private static List<Account> getAllEmailProviderAccounts(final Context context) {
final Cursor c = context.getContentResolver().query(Account.CONTENT_URI,
Account.CONTENT_PROJECTION, null, null, null);
if (c == null) {
return Collections.emptyList();
}
return reconcileAccountsInternal(
context, emailProviderAccounts, accountManagerAccounts,
context, false /* performReconciliation */);
final ImmutableList.Builder<Account> builder = ImmutableList.builder();
try {
while (c.moveToNext()) {
final Account account = new Account();
account.restore(c);
builder.add(account);
}
} finally {
c.close();
}
return builder.build();
}
/**
@ -71,18 +93,42 @@ public class AccountReconciler {
* into the account manager.
*
* @param context The context in which to operate
* @param emailProviderAccounts the exchange provider accounts to work from
* @param accountManagerAccounts The account manager accounts to work from
* @param providerContext application provider context
*/
public static void reconcileAccounts(
Context context,
List<Account> emailProviderAccounts,
android.accounts.Account[] accountManagerAccounts,
Context providerContext) {
reconcileAccountsInternal(
context, emailProviderAccounts, accountManagerAccounts,
providerContext, true /* performReconciliation */);
public static void reconcileAccounts(final Context context) {
final List<android.accounts.Account> amAccounts = getAllAmAccounts(context);
final List<Account> providerAccounts = getAllEmailProviderAccounts(context);
reconcileAccountsInternal(context, providerAccounts, amAccounts, true);
}
/**
* Check if the AccountManager accounts list contains a specific account.
* @param accounts The list of {@link android.accounts.Account} objects.
* @param name The name of the account to find.
* @return Whether the account is in the list.
*/
private static boolean hasAmAccount(final List<android.accounts.Account> accounts,
final String name) {
for (final android.accounts.Account account : accounts) {
if (account.name.equalsIgnoreCase(name)) {
return true;
}
}
return false;
}
/**
* Check if the EmailProvider accounts list contains a specific account.
* @param accounts The list of {@link Account} objects.
* @param name The name of the account to find.
* @return Whether the account is in the list.
*/
private static boolean hasEpAccount(final List<Account> accounts, final String name) {
for (final Account account : accounts) {
if (account.mEmailAddress.equalsIgnoreCase(name)) {
return true;
}
}
return false;
}
/**
@ -91,31 +137,23 @@ public class AccountReconciler {
* {@code performReconciliation}.
*/
private static boolean reconcileAccountsInternal(
Context context,
List<Account> emailProviderAccounts,
android.accounts.Account[] accountManagerAccounts,
Context providerContext,
boolean performReconciliation) {
final Context context,
final List<Account> emailProviderAccounts,
final List<android.accounts.Account> accountManagerAccounts,
final boolean performReconciliation) {
boolean needsReconciling = false;
// First, look through our EmailProvider accounts to make sure there's a corresponding
// AccountManager account
for (Account providerAccount: emailProviderAccounts) {
String providerAccountName = providerAccount.mEmailAddress;
boolean found = false;
for (android.accounts.Account accountManagerAccount: accountManagerAccounts) {
if (accountManagerAccount.name.equalsIgnoreCase(providerAccountName)) {
found = true;
break;
}
}
if (!found) {
for (final Account providerAccount : emailProviderAccounts) {
final String providerAccountName = providerAccount.mEmailAddress;
if (!hasAmAccount(accountManagerAccounts, providerAccountName)) {
if ((providerAccount.mFlags & Account.FLAGS_INCOMPLETE) != 0) {
// Do another test before giving up; an incomplete account shouldn't have
// any mailboxes (the incomplete flag is used to prevent reconciliation
// between the time the EP account is created and when the AM account is
// asynchronously created)
if (EmailContent.count(providerContext, Mailbox.CONTENT_URI,
if (EmailContent.count(context, Mailbox.CONTENT_URI,
Mailbox.ACCOUNT_KEY + "=?",
new String[] { Long.toString(providerAccount.mId) } ) > 0) {
LogUtils.w(Logging.LOG_TAG,
@ -133,8 +171,8 @@ public class AccountReconciler {
LogUtils.d(Logging.LOG_TAG,
"Account deleted in AccountManager; deleting from provider: " +
providerAccountName);
Uri uri = EmailProvider.uiUri("uiaccount", providerAccount.mId);
context.getContentResolver().delete(uri, null, null);
context.getContentResolver().delete(
EmailProvider.uiUri("uiaccount", providerAccount.mId), null, null);
// Cancel all notifications for this account
NotificationController.cancelNotifications(context, providerAccount);
@ -143,18 +181,9 @@ public class AccountReconciler {
}
// 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: emailProviderAccounts) {
if (cachedEasAccount.mEmailAddress.equalsIgnoreCase(accountManagerAccountName)) {
found = true;
}
}
if (accountManagerAccountName.startsWith(ACCOUNT_MANAGER_ACCOUNT_TEST_PREFIX)) {
found = true;
}
if (!found) {
for (final android.accounts.Account accountManagerAccount : accountManagerAccounts) {
final String accountManagerAccountName = accountManagerAccount.name;
if (!hasEpAccount(emailProviderAccounts, accountManagerAccountName)) {
// This account has been deleted from the EmailProvider database
needsReconciling = true;

View File

@ -16,27 +16,22 @@
package com.android.email.service;
import android.accounts.AccountManager;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle;
import android.os.IBinder;
import com.android.email.NotificationController;
import com.android.email.ResourceHelper;
import com.android.email.provider.AccountReconciler;
import com.android.email2.ui.MailActivityEmail;
import com.android.emailcommon.Configuration;
import com.android.emailcommon.Device;
import com.android.emailcommon.VendorPolicyLoader;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.service.IAccountService;
import com.android.emailcommon.utility.EmailAsyncTask;
import java.io.IOException;
import java.util.ArrayList;
public class AccountService extends Service {
@ -56,33 +51,9 @@ public class AccountService extends Service {
NotificationController.getInstance(mContext).cancelLoginFailedNotification(accountId);
}
private ArrayList<Account> getAccountList(String forProtocol) {
ArrayList<Account> providerAccounts = new ArrayList<Account>();
Cursor c = mContext.getContentResolver().query(Account.CONTENT_URI,
Account.ID_PROJECTION, null, null, null);
try {
while (c.moveToNext()) {
long accountId = c.getLong(Account.CONTENT_ID_COLUMN);
String protocol = Account.getProtocol(mContext, accountId);
if ((protocol != null) && forProtocol.equals(protocol)) {
Account account = Account.restoreAccountWithId(mContext, accountId);
if (account != null) {
providerAccounts.add(account);
}
}
}
} finally {
c.close();
}
return providerAccounts;
}
@Override
public void reconcileAccounts(String protocol, String accountManagerType) {
ArrayList<Account> providerList = getAccountList(protocol);
android.accounts.Account[] accountMgrList =
AccountManager.get(mContext).getAccountsByType(accountManagerType);
AccountReconciler.reconcileAccounts(mContext, providerList, accountMgrList, mContext);
// TODO: No longer used, delete this.
}
@Override

View File

@ -32,6 +32,7 @@ import com.android.email.Preferences;
import com.android.email.R;
import com.android.email.SecurityPolicy;
import com.android.email.activity.setup.AccountSettings;
import com.android.email.provider.AccountReconciler;
import com.android.emailcommon.Logging;
import com.android.emailcommon.VendorPolicyLoader;
import com.android.emailcommon.provider.Account;
@ -184,7 +185,7 @@ public class EmailBroadcastProcessorService extends IntentService {
private void reconcileAndStartServices() {
// Reconcile accounts
MailService.reconcileLocalAccountsSync(this);
AccountReconciler.reconcileAccounts(this);
// Starts remote services, if any
EmailServiceUtils.startRemoteServices(this);
}

View File

@ -29,6 +29,7 @@ import android.text.TextUtils;
import com.android.email.NotificationController;
import com.android.email.mail.Sender;
import com.android.email.mail.Store;
import com.android.email.provider.AccountReconciler;
import com.android.email.provider.Utilities;
import com.android.email.service.EmailServiceUtils.EmailServiceInfo;
import com.android.email2.ui.MailActivityEmail;
@ -477,7 +478,7 @@ public abstract class EmailServiceStub extends IEmailService.Stub implements IEm
@Override
public void deleteAccountPIMData(final String emailAddress) throws RemoteException {
MailService.reconcileLocalAccountsSync(mContext);
AccountReconciler.reconcileAccounts(mContext);
}
@Override

View File

@ -22,7 +22,6 @@ import android.accounts.AccountManagerFuture;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle;
import android.os.IBinder;
@ -31,22 +30,17 @@ import com.android.email.service.EmailServiceUtils.EmailServiceInfo;
import com.android.email2.ui.MailActivityEmail;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.HostAuth;
import com.android.emailcommon.utility.EmailAsyncTask;
import com.android.mail.utils.LogUtils;
import com.google.common.annotations.VisibleForTesting;
import java.util.ArrayList;
import java.util.List;
/**
* Legacy service, now used mainly for account reconciliation
* TODO: Eliminate this service, since it doesn't actually do anything.
*/
public class MailService extends Service {
@Override
public int onStartCommand(final Intent intent, int flags, final int startId) {
super.onStartCommand(intent, flags, startId);
reconcileLocalAccountsSync(this);
AccountReconciler.reconcileAccounts(this);
// Make sure our services are running, if necessary
MailActivityEmail.setServicesEnabledAsync(this);
return START_STICKY;
@ -57,78 +51,6 @@ public class MailService extends Service {
return null;
}
public static ArrayList<Account> getAccountList(Context context, String protocol) {
ArrayList<Account> providerAccounts = new ArrayList<Account>();
Cursor c = context.getContentResolver().query(Account.CONTENT_URI, Account.ID_PROJECTION,
null, null, null);
try {
while (c.moveToNext()) {
long accountId = c.getLong(Account.CONTENT_ID_COLUMN);
if (protocol.equals(Account.getProtocol(context, accountId))) {
Account account = Account.restoreAccountWithId(context, accountId);
if (account != null) {
providerAccounts.add(account);
}
}
}
} finally {
c.close();
}
return providerAccounts;
}
/**
* Reconcile local (i.e. non-remote) accounts.
*/
public static void reconcileLocalAccountsSync(Context context) {
List<EmailServiceInfo> serviceList = EmailServiceUtils.getServiceInfoList(context);
for (EmailServiceInfo info: serviceList) {
if (info.klass != null) {
new AccountReconcilerTask(context, info).runAsync();
}
}
}
static class AccountReconcilerTask implements Runnable {
private final Context mContext;
private final EmailServiceInfo mInfo;
AccountReconcilerTask(Context context, EmailServiceInfo info) {
mContext = context;
mInfo = info;
}
public void runAsync() {
EmailAsyncTask.runAsyncSerial(this);
}
@Override
public void run() {
LogUtils.d("MailService", "Reconciling accounts of type " + mInfo.accountType +
", protocol " + mInfo.protocol);
android.accounts.Account[] accountManagerAccounts = AccountManager.get(mContext)
.getAccountsByType(mInfo.accountType);
ArrayList<Account> providerAccounts = getAccountList(mContext, mInfo.protocol);
reconcileAccountsWithAccountManager(mContext, providerAccounts,
accountManagerAccounts, mContext);
}
}
/**
* See Utility.reconcileAccounts for details
* @param context The context in which to operate
* @param emailProviderAccounts the exchange provider accounts to work from
* @param accountManagerAccounts The account manager accounts to work from
* @param providerContext the provider's context (in unit tests, this may differ from context)
*/
@VisibleForTesting
public static void reconcileAccountsWithAccountManager(Context context,
List<Account> emailProviderAccounts, android.accounts.Account[] accountManagerAccounts,
Context providerContext) {
AccountReconciler.reconcileAccounts(context, emailProviderAccounts, accountManagerAccounts,
providerContext);
}
public static AccountManagerFuture<Bundle> setupAccountManagerAccount(Context context,
Account account, boolean email, boolean calendar, boolean contacts,
AccountManagerCallback<Bundle> callback) {

View File

@ -167,50 +167,6 @@ public class MailServiceTests extends AccountTestCase {
assertEquals(getTestAccountEmailAddress("3"), accountManagerAccounts[0].name);
}
public void testReconcileDetection() {
Context context = getContext();
List<Account> providerAccounts;
android.accounts.Account[] accountManagerAccounts;
android.accounts.Account[] baselineAccounts =
AccountManager.get(context).getAccountsByType(TEST_ACCOUNT_TYPE);
// Empty lists match.
providerAccounts = new ArrayList<Account>();
accountManagerAccounts = new android.accounts.Account[0];
assertFalse(AccountReconciler.accountsNeedReconciling(
context, providerAccounts, accountManagerAccounts));
setupProviderAndAccountManagerAccount(getTestAccountName("1"));
accountManagerAccounts = getAccountManagerAccounts(baselineAccounts);
providerAccounts = makeExchangeServiceAccountList();
// A single account, but empty list on the other side is detected as needing reconciliation
assertTrue(AccountReconciler.accountsNeedReconciling(
context, new ArrayList<Account>(), accountManagerAccounts));
assertTrue(AccountReconciler.accountsNeedReconciling(
context, providerAccounts, new android.accounts.Account[0]));
// Note that no reconciliation should have happened though - we just wanted to detect it.
assertEquals(1, makeExchangeServiceAccountList().size());
assertEquals(1, getAccountManagerAccounts(baselineAccounts).length);
// Single account matches - no reconciliation should be detected.
assertFalse(AccountReconciler.accountsNeedReconciling(
context, providerAccounts, accountManagerAccounts));
// Provider: 1,2,3. AccountManager: 1, 3.
String username = getTestAccountName("2");
ProviderTestUtils.setupAccount(getTestAccountName("2"), true, getMockContext());
setupProviderAndAccountManagerAccount(getTestAccountName("3"));
accountManagerAccounts = getAccountManagerAccounts(baselineAccounts);
providerAccounts = makeExchangeServiceAccountList();
assertTrue(AccountReconciler.accountsNeedReconciling(
context, providerAccounts, accountManagerAccounts));
}
/**
* Lightweight subclass of the Controller class allows injection of mock context
*/