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