1 /*
2  * Copyright (C) 2010 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.service;
18 
19 import android.accounts.AccountManager;
20 import android.app.IntentService;
21 import android.content.ComponentName;
22 import android.content.ContentResolver;
23 import android.content.ContentUris;
24 import android.content.ContentValues;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.PeriodicSync;
28 import android.content.pm.PackageManager;
29 import android.database.Cursor;
30 import android.net.Uri;
31 import android.os.Bundle;
32 import android.provider.CalendarContract;
33 import android.provider.ContactsContract;
34 import android.text.TextUtils;
35 import android.text.format.DateUtils;
36 
37 import com.android.email.EmailIntentService;
38 import com.android.email.Preferences;
39 import com.android.email.R;
40 import com.android.email.SecurityPolicy;
41 import com.android.email.provider.AccountReconciler;
42 import com.android.emailcommon.Logging;
43 import com.android.emailcommon.provider.Account;
44 import com.android.emailcommon.provider.EmailContent;
45 import com.android.emailcommon.provider.EmailContent.AccountColumns;
46 import com.android.emailcommon.provider.HostAuth;
47 import com.android.mail.providers.UIProvider;
48 import com.android.mail.utils.LogUtils;
49 import com.android.mail.utils.NotificationActionUtils;
50 import com.google.common.annotations.VisibleForTesting;
51 import com.google.common.collect.Maps;
52 
53 import java.util.Collections;
54 import java.util.HashSet;
55 import java.util.List;
56 import java.util.Map;
57 import java.util.Set;
58 
59 /**
60  * The service that really handles broadcast intents on a worker thread.
61  *
62  * We make it a service, because:
63  * <ul>
64  *   <li>So that it's less likely for the process to get killed.
65  *   <li>Even if it does, the Intent that have started it will be re-delivered by the system,
66  *   and we can start the process again.  (Using {@link #setIntentRedelivery}).
67  * </ul>
68  *
69  * This also handles the DeviceAdminReceiver in SecurityPolicy, because it is also
70  * a BroadcastReceiver and requires the same processing semantics.
71  */
72 public class EmailBroadcastProcessorService extends IntentService {
73     // Action used for BroadcastReceiver entry point
74     private static final String ACTION_BROADCAST = "broadcast_receiver";
75 
76     // This is a helper used to process DeviceAdminReceiver messages
77     private static final String ACTION_DEVICE_POLICY_ADMIN = "com.android.email.devicepolicy";
78     private static final String EXTRA_DEVICE_POLICY_ADMIN = "message_code";
79 
80     // Action used for EmailUpgradeBroadcastReceiver.
81     private static final String ACTION_UPGRADE_BROADCAST = "upgrade_broadcast_receiver";
82 
EmailBroadcastProcessorService()83     public EmailBroadcastProcessorService() {
84         // Class name will be the thread name.
85         super(EmailBroadcastProcessorService.class.getName());
86 
87         // Intent should be redelivered if the process gets killed before completing the job.
88         setIntentRedelivery(true);
89     }
90 
91     /**
92      * Entry point for {@link EmailBroadcastReceiver}.
93      */
processBroadcastIntent(Context context, Intent broadcastIntent)94     public static void processBroadcastIntent(Context context, Intent broadcastIntent) {
95         Intent i = new Intent(context, EmailBroadcastProcessorService.class);
96         i.setAction(ACTION_BROADCAST);
97         i.putExtra(Intent.EXTRA_INTENT, broadcastIntent);
98         context.startService(i);
99     }
100 
processUpgradeBroadcastIntent(final Context context)101     public static void processUpgradeBroadcastIntent(final Context context) {
102         final Intent i = new Intent(context, EmailBroadcastProcessorService.class);
103         i.setAction(ACTION_UPGRADE_BROADCAST);
104         context.startService(i);
105     }
106 
107     /**
108      * Entry point for {@link com.android.email.SecurityPolicy.PolicyAdmin}.  These will
109      * simply callback to {@link
110      * com.android.email.SecurityPolicy#onDeviceAdminReceiverMessage(Context, int)}.
111      */
processDevicePolicyMessage(Context context, int message)112     public static void processDevicePolicyMessage(Context context, int message) {
113         Intent i = new Intent(context, EmailBroadcastProcessorService.class);
114         i.setAction(ACTION_DEVICE_POLICY_ADMIN);
115         i.putExtra(EXTRA_DEVICE_POLICY_ADMIN, message);
116         context.startService(i);
117     }
118 
119     @Override
onHandleIntent(Intent intent)120     protected void onHandleIntent(Intent intent) {
121         // This method is called on a worker thread.
122 
123         // Dispatch from entry point
124         final String action = intent.getAction();
125         if (ACTION_BROADCAST.equals(action)) {
126             final Intent broadcastIntent = intent.getParcelableExtra(Intent.EXTRA_INTENT);
127             final String broadcastAction = broadcastIntent.getAction();
128 
129             if (Intent.ACTION_BOOT_COMPLETED.equals(broadcastAction)) {
130                 onBootCompleted();
131             } else if (AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION.equals(broadcastAction)) {
132                 onSystemAccountChanged();
133             } else if (Intent.ACTION_LOCALE_CHANGED.equals(broadcastAction) ||
134                     UIProvider.ACTION_UPDATE_NOTIFICATION.equals((broadcastAction))) {
135                 broadcastIntent.setClass(this, EmailIntentService.class);
136                 startService(broadcastIntent);
137             }
138         } else if (ACTION_DEVICE_POLICY_ADMIN.equals(action)) {
139             int message = intent.getIntExtra(EXTRA_DEVICE_POLICY_ADMIN, -1);
140             SecurityPolicy.onDeviceAdminReceiverMessage(this, message);
141         } else if (ACTION_UPGRADE_BROADCAST.equals(action)) {
142             onAppUpgrade();
143         }
144     }
145 
disableComponent(final Class<?> klass)146     private void disableComponent(final Class<?> klass) {
147         getPackageManager().setComponentEnabledSetting(new ComponentName(this, klass),
148                 PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
149     }
150 
isComponentDisabled(final Class<?> klass)151     private boolean isComponentDisabled(final Class<?> klass) {
152         return getPackageManager().getComponentEnabledSetting(new ComponentName(this, klass))
153                 == PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
154     }
155 
updateAccountManagerAccountsOfType(final String amAccountType, final Map<String, String> protocolMap)156     private void updateAccountManagerAccountsOfType(final String amAccountType,
157             final Map<String, String> protocolMap) {
158         final android.accounts.Account[] amAccounts =
159                 AccountManager.get(this).getAccountsByType(amAccountType);
160 
161         for (android.accounts.Account amAccount: amAccounts) {
162             EmailServiceUtils.updateAccountManagerType(this, amAccount, protocolMap);
163         }
164     }
165 
166     /**
167      * Delete all periodic syncs for an account.
168      * @param amAccount The account for which to disable syncs.
169      * @param authority The authority for which to disable syncs.
170      */
removePeriodicSyncs(final android.accounts.Account amAccount, final String authority)171     private static void removePeriodicSyncs(final android.accounts.Account amAccount,
172             final String authority) {
173         final List<PeriodicSync> syncs =
174                 ContentResolver.getPeriodicSyncs(amAccount, authority);
175         for (final PeriodicSync sync : syncs) {
176             ContentResolver.removePeriodicSync(amAccount, authority, sync.extras);
177         }
178     }
179 
180     /**
181      * Remove all existing periodic syncs for an account type, and add the necessary syncs.
182      * @param amAccountType The account type to handle.
183      * @param syncIntervals The map of all account addresses to sync intervals in the DB.
184      */
fixPeriodicSyncs(final String amAccountType, final Map<String, Integer> syncIntervals)185     private void fixPeriodicSyncs(final String amAccountType,
186             final Map<String, Integer> syncIntervals) {
187         final android.accounts.Account[] amAccounts =
188                 AccountManager.get(this).getAccountsByType(amAccountType);
189         for (android.accounts.Account amAccount : amAccounts) {
190             // First delete existing periodic syncs.
191             removePeriodicSyncs(amAccount, EmailContent.AUTHORITY);
192             removePeriodicSyncs(amAccount, CalendarContract.AUTHORITY);
193             removePeriodicSyncs(amAccount, ContactsContract.AUTHORITY);
194 
195             // Add back a sync for this account if necessary (i.e. the account has a positive
196             // sync interval in the DB). This assumes that the email app requires unique email
197             // addresses for each account, which is currently the case.
198             final Integer syncInterval = syncIntervals.get(amAccount.name);
199             if (syncInterval != null && syncInterval > 0) {
200                 // Sync interval is stored in minutes in DB, but we want the value in seconds.
201                 ContentResolver.addPeriodicSync(amAccount, EmailContent.AUTHORITY, Bundle.EMPTY,
202                         syncInterval * DateUtils.MINUTE_IN_MILLIS / DateUtils.SECOND_IN_MILLIS);
203             }
204         }
205     }
206 
207     /** Projection used for getting sync intervals for all accounts. */
208     private static final String[] ACCOUNT_SYNC_INTERVAL_PROJECTION =
209             { AccountColumns.EMAIL_ADDRESS, AccountColumns.SYNC_INTERVAL };
210     private static final int ACCOUNT_SYNC_INTERVAL_ADDRESS_COLUMN = 0;
211     private static final int ACCOUNT_SYNC_INTERVAL_INTERVAL_COLUMN = 1;
212 
213     /**
214      * Get the sync interval for all accounts, as stored in the DB.
215      * @return The map of all sync intervals by account email address.
216      */
getSyncIntervals()217     private Map<String, Integer> getSyncIntervals() {
218         final Cursor c = getContentResolver().query(Account.CONTENT_URI,
219                 ACCOUNT_SYNC_INTERVAL_PROJECTION, null, null, null);
220         if (c != null) {
221             final Map<String, Integer> periodicSyncs =
222                     Maps.newHashMapWithExpectedSize(c.getCount());
223             try {
224                 while (c.moveToNext()) {
225                     periodicSyncs.put(c.getString(ACCOUNT_SYNC_INTERVAL_ADDRESS_COLUMN),
226                             c.getInt(ACCOUNT_SYNC_INTERVAL_INTERVAL_COLUMN));
227                 }
228             } finally {
229                 c.close();
230             }
231             return periodicSyncs;
232         }
233         return Collections.emptyMap();
234     }
235 
236     @VisibleForTesting
removeNoopUpgrades(final Map<String, String> protocolMap)237     protected static void removeNoopUpgrades(final Map<String, String> protocolMap) {
238         final Set<String> keySet = new HashSet<String>(protocolMap.keySet());
239         for (final String key : keySet) {
240             if (TextUtils.equals(key, protocolMap.get(key))) {
241                 protocolMap.remove(key);
242             }
243         }
244     }
245 
onAppUpgrade()246     private void onAppUpgrade() {
247         if (isComponentDisabled(EmailUpgradeBroadcastReceiver.class)) {
248             return;
249         }
250         // When upgrading to a version that changes the protocol strings, we need to essentially
251         // rename the account manager type for all existing accounts, so we add new ones and delete
252         // the old.
253         // We specify the translations in this map. We map from old protocol name to new protocol
254         // name, and from protocol name + "_type" to new account manager type name. (Email1 did
255         // not use distinct account manager types for POP and IMAP, but Email2 does, hence this
256         // weird mapping.)
257         final Map<String, String> protocolMap = Maps.newHashMapWithExpectedSize(4);
258         protocolMap.put("imap", getString(R.string.protocol_legacy_imap));
259         protocolMap.put("pop3", getString(R.string.protocol_pop3));
260         removeNoopUpgrades(protocolMap);
261         if (!protocolMap.isEmpty()) {
262             protocolMap.put("imap_type", getString(R.string.account_manager_type_legacy_imap));
263             protocolMap.put("pop3_type", getString(R.string.account_manager_type_pop3));
264             updateAccountManagerAccountsOfType("com.android.email", protocolMap);
265         }
266 
267         protocolMap.clear();
268         protocolMap.put("eas", getString(R.string.protocol_eas));
269         removeNoopUpgrades(protocolMap);
270         if (!protocolMap.isEmpty()) {
271             protocolMap.put("eas_type", getString(R.string.account_manager_type_exchange));
272             updateAccountManagerAccountsOfType("com.android.exchange", protocolMap);
273         }
274 
275         // Disable the old authenticators.
276         disableComponent(LegacyEmailAuthenticatorService.class);
277         disableComponent(LegacyEasAuthenticatorService.class);
278 
279         // Fix periodic syncs.
280         final Map<String, Integer> syncIntervals = getSyncIntervals();
281         for (final EmailServiceUtils.EmailServiceInfo service
282                 : EmailServiceUtils.getServiceInfoList(this)) {
283             fixPeriodicSyncs(service.accountType, syncIntervals);
284         }
285 
286         // Disable the upgrade broadcast receiver now that we're fully upgraded.
287         disableComponent(EmailUpgradeBroadcastReceiver.class);
288     }
289 
290     /**
291      * Handles {@link Intent#ACTION_BOOT_COMPLETED}.  Called on a worker thread.
292      */
onBootCompleted()293     private void onBootCompleted() {
294         performOneTimeInitialization();
295         reconcileAndStartServices();
296     }
297 
reconcileAndStartServices()298     private void reconcileAndStartServices() {
299         /**
300          *  We can get here before the ACTION_UPGRADE_BROADCAST is received, so make sure the
301          *  accounts are converted otherwise terrible, horrible things will happen.
302          */
303         onAppUpgrade();
304         // Reconcile accounts
305         AccountReconciler.reconcileAccounts(this);
306         // Starts remote services, if any
307         EmailServiceUtils.startRemoteServices(this);
308     }
309 
performOneTimeInitialization()310     private void performOneTimeInitialization() {
311         final Preferences pref = Preferences.getPreferences(this);
312         int progress = pref.getOneTimeInitializationProgress();
313         final int initialProgress = progress;
314 
315         if (progress < 1) {
316             LogUtils.i(Logging.LOG_TAG, "Onetime initialization: 1");
317             progress = 1;
318             EmailServiceUtils.enableExchangeComponent(this);
319         }
320 
321         if (progress < 2) {
322             LogUtils.i(Logging.LOG_TAG, "Onetime initialization: 2");
323             progress = 2;
324             setImapDeletePolicy(this);
325         }
326 
327         // Add your initialization steps here.
328         // Use "progress" to skip the initializations that's already done before.
329         // Using this preference also makes it safe when a user skips an upgrade.  (i.e. upgrading
330         // version N to version N+2)
331 
332         if (progress != initialProgress) {
333             pref.setOneTimeInitializationProgress(progress);
334             LogUtils.i(Logging.LOG_TAG, "Onetime initialization: completed.");
335         }
336     }
337 
338     /**
339      * Sets the delete policy to the correct value for all IMAP accounts. This will have no
340      * effect on either EAS or POP3 accounts.
341      */
setImapDeletePolicy(Context context)342     /*package*/ static void setImapDeletePolicy(Context context) {
343         ContentResolver resolver = context.getContentResolver();
344         Cursor c = resolver.query(Account.CONTENT_URI, Account.CONTENT_PROJECTION,
345                 null, null, null);
346         try {
347             while (c.moveToNext()) {
348                 long recvAuthKey = c.getLong(Account.CONTENT_HOST_AUTH_KEY_RECV_COLUMN);
349                 HostAuth recvAuth = HostAuth.restoreHostAuthWithId(context, recvAuthKey);
350                 String legacyImapProtocol = context.getString(R.string.protocol_legacy_imap);
351                 if (legacyImapProtocol.equals(recvAuth.mProtocol)) {
352                     int flags = c.getInt(Account.CONTENT_FLAGS_COLUMN);
353                     flags &= ~Account.FLAGS_DELETE_POLICY_MASK;
354                     flags |= Account.DELETE_POLICY_ON_DELETE << Account.FLAGS_DELETE_POLICY_SHIFT;
355                     ContentValues cv = new ContentValues();
356                     cv.put(AccountColumns.FLAGS, flags);
357                     long accountId = c.getLong(Account.CONTENT_ID_COLUMN);
358                     Uri uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId);
359                     resolver.update(uri, cv, null, null);
360                 }
361             }
362         } finally {
363             c.close();
364         }
365     }
366 
onSystemAccountChanged()367     private void onSystemAccountChanged() {
368         LogUtils.i(Logging.LOG_TAG, "System accounts updated.");
369         reconcileAndStartServices();
370         // Resend all notifications, so that there is no notification that points to a removed
371         // account.
372         NotificationActionUtils.resendNotifications(getApplicationContext(),
373                 null /* all accounts */, null /* all folders */);
374     }
375 }
376