AccountReconciler.java revision afe097f318a7eeac9d240d2bedbc5caba7640ea8
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;
25
26import com.android.email.NotificationController;
27import com.android.email.R;
28import com.android.emailcommon.Logging;
29import com.android.emailcommon.provider.Account;
30import com.android.emailcommon.provider.EmailContent;
31import com.android.emailcommon.provider.Mailbox;
32import com.android.mail.utils.LogUtils;
33import com.google.common.collect.ImmutableList;
34
35import java.io.IOException;
36import java.util.Arrays;
37import java.util.Collections;
38import java.util.List;
39
40public class AccountReconciler {
41    /**
42     * Get all AccountManager accounts for all email types.
43     * @param context Our {@link Context}.
44     * @return A list of all {@link android.accounts.Account}s created by our app.
45     */
46    private static List<android.accounts.Account> getAllAmAccounts(final Context context) {
47        final AccountManager am = AccountManager.get(context);
48        final ImmutableList.Builder<android.accounts.Account> builder = ImmutableList.builder();
49        // TODO: Consider getting the types programmatically, in case we add more types.
50        builder.addAll(Arrays.asList(am.getAccountsByType(
51                context.getString(R.string.account_manager_type_legacy_imap))));
52        builder.addAll(Arrays.asList(am.getAccountsByType(
53                context.getString(R.string.account_manager_type_pop3))));
54        builder.addAll(Arrays.asList(am.getAccountsByType(
55                context.getString(R.string.account_manager_type_exchange))));
56        return builder.build();
57    }
58
59    /**
60     * Get a all {@link Account} objects from the {@link EmailProvider}.
61     * @param context Our {@link Context}.
62     * @return A list of all {@link Account}s from the {@link EmailProvider}.
63     */
64    private static List<Account> getAllEmailProviderAccounts(final Context context) {
65        final Cursor c = context.getContentResolver().query(Account.CONTENT_URI,
66                Account.CONTENT_PROJECTION, null, null, null);
67        if (c == null) {
68            return Collections.emptyList();
69        }
70
71        final ImmutableList.Builder<Account> builder = ImmutableList.builder();
72        try {
73            while (c.moveToNext()) {
74                final Account account = new Account();
75                account.restore(c);
76                builder.add(account);
77            }
78        } finally {
79            c.close();
80        }
81        return builder.build();
82    }
83
84    /**
85     * Compare our account list (obtained from EmailProvider) with the account list owned by
86     * AccountManager.  If there are any orphans (an account in one list without a corresponding
87     * account in the other list), delete the orphan, as these must remain in sync.
88     *
89     * Note that the duplication of account information is caused by the Email application's
90     * incomplete integration with AccountManager.
91     *
92     * This function may not be called from the main/UI thread, because it makes blocking calls
93     * into the account manager.
94     *
95     * @param context The context in which to operate
96     */
97    public static void reconcileAccounts(final Context context) {
98        final List<android.accounts.Account> amAccounts = getAllAmAccounts(context);
99        final List<Account> providerAccounts = getAllEmailProviderAccounts(context);
100        reconcileAccountsInternal(context, providerAccounts, amAccounts, true);
101    }
102
103    /**
104     * Check if the AccountManager accounts list contains a specific account.
105     * @param accounts The list of {@link android.accounts.Account} objects.
106     * @param name The name of the account to find.
107     * @return Whether the account is in the list.
108     */
109    private static boolean hasAmAccount(final List<android.accounts.Account> accounts,
110            final String name) {
111        for (final android.accounts.Account account : accounts) {
112            if (account.name.equalsIgnoreCase(name)) {
113                return true;
114            }
115        }
116        return false;
117    }
118
119    /**
120     * Check if the EmailProvider accounts list contains a specific account.
121     * @param accounts The list of {@link Account} objects.
122     * @param name The name of the account to find.
123     * @return Whether the account is in the list.
124     */
125    private static boolean hasEpAccount(final List<Account> accounts, final String name) {
126        for (final Account account : accounts) {
127            if (account.mEmailAddress.equalsIgnoreCase(name)) {
128                return true;
129            }
130        }
131        return false;
132    }
133
134    /**
135     * Internal method to actually perform reconciliation, or simply check that it needs to be done
136     * and avoid doing any heavy work, depending on the value of the passed in
137     * {@code performReconciliation}.
138     */
139    private static boolean reconcileAccountsInternal(
140            final Context context,
141            final List<Account> emailProviderAccounts,
142            final List<android.accounts.Account> accountManagerAccounts,
143            final boolean performReconciliation) {
144        boolean needsReconciling = false;
145
146        // First, look through our EmailProvider accounts to make sure there's a corresponding
147        // AccountManager account
148        for (final Account providerAccount : emailProviderAccounts) {
149            final String providerAccountName = providerAccount.mEmailAddress;
150            if (!hasAmAccount(accountManagerAccounts, providerAccountName)) {
151                if ((providerAccount.mFlags & Account.FLAGS_INCOMPLETE) != 0) {
152                    // Do another test before giving up; an incomplete account shouldn't have
153                    // any mailboxes (the incomplete flag is used to prevent reconciliation
154                    // between the time the EP account is created and when the AM account is
155                    // asynchronously created)
156                    if (EmailContent.count(context, Mailbox.CONTENT_URI,
157                            Mailbox.ACCOUNT_KEY + "=?",
158                            new String[] { Long.toString(providerAccount.mId) } ) > 0) {
159                        LogUtils.w(Logging.LOG_TAG,
160                                "Account reconciler found wrongly incomplete account");
161                    } else {
162                        LogUtils.w(Logging.LOG_TAG,
163                                "Account reconciler noticed incomplete account; ignoring");
164                        continue;
165                    }
166                }
167
168                needsReconciling = true;
169                if (performReconciliation) {
170                    // This account has been deleted in the AccountManager!
171                    LogUtils.d(Logging.LOG_TAG,
172                            "Account deleted in AccountManager; deleting from provider: " +
173                            providerAccountName);
174                    context.getContentResolver().delete(
175                            EmailProvider.uiUri("uiaccount", providerAccount.mId), null, null);
176
177                    // Cancel all notifications for this account
178                    NotificationController.cancelNotifications(context, providerAccount);
179                }
180            }
181        }
182        // Now, look through AccountManager accounts to make sure we have a corresponding cached EAS
183        // account from EmailProvider
184        for (final android.accounts.Account accountManagerAccount : accountManagerAccounts) {
185            final String accountManagerAccountName = accountManagerAccount.name;
186            if (!hasEpAccount(emailProviderAccounts, accountManagerAccountName)) {
187                // This account has been deleted from the EmailProvider database
188                needsReconciling = true;
189
190                if (performReconciliation) {
191                    LogUtils.d(Logging.LOG_TAG,
192                            "Account deleted from provider; deleting from AccountManager: " +
193                            accountManagerAccountName);
194                    // Delete the account
195                    AccountManagerFuture<Boolean> blockingResult = AccountManager.get(context)
196                            .removeAccount(accountManagerAccount, null, null);
197                    try {
198                        // Note: All of the potential errors from removeAccount() are simply logged
199                        // here, as there is nothing to actually do about them.
200                        blockingResult.getResult();
201                    } catch (OperationCanceledException e) {
202                        LogUtils.w(Logging.LOG_TAG, e.toString());
203                    } catch (AuthenticatorException e) {
204                        LogUtils.w(Logging.LOG_TAG, e.toString());
205                    } catch (IOException e) {
206                        LogUtils.w(Logging.LOG_TAG, e.toString());
207                    }
208                }
209            }
210        }
211
212        return needsReconciling;
213    }
214}
215