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