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