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