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