| /* |
| * 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; |
| } |
| } |