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.R;
33import com.android.email.NotificationController;
34import com.android.email.NotificationControllerCreatorHolder;
35import com.android.email.SecurityPolicy;
36import com.android.email.service.EmailServiceUtils;
37import com.android.email.service.EmailServiceUtils.EmailServiceInfo;
38import com.android.emailcommon.Logging;
39import com.android.emailcommon.provider.Account;
40import com.android.emailcommon.provider.HostAuth;
41import com.android.emailcommon.utility.MigrationUtils;
42import com.android.mail.utils.LogUtils;
43import com.google.common.collect.ImmutableList;
44
45import java.io.IOException;
46import java.util.Collections;
47import java.util.LinkedHashSet;
48import java.util.List;
49
50public class AccountReconciler {
51    /**
52     * Get all AccountManager accounts for all email types.
53     * @param context Our {@link Context}.
54     * @return A list of all {@link android.accounts.Account}s created by our app.
55     */
56    private static List<android.accounts.Account> getAllAmAccounts(final Context context) {
57        final AccountManager am = AccountManager.get(context);
58
59        // TODO: Consider getting the types programmatically, in case we add more types.
60        // Some Accounts types can be identical, the set de-duplicates.
61        final LinkedHashSet<String> accountTypes = new LinkedHashSet<String>();
62        accountTypes.add(context.getString(R.string.account_manager_type_legacy_imap));
63        accountTypes.add(context.getString(R.string.account_manager_type_pop3));
64        accountTypes.add(context.getString(R.string.account_manager_type_exchange));
65
66        final ImmutableList.Builder<android.accounts.Account> builder = ImmutableList.builder();
67        for (final String type : accountTypes) {
68            final android.accounts.Account[] accounts = am.getAccountsByType(type);
69            builder.add(accounts);
70        }
71        return builder.build();
72    }
73
74    /**
75     * Get a all {@link Account} objects from the {@link EmailProvider}.
76     * @param context Our {@link Context}.
77     * @return A list of all {@link Account}s from the {@link EmailProvider}.
78     */
79    private static List<Account> getAllEmailProviderAccounts(final Context context) {
80        final Cursor c = context.getContentResolver().query(Account.CONTENT_URI,
81                Account.CONTENT_PROJECTION, null, null, null);
82        if (c == null) {
83            return Collections.emptyList();
84        }
85
86        final ImmutableList.Builder<Account> builder = ImmutableList.builder();
87        try {
88            while (c.moveToNext()) {
89                final Account account = new Account();
90                account.restore(c);
91                builder.add(account);
92            }
93        } finally {
94            c.close();
95        }
96        return builder.build();
97    }
98
99    /**
100     * Compare our account list (obtained from EmailProvider) with the account list owned by
101     * AccountManager.  If there are any orphans (an account in one list without a corresponding
102     * account in the other list), delete the orphan, as these must remain in sync.
103     *
104     * Note that the duplication of account information is caused by the Email application's
105     * incomplete integration with AccountManager.
106     *
107     * This function may not be called from the main/UI thread, because it makes blocking calls
108     * into the account manager.
109     *
110     * @param context The context in which to operate
111     */
112    public static synchronized void reconcileAccounts(final Context context) {
113        final List<android.accounts.Account> amAccounts = getAllAmAccounts(context);
114        final List<Account> providerAccounts = getAllEmailProviderAccounts(context);
115        reconcileAccountsInternal(context, providerAccounts, amAccounts, true);
116    }
117
118    /**
119     * Check if the AccountManager accounts list contains a specific account.
120     * @param accounts The list of {@link android.accounts.Account} objects.
121     * @param name The name of the account to find.
122     * @return Whether the account is in the list.
123     */
124    private static boolean hasAmAccount(final List<android.accounts.Account> accounts,
125            final String name, final String type) {
126        for (final android.accounts.Account account : accounts) {
127            if (account.name.equalsIgnoreCase(name) && account.type.equalsIgnoreCase(type)) {
128                return true;
129            }
130        }
131        return false;
132    }
133
134    /**
135     * Check if the EmailProvider accounts list contains a specific account.
136     * @param accounts The list of {@link Account} objects.
137     * @param name The name of the account to find.
138     * @return Whether the account is in the list.
139     */
140    private static boolean hasEpAccount(final List<Account> accounts, final String name) {
141        for (final Account account : accounts) {
142            if (account.mEmailAddress.equalsIgnoreCase(name)) {
143                return true;
144            }
145        }
146        return false;
147    }
148
149    /**
150     * Internal method to actually perform reconciliation, or simply check that it needs to be done
151     * and avoid doing any heavy work, depending on the value of the passed in
152     * {@code performReconciliation}.
153     */
154    private static boolean reconcileAccountsInternal(
155            final Context context,
156            final List<Account> emailProviderAccounts,
157            final List<android.accounts.Account> accountManagerAccounts,
158            final boolean performReconciliation) {
159        boolean needsReconciling = false;
160        int accountsDeleted = 0;
161        boolean exchangeAccountDeleted = false;
162
163        LogUtils.d(Logging.LOG_TAG, "reconcileAccountsInternal");
164
165        if (MigrationUtils.migrationInProgress()) {
166            LogUtils.d(Logging.LOG_TAG, "deferring reconciliation, migration in progress");
167            return false;
168        }
169
170        // See if we should have the Eas authenticators enabled.
171        if (!EmailServiceUtils.isServiceAvailable(context,
172                context.getString(R.string.protocol_eas))) {
173            EmailServiceUtils.disableExchangeComponents(context);
174        } else {
175            EmailServiceUtils.enableExchangeComponent(context);
176        }
177        // First, look through our EmailProvider accounts to make sure there's a corresponding
178        // AccountManager account
179        for (final Account providerAccount : emailProviderAccounts) {
180            final String providerAccountName = providerAccount.mEmailAddress;
181            final EmailServiceUtils.EmailServiceInfo infoForAccount = EmailServiceUtils
182                    .getServiceInfoForAccount(context, providerAccount.mId);
183
184            // We want to delete the account if there is no matching Account Manager account for it
185            // unless it is flagged as incomplete. We also want to delete it if we can't find
186            // an accountInfo object for it.
187            if (infoForAccount == null || !hasAmAccount(
188                    accountManagerAccounts, providerAccountName, infoForAccount.accountType)) {
189                if (infoForAccount != null &&
190                        (providerAccount.mFlags & Account.FLAGS_INCOMPLETE) != 0) {
191                    LogUtils.w(Logging.LOG_TAG,
192                            "Account reconciler noticed incomplete account; ignoring");
193                    continue;
194                }
195
196                needsReconciling = true;
197                if (performReconciliation) {
198                    // This account has been deleted in the AccountManager!
199                    LogUtils.d(Logging.LOG_TAG,
200                            "Account deleted in AccountManager; deleting from provider: " +
201                            providerAccountName);
202                    // See if this is an exchange account
203                    final HostAuth auth = providerAccount.getOrCreateHostAuthRecv(context);
204                    LogUtils.d(Logging.LOG_TAG, "deleted account with hostAuth " + auth);
205                    if (auth != null && TextUtils.equals(auth.mProtocol,
206                            context.getString(R.string.protocol_eas))) {
207                        exchangeAccountDeleted = true;
208                    }
209                    // Cancel all notifications for this account
210                    final NotificationController nc =
211                            NotificationControllerCreatorHolder.getInstance(context);
212                    if (nc != null) {
213                        nc.cancelNotifications(context, providerAccount);
214                    }
215
216                    context.getContentResolver().delete(
217                            EmailProvider.uiUri("uiaccount", providerAccount.mId), null, null);
218
219                    accountsDeleted++;
220
221                }
222            }
223        }
224        // Now, look through AccountManager accounts to make sure we have a corresponding cached EAS
225        // account from EmailProvider
226        boolean needsPolicyUpdate = false;
227        for (final android.accounts.Account accountManagerAccount : accountManagerAccounts) {
228            final String accountManagerAccountName = accountManagerAccount.name;
229            if (!hasEpAccount(emailProviderAccounts, accountManagerAccountName)) {
230                // This account has been deleted from the EmailProvider database
231                needsReconciling = true;
232
233                if (performReconciliation) {
234                    LogUtils.d(Logging.LOG_TAG,
235                            "Account deleted from provider; deleting from AccountManager: " +
236                            accountManagerAccountName);
237                    // Delete the account
238                    AccountManagerFuture<Boolean> blockingResult = AccountManager.get(context)
239                            .removeAccount(accountManagerAccount, null, null);
240                    try {
241                        // Note: All of the potential errors from removeAccount() are simply logged
242                        // here, as there is nothing to actually do about them.
243                        blockingResult.getResult();
244                    } catch (OperationCanceledException e) {
245                        LogUtils.w(Logging.LOG_TAG, e.toString());
246                    } catch (AuthenticatorException e) {
247                        LogUtils.w(Logging.LOG_TAG, e.toString());
248                    } catch (IOException e) {
249                        LogUtils.w(Logging.LOG_TAG, e.toString());
250                    }
251                    // Just set a flag that our policies need to be updated with device
252                    // So we can do the update, one time, at a later point in time.
253                    needsPolicyUpdate = true;
254                }
255            } else {
256                // Fix up the Calendar and Contacts syncing. It used to be possible for IMAP and
257                // POP accounts to get calendar and contacts syncing enabled.
258                // See b/11818312
259                final String accountType = accountManagerAccount.type;
260                final String protocol = EmailServiceUtils.getProtocolFromAccountType(
261                        context, accountType);
262                final EmailServiceInfo info = EmailServiceUtils.getServiceInfo(context, protocol);
263                if (info == null || !info.syncCalendar) {
264                    ContentResolver.setIsSyncable(accountManagerAccount,
265                            CalendarContract.AUTHORITY, 0);
266                }
267                if (info == null || !info.syncContacts) {
268                    ContentResolver.setIsSyncable(accountManagerAccount,
269                            ContactsContract.AUTHORITY, 0);
270                }
271            }
272        }
273
274        if (needsPolicyUpdate) {
275            // We have removed accounts from the AccountManager, let's make sure that
276            // our policies are up to date.
277            SecurityPolicy.getInstance(context).policiesUpdated();
278        }
279
280        final String composeActivityName =
281                context.getString(R.string.reconciliation_compose_activity_name);
282        if (!TextUtils.isEmpty(composeActivityName)) {
283            // If there are no accounts remaining after reconciliation, disable the compose activity
284            final boolean enableCompose = emailProviderAccounts.size() - accountsDeleted > 0;
285            final ComponentName componentName = new ComponentName(context, composeActivityName);
286            context.getPackageManager().setComponentEnabledSetting(componentName,
287                    enableCompose ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED :
288                            PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
289                    PackageManager.DONT_KILL_APP);
290            LogUtils.d(LogUtils.TAG, "Setting compose activity to "
291                    + (enableCompose ? "enabled" : "disabled"));
292        }
293
294
295        // If an account has been deleted, the simplest thing is just to kill our process.
296        // Otherwise we might have a service running trying to do something for the account
297        // which has been deleted, which can get NPEs. It's not as clean is it could be, but
298        // it still works pretty well because there is nowhere in the email app to delete the
299        // account. You have to go to Settings, so it's not user visible that the Email app
300        // has been killed.
301        if (accountsDeleted > 0) {
302            LogUtils.i(Logging.LOG_TAG, "Restarting because account deleted");
303            if (exchangeAccountDeleted) {
304                EmailServiceUtils.killService(context, context.getString(R.string.protocol_eas));
305            }
306            System.exit(-1);
307        }
308
309        return needsReconciling;
310    }
311}
312