AccountReconciler.java revision 78b8c04898c04890b757fbd83df9666e0a9068f5
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.net.Uri;
25import android.util.Log;
26
27import com.android.emailcommon.Logging;
28import com.android.emailcommon.provider.Account;
29import com.android.emailcommon.provider.EmailContent;
30import com.android.emailcommon.provider.Mailbox;
31import com.google.common.annotations.VisibleForTesting;
32
33import java.io.IOException;
34import java.util.List;
35
36public class AccountReconciler {
37    // AccountManager accounts with a name beginning with this constant are ignored for purposes
38    // of reconcilation.  This is for unit test purposes only; the caller may NOT be in the same
39    // package as this class, so we make the constant public.
40    @VisibleForTesting
41    static final String ACCOUNT_MANAGER_ACCOUNT_TEST_PREFIX = " _";
42
43    /**
44     * Checks two account lists to see if there is any reconciling to be done. Can be done on the
45     * UI thread.
46     * @param context the app context
47     * @param emailProviderAccounts accounts as reported in the Email provider
48     * @param accountManagerAccounts accounts as reported by the system account manager, for the
49     *     particular protocol types that match emailProviderAccounts
50     */
51    public static boolean accountsNeedReconciling(
52            final Context context,
53            List<Account> emailProviderAccounts,
54            android.accounts.Account[] accountManagerAccounts) {
55
56        return reconcileAccountsInternal(
57                context, emailProviderAccounts, accountManagerAccounts,
58                context, false /* performReconciliation */);
59    }
60
61    /**
62     * Compare our account list (obtained from EmailProvider) with the account list owned by
63     * AccountManager.  If there are any orphans (an account in one list without a corresponding
64     * account in the other list), delete the orphan, as these must remain in sync.
65     *
66     * Note that the duplication of account information is caused by the Email application's
67     * incomplete integration with AccountManager.
68     *
69     * This function may not be called from the main/UI thread, because it makes blocking calls
70     * into the account manager.
71     *
72     * @param context The context in which to operate
73     * @param emailProviderAccounts the exchange provider accounts to work from
74     * @param accountManagerAccounts The account manager accounts to work from
75     * @param providerContext application provider context
76     */
77    public static void reconcileAccounts(
78            Context context,
79            List<Account> emailProviderAccounts,
80            android.accounts.Account[] accountManagerAccounts,
81            Context providerContext) {
82        reconcileAccountsInternal(
83                context, emailProviderAccounts, accountManagerAccounts,
84                providerContext, true /* performReconciliation */);
85    }
86
87    /**
88     * Internal method to actually perform reconciliation, or simply check that it needs to be done
89     * and avoid doing any heavy work, depending on the value of the passed in
90     * {@code performReconciliation}.
91     */
92    private static boolean reconcileAccountsInternal(
93            Context context,
94            List<Account> emailProviderAccounts,
95            android.accounts.Account[] accountManagerAccounts,
96            Context providerContext,
97            boolean performReconciliation) {
98        boolean needsReconciling = false;
99
100        // First, look through our EmailProvider accounts to make sure there's a corresponding
101        // AccountManager account
102        for (Account providerAccount: emailProviderAccounts) {
103            String providerAccountName = providerAccount.mEmailAddress;
104            boolean found = false;
105            for (android.accounts.Account accountManagerAccount: accountManagerAccounts) {
106                if (accountManagerAccount.name.equalsIgnoreCase(providerAccountName)) {
107                    found = true;
108                    break;
109                }
110            }
111            if (!found) {
112                if ((providerAccount.mFlags & Account.FLAGS_INCOMPLETE) != 0) {
113                    // Do another test before giving up; an incomplete account shouldn't have
114                    // any mailboxes (the incomplete flag is used to prevent reconciliation
115                    // between the time the EP account is created and when the AM account is
116                    // asynchronously created)
117                    if (EmailContent.count(providerContext, Mailbox.CONTENT_URI,
118                            Mailbox.ACCOUNT_KEY + "=?",
119                            new String[] { Long.toString(providerAccount.mId) } ) > 0) {
120                        Log.w(Logging.LOG_TAG,
121                                "Account reconciler found wrongly incomplete account");
122                    } else {
123                        Log.w(Logging.LOG_TAG,
124                                "Account reconciler noticed incomplete account; ignoring");
125                        continue;
126                    }
127                }
128
129                needsReconciling = true;
130                if (performReconciliation) {
131                    // This account has been deleted in the AccountManager!
132                    Log.d(Logging.LOG_TAG,
133                            "Account deleted in AccountManager; deleting from provider: " +
134                            providerAccountName);
135                    Uri uri = EmailProvider.uiUri("uiaccount", providerAccount.mId);
136                    context.getContentResolver().delete(uri, null, null);
137                }
138            }
139        }
140        // Now, look through AccountManager accounts to make sure we have a corresponding cached EAS
141        // account from EmailProvider
142        for (android.accounts.Account accountManagerAccount: accountManagerAccounts) {
143            String accountManagerAccountName = accountManagerAccount.name;
144            boolean found = false;
145            for (Account cachedEasAccount: emailProviderAccounts) {
146                if (cachedEasAccount.mEmailAddress.equalsIgnoreCase(accountManagerAccountName)) {
147                    found = true;
148                }
149            }
150            if (accountManagerAccountName.startsWith(ACCOUNT_MANAGER_ACCOUNT_TEST_PREFIX)) {
151                found = true;
152            }
153            if (!found) {
154                // This account has been deleted from the EmailProvider database
155                needsReconciling = true;
156
157                if (performReconciliation) {
158                    Log.d(Logging.LOG_TAG,
159                            "Account deleted from provider; deleting from AccountManager: " +
160                            accountManagerAccountName);
161                    // Delete the account
162                    AccountManagerFuture<Boolean> blockingResult = AccountManager.get(context)
163                    .removeAccount(accountManagerAccount, null, null);
164                    try {
165                        // Note: All of the potential errors from removeAccount() are simply logged
166                        // here, as there is nothing to actually do about them.
167                        blockingResult.getResult();
168                    } catch (OperationCanceledException e) {
169                        Log.w(Logging.LOG_TAG, e.toString());
170                    } catch (AuthenticatorException e) {
171                        Log.w(Logging.LOG_TAG, e.toString());
172                    } catch (IOException e) {
173                        Log.w(Logging.LOG_TAG, e.toString());
174                    }
175                }
176            }
177        }
178
179        return needsReconciling;
180    }
181}
182