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