1 /* 2 * Copyright (C) 2008-2009 Marc Blank 3 * Licensed to The Android Open Source Project. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18 package com.android.exchange; 19 20 import android.content.ContentResolver; 21 import android.content.ContentUris; 22 import android.content.ContentValues; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.database.ContentObserver; 26 import android.database.Cursor; 27 import android.net.Uri; 28 import android.os.Bundle; 29 import android.os.Handler; 30 import android.os.IBinder; 31 import android.os.RemoteException; 32 import android.provider.CalendarContract; 33 import android.provider.CalendarContract.Calendars; 34 import android.provider.CalendarContract.Events; 35 36 import com.android.emailcommon.provider.Account; 37 import com.android.emailcommon.provider.EmailContent.Attachment; 38 import com.android.emailcommon.provider.EmailContent.MailboxColumns; 39 import com.android.emailcommon.provider.EmailContent.Message; 40 import com.android.emailcommon.provider.HostAuth; 41 import com.android.emailcommon.provider.Mailbox; 42 import com.android.emailcommon.provider.MailboxUtilities; 43 import com.android.emailcommon.provider.ProviderUnavailableException; 44 import com.android.emailcommon.service.AccountServiceProxy; 45 import com.android.emailcommon.service.IEmailService; 46 import com.android.emailcommon.service.IEmailServiceCallback; 47 import com.android.emailcommon.service.IEmailServiceCallback.Stub; 48 import com.android.emailcommon.service.SearchParams; 49 import com.android.emailsync.AbstractSyncService; 50 import com.android.emailsync.PartRequest; 51 import com.android.emailsync.SyncManager; 52 import com.android.exchange.eas.EasSearch; 53 import com.android.exchange.utility.FileLogger; 54 import com.android.mail.providers.UIProvider.AccountCapabilities; 55 import com.android.mail.utils.LogUtils; 56 57 import java.util.concurrent.ConcurrentHashMap; 58 59 /** 60 * The ExchangeService handles all aspects of starting, maintaining, and stopping the various sync 61 * adapters used by Exchange. However, it is capable of handing any kind of email sync, and it 62 * would be appropriate to use for IMAP push, when that functionality is added to the Email 63 * application. 64 * 65 * The Email application communicates with EAS sync adapters via ExchangeService's binder interface, 66 * which exposes UI-related functionality to the application (see the definitions below) 67 * 68 * ExchangeService uses ContentObservers to detect changes to accounts, mailboxes, and messages in 69 * order to maintain proper 2-way syncing of data. (More documentation to follow) 70 * 71 */ 72 public class ExchangeService extends SyncManager { 73 74 private static final String TAG = Eas.LOG_TAG; 75 76 private static final String WHERE_PUSH_OR_PING_NOT_ACCOUNT_MAILBOX = 77 MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.TYPE + "!=" + 78 Mailbox.TYPE_EAS_ACCOUNT_MAILBOX + " and " + MailboxColumns.SYNC_INTERVAL + 79 " IN (" + Mailbox.CHECK_INTERVAL_PING + ',' + Mailbox.CHECK_INTERVAL_PUSH + ')'; 80 private static final String WHERE_MAILBOX_KEY = Message.MAILBOX_KEY + "=?"; 81 private static final String WHERE_CALENDAR_ID = Events.CALENDAR_ID + "=?"; 82 private static final String ACCOUNT_KEY_IN = MailboxColumns.ACCOUNT_KEY + " in ("; 83 84 // Offsets into the syncStatus data for EAS that indicate type, exit status, and change count 85 // The format is S<type_char>:<exit_char>:<change_count> 86 public static final int STATUS_TYPE_CHAR = 1; 87 public static final int STATUS_EXIT_CHAR = 3; 88 public static final int STATUS_CHANGE_COUNT_OFFSET = 5; 89 90 private static final int EAS_12_CAPABILITIES = 91 AccountCapabilities.SYNCABLE_FOLDERS | 92 AccountCapabilities.SERVER_SEARCH | 93 AccountCapabilities.FOLDER_SERVER_SEARCH | 94 AccountCapabilities.SMART_REPLY | 95 AccountCapabilities.SERVER_SEARCH | 96 AccountCapabilities.UNDO; 97 98 private static final int EAS_2_CAPABILITIES = 99 AccountCapabilities.SYNCABLE_FOLDERS | 100 AccountCapabilities.SMART_REPLY | 101 AccountCapabilities.UNDO; 102 103 // We synchronize on this for all actions affecting the service and error maps 104 private static final Object sSyncLock = new Object(); 105 private String mEasAccountSelector; 106 107 // Concurrent because CalendarSyncAdapter can modify the map during a wipe 108 private final ConcurrentHashMap<Long, CalendarObserver> mCalendarObservers = 109 new ConcurrentHashMap<Long, CalendarObserver>(); 110 111 private final Intent mIntent = new Intent(Eas.EXCHANGE_SERVICE_INTENT_ACTION); 112 113 /** 114 * Create our EmailService implementation here. 115 */ 116 private final IEmailService.Stub mBinder = new IEmailService.Stub() { 117 118 @Override 119 public Bundle validate(HostAuth hostAuth) throws RemoteException { 120 return AbstractSyncService.validate(EasSyncService.class, 121 hostAuth, ExchangeService.this); 122 } 123 124 @Override 125 public Bundle autoDiscover(String userName, String password) throws RemoteException { 126 HostAuth hostAuth = new HostAuth(); 127 hostAuth.mLogin = userName; 128 hostAuth.mPassword = password; 129 hostAuth.mFlags = HostAuth.FLAG_AUTHENTICATE | HostAuth.FLAG_SSL; 130 hostAuth.mPort = 443; 131 return new EasSyncService().tryAutodiscover(ExchangeService.this, hostAuth); 132 } 133 134 @Override 135 public void loadAttachment(final IEmailServiceCallback callback, final long accountId, 136 final long attachmentId, final boolean background) throws RemoteException { 137 Attachment att = Attachment.restoreAttachmentWithId(ExchangeService.this, attachmentId); 138 log("loadAttachment " + attachmentId + ": " + att.mFileName); 139 sendMessageRequest(new PartRequest(att, null, null)); 140 } 141 142 @Override 143 public void updateFolderList(long accountId) throws RemoteException { 144 reloadFolderList(ExchangeService.this, accountId, false); 145 } 146 147 @Override 148 public void setLogging(int flags) throws RemoteException { 149 // Protocol logging 150 Eas.setUserDebug(flags); 151 // Sync logging 152 setUserDebug(flags); 153 } 154 155 @Override 156 public void sendMeetingResponse(long messageId, int response) throws RemoteException { 157 sendMessageRequest(new MeetingResponseRequest(messageId, response)); 158 } 159 160 /** 161 * Delete PIM (calendar, contacts) data for the specified account 162 * 163 * @param emailAddress the email address for the account whose data should be deleted 164 * @throws RemoteException 165 */ 166 @Override 167 public void deleteAccountPIMData(final String emailAddress) throws RemoteException { 168 // ExchangeService is deprecated so I am deleting rather than fixing this function. 169 } 170 171 @Override 172 public int searchMessages(long accountId, SearchParams searchParams, long destMailboxId) { 173 SyncManager exchangeService = INSTANCE; 174 if (exchangeService == null) return 0; 175 EasSearch op = new EasSearch(exchangeService, accountId, searchParams, destMailboxId); 176 op.performOperation(); 177 return op.getTotalResults(); 178 } 179 180 @Override 181 public void sendMail(long accountId) throws RemoteException {} 182 183 @Override 184 public void pushModify(long accountId) throws RemoteException {} 185 186 @Override 187 public void sync(final long accountId, final boolean updateFolderList, 188 final int mailboxType, final long[] folders) {} 189 }; 190 191 /** 192 * Return a list of all Accounts in EmailProvider. Because the result of this call may be used 193 * in account reconciliation, an exception is thrown if the result cannot be guaranteed accurate 194 * @param context the caller's context 195 * @param accounts a list that Accounts will be added into 196 * @return the list of Accounts 197 * @throws ProviderUnavailableException if the list of Accounts cannot be guaranteed valid 198 */ 199 @Override 200 public AccountList collectAccounts(Context context, AccountList accounts) { 201 ContentResolver resolver = context.getContentResolver(); 202 Cursor c = resolver.query(Account.CONTENT_URI, Account.CONTENT_PROJECTION, null, null, 203 null); 204 // We must throw here; callers might use the information we provide for reconciliation, etc. 205 if (c == null) throw new ProviderUnavailableException(); 206 try { 207 ContentValues cv = new ContentValues(); 208 while (c.moveToNext()) { 209 long hostAuthId = c.getLong(Account.CONTENT_HOST_AUTH_KEY_RECV_COLUMN); 210 if (hostAuthId > 0) { 211 HostAuth ha = HostAuth.restoreHostAuthWithId(context, hostAuthId); 212 if (ha != null && ha.mProtocol.equals(Eas.PROTOCOL)) { 213 Account account = new Account(); 214 account.restore(c); 215 // Cache the HostAuth 216 account.mHostAuthRecv = ha; 217 accounts.add(account); 218 // Fixup flags for inbox (should accept moved mail) 219 Mailbox inbox = Mailbox.restoreMailboxOfType(context, account.mId, 220 Mailbox.TYPE_INBOX); 221 if (inbox != null && 222 ((inbox.mFlags & Mailbox.FLAG_ACCEPTS_MOVED_MAIL) == 0)) { 223 cv.put(MailboxColumns.FLAGS, 224 inbox.mFlags | Mailbox.FLAG_ACCEPTS_MOVED_MAIL); 225 resolver.update( 226 ContentUris.withAppendedId(Mailbox.CONTENT_URI, inbox.mId), cv, 227 null, null); 228 } 229 } 230 } 231 } 232 } finally { 233 c.close(); 234 } 235 return accounts; 236 } 237 238 public static boolean onSecurityHold(Account account) { 239 return (account.mFlags & Account.FLAGS_SECURITY_HOLD) != 0; 240 } 241 242 private static boolean onSyncDisabledHold(Account account) { 243 return (account.mFlags & Account.FLAGS_SYNC_DISABLED) != 0; 244 } 245 246 private static Uri eventsAsSyncAdapter(final Uri uri, final String account, 247 final String accountType) { 248 return uri.buildUpon() 249 .appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true") 250 .appendQueryParameter(Calendars.ACCOUNT_NAME, account) 251 .appendQueryParameter(Calendars.ACCOUNT_TYPE, accountType).build(); 252 } 253 254 /** 255 * Unregister all CalendarObserver's 256 */ 257 static public void unregisterCalendarObservers() { 258 ExchangeService exchangeService = (ExchangeService)INSTANCE; 259 if (exchangeService == null) return; 260 ContentResolver resolver = exchangeService.mResolver; 261 for (CalendarObserver observer: exchangeService.mCalendarObservers.values()) { 262 resolver.unregisterContentObserver(observer); 263 } 264 exchangeService.mCalendarObservers.clear(); 265 } 266 267 private class CalendarObserver extends ContentObserver { 268 long mAccountId; 269 long mCalendarId; 270 long mSyncEvents; 271 String mAccountName; 272 273 public CalendarObserver(Handler handler, Account account) { 274 super(handler); 275 mAccountId = account.mId; 276 mAccountName = account.mEmailAddress; 277 278 // Find the Calendar for this account 279 Cursor c = mResolver.query(Calendars.CONTENT_URI, 280 new String[] {Calendars._ID, Calendars.SYNC_EVENTS}, 281 CALENDAR_SELECTION, 282 new String[] {account.mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE}, 283 null); 284 if (c != null) { 285 // Save its id and its sync events status 286 try { 287 if (c.moveToFirst()) { 288 mCalendarId = c.getLong(0); 289 mSyncEvents = c.getLong(1); 290 } 291 } finally { 292 c.close(); 293 } 294 } 295 } 296 297 private void onChangeInBackground() { 298 try { 299 Cursor c = mResolver.query(Calendars.CONTENT_URI, 300 new String[] {Calendars.SYNC_EVENTS}, Calendars._ID + "=?", 301 new String[] {Long.toString(mCalendarId)}, null); 302 if (c == null) return; 303 // Get its sync events; if it's changed, we've got work to do 304 try { 305 if (c.moveToFirst()) { 306 long newSyncEvents = c.getLong(0); 307 if (newSyncEvents != mSyncEvents) { 308 log("_sync_events changed for calendar in " + mAccountName); 309 Mailbox mailbox = Mailbox.restoreMailboxOfType(INSTANCE, 310 mAccountId, Mailbox.TYPE_CALENDAR); 311 // Sanity check for mailbox deletion 312 if (mailbox == null) return; 313 ContentValues cv = new ContentValues(); 314 if (newSyncEvents == 0) { 315 // When sync is disabled, we're supposed to delete 316 // all events in the calendar 317 log("Deleting events and setting syncKey to 0 for " + 318 mAccountName); 319 // First, stop any sync that's ongoing 320 stopManualSync(mailbox.mId); 321 // Set the syncKey to 0 (reset) 322 EasSyncService service = 323 EasSyncService.getServiceForMailbox( 324 INSTANCE, mailbox); 325 326 // CalendarSyncAdapter is gone, and this class is deprecated. 327 // Just leaving this commented out code here for reference: 328 // Reset the sync key locally and stop syncing 329 // CalendarSyncAdapter adapter = 330 // new CalendarSyncAdapter(service); 331 // try { 332 // adapter.setSyncKey("0", false); 333 // } catch (IOException e) { 334 // // The provider can't be reached; nothing to be done 335 // } 336 337 cv.put(Mailbox.SYNC_KEY, "0"); 338 cv.put(Mailbox.SYNC_INTERVAL, 339 Mailbox.CHECK_INTERVAL_NEVER); 340 mResolver.update(ContentUris.withAppendedId( 341 Mailbox.CONTENT_URI, mailbox.mId), cv, null, 342 null); 343 // Delete all events using the sync adapter 344 // parameter so that the deletion is only local 345 Uri eventsAsSyncAdapter = eventsAsSyncAdapter(Events.CONTENT_URI, 346 mAccountName, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE); 347 mResolver.delete(eventsAsSyncAdapter, WHERE_CALENDAR_ID, 348 new String[] {Long.toString(mCalendarId)}); 349 } else { 350 // Make this a push mailbox and kick; this will start 351 // a resync of the Calendar; the account mailbox will 352 // ping on this during the next cycle of the ping loop 353 cv.put(Mailbox.SYNC_INTERVAL, 354 Mailbox.CHECK_INTERVAL_PUSH); 355 mResolver.update(ContentUris.withAppendedId( 356 Mailbox.CONTENT_URI, mailbox.mId), cv, null, 357 null); 358 kick("calendar sync changed"); 359 } 360 361 // Save away the new value 362 mSyncEvents = newSyncEvents; 363 } 364 } 365 } finally { 366 c.close(); 367 } 368 } catch (ProviderUnavailableException e) { 369 LogUtils.w(TAG, "Observer failed; provider unavailable"); 370 } 371 } 372 373 374 @Override 375 public synchronized void onChange(boolean selfChange) { 376 // See if the user has changed syncing of our calendar 377 if (!selfChange) { 378 new Thread(new Runnable() { 379 @Override 380 public void run() { 381 onChangeInBackground(); 382 } 383 }, "Calendar Observer").start(); 384 } 385 } 386 } 387 388 /** 389 * Blocking call to the account reconciler 390 */ 391 @Override 392 public void runAccountReconcilerSync(Context context) { 393 alwaysLog("Reconciling accounts..."); 394 new AccountServiceProxy(context).reconcileAccounts( 395 Eas.PROTOCOL, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE); 396 } 397 398 public static void log(String str) { 399 log(TAG, str); 400 } 401 402 public static void log(String tag, String str) { 403 if (Eas.USER_LOG) { 404 LogUtils.d(tag, str); 405 if (Eas.FILE_LOG) { 406 FileLogger.log(tag, str); 407 } 408 } 409 } 410 411 public static void alwaysLog(String str) { 412 if (!Eas.USER_LOG) { 413 LogUtils.d(TAG, str); 414 } else { 415 log(str); 416 } 417 } 418 419 /** 420 * EAS requires a unique device id, so that sync is possible from a variety of different 421 * devices (e.g. the syncKey is specific to a device) If we're on an emulator or some other 422 * device that doesn't provide one, we can create it as "device". 423 * This would work on a real device as well, but it would be better to use the "real" id if 424 * it's available 425 */ 426 static public String getDeviceId(Context context) { 427 if (sDeviceId == null) { 428 sDeviceId = new AccountServiceProxy(context).getDeviceId(); 429 alwaysLog("Received deviceId from Email app: " + sDeviceId); 430 } 431 return sDeviceId; 432 } 433 434 @Override 435 public IBinder onBind(Intent arg0) { 436 return mBinder; 437 } 438 439 static private void reloadFolderListFailed(long accountId) { 440 441 } 442 443 static public void reloadFolderList(Context context, long accountId, boolean force) { 444 SyncManager exchangeService = INSTANCE; 445 if (exchangeService == null) return; 446 Cursor c = context.getContentResolver().query(Mailbox.CONTENT_URI, 447 Mailbox.CONTENT_PROJECTION, MailboxColumns.ACCOUNT_KEY + "=? AND " + 448 MailboxColumns.TYPE + "=?", 449 new String[] {Long.toString(accountId), 450 Long.toString(Mailbox.TYPE_EAS_ACCOUNT_MAILBOX)}, null); 451 try { 452 if (c.moveToFirst()) { 453 synchronized(sSyncLock) { 454 Mailbox mailbox = new Mailbox(); 455 mailbox.restore(c); 456 Account acct = Account.restoreAccountWithId(context, accountId); 457 if (acct == null) { 458 reloadFolderListFailed(accountId); 459 return; 460 } 461 String syncKey = acct.mSyncKey; 462 // No need to reload the list if we don't have one 463 if (!force && (syncKey == null || syncKey.equals("0"))) { 464 reloadFolderListFailed(accountId); 465 return; 466 } 467 468 // Change all ping/push boxes to push/hold 469 ContentValues cv = new ContentValues(); 470 cv.put(Mailbox.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PUSH_HOLD); 471 context.getContentResolver().update(Mailbox.CONTENT_URI, cv, 472 WHERE_PUSH_OR_PING_NOT_ACCOUNT_MAILBOX, 473 new String[] {Long.toString(accountId)}); 474 log("Set push/ping boxes to push/hold"); 475 476 long id = mailbox.mId; 477 AbstractSyncService svc = exchangeService.mServiceMap.get(id); 478 // Tell the service we're done 479 if (svc != null) { 480 synchronized (svc.getSynchronizer()) { 481 svc.stop(); 482 // Interrupt the thread so that it can stop 483 Thread thread = svc.mThread; 484 if (thread != null) { 485 thread.setName(thread.getName() + " (Stopped)"); 486 thread.interrupt(); 487 } 488 } 489 // Abandon the service 490 exchangeService.releaseMailbox(id); 491 // And have it start naturally 492 kick("reload folder list"); 493 } 494 } 495 } 496 } finally { 497 c.close(); 498 } 499 } 500 501 /** 502 * Informs ExchangeService that an account has a new folder list; as a result, any existing 503 * folder might have become invalid. Therefore, we act as if the account has been deleted, and 504 * then we reinitialize it. 505 * 506 * @param acctId 507 */ 508 static public void stopNonAccountMailboxSyncsForAccount(long acctId) { 509 SyncManager exchangeService = INSTANCE; 510 if (exchangeService != null) { 511 exchangeService.stopAccountSyncs(acctId, false); 512 kick("reload folder list"); 513 } 514 } 515 516 /** 517 * Start up the ExchangeService service if it's not already running 518 * This is a stopgap for cases in which ExchangeService died (due to a crash somewhere in 519 * com.android.email) and hasn't been restarted. See the comment for onCreate for details 520 */ 521 static void checkExchangeServiceServiceRunning() { 522 SyncManager exchangeService = INSTANCE; 523 if (exchangeService == null) return; 524 if (sServiceThread == null) { 525 log("!!! checkExchangeServiceServiceRunning; starting service..."); 526 exchangeService.startService(new Intent(exchangeService, ExchangeService.class)); 527 } 528 } 529 530 @Override 531 public AccountObserver getAccountObserver( 532 Handler handler) { 533 return new AccountObserver(handler) { 534 @Override 535 public void newAccount(long acctId) { 536 Account acct = Account.restoreAccountWithId(getContext(), acctId); 537 if (acct == null) { 538 // This account is in a bad state; don't create the mailbox. 539 LogUtils.e(TAG, "Cannot initialize bad acctId: " + acctId); 540 return; 541 } 542 Mailbox main = new Mailbox(); 543 main.mDisplayName = Eas.ACCOUNT_MAILBOX_PREFIX; 544 main.mServerId = Eas.ACCOUNT_MAILBOX_PREFIX + System.nanoTime(); 545 main.mAccountKey = acct.mId; 546 main.mType = Mailbox.TYPE_EAS_ACCOUNT_MAILBOX; 547 main.mSyncInterval = Mailbox.CHECK_INTERVAL_PUSH; 548 main.mFlagVisible = false; 549 main.save(getContext()); 550 log("Initializing account: " + acct.mDisplayName); 551 } 552 }; 553 } 554 555 @Override 556 public void onStartup() { 557 // Do any required work to clean up our Mailboxes (this serves to upgrade 558 // mailboxes that existed prior to EmailProvider database version 17) 559 MailboxUtilities.fixupUninitializedParentKeys(this, getAccountsSelector()); 560 } 561 562 @Override 563 public AbstractSyncService getServiceForMailbox(Context context, 564 Mailbox m) { 565 switch(m.mType) { 566 case Mailbox.TYPE_EAS_ACCOUNT_MAILBOX: 567 return new EasAccountService(context, m); 568 case Mailbox.TYPE_OUTBOX: 569 return new EasOutboxService(context, m); 570 default: 571 return new EasSyncService(context, m); 572 } 573 } 574 575 @Override 576 public String getAccountsSelector() { 577 if (mEasAccountSelector == null) { 578 StringBuilder sb = new StringBuilder(ACCOUNT_KEY_IN); 579 boolean first = true; 580 synchronized (mAccountList) { 581 for (Account account : mAccountList) { 582 if (!first) { 583 sb.append(','); 584 } else { 585 first = false; 586 } 587 sb.append(account.mId); 588 } 589 } 590 sb.append(')'); 591 mEasAccountSelector = sb.toString(); 592 } 593 return mEasAccountSelector; 594 } 595 596 @Override 597 public String getAccountManagerType() { 598 return Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE; 599 } 600 601 @Override 602 public Intent getServiceIntent() { 603 return mIntent; 604 } 605 606 @Override 607 public Stub getCallbackProxy() { 608 return null; 609 } 610 611 /** 612 * Stop any ping in progress if required 613 * 614 * @param mailbox whose service has started 615 */ 616 @Override 617 public void onStartService(Mailbox mailbox) { 618 // If this is a ping mailbox, stop the ping 619 if (mailbox.mSyncInterval != Mailbox.CHECK_INTERVAL_PING) return; 620 long accountMailboxId = Mailbox.findMailboxOfType(this, mailbox.mAccountKey, 621 Mailbox.TYPE_EAS_ACCOUNT_MAILBOX); 622 // If our ping is running, stop it 623 final AbstractSyncService svc = getRunningService(accountMailboxId); 624 if (svc != null) { 625 log("Stopping ping due to sync of mailbox: " + mailbox.mDisplayName); 626 // Don't block; reset might perform network activity 627 new Thread(new Runnable() { 628 @Override 629 public void run() { 630 svc.reset(); 631 }}).start(); 632 } 633 } 634 } 635