Home | History | Annotate | Download | only in provider
      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