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