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