blob: 328e329af094f2deebe17f7b73519c0c89354b56 [file] [log] [blame]
/*
* 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.Context;
import android.database.Cursor;
import android.text.TextUtils;
import com.android.email.NotificationController;
import com.android.email.R;
import com.android.email.service.EmailServiceUtils;
import com.android.emailcommon.Logging;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.EmailContent;
import com.android.emailcommon.provider.HostAuth;
import com.android.emailcommon.provider.Mailbox;
import com.android.mail.utils.LogUtils;
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 {
/**
* 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();
}
/**
* 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 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;
boolean accountDeleted = false;
boolean exchangeAccountDeleted = false;
LogUtils.d(Logging.LOG_TAG, "reconcileAccountsInternal");
// 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
NotificationController.cancelNotifications(context, providerAccount);
context.getContentResolver().delete(
EmailProvider.uiUri("uiaccount", providerAccount.mId), null, null);
accountDeleted = true;
}
}
}
// Now, look through AccountManager accounts to make sure we have a corresponding cached EAS
// account from EmailProvider
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());
}
}
}
}
// 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 (accountDeleted) {
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;
}
}