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.ComponentName; 24import android.content.ContentResolver; 25import android.content.Context; 26import android.content.pm.PackageManager; 27import android.database.Cursor; 28import android.provider.CalendarContract; 29import android.provider.ContactsContract; 30import android.text.TextUtils; 31 32import com.android.email.NotificationController; 33import com.android.email.R; 34import com.android.email.service.EmailServiceUtils; 35import com.android.email.service.EmailServiceUtils.EmailServiceInfo; 36import com.android.emailcommon.Logging; 37import com.android.emailcommon.provider.Account; 38import com.android.emailcommon.provider.HostAuth; 39import com.android.mail.utils.LogUtils; 40import com.google.common.collect.ImmutableList; 41 42import java.io.IOException; 43import java.util.Arrays; 44import java.util.Collections; 45import java.util.LinkedHashSet; 46import java.util.List; 47 48public class AccountReconciler { 49 /** 50 * Get all AccountManager accounts for all email types. 51 * @param context Our {@link Context}. 52 * @return A list of all {@link android.accounts.Account}s created by our app. 53 */ 54 private static List<android.accounts.Account> getAllAmAccounts(final Context context) { 55 final AccountManager am = AccountManager.get(context); 56 57 // TODO: Consider getting the types programmatically, in case we add more types. 58 // Some Accounts types can be identical, the set de-duplicates. 59 final LinkedHashSet<String> accountTypes = new LinkedHashSet<String>(); 60 accountTypes.add(context.getString(R.string.account_manager_type_legacy_imap)); 61 accountTypes.add(context.getString(R.string.account_manager_type_pop3)); 62 accountTypes.add(context.getString(R.string.account_manager_type_exchange)); 63 64 final ImmutableList.Builder<android.accounts.Account> builder = ImmutableList.builder(); 65 for (final String type : accountTypes) { 66 final android.accounts.Account[] accounts = am.getAccountsByType(type); 67 builder.add(accounts); 68 } 69 return builder.build(); 70 } 71 72 /** 73 * Get a all {@link Account} objects from the {@link EmailProvider}. 74 * @param context Our {@link Context}. 75 * @return A list of all {@link Account}s from the {@link EmailProvider}. 76 */ 77 private static List<Account> getAllEmailProviderAccounts(final Context context) { 78 final Cursor c = context.getContentResolver().query(Account.CONTENT_URI, 79 Account.CONTENT_PROJECTION, null, null, null); 80 if (c == null) { 81 return Collections.emptyList(); 82 } 83 84 final ImmutableList.Builder<Account> builder = ImmutableList.builder(); 85 try { 86 while (c.moveToNext()) { 87 final Account account = new Account(); 88 account.restore(c); 89 builder.add(account); 90 } 91 } finally { 92 c.close(); 93 } 94 return builder.build(); 95 } 96 97 /** 98 * Compare our account list (obtained from EmailProvider) with the account list owned by 99 * AccountManager. If there are any orphans (an account in one list without a corresponding 100 * account in the other list), delete the orphan, as these must remain in sync. 101 * 102 * Note that the duplication of account information is caused by the Email application's 103 * incomplete integration with AccountManager. 104 * 105 * This function may not be called from the main/UI thread, because it makes blocking calls 106 * into the account manager. 107 * 108 * @param context The context in which to operate 109 */ 110 public static synchronized void reconcileAccounts(final Context context) { 111 final List<android.accounts.Account> amAccounts = getAllAmAccounts(context); 112 final List<Account> providerAccounts = getAllEmailProviderAccounts(context); 113 reconcileAccountsInternal(context, providerAccounts, amAccounts, true); 114 } 115 116 /** 117 * Check if the AccountManager accounts list contains a specific account. 118 * @param accounts The list of {@link android.accounts.Account} objects. 119 * @param name The name of the account to find. 120 * @return Whether the account is in the list. 121 */ 122 private static boolean hasAmAccount(final List<android.accounts.Account> accounts, 123 final String name, final String type) { 124 for (final android.accounts.Account account : accounts) { 125 if (account.name.equalsIgnoreCase(name) && account.type.equalsIgnoreCase(type)) { 126 return true; 127 } 128 } 129 return false; 130 } 131 132 /** 133 * Check if the EmailProvider accounts list contains a specific account. 134 * @param accounts The list of {@link Account} objects. 135 * @param name The name of the account to find. 136 * @return Whether the account is in the list. 137 */ 138 private static boolean hasEpAccount(final List<Account> accounts, final String name) { 139 for (final Account account : accounts) { 140 if (account.mEmailAddress.equalsIgnoreCase(name)) { 141 return true; 142 } 143 } 144 return false; 145 } 146 147 /** 148 * Internal method to actually perform reconciliation, or simply check that it needs to be done 149 * and avoid doing any heavy work, depending on the value of the passed in 150 * {@code performReconciliation}. 151 */ 152 private static boolean reconcileAccountsInternal( 153 final Context context, 154 final List<Account> emailProviderAccounts, 155 final List<android.accounts.Account> accountManagerAccounts, 156 final boolean performReconciliation) { 157 boolean needsReconciling = false; 158 int accountsDeleted = 0; 159 boolean exchangeAccountDeleted = false; 160 161 LogUtils.d(Logging.LOG_TAG, "reconcileAccountsInternal"); 162 // See if we should have the Eas authenticators enabled. 163 164 if (!EmailServiceUtils.isServiceAvailable(context, 165 context.getString(R.string.protocol_eas))) { 166 EmailServiceUtils.disableExchangeComponents(context); 167 } else { 168 EmailServiceUtils.enableExchangeComponent(context); 169 } 170 // First, look through our EmailProvider accounts to make sure there's a corresponding 171 // AccountManager account 172 for (final Account providerAccount : emailProviderAccounts) { 173 final String providerAccountName = providerAccount.mEmailAddress; 174 final EmailServiceUtils.EmailServiceInfo infoForAccount = EmailServiceUtils 175 .getServiceInfoForAccount(context, providerAccount.mId); 176 177 // We want to delete the account if there is no matching Account Manager account for it 178 // unless it is flagged as incomplete. We also want to delete it if we can't find 179 // an accountInfo object for it. 180 if (infoForAccount == null || !hasAmAccount( 181 accountManagerAccounts, providerAccountName, infoForAccount.accountType)) { 182 if (infoForAccount != null && 183 (providerAccount.mFlags & Account.FLAGS_INCOMPLETE) != 0) { 184 LogUtils.w(Logging.LOG_TAG, 185 "Account reconciler noticed incomplete account; ignoring"); 186 continue; 187 } 188 189 needsReconciling = true; 190 if (performReconciliation) { 191 // This account has been deleted in the AccountManager! 192 LogUtils.d(Logging.LOG_TAG, 193 "Account deleted in AccountManager; deleting from provider: " + 194 providerAccountName); 195 // See if this is an exchange account 196 final HostAuth auth = providerAccount.getOrCreateHostAuthRecv(context); 197 LogUtils.d(Logging.LOG_TAG, "deleted account with hostAuth " + auth); 198 if (auth != null && TextUtils.equals(auth.mProtocol, 199 context.getString(R.string.protocol_eas))) { 200 exchangeAccountDeleted = true; 201 } 202 // Cancel all notifications for this account 203 NotificationController.cancelNotifications(context, providerAccount); 204 205 context.getContentResolver().delete( 206 EmailProvider.uiUri("uiaccount", providerAccount.mId), null, null); 207 208 accountsDeleted++; 209 210 } 211 } 212 } 213 // Now, look through AccountManager accounts to make sure we have a corresponding cached EAS 214 // account from EmailProvider 215 for (final android.accounts.Account accountManagerAccount : accountManagerAccounts) { 216 final String accountManagerAccountName = accountManagerAccount.name; 217 if (!hasEpAccount(emailProviderAccounts, accountManagerAccountName)) { 218 // This account has been deleted from the EmailProvider database 219 needsReconciling = true; 220 221 if (performReconciliation) { 222 LogUtils.d(Logging.LOG_TAG, 223 "Account deleted from provider; deleting from AccountManager: " + 224 accountManagerAccountName); 225 // Delete the account 226 AccountManagerFuture<Boolean> blockingResult = AccountManager.get(context) 227 .removeAccount(accountManagerAccount, null, null); 228 try { 229 // Note: All of the potential errors from removeAccount() are simply logged 230 // here, as there is nothing to actually do about them. 231 blockingResult.getResult(); 232 } catch (OperationCanceledException e) { 233 LogUtils.w(Logging.LOG_TAG, e.toString()); 234 } catch (AuthenticatorException e) { 235 LogUtils.w(Logging.LOG_TAG, e.toString()); 236 } catch (IOException e) { 237 LogUtils.w(Logging.LOG_TAG, e.toString()); 238 } 239 } 240 } else { 241 // Fix up the Calendar and Contacts syncing. It used to be possible for IMAP and 242 // POP accounts to get calendar and contacts syncing enabled. 243 // See b/11818312 244 final String accountType = accountManagerAccount.type; 245 final String protocol = EmailServiceUtils.getProtocolFromAccountType( 246 context, accountType); 247 final EmailServiceInfo info = EmailServiceUtils.getServiceInfo(context, protocol); 248 if (!info.syncCalendar) { 249 ContentResolver.setIsSyncable(accountManagerAccount, 250 CalendarContract.AUTHORITY, 0); 251 } 252 if (!info.syncContacts) { 253 ContentResolver.setIsSyncable(accountManagerAccount, 254 ContactsContract.AUTHORITY, 0); 255 } 256 } 257 } 258 259 final String composeActivityName = 260 context.getString(R.string.reconciliation_compose_activity_name); 261 if (!TextUtils.isEmpty(composeActivityName)) { 262 // If there are no accounts remaining after reconciliation, disable the compose activity 263 final boolean enableCompose = emailProviderAccounts.size() - accountsDeleted > 0; 264 final ComponentName componentName = new ComponentName(context, composeActivityName); 265 context.getPackageManager().setComponentEnabledSetting(componentName, 266 enableCompose ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : 267 PackageManager.COMPONENT_ENABLED_STATE_DISABLED, 268 PackageManager.DONT_KILL_APP); 269 LogUtils.d(LogUtils.TAG, "Setting compose activity to " 270 + (enableCompose ? "enabled" : "disabled")); 271 } 272 273 274 // If an account has been deleted, the simplest thing is just to kill our process. 275 // Otherwise we might have a service running trying to do something for the account 276 // which has been deleted, which can get NPEs. It's not as clean is it could be, but 277 // it still works pretty well because there is nowhere in the email app to delete the 278 // account. You have to go to Settings, so it's not user visible that the Email app 279 // has been killed. 280 if (accountsDeleted > 0) { 281 LogUtils.i(Logging.LOG_TAG, "Restarting because account deleted"); 282 if (exchangeAccountDeleted) { 283 EmailServiceUtils.killService(context, context.getString(R.string.protocol_eas)); 284 } 285 System.exit(-1); 286 } 287 288 return needsReconciling; 289 } 290} 291