/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.email.service; import android.accounts.AccountManager; import android.app.IntentService; import android.content.ComponentName; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.PeriodicSync; import android.content.pm.PackageManager; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.provider.CalendarContract; import android.provider.ContactsContract; import android.text.TextUtils; import android.text.format.DateUtils; import com.android.email.EmailIntentService; import com.android.email.Preferences; import com.android.email.R; import com.android.email.SecurityPolicy; import com.android.email.provider.AccountReconciler; import com.android.emailcommon.Logging; import com.android.emailcommon.provider.Account; import com.android.emailcommon.provider.EmailContent; import com.android.emailcommon.provider.EmailContent.AccountColumns; import com.android.emailcommon.provider.HostAuth; import com.android.mail.providers.UIProvider; import com.android.mail.utils.LogUtils; import com.android.mail.utils.NotificationActionUtils; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Maps; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * The service that really handles broadcast intents on a worker thread. * * We make it a service, because: * * * This also handles the DeviceAdminReceiver in SecurityPolicy, because it is also * a BroadcastReceiver and requires the same processing semantics. */ public class EmailBroadcastProcessorService extends IntentService { // Action used for BroadcastReceiver entry point private static final String ACTION_BROADCAST = "broadcast_receiver"; // This is a helper used to process DeviceAdminReceiver messages private static final String ACTION_DEVICE_POLICY_ADMIN = "com.android.email.devicepolicy"; private static final String EXTRA_DEVICE_POLICY_ADMIN = "message_code"; // Action used for EmailUpgradeBroadcastReceiver. private static final String ACTION_UPGRADE_BROADCAST = "upgrade_broadcast_receiver"; public EmailBroadcastProcessorService() { // Class name will be the thread name. super(EmailBroadcastProcessorService.class.getName()); // Intent should be redelivered if the process gets killed before completing the job. setIntentRedelivery(true); } /** * Entry point for {@link EmailBroadcastReceiver}. */ public static void processBroadcastIntent(Context context, Intent broadcastIntent) { Intent i = new Intent(context, EmailBroadcastProcessorService.class); i.setAction(ACTION_BROADCAST); i.putExtra(Intent.EXTRA_INTENT, broadcastIntent); context.startService(i); } public static void processUpgradeBroadcastIntent(final Context context) { final Intent i = new Intent(context, EmailBroadcastProcessorService.class); i.setAction(ACTION_UPGRADE_BROADCAST); context.startService(i); } /** * Entry point for {@link com.android.email.SecurityPolicy.PolicyAdmin}. These will * simply callback to {@link * com.android.email.SecurityPolicy#onDeviceAdminReceiverMessage(Context, int)}. */ public static void processDevicePolicyMessage(Context context, int message) { Intent i = new Intent(context, EmailBroadcastProcessorService.class); i.setAction(ACTION_DEVICE_POLICY_ADMIN); i.putExtra(EXTRA_DEVICE_POLICY_ADMIN, message); context.startService(i); } @Override protected void onHandleIntent(Intent intent) { // This method is called on a worker thread. // Dispatch from entry point final String action = intent.getAction(); if (ACTION_BROADCAST.equals(action)) { final Intent broadcastIntent = intent.getParcelableExtra(Intent.EXTRA_INTENT); final String broadcastAction = broadcastIntent.getAction(); if (Intent.ACTION_BOOT_COMPLETED.equals(broadcastAction)) { onBootCompleted(); } else if (AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION.equals(broadcastAction)) { onSystemAccountChanged(); } else if (Intent.ACTION_LOCALE_CHANGED.equals(broadcastAction) || UIProvider.ACTION_UPDATE_NOTIFICATION.equals((broadcastAction))) { broadcastIntent.setClass(this, EmailIntentService.class); startService(broadcastIntent); } } else if (ACTION_DEVICE_POLICY_ADMIN.equals(action)) { int message = intent.getIntExtra(EXTRA_DEVICE_POLICY_ADMIN, -1); SecurityPolicy.onDeviceAdminReceiverMessage(this, message); } else if (ACTION_UPGRADE_BROADCAST.equals(action)) { onAppUpgrade(); } } private void disableComponent(final Class klass) { getPackageManager().setComponentEnabledSetting(new ComponentName(this, klass), PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP); } private boolean isComponentDisabled(final Class klass) { return getPackageManager().getComponentEnabledSetting(new ComponentName(this, klass)) == PackageManager.COMPONENT_ENABLED_STATE_DISABLED; } private void updateAccountManagerAccountsOfType(final String amAccountType, final Map protocolMap) { final android.accounts.Account[] amAccounts = AccountManager.get(this).getAccountsByType(amAccountType); for (android.accounts.Account amAccount: amAccounts) { EmailServiceUtils.updateAccountManagerType(this, amAccount, protocolMap); } } /** * Delete all periodic syncs for an account. * @param amAccount The account for which to disable syncs. * @param authority The authority for which to disable syncs. */ private static void removePeriodicSyncs(final android.accounts.Account amAccount, final String authority) { final List syncs = ContentResolver.getPeriodicSyncs(amAccount, authority); for (final PeriodicSync sync : syncs) { ContentResolver.removePeriodicSync(amAccount, authority, sync.extras); } } /** * Remove all existing periodic syncs for an account type, and add the necessary syncs. * @param amAccountType The account type to handle. * @param syncIntervals The map of all account addresses to sync intervals in the DB. */ private void fixPeriodicSyncs(final String amAccountType, final Map syncIntervals) { final android.accounts.Account[] amAccounts = AccountManager.get(this).getAccountsByType(amAccountType); for (android.accounts.Account amAccount : amAccounts) { // First delete existing periodic syncs. removePeriodicSyncs(amAccount, EmailContent.AUTHORITY); removePeriodicSyncs(amAccount, CalendarContract.AUTHORITY); removePeriodicSyncs(amAccount, ContactsContract.AUTHORITY); // Add back a sync for this account if necessary (i.e. the account has a positive // sync interval in the DB). This assumes that the email app requires unique email // addresses for each account, which is currently the case. final Integer syncInterval = syncIntervals.get(amAccount.name); if (syncInterval != null && syncInterval > 0) { // Sync interval is stored in minutes in DB, but we want the value in seconds. ContentResolver.addPeriodicSync(amAccount, EmailContent.AUTHORITY, Bundle.EMPTY, syncInterval * DateUtils.MINUTE_IN_MILLIS / DateUtils.SECOND_IN_MILLIS); } } } /** Projection used for getting sync intervals for all accounts. */ private static final String[] ACCOUNT_SYNC_INTERVAL_PROJECTION = { AccountColumns.EMAIL_ADDRESS, AccountColumns.SYNC_INTERVAL }; private static final int ACCOUNT_SYNC_INTERVAL_ADDRESS_COLUMN = 0; private static final int ACCOUNT_SYNC_INTERVAL_INTERVAL_COLUMN = 1; /** * Get the sync interval for all accounts, as stored in the DB. * @return The map of all sync intervals by account email address. */ private Map getSyncIntervals() { final Cursor c = getContentResolver().query(Account.CONTENT_URI, ACCOUNT_SYNC_INTERVAL_PROJECTION, null, null, null); if (c != null) { final Map periodicSyncs = Maps.newHashMapWithExpectedSize(c.getCount()); try { while (c.moveToNext()) { periodicSyncs.put(c.getString(ACCOUNT_SYNC_INTERVAL_ADDRESS_COLUMN), c.getInt(ACCOUNT_SYNC_INTERVAL_INTERVAL_COLUMN)); } } finally { c.close(); } return periodicSyncs; } return Collections.emptyMap(); } @VisibleForTesting protected static void removeNoopUpgrades(final Map protocolMap) { final Set keySet = new HashSet(protocolMap.keySet()); for (final String key : keySet) { if (TextUtils.equals(key, protocolMap.get(key))) { protocolMap.remove(key); } } } private void onAppUpgrade() { if (isComponentDisabled(EmailUpgradeBroadcastReceiver.class)) { return; } // When upgrading to a version that changes the protocol strings, we need to essentially // rename the account manager type for all existing accounts, so we add new ones and delete // the old. // We specify the translations in this map. We map from old protocol name to new protocol // name, and from protocol name + "_type" to new account manager type name. (Email1 did // not use distinct account manager types for POP and IMAP, but Email2 does, hence this // weird mapping.) final Map protocolMap = Maps.newHashMapWithExpectedSize(4); protocolMap.put("imap", getString(R.string.protocol_legacy_imap)); protocolMap.put("pop3", getString(R.string.protocol_pop3)); removeNoopUpgrades(protocolMap); if (!protocolMap.isEmpty()) { protocolMap.put("imap_type", getString(R.string.account_manager_type_legacy_imap)); protocolMap.put("pop3_type", getString(R.string.account_manager_type_pop3)); updateAccountManagerAccountsOfType("com.android.email", protocolMap); } protocolMap.clear(); protocolMap.put("eas", getString(R.string.protocol_eas)); removeNoopUpgrades(protocolMap); if (!protocolMap.isEmpty()) { protocolMap.put("eas_type", getString(R.string.account_manager_type_exchange)); updateAccountManagerAccountsOfType("com.android.exchange", protocolMap); } // Disable the old authenticators. disableComponent(LegacyEmailAuthenticatorService.class); disableComponent(LegacyEasAuthenticatorService.class); // Fix periodic syncs. final Map syncIntervals = getSyncIntervals(); for (final EmailServiceUtils.EmailServiceInfo service : EmailServiceUtils.getServiceInfoList(this)) { fixPeriodicSyncs(service.accountType, syncIntervals); } // Disable the upgrade broadcast receiver now that we're fully upgraded. disableComponent(EmailUpgradeBroadcastReceiver.class); } /** * Handles {@link Intent#ACTION_BOOT_COMPLETED}. Called on a worker thread. */ private void onBootCompleted() { performOneTimeInitialization(); reconcileAndStartServices(); // This is an special case to start IMAP PUSH via its adapter Intent imap = new Intent(this, LegacyImapSyncAdapterService.class); startService(imap); } private void reconcileAndStartServices() { /** * We can get here before the ACTION_UPGRADE_BROADCAST is received, so make sure the * accounts are converted otherwise terrible, horrible things will happen. */ onAppUpgrade(); // Reconcile accounts AccountReconciler.reconcileAccounts(this); // Starts remote services, if any EmailServiceUtils.startRemoteServices(this); } private void performOneTimeInitialization() { final Preferences pref = Preferences.getPreferences(this); int progress = pref.getOneTimeInitializationProgress(); final int initialProgress = progress; if (progress < 1) { LogUtils.i(Logging.LOG_TAG, "Onetime initialization: 1"); progress = 1; EmailServiceUtils.enableExchangeComponent(this); } if (progress < 2) { LogUtils.i(Logging.LOG_TAG, "Onetime initialization: 2"); progress = 2; setImapDeletePolicy(this); } // Add your initialization steps here. // Use "progress" to skip the initializations that's already done before. // Using this preference also makes it safe when a user skips an upgrade. (i.e. upgrading // version N to version N+2) if (progress != initialProgress) { pref.setOneTimeInitializationProgress(progress); LogUtils.i(Logging.LOG_TAG, "Onetime initialization: completed."); } } /** * Sets the delete policy to the correct value for all IMAP accounts. This will have no * effect on either EAS or POP3 accounts. */ /*package*/ static void setImapDeletePolicy(Context context) { ContentResolver resolver = context.getContentResolver(); Cursor c = resolver.query(Account.CONTENT_URI, Account.CONTENT_PROJECTION, null, null, null); try { while (c.moveToNext()) { long recvAuthKey = c.getLong(Account.CONTENT_HOST_AUTH_KEY_RECV_COLUMN); HostAuth recvAuth = HostAuth.restoreHostAuthWithId(context, recvAuthKey); String legacyImapProtocol = context.getString(R.string.protocol_legacy_imap); if (legacyImapProtocol.equals(recvAuth.mProtocol)) { int flags = c.getInt(Account.CONTENT_FLAGS_COLUMN); flags &= ~Account.FLAGS_DELETE_POLICY_MASK; flags |= Account.DELETE_POLICY_ON_DELETE << Account.FLAGS_DELETE_POLICY_SHIFT; ContentValues cv = new ContentValues(); cv.put(AccountColumns.FLAGS, flags); long accountId = c.getLong(Account.CONTENT_ID_COLUMN); Uri uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId); resolver.update(uri, cv, null, null); } } } finally { c.close(); } } private void onSystemAccountChanged() { LogUtils.i(Logging.LOG_TAG, "System accounts updated."); reconcileAndStartServices(); // Resend all notifications, so that there is no notification that points to a removed // account. NotificationActionUtils.resendNotifications(getApplicationContext(), null /* all accounts */, null /* all folders */); } }