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