1/* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.email.provider; 18 19import android.accounts.AccountManager; 20import android.accounts.AccountManagerFuture; 21import android.accounts.AuthenticatorException; 22import android.accounts.OperationCanceledException; 23import android.content.Context; 24import android.database.Cursor; 25import android.text.TextUtils; 26 27import com.android.email.NotificationController; 28import com.android.email.R; 29import com.android.email.service.EmailServiceUtils; 30import com.android.emailcommon.Logging; 31import com.android.emailcommon.provider.Account; 32import com.android.emailcommon.provider.EmailContent; 33import com.android.emailcommon.provider.HostAuth; 34import com.android.emailcommon.provider.Mailbox; 35import com.android.mail.utils.LogUtils; 36import com.google.common.collect.ImmutableList; 37 38import java.io.IOException; 39import java.util.Arrays; 40import java.util.Collections; 41import java.util.List; 42 43public class AccountReconciler { 44 /** 45 * Get all AccountManager accounts for all email types. 46 * @param context Our {@link Context}. 47 * @return A list of all {@link android.accounts.Account}s created by our app. 48 */ 49 private static List<android.accounts.Account> getAllAmAccounts(final Context context) { 50 final AccountManager am = AccountManager.get(context); 51 final ImmutableList.Builder<android.accounts.Account> builder = ImmutableList.builder(); 52 // TODO: Consider getting the types programmatically, in case we add more types. 53 builder.addAll(Arrays.asList(am.getAccountsByType( 54 context.getString(R.string.account_manager_type_legacy_imap)))); 55 builder.addAll(Arrays.asList(am.getAccountsByType( 56 context.getString(R.string.account_manager_type_pop3)))); 57 builder.addAll(Arrays.asList(am.getAccountsByType( 58 context.getString(R.string.account_manager_type_exchange)))); 59 return builder.build(); 60 } 61 62 /** 63 * Get a all {@link Account} objects from the {@link EmailProvider}. 64 * @param context Our {@link Context}. 65 * @return A list of all {@link Account}s from the {@link EmailProvider}. 66 */ 67 private static List<Account> getAllEmailProviderAccounts(final Context context) { 68 final Cursor c = context.getContentResolver().query(Account.CONTENT_URI, 69 Account.CONTENT_PROJECTION, null, null, null); 70 if (c == null) { 71 return Collections.emptyList(); 72 } 73 74 final ImmutableList.Builder<Account> builder = ImmutableList.builder(); 75 try { 76 while (c.moveToNext()) { 77 final Account account = new Account(); 78 account.restore(c); 79 builder.add(account); 80 } 81 } finally { 82 c.close(); 83 } 84 return builder.build(); 85 } 86 87 /** 88 * Compare our account list (obtained from EmailProvider) with the account list owned by 89 * AccountManager. If there are any orphans (an account in one list without a corresponding 90 * account in the other list), delete the orphan, as these must remain in sync. 91 * 92 * Note that the duplication of account information is caused by the Email application's 93 * incomplete integration with AccountManager. 94 * 95 * This function may not be called from the main/UI thread, because it makes blocking calls 96 * into the account manager. 97 * 98 * @param context The context in which to operate 99 */ 100 public static void reconcileAccounts(final Context context) { 101 final List<android.accounts.Account> amAccounts = getAllAmAccounts(context); 102 final List<Account> providerAccounts = getAllEmailProviderAccounts(context); 103 reconcileAccountsInternal(context, providerAccounts, amAccounts, true); 104 } 105 106 /** 107 * Check if the AccountManager accounts list contains a specific account. 108 * @param accounts The list of {@link android.accounts.Account} objects. 109 * @param name The name of the account to find. 110 * @return Whether the account is in the list. 111 */ 112 private static boolean hasAmAccount(final List<android.accounts.Account> accounts, 113 final String name, final String type) { 114 for (final android.accounts.Account account : accounts) { 115 if (account.name.equalsIgnoreCase(name) && account.type.equalsIgnoreCase(type)) { 116 return true; 117 } 118 } 119 return false; 120 } 121 122 /** 123 * Check if the EmailProvider accounts list contains a specific account. 124 * @param accounts The list of {@link Account} objects. 125 * @param name The name of the account to find. 126 * @return Whether the account is in the list. 127 */ 128 private static boolean hasEpAccount(final List<Account> accounts, final String name) { 129 for (final Account account : accounts) { 130 if (account.mEmailAddress.equalsIgnoreCase(name)) { 131 return true; 132 } 133 } 134 return false; 135 } 136 137 /** 138 * Internal method to actually perform reconciliation, or simply check that it needs to be done 139 * and avoid doing any heavy work, depending on the value of the passed in 140 * {@code performReconciliation}. 141 */ 142 private static boolean reconcileAccountsInternal( 143 final Context context, 144 final List<Account> emailProviderAccounts, 145 final List<android.accounts.Account> accountManagerAccounts, 146 final boolean performReconciliation) { 147 boolean needsReconciling = false; 148 boolean accountDeleted = false; 149 boolean exchangeAccountDeleted = false; 150 151 LogUtils.d(Logging.LOG_TAG, "reconcileAccountsInternal"); 152 153 // First, look through our EmailProvider accounts to make sure there's a corresponding 154 // AccountManager account 155 for (final Account providerAccount : emailProviderAccounts) { 156 final String providerAccountName = providerAccount.mEmailAddress; 157 final EmailServiceUtils.EmailServiceInfo infoForAccount = EmailServiceUtils 158 .getServiceInfoForAccount(context, providerAccount.mId); 159 160 // We want to delete the account if there is no matching Account Manager account for it 161 // unless it is flagged as incomplete. We also want to delete it if we can't find 162 // an accountInfo object for it. 163 if (infoForAccount == null || !hasAmAccount( 164 accountManagerAccounts, providerAccountName, infoForAccount.accountType)) { 165 if (infoForAccount != null && 166 (providerAccount.mFlags & Account.FLAGS_INCOMPLETE) != 0) { 167 LogUtils.w(Logging.LOG_TAG, 168 "Account reconciler noticed incomplete account; ignoring"); 169 continue; 170 } 171 172 needsReconciling = true; 173 if (performReconciliation) { 174 // This account has been deleted in the AccountManager! 175 LogUtils.d(Logging.LOG_TAG, 176 "Account deleted in AccountManager; deleting from provider: " + 177 providerAccountName); 178 // See if this is an exchange account 179 final HostAuth auth = providerAccount.getOrCreateHostAuthRecv(context); 180 LogUtils.d(Logging.LOG_TAG, "deleted account with hostAuth " + auth); 181 if (auth != null && TextUtils.equals(auth.mProtocol, 182 context.getString(R.string.protocol_eas))) { 183 exchangeAccountDeleted = true; 184 } 185 // Cancel all notifications for this account 186 NotificationController.cancelNotifications(context, providerAccount); 187 188 context.getContentResolver().delete( 189 EmailProvider.uiUri("uiaccount", providerAccount.mId), null, null); 190 191 accountDeleted = true; 192 193 } 194 } 195 } 196 // Now, look through AccountManager accounts to make sure we have a corresponding cached EAS 197 // account from EmailProvider 198 for (final android.accounts.Account accountManagerAccount : accountManagerAccounts) { 199 final String accountManagerAccountName = accountManagerAccount.name; 200 if (!hasEpAccount(emailProviderAccounts, accountManagerAccountName)) { 201 // This account has been deleted from the EmailProvider database 202 needsReconciling = true; 203 204 if (performReconciliation) { 205 LogUtils.d(Logging.LOG_TAG, 206 "Account deleted from provider; deleting from AccountManager: " + 207 accountManagerAccountName); 208 // Delete the account 209 AccountManagerFuture<Boolean> blockingResult = AccountManager.get(context) 210 .removeAccount(accountManagerAccount, null, null); 211 try { 212 // Note: All of the potential errors from removeAccount() are simply logged 213 // here, as there is nothing to actually do about them. 214 blockingResult.getResult(); 215 } catch (OperationCanceledException e) { 216 LogUtils.w(Logging.LOG_TAG, e.toString()); 217 } catch (AuthenticatorException e) { 218 LogUtils.w(Logging.LOG_TAG, e.toString()); 219 } catch (IOException e) { 220 LogUtils.w(Logging.LOG_TAG, e.toString()); 221 } 222 } 223 } 224 } 225 226 // If an account has been deleted, the simplest thing is just to kill our process. 227 // Otherwise we might have a service running trying to do something for the account 228 // which has been deleted, which can get NPEs. It's not as clean is it could be, but 229 // it still works pretty well because there is nowhere in the email app to delete the 230 // account. You have to go to Settings, so it's not user visible that the Email app 231 // has been killed. 232 if (accountDeleted) { 233 LogUtils.i(Logging.LOG_TAG, "Restarting because account deleted"); 234 if (exchangeAccountDeleted) { 235 EmailServiceUtils.killService(context, context.getString(R.string.protocol_eas)); 236 } 237 System.exit(-1); 238 } 239 240 return needsReconciling; 241 } 242} 243