Home | History | Annotate | Download | only in service
      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 
     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      */
     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 
    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      */
    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
    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 
    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 
    151     private boolean isComponentDisabled(final Class<?> klass) {
    152         return getPackageManager().getComponentEnabledSetting(new ComponentName(this, klass))
    153                 == PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
    154     }
    155 
    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      */
    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      */
    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      */
    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
    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 
    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      */
    293     private void onBootCompleted() {
    294         performOneTimeInitialization();
    295         reconcileAndStartServices();
    296     }
    297 
    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 
    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      */
    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 
    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