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