Controller.java revision 85cf69d371e70c5b58aa736c90788e6ba545fbba
1/* 2 * Copyright (C) 2009 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 17package com.android.email; 18 19import com.android.email.mail.AuthenticationFailedException; 20import com.android.email.mail.MessagingException; 21import com.android.email.mail.Store; 22import com.android.email.mail.Folder.MessageRetrievalListener; 23import com.android.email.mail.store.Pop3Store.Pop3Message; 24import com.android.email.provider.AttachmentProvider; 25import com.android.email.provider.EmailContent; 26import com.android.email.provider.EmailContent.Account; 27import com.android.email.provider.EmailContent.Attachment; 28import com.android.email.provider.EmailContent.Body; 29import com.android.email.provider.EmailContent.Mailbox; 30import com.android.email.provider.EmailContent.MailboxColumns; 31import com.android.email.provider.EmailContent.Message; 32import com.android.email.provider.EmailContent.MessageColumns; 33import com.android.email.service.EmailServiceStatus; 34import com.android.email.service.IEmailService; 35import com.android.email.service.IEmailServiceCallback; 36 37import android.app.Service; 38import android.content.ContentResolver; 39import android.content.ContentUris; 40import android.content.ContentValues; 41import android.content.Context; 42import android.content.Intent; 43import android.database.Cursor; 44import android.net.Uri; 45import android.os.AsyncTask; 46import android.os.Bundle; 47import android.os.IBinder; 48import android.os.RemoteCallbackList; 49import android.os.RemoteException; 50import android.text.TextUtils; 51import android.util.Log; 52 53import java.io.FileNotFoundException; 54import java.io.IOException; 55import java.io.InputStream; 56import java.util.HashSet; 57import java.util.concurrent.ConcurrentHashMap; 58 59/** 60 * New central controller/dispatcher for Email activities that may require remote operations. 61 * Handles disambiguating between legacy MessagingController operations and newer provider/sync 62 * based code. We implement Service to allow loadAttachment calls to be sent in a consistent manner 63 * to IMAP, POP3, and EAS by AttachmentDownloadService 64 */ 65public class Controller { 66 private static final String TAG = "Controller"; 67 private static Controller sInstance; 68 private final Context mContext; 69 private Context mProviderContext; 70 private final MessagingController mLegacyController; 71 private final LegacyListener mLegacyListener = new LegacyListener(); 72 private final ServiceCallback mServiceCallback = new ServiceCallback(); 73 private final HashSet<Result> mListeners = new HashSet<Result>(); 74 /*package*/ final ConcurrentHashMap<Long, Boolean> mLegacyControllerMap = 75 new ConcurrentHashMap<Long, Boolean>(); 76 77 78 // Note that 0 is a syntactically valid account key; however there can never be an account 79 // with id = 0, so attempts to restore the account will return null. Null values are 80 // handled properly within the code, so this won't cause any issues. 81 private static final long ATTACHMENT_MAILBOX_ACCOUNT_KEY = 0; 82 /*package*/ static final String ATTACHMENT_MAILBOX_SERVER_ID = "__attachment_mailbox__"; 83 /*package*/ static final String ATTACHMENT_MESSAGE_UID_PREFIX = "__attachment_message__"; 84 private static final String WHERE_TYPE_ATTACHMENT = 85 MailboxColumns.TYPE + "=" + Mailbox.TYPE_ATTACHMENT; 86 private static final String WHERE_MAILBOX_KEY = MessageColumns.MAILBOX_KEY + "=?"; 87 88 private static final String[] MESSAGEID_TO_ACCOUNTID_PROJECTION = new String[] { 89 EmailContent.RECORD_ID, 90 EmailContent.MessageColumns.ACCOUNT_KEY 91 }; 92 private static final int MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID = 1; 93 94 private static final String[] BODY_SOURCE_KEY_PROJECTION = 95 new String[] {Body.SOURCE_MESSAGE_KEY}; 96 private static final int BODY_SOURCE_KEY_COLUMN = 0; 97 private static final String WHERE_MESSAGE_KEY = Body.MESSAGE_KEY + "=?"; 98 99 private static final String[] MESSAGEID_TO_MAILBOXID_PROJECTION = new String[] { 100 EmailContent.RECORD_ID, 101 EmailContent.MessageColumns.MAILBOX_KEY 102 }; 103 private static final int MESSAGEID_TO_MAILBOXID_COLUMN_MAILBOXID = 1; 104 105 // Service callbacks as set up via setCallback 106 private static RemoteCallbackList<IEmailServiceCallback> sCallbackList = 107 new RemoteCallbackList<IEmailServiceCallback>(); 108 109 protected Controller(Context _context) { 110 mContext = _context.getApplicationContext(); 111 mProviderContext = _context; 112 mLegacyController = MessagingController.getInstance(mProviderContext, this); 113 mLegacyController.addListener(mLegacyListener); 114 } 115 116 /** 117 * Cleanup for test. Mustn't be called for the regular {@link Controller}, as it's a 118 * singleton and lives till the process finishes. 119 * 120 * <p>However, this method MUST be called for mock instances. 121 */ 122 public void cleanupForTest() { 123 mLegacyController.removeListener(mLegacyListener); 124 } 125 126 /** 127 * Gets or creates the singleton instance of Controller. 128 */ 129 public synchronized static Controller getInstance(Context _context) { 130 if (sInstance == null) { 131 sInstance = new Controller(_context); 132 } 133 return sInstance; 134 } 135 136 /** 137 * Inject a mock controller. Used only for testing. Affects future calls to getInstance(). 138 * 139 * Tests that use this method MUST clean it up by calling this method again with null. 140 */ 141 public synchronized static void injectMockControllerForTest(Controller mockController) { 142 sInstance = mockController; 143 } 144 145 /** 146 * For testing only: Inject a different context for provider access. This will be 147 * used internally for access the underlying provider (e.g. getContentResolver().query()). 148 * @param providerContext the provider context to be used by this instance 149 */ 150 public void setProviderContext(Context providerContext) { 151 mProviderContext = providerContext; 152 } 153 154 /** 155 * Any UI code that wishes for callback results (on async ops) should register their callback 156 * here (typically from onResume()). Unregistered callbacks will never be called, to prevent 157 * problems when the command completes and the activity has already paused or finished. 158 * @param listener The callback that may be used in action methods 159 */ 160 public void addResultCallback(Result listener) { 161 synchronized (mListeners) { 162 mListeners.add(listener); 163 } 164 } 165 166 /** 167 * Any UI code that no longer wishes for callback results (on async ops) should unregister 168 * their callback here (typically from onPause()). Unregistered callbacks will never be called, 169 * to prevent problems when the command completes and the activity has already paused or 170 * finished. 171 * @param listener The callback that may no longer be used 172 */ 173 public void removeResultCallback(Result listener) { 174 synchronized (mListeners) { 175 mListeners.remove(listener); 176 } 177 } 178 179 /** 180 * Delete all Messages that live in the attachment mailbox 181 */ 182 public void deleteAttachmentMessages() { 183 // Note: There should only be one attachment mailbox at present 184 ContentResolver resolver = mProviderContext.getContentResolver(); 185 Cursor c = null; 186 try { 187 c = resolver.query(Mailbox.CONTENT_URI, EmailContent.ID_PROJECTION, 188 WHERE_TYPE_ATTACHMENT, null, null); 189 while (c.moveToNext()) { 190 long mailboxId = c.getLong(EmailContent.ID_PROJECTION_COLUMN); 191 // Must delete attachments BEFORE messages 192 AttachmentProvider.deleteAllMailboxAttachmentFiles(mProviderContext, 0, mailboxId); 193 resolver.delete(Message.CONTENT_URI, WHERE_MAILBOX_KEY, 194 new String[] {Long.toString(mailboxId)}); 195 } 196 } finally { 197 if (c != null) { 198 c.close(); 199 } 200 } 201 } 202 203 /** 204 * Returns the attachment Mailbox (where we store eml attachment Emails), creating one 205 * if necessary 206 * @return the account's temporary Mailbox 207 */ 208 public Mailbox getAttachmentMailbox() { 209 Cursor c = mProviderContext.getContentResolver().query(Mailbox.CONTENT_URI, 210 Mailbox.CONTENT_PROJECTION, WHERE_TYPE_ATTACHMENT, null, null); 211 try { 212 if (c.moveToFirst()) { 213 return new Mailbox().restore(c); 214 } 215 } finally { 216 c.close(); 217 } 218 Mailbox m = new Mailbox(); 219 m.mAccountKey = ATTACHMENT_MAILBOX_ACCOUNT_KEY; 220 m.mServerId = ATTACHMENT_MAILBOX_SERVER_ID; 221 m.mFlagVisible = false; 222 m.mDisplayName = ATTACHMENT_MAILBOX_SERVER_ID; 223 m.mSyncInterval = Mailbox.CHECK_INTERVAL_NEVER; 224 m.mType = Mailbox.TYPE_ATTACHMENT; 225 m.save(mProviderContext); 226 return m; 227 } 228 229 /** 230 * Create a Message from the Uri and store it in the attachment mailbox 231 * @param uri the uri containing message content 232 * @return the Message or null 233 */ 234 public Message loadMessageFromUri(Uri uri) { 235 Mailbox mailbox = getAttachmentMailbox(); 236 if (mailbox == null) return null; 237 try { 238 InputStream is = mProviderContext.getContentResolver().openInputStream(uri); 239 try { 240 // First, create a Pop3Message from the attachment and then parse it 241 Pop3Message pop3Message = new Pop3Message( 242 ATTACHMENT_MESSAGE_UID_PREFIX + System.currentTimeMillis(), null); 243 pop3Message.parse(is); 244 // Now, pull out the header fields 245 Message msg = new Message(); 246 LegacyConversions.updateMessageFields(msg, pop3Message, 0, mailbox.mId); 247 // Commit the message to the local store 248 msg.save(mProviderContext); 249 // Setup the rest of the message and mark it completely loaded 250 mLegacyController.copyOneMessageToProvider(pop3Message, msg, 251 Message.FLAG_LOADED_COMPLETE, mProviderContext); 252 // Restore the complete message and return it 253 return Message.restoreMessageWithId(mProviderContext, msg.mId); 254 } catch (MessagingException e) { 255 } catch (IOException e) { 256 } 257 } catch (FileNotFoundException e) { 258 } 259 return null; 260 } 261 262 /** 263 * Enable/disable logging for external sync services 264 * 265 * Generally this should be called by anybody who changes Email.DEBUG 266 */ 267 public void serviceLogging(int debugEnabled) { 268 IEmailService service = ExchangeUtils.getExchangeEmailService(mContext, mServiceCallback); 269 try { 270 service.setLogging(debugEnabled); 271 } catch (RemoteException e) { 272 // TODO Change exception handling to be consistent with however this method 273 // is implemented for other protocols 274 Log.d("updateMailboxList", "RemoteException" + e); 275 } 276 } 277 278 /** 279 * Request a remote update of mailboxes for an account. 280 */ 281 public void updateMailboxList(final long accountId) { 282 Utility.runAsync(new Runnable() { 283 @Override 284 public void run() { 285 final IEmailService service = getServiceForAccount(accountId); 286 if (service != null) { 287 // Service implementation 288 try { 289 service.updateFolderList(accountId); 290 } catch (RemoteException e) { 291 // TODO Change exception handling to be consistent with however this method 292 // is implemented for other protocols 293 Log.d("updateMailboxList", "RemoteException" + e); 294 } 295 } else { 296 // MessagingController implementation 297 mLegacyController.listFolders(accountId, mLegacyListener); 298 } 299 } 300 }); 301 } 302 303 /** 304 * Request a remote update of a mailbox. For use by the timed service. 305 * 306 * Functionally this is quite similar to updateMailbox(), but it's a separate API and 307 * separate callback in order to keep UI callbacks from affecting the service loop. 308 */ 309 public void serviceCheckMail(final long accountId, final long mailboxId, final long tag) { 310 IEmailService service = getServiceForAccount(accountId); 311 if (service != null) { 312 // Service implementation 313// try { 314 // TODO this isn't quite going to work, because we're going to get the 315 // generic (UI) callbacks and not the ones we need to restart the ol' service. 316 // service.startSync(mailboxId, tag); 317 mLegacyListener.checkMailFinished(mContext, accountId, mailboxId, tag); 318// } catch (RemoteException e) { 319 // TODO Change exception handling to be consistent with however this method 320 // is implemented for other protocols 321// Log.d("updateMailbox", "RemoteException" + e); 322// } 323 } else { 324 // MessagingController implementation 325 Utility.runAsync(new Runnable() { 326 public void run() { 327 mLegacyController.checkMail(accountId, tag, mLegacyListener); 328 } 329 }); 330 } 331 } 332 333 /** 334 * Request a remote update of a mailbox. 335 * 336 * The contract here should be to try and update the headers ASAP, in order to populate 337 * a simple message list. We should also at this point queue up a background task of 338 * downloading some/all of the messages in this mailbox, but that should be interruptable. 339 */ 340 public void updateMailbox(final long accountId, final long mailboxId) { 341 342 IEmailService service = getServiceForAccount(accountId); 343 if (service != null) { 344 // Service implementation 345 try { 346 service.startSync(mailboxId); 347 } catch (RemoteException e) { 348 // TODO Change exception handling to be consistent with however this method 349 // is implemented for other protocols 350 Log.d("updateMailbox", "RemoteException" + e); 351 } 352 } else { 353 // MessagingController implementation 354 Utility.runAsync(new Runnable() { 355 public void run() { 356 // TODO shouldn't be passing fully-build accounts & mailboxes into APIs 357 Account account = 358 EmailContent.Account.restoreAccountWithId(mProviderContext, accountId); 359 Mailbox mailbox = 360 EmailContent.Mailbox.restoreMailboxWithId(mProviderContext, mailboxId); 361 if (account == null || mailbox == null) { 362 return; 363 } 364 mLegacyController.synchronizeMailbox(account, mailbox, mLegacyListener); 365 } 366 }); 367 } 368 } 369 370 /** 371 * Request that any final work necessary be done, to load a message. 372 * 373 * Note, this assumes that the caller has already checked message.mFlagLoaded and that 374 * additional work is needed. There is no optimization here for a message which is already 375 * loaded. 376 * 377 * @param messageId the message to load 378 * @param callback the Controller callback by which results will be reported 379 */ 380 public void loadMessageForView(final long messageId) { 381 382 // Split here for target type (Service or MessagingController) 383 IEmailService service = getServiceForMessage(messageId); 384 if (service != null) { 385 // There is no service implementation, so we'll just jam the value, log the error, 386 // and get out of here. 387 Uri uri = ContentUris.withAppendedId(Message.CONTENT_URI, messageId); 388 ContentValues cv = new ContentValues(); 389 cv.put(MessageColumns.FLAG_LOADED, Message.FLAG_LOADED_COMPLETE); 390 mProviderContext.getContentResolver().update(uri, cv, null, null); 391 Log.d(Email.LOG_TAG, "Unexpected loadMessageForView() for service-based message."); 392 synchronized (mListeners) { 393 for (Result listener : mListeners) { 394 listener.loadMessageForViewCallback(null, messageId, 100); 395 } 396 } 397 } else { 398 // MessagingController implementation 399 Utility.runAsync(new Runnable() { 400 public void run() { 401 mLegacyController.loadMessageForView(messageId, mLegacyListener); 402 } 403 }); 404 } 405 } 406 407 408 /** 409 * Saves the message to a mailbox of given type. 410 * This is a synchronous operation taking place in the same thread as the caller. 411 * Upon return the message.mId is set. 412 * @param message the message (must have the mAccountId set). 413 * @param mailboxType the mailbox type (e.g. Mailbox.TYPE_DRAFTS). 414 */ 415 public void saveToMailbox(final EmailContent.Message message, final int mailboxType) { 416 long accountId = message.mAccountKey; 417 long mailboxId = findOrCreateMailboxOfType(accountId, mailboxType); 418 message.mMailboxKey = mailboxId; 419 message.save(mProviderContext); 420 } 421 422 /** 423 * @param accountId the account id 424 * @param mailboxType the mailbox type (e.g. EmailContent.Mailbox.TYPE_TRASH) 425 * @return the id of the mailbox. The mailbox is created if not existing. 426 * Returns Mailbox.NO_MAILBOX if the accountId or mailboxType are negative. 427 * Does not validate the input in other ways (e.g. does not verify the existence of account). 428 */ 429 public long findOrCreateMailboxOfType(long accountId, int mailboxType) { 430 if (accountId < 0 || mailboxType < 0) { 431 return Mailbox.NO_MAILBOX; 432 } 433 long mailboxId = 434 Mailbox.findMailboxOfType(mProviderContext, accountId, mailboxType); 435 return mailboxId == Mailbox.NO_MAILBOX ? createMailbox(accountId, mailboxType) : mailboxId; 436 } 437 438 /** 439 * Returns the server-side name for a specific mailbox. 440 * 441 * @param mailboxType the mailbox type 442 * @return the resource string corresponding to the mailbox type, empty if not found. 443 */ 444 /* package */ String getMailboxServerName(int mailboxType) { 445 int resId = -1; 446 switch (mailboxType) { 447 case Mailbox.TYPE_INBOX: 448 resId = R.string.mailbox_name_server_inbox; 449 break; 450 case Mailbox.TYPE_OUTBOX: 451 resId = R.string.mailbox_name_server_outbox; 452 break; 453 case Mailbox.TYPE_DRAFTS: 454 resId = R.string.mailbox_name_server_drafts; 455 break; 456 case Mailbox.TYPE_TRASH: 457 resId = R.string.mailbox_name_server_trash; 458 break; 459 case Mailbox.TYPE_SENT: 460 resId = R.string.mailbox_name_server_sent; 461 break; 462 case Mailbox.TYPE_JUNK: 463 resId = R.string.mailbox_name_server_junk; 464 break; 465 } 466 return resId != -1 ? mContext.getString(resId) : ""; 467 } 468 469 /** 470 * Create a mailbox given the account and mailboxType. 471 * TODO: Does this need to be signaled explicitly to the sync engines? 472 */ 473 /* package */ long createMailbox(long accountId, int mailboxType) { 474 if (accountId < 0 || mailboxType < 0) { 475 String mes = "Invalid arguments " + accountId + ' ' + mailboxType; 476 Log.e(Email.LOG_TAG, mes); 477 throw new RuntimeException(mes); 478 } 479 Mailbox box = new Mailbox(); 480 box.mAccountKey = accountId; 481 box.mType = mailboxType; 482 box.mSyncInterval = EmailContent.Account.CHECK_INTERVAL_NEVER; 483 box.mFlagVisible = true; 484 box.mDisplayName = getMailboxServerName(mailboxType); 485 box.save(mProviderContext); 486 return box.mId; 487 } 488 489 /** 490 * Send a message: 491 * - move the message to Outbox (the message is assumed to be in Drafts). 492 * - EAS service will take it from there 493 * - trigger send for POP/IMAP 494 * @param messageId the id of the message to send 495 */ 496 public void sendMessage(long messageId, long accountId) { 497 ContentResolver resolver = mProviderContext.getContentResolver(); 498 if (accountId == -1) { 499 accountId = lookupAccountForMessage(messageId); 500 } 501 if (accountId == -1) { 502 // probably the message was not found 503 if (Email.LOGD) { 504 Email.log("no account found for message " + messageId); 505 } 506 return; 507 } 508 509 // Move to Outbox 510 long outboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_OUTBOX); 511 ContentValues cv = new ContentValues(); 512 cv.put(EmailContent.MessageColumns.MAILBOX_KEY, outboxId); 513 514 // does this need to be SYNCED_CONTENT_URI instead? 515 Uri uri = ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, messageId); 516 resolver.update(uri, cv, null, null); 517 518 sendPendingMessages(accountId); 519 } 520 521 private void sendPendingMessagesSmtp(long accountId) { 522 // for IMAP & POP only, (attempt to) send the message now 523 final EmailContent.Account account = 524 EmailContent.Account.restoreAccountWithId(mProviderContext, accountId); 525 if (account == null) { 526 return; 527 } 528 final long sentboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_SENT); 529 Utility.runAsync(new Runnable() { 530 public void run() { 531 mLegacyController.sendPendingMessages(account, sentboxId, mLegacyListener); 532 } 533 }); 534 } 535 536 /** 537 * Try to send all pending messages for a given account 538 * 539 * @param accountId the account for which to send messages 540 */ 541 public void sendPendingMessages(long accountId) { 542 // 1. make sure we even have an outbox, exit early if not 543 final long outboxId = 544 Mailbox.findMailboxOfType(mProviderContext, accountId, Mailbox.TYPE_OUTBOX); 545 if (outboxId == Mailbox.NO_MAILBOX) { 546 return; 547 } 548 549 // 2. dispatch as necessary 550 IEmailService service = getServiceForAccount(accountId); 551 if (service != null) { 552 // Service implementation 553 try { 554 service.startSync(outboxId); 555 } catch (RemoteException e) { 556 // TODO Change exception handling to be consistent with however this method 557 // is implemented for other protocols 558 Log.d("updateMailbox", "RemoteException" + e); 559 } 560 } else { 561 // MessagingController implementation 562 sendPendingMessagesSmtp(accountId); 563 } 564 } 565 566 /** 567 * Reset visible limits for all accounts. 568 * For each account: 569 * look up limit 570 * write limit into all mailboxes for that account 571 */ 572 public void resetVisibleLimits() { 573 Utility.runAsync(new Runnable() { 574 public void run() { 575 ContentResolver resolver = mProviderContext.getContentResolver(); 576 Cursor c = null; 577 try { 578 c = resolver.query( 579 Account.CONTENT_URI, 580 Account.ID_PROJECTION, 581 null, null, null); 582 while (c.moveToNext()) { 583 long accountId = c.getLong(Account.ID_PROJECTION_COLUMN); 584 Account account = Account.restoreAccountWithId(mProviderContext, accountId); 585 if (account != null) { 586 Store.StoreInfo info = Store.StoreInfo.getStoreInfo( 587 account.getStoreUri(mProviderContext), mContext); 588 if (info != null && info.mVisibleLimitDefault > 0) { 589 int limit = info.mVisibleLimitDefault; 590 ContentValues cv = new ContentValues(); 591 cv.put(MailboxColumns.VISIBLE_LIMIT, limit); 592 resolver.update(Mailbox.CONTENT_URI, cv, 593 MailboxColumns.ACCOUNT_KEY + "=?", 594 new String[] { Long.toString(accountId) }); 595 } 596 } 597 } 598 } finally { 599 if (c != null) { 600 c.close(); 601 } 602 } 603 } 604 }); 605 } 606 607 /** 608 * Increase the load count for a given mailbox, and trigger a refresh. Applies only to 609 * IMAP and POP. 610 * 611 * @param mailboxId the mailbox 612 * @param callback 613 */ 614 public void loadMoreMessages(final long mailboxId) { 615 Utility.runAsync(new Runnable() { 616 public void run() { 617 Mailbox mailbox = Mailbox.restoreMailboxWithId(mProviderContext, mailboxId); 618 if (mailbox == null) { 619 return; 620 } 621 Account account = Account.restoreAccountWithId(mProviderContext, 622 mailbox.mAccountKey); 623 if (account == null) { 624 return; 625 } 626 Store.StoreInfo info = Store.StoreInfo.getStoreInfo( 627 account.getStoreUri(mProviderContext), mContext); 628 if (info != null && info.mVisibleLimitIncrement > 0) { 629 // Use provider math to increment the field 630 ContentValues cv = new ContentValues();; 631 cv.put(EmailContent.FIELD_COLUMN_NAME, MailboxColumns.VISIBLE_LIMIT); 632 cv.put(EmailContent.ADD_COLUMN_NAME, info.mVisibleLimitIncrement); 633 Uri uri = ContentUris.withAppendedId(Mailbox.ADD_TO_FIELD_URI, mailboxId); 634 mProviderContext.getContentResolver().update(uri, cv, null, null); 635 // Trigger a refresh using the new, longer limit 636 mailbox.mVisibleLimit += info.mVisibleLimitIncrement; 637 mLegacyController.synchronizeMailbox(account, mailbox, mLegacyListener); 638 } 639 } 640 }); 641 } 642 643 /** 644 * @param messageId the id of message 645 * @return the accountId corresponding to the given messageId, or -1 if not found. 646 */ 647 private long lookupAccountForMessage(long messageId) { 648 ContentResolver resolver = mProviderContext.getContentResolver(); 649 Cursor c = resolver.query(EmailContent.Message.CONTENT_URI, 650 MESSAGEID_TO_ACCOUNTID_PROJECTION, EmailContent.RECORD_ID + "=?", 651 new String[] { Long.toString(messageId) }, null); 652 try { 653 return c.moveToFirst() 654 ? c.getLong(MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID) 655 : -1; 656 } finally { 657 c.close(); 658 } 659 } 660 661 /** 662 * Delete a single attachment entry from the DB given its id. 663 * Does not delete any eventual associated files. 664 */ 665 public void deleteAttachment(long attachmentId) { 666 ContentResolver resolver = mProviderContext.getContentResolver(); 667 Uri uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, attachmentId); 668 resolver.delete(uri, null, null); 669 } 670 671 /** 672 * Delete a single message by moving it to the trash, or deleting it from the trash 673 * 674 * This function has no callback, no result reporting, because the desired outcome 675 * is reflected entirely by changes to one or more cursors. 676 * 677 * @param messageId The id of the message to "delete". 678 * @param accountId The id of the message's account, or -1 if not known by caller 679 * @return the AsyncTask used to execute the deletion 680 */ 681 public AsyncTask<Void, Void, Void> deleteMessage(final long messageId, long accountId) { 682 return Utility.runAsync(new Runnable() { 683 public void run() { 684 // 1. Get the message's account 685 Account account = Account.getAccountForMessageId(mProviderContext, messageId); 686 687 // 2. Confirm that there is a trash mailbox available. If not, create one 688 long trashMailboxId = findOrCreateMailboxOfType(account.mId, Mailbox.TYPE_TRASH); 689 690 // 3. Get the message's original mailbox 691 Mailbox mailbox = Mailbox.getMailboxForMessageId(mProviderContext, messageId); 692 693 // 4. Drop non-essential data for the message (e.g. attachment files) 694 AttachmentProvider.deleteAllAttachmentFiles(mProviderContext, account.mId, 695 messageId); 696 697 Uri uri = ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, 698 messageId); 699 ContentResolver resolver = mProviderContext.getContentResolver(); 700 701 // 5. Perform "delete" as appropriate 702 if (mailbox.mId == trashMailboxId) { 703 // 5a. Delete from trash 704 resolver.delete(uri, null, null); 705 } else { 706 // 5b. Move to trash 707 ContentValues cv = new ContentValues(); 708 cv.put(EmailContent.MessageColumns.MAILBOX_KEY, trashMailboxId); 709 resolver.update(uri, cv, null, null); 710 } 711 712 if (isMessagingController(account)) { 713 mLegacyController.processPendingActions(account.mId); 714 } 715 } 716 }); 717 } 718 719 /** 720 * Moving a message to another folder 721 * 722 * This function has no callback, no result reporting, because the desired outcome 723 * is reflected entirely by changes to one or more cursors. 724 * 725 * @param messageId The id of the message to move 726 * @param mailboxId The id of the folder we're supposed to move the folder to 727 * @return the AsyncTask that will execute the move 728 */ 729 public AsyncTask<Void, Void, Void> moveMessage(final long messageId, final long mailboxId) { 730 return Utility.runAsync(new Runnable() { 731 public void run() { 732 Account account = Account.getAccountForMessageId(mProviderContext, messageId); 733 if (account != null) { 734 Uri uri = ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, 735 messageId); 736 ContentValues cv = new ContentValues(); 737 cv.put(EmailContent.MessageColumns.MAILBOX_KEY, mailboxId); 738 mProviderContext.getContentResolver().update(uri, cv, null, null); 739 if (isMessagingController(account)) { 740 mLegacyController.processPendingActions(account.mId); 741 } 742 } 743 } 744 }); 745 } 746 747 /** 748 * Set/clear the unread status of a message 749 * 750 * TODO db ops should not be in this thread. queue it up. 751 * 752 * @param messageId the message to update 753 * @param isRead the new value for the isRead flag 754 */ 755 public void setMessageRead(final long messageId, boolean isRead) { 756 ContentValues cv = new ContentValues(); 757 cv.put(EmailContent.MessageColumns.FLAG_READ, isRead); 758 Uri uri = ContentUris.withAppendedId( 759 EmailContent.Message.SYNCED_CONTENT_URI, messageId); 760 mProviderContext.getContentResolver().update(uri, cv, null, null); 761 762 // Service runs automatically, MessagingController needs a kick 763 final Message message = Message.restoreMessageWithId(mProviderContext, messageId); 764 if (message == null) { 765 return; 766 } 767 Account account = Account.restoreAccountWithId(mProviderContext, message.mAccountKey); 768 if (account == null) { 769 return; // isMessagingController returns false for null, but let's make it clear. 770 } 771 if (isMessagingController(account)) { 772 Utility.runAsync(new Runnable() { 773 public void run() { 774 mLegacyController.processPendingActions(message.mAccountKey); 775 } 776 }); 777 } 778 } 779 780 /** 781 * Set/clear the favorite status of a message 782 * 783 * TODO db ops should not be in this thread. queue it up. 784 * 785 * @param messageId the message to update 786 * @param isFavorite the new value for the isFavorite flag 787 */ 788 public void setMessageFavorite(final long messageId, boolean isFavorite) { 789 ContentValues cv = new ContentValues(); 790 cv.put(EmailContent.MessageColumns.FLAG_FAVORITE, isFavorite); 791 Uri uri = ContentUris.withAppendedId( 792 EmailContent.Message.SYNCED_CONTENT_URI, messageId); 793 mProviderContext.getContentResolver().update(uri, cv, null, null); 794 795 // Service runs automatically, MessagingController needs a kick 796 final Message message = Message.restoreMessageWithId(mProviderContext, messageId); 797 if (message == null) { 798 return; 799 } 800 Account account = Account.restoreAccountWithId(mProviderContext, message.mAccountKey); 801 if (account == null) { 802 return; // isMessagingController returns false for null, but let's make it clear. 803 } 804 if (isMessagingController(account)) { 805 Utility.runAsync(new Runnable() { 806 public void run() { 807 mLegacyController.processPendingActions(message.mAccountKey); 808 } 809 }); 810 } 811 } 812 813 /** 814 * Respond to a meeting invitation. 815 * 816 * @param messageId the id of the invitation being responded to 817 * @param response the code representing the response to the invitation 818 */ 819 public void sendMeetingResponse(final long messageId, final int response) { 820 // Split here for target type (Service or MessagingController) 821 IEmailService service = getServiceForMessage(messageId); 822 if (service != null) { 823 // Service implementation 824 try { 825 service.sendMeetingResponse(messageId, response); 826 } catch (RemoteException e) { 827 // TODO Change exception handling to be consistent with however this method 828 // is implemented for other protocols 829 Log.e("onDownloadAttachment", "RemoteException", e); 830 } 831 } 832 } 833 834 /** 835 * Request that an attachment be loaded. It will be stored at a location controlled 836 * by the AttachmentProvider. 837 * 838 * @param attachmentId the attachment to load 839 * @param messageId the owner message 840 * @param mailboxId the owner mailbox 841 * @param accountId the owner account 842 */ 843 public void loadAttachment(final long attachmentId, final long messageId, final long mailboxId, 844 final long accountId) { 845 846 Attachment attachInfo = Attachment.restoreAttachmentWithId(mProviderContext, attachmentId); 847 if (Utility.attachmentExists(mProviderContext, attachInfo)) { 848 // The attachment has already been downloaded, so we will just "pretend" to download it 849 // This presumably is for POP3 messages 850 synchronized (mListeners) { 851 for (Result listener : mListeners) { 852 listener.loadAttachmentCallback(null, messageId, attachmentId, 0); 853 } 854 for (Result listener : mListeners) { 855 listener.loadAttachmentCallback(null, messageId, attachmentId, 100); 856 } 857 } 858 return; 859 } 860 861 // Flag the attachment as needing download at the user's request 862 ContentValues cv = new ContentValues(); 863 cv.put(Attachment.FLAGS, attachInfo.mFlags | Attachment.FLAG_DOWNLOAD_USER_REQUEST); 864 attachInfo.update(mContext, cv); 865 } 866 867 /** 868 * For a given message id, return a service proxy if applicable, or null. 869 * 870 * @param messageId the message of interest 871 * @result service proxy, or null if n/a 872 */ 873 private IEmailService getServiceForMessage(long messageId) { 874 // TODO make this more efficient, caching the account, smaller lookup here, etc. 875 Message message = Message.restoreMessageWithId(mProviderContext, messageId); 876 if (message == null) { 877 return null; 878 } 879 return getServiceForAccount(message.mAccountKey); 880 } 881 882 /** 883 * For a given account id, return a service proxy if applicable, or null. 884 * 885 * @param accountId the message of interest 886 * @result service proxy, or null if n/a 887 */ 888 private IEmailService getServiceForAccount(long accountId) { 889 if (isMessagingController(accountId)) return null; 890 return getExchangeEmailService(); 891 } 892 893 private IEmailService getExchangeEmailService() { 894 return ExchangeUtils.getExchangeEmailService(mContext, mServiceCallback); 895 } 896 897 /** 898 * Simple helper to determine if legacy MessagingController should be used 899 */ 900 public boolean isMessagingController(EmailContent.Account account) { 901 if (account == null) return false; 902 return isMessagingController(account.mId); 903 } 904 905 public boolean isMessagingController(long accountId) { 906 Boolean isLegacyController = mLegacyControllerMap.get(accountId); 907 if (isLegacyController == null) { 908 String protocol = Account.getProtocol(mProviderContext, accountId); 909 isLegacyController = ("pop3".equals(protocol) || "imap".equals(protocol)); 910 mLegacyControllerMap.put(accountId, isLegacyController); 911 } 912 return isLegacyController; 913 } 914 915 /** 916 * Delete an account. 917 */ 918 public void deleteAccount(final long accountId) { 919 Utility.runAsync(new Runnable() { 920 public void run() { 921 deleteAccountSync(accountId); 922 } 923 }); 924 } 925 926 /** 927 * Delete an account synchronously. Intended to be used only by unit tests. 928 */ 929 public void deleteAccountSync(long accountId) { 930 try { 931 mLegacyControllerMap.remove(accountId); 932 // Get the account URI. 933 final Account account = Account.restoreAccountWithId(mContext, accountId); 934 if (account == null) { 935 return; // Already deleted? 936 } 937 938 final String accountUri = account.getStoreUri(mContext); 939 // Delete Remote store at first. 940 if (!TextUtils.isEmpty(accountUri)) { 941 Store.getInstance(accountUri, mContext, null).delete(); 942 // Remove the Store instance from cache. 943 Store.removeInstance(accountUri); 944 } 945 946 Uri uri = ContentUris.withAppendedId( 947 EmailContent.Account.CONTENT_URI, accountId); 948 mContext.getContentResolver().delete(uri, null, null); 949 950 // Update the backup (side copy) of the accounts 951 AccountBackupRestore.backupAccounts(mContext); 952 953 // Release or relax device administration, if relevant 954 SecurityPolicy.getInstance(mContext).reducePolicies(); 955 956 Email.setServicesEnabled(mContext); 957 } catch (Exception e) { 958 Log.w(Email.LOG_TAG, "Exception while deleting account", e); 959 } finally { 960 synchronized (mListeners) { 961 for (Result l : mListeners) { 962 l.deleteAccountCallback(accountId); 963 } 964 } 965 } 966 } 967 968 /** 969 * Simple callback for synchronous commands. For many commands, this can be largely ignored 970 * and the result is observed via provider cursors. The callback will *not* necessarily be 971 * made from the UI thread, so you may need further handlers to safely make UI updates. 972 */ 973 public static abstract class Result { 974 /** 975 * Callback for updateMailboxList 976 * 977 * @param result If null, the operation completed without error 978 * @param accountId The account being operated on 979 * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete 980 */ 981 public void updateMailboxListCallback(MessagingException result, long accountId, 982 int progress) { 983 } 984 985 /** 986 * Callback for updateMailbox. Note: This looks a lot like checkMailCallback, but 987 * it's a separate call used only by UI's, so we can keep things separate. 988 * 989 * @param result If null, the operation completed without error 990 * @param accountId The account being operated on 991 * @param mailboxId The mailbox being operated on 992 * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete 993 * @param numNewMessages the number of new messages delivered 994 */ 995 public void updateMailboxCallback(MessagingException result, long accountId, 996 long mailboxId, int progress, int numNewMessages) { 997 } 998 999 /** 1000 * Callback for loadMessageForView 1001 * 1002 * @param result if null, the attachment completed - if non-null, terminating with failure 1003 * @param messageId the message which contains the attachment 1004 * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete 1005 */ 1006 public void loadMessageForViewCallback(MessagingException result, long messageId, 1007 int progress) { 1008 } 1009 1010 /** 1011 * Callback for loadAttachment 1012 * 1013 * @param result if null, the attachment completed - if non-null, terminating with failure 1014 * @param messageId the message which contains the attachment 1015 * @param attachmentId the attachment being loaded 1016 * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete 1017 */ 1018 public void loadAttachmentCallback(MessagingException result, long messageId, 1019 long attachmentId, int progress) { 1020 } 1021 1022 /** 1023 * Callback for checkmail. Note: This looks a lot like updateMailboxCallback, but 1024 * it's a separate call used only by the automatic checker service, so we can keep 1025 * things separate. 1026 * 1027 * @param result If null, the operation completed without error 1028 * @param accountId The account being operated on 1029 * @param mailboxId The mailbox being operated on (may be unknown at start) 1030 * @param progress 0 for "starting", no updates, 100 for complete 1031 * @param tag the same tag that was passed to serviceCheckMail() 1032 */ 1033 public void serviceCheckMailCallback(MessagingException result, long accountId, 1034 long mailboxId, int progress, long tag) { 1035 } 1036 1037 /** 1038 * Callback for sending pending messages. This will be called once to start the 1039 * group, multiple times for messages, and once to complete the group. 1040 * 1041 * @param result If null, the operation completed without error 1042 * @param accountId The account being operated on 1043 * @param messageId The being sent (may be unknown at start) 1044 * @param progress 0 for "starting", 100 for complete 1045 */ 1046 public void sendMailCallback(MessagingException result, long accountId, 1047 long messageId, int progress) { 1048 } 1049 1050 /** 1051 * Callback from {@link Controller#deleteAccount}. 1052 */ 1053 public void deleteAccountCallback(long accountId) { 1054 } 1055 } 1056 1057 /** 1058 * Support for receiving callbacks from MessagingController and dealing with UI going 1059 * out of scope. 1060 */ 1061 public class LegacyListener extends MessagingListener implements MessageRetrievalListener { 1062 public LegacyListener(long messageId, long attachmentId) { 1063 super(messageId, attachmentId); 1064 } 1065 1066 public LegacyListener() { 1067 } 1068 1069 @Override 1070 public void listFoldersStarted(long accountId) { 1071 synchronized (mListeners) { 1072 for (Result l : mListeners) { 1073 l.updateMailboxListCallback(null, accountId, 0); 1074 } 1075 } 1076 } 1077 1078 @Override 1079 public void listFoldersFailed(long accountId, String message) { 1080 synchronized (mListeners) { 1081 for (Result l : mListeners) { 1082 l.updateMailboxListCallback(new MessagingException(message), accountId, 0); 1083 } 1084 } 1085 } 1086 1087 @Override 1088 public void listFoldersFinished(long accountId) { 1089 synchronized (mListeners) { 1090 for (Result l : mListeners) { 1091 l.updateMailboxListCallback(null, accountId, 100); 1092 } 1093 } 1094 } 1095 1096 @Override 1097 public void synchronizeMailboxStarted(long accountId, long mailboxId) { 1098 synchronized (mListeners) { 1099 for (Result l : mListeners) { 1100 l.updateMailboxCallback(null, accountId, mailboxId, 0, 0); 1101 } 1102 } 1103 } 1104 1105 @Override 1106 public void synchronizeMailboxFinished(long accountId, long mailboxId, 1107 int totalMessagesInMailbox, int numNewMessages) { 1108 synchronized (mListeners) { 1109 for (Result l : mListeners) { 1110 l.updateMailboxCallback(null, accountId, mailboxId, 100, numNewMessages); 1111 } 1112 } 1113 } 1114 1115 @Override 1116 public void synchronizeMailboxFailed(long accountId, long mailboxId, Exception e) { 1117 MessagingException me; 1118 if (e instanceof MessagingException) { 1119 me = (MessagingException) e; 1120 } else { 1121 me = new MessagingException(e.toString()); 1122 } 1123 synchronized (mListeners) { 1124 for (Result l : mListeners) { 1125 l.updateMailboxCallback(me, accountId, mailboxId, 0, 0); 1126 } 1127 } 1128 } 1129 1130 @Override 1131 public void checkMailStarted(Context context, long accountId, long tag) { 1132 synchronized (mListeners) { 1133 for (Result l : mListeners) { 1134 l.serviceCheckMailCallback(null, accountId, -1, 0, tag); 1135 } 1136 } 1137 } 1138 1139 @Override 1140 public void checkMailFinished(Context context, long accountId, long folderId, long tag) { 1141 synchronized (mListeners) { 1142 for (Result l : mListeners) { 1143 l.serviceCheckMailCallback(null, accountId, folderId, 100, tag); 1144 } 1145 } 1146 } 1147 1148 @Override 1149 public void loadMessageForViewStarted(long messageId) { 1150 synchronized (mListeners) { 1151 for (Result listener : mListeners) { 1152 listener.loadMessageForViewCallback(null, messageId, 0); 1153 } 1154 } 1155 } 1156 1157 @Override 1158 public void loadMessageForViewFinished(long messageId) { 1159 synchronized (mListeners) { 1160 for (Result listener : mListeners) { 1161 listener.loadMessageForViewCallback(null, messageId, 100); 1162 } 1163 } 1164 } 1165 1166 @Override 1167 public void loadMessageForViewFailed(long messageId, String message) { 1168 synchronized (mListeners) { 1169 for (Result listener : mListeners) { 1170 listener.loadMessageForViewCallback(new MessagingException(message), 1171 messageId, 0); 1172 } 1173 } 1174 } 1175 1176 @Override 1177 public void loadAttachmentStarted(long accountId, long messageId, long attachmentId, 1178 boolean requiresDownload) { 1179 try { 1180 mCallbackProxy.loadAttachmentStatus(messageId, attachmentId, 1181 EmailServiceStatus.IN_PROGRESS, 0); 1182 } catch (RemoteException e) { 1183 } 1184 synchronized (mListeners) { 1185 for (Result listener : mListeners) { 1186 listener.loadAttachmentCallback(null, messageId, attachmentId, 0); 1187 } 1188 } 1189 } 1190 1191 @Override 1192 public void loadAttachmentFinished(long accountId, long messageId, long attachmentId) { 1193 try { 1194 mCallbackProxy.loadAttachmentStatus(messageId, attachmentId, 1195 EmailServiceStatus.SUCCESS, 100); 1196 } catch (RemoteException e) { 1197 } 1198 synchronized (mListeners) { 1199 for (Result listener : mListeners) { 1200 listener.loadAttachmentCallback(null, messageId, attachmentId, 100); 1201 } 1202 } 1203 } 1204 1205 @Override 1206 public void loadAttachmentProgress(int progress) { 1207 synchronized (mListeners) { 1208 for (Result listener : mListeners) { 1209 listener.loadAttachmentCallback(null, messageId, attachmentId, progress); 1210 } 1211 } 1212 } 1213 1214 @Override 1215 public void loadAttachmentFailed(long accountId, long messageId, long attachmentId, 1216 MessagingException me) { 1217 try { 1218 // If the cause of the MessagingException is an IOException, we send a status of 1219 // CONNECTION_ERROR; in this case, AttachmentDownloadService will try again to 1220 // download the attachment. Otherwise, the error is considered non-recoverable. 1221 int status = EmailServiceStatus.ATTACHMENT_NOT_FOUND; 1222 if (me.getCause() instanceof IOException) { 1223 status = EmailServiceStatus.CONNECTION_ERROR; 1224 } 1225 mCallbackProxy.loadAttachmentStatus(messageId, attachmentId, status, 0); 1226 } catch (RemoteException e) { 1227 } 1228 synchronized (mListeners) { 1229 for (Result listener : mListeners) { 1230 listener.loadAttachmentCallback(me, messageId, attachmentId, 0); 1231 } 1232 } 1233 } 1234 1235 @Override 1236 synchronized public void sendPendingMessagesStarted(long accountId, long messageId) { 1237 synchronized (mListeners) { 1238 for (Result listener : mListeners) { 1239 listener.sendMailCallback(null, accountId, messageId, 0); 1240 } 1241 } 1242 } 1243 1244 @Override 1245 synchronized public void sendPendingMessagesCompleted(long accountId) { 1246 synchronized (mListeners) { 1247 for (Result listener : mListeners) { 1248 listener.sendMailCallback(null, accountId, -1, 100); 1249 } 1250 } 1251 } 1252 1253 @Override 1254 synchronized public void sendPendingMessagesFailed(long accountId, long messageId, 1255 Exception reason) { 1256 MessagingException me; 1257 if (reason instanceof MessagingException) { 1258 me = (MessagingException) reason; 1259 } else { 1260 me = new MessagingException(reason.toString()); 1261 } 1262 synchronized (mListeners) { 1263 for (Result listener : mListeners) { 1264 listener.sendMailCallback(me, accountId, messageId, 0); 1265 } 1266 } 1267 } 1268 1269 @Override 1270 public void messageRetrieved(com.android.email.mail.Message message) { 1271 } 1272 } 1273 1274 /** 1275 * Service callback for service operations 1276 */ 1277 private class ServiceCallback extends IEmailServiceCallback.Stub { 1278 1279 private final static boolean DEBUG_FAIL_DOWNLOADS = false; // do not check in "true" 1280 1281 public void loadAttachmentStatus(long messageId, long attachmentId, int statusCode, 1282 int progress) { 1283 MessagingException result = mapStatusToException(statusCode); 1284 switch (statusCode) { 1285 case EmailServiceStatus.SUCCESS: 1286 progress = 100; 1287 break; 1288 case EmailServiceStatus.IN_PROGRESS: 1289 if (DEBUG_FAIL_DOWNLOADS && progress > 75) { 1290 result = new MessagingException( 1291 String.valueOf(EmailServiceStatus.CONNECTION_ERROR)); 1292 } 1293 // discard progress reports that look like sentinels 1294 if (progress < 0 || progress >= 100) { 1295 return; 1296 } 1297 break; 1298 } 1299 synchronized (mListeners) { 1300 for (Result listener : mListeners) { 1301 listener.loadAttachmentCallback(result, messageId, attachmentId, progress); 1302 } 1303 } 1304 } 1305 1306 /** 1307 * Note, this is an incomplete implementation of this callback, because we are 1308 * not getting things back from Service in quite the same way as from MessagingController. 1309 * However, this is sufficient for basic "progress=100" notification that message send 1310 * has just completed. 1311 */ 1312 public void sendMessageStatus(long messageId, String subject, int statusCode, 1313 int progress) { 1314 long accountId = -1; // This should be in the callback 1315 MessagingException result = mapStatusToException(statusCode); 1316 switch (statusCode) { 1317 case EmailServiceStatus.SUCCESS: 1318 progress = 100; 1319 break; 1320 case EmailServiceStatus.IN_PROGRESS: 1321 // discard progress reports that look like sentinels 1322 if (progress < 0 || progress >= 100) { 1323 return; 1324 } 1325 break; 1326 } 1327 synchronized(mListeners) { 1328 for (Result listener : mListeners) { 1329 listener.sendMailCallback(result, accountId, messageId, progress); 1330 } 1331 } 1332 } 1333 1334 public void syncMailboxListStatus(long accountId, int statusCode, int progress) { 1335 MessagingException result = mapStatusToException(statusCode); 1336 switch (statusCode) { 1337 case EmailServiceStatus.SUCCESS: 1338 progress = 100; 1339 break; 1340 case EmailServiceStatus.IN_PROGRESS: 1341 // discard progress reports that look like sentinels 1342 if (progress < 0 || progress >= 100) { 1343 return; 1344 } 1345 break; 1346 } 1347 synchronized(mListeners) { 1348 for (Result listener : mListeners) { 1349 listener.updateMailboxListCallback(result, accountId, progress); 1350 } 1351 } 1352 } 1353 1354 public void syncMailboxStatus(long mailboxId, int statusCode, int progress) { 1355 MessagingException result = mapStatusToException(statusCode); 1356 switch (statusCode) { 1357 case EmailServiceStatus.SUCCESS: 1358 progress = 100; 1359 break; 1360 case EmailServiceStatus.IN_PROGRESS: 1361 // discard progress reports that look like sentinels 1362 if (progress < 0 || progress >= 100) { 1363 return; 1364 } 1365 break; 1366 } 1367 // TODO should pass this back instead of looking it up here 1368 Mailbox mbx = Mailbox.restoreMailboxWithId(mProviderContext, mailboxId); 1369 // The mailbox could have disappeared if the server commanded it 1370 if (mbx == null) return; 1371 long accountId = mbx.mAccountKey; 1372 synchronized(mListeners) { 1373 for (Result listener : mListeners) { 1374 listener.updateMailboxCallback(result, accountId, mailboxId, progress, 0); 1375 } 1376 } 1377 } 1378 1379 private MessagingException mapStatusToException(int statusCode) { 1380 switch (statusCode) { 1381 case EmailServiceStatus.SUCCESS: 1382 case EmailServiceStatus.IN_PROGRESS: 1383 return null; 1384 1385 case EmailServiceStatus.LOGIN_FAILED: 1386 return new AuthenticationFailedException(""); 1387 1388 case EmailServiceStatus.CONNECTION_ERROR: 1389 return new MessagingException(MessagingException.IOERROR); 1390 1391 case EmailServiceStatus.SECURITY_FAILURE: 1392 return new MessagingException(MessagingException.SECURITY_POLICIES_REQUIRED); 1393 1394 case EmailServiceStatus.MESSAGE_NOT_FOUND: 1395 case EmailServiceStatus.ATTACHMENT_NOT_FOUND: 1396 case EmailServiceStatus.FOLDER_NOT_DELETED: 1397 case EmailServiceStatus.FOLDER_NOT_RENAMED: 1398 case EmailServiceStatus.FOLDER_NOT_CREATED: 1399 case EmailServiceStatus.REMOTE_EXCEPTION: 1400 // TODO: define exception code(s) & UI string(s) for server-side errors 1401 default: 1402 return new MessagingException(String.valueOf(statusCode)); 1403 } 1404 } 1405 } 1406 1407 private interface ServiceCallbackWrapper { 1408 public void call(IEmailServiceCallback cb) throws RemoteException; 1409 } 1410 1411 /** 1412 * Proxy that can be used to broadcast service callbacks; we currently use this only for 1413 * loadAttachment callbacks 1414 */ 1415 private final IEmailServiceCallback.Stub mCallbackProxy = 1416 new IEmailServiceCallback.Stub() { 1417 1418 /** 1419 * Broadcast a callback to the everyone that's registered 1420 * 1421 * @param wrapper the ServiceCallbackWrapper used in the broadcast 1422 */ 1423 private synchronized void broadcastCallback(ServiceCallbackWrapper wrapper) { 1424 if (sCallbackList != null) { 1425 // Call everyone on our callback list 1426 // Exceptions can be safely ignored 1427 int count = sCallbackList.beginBroadcast(); 1428 for (int i = 0; i < count; i++) { 1429 try { 1430 wrapper.call(sCallbackList.getBroadcastItem(i)); 1431 } catch (RemoteException e) { 1432 } 1433 } 1434 sCallbackList.finishBroadcast(); 1435 } 1436 } 1437 1438 public void loadAttachmentStatus(final long messageId, final long attachmentId, 1439 final int status, final int progress) { 1440 broadcastCallback(new ServiceCallbackWrapper() { 1441 @Override 1442 public void call(IEmailServiceCallback cb) throws RemoteException { 1443 cb.loadAttachmentStatus(messageId, attachmentId, status, progress); 1444 } 1445 }); 1446 } 1447 1448 @Override 1449 public void sendMessageStatus(long messageId, String subject, int statusCode, int progress) 1450 throws RemoteException { 1451 } 1452 1453 @Override 1454 public void syncMailboxListStatus(long accountId, int statusCode, int progress) 1455 throws RemoteException { 1456 } 1457 1458 @Override 1459 public void syncMailboxStatus(long mailboxId, int statusCode, int progress) 1460 throws RemoteException { 1461 } 1462 }; 1463 1464 public static class ControllerService extends Service { 1465 /** 1466 * Create our EmailService implementation here. For now, only loadAttachment is supported; the 1467 * intention, however, is to move more functionality to the service interface 1468 */ 1469 private final IEmailService.Stub mBinder = new IEmailService.Stub() { 1470 1471 public Bundle validate(String protocol, String host, String userName, String password, 1472 int port, boolean ssl, boolean trustCertificates) throws RemoteException { 1473 return null; 1474 } 1475 1476 public Bundle autoDiscover(String userName, String password) throws RemoteException { 1477 return null; 1478 } 1479 1480 public void startSync(long mailboxId) throws RemoteException { 1481 } 1482 1483 public void stopSync(long mailboxId) throws RemoteException { 1484 } 1485 1486 public void loadAttachment(long attachmentId, String destinationFile, 1487 String contentUriString) throws RemoteException { 1488 if (Email.DEBUG) { 1489 Log.d(TAG, "loadAttachment: " + attachmentId + " to " + destinationFile); 1490 } 1491 Attachment att = Attachment.restoreAttachmentWithId(ControllerService.this, 1492 attachmentId); 1493 if (att != null) { 1494 Message msg = Message.restoreMessageWithId(ControllerService.this, 1495 att.mMessageKey); 1496 if (msg != null) { 1497 // If the message is a forward and the attachment needs downloading, we need 1498 // to retrieve the message from the source, rather than from the message 1499 // itself 1500 if ((msg.mFlags & Message.FLAG_TYPE_FORWARD) != 0) { 1501 String[] cols = Utility.getRowColumns(ControllerService.this, 1502 Body.CONTENT_URI, BODY_SOURCE_KEY_PROJECTION, WHERE_MESSAGE_KEY, 1503 new String[] {Long.toString(msg.mId)}); 1504 if (cols != null) { 1505 msg = Message.restoreMessageWithId(ControllerService.this, 1506 Long.parseLong(cols[BODY_SOURCE_KEY_COLUMN])); 1507 if (msg == null) { 1508 // TODO: We can try restoring from the deleted table at this point... 1509 return; 1510 } 1511 } 1512 } 1513 MessagingController legacyController = sInstance.mLegacyController; 1514 LegacyListener legacyListener = sInstance.mLegacyListener; 1515 legacyController.loadAttachment(msg.mAccountKey, msg.mId, msg.mMailboxKey, 1516 attachmentId, legacyListener); 1517 } 1518 } 1519 } 1520 1521 public void updateFolderList(long accountId) throws RemoteException { 1522 } 1523 1524 public void hostChanged(long accountId) throws RemoteException { 1525 } 1526 1527 public void setLogging(int on) throws RemoteException { 1528 } 1529 1530 public void sendMeetingResponse(long messageId, int response) throws RemoteException { 1531 } 1532 1533 public void loadMore(long messageId) throws RemoteException { 1534 } 1535 1536 // The following three methods are not implemented in this version 1537 public boolean createFolder(long accountId, String name) throws RemoteException { 1538 return false; 1539 } 1540 1541 public boolean deleteFolder(long accountId, String name) throws RemoteException { 1542 return false; 1543 } 1544 1545 public boolean renameFolder(long accountId, String oldName, String newName) 1546 throws RemoteException { 1547 return false; 1548 } 1549 1550 public void setCallback(IEmailServiceCallback cb) throws RemoteException { 1551 sCallbackList.register(cb); 1552 } 1553 1554 public void moveMessage(long messageId, long mailboxId) throws RemoteException { 1555 } 1556 }; 1557 1558 @Override 1559 public IBinder onBind(Intent intent) { 1560 return mBinder; 1561 } 1562 } 1563} 1564