Controller.java revision c6893ddf0fc1a647ca13a2b3aac2c68ca345de37
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.Mailbox; 23 24import android.content.ContentResolver; 25import android.content.ContentUris; 26import android.content.ContentValues; 27import android.content.Context; 28import android.database.Cursor; 29import android.net.Uri; 30import android.util.Log; 31 32import java.util.HashSet; 33 34/** 35 * New central controller/dispatcher for Email activities that may require remote operations. 36 * Handles disambiguating between legacy MessagingController operations and newer provider/sync 37 * based code. 38 */ 39public class Controller { 40 41 static Controller sInstance; 42 private Context mContext; 43 private Context mProviderContext; 44 private MessagingController mLegacyController; 45 private HashSet<Result> mListeners = new HashSet<Result>(); 46 47 private static String[] MESSAGEID_TO_ACCOUNTID_PROJECTION = new String[] { 48 EmailContent.RECORD_ID, 49 EmailContent.MessageColumns.ACCOUNT_KEY 50 }; 51 private static int MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID = 1; 52 53 protected Controller(Context _context) { 54 mContext = _context; 55 mProviderContext = _context; 56 mLegacyController = MessagingController.getInstance(mContext); 57 } 58 59 /** 60 * Gets or creates the singleton instance of Controller. 61 * @param _context The context that will be used for all underlying system access 62 */ 63 public synchronized static Controller getInstance(Context _context) { 64 if (sInstance == null) { 65 sInstance = new Controller(_context); 66 } 67 return sInstance; 68 } 69 70 /** 71 * For testing only: Inject a different context for provider access. This will be 72 * used internally for access the underlying provider (e.g. getContentResolver().query()). 73 * @param providerContext the provider context to be used by this instance 74 */ 75 public void setProviderContext(Context providerContext) { 76 mProviderContext = providerContext; 77 } 78 79 /** 80 * Any UI code that wishes for callback results (on async ops) should register their callback 81 * here (typically from onResume()). Unregistered callbacks will never be called, to prevent 82 * problems when the command completes and the activity has already paused or finished. 83 * @param listener The callback that may be used in action methods 84 */ 85 public void addResultCallback(Result listener) { 86 synchronized (mListeners) { 87 mListeners.add(listener); 88 } 89 } 90 91 /** 92 * Any UI code that no longer wishes for callback results (on async ops) should unregister 93 * their callback here (typically from onPause()). Unregistered callbacks will never be called, 94 * to prevent problems when the command completes and the activity has already paused or 95 * finished. 96 * @param listener The callback that may no longer be used 97 */ 98 public void removeResultCallback(Result listener) { 99 synchronized (mListeners) { 100 mListeners.remove(listener); 101 } 102 } 103 104 private boolean isActiveResultCallback(Result listener) { 105 synchronized (mListeners) { 106 return mListeners.contains(listener); 107 } 108 } 109 110 /** 111 * Request a remote update of mailboxes for an account. 112 * 113 * TODO: Implement (if any) for non-MessagingController 114 * TODO: Probably the right way is to create a fake "service" for MessagingController ops 115 */ 116 public void updateMailboxList(final EmailContent.Account account, final Result callback) { 117 118 // 1. determine if we can use MessagingController for this 119 boolean legacyController = isMessagingController(account); 120 121 // 2. if not...? 122 // TODO: for now, just pretend "it worked" 123 if (!legacyController) { 124 if (callback != null) { 125 callback.updateMailboxListCallback(null, account.mId); 126 } 127 return; 128 } 129 130 // 3. if so, make the call 131 new Thread() { 132 @Override 133 public void run() { 134 MessagingListener listener = new LegacyListener(callback); 135 mLegacyController.addListener(listener); 136 mLegacyController.listFolders(account, listener); 137 } 138 }.start(); 139 } 140 141 /** 142 * Request a remote update of a mailbox. 143 * 144 * The contract here should be to try and update the headers ASAP, in order to populate 145 * a simple message list. We should also at this point queue up a background task of 146 * downloading some/all of the messages in this mailbox, but that should be interruptable. 147 */ 148 public void updateMailbox(final EmailContent.Account account, 149 final EmailContent.Mailbox mailbox, final Result callback) { 150 151 // 1. determine if we can use MessagingController for this 152 boolean legacyController = isMessagingController(account); 153 154 // 2. if not...? 155 // TODO: for now, just pretend "it worked" 156 if (!legacyController) { 157 if (callback != null) { 158 callback.updateMailboxCallback(null, account.mId, mailbox.mId, -1, -1); 159 } 160 return; 161 } 162 163 // 3. if so, make the call 164 new Thread() { 165 @Override 166 public void run() { 167 MessagingListener listener = new LegacyListener(callback); 168 mLegacyController.addListener(listener); 169 mLegacyController.synchronizeMailbox(account, mailbox, listener); 170 } 171 }.start(); 172 } 173 174 /** 175 * Saves the message to a mailbox of given type. 176 * @param message the message (must have the mAccountId set). 177 * @param mailboxType the mailbox type (e.g. Mailbox.TYPE_DRAFTS). 178 * TODO: UI feedback. 179 * TODO: use AsyncTask instead of Thread 180 */ 181 public void saveToMailbox(final EmailContent.Message message, final int mailboxType) { 182 new Thread() { 183 @Override 184 public void run() { 185 long accountId = message.mAccountKey; 186 long mailboxId = findOrCreateMailboxOfType(accountId, mailboxType); 187 message.mMailboxKey = mailboxId; 188 message.save(mContext); 189 } 190 }.start(); 191 } 192 193 /** 194 * @param accountId the account id 195 * @param mailboxType the mailbox type (e.g. EmailContent.Mailbox.TYPE_TRASH) 196 * @return the id of the mailbox. The mailbox is created if not existing. 197 * Returns Mailbox.NO_MAILBOX if the accountId or mailboxType are negative. 198 * Does not validate the input in other ways (e.g. does not verify the existence of account). 199 */ 200 public long findOrCreateMailboxOfType(long accountId, int mailboxType) { 201 if (accountId < 0 || mailboxType < 0) { 202 return Mailbox.NO_MAILBOX; 203 } 204 long mailboxId = 205 Mailbox.findMailboxOfType(mProviderContext, accountId, mailboxType); 206 return mailboxId == Mailbox.NO_MAILBOX ? createMailbox(accountId, mailboxType) : mailboxId; 207 } 208 209 /** 210 * @param mailboxType the mailbox type 211 * @return the resource string corresponding to the mailbox type, empty if not found. 212 */ 213 /* package */ String getSpecialMailboxDisplayName(int mailboxType) { 214 int resId = -1; 215 switch (mailboxType) { 216 case Mailbox.TYPE_INBOX: 217 // TODO: there is no special_mailbox_display_name_inbox; why? 218 resId = R.string.special_mailbox_name_inbox; 219 break; 220 case Mailbox.TYPE_OUTBOX: 221 resId = R.string.special_mailbox_display_name_outbox; 222 break; 223 case Mailbox.TYPE_DRAFTS: 224 resId = R.string.special_mailbox_display_name_drafts; 225 break; 226 case Mailbox.TYPE_TRASH: 227 resId = R.string.special_mailbox_display_name_trash; 228 break; 229 case Mailbox.TYPE_SENT: 230 resId = R.string.special_mailbox_display_name_sent; 231 break; 232 } 233 return resId != -1 ? mContext.getString(resId) : ""; 234 } 235 236 /** 237 * Create a mailbox given the account and mailboxType. 238 * TODO: Does this need to be signaled explicitly to the sync engines? 239 * As this method is only used internally ('private'), it does not 240 * validate its inputs (accountId and mailboxType). 241 */ 242 /* package */ long createMailbox(long accountId, int mailboxType) { 243 if (accountId < 0 || mailboxType < 0) { 244 String mes = "Invalid arguments " + accountId + ' ' + mailboxType; 245 Log.e(Email.LOG_TAG, mes); 246 throw new RuntimeException(mes); 247 } 248 Mailbox box = new Mailbox(); 249 box.mAccountKey = accountId; 250 box.mType = mailboxType; 251 box.mSyncFrequency = EmailContent.Account.CHECK_INTERVAL_NEVER; 252 box.mFlagVisible = true; 253 box.mDisplayName = getSpecialMailboxDisplayName(mailboxType); 254 box.saveOrUpdate(mProviderContext); 255 return box.mId; 256 } 257 258 /** 259 * Delete a single message by moving it to the trash. 260 * 261 * This function has no callback, no result reporting, because the desired outcome 262 * is reflected entirely by changes to one or more cursors. 263 * 264 * @param messageId The id of the message to "delete". 265 * @param accountId The id of the message's account, or -1 if not known by caller 266 * 267 * TODO: Move out of UI thread 268 * TODO: "get account a for message m" should be a utility 269 * TODO: "get mailbox of type n for account a" should be a utility 270 */ 271 public void deleteMessage(long messageId, long accountId) { 272 ContentResolver resolver = mProviderContext.getContentResolver(); 273 274 // 1. Look up acct# for message we're deleting 275 Cursor c = null; 276 if (accountId == -1) { 277 try { 278 c = resolver.query(EmailContent.Message.CONTENT_URI, 279 MESSAGEID_TO_ACCOUNTID_PROJECTION, EmailContent.RECORD_ID + "=?", 280 new String[] { Long.toString(messageId) }, null); 281 if (c.moveToFirst()) { 282 accountId = c.getLong(MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID); 283 } else { 284 return; 285 } 286 } finally { 287 if (c != null) c.close(); 288 } 289 } 290 291 // 2. Confirm that there is a trash mailbox available 292 // 3. If there's no trash mailbox, create one 293 // TODO: Does this need to be signaled explicitly to the sync engines? 294 long trashMailboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_TRASH); 295 296 // 4. Change the mailbox key for the message we're "deleting" 297 ContentValues cv = new ContentValues(); 298 cv.put(EmailContent.MessageColumns.MAILBOX_KEY, trashMailboxId); 299 Uri uri = ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, messageId); 300 resolver.update(uri, cv, null, null); 301 302 // 5. Drop non-essential data for the message (e.g. attachments) 303 // TODO: find the actual files (if any, if loaded) & delete them 304 c = null; 305 try { 306 c = resolver.query(EmailContent.Attachment.CONTENT_URI, 307 EmailContent.Attachment.CONTENT_PROJECTION, 308 EmailContent.AttachmentColumns.MESSAGE_KEY + "=?", 309 new String[] { Long.toString(messageId) }, null); 310 while (c.moveToNext()) { 311 // delete any associated storage 312 // delete row? 313 } 314 } finally { 315 if (c != null) c.close(); 316 } 317 318 // 6. For IMAP/POP3 we may need to kick off an immediate delete (depends on acct settings) 319 // TODO write this 320 } 321 322 /** 323 * Simple helper to determine if legacy MessagingController should be used 324 */ 325 private boolean isMessagingController(EmailContent.Account account) { 326 Store.StoreInfo info = 327 Store.StoreInfo.getStoreInfo(account.getStoreUri(mContext), mContext); 328 String scheme = info.mScheme; 329 330 return ("pop3".equals(scheme) || "imap".equals(scheme)); 331 } 332 333 /** 334 * Simple callback for synchronous commands. For many commands, this can be largely ignored 335 * and the result is observed via provider cursors. The callback will *not* necessarily be 336 * made from the UI thread, so you may need further handlers to safely make UI updates. 337 */ 338 public interface Result { 339 340 /** 341 * Callback for updateMailboxList 342 * 343 * @param result If null, the operation completed without error 344 * @param accountKey The account being operated on 345 */ 346 public void updateMailboxListCallback(MessagingException result, long accountKey); 347 348 /** 349 * Callback for updateMailbox 350 * 351 * @param result If null, the operation completed without error 352 * @param accountKey The account being operated on 353 * @param mailboxKey The mailbox being operated on 354 */ 355 public void updateMailboxCallback(MessagingException result, long accountKey, 356 long mailboxKey, int totalMessagesInMailbox, int numNewMessages); 357 } 358 359 /** 360 * Support for receiving callbacks from MessagingController and dealing with UI going 361 * out of scope. 362 */ 363 private class LegacyListener extends MessagingListener { 364 Result mResultCallback; 365 366 public LegacyListener(Result callback) { 367 mResultCallback = callback; 368 } 369 370 @Override 371 public void listFoldersFailed(EmailContent.Account account, String message) { 372 if (mResultCallback != null && isActiveResultCallback(mResultCallback)) { 373 mResultCallback.updateMailboxListCallback(new MessagingException(message), 374 account.mId); 375 } 376 mLegacyController.removeListener(this); 377 } 378 379 @Override 380 public void listFoldersFinished(EmailContent.Account account) { 381 if (mResultCallback != null && isActiveResultCallback(mResultCallback)) { 382 mResultCallback.updateMailboxListCallback(null, account.mId); 383 } 384 mLegacyController.removeListener(this); 385 } 386 387 @Override 388 public void synchronizeMailboxFinished(EmailContent.Account account, 389 EmailContent.Mailbox folder, int totalMessagesInMailbox, int numNewMessages) { 390 if (mResultCallback != null && isActiveResultCallback(mResultCallback)) { 391 mResultCallback.updateMailboxCallback(null, account.mId, folder.mId, 392 totalMessagesInMailbox, numNewMessages); 393 } 394 mLegacyController.removeListener(this); 395 } 396 397 @Override 398 public void synchronizeMailboxFailed(EmailContent.Account account, 399 EmailContent.Mailbox folder, Exception e) { 400 if (mResultCallback != null && isActiveResultCallback(mResultCallback)) { 401 MessagingException me; 402 if (e instanceof MessagingException) { 403 me = (MessagingException) e; 404 } else { 405 me = new MessagingException(e.toString()); 406 } 407 mResultCallback.updateMailboxCallback(me, account.mId, folder.mId, -1, -1); 408 } 409 mLegacyController.removeListener(this); 410 } 411 412 413 } 414 415 416} 417