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