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