Controller.java revision e1a6088ee4e3f0e4344dd9bc38029b6d01431eab
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 android.content.ContentResolver; 20import android.content.ContentUris; 21import android.content.ContentValues; 22import android.content.Context; 23import android.database.Cursor; 24import android.net.Uri; 25import android.os.RemoteCallbackList; 26import android.os.RemoteException; 27import android.util.Log; 28 29import com.android.email.mail.store.Pop3Store.Pop3Message; 30import com.android.email.provider.AccountBackupRestore; 31import com.android.email.provider.Utilities; 32import com.android.email.service.EmailServiceUtils; 33import com.android.emailcommon.Logging; 34import com.android.emailcommon.mail.AuthenticationFailedException; 35import com.android.emailcommon.mail.MessagingException; 36import com.android.emailcommon.provider.Account; 37import com.android.emailcommon.provider.EmailContent; 38import com.android.emailcommon.provider.EmailContent.Attachment; 39import com.android.emailcommon.provider.EmailContent.MailboxColumns; 40import com.android.emailcommon.provider.EmailContent.Message; 41import com.android.emailcommon.provider.EmailContent.MessageColumns; 42import com.android.emailcommon.provider.HostAuth; 43import com.android.emailcommon.provider.Mailbox; 44import com.android.emailcommon.service.EmailServiceProxy; 45import com.android.emailcommon.service.EmailServiceStatus; 46import com.android.emailcommon.service.IEmailService; 47import com.android.emailcommon.service.IEmailServiceCallback; 48import com.android.emailcommon.service.SearchParams; 49import com.android.emailcommon.utility.AttachmentUtilities; 50import com.android.emailcommon.utility.EmailAsyncTask; 51import com.android.emailcommon.utility.Utility; 52import com.google.common.annotations.VisibleForTesting; 53 54import java.io.FileNotFoundException; 55import java.io.IOException; 56import java.io.InputStream; 57import java.util.ArrayList; 58import java.util.Collection; 59import java.util.HashMap; 60import java.util.HashSet; 61import java.util.concurrent.ConcurrentHashMap; 62 63/** 64 * New central controller/dispatcher for Email activities that may require remote operations. 65 * Handles disambiguating between legacy MessagingController operations and newer provider/sync 66 * based code. We implement Service to allow loadAttachment calls to be sent in a consistent manner 67 * to IMAP, POP3, and EAS by AttachmentDownloadService 68 */ 69public class Controller { 70 private static Controller sInstance; 71 private final Context mContext; 72 private Context mProviderContext; 73 private final ServiceCallback mServiceCallback = new ServiceCallback(); 74 private final HashSet<Result> mListeners = new HashSet<Result>(); 75 /*package*/ final ConcurrentHashMap<Long, Boolean> mLegacyControllerMap = 76 new ConcurrentHashMap<Long, Boolean>(); 77 78 // Note that 0 is a syntactically valid account key; however there can never be an account 79 // with id = 0, so attempts to restore the account will return null. Null values are 80 // handled properly within the code, so this won't cause any issues. 81 private static final long GLOBAL_MAILBOX_ACCOUNT_KEY = 0; 82 /*package*/ static final String ATTACHMENT_MAILBOX_SERVER_ID = "__attachment_mailbox__"; 83 /*package*/ static final String ATTACHMENT_MESSAGE_UID_PREFIX = "__attachment_message__"; 84 /*package*/ static final String SEARCH_MAILBOX_SERVER_ID = "__search_mailbox__"; 85 private static final String WHERE_TYPE_ATTACHMENT = 86 MailboxColumns.TYPE + "=" + Mailbox.TYPE_ATTACHMENT; 87 private static final String WHERE_MAILBOX_KEY = MessageColumns.MAILBOX_KEY + "=?"; 88 89 private static final String[] MESSAGEID_TO_ACCOUNTID_PROJECTION = new String[] { 90 EmailContent.RECORD_ID, 91 EmailContent.MessageColumns.ACCOUNT_KEY 92 }; 93 private static final int MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID = 1; 94 95 private static final String MAILBOXES_FOR_ACCOUNT_SELECTION = MailboxColumns.ACCOUNT_KEY + "=?"; 96 private static final String MAILBOXES_FOR_ACCOUNT_EXCEPT_ACCOUNT_MAILBOX_SELECTION = 97 MAILBOXES_FOR_ACCOUNT_SELECTION + " AND " + MailboxColumns.TYPE + "!=" + 98 Mailbox.TYPE_EAS_ACCOUNT_MAILBOX; 99 private static final String MESSAGES_FOR_ACCOUNT_SELECTION = MessageColumns.ACCOUNT_KEY + "=?"; 100 101 // Service callbacks as set up via setCallback 102 private static RemoteCallbackList<IEmailServiceCallback> sCallbackList = 103 new RemoteCallbackList<IEmailServiceCallback>(); 104 105 private volatile boolean mInUnitTests = false; 106 107 protected Controller(Context _context) { 108 mContext = _context.getApplicationContext(); 109 mProviderContext = _context; 110 } 111 112 /** 113 * Mark this controller as being in use in a unit test. 114 * This is a kludge vs having proper mocks and dependency injection; since the Controller is a 115 * global singleton there isn't much else we can do. 116 */ 117 public void markForTest(boolean inUnitTests) { 118 mInUnitTests = inUnitTests; 119 } 120 121 /** 122 * Gets or creates the singleton instance of Controller. 123 */ 124 public synchronized static Controller getInstance(Context _context) { 125 if (sInstance == null) { 126 sInstance = new Controller(_context); 127 } 128 return sInstance; 129 } 130 131 /** 132 * Inject a mock controller. Used only for testing. Affects future calls to getInstance(). 133 * 134 * Tests that use this method MUST clean it up by calling this method again with null. 135 */ 136 public synchronized static void injectMockControllerForTest(Controller mockController) { 137 sInstance = mockController; 138 } 139 140 /** 141 * For testing only: Inject a different context for provider access. This will be 142 * used internally for access the underlying provider (e.g. getContentResolver().query()). 143 * @param providerContext the provider context to be used by this instance 144 */ 145 public void setProviderContext(Context providerContext) { 146 mProviderContext = providerContext; 147 } 148 149 /** 150 * Any UI code that wishes for callback results (on async ops) should register their callback 151 * here (typically from onResume()). Unregistered callbacks will never be called, to prevent 152 * problems when the command completes and the activity has already paused or finished. 153 * @param listener The callback that may be used in action methods 154 */ 155 public void addResultCallback(Result listener) { 156 synchronized (mListeners) { 157 listener.setRegistered(true); 158 mListeners.add(listener); 159 } 160 } 161 162 /** 163 * Any UI code that no longer wishes for callback results (on async ops) should unregister 164 * their callback here (typically from onPause()). Unregistered callbacks will never be called, 165 * to prevent problems when the command completes and the activity has already paused or 166 * finished. 167 * @param listener The callback that may no longer be used 168 */ 169 public void removeResultCallback(Result listener) { 170 synchronized (mListeners) { 171 listener.setRegistered(false); 172 mListeners.remove(listener); 173 } 174 } 175 176 public Collection<Result> getResultCallbacksForTest() { 177 return mListeners; 178 } 179 180 /** 181 * Delete all Messages that live in the attachment mailbox 182 */ 183 public void deleteAttachmentMessages() { 184 // Note: There should only be one attachment mailbox at present 185 ContentResolver resolver = mProviderContext.getContentResolver(); 186 Cursor c = null; 187 try { 188 c = resolver.query(Mailbox.CONTENT_URI, EmailContent.ID_PROJECTION, 189 WHERE_TYPE_ATTACHMENT, null, null); 190 while (c.moveToNext()) { 191 long mailboxId = c.getLong(EmailContent.ID_PROJECTION_COLUMN); 192 // Must delete attachments BEFORE messages 193 AttachmentUtilities.deleteAllMailboxAttachmentFiles(mProviderContext, 0, 194 mailboxId); 195 resolver.delete(Message.CONTENT_URI, WHERE_MAILBOX_KEY, 196 new String[] {Long.toString(mailboxId)}); 197 } 198 } finally { 199 if (c != null) { 200 c.close(); 201 } 202 } 203 } 204 205 /** 206 * Get a mailbox based on a sqlite WHERE clause 207 */ 208 private Mailbox getGlobalMailboxWhere(String where) { 209 Cursor c = mProviderContext.getContentResolver().query(Mailbox.CONTENT_URI, 210 Mailbox.CONTENT_PROJECTION, where, null, null); 211 try { 212 if (c.moveToFirst()) { 213 Mailbox m = new Mailbox(); 214 m.restore(c); 215 return m; 216 } 217 } finally { 218 c.close(); 219 } 220 return null; 221 } 222 223 /** 224 * Returns the attachment mailbox (where we store eml attachment Emails), creating one 225 * if necessary 226 * @return the global attachment mailbox 227 */ 228 public Mailbox getAttachmentMailbox() { 229 Mailbox m = getGlobalMailboxWhere(WHERE_TYPE_ATTACHMENT); 230 if (m == null) { 231 m = new Mailbox(); 232 m.mAccountKey = GLOBAL_MAILBOX_ACCOUNT_KEY; 233 m.mServerId = ATTACHMENT_MAILBOX_SERVER_ID; 234 m.mFlagVisible = false; 235 m.mDisplayName = ATTACHMENT_MAILBOX_SERVER_ID; 236 m.mSyncInterval = Mailbox.CHECK_INTERVAL_NEVER; 237 m.mType = Mailbox.TYPE_ATTACHMENT; 238 m.save(mProviderContext); 239 } 240 return m; 241 } 242 243 /** 244 * Returns the search mailbox for the specified account, creating one if necessary 245 * @return the search mailbox for the passed in account 246 */ 247 public Mailbox getSearchMailbox(long accountId) { 248 Mailbox m = Mailbox.restoreMailboxOfType(mContext, accountId, Mailbox.TYPE_SEARCH); 249 if (m == null) { 250 m = new Mailbox(); 251 m.mAccountKey = accountId; 252 m.mServerId = SEARCH_MAILBOX_SERVER_ID; 253 m.mFlagVisible = false; 254 m.mDisplayName = SEARCH_MAILBOX_SERVER_ID; 255 m.mSyncInterval = Mailbox.CHECK_INTERVAL_NEVER; 256 m.mType = Mailbox.TYPE_SEARCH; 257 m.mFlags = Mailbox.FLAG_HOLDS_MAIL; 258 m.mParentKey = Mailbox.NO_MAILBOX; 259 m.save(mProviderContext); 260 } 261 return m; 262 } 263 264 /** 265 * Create a Message from the Uri and store it in the attachment mailbox 266 * @param uri the uri containing message content 267 * @return the Message or null 268 */ 269 public Message loadMessageFromUri(Uri uri) { 270 Mailbox mailbox = getAttachmentMailbox(); 271 if (mailbox == null) return null; 272 try { 273 InputStream is = mProviderContext.getContentResolver().openInputStream(uri); 274 try { 275 // First, create a Pop3Message from the attachment and then parse it 276 Pop3Message pop3Message = new Pop3Message( 277 ATTACHMENT_MESSAGE_UID_PREFIX + System.currentTimeMillis(), null); 278 pop3Message.parse(is); 279 // Now, pull out the header fields 280 Message msg = new Message(); 281 LegacyConversions.updateMessageFields(msg, pop3Message, 0, mailbox.mId); 282 // Commit the message to the local store 283 msg.save(mProviderContext); 284 // Setup the rest of the message and mark it completely loaded 285 Utilities.copyOneMessageToProvider(mProviderContext, pop3Message, msg, 286 Message.FLAG_LOADED_COMPLETE); 287 // Restore the complete message and return it 288 return Message.restoreMessageWithId(mProviderContext, msg.mId); 289 } catch (MessagingException e) { 290 } catch (IOException e) { 291 } 292 } catch (FileNotFoundException e) { 293 } 294 return null; 295 } 296 297 /** 298 * Set logging flags for external sync services 299 * 300 * Generally this should be called by anybody who changes Email.DEBUG 301 */ 302 public void serviceLogging(int debugFlags) { 303 IEmailService service = EmailServiceUtils.getExchangeService(mContext, mServiceCallback); 304 try { 305 service.setLogging(debugFlags); 306 } catch (RemoteException e) { 307 // TODO Change exception handling to be consistent with however this method 308 // is implemented for other protocols 309 Log.d("setLogging", "RemoteException" + e); 310 } 311 } 312 313 /** 314 * Request a remote update of mailboxes for an account. 315 */ 316 @SuppressWarnings("deprecation") 317 public void updateMailboxList(final long accountId) { 318 if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) return; 319 Utility.runAsync(new Runnable() { 320 @Override 321 public void run() { 322 final IEmailService service = getServiceForAccount(accountId); 323 if (service != null) { 324 // Service implementation 325 try { 326 service.updateFolderList(accountId); 327 } catch (RemoteException e) { 328 // TODO Change exception handling to be consistent with however this method 329 // is implemented for other protocols 330 Log.d("updateMailboxList", "RemoteException" + e); 331 } 332 } else { 333 throw new IllegalStateException("No service for updateMailboxList?"); 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, boolean userRequest) { 347 348 IEmailService service = getServiceForAccount(accountId); 349 if (service != null) { 350 try { 351 service.startSync(mailboxId, userRequest); 352 } catch (RemoteException e) { 353 // TODO Change exception handling to be consistent with however this method 354 // is implemented for other protocols 355 Log.d("updateMailbox", "RemoteException" + e); 356 } 357 } else { 358 throw new IllegalStateException("No service for loadMessageForView?"); 359 } 360 } 361 362 /** 363 * Request that any final work necessary be done, to load a message. 364 * 365 * Note, this assumes that the caller has already checked message.mFlagLoaded and that 366 * additional work is needed. There is no optimization here for a message which is already 367 * loaded. 368 * 369 * @param messageId the message to load 370 * @param callback the Controller callback by which results will be reported 371 */ 372 public void loadMessageForView(final long messageId) { 373 374 // Split here for target type (Service or MessagingController) 375 EmailServiceProxy service = getServiceForMessage(messageId); 376 if (service.isRemote()) { 377 // There is no service implementation, so we'll just jam the value, log the error, 378 // and get out of here. 379 Uri uri = ContentUris.withAppendedId(Message.CONTENT_URI, messageId); 380 ContentValues cv = new ContentValues(); 381 cv.put(MessageColumns.FLAG_LOADED, Message.FLAG_LOADED_COMPLETE); 382 mProviderContext.getContentResolver().update(uri, cv, null, null); 383 Log.d(Logging.LOG_TAG, "Unexpected loadMessageForView() for remote service message."); 384 final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId); 385 synchronized (mListeners) { 386 for (Result listener : mListeners) { 387 listener.loadMessageForViewCallback(null, accountId, messageId, 100); 388 } 389 } 390 } else { 391 try { 392 service.loadMore(messageId); 393 } catch (RemoteException e) { 394 } 395 } 396 } 397 398 399 /** 400 * Saves the message to a mailbox of given type. 401 * This is a synchronous operation taking place in the same thread as the caller. 402 * Upon return the message.mId is set. 403 * @param message the message (must have the mAccountId set). 404 * @param mailboxType the mailbox type (e.g. Mailbox.TYPE_DRAFTS). 405 */ 406 public void saveToMailbox(final EmailContent.Message message, final int mailboxType) { 407 long accountId = message.mAccountKey; 408 long mailboxId = findOrCreateMailboxOfType(accountId, mailboxType); 409 message.mMailboxKey = mailboxId; 410 message.save(mProviderContext); 411 } 412 413 /** 414 * Look for a specific system mailbox, creating it if necessary, and return the mailbox id. 415 * This is a blocking operation and should not be called from the UI thread. 416 * 417 * Synchronized so multiple threads can call it (and not risk creating duplicate boxes). 418 * 419 * @param accountId the account id 420 * @param mailboxType the mailbox type (e.g. EmailContent.Mailbox.TYPE_TRASH) 421 * @return the id of the mailbox. The mailbox is created if not existing. 422 * Returns Mailbox.NO_MAILBOX if the accountId or mailboxType are negative. 423 * Does not validate the input in other ways (e.g. does not verify the existence of account). 424 */ 425 public synchronized long findOrCreateMailboxOfType(long accountId, int mailboxType) { 426 if (accountId < 0 || mailboxType < 0) { 427 return Mailbox.NO_MAILBOX; 428 } 429 long mailboxId = 430 Mailbox.findMailboxOfType(mProviderContext, accountId, mailboxType); 431 return mailboxId == Mailbox.NO_MAILBOX ? createMailbox(accountId, mailboxType) : mailboxId; 432 } 433 434 /** 435 * Returns the server-side name for a specific mailbox. 436 * 437 * @return the resource string corresponding to the mailbox type, empty if not found. 438 */ 439 public static String getMailboxServerName(Context context, int mailboxType) { 440 int resId = -1; 441 switch (mailboxType) { 442 case Mailbox.TYPE_INBOX: 443 resId = R.string.mailbox_name_server_inbox; 444 break; 445 case Mailbox.TYPE_OUTBOX: 446 resId = R.string.mailbox_name_server_outbox; 447 break; 448 case Mailbox.TYPE_DRAFTS: 449 resId = R.string.mailbox_name_server_drafts; 450 break; 451 case Mailbox.TYPE_TRASH: 452 resId = R.string.mailbox_name_server_trash; 453 break; 454 case Mailbox.TYPE_SENT: 455 resId = R.string.mailbox_name_server_sent; 456 break; 457 case Mailbox.TYPE_JUNK: 458 resId = R.string.mailbox_name_server_junk; 459 break; 460 } 461 return resId != -1 ? context.getString(resId) : ""; 462 } 463 464 /** 465 * Create a mailbox given the account and mailboxType. 466 * TODO: Does this need to be signaled explicitly to the sync engines? 467 */ 468 @VisibleForTesting 469 long createMailbox(long accountId, int mailboxType) { 470 if (accountId < 0 || mailboxType < 0) { 471 String mes = "Invalid arguments " + accountId + ' ' + mailboxType; 472 Log.e(Logging.LOG_TAG, mes); 473 throw new RuntimeException(mes); 474 } 475 Mailbox box = Mailbox.newSystemMailbox( 476 accountId, mailboxType, getMailboxServerName(mContext, mailboxType)); 477 box.save(mProviderContext); 478 return box.mId; 479 } 480 481 /** 482 * Send a message: 483 * - move the message to Outbox (the message is assumed to be in Drafts). 484 * - EAS service will take it from there 485 * - mark reply/forward state in source message (if any) 486 * - trigger send for POP/IMAP 487 * @param message the fully populated Message (usually retrieved from the Draft box). Note that 488 * all transient fields (e.g. Body related fields) are also expected to be fully loaded 489 */ 490 public void sendMessage(Message message) { 491 ContentResolver resolver = mProviderContext.getContentResolver(); 492 long accountId = message.mAccountKey; 493 long messageId = message.mId; 494 if (accountId == Account.NO_ACCOUNT) { 495 accountId = lookupAccountForMessage(messageId); 496 } 497 if (accountId == Account.NO_ACCOUNT) { 498 // probably the message was not found 499 if (Logging.LOGD) { 500 Email.log("no account found for message " + messageId); 501 } 502 return; 503 } 504 505 // Move to Outbox 506 long outboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_OUTBOX); 507 ContentValues cv = new ContentValues(); 508 cv.put(EmailContent.MessageColumns.MAILBOX_KEY, outboxId); 509 510 // does this need to be SYNCED_CONTENT_URI instead? 511 Uri uri = ContentUris.withAppendedId(Message.CONTENT_URI, messageId); 512 resolver.update(uri, cv, null, null); 513 514 // If this is a reply/forward, indicate it as such on the source. 515 long sourceKey = message.mSourceKey; 516 if (sourceKey != Message.NO_MESSAGE) { 517 boolean isReply = (message.mFlags & Message.FLAG_TYPE_REPLY) != 0; 518 int flagUpdate = isReply ? Message.FLAG_REPLIED_TO : Message.FLAG_FORWARDED; 519 setMessageAnsweredOrForwarded(sourceKey, flagUpdate); 520 } 521 522 sendPendingMessages(accountId); 523 } 524 525 public void sendPendingMessages(long accountId) { 526 EmailServiceProxy service = 527 EmailServiceUtils.getServiceForAccount(mContext, null, accountId); 528 try { 529 service.sendMail(accountId); 530 } catch (RemoteException e) { 531 } 532 } 533 534 /** 535 * Reset visible limits for all accounts. 536 * For each account: 537 * look up limit 538 * write limit into all mailboxes for that account 539 */ 540 @SuppressWarnings("deprecation") 541 public void resetVisibleLimits() { 542 Utility.runAsync(new Runnable() { 543 @Override 544 public void run() { 545 ContentResolver resolver = mProviderContext.getContentResolver(); 546 Cursor c = null; 547 try { 548 c = resolver.query( 549 Account.CONTENT_URI, 550 Account.ID_PROJECTION, 551 null, null, null); 552 while (c.moveToNext()) { 553 long accountId = c.getLong(Account.ID_PROJECTION_COLUMN); 554 String protocol = Account.getProtocol(mProviderContext, accountId); 555 if (!HostAuth.SCHEME_EAS.equals(protocol)) { 556 ContentValues cv = new ContentValues(); 557 cv.put(MailboxColumns.VISIBLE_LIMIT, Email.VISIBLE_LIMIT_DEFAULT); 558 resolver.update(Mailbox.CONTENT_URI, cv, 559 MailboxColumns.ACCOUNT_KEY + "=?", 560 new String[] { Long.toString(accountId) }); 561 } 562 } 563 } finally { 564 if (c != null) { 565 c.close(); 566 } 567 } 568 } 569 }); 570 } 571 572 /** 573 * Increase the load count for a given mailbox, and trigger a refresh. Applies only to 574 * IMAP and POP mailboxes, with the exception of the EAS search mailbox. 575 * 576 * @param mailboxId the mailbox 577 */ 578 public void loadMoreMessages(final long mailboxId) { 579 EmailAsyncTask.runAsyncParallel(new Runnable() { 580 @Override 581 public void run() { 582 Mailbox mailbox = Mailbox.restoreMailboxWithId(mProviderContext, mailboxId); 583 if (mailbox == null) { 584 return; 585 } 586 if (mailbox.mType == Mailbox.TYPE_SEARCH) { 587 try { 588 searchMore(mailbox.mAccountKey); 589 } catch (MessagingException e) { 590 // Nothing to be done 591 } 592 return; 593 } 594 Account account = Account.restoreAccountWithId(mProviderContext, 595 mailbox.mAccountKey); 596 if (account == null) { 597 return; 598 } 599 // Use provider math to increment the field 600 ContentValues cv = new ContentValues();; 601 cv.put(EmailContent.FIELD_COLUMN_NAME, MailboxColumns.VISIBLE_LIMIT); 602 cv.put(EmailContent.ADD_COLUMN_NAME, Email.VISIBLE_LIMIT_INCREMENT); 603 Uri uri = ContentUris.withAppendedId(Mailbox.ADD_TO_FIELD_URI, mailboxId); 604 mProviderContext.getContentResolver().update(uri, cv, null, null); 605 // Trigger a refresh using the new, longer limit 606 mailbox.mVisibleLimit += Email.VISIBLE_LIMIT_INCREMENT; 607 updateMailbox(account.mId, mailboxId, true); 608 } 609 }); 610 } 611 612 /** 613 * @param messageId the id of message 614 * @return the accountId corresponding to the given messageId, or -1 if not found. 615 */ 616 private long lookupAccountForMessage(long messageId) { 617 ContentResolver resolver = mProviderContext.getContentResolver(); 618 Cursor c = resolver.query(EmailContent.Message.CONTENT_URI, 619 MESSAGEID_TO_ACCOUNTID_PROJECTION, EmailContent.RECORD_ID + "=?", 620 new String[] { Long.toString(messageId) }, null); 621 try { 622 return c.moveToFirst() 623 ? c.getLong(MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID) 624 : -1; 625 } finally { 626 c.close(); 627 } 628 } 629 630 /** 631 * Delete a single attachment entry from the DB given its id. 632 * Does not delete any eventual associated files. 633 */ 634 public void deleteAttachment(long attachmentId) { 635 ContentResolver resolver = mProviderContext.getContentResolver(); 636 Uri uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, attachmentId); 637 resolver.delete(uri, null, null); 638 } 639 640 /** 641 * Async version of {@link #deleteMessageSync}. 642 */ 643 public void deleteMessage(final long messageId) { 644 EmailAsyncTask.runAsyncParallel(new Runnable() { 645 @Override 646 public void run() { 647 deleteMessageSync(messageId); 648 } 649 }); 650 } 651 652 /** 653 * Batch & async version of {@link #deleteMessageSync}. 654 */ 655 public void deleteMessages(final long[] messageIds) { 656 if (messageIds == null || messageIds.length == 0) { 657 throw new IllegalArgumentException(); 658 } 659 EmailAsyncTask.runAsyncParallel(new Runnable() { 660 @Override 661 public void run() { 662 for (long messageId: messageIds) { 663 deleteMessageSync(messageId); 664 } 665 } 666 }); 667 } 668 669 /** 670 * Delete a single message by moving it to the trash, or really delete it if it's already in 671 * trash or a draft message. 672 * 673 * This function has no callback, no result reporting, because the desired outcome 674 * is reflected entirely by changes to one or more cursors. 675 * 676 * @param messageId The id of the message to "delete". 677 */ 678 /* package */ void deleteMessageSync(long messageId) { 679 // 1. Get the message's account 680 Account account = Account.getAccountForMessageId(mProviderContext, messageId); 681 682 if (account == null) return; 683 684 // 2. Confirm that there is a trash mailbox available. If not, create one 685 long trashMailboxId = findOrCreateMailboxOfType(account.mId, Mailbox.TYPE_TRASH); 686 687 // 3. Get the message's original mailbox 688 Mailbox mailbox = Mailbox.getMailboxForMessageId(mProviderContext, messageId); 689 690 if (mailbox == null) return; 691 692 // 4. Drop non-essential data for the message (e.g. attachment files) 693 AttachmentUtilities.deleteAllAttachmentFiles(mProviderContext, account.mId, 694 messageId); 695 696 Uri uri = ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, 697 messageId); 698 ContentResolver resolver = mProviderContext.getContentResolver(); 699 700 // 5. Perform "delete" as appropriate 701 if ((mailbox.mId == trashMailboxId) || (mailbox.mType == Mailbox.TYPE_DRAFTS)) { 702 // 5a. Really delete it 703 resolver.delete(uri, null, null); 704 } else { 705 // 5b. Move to trash 706 ContentValues cv = new ContentValues(); 707 cv.put(EmailContent.MessageColumns.MAILBOX_KEY, trashMailboxId); 708 resolver.update(uri, cv, null, null); 709 } 710 } 711 712 /** 713 * Moves messages to a new mailbox. 714 * 715 * This function has no callback, no result reporting, because the desired outcome 716 * is reflected entirely by changes to one or more cursors. 717 * 718 * Note this method assumes all of the given message and mailbox IDs belong to the same 719 * account. 720 * 721 * @param messageIds IDs of the messages that are to be moved 722 * @param newMailboxId ID of the new mailbox that the messages will be moved to 723 * @return an asynchronous task that executes the move (for testing only) 724 */ 725 public EmailAsyncTask<Void, Void, Void> moveMessages(final long[] messageIds, 726 final long newMailboxId) { 727 if (messageIds == null || messageIds.length == 0) { 728 throw new IllegalArgumentException(); 729 } 730 return EmailAsyncTask.runAsyncParallel(new Runnable() { 731 @Override 732 public void run() { 733 Account account = Account.getAccountForMessageId(mProviderContext, messageIds[0]); 734 if (account != null) { 735 ContentValues cv = new ContentValues(); 736 cv.put(EmailContent.MessageColumns.MAILBOX_KEY, newMailboxId); 737 ContentResolver resolver = mProviderContext.getContentResolver(); 738 for (long messageId : messageIds) { 739 Uri uri = ContentUris.withAppendedId( 740 EmailContent.Message.SYNCED_CONTENT_URI, messageId); 741 resolver.update(uri, cv, null, null); 742 } 743 } 744 } 745 }); 746 } 747 748 /** 749 * Set/clear the unread status of a message 750 * 751 * @param messageId the message to update 752 * @param isRead the new value for the isRead flag 753 */ 754 public void setMessageReadSync(long messageId, boolean isRead) { 755 setMessageBooleanSync(messageId, EmailContent.MessageColumns.FLAG_READ, isRead); 756 } 757 758 /** 759 * Set/clear the unread status of a message from UI thread 760 * 761 * @param messageId the message to update 762 * @param isRead the new value for the isRead flag 763 * @return the EmailAsyncTask created 764 */ 765 public EmailAsyncTask<Void, Void, Void> setMessageRead(final long messageId, 766 final boolean isRead) { 767 return EmailAsyncTask.runAsyncParallel(new Runnable() { 768 @Override 769 public void run() { 770 setMessageBooleanSync(messageId, EmailContent.MessageColumns.FLAG_READ, isRead); 771 }}); 772 } 773 774 /** 775 * Update a message record and ping MessagingController, if necessary 776 * 777 * @param messageId the message to update 778 * @param cv the ContentValues used in the update 779 */ 780 private void updateMessageSync(long messageId, ContentValues cv) { 781 Uri uri = ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, messageId); 782 mProviderContext.getContentResolver().update(uri, cv, null, null); 783 } 784 785 /** 786 * Set the answered status of a message 787 * 788 * @param messageId the message to update 789 * @return the AsyncTask that will execute the changes (for testing only) 790 */ 791 public void setMessageAnsweredOrForwarded(final long messageId, 792 final int flag) { 793 EmailAsyncTask.runAsyncParallel(new Runnable() { 794 @Override 795 public void run() { 796 Message msg = Message.restoreMessageWithId(mProviderContext, messageId); 797 if (msg == null) { 798 Log.w(Logging.LOG_TAG, "Unable to find source message for a reply/forward"); 799 return; 800 } 801 ContentValues cv = new ContentValues(); 802 cv.put(MessageColumns.FLAGS, msg.mFlags | flag); 803 updateMessageSync(messageId, cv); 804 } 805 }); 806 } 807 808 /** 809 * Set/clear the favorite status of a message from UI thread 810 * 811 * @param messageId the message to update 812 * @param isFavorite the new value for the isFavorite flag 813 * @return the EmailAsyncTask created 814 */ 815 public EmailAsyncTask<Void, Void, Void> setMessageFavorite(final long messageId, 816 final boolean isFavorite) { 817 return EmailAsyncTask.runAsyncParallel(new Runnable() { 818 @Override 819 public void run() { 820 setMessageBooleanSync(messageId, EmailContent.MessageColumns.FLAG_FAVORITE, 821 isFavorite); 822 }}); 823 } 824 /** 825 * Set/clear the favorite status of a message 826 * 827 * @param messageId the message to update 828 * @param isFavorite the new value for the isFavorite flag 829 */ 830 public void setMessageFavoriteSync(long messageId, boolean isFavorite) { 831 setMessageBooleanSync(messageId, EmailContent.MessageColumns.FLAG_FAVORITE, isFavorite); 832 } 833 834 /** 835 * Set/clear boolean columns of a message 836 * 837 * @param messageId the message to update 838 * @param columnName the column to update 839 * @param columnValue the new value for the column 840 */ 841 private void setMessageBooleanSync(long messageId, String columnName, boolean columnValue) { 842 ContentValues cv = new ContentValues(); 843 cv.put(columnName, columnValue); 844 updateMessageSync(messageId, cv); 845 } 846 847 848 private static final HashMap<Long, SearchParams> sSearchParamsMap = 849 new HashMap<Long, SearchParams>(); 850 851 public void searchMore(long accountId) throws MessagingException { 852 SearchParams params = sSearchParamsMap.get(accountId); 853 if (params == null) return; 854 params.mOffset += params.mLimit; 855 searchMessages(accountId, params); 856 } 857 858 /** 859 * Search for messages on the (IMAP) server; do not call this on the UI thread! 860 * @param accountId the id of the account to be searched 861 * @param searchParams the parameters for this search 862 * @throws MessagingException 863 */ 864 public int searchMessages(final long accountId, final SearchParams searchParams) 865 throws MessagingException { 866 // Find/create our search mailbox 867 Mailbox searchMailbox = getSearchMailbox(accountId); 868 if (searchMailbox == null) return 0; 869 final long searchMailboxId = searchMailbox.mId; 870 // Save this away (per account) 871 sSearchParamsMap.put(accountId, searchParams); 872 873 if (searchParams.mOffset == 0) { 874 // Delete existing contents of search mailbox 875 ContentResolver resolver = mContext.getContentResolver(); 876 resolver.delete(Message.CONTENT_URI, Message.MAILBOX_KEY + "=" + searchMailboxId, 877 null); 878 ContentValues cv = new ContentValues(); 879 // For now, use the actual query as the name of the mailbox 880 cv.put(Mailbox.DISPLAY_NAME, searchParams.mFilter); 881 resolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, searchMailboxId), 882 cv, null, null); 883 } 884 885 IEmailService service = getServiceForAccount(accountId); 886 if (service != null) { 887 // Service implementation 888 try { 889 return service.searchMessages(accountId, searchParams, searchMailboxId); 890 } catch (RemoteException e) { 891 // TODO Change exception handling to be consistent with however this method 892 // is implemented for other protocols 893 Log.e("searchMessages", "RemoteException", e); 894 } 895 } 896 return 0; 897 } 898 899 private EmailServiceProxy getServiceForAccount(long accountId) { 900 return EmailServiceUtils.getServiceForAccount(mContext, mServiceCallback, accountId); 901 } 902 903 /** 904 * Respond to a meeting invitation. 905 * 906 * @param messageId the id of the invitation being responded to 907 * @param response the code representing the response to the invitation 908 */ 909 public void sendMeetingResponse(final long messageId, final int response) { 910 // Split here for target type (Service or MessagingController) 911 IEmailService service = getServiceForMessage(messageId); 912 if (service != null) { 913 // Service implementation 914 try { 915 service.sendMeetingResponse(messageId, response); 916 } catch (RemoteException e) { 917 // TODO Change exception handling to be consistent with however this method 918 // is implemented for other protocols 919 Log.e("onDownloadAttachment", "RemoteException", e); 920 } 921 } 922 } 923 924 /** 925 * Request that an attachment be loaded. It will be stored at a location controlled 926 * by the AttachmentProvider. 927 * 928 * @param attachmentId the attachment to load 929 * @param messageId the owner message 930 * @param accountId the owner account 931 */ 932 public void loadAttachment(final long attachmentId, final long messageId, 933 final long accountId) { 934 Attachment attachInfo = Attachment.restoreAttachmentWithId(mProviderContext, attachmentId); 935 if (attachInfo == null) { 936 return; 937 } 938 939 if (Utility.attachmentExists(mProviderContext, attachInfo)) { 940 // The attachment has already been downloaded, so we will just "pretend" to download it 941 // This presumably is for POP3 messages 942 synchronized (mListeners) { 943 for (Result listener : mListeners) { 944 listener.loadAttachmentCallback(null, accountId, messageId, attachmentId, 0); 945 } 946 for (Result listener : mListeners) { 947 listener.loadAttachmentCallback(null, accountId, messageId, attachmentId, 100); 948 } 949 } 950 return; 951 } 952 953 // Flag the attachment as needing download at the user's request 954 ContentValues cv = new ContentValues(); 955 cv.put(Attachment.FLAGS, attachInfo.mFlags | Attachment.FLAG_DOWNLOAD_USER_REQUEST); 956 attachInfo.update(mProviderContext, cv); 957 } 958 959 /** 960 * For a given message id, return a service proxy if applicable, or null. 961 * 962 * @param messageId the message of interest 963 * @result service proxy, or null if n/a 964 */ 965 private EmailServiceProxy getServiceForMessage(long messageId) { 966 // TODO make this more efficient, caching the account, smaller lookup here, etc. 967 Message message = Message.restoreMessageWithId(mProviderContext, messageId); 968 if (message == null) { 969 return null; 970 } 971 return getServiceForAccount(message.mAccountKey); 972 } 973 974 /** 975 * Delete an account. 976 */ 977 public void deleteAccount(final long accountId) { 978 EmailAsyncTask.runAsyncParallel(new Runnable() { 979 @Override 980 public void run() { 981 deleteAccountSync(accountId, mProviderContext); 982 } 983 }); 984 } 985 986 /** 987 * Delete an account synchronously. 988 */ 989 public void deleteAccountSync(long accountId, Context context) { 990 try { 991 mLegacyControllerMap.remove(accountId); 992 // Get the account URI. 993 final Account account = Account.restoreAccountWithId(context, accountId); 994 if (account == null) { 995 return; // Already deleted? 996 } 997 998 // Delete account data, attachments, PIM data, etc. 999 deleteSyncedDataSync(accountId); 1000 1001 // Now delete the account itself 1002 Uri uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId); 1003 context.getContentResolver().delete(uri, null, null); 1004 1005 // For unit tests, don't run backup, security, and ui pieces. 1006 if (mInUnitTests) { 1007 return; 1008 } 1009 1010 // Clean up 1011 AccountBackupRestore.backup(context); 1012 SecurityPolicy.getInstance(context).reducePolicies(); 1013 Email.setServicesEnabledSync(context); 1014 Email.setNotifyUiAccountsChanged(true); 1015 } catch (Exception e) { 1016 Log.w(Logging.LOG_TAG, "Exception while deleting account", e); 1017 } 1018 } 1019 1020 /** 1021 * Delete all synced data, but don't delete the actual account. This is used when security 1022 * policy requirements are not met, and we don't want to reveal any synced data, but we do 1023 * wish to keep the account configured (e.g. to accept remote wipe commands). 1024 * 1025 * The only mailbox not deleted is the account mailbox (if any) 1026 * Also, clear the sync keys on the remaining account, since the data is gone. 1027 * 1028 * SYNCHRONOUS - do not call from UI thread. 1029 * 1030 * @param accountId The account to wipe. 1031 */ 1032 public void deleteSyncedDataSync(long accountId) { 1033 try { 1034 // Delete synced attachments 1035 AttachmentUtilities.deleteAllAccountAttachmentFiles(mProviderContext, 1036 accountId); 1037 1038 // Delete synced email, leaving only an empty inbox. We do this in two phases: 1039 // 1. Delete all non-inbox mailboxes (which will delete all of their messages) 1040 // 2. Delete all remaining messages (which will be the inbox messages) 1041 ContentResolver resolver = mProviderContext.getContentResolver(); 1042 String[] accountIdArgs = new String[] { Long.toString(accountId) }; 1043 resolver.delete(Mailbox.CONTENT_URI, 1044 MAILBOXES_FOR_ACCOUNT_EXCEPT_ACCOUNT_MAILBOX_SELECTION, 1045 accountIdArgs); 1046 resolver.delete(Message.CONTENT_URI, MESSAGES_FOR_ACCOUNT_SELECTION, accountIdArgs); 1047 1048 // Delete sync keys on remaining items 1049 ContentValues cv = new ContentValues(); 1050 cv.putNull(Account.SYNC_KEY); 1051 resolver.update(Account.CONTENT_URI, cv, Account.ID_SELECTION, accountIdArgs); 1052 cv.clear(); 1053 cv.putNull(Mailbox.SYNC_KEY); 1054 resolver.update(Mailbox.CONTENT_URI, cv, 1055 MAILBOXES_FOR_ACCOUNT_SELECTION, accountIdArgs); 1056 1057 // Delete PIM data (contacts, calendar), stop syncs, etc. if applicable 1058 IEmailService service = getServiceForAccount(accountId); 1059 if (service != null) { 1060 service.deleteAccountPIMData(accountId); 1061 } 1062 } catch (Exception e) { 1063 Log.w(Logging.LOG_TAG, "Exception while deleting account synced data", e); 1064 } 1065 } 1066 1067 /** 1068 * Simple callback for synchronous commands. For many commands, this can be largely ignored 1069 * and the result is observed via provider cursors. The callback will *not* necessarily be 1070 * made from the UI thread, so you may need further handlers to safely make UI updates. 1071 */ 1072 public static abstract class Result { 1073 private volatile boolean mRegistered; 1074 1075 protected void setRegistered(boolean registered) { 1076 mRegistered = registered; 1077 } 1078 1079 protected final boolean isRegistered() { 1080 return mRegistered; 1081 } 1082 1083 /** 1084 * Callback for updateMailboxList 1085 * 1086 * @param result If null, the operation completed without error 1087 * @param accountId The account being operated on 1088 * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete 1089 */ 1090 public void updateMailboxListCallback(MessagingException result, long accountId, 1091 int progress) { 1092 } 1093 1094 /** 1095 * Callback for updateMailbox. Note: This looks a lot like checkMailCallback, but 1096 * it's a separate call used only by UI's, so we can keep things separate. 1097 * 1098 * @param result If null, the operation completed without error 1099 * @param accountId The account being operated on 1100 * @param mailboxId The mailbox being operated on 1101 * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete 1102 * @param numNewMessages the number of new messages delivered 1103 */ 1104 public void updateMailboxCallback(MessagingException result, long accountId, 1105 long mailboxId, int progress, int numNewMessages, ArrayList<Long> addedMessages) { 1106 } 1107 1108 /** 1109 * Callback for loadMessageForView 1110 * 1111 * @param result if null, the attachment completed - if non-null, terminating with failure 1112 * @param messageId the message which contains the attachment 1113 * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete 1114 */ 1115 public void loadMessageForViewCallback(MessagingException result, long accountId, 1116 long messageId, int progress) { 1117 } 1118 1119 /** 1120 * Callback for loadAttachment 1121 * 1122 * @param result if null, the attachment completed - if non-null, terminating with failure 1123 * @param messageId the message which contains the attachment 1124 * @param attachmentId the attachment being loaded 1125 * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete 1126 */ 1127 public void loadAttachmentCallback(MessagingException result, long accountId, 1128 long messageId, long attachmentId, int progress) { 1129 } 1130 1131 /** 1132 * Callback for checkmail. Note: This looks a lot like updateMailboxCallback, but 1133 * it's a separate call used only by the automatic checker service, so we can keep 1134 * things separate. 1135 * 1136 * @param result If null, the operation completed without error 1137 * @param accountId The account being operated on 1138 * @param mailboxId The mailbox being operated on (may be unknown at start) 1139 * @param progress 0 for "starting", no updates, 100 for complete 1140 * @param tag the same tag that was passed to serviceCheckMail() 1141 */ 1142 public void serviceCheckMailCallback(MessagingException result, long accountId, 1143 long mailboxId, int progress, long tag) { 1144 } 1145 } 1146 1147 /** 1148 * Service callback for service operations 1149 */ 1150 private class ServiceCallback extends IEmailServiceCallback.Stub { 1151 1152 @Override 1153 public void loadAttachmentStatus(long messageId, long attachmentId, int statusCode, 1154 int progress) { 1155 MessagingException result = mapStatusToException(statusCode); 1156 switch (statusCode) { 1157 case EmailServiceStatus.SUCCESS: 1158 progress = 100; 1159 break; 1160 case EmailServiceStatus.IN_PROGRESS: 1161 // discard progress reports that look like sentinels 1162 if (progress < 0 || progress >= 100) { 1163 return; 1164 } 1165 break; 1166 } 1167 final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId); 1168 synchronized (mListeners) { 1169 for (Result listener : mListeners) { 1170 listener.loadAttachmentCallback(result, accountId, messageId, attachmentId, 1171 progress); 1172 } 1173 } 1174 } 1175 1176 /** 1177 * Unused 1178 */ 1179 @Override 1180 public void sendMessageStatus(long messageId, String subject, int statusCode, 1181 int progress) { 1182 } 1183 1184 /** 1185 * Note, this is an incomplete implementation of this callback, because we are 1186 * not getting things back from Service in quite the same way as from MessagingController. 1187 * However, this is sufficient for basic "progress=100" notification that message send 1188 * has just completed. 1189 */ 1190 @Override 1191 public void loadMessageStatus(long messageId, int statusCode, int progress) { 1192 long accountId = -1; // This should be in the callback 1193 MessagingException result = mapStatusToException(statusCode); 1194 switch (statusCode) { 1195 case EmailServiceStatus.SUCCESS: 1196 progress = 100; 1197 break; 1198 case EmailServiceStatus.IN_PROGRESS: 1199 // discard progress reports that look like sentinels 1200 if (progress < 0 || progress >= 100) { 1201 return; 1202 } 1203 break; 1204 } 1205 synchronized(mListeners) { 1206 for (Result listener : mListeners) { 1207 listener.loadMessageForViewCallback(result, accountId, messageId, progress); 1208 } 1209 } 1210 } 1211 1212 @Override 1213 public void syncMailboxListStatus(long accountId, int statusCode, int progress) { 1214 MessagingException result = mapStatusToException(statusCode); 1215 switch (statusCode) { 1216 case EmailServiceStatus.SUCCESS: 1217 progress = 100; 1218 break; 1219 case EmailServiceStatus.IN_PROGRESS: 1220 // discard progress reports that look like sentinels 1221 if (progress < 0 || progress >= 100) { 1222 return; 1223 } 1224 break; 1225 } 1226 synchronized(mListeners) { 1227 for (Result listener : mListeners) { 1228 listener.updateMailboxListCallback(result, accountId, progress); 1229 } 1230 } 1231 } 1232 1233 @Override 1234 public void syncMailboxStatus(long mailboxId, int statusCode, int progress) { 1235 MessagingException result = mapStatusToException(statusCode); 1236 switch (statusCode) { 1237 case EmailServiceStatus.SUCCESS: 1238 progress = 100; 1239 break; 1240 case EmailServiceStatus.IN_PROGRESS: 1241 // discard progress reports that look like sentinels 1242 if (progress < 0 || progress >= 100) { 1243 return; 1244 } 1245 break; 1246 } 1247 // TODO should pass this back instead of looking it up here 1248 Mailbox mbx = Mailbox.restoreMailboxWithId(mProviderContext, mailboxId); 1249 // The mailbox could have disappeared if the server commanded it 1250 if (mbx == null) return; 1251 long accountId = mbx.mAccountKey; 1252 synchronized(mListeners) { 1253 for (Result listener : mListeners) { 1254 listener.updateMailboxCallback(result, accountId, mailboxId, progress, 0, null); 1255 } 1256 } 1257 } 1258 1259 private MessagingException mapStatusToException(int statusCode) { 1260 switch (statusCode) { 1261 case EmailServiceStatus.SUCCESS: 1262 case EmailServiceStatus.IN_PROGRESS: 1263 // Don't generate error if the account is uninitialized 1264 case EmailServiceStatus.ACCOUNT_UNINITIALIZED: 1265 return null; 1266 1267 case EmailServiceStatus.LOGIN_FAILED: 1268 return new AuthenticationFailedException(""); 1269 1270 case EmailServiceStatus.CONNECTION_ERROR: 1271 return new MessagingException(MessagingException.IOERROR); 1272 1273 case EmailServiceStatus.SECURITY_FAILURE: 1274 return new MessagingException(MessagingException.SECURITY_POLICIES_REQUIRED); 1275 1276 case EmailServiceStatus.ACCESS_DENIED: 1277 return new MessagingException(MessagingException.ACCESS_DENIED); 1278 1279 case EmailServiceStatus.ATTACHMENT_NOT_FOUND: 1280 return new MessagingException(MessagingException.ATTACHMENT_NOT_FOUND); 1281 1282 case EmailServiceStatus.CLIENT_CERTIFICATE_ERROR: 1283 return new MessagingException(MessagingException.CLIENT_CERTIFICATE_ERROR); 1284 1285 case EmailServiceStatus.MESSAGE_NOT_FOUND: 1286 case EmailServiceStatus.FOLDER_NOT_DELETED: 1287 case EmailServiceStatus.FOLDER_NOT_RENAMED: 1288 case EmailServiceStatus.FOLDER_NOT_CREATED: 1289 case EmailServiceStatus.REMOTE_EXCEPTION: 1290 // TODO: define exception code(s) & UI string(s) for server-side errors 1291 default: 1292 return new MessagingException(String.valueOf(statusCode)); 1293 } 1294 } 1295 } 1296 1297 private interface ServiceCallbackWrapper { 1298 public void call(IEmailServiceCallback cb) throws RemoteException; 1299 } 1300 1301 /** 1302 * Proxy that can be used to broadcast service callbacks; we currently use this only for 1303 * loadAttachment callbacks 1304 */ 1305 private final IEmailServiceCallback.Stub mCallbackProxy = new IEmailServiceCallback.Stub() { 1306 1307 /** 1308 * Broadcast a callback to the everyone that's registered 1309 * 1310 * @param wrapper the ServiceCallbackWrapper used in the broadcast 1311 */ 1312 private synchronized void broadcastCallback(ServiceCallbackWrapper wrapper) { 1313 if (sCallbackList != null) { 1314 // Call everyone on our callback list 1315 // Exceptions can be safely ignored 1316 int count = sCallbackList.beginBroadcast(); 1317 for (int i = 0; i < count; i++) { 1318 try { 1319 wrapper.call(sCallbackList.getBroadcastItem(i)); 1320 } catch (RemoteException e) { 1321 } 1322 } 1323 sCallbackList.finishBroadcast(); 1324 } 1325 } 1326 1327 @Override 1328 public void loadAttachmentStatus(final long messageId, final long attachmentId, 1329 final int status, final int progress) { 1330 broadcastCallback(new ServiceCallbackWrapper() { 1331 @Override 1332 public void call(IEmailServiceCallback cb) throws RemoteException { 1333 cb.loadAttachmentStatus(messageId, attachmentId, status, progress); 1334 } 1335 }); 1336 } 1337 1338 @Override 1339 public void syncMailboxListStatus(long accountId, int statusCode, int progress) 1340 throws RemoteException { 1341 } 1342 1343 @Override 1344 public void syncMailboxStatus(final long mailboxId, final int statusCode, 1345 final int progress) throws RemoteException { 1346 } 1347 1348 @Override 1349 public void sendMessageStatus(long messageId, String subject, int statusCode, int progress) 1350 throws RemoteException { 1351 } 1352 1353 @Override 1354 public void loadMessageStatus(long messageId, int statusCode, int progress) 1355 throws RemoteException { 1356 } 1357 }; 1358} 1359