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 
17 package com.android.email.provider;
18 
19 import android.accounts.AccountManager;
20 import android.accounts.AccountManagerFuture;
21 import android.accounts.AuthenticatorException;
22 import android.accounts.OperationCanceledException;
23 import android.content.ComponentName;
24 import android.content.ContentResolver;
25 import android.content.Context;
26 import android.content.pm.PackageManager;
27 import android.database.Cursor;
28 import android.provider.CalendarContract;
29 import android.provider.ContactsContract;
30 import android.text.TextUtils;
31 
32 import com.android.email.R;
33 import com.android.email.NotificationController;
34 import com.android.email.NotificationControllerCreatorHolder;
35 import com.android.email.SecurityPolicy;
36 import com.android.email.service.EmailServiceUtils;
37 import com.android.email.service.EmailServiceUtils.EmailServiceInfo;
38 import com.android.emailcommon.Logging;
39 import com.android.emailcommon.provider.Account;
40 import com.android.emailcommon.provider.HostAuth;
41 import com.android.emailcommon.utility.MigrationUtils;
42 import com.android.mail.utils.LogUtils;
43 import com.google.common.collect.ImmutableList;
44 
45 import java.io.IOException;
46 import java.util.Collections;
47 import java.util.LinkedHashSet;
48 import java.util.List;
49 
50 public 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      */
getAllAmAccounts(final Context context)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      */
getAllEmailProviderAccounts(final Context context)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      */
reconcileAccounts(final Context context)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      */
hasAmAccount(final List<android.accounts.Account> accounts, final String name, final String type)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      */
hasEpAccount(final List<Account> accounts, final String name)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      */
reconcileAccountsInternal( final Context context, final List<Account> emailProviderAccounts, final List<android.accounts.Account> accountManagerAccounts, final boolean performReconciliation)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