1 /* 2 * Copyright (C) 2014 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.exchange.service; 18 19 import android.app.Service; 20 import android.content.ContentResolver; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.database.Cursor; 24 import android.os.AsyncTask; 25 import android.os.Bundle; 26 import android.os.IBinder; 27 import android.provider.CalendarContract; 28 import android.provider.ContactsContract; 29 import android.text.TextUtils; 30 31 import com.android.emailcommon.TempDirectory; 32 import com.android.emailcommon.provider.Account; 33 import com.android.emailcommon.provider.EmailContent; 34 import com.android.emailcommon.provider.HostAuth; 35 import com.android.emailcommon.provider.Mailbox; 36 import com.android.emailcommon.service.EmailServiceProxy; 37 import com.android.emailcommon.service.EmailServiceStatus; 38 import com.android.emailcommon.service.EmailServiceVersion; 39 import com.android.emailcommon.service.HostAuthCompat; 40 import com.android.emailcommon.service.IEmailService; 41 import com.android.emailcommon.service.IEmailServiceCallback; 42 import com.android.emailcommon.service.SearchParams; 43 import com.android.emailcommon.service.ServiceProxy; 44 import com.android.exchange.Eas; 45 import com.android.exchange.eas.EasAutoDiscover; 46 import com.android.exchange.eas.EasFolderSync; 47 import com.android.exchange.eas.EasFullSyncOperation; 48 import com.android.exchange.eas.EasLoadAttachment; 49 import com.android.exchange.eas.EasOperation; 50 import com.android.exchange.eas.EasSearch; 51 import com.android.exchange.eas.EasSearchGal; 52 import com.android.exchange.eas.EasSendMeetingResponse; 53 import com.android.exchange.eas.EasSyncCalendar; 54 import com.android.exchange.eas.EasSyncContacts; 55 import com.android.exchange.provider.GalResult; 56 import com.android.mail.utils.LogUtils; 57 58 import java.util.HashSet; 59 import java.util.Set; 60 61 /** 62 * Service to handle all communication with the EAS server. Note that this is completely decoupled 63 * from the sync adapters; sync adapters should make blocking calls on this service to actually 64 * perform any operations. 65 */ 66 public class EasService extends Service { 67 68 private static final String TAG = Eas.LOG_TAG; 69 70 /** 71 * The content authorities that can be synced for EAS accounts. Initialization must wait until 72 * after we have a chance to call {@link EmailContent#init} (and, for future content types, 73 * possibly other initializations) because that's how we can know what the email authority is. 74 */ 75 private static String[] AUTHORITIES_TO_SYNC; 76 77 /** Bookkeeping for ping tasks & sync threads management. */ 78 private final PingSyncSynchronizer mSynchronizer; 79 80 /** 81 * Implementation of the IEmailService interface. 82 * For the most part these calls should consist of creating the correct {@link EasOperation} 83 * class and calling {@link #doOperation} with it. 84 */ 85 private final IEmailService.Stub mBinder = new IEmailService.Stub() { 86 @Override 87 public void loadAttachment(final IEmailServiceCallback callback, final long accountId, 88 final long attachmentId, final boolean background) { 89 LogUtils.d(TAG, "IEmailService.loadAttachment: %d", attachmentId); 90 final EasLoadAttachment operation = new EasLoadAttachment(EasService.this, accountId, 91 attachmentId, callback); 92 doOperation(operation, "IEmailService.loadAttachment"); 93 } 94 95 @Override 96 public void updateFolderList(final long accountId) { 97 final EasFolderSync operation = new EasFolderSync(EasService.this, accountId); 98 doOperation(operation, "IEmailService.updateFolderList"); 99 } 100 101 public void sendMail(final long accountId) { 102 // TODO: We should get rid of sendMail, and this is done in sync. 103 LogUtils.wtf(TAG, "unexpected call to EasService.sendMail"); 104 } 105 106 public int sync(final long accountId, Bundle syncExtras) { 107 EasFullSyncOperation op = new EasFullSyncOperation(EasService.this, accountId, syncExtras); 108 return convertToEmailServiceStatus(doOperation(op, "IEmailService.sync")); 109 } 110 111 @Override 112 public void pushModify(final long accountId) { 113 LogUtils.d(TAG, "IEmailService.pushModify: %d", accountId); 114 final Account account = Account.restoreAccountWithId(EasService.this, accountId); 115 if (pingNeededForAccount(account)) { 116 mSynchronizer.pushModify(account); 117 } else { 118 mSynchronizer.pushStop(accountId); 119 } 120 } 121 122 @Override 123 public Bundle validate(final HostAuthCompat hostAuthCom) { 124 final HostAuth hostAuth = hostAuthCom.toHostAuth(); 125 final EasFolderSync operation = new EasFolderSync(EasService.this, hostAuth); 126 doOperation(operation, "IEmailService.validate"); 127 return operation.getValidationResult(); 128 } 129 130 @Override 131 public int searchMessages(final long accountId, final SearchParams searchParams, 132 final long destMailboxId) { 133 final EasSearch operation = new EasSearch(EasService.this, accountId, searchParams, 134 destMailboxId); 135 doOperation(operation, "IEmailService.searchMessages"); 136 return operation.getTotalResults(); 137 } 138 139 @Override 140 public void sendMeetingResponse(final long messageId, final int response) { 141 EmailContent.Message msg = EmailContent.Message.restoreMessageWithId(EasService.this, 142 messageId); 143 if (msg == null) { 144 LogUtils.e(TAG, "Could not load message %d in sendMeetingResponse", messageId); 145 return; 146 } 147 148 final EasSendMeetingResponse operation = new EasSendMeetingResponse(EasService.this, 149 msg.mAccountKey, msg, response); 150 doOperation(operation, "IEmailService.sendMeetingResponse"); 151 } 152 153 @Override 154 public Bundle autoDiscover(final String username, final String password) { 155 final String domain = EasAutoDiscover.getDomain(username); 156 for (int attempt = 0; attempt <= EasAutoDiscover.ATTEMPT_MAX; attempt++) { 157 LogUtils.d(TAG, "autodiscover attempt %d", attempt); 158 final String uri = EasAutoDiscover.genUri(domain, attempt); 159 Bundle result = autoDiscoverInternal(uri, attempt, username, password, true); 160 int resultCode = result.getInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE); 161 if (resultCode != EasAutoDiscover.RESULT_BAD_RESPONSE) { 162 return result; 163 } else { 164 LogUtils.d(TAG, "got BAD_RESPONSE"); 165 } 166 } 167 return null; 168 } 169 170 private Bundle autoDiscoverInternal(final String uri, final int attempt, 171 final String username, final String password, 172 final boolean canRetry) { 173 final EasAutoDiscover op = new EasAutoDiscover(EasService.this, uri, attempt, 174 username, password); 175 final int result = op.performOperation(); 176 if (result == EasAutoDiscover.RESULT_REDIRECT) { 177 // Try again recursively with the new uri. TODO we should limit the number of redirects. 178 final String redirectUri = op.getRedirectUri(); 179 return autoDiscoverInternal(redirectUri, attempt, username, password, canRetry); 180 } else if (result == EasAutoDiscover.RESULT_SC_UNAUTHORIZED) { 181 if (canRetry && username.contains("@")) { 182 // Try again using the bare user name 183 final int atSignIndex = username.indexOf('@'); 184 final String bareUsername = username.substring(0, atSignIndex); 185 LogUtils.d(TAG, "%d received; trying username: %s", result, atSignIndex); 186 // Try again recursively, but this time don't allow retries for username. 187 return autoDiscoverInternal(uri, attempt, bareUsername, password, false); 188 } else { 189 // Either we're already on our second try or the username didn't have an "@" 190 // to begin with. Either way, failure. 191 final Bundle bundle = new Bundle(1); 192 bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE, 193 EasAutoDiscover.RESULT_OTHER_FAILURE); 194 return bundle; 195 } 196 } else if (result != EasAutoDiscover.RESULT_OK) { 197 // Return failure, we'll try again with an alternate address 198 final Bundle bundle = new Bundle(1); 199 bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE, 200 EasAutoDiscover.RESULT_BAD_RESPONSE); 201 return bundle; 202 } 203 // Success. 204 return op.getResultBundle(); 205 } 206 207 @Override 208 public void setLogging(final int flags) { 209 LogUtils.d(TAG, "IEmailService.setLogging"); 210 } 211 212 @Override 213 public void deleteExternalAccountPIMData(final String emailAddress) { 214 LogUtils.d(TAG, "IEmailService.deleteAccountPIMData"); 215 if (emailAddress != null) { 216 // TODO: stop pings 217 final Context context = EasService.this; 218 EasSyncContacts.wipeAccountFromContentProvider(context, emailAddress); 219 EasSyncCalendar.wipeAccountFromContentProvider(context, emailAddress); 220 } 221 } 222 223 public int getApiVersion() { 224 return EmailServiceVersion.CURRENT; 225 } 226 }; 227 228 /** 229 * Content selection string for getting all accounts that are configured for push. 230 * TODO: Add protocol check so that we don't get e.g. IMAP accounts here. 231 * (Not currently necessary but eventually will be.) 232 */ 233 private static final String PUSH_ACCOUNTS_SELECTION = 234 EmailContent.AccountColumns.SYNC_INTERVAL + 235 "=" + Integer.toString(Account.CHECK_INTERVAL_PUSH); 236 237 /** {@link AsyncTask} to restart pings for all accounts that need it. */ 238 private class RestartPingsTask extends AsyncTask<Void, Void, Void> { 239 private boolean mHasRestartedPing = false; 240 241 @Override doInBackground(Void... params)242 protected Void doInBackground(Void... params) { 243 final Cursor c = EasService.this.getContentResolver().query(Account.CONTENT_URI, 244 Account.CONTENT_PROJECTION, PUSH_ACCOUNTS_SELECTION, null, null); 245 if (c != null) { 246 try { 247 while (c.moveToNext()) { 248 final Account account = new Account(); 249 LogUtils.d(TAG, "RestartPingsTask starting ping for %s", account); 250 account.restore(c); 251 if (EasService.this.pingNeededForAccount(account)) { 252 mHasRestartedPing = true; 253 EasService.this.mSynchronizer.pushModify(account); 254 } 255 } 256 } finally { 257 c.close(); 258 } 259 } 260 return null; 261 } 262 263 @Override onPostExecute(Void result)264 protected void onPostExecute(Void result) { 265 if (!mHasRestartedPing) { 266 LogUtils.d(TAG, "RestartPingsTask did not start any pings."); 267 EasService.this.mSynchronizer.stopServiceIfIdle(); 268 } 269 } 270 } 271 EasService()272 public EasService() { 273 super(); 274 mSynchronizer = new PingSyncSynchronizer(this); 275 } 276 277 @Override onCreate()278 public void onCreate() { 279 LogUtils.d(TAG, "EasService.onCreate"); 280 super.onCreate(); 281 TempDirectory.setTempDirectory(this); 282 EmailContent.init(this); 283 AUTHORITIES_TO_SYNC = new String[] { 284 EmailContent.AUTHORITY, 285 CalendarContract.AUTHORITY, 286 ContactsContract.AUTHORITY 287 }; 288 289 // Restart push for all accounts that need it. Because this requires DB loads, we do it in 290 // an AsyncTask, and we startService to ensure that we stick around long enough for the 291 // task to complete. The task will stop the service if necessary after it's done. 292 startService(new Intent(this, EasService.class)); 293 new RestartPingsTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 294 } 295 296 @Override onDestroy()297 public void onDestroy() { 298 mSynchronizer.stopAllPings(); 299 } 300 301 @Override onBind(final Intent intent)302 public IBinder onBind(final Intent intent) { 303 return mBinder; 304 } 305 306 @Override onStartCommand(final Intent intent, final int flags, final int startId)307 public int onStartCommand(final Intent intent, final int flags, final int startId) { 308 if (intent != null && 309 TextUtils.equals(Eas.EXCHANGE_SERVICE_INTENT_ACTION, intent.getAction())) { 310 if (intent.getBooleanExtra(ServiceProxy.EXTRA_FORCE_SHUTDOWN, false)) { 311 // We've been asked to forcibly shutdown. This happens if email accounts are 312 // deleted, otherwise we can get errors if services are still running for 313 // accounts that are now gone. 314 // TODO: This is kind of a hack, it would be nicer if we could handle it correctly 315 // if accounts disappear out from under us. 316 LogUtils.d(TAG, "Forced shutdown, killing process"); 317 System.exit(-1); 318 } 319 } 320 return START_STICKY; 321 } 322 doOperation(final EasOperation operation, final String loggingName)323 public int doOperation(final EasOperation operation, final String loggingName) { 324 LogUtils.d(TAG, "%s: %d", loggingName, operation.getAccountId()); 325 mSynchronizer.syncStart(operation.getAccountId()); 326 int result = EasOperation.RESULT_MIN_OK_RESULT; 327 // TODO: Do we need a wakelock here? For RPC coming from sync adapters, no -- the SA 328 // already has one. But for others, maybe? Not sure what's guaranteed for AIDL calls. 329 // If we add a wakelock (or anything else for that matter) here, must remember to undo 330 // it in the finally block below. 331 // On the other hand, even for SAs, it doesn't hurt to get a wakelock here. 332 try { 333 result = operation.performOperation(); 334 LogUtils.d(TAG, "Operation result %d", result); 335 return result; 336 } finally { 337 mSynchronizer.syncEnd(result >= EasOperation.RESULT_MIN_OK_RESULT, 338 operation.getAccount()); 339 } 340 } 341 342 /** 343 * Determine whether this account is configured with folders that are ready for push 344 * notifications. 345 * @param account The {@link Account} that we're interested in. 346 * @return Whether this account needs to ping. 347 */ pingNeededForAccount(final Account account)348 public boolean pingNeededForAccount(final Account account) { 349 // Check account existence. 350 if (account == null || account.mId == Account.NO_ACCOUNT) { 351 LogUtils.d(TAG, "Do not ping: Account not found or not valid"); 352 return false; 353 } 354 355 // Check if account is configured for a push sync interval. 356 if (account.mSyncInterval != Account.CHECK_INTERVAL_PUSH) { 357 LogUtils.d(TAG, "Do not ping: Account %d not configured for push", account.mId); 358 return false; 359 } 360 361 // Check security hold status of the account. 362 if ((account.mFlags & Account.FLAGS_SECURITY_HOLD) != 0) { 363 LogUtils.d(TAG, "Do not ping: Account %d is on security hold", account.mId); 364 return false; 365 } 366 367 // Check if the account has performed at least one sync so far (accounts must perform 368 // the initial sync before push is possible). 369 if (EmailContent.isInitialSyncKey(account.mSyncKey)) { 370 LogUtils.d(TAG, "Do not ping: Account %d has not done initial sync", account.mId); 371 return false; 372 } 373 374 // Check that there's at least one mailbox that is both configured for push notifications, 375 // and whose content type is enabled for sync in the account manager. 376 final android.accounts.Account amAccount = new android.accounts.Account( 377 account.mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE); 378 379 final Set<String> authsToSync = getAuthoritiesToSync(amAccount, AUTHORITIES_TO_SYNC); 380 // If we have at least one sync-enabled content type, check for syncing mailboxes. 381 if (!authsToSync.isEmpty()) { 382 final Cursor c = Mailbox.getMailboxesForPush(getContentResolver(), account.mId); 383 if (c != null) { 384 try { 385 while (c.moveToNext()) { 386 final int mailboxType = c.getInt(Mailbox.CONTENT_TYPE_COLUMN); 387 if (authsToSync.contains(Mailbox.getAuthority(mailboxType))) { 388 return true; 389 } 390 } 391 } finally { 392 c.close(); 393 } 394 } 395 } 396 LogUtils.d(TAG, "Do not ping: Account %d has no folders configured for push", account.mId); 397 return false; 398 } 399 searchGal(final Context context, final long accountId, final String filter, final int limit)400 static public GalResult searchGal(final Context context, final long accountId, 401 final String filter, final int limit) { 402 final EasSearchGal operation = new EasSearchGal(context, accountId, filter, limit); 403 // We don't use doOperation() here for two reasons: 404 // 1. This is a static function, doOperation is not, and we don't have an instance of 405 // EasService. 406 // 2. All doOperation() does besides this is stop the ping and then restart it. This is 407 // required during syncs, but not for GalSearches. 408 final int result = operation.performOperation(); 409 if (result == EasSearchGal.RESULT_OK) { 410 return operation.getResult(); 411 } else { 412 return null; 413 } 414 } 415 416 /** 417 * Converts from an EasOperation status to a status code defined in EmailServiceStatus. 418 * This is used to communicate the status of a sync operation to the caller. 419 * @param easStatus result returned from an EasOperation 420 * @return EmailServiceStatus 421 */ convertToEmailServiceStatus(int easStatus)422 private int convertToEmailServiceStatus(int easStatus) { 423 if (easStatus >= EasOperation.RESULT_MIN_OK_RESULT) { 424 return EmailServiceStatus.SUCCESS; 425 } 426 switch (easStatus) { 427 case EasOperation.RESULT_ABORT: 428 case EasOperation.RESULT_RESTART: 429 // This should only happen if a ping is interruped for some reason. We would not 430 // expect see that here, since this should only be called for a sync. 431 LogUtils.e(TAG, "Abort or Restart easStatus"); 432 return EmailServiceStatus.SUCCESS; 433 434 case EasOperation.RESULT_TOO_MANY_REDIRECTS: 435 return EmailServiceStatus.INTERNAL_ERROR; 436 437 case EasOperation.RESULT_NETWORK_PROBLEM: 438 // This is due to an IO error, we need the caller to know about this so that it 439 // can let the syncManager know. 440 return EmailServiceStatus.IO_ERROR; 441 442 case EasOperation.RESULT_FORBIDDEN: 443 case EasOperation.RESULT_AUTHENTICATION_ERROR: 444 return EmailServiceStatus.LOGIN_FAILED; 445 446 case EasOperation.RESULT_PROVISIONING_ERROR: 447 return EmailServiceStatus.PROVISIONING_ERROR; 448 449 case EasOperation.RESULT_CLIENT_CERTIFICATE_REQUIRED: 450 return EmailServiceStatus.CLIENT_CERTIFICATE_ERROR; 451 452 case EasOperation.RESULT_PROTOCOL_VERSION_UNSUPPORTED: 453 return EmailServiceStatus.PROTOCOL_ERROR; 454 455 case EasOperation.RESULT_INITIALIZATION_FAILURE: 456 case EasOperation.RESULT_HARD_DATA_FAILURE: 457 case EasOperation.RESULT_OTHER_FAILURE: 458 return EmailServiceStatus.INTERNAL_ERROR; 459 460 case EasOperation.RESULT_NON_FATAL_ERROR: 461 // We do not expect to see this error here: This should be consumed in 462 // EasFullSyncOperation. The only case this occurs in is when we try to send 463 // a message in the outbox, and there's some problem with the message locally 464 // that prevents it from being sent. We return a 465 LogUtils.e(TAG, "Other non-fatal error easStatus %d", easStatus); 466 return EmailServiceStatus.SUCCESS; 467 } 468 LogUtils.e(TAG, "Unexpected easStatus %d", easStatus); 469 return EmailServiceStatus.INTERNAL_ERROR; 470 } 471 472 473 /** 474 * Determine which content types are set to sync for an account. 475 * @param account The account whose sync settings we're looking for. 476 * @param authorities All possible authorities we could care about. 477 * @return The authorities for the content types we want to sync for account. 478 */ getAuthoritiesToSync(final android.accounts.Account account, final String[] authorities)479 public static Set<String> getAuthoritiesToSync(final android.accounts.Account account, 480 final String[] authorities) { 481 final HashSet<String> authsToSync = new HashSet(); 482 for (final String authority : authorities) { 483 if (ContentResolver.getSyncAutomatically(account, authority)) { 484 authsToSync.add(authority); 485 } 486 } 487 return authsToSync; 488 } 489 } 490