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