Controller.java revision b8a781f220617d6e7750c5e9f093742206add45f
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.MessagingException; 20import com.android.email.mail.Store; 21import com.android.email.provider.EmailContent; 22import com.android.email.provider.EmailContent.Account; 23import com.android.email.provider.EmailContent.Attachment; 24import com.android.email.provider.EmailContent.Mailbox; 25import com.android.email.provider.EmailContent.Message; 26import com.android.email.service.EmailServiceProxy; 27import com.android.exchange.EmailServiceStatus; 28import com.android.exchange.IEmailService; 29import com.android.exchange.IEmailServiceCallback; 30import com.android.exchange.SyncManager; 31 32import android.content.ContentResolver; 33import android.content.ContentUris; 34import android.content.ContentValues; 35import android.content.Context; 36import android.database.Cursor; 37import android.net.Uri; 38import android.os.RemoteException; 39import android.util.Log; 40 41import java.util.HashSet; 42 43/** 44 * New central controller/dispatcher for Email activities that may require remote operations. 45 * Handles disambiguating between legacy MessagingController operations and newer provider/sync 46 * based code. 47 */ 48public class Controller { 49 50 static Controller sInstance; 51 private Context mContext; 52 private Context mProviderContext; 53 private MessagingController mLegacyController; 54 private HashSet<Result> mListeners = new HashSet<Result>(); 55 56 private static String[] MESSAGEID_TO_ACCOUNTID_PROJECTION = new String[] { 57 EmailContent.RECORD_ID, 58 EmailContent.MessageColumns.ACCOUNT_KEY 59 }; 60 private static int MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID = 1; 61 62 protected Controller(Context _context) { 63 mContext = _context; 64 mProviderContext = _context; 65 mLegacyController = MessagingController.getInstance(mContext); 66 } 67 68 /** 69 * Gets or creates the singleton instance of Controller. 70 * @param _context The context that will be used for all underlying system access 71 */ 72 public synchronized static Controller getInstance(Context _context) { 73 if (sInstance == null) { 74 sInstance = new Controller(_context); 75 } 76 return sInstance; 77 } 78 79 /** 80 * For testing only: Inject a different context for provider access. This will be 81 * used internally for access the underlying provider (e.g. getContentResolver().query()). 82 * @param providerContext the provider context to be used by this instance 83 */ 84 public void setProviderContext(Context providerContext) { 85 mProviderContext = providerContext; 86 } 87 88 /** 89 * Any UI code that wishes for callback results (on async ops) should register their callback 90 * here (typically from onResume()). Unregistered callbacks will never be called, to prevent 91 * problems when the command completes and the activity has already paused or finished. 92 * @param listener The callback that may be used in action methods 93 */ 94 public void addResultCallback(Result listener) { 95 synchronized (mListeners) { 96 mListeners.add(listener); 97 } 98 } 99 100 /** 101 * Any UI code that no longer wishes for callback results (on async ops) should unregister 102 * their callback here (typically from onPause()). Unregistered callbacks will never be called, 103 * to prevent problems when the command completes and the activity has already paused or 104 * finished. 105 * @param listener The callback that may no longer be used 106 */ 107 public void removeResultCallback(Result listener) { 108 synchronized (mListeners) { 109 mListeners.remove(listener); 110 } 111 } 112 113 private boolean isActiveResultCallback(Result listener) { 114 synchronized (mListeners) { 115 return mListeners.contains(listener); 116 } 117 } 118 119 /** 120 * Request a remote update of mailboxes for an account. 121 * 122 * TODO: Implement (if any) for non-MessagingController 123 * TODO: Probably the right way is to create a fake "service" for MessagingController ops 124 */ 125 public void updateMailboxList(final EmailContent.Account account, final Result callback) { 126 127 // 1. determine if we can use MessagingController for this 128 boolean legacyController = isMessagingController(account); 129 130 // 2. if not...? 131 // TODO: for now, just pretend "it worked" 132 if (!legacyController) { 133 if (callback != null) { 134 callback.updateMailboxListCallback(null, account.mId); 135 } 136 return; 137 } 138 139 // 3. if so, make the call 140 new Thread() { 141 @Override 142 public void run() { 143 MessagingListener listener = new LegacyListener(callback); 144 mLegacyController.addListener(listener); 145 mLegacyController.listFolders(account, listener); 146 } 147 }.start(); 148 } 149 150 /** 151 * Request a remote update of a mailbox. 152 * 153 * The contract here should be to try and update the headers ASAP, in order to populate 154 * a simple message list. We should also at this point queue up a background task of 155 * downloading some/all of the messages in this mailbox, but that should be interruptable. 156 */ 157 public void updateMailbox(final EmailContent.Account account, 158 final EmailContent.Mailbox mailbox, final Result callback) { 159 160 // 1. determine if we can use MessagingController for this 161 boolean legacyController = isMessagingController(account); 162 163 // 2. if not...? 164 // TODO: for now, just pretend "it worked" 165 if (!legacyController) { 166 if (callback != null) { 167 callback.updateMailboxCallback(null, account.mId, mailbox.mId, -1, -1); 168 } 169 return; 170 } 171 172 // 3. if so, make the call 173 new Thread() { 174 @Override 175 public void run() { 176 MessagingListener listener = new LegacyListener(callback); 177 mLegacyController.addListener(listener); 178 mLegacyController.synchronizeMailbox(account, mailbox, listener); 179 } 180 }.start(); 181 } 182 183 /** 184 * Saves the message to a mailbox of given type. 185 * @param message the message (must have the mAccountId set). 186 * @param mailboxType the mailbox type (e.g. Mailbox.TYPE_DRAFTS). 187 * TODO: UI feedback. 188 * TODO: use AsyncTask instead of Thread 189 */ 190 public void saveToMailbox(final EmailContent.Message message, final int mailboxType) { 191 new Thread() { 192 @Override 193 public void run() { 194 long accountId = message.mAccountKey; 195 long mailboxId = findOrCreateMailboxOfType(accountId, mailboxType); 196 message.mMailboxKey = mailboxId; 197 message.save(mContext); 198 } 199 }.start(); 200 } 201 202 /** 203 * @param accountId the account id 204 * @param mailboxType the mailbox type (e.g. EmailContent.Mailbox.TYPE_TRASH) 205 * @return the id of the mailbox. The mailbox is created if not existing. 206 * Returns Mailbox.NO_MAILBOX if the accountId or mailboxType are negative. 207 * Does not validate the input in other ways (e.g. does not verify the existence of account). 208 */ 209 public long findOrCreateMailboxOfType(long accountId, int mailboxType) { 210 if (accountId < 0 || mailboxType < 0) { 211 return Mailbox.NO_MAILBOX; 212 } 213 long mailboxId = 214 Mailbox.findMailboxOfType(mProviderContext, accountId, mailboxType); 215 return mailboxId == Mailbox.NO_MAILBOX ? createMailbox(accountId, mailboxType) : mailboxId; 216 } 217 218 /** 219 * @param mailboxType the mailbox type 220 * @return the resource string corresponding to the mailbox type, empty if not found. 221 */ 222 /* package */ String getSpecialMailboxDisplayName(int mailboxType) { 223 int resId = -1; 224 switch (mailboxType) { 225 case Mailbox.TYPE_INBOX: 226 // TODO: there is no special_mailbox_display_name_inbox; why? 227 resId = R.string.special_mailbox_name_inbox; 228 break; 229 case Mailbox.TYPE_OUTBOX: 230 resId = R.string.special_mailbox_display_name_outbox; 231 break; 232 case Mailbox.TYPE_DRAFTS: 233 resId = R.string.special_mailbox_display_name_drafts; 234 break; 235 case Mailbox.TYPE_TRASH: 236 resId = R.string.special_mailbox_display_name_trash; 237 break; 238 case Mailbox.TYPE_SENT: 239 resId = R.string.special_mailbox_display_name_sent; 240 break; 241 } 242 return resId != -1 ? mContext.getString(resId) : ""; 243 } 244 245 /** 246 * Create a mailbox given the account and mailboxType. 247 * TODO: Does this need to be signaled explicitly to the sync engines? 248 * As this method is only used internally ('private'), it does not 249 * validate its inputs (accountId and mailboxType). 250 */ 251 /* package */ long createMailbox(long accountId, int mailboxType) { 252 if (accountId < 0 || mailboxType < 0) { 253 String mes = "Invalid arguments " + accountId + ' ' + mailboxType; 254 Log.e(Email.LOG_TAG, mes); 255 throw new RuntimeException(mes); 256 } 257 Mailbox box = new Mailbox(); 258 box.mAccountKey = accountId; 259 box.mType = mailboxType; 260 box.mSyncInterval = EmailContent.Account.CHECK_INTERVAL_NEVER; 261 box.mFlagVisible = true; 262 box.mDisplayName = getSpecialMailboxDisplayName(mailboxType); 263 box.save(mProviderContext); 264 return box.mId; 265 } 266 267 /** 268 * Delete a single message by moving it to the trash. 269 * 270 * This function has no callback, no result reporting, because the desired outcome 271 * is reflected entirely by changes to one or more cursors. 272 * 273 * @param messageId The id of the message to "delete". 274 * @param accountId The id of the message's account, or -1 if not known by caller 275 * 276 * TODO: Move out of UI thread 277 * TODO: "get account a for message m" should be a utility 278 * TODO: "get mailbox of type n for account a" should be a utility 279 */ 280 public void deleteMessage(long messageId, long accountId) { 281 ContentResolver resolver = mProviderContext.getContentResolver(); 282 283 // 1. Look up acct# for message we're deleting 284 Cursor c = null; 285 if (accountId == -1) { 286 try { 287 c = resolver.query(EmailContent.Message.CONTENT_URI, 288 MESSAGEID_TO_ACCOUNTID_PROJECTION, EmailContent.RECORD_ID + "=?", 289 new String[] { Long.toString(messageId) }, null); 290 if (c.moveToFirst()) { 291 accountId = c.getLong(MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID); 292 } else { 293 return; 294 } 295 } finally { 296 if (c != null) c.close(); 297 } 298 } 299 300 // 2. Confirm that there is a trash mailbox available 301 // 3. If there's no trash mailbox, create one 302 // TODO: Does this need to be signaled explicitly to the sync engines? 303 long trashMailboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_TRASH); 304 305 // 4. Change the mailbox key for the message we're "deleting" 306 ContentValues cv = new ContentValues(); 307 cv.put(EmailContent.MessageColumns.MAILBOX_KEY, trashMailboxId); 308 Uri uri = ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, messageId); 309 resolver.update(uri, cv, null, null); 310 311 // 5. Drop non-essential data for the message (e.g. attachments) 312 // TODO: find the actual files (if any, if loaded) & delete them 313 c = null; 314 try { 315 c = resolver.query(EmailContent.Attachment.CONTENT_URI, 316 EmailContent.Attachment.CONTENT_PROJECTION, 317 EmailContent.AttachmentColumns.MESSAGE_KEY + "=?", 318 new String[] { Long.toString(messageId) }, null); 319 while (c.moveToNext()) { 320 // delete any associated storage 321 // delete row? 322 } 323 } finally { 324 if (c != null) c.close(); 325 } 326 327 // 6. For IMAP/POP3 we may need to kick off an immediate delete (depends on acct settings) 328 // TODO write this 329 } 330 331 /** 332 * Set/clear the unread status of a message 333 * 334 * @param messageId the message to update 335 * @param isRead the new value for the isRead flag 336 */ 337 public void setMessageRead(long messageId, boolean isRead) { 338 // TODO this should not be in this thread. queue it up. 339 // TODO Also, it needs to update the read/unread count in the mailbox 340 // TODO kick off service/messagingcontroller actions 341 342 ContentValues cv = new ContentValues(); 343 cv.put(EmailContent.MessageColumns.FLAG_READ, isRead); 344 Uri uri = ContentUris.withAppendedId( 345 EmailContent.Message.SYNCED_CONTENT_URI, messageId); 346 mProviderContext.getContentResolver().update(uri, cv, null, null); 347 } 348 349 /** 350 * Set/clear the favorite status of a message 351 * 352 * @param messageId the message to update 353 * @param isFavorite the new value for the isFavorite flag 354 */ 355 public void setMessageFavorite(long messageId, boolean isFavorite) { 356 // TODO this should not be in this thread. queue it up. 357 // TODO kick off service/messagingcontroller actions 358 359 ContentValues cv = new ContentValues(); 360 cv.put(EmailContent.MessageColumns.FLAG_FAVORITE, isFavorite); 361 Uri uri = ContentUris.withAppendedId( 362 EmailContent.Message.SYNCED_CONTENT_URI, messageId); 363 mProviderContext.getContentResolver().update(uri, cv, null, null); 364 } 365 366 /** 367 * Request that an attachment be loaded 368 * 369 * @param save If true, attachment will be saved into a well-known place e.g. sdcard 370 * @param attachmentId the attachment to load 371 * @param messageId the owner message 372 * @param callback the Controller callback by which results will be reported 373 */ 374 public void loadAttachment(boolean save, long attachmentId, long messageId, 375 final Result callback, Object tag) { 376 377 Attachment attachInfo = Attachment.restoreAttachmentWithId(mProviderContext, attachmentId); 378 379 // Split here for target type (Service or MessagingController) 380 IEmailService service = getServiceForMessage(messageId); 381 if (service != null) { 382 // Service implementation 383 try { 384 service.loadAttachment(attachInfo.mId, null, 385 new LoadAttachmentCallback(callback, tag)); 386 } catch (RemoteException e) { 387 // TODO Change exception handling to be consistent with however this method 388 // is implemented for other protocols 389 Log.e("onDownloadAttachment", "RemoteException", e); 390 } 391 } else { 392 // MessagingController implementation 393 } 394 } 395 396 /** 397 * For a given message id, return a service proxy if applicable, or null. 398 * 399 * @param messageId the message of interest 400 * @result service proxy, or null if n/a 401 */ 402 private IEmailService getServiceForMessage(long messageId) { 403 // TODO make this more efficient, caching the account, smaller lookup here, etc. 404 Message message = Message.restoreMessageWithId(mProviderContext, messageId); 405 long accountId = message.mAccountKey; 406 Account account = EmailContent.Account.restoreAccountWithId(mProviderContext, accountId); 407 if (isMessagingController(account)) { 408 return null; 409 } else { 410 return new EmailServiceProxy(mContext, SyncManager.class); 411 } 412 } 413 414 /** 415 * Simple helper to determine if legacy MessagingController should be used 416 * 417 * TODO this should not require a full account, just an accountId 418 * TODO this should use a cache because we'll be doing this a lot 419 */ 420 private boolean isMessagingController(EmailContent.Account account) { 421 Store.StoreInfo info = 422 Store.StoreInfo.getStoreInfo(account.getStoreUri(mContext), mContext); 423 String scheme = info.mScheme; 424 425 return ("pop3".equals(scheme) || "imap".equals(scheme)); 426 } 427 428 /** 429 * Simple callback for synchronous commands. For many commands, this can be largely ignored 430 * and the result is observed via provider cursors. The callback will *not* necessarily be 431 * made from the UI thread, so you may need further handlers to safely make UI updates. 432 */ 433 public interface Result { 434 435 /** 436 * Callback for updateMailboxList 437 * 438 * @param result If null, the operation completed without error 439 * @param accountId The account being operated on 440 */ 441 public void updateMailboxListCallback(MessagingException result, long accountId); 442 443 /** 444 * Callback for updateMailbox 445 * 446 * @param result If null, the operation completed without error 447 * @param accountId The account being operated on 448 * @param mailboxId The mailbox being operated on 449 */ 450 public void updateMailboxCallback(MessagingException result, long accountId, 451 long mailboxId, int totalMessagesInMailbox, int numNewMessages); 452 453 /** 454 * Callback for loadAttachment 455 * 456 * @param result if null, the attachment completed - if non-null, terminating with failure 457 * @param messageId the message which contains the attachment 458 * @param attachmentId the attachment being loaded 459 * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete 460 * @param tag caller-defined tag, if supplied 461 */ 462 public void loadAttachmentCallback(MessagingException result, long messageId, 463 long attachmentId, int progress, Object tag); 464 } 465 466 /** 467 * Support for receiving callbacks from MessagingController and dealing with UI going 468 * out of scope. 469 */ 470 private class LegacyListener extends MessagingListener { 471 Result mResultCallback; 472 473 public LegacyListener(Result callback) { 474 mResultCallback = callback; 475 } 476 477 @Override 478 public void listFoldersFailed(EmailContent.Account account, String message) { 479 if (mResultCallback != null && isActiveResultCallback(mResultCallback)) { 480 mResultCallback.updateMailboxListCallback(new MessagingException(message), 481 account.mId); 482 } 483 mLegacyController.removeListener(this); 484 } 485 486 @Override 487 public void listFoldersFinished(EmailContent.Account account) { 488 if (mResultCallback != null && isActiveResultCallback(mResultCallback)) { 489 mResultCallback.updateMailboxListCallback(null, account.mId); 490 } 491 mLegacyController.removeListener(this); 492 } 493 494 @Override 495 public void synchronizeMailboxFinished(EmailContent.Account account, 496 EmailContent.Mailbox folder, int totalMessagesInMailbox, int numNewMessages) { 497 if (mResultCallback != null && isActiveResultCallback(mResultCallback)) { 498 mResultCallback.updateMailboxCallback(null, account.mId, folder.mId, 499 totalMessagesInMailbox, numNewMessages); 500 } 501 mLegacyController.removeListener(this); 502 } 503 504 @Override 505 public void synchronizeMailboxFailed(EmailContent.Account account, 506 EmailContent.Mailbox folder, Exception e) { 507 if (mResultCallback != null && isActiveResultCallback(mResultCallback)) { 508 MessagingException me; 509 if (e instanceof MessagingException) { 510 me = (MessagingException) e; 511 } else { 512 me = new MessagingException(e.toString()); 513 } 514 mResultCallback.updateMailboxCallback(me, account.mId, folder.mId, -1, -1); 515 } 516 mLegacyController.removeListener(this); 517 } 518 519 520 } 521 522 /** 523 * Service callback for load attachment 524 */ 525 private class LoadAttachmentCallback extends IEmailServiceCallback.Stub { 526 527 private final static boolean DEBUG_FAIL_DOWNLOADS = false; // do not check in "true" 528 529 Result mCallback; 530 boolean mMadeFirstCallback; 531 Object mTag; 532 533 public LoadAttachmentCallback(Result callback, Object tag) { 534 super(); 535 mCallback = callback; 536 mMadeFirstCallback = false; 537 mTag = tag; 538 } 539 540 /** 541 * Callback from Service for load attachment status. 542 * 543 * This performs some translations to what the UI expects, which is (assuming no fail): 544 * progress = 0 ("started") 545 * progress = 1..99 ("running") 546 * progress = 100 ("finished") 547 * 548 * @param messageId the id of the message the callback relates to 549 * @param attachmentId the id of the attachment (if any) 550 * @param statusCode from the definitions in EmailServiceStatus 551 * @param progress the progress (from 0 to 100) of a download 552 */ 553 public void status(long messageId, long attachmentId, int statusCode, int progress) { 554 if (mCallback != null && isActiveResultCallback(mCallback)) { 555 MessagingException result = null; 556 switch (statusCode) { 557 case EmailServiceStatus.SUCCESS: 558 progress = 100; 559 break; 560 case EmailServiceStatus.IN_PROGRESS: 561 // special case, force a single "progress = 0" for the first time 562 if (!mMadeFirstCallback) { 563 progress = 0; 564 mMadeFirstCallback = true; 565 } else if (DEBUG_FAIL_DOWNLOADS && progress > 75) { 566 result = new MessagingException( 567 String.valueOf(EmailServiceStatus.CONNECTION_ERROR)); 568 } else if (progress <= 0 || progress >= 100) { 569 return; 570 } 571 break; 572 default: 573 result = new MessagingException(String.valueOf(statusCode)); 574 break; 575 } 576 mCallback.loadAttachmentCallback(result, messageId, attachmentId, progress, mTag); 577 // prevent any trailing reports if there was an error 578 if (result != null) { 579 mCallback = null; 580 } 581 } 582 } 583 } 584} 585