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