diff options
Diffstat (limited to 'provider_src/com/android/email/provider/AccountReconciler.java')
-rw-r--r-- | provider_src/com/android/email/provider/AccountReconciler.java | 311 |
1 files changed, 311 insertions, 0 deletions
diff --git a/provider_src/com/android/email/provider/AccountReconciler.java b/provider_src/com/android/email/provider/AccountReconciler.java new file mode 100644 index 000000000..8031f17c9 --- /dev/null +++ b/provider_src/com/android/email/provider/AccountReconciler.java @@ -0,0 +1,311 @@ +/* + * Copyright (C) 2011 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.email.provider; + +import android.accounts.AccountManager; +import android.accounts.AccountManagerFuture; +import android.accounts.AuthenticatorException; +import android.accounts.OperationCanceledException; +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.Context; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.provider.CalendarContract; +import android.provider.ContactsContract; +import android.text.TextUtils; + +import com.android.email.R; +import com.android.email.NotificationController; +import com.android.email.NotificationControllerCreatorHolder; +import com.android.email.SecurityPolicy; +import com.android.email.service.EmailServiceUtils; +import com.android.email.service.EmailServiceUtils.EmailServiceInfo; +import com.android.emailcommon.Logging; +import com.android.emailcommon.provider.Account; +import com.android.emailcommon.provider.HostAuth; +import com.android.emailcommon.utility.MigrationUtils; +import com.android.mail.utils.LogUtils; +import com.google.common.collect.ImmutableList; + +import java.io.IOException; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; + +public class AccountReconciler { + /** + * 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); + + // TODO: Consider getting the types programmatically, in case we add more types. + // Some Accounts types can be identical, the set de-duplicates. + final LinkedHashSet<String> accountTypes = new LinkedHashSet<String>(); + accountTypes.add(context.getString(R.string.account_manager_type_legacy_imap)); + accountTypes.add(context.getString(R.string.account_manager_type_pop3)); + accountTypes.add(context.getString(R.string.account_manager_type_exchange)); + + final ImmutableList.Builder<android.accounts.Account> builder = ImmutableList.builder(); + for (final String type : accountTypes) { + final android.accounts.Account[] accounts = am.getAccountsByType(type); + builder.add(accounts); + } + return builder.build(); + } + + /** + * 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}. + */ + 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(); + } + + 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(); + } + + /** + * 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. + * + * This function may not be called from the main/UI thread, because it makes blocking calls + * into the account manager. + * + * @param context The context in which to operate + */ + public static synchronized 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, final String type) { + for (final android.accounts.Account account : accounts) { + if (account.name.equalsIgnoreCase(name) && account.type.equalsIgnoreCase(type)) { + 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; + } + + /** + * Internal method to actually perform reconciliation, or simply check that it needs to be done + * and avoid doing any heavy work, depending on the value of the passed in + * {@code performReconciliation}. + */ + private static boolean reconcileAccountsInternal( + final Context context, + final List<Account> emailProviderAccounts, + final List<android.accounts.Account> accountManagerAccounts, + final boolean performReconciliation) { + boolean needsReconciling = false; + int accountsDeleted = 0; + boolean exchangeAccountDeleted = false; + + LogUtils.d(Logging.LOG_TAG, "reconcileAccountsInternal"); + + if (MigrationUtils.migrationInProgress()) { + LogUtils.d(Logging.LOG_TAG, "deferring reconciliation, migration in progress"); + return false; + } + + // See if we should have the Eas authenticators enabled. + if (!EmailServiceUtils.isServiceAvailable(context, + context.getString(R.string.protocol_eas))) { + EmailServiceUtils.disableExchangeComponents(context); + } else { + EmailServiceUtils.enableExchangeComponent(context); + } + // First, look through our EmailProvider accounts to make sure there's a corresponding + // AccountManager account + for (final Account providerAccount : emailProviderAccounts) { + final String providerAccountName = providerAccount.mEmailAddress; + final EmailServiceUtils.EmailServiceInfo infoForAccount = EmailServiceUtils + .getServiceInfoForAccount(context, providerAccount.mId); + + // We want to delete the account if there is no matching Account Manager account for it + // unless it is flagged as incomplete. We also want to delete it if we can't find + // an accountInfo object for it. + if (infoForAccount == null || !hasAmAccount( + accountManagerAccounts, providerAccountName, infoForAccount.accountType)) { + if (infoForAccount != null && + (providerAccount.mFlags & Account.FLAGS_INCOMPLETE) != 0) { + LogUtils.w(Logging.LOG_TAG, + "Account reconciler noticed incomplete account; ignoring"); + continue; + } + + needsReconciling = true; + if (performReconciliation) { + // This account has been deleted in the AccountManager! + LogUtils.d(Logging.LOG_TAG, + "Account deleted in AccountManager; deleting from provider: " + + providerAccountName); + // See if this is an exchange account + final HostAuth auth = providerAccount.getOrCreateHostAuthRecv(context); + LogUtils.d(Logging.LOG_TAG, "deleted account with hostAuth " + auth); + if (auth != null && TextUtils.equals(auth.mProtocol, + context.getString(R.string.protocol_eas))) { + exchangeAccountDeleted = true; + } + // Cancel all notifications for this account + final NotificationController nc = + NotificationControllerCreatorHolder.getInstance(context); + if (nc != null) { + nc.cancelNotifications(context, providerAccount); + } + + context.getContentResolver().delete( + EmailProvider.uiUri("uiaccount", providerAccount.mId), null, null); + + accountsDeleted++; + + } + } + } + // Now, look through AccountManager accounts to make sure we have a corresponding cached EAS + // account from EmailProvider + boolean needsPolicyUpdate = false; + 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; + + if (performReconciliation) { + LogUtils.d(Logging.LOG_TAG, + "Account deleted from provider; deleting from AccountManager: " + + accountManagerAccountName); + // Delete the account + AccountManagerFuture<Boolean> blockingResult = AccountManager.get(context) + .removeAccount(accountManagerAccount, null, null); + try { + // Note: All of the potential errors from removeAccount() are simply logged + // here, as there is nothing to actually do about them. + blockingResult.getResult(); + } catch (OperationCanceledException e) { + LogUtils.w(Logging.LOG_TAG, e.toString()); + } catch (AuthenticatorException e) { + LogUtils.w(Logging.LOG_TAG, e.toString()); + } catch (IOException e) { + LogUtils.w(Logging.LOG_TAG, e.toString()); + } + // Just set a flag that our policies need to be updated with device + // So we can do the update, one time, at a later point in time. + needsPolicyUpdate = true; + } + } else { + // Fix up the Calendar and Contacts syncing. It used to be possible for IMAP and + // POP accounts to get calendar and contacts syncing enabled. + // See b/11818312 + final String accountType = accountManagerAccount.type; + final String protocol = EmailServiceUtils.getProtocolFromAccountType( + context, accountType); + final EmailServiceInfo info = EmailServiceUtils.getServiceInfo(context, protocol); + if (info == null || !info.syncCalendar) { + ContentResolver.setIsSyncable(accountManagerAccount, + CalendarContract.AUTHORITY, 0); + } + if (info == null || !info.syncContacts) { + ContentResolver.setIsSyncable(accountManagerAccount, + ContactsContract.AUTHORITY, 0); + } + } + } + + if (needsPolicyUpdate) { + // We have removed accounts from the AccountManager, let's make sure that + // our policies are up to date. + SecurityPolicy.getInstance(context).policiesUpdated(); + } + + final String composeActivityName = + context.getString(R.string.reconciliation_compose_activity_name); + if (!TextUtils.isEmpty(composeActivityName)) { + // If there are no accounts remaining after reconciliation, disable the compose activity + final boolean enableCompose = emailProviderAccounts.size() - accountsDeleted > 0; + final ComponentName componentName = new ComponentName(context, composeActivityName); + context.getPackageManager().setComponentEnabledSetting(componentName, + enableCompose ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : + PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + PackageManager.DONT_KILL_APP); + LogUtils.d(LogUtils.TAG, "Setting compose activity to " + + (enableCompose ? "enabled" : "disabled")); + } + + + // If an account has been deleted, the simplest thing is just to kill our process. + // Otherwise we might have a service running trying to do something for the account + // which has been deleted, which can get NPEs. It's not as clean is it could be, but + // it still works pretty well because there is nowhere in the email app to delete the + // account. You have to go to Settings, so it's not user visible that the Email app + // has been killed. + if (accountsDeleted > 0) { + LogUtils.i(Logging.LOG_TAG, "Restarting because account deleted"); + if (exchangeAccountDeleted) { + EmailServiceUtils.killService(context, context.getString(R.string.protocol_eas)); + } + System.exit(-1); + } + + return needsReconciling; + } +} |