| /* |
| * Copyright (C) 2009 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.email; |
| |
| import android.app.Service; |
| import android.content.ContentResolver; |
| import android.content.ContentUris; |
| import android.content.ContentValues; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.database.Cursor; |
| import android.net.Uri; |
| import android.os.Bundle; |
| import android.os.IBinder; |
| import android.os.RemoteCallbackList; |
| import android.os.RemoteException; |
| import android.util.Log; |
| |
| import com.android.email.mail.store.Pop3Store.Pop3Message; |
| import com.android.email.provider.AccountBackupRestore; |
| import com.android.email.service.EmailServiceUtils; |
| import com.android.email.service.MailService; |
| import com.android.emailcommon.Api; |
| import com.android.emailcommon.Logging; |
| import com.android.emailcommon.mail.AuthenticationFailedException; |
| import com.android.emailcommon.mail.Folder.MessageRetrievalListener; |
| import com.android.emailcommon.mail.MessagingException; |
| import com.android.emailcommon.provider.Account; |
| import com.android.emailcommon.provider.EmailContent; |
| import com.android.emailcommon.provider.EmailContent.Attachment; |
| import com.android.emailcommon.provider.EmailContent.Body; |
| import com.android.emailcommon.provider.EmailContent.MailboxColumns; |
| import com.android.emailcommon.provider.EmailContent.Message; |
| import com.android.emailcommon.provider.EmailContent.MessageColumns; |
| import com.android.emailcommon.provider.HostAuth; |
| import com.android.emailcommon.provider.Mailbox; |
| import com.android.emailcommon.service.EmailServiceStatus; |
| import com.android.emailcommon.service.IEmailService; |
| import com.android.emailcommon.service.IEmailServiceCallback; |
| import com.android.emailcommon.service.SearchParams; |
| import com.android.emailcommon.utility.AttachmentUtilities; |
| import com.android.emailcommon.utility.EmailAsyncTask; |
| import com.android.emailcommon.utility.Utility; |
| import com.google.common.annotations.VisibleForTesting; |
| |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.concurrent.ConcurrentHashMap; |
| |
| /** |
| * New central controller/dispatcher for Email activities that may require remote operations. |
| * Handles disambiguating between legacy MessagingController operations and newer provider/sync |
| * based code. We implement Service to allow loadAttachment calls to be sent in a consistent manner |
| * to IMAP, POP3, and EAS by AttachmentDownloadService |
| */ |
| public class Controller { |
| private static final String TAG = "Controller"; |
| private static Controller sInstance; |
| private final Context mContext; |
| private Context mProviderContext; |
| private final MessagingController mLegacyController; |
| private final LegacyListener mLegacyListener = new LegacyListener(); |
| private final ServiceCallback mServiceCallback = new ServiceCallback(); |
| private final HashSet<Result> mListeners = new HashSet<Result>(); |
| /*package*/ final ConcurrentHashMap<Long, Boolean> mLegacyControllerMap = |
| new ConcurrentHashMap<Long, Boolean>(); |
| |
| // Note that 0 is a syntactically valid account key; however there can never be an account |
| // with id = 0, so attempts to restore the account will return null. Null values are |
| // handled properly within the code, so this won't cause any issues. |
| private static final long GLOBAL_MAILBOX_ACCOUNT_KEY = 0; |
| /*package*/ static final String ATTACHMENT_MAILBOX_SERVER_ID = "__attachment_mailbox__"; |
| /*package*/ static final String ATTACHMENT_MESSAGE_UID_PREFIX = "__attachment_message__"; |
| /*package*/ static final String SEARCH_MAILBOX_SERVER_ID = "__search_mailbox__"; |
| private static final String WHERE_TYPE_ATTACHMENT = |
| MailboxColumns.TYPE + "=" + Mailbox.TYPE_ATTACHMENT; |
| private static final String WHERE_MAILBOX_KEY = MessageColumns.MAILBOX_KEY + "=?"; |
| |
| private static final String[] MESSAGEID_TO_ACCOUNTID_PROJECTION = new String[] { |
| EmailContent.RECORD_ID, |
| EmailContent.MessageColumns.ACCOUNT_KEY |
| }; |
| private static final int MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID = 1; |
| |
| private static final String[] BODY_SOURCE_KEY_PROJECTION = |
| new String[] {Body.SOURCE_MESSAGE_KEY}; |
| private static final int BODY_SOURCE_KEY_COLUMN = 0; |
| private static final String WHERE_MESSAGE_KEY = Body.MESSAGE_KEY + "=?"; |
| |
| private static final String MAILBOXES_FOR_ACCOUNT_SELECTION = MailboxColumns.ACCOUNT_KEY + "=?"; |
| private static final String MAILBOXES_FOR_ACCOUNT_EXCEPT_ACCOUNT_MAILBOX_SELECTION = |
| MAILBOXES_FOR_ACCOUNT_SELECTION + " AND " + MailboxColumns.TYPE + "!=" + |
| Mailbox.TYPE_EAS_ACCOUNT_MAILBOX; |
| private static final String MESSAGES_FOR_ACCOUNT_SELECTION = MessageColumns.ACCOUNT_KEY + "=?"; |
| |
| // Service callbacks as set up via setCallback |
| private static RemoteCallbackList<IEmailServiceCallback> sCallbackList = |
| new RemoteCallbackList<IEmailServiceCallback>(); |
| |
| private volatile boolean mInUnitTests = false; |
| |
| protected Controller(Context _context) { |
| mContext = _context.getApplicationContext(); |
| mProviderContext = _context; |
| mLegacyController = MessagingController.getInstance(mProviderContext, this); |
| mLegacyController.addListener(mLegacyListener); |
| } |
| |
| /** |
| * Mark this controller as being in use in a unit test. |
| * This is a kludge vs having proper mocks and dependency injection; since the Controller is a |
| * global singleton there isn't much else we can do. |
| */ |
| public void markForTest(boolean inUnitTests) { |
| mInUnitTests = inUnitTests; |
| } |
| |
| /** |
| * Cleanup for test. Mustn't be called for the regular {@link Controller}, as it's a |
| * singleton and lives till the process finishes. |
| * |
| * <p>However, this method MUST be called for mock instances. |
| */ |
| public void cleanupForTest() { |
| mLegacyController.removeListener(mLegacyListener); |
| } |
| |
| /** |
| * Gets or creates the singleton instance of Controller. |
| */ |
| public synchronized static Controller getInstance(Context _context) { |
| if (sInstance == null) { |
| sInstance = new Controller(_context); |
| } |
| return sInstance; |
| } |
| |
| /** |
| * Inject a mock controller. Used only for testing. Affects future calls to getInstance(). |
| * |
| * Tests that use this method MUST clean it up by calling this method again with null. |
| */ |
| public synchronized static void injectMockControllerForTest(Controller mockController) { |
| sInstance = mockController; |
| } |
| |
| /** |
| * For testing only: Inject a different context for provider access. This will be |
| * used internally for access the underlying provider (e.g. getContentResolver().query()). |
| * @param providerContext the provider context to be used by this instance |
| */ |
| public void setProviderContext(Context providerContext) { |
| mProviderContext = providerContext; |
| } |
| |
| /** |
| * Any UI code that wishes for callback results (on async ops) should register their callback |
| * here (typically from onResume()). Unregistered callbacks will never be called, to prevent |
| * problems when the command completes and the activity has already paused or finished. |
| * @param listener The callback that may be used in action methods |
| */ |
| public void addResultCallback(Result listener) { |
| synchronized (mListeners) { |
| listener.setRegistered(true); |
| mListeners.add(listener); |
| } |
| } |
| |
| /** |
| * Any UI code that no longer wishes for callback results (on async ops) should unregister |
| * their callback here (typically from onPause()). Unregistered callbacks will never be called, |
| * to prevent problems when the command completes and the activity has already paused or |
| * finished. |
| * @param listener The callback that may no longer be used |
| */ |
| public void removeResultCallback(Result listener) { |
| synchronized (mListeners) { |
| listener.setRegistered(false); |
| mListeners.remove(listener); |
| } |
| } |
| |
| public Collection<Result> getResultCallbacksForTest() { |
| return mListeners; |
| } |
| |
| /** |
| * Delete all Messages that live in the attachment mailbox |
| */ |
| public void deleteAttachmentMessages() { |
| // Note: There should only be one attachment mailbox at present |
| ContentResolver resolver = mProviderContext.getContentResolver(); |
| Cursor c = null; |
| try { |
| c = resolver.query(Mailbox.CONTENT_URI, EmailContent.ID_PROJECTION, |
| WHERE_TYPE_ATTACHMENT, null, null); |
| while (c.moveToNext()) { |
| long mailboxId = c.getLong(EmailContent.ID_PROJECTION_COLUMN); |
| // Must delete attachments BEFORE messages |
| AttachmentUtilities.deleteAllMailboxAttachmentFiles(mProviderContext, 0, |
| mailboxId); |
| resolver.delete(Message.CONTENT_URI, WHERE_MAILBOX_KEY, |
| new String[] {Long.toString(mailboxId)}); |
| } |
| } finally { |
| if (c != null) { |
| c.close(); |
| } |
| } |
| } |
| |
| /** |
| * Get a mailbox based on a sqlite WHERE clause |
| */ |
| private Mailbox getGlobalMailboxWhere(String where) { |
| Cursor c = mProviderContext.getContentResolver().query(Mailbox.CONTENT_URI, |
| Mailbox.CONTENT_PROJECTION, where, null, null); |
| try { |
| if (c.moveToFirst()) { |
| Mailbox m = new Mailbox(); |
| m.restore(c); |
| return m; |
| } |
| } finally { |
| c.close(); |
| } |
| return null; |
| } |
| |
| /** |
| * Returns the attachment mailbox (where we store eml attachment Emails), creating one |
| * if necessary |
| * @return the global attachment mailbox |
| */ |
| public Mailbox getAttachmentMailbox() { |
| Mailbox m = getGlobalMailboxWhere(WHERE_TYPE_ATTACHMENT); |
| if (m == null) { |
| m = new Mailbox(); |
| m.mAccountKey = GLOBAL_MAILBOX_ACCOUNT_KEY; |
| m.mServerId = ATTACHMENT_MAILBOX_SERVER_ID; |
| m.mFlagVisible = false; |
| m.mDisplayName = ATTACHMENT_MAILBOX_SERVER_ID; |
| m.mSyncInterval = Mailbox.CHECK_INTERVAL_NEVER; |
| m.mType = Mailbox.TYPE_ATTACHMENT; |
| m.save(mProviderContext); |
| } |
| return m; |
| } |
| |
| /** |
| * Returns the search mailbox for the specified account, creating one if necessary |
| * @return the search mailbox for the passed in account |
| */ |
| public Mailbox getSearchMailbox(long accountId) { |
| Mailbox m = Mailbox.restoreMailboxOfType(mContext, accountId, Mailbox.TYPE_SEARCH); |
| if (m == null) { |
| m = new Mailbox(); |
| m.mAccountKey = accountId; |
| m.mServerId = SEARCH_MAILBOX_SERVER_ID; |
| m.mFlagVisible = false; |
| m.mDisplayName = SEARCH_MAILBOX_SERVER_ID; |
| m.mSyncInterval = Mailbox.CHECK_INTERVAL_NEVER; |
| m.mType = Mailbox.TYPE_SEARCH; |
| m.mFlags = Mailbox.FLAG_HOLDS_MAIL; |
| m.mParentKey = Mailbox.NO_MAILBOX; |
| m.save(mProviderContext); |
| } |
| return m; |
| } |
| |
| /** |
| * Create a Message from the Uri and store it in the attachment mailbox |
| * @param uri the uri containing message content |
| * @return the Message or null |
| */ |
| public Message loadMessageFromUri(Uri uri) { |
| Mailbox mailbox = getAttachmentMailbox(); |
| if (mailbox == null) return null; |
| try { |
| InputStream is = mProviderContext.getContentResolver().openInputStream(uri); |
| try { |
| // First, create a Pop3Message from the attachment and then parse it |
| Pop3Message pop3Message = new Pop3Message( |
| ATTACHMENT_MESSAGE_UID_PREFIX + System.currentTimeMillis(), null); |
| pop3Message.parse(is); |
| // Now, pull out the header fields |
| Message msg = new Message(); |
| LegacyConversions.updateMessageFields(msg, pop3Message, 0, mailbox.mId); |
| // Commit the message to the local store |
| msg.save(mProviderContext); |
| // Setup the rest of the message and mark it completely loaded |
| mLegacyController.copyOneMessageToProvider(pop3Message, msg, |
| Message.FLAG_LOADED_COMPLETE, mProviderContext); |
| // Restore the complete message and return it |
| return Message.restoreMessageWithId(mProviderContext, msg.mId); |
| } catch (MessagingException e) { |
| } catch (IOException e) { |
| } |
| } catch (FileNotFoundException e) { |
| } |
| return null; |
| } |
| |
| /** |
| * Set logging flags for external sync services |
| * |
| * Generally this should be called by anybody who changes Email.DEBUG |
| */ |
| public void serviceLogging(int debugFlags) { |
| IEmailService service = EmailServiceUtils.getExchangeService(mContext, mServiceCallback); |
| try { |
| service.setLogging(debugFlags); |
| } catch (RemoteException e) { |
| // TODO Change exception handling to be consistent with however this method |
| // is implemented for other protocols |
| Log.d("setLogging", "RemoteException" + e); |
| } |
| } |
| |
| /** |
| * Request a remote update of mailboxes for an account. |
| */ |
| public void updateMailboxList(final long accountId) { |
| Utility.runAsync(new Runnable() { |
| @Override |
| public void run() { |
| final IEmailService service = getServiceForAccount(accountId); |
| if (service != null) { |
| // Service implementation |
| try { |
| service.updateFolderList(accountId); |
| } catch (RemoteException e) { |
| // TODO Change exception handling to be consistent with however this method |
| // is implemented for other protocols |
| Log.d("updateMailboxList", "RemoteException" + e); |
| } |
| } else { |
| // MessagingController implementation |
| mLegacyController.listFolders(accountId, mLegacyListener); |
| } |
| } |
| }); |
| } |
| |
| /** |
| * Request a remote update of a mailbox. For use by the timed service. |
| * |
| * Functionally this is quite similar to updateMailbox(), but it's a separate API and |
| * separate callback in order to keep UI callbacks from affecting the service loop. |
| */ |
| public void serviceCheckMail(final long accountId, final long mailboxId, final long tag) { |
| IEmailService service = getServiceForAccount(accountId); |
| if (service != null) { |
| // Service implementation |
| // try { |
| // TODO this isn't quite going to work, because we're going to get the |
| // generic (UI) callbacks and not the ones we need to restart the ol' service. |
| // service.startSync(mailboxId, tag); |
| mLegacyListener.checkMailFinished(mContext, accountId, mailboxId, tag); |
| // } catch (RemoteException e) { |
| // TODO Change exception handling to be consistent with however this method |
| // is implemented for other protocols |
| // Log.d("updateMailbox", "RemoteException" + e); |
| // } |
| } else { |
| // MessagingController implementation |
| Utility.runAsync(new Runnable() { |
| public void run() { |
| mLegacyController.checkMail(accountId, tag, mLegacyListener); |
| } |
| }); |
| } |
| } |
| |
| /** |
| * Request a remote update of a mailbox. |
| * |
| * The contract here should be to try and update the headers ASAP, in order to populate |
| * a simple message list. We should also at this point queue up a background task of |
| * downloading some/all of the messages in this mailbox, but that should be interruptable. |
| */ |
| public void updateMailbox(final long accountId, final long mailboxId, boolean userRequest) { |
| |
| IEmailService service = getServiceForAccount(accountId); |
| if (service != null) { |
| try { |
| service.startSync(mailboxId, userRequest); |
| } catch (RemoteException e) { |
| // TODO Change exception handling to be consistent with however this method |
| // is implemented for other protocols |
| Log.d("updateMailbox", "RemoteException" + e); |
| } |
| } else { |
| // MessagingController implementation |
| Utility.runAsync(new Runnable() { |
| public void run() { |
| // TODO shouldn't be passing fully-build accounts & mailboxes into APIs |
| Account account = |
| Account.restoreAccountWithId(mProviderContext, accountId); |
| Mailbox mailbox = |
| Mailbox.restoreMailboxWithId(mProviderContext, mailboxId); |
| if (account == null || mailbox == null || |
| mailbox.mType == Mailbox.TYPE_SEARCH) { |
| return; |
| } |
| mLegacyController.synchronizeMailbox(account, mailbox, mLegacyListener); |
| } |
| }); |
| } |
| } |
| |
| /** |
| * Request that any final work necessary be done, to load a message. |
| * |
| * Note, this assumes that the caller has already checked message.mFlagLoaded and that |
| * additional work is needed. There is no optimization here for a message which is already |
| * loaded. |
| * |
| * @param messageId the message to load |
| * @param callback the Controller callback by which results will be reported |
| */ |
| public void loadMessageForView(final long messageId) { |
| |
| // Split here for target type (Service or MessagingController) |
| IEmailService service = getServiceForMessage(messageId); |
| if (service != null) { |
| // There is no service implementation, so we'll just jam the value, log the error, |
| // and get out of here. |
| Uri uri = ContentUris.withAppendedId(Message.CONTENT_URI, messageId); |
| ContentValues cv = new ContentValues(); |
| cv.put(MessageColumns.FLAG_LOADED, Message.FLAG_LOADED_COMPLETE); |
| mProviderContext.getContentResolver().update(uri, cv, null, null); |
| Log.d(Logging.LOG_TAG, "Unexpected loadMessageForView() for service-based message."); |
| final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId); |
| synchronized (mListeners) { |
| for (Result listener : mListeners) { |
| listener.loadMessageForViewCallback(null, accountId, messageId, 100); |
| } |
| } |
| } else { |
| // MessagingController implementation |
| Utility.runAsync(new Runnable() { |
| public void run() { |
| mLegacyController.loadMessageForView(messageId, mLegacyListener); |
| } |
| }); |
| } |
| } |
| |
| |
| /** |
| * Saves the message to a mailbox of given type. |
| * This is a synchronous operation taking place in the same thread as the caller. |
| * Upon return the message.mId is set. |
| * @param message the message (must have the mAccountId set). |
| * @param mailboxType the mailbox type (e.g. Mailbox.TYPE_DRAFTS). |
| */ |
| public void saveToMailbox(final EmailContent.Message message, final int mailboxType) { |
| long accountId = message.mAccountKey; |
| long mailboxId = findOrCreateMailboxOfType(accountId, mailboxType); |
| message.mMailboxKey = mailboxId; |
| message.save(mProviderContext); |
| } |
| |
| /** |
| * Look for a specific system mailbox, creating it if necessary, and return the mailbox id. |
| * This is a blocking operation and should not be called from the UI thread. |
| * |
| * Synchronized so multiple threads can call it (and not risk creating duplicate boxes). |
| * |
| * @param accountId the account id |
| * @param mailboxType the mailbox type (e.g. EmailContent.Mailbox.TYPE_TRASH) |
| * @return the id of the mailbox. The mailbox is created if not existing. |
| * Returns Mailbox.NO_MAILBOX if the accountId or mailboxType are negative. |
| * Does not validate the input in other ways (e.g. does not verify the existence of account). |
| */ |
| public synchronized long findOrCreateMailboxOfType(long accountId, int mailboxType) { |
| if (accountId < 0 || mailboxType < 0) { |
| return Mailbox.NO_MAILBOX; |
| } |
| long mailboxId = |
| Mailbox.findMailboxOfType(mProviderContext, accountId, mailboxType); |
| return mailboxId == Mailbox.NO_MAILBOX ? createMailbox(accountId, mailboxType) : mailboxId; |
| } |
| |
| /** |
| * Returns the server-side name for a specific mailbox. |
| * |
| * @return the resource string corresponding to the mailbox type, empty if not found. |
| */ |
| public static String getMailboxServerName(Context context, int mailboxType) { |
| int resId = -1; |
| switch (mailboxType) { |
| case Mailbox.TYPE_INBOX: |
| resId = R.string.mailbox_name_server_inbox; |
| break; |
| case Mailbox.TYPE_OUTBOX: |
| resId = R.string.mailbox_name_server_outbox; |
| break; |
| case Mailbox.TYPE_DRAFTS: |
| resId = R.string.mailbox_name_server_drafts; |
| break; |
| case Mailbox.TYPE_TRASH: |
| resId = R.string.mailbox_name_server_trash; |
| break; |
| case Mailbox.TYPE_SENT: |
| resId = R.string.mailbox_name_server_sent; |
| break; |
| case Mailbox.TYPE_JUNK: |
| resId = R.string.mailbox_name_server_junk; |
| break; |
| } |
| return resId != -1 ? context.getString(resId) : ""; |
| } |
| |
| /** |
| * Create a mailbox given the account and mailboxType. |
| * TODO: Does this need to be signaled explicitly to the sync engines? |
| */ |
| @VisibleForTesting |
| long createMailbox(long accountId, int mailboxType) { |
| if (accountId < 0 || mailboxType < 0) { |
| String mes = "Invalid arguments " + accountId + ' ' + mailboxType; |
| Log.e(Logging.LOG_TAG, mes); |
| throw new RuntimeException(mes); |
| } |
| Mailbox box = Mailbox.newSystemMailbox( |
| accountId, mailboxType, getMailboxServerName(mContext, mailboxType)); |
| box.save(mProviderContext); |
| return box.mId; |
| } |
| |
| /** |
| * Send a message: |
| * - move the message to Outbox (the message is assumed to be in Drafts). |
| * - EAS service will take it from there |
| * - mark reply/forward state in source message (if any) |
| * - trigger send for POP/IMAP |
| * @param message the fully populated Message (usually retrieved from the Draft box). Note that |
| * all transient fields (e.g. Body related fields) are also expected to be fully loaded |
| */ |
| public void sendMessage(Message message) { |
| ContentResolver resolver = mProviderContext.getContentResolver(); |
| long accountId = message.mAccountKey; |
| long messageId = message.mId; |
| if (accountId == Account.NO_ACCOUNT) { |
| accountId = lookupAccountForMessage(messageId); |
| } |
| if (accountId == Account.NO_ACCOUNT) { |
| // probably the message was not found |
| if (Logging.LOGD) { |
| Email.log("no account found for message " + messageId); |
| } |
| return; |
| } |
| |
| // Move to Outbox |
| long outboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_OUTBOX); |
| ContentValues cv = new ContentValues(); |
| cv.put(EmailContent.MessageColumns.MAILBOX_KEY, outboxId); |
| |
| // does this need to be SYNCED_CONTENT_URI instead? |
| Uri uri = ContentUris.withAppendedId(Message.CONTENT_URI, messageId); |
| resolver.update(uri, cv, null, null); |
| |
| // If this is a reply/forward, indicate it as such on the source. |
| long sourceKey = message.mSourceKey; |
| if (sourceKey != Message.NO_MESSAGE) { |
| boolean isReply = (message.mFlags & Message.FLAG_TYPE_REPLY) != 0; |
| int flagUpdate = isReply ? Message.FLAG_REPLIED_TO : Message.FLAG_FORWARDED; |
| setMessageAnsweredOrForwarded(sourceKey, flagUpdate); |
| } |
| |
| sendPendingMessages(accountId); |
| } |
| |
| private void sendPendingMessagesSmtp(long accountId) { |
| // for IMAP & POP only, (attempt to) send the message now |
| final Account account = |
| Account.restoreAccountWithId(mProviderContext, accountId); |
| if (account == null) { |
| return; |
| } |
| final long sentboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_SENT); |
| Utility.runAsync(new Runnable() { |
| public void run() { |
| mLegacyController.sendPendingMessages(account, sentboxId, mLegacyListener); |
| } |
| }); |
| } |
| |
| /** |
| * Try to send all pending messages for a given account |
| * |
| * @param accountId the account for which to send messages |
| */ |
| public void sendPendingMessages(long accountId) { |
| // 1. make sure we even have an outbox, exit early if not |
| final long outboxId = |
| Mailbox.findMailboxOfType(mProviderContext, accountId, Mailbox.TYPE_OUTBOX); |
| if (outboxId == Mailbox.NO_MAILBOX) { |
| return; |
| } |
| |
| // 2. dispatch as necessary |
| IEmailService service = getServiceForAccount(accountId); |
| if (service != null) { |
| // Service implementation |
| try { |
| service.startSync(outboxId, false); |
| } catch (RemoteException e) { |
| // TODO Change exception handling to be consistent with however this method |
| // is implemented for other protocols |
| Log.d("updateMailbox", "RemoteException" + e); |
| } |
| } else { |
| // MessagingController implementation |
| sendPendingMessagesSmtp(accountId); |
| } |
| } |
| |
| /** |
| * Reset visible limits for all accounts. |
| * For each account: |
| * look up limit |
| * write limit into all mailboxes for that account |
| */ |
| public void resetVisibleLimits() { |
| Utility.runAsync(new Runnable() { |
| public void run() { |
| ContentResolver resolver = mProviderContext.getContentResolver(); |
| Cursor c = null; |
| try { |
| c = resolver.query( |
| Account.CONTENT_URI, |
| Account.ID_PROJECTION, |
| null, null, null); |
| while (c.moveToNext()) { |
| long accountId = c.getLong(Account.ID_PROJECTION_COLUMN); |
| String protocol = Account.getProtocol(mProviderContext, accountId); |
| if (!HostAuth.SCHEME_EAS.equals(protocol)) { |
| ContentValues cv = new ContentValues(); |
| cv.put(MailboxColumns.VISIBLE_LIMIT, Email.VISIBLE_LIMIT_DEFAULT); |
| resolver.update(Mailbox.CONTENT_URI, cv, |
| MailboxColumns.ACCOUNT_KEY + "=?", |
| new String[] { Long.toString(accountId) }); |
| } |
| } |
| } finally { |
| if (c != null) { |
| c.close(); |
| } |
| } |
| } |
| }); |
| } |
| |
| /** |
| * Increase the load count for a given mailbox, and trigger a refresh. Applies only to |
| * IMAP and POP mailboxes, with the exception of the EAS search mailbox. |
| * |
| * @param mailboxId the mailbox |
| */ |
| public void loadMoreMessages(final long mailboxId) { |
| EmailAsyncTask.runAsyncParallel(new Runnable() { |
| public void run() { |
| Mailbox mailbox = Mailbox.restoreMailboxWithId(mProviderContext, mailboxId); |
| if (mailbox == null) { |
| return; |
| } |
| if (mailbox.mType == Mailbox.TYPE_SEARCH) { |
| try { |
| searchMore(mailbox.mAccountKey); |
| } catch (MessagingException e) { |
| // Nothing to be done |
| } |
| return; |
| } |
| Account account = Account.restoreAccountWithId(mProviderContext, |
| mailbox.mAccountKey); |
| if (account == null) { |
| return; |
| } |
| // Use provider math to increment the field |
| ContentValues cv = new ContentValues();; |
| cv.put(EmailContent.FIELD_COLUMN_NAME, MailboxColumns.VISIBLE_LIMIT); |
| cv.put(EmailContent.ADD_COLUMN_NAME, Email.VISIBLE_LIMIT_INCREMENT); |
| Uri uri = ContentUris.withAppendedId(Mailbox.ADD_TO_FIELD_URI, mailboxId); |
| mProviderContext.getContentResolver().update(uri, cv, null, null); |
| // Trigger a refresh using the new, longer limit |
| mailbox.mVisibleLimit += Email.VISIBLE_LIMIT_INCREMENT; |
| mLegacyController.synchronizeMailbox(account, mailbox, mLegacyListener); |
| } |
| }); |
| } |
| |
| /** |
| * @param messageId the id of message |
| * @return the accountId corresponding to the given messageId, or -1 if not found. |
| */ |
| private long lookupAccountForMessage(long messageId) { |
| ContentResolver resolver = mProviderContext.getContentResolver(); |
| Cursor c = resolver.query(EmailContent.Message.CONTENT_URI, |
| MESSAGEID_TO_ACCOUNTID_PROJECTION, EmailContent.RECORD_ID + "=?", |
| new String[] { Long.toString(messageId) }, null); |
| try { |
| return c.moveToFirst() |
| ? c.getLong(MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID) |
| : -1; |
| } finally { |
| c.close(); |
| } |
| } |
| |
| /** |
| * Delete a single attachment entry from the DB given its id. |
| * Does not delete any eventual associated files. |
| */ |
| public void deleteAttachment(long attachmentId) { |
| ContentResolver resolver = mProviderContext.getContentResolver(); |
| Uri uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, attachmentId); |
| resolver.delete(uri, null, null); |
| } |
| |
| /** |
| * Async version of {@link #deleteMessageSync}. |
| */ |
| public void deleteMessage(final long messageId) { |
| EmailAsyncTask.runAsyncParallel(new Runnable() { |
| public void run() { |
| deleteMessageSync(messageId); |
| } |
| }); |
| } |
| |
| /** |
| * Batch & async version of {@link #deleteMessageSync}. |
| */ |
| public void deleteMessages(final long[] messageIds) { |
| if (messageIds == null || messageIds.length == 0) { |
| throw new IllegalArgumentException(); |
| } |
| EmailAsyncTask.runAsyncParallel(new Runnable() { |
| public void run() { |
| for (long messageId: messageIds) { |
| deleteMessageSync(messageId); |
| } |
| } |
| }); |
| } |
| |
| /** |
| * Delete a single message by moving it to the trash, or really delete it if it's already in |
| * trash or a draft message. |
| * |
| * This function has no callback, no result reporting, because the desired outcome |
| * is reflected entirely by changes to one or more cursors. |
| * |
| * @param messageId The id of the message to "delete". |
| */ |
| /* package */ void deleteMessageSync(long messageId) { |
| // 1. Get the message's account |
| Account account = Account.getAccountForMessageId(mProviderContext, messageId); |
| |
| if (account == null) return; |
| |
| // 2. Confirm that there is a trash mailbox available. If not, create one |
| long trashMailboxId = findOrCreateMailboxOfType(account.mId, Mailbox.TYPE_TRASH); |
| |
| // 3. Get the message's original mailbox |
| Mailbox mailbox = Mailbox.getMailboxForMessageId(mProviderContext, messageId); |
| |
| if (mailbox == null) return; |
| |
| // 4. Drop non-essential data for the message (e.g. attachment files) |
| AttachmentUtilities.deleteAllAttachmentFiles(mProviderContext, account.mId, |
| messageId); |
| |
| Uri uri = ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, |
| messageId); |
| ContentResolver resolver = mProviderContext.getContentResolver(); |
| |
| // 5. Perform "delete" as appropriate |
| if ((mailbox.mId == trashMailboxId) || (mailbox.mType == Mailbox.TYPE_DRAFTS)) { |
| // 5a. Really delete it |
| resolver.delete(uri, null, null); |
| } else { |
| // 5b. Move to trash |
| ContentValues cv = new ContentValues(); |
| cv.put(EmailContent.MessageColumns.MAILBOX_KEY, trashMailboxId); |
| resolver.update(uri, cv, null, null); |
| } |
| |
| if (isMessagingController(account)) { |
| mLegacyController.processPendingActions(account.mId); |
| } |
| } |
| |
| /** |
| * Moves messages to a new mailbox. |
| * |
| * This function has no callback, no result reporting, because the desired outcome |
| * is reflected entirely by changes to one or more cursors. |
| * |
| * Note this method assumes all of the given message and mailbox IDs belong to the same |
| * account. |
| * |
| * @param messageIds IDs of the messages that are to be moved |
| * @param newMailboxId ID of the new mailbox that the messages will be moved to |
| * @return an asynchronous task that executes the move (for testing only) |
| */ |
| public EmailAsyncTask<Void, Void, Void> moveMessages(final long[] messageIds, |
| final long newMailboxId) { |
| if (messageIds == null || messageIds.length == 0) { |
| throw new IllegalArgumentException(); |
| } |
| return EmailAsyncTask.runAsyncParallel(new Runnable() { |
| public void run() { |
| Account account = Account.getAccountForMessageId(mProviderContext, messageIds[0]); |
| if (account != null) { |
| ContentValues cv = new ContentValues(); |
| cv.put(EmailContent.MessageColumns.MAILBOX_KEY, newMailboxId); |
| ContentResolver resolver = mProviderContext.getContentResolver(); |
| for (long messageId : messageIds) { |
| Uri uri = ContentUris.withAppendedId( |
| EmailContent.Message.SYNCED_CONTENT_URI, messageId); |
| resolver.update(uri, cv, null, null); |
| } |
| if (isMessagingController(account)) { |
| mLegacyController.processPendingActions(account.mId); |
| } |
| } |
| } |
| }); |
| } |
| |
| /** |
| * Set/clear the unread status of a message |
| * |
| * @param messageId the message to update |
| * @param isRead the new value for the isRead flag |
| */ |
| public void setMessageReadSync(long messageId, boolean isRead) { |
| setMessageBooleanSync(messageId, EmailContent.MessageColumns.FLAG_READ, isRead); |
| } |
| |
| /** |
| * Set/clear the unread status of a message from UI thread |
| * |
| * @param messageId the message to update |
| * @param isRead the new value for the isRead flag |
| * @return the EmailAsyncTask created |
| */ |
| public EmailAsyncTask<Void, Void, Void> setMessageRead(final long messageId, |
| final boolean isRead) { |
| return EmailAsyncTask.runAsyncParallel(new Runnable() { |
| @Override |
| public void run() { |
| setMessageBooleanSync(messageId, EmailContent.MessageColumns.FLAG_READ, isRead); |
| }}); |
| } |
| |
| /** |
| * Update a message record and ping MessagingController, if necessary |
| * |
| * @param messageId the message to update |
| * @param cv the ContentValues used in the update |
| */ |
| private void updateMessageSync(long messageId, ContentValues cv) { |
| Uri uri = ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, messageId); |
| mProviderContext.getContentResolver().update(uri, cv, null, null); |
| |
| // Service runs automatically, MessagingController needs a kick |
| long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId); |
| if (accountId == Account.NO_ACCOUNT) return; |
| if (isMessagingController(accountId)) { |
| mLegacyController.processPendingActions(accountId); |
| } |
| } |
| |
| /** |
| * Set the answered status of a message |
| * |
| * @param messageId the message to update |
| * @return the AsyncTask that will execute the changes (for testing only) |
| */ |
| public void setMessageAnsweredOrForwarded(final long messageId, |
| final int flag) { |
| EmailAsyncTask.runAsyncParallel(new Runnable() { |
| public void run() { |
| Message msg = Message.restoreMessageWithId(mProviderContext, messageId); |
| if (msg == null) { |
| Log.w(Logging.LOG_TAG, "Unable to find source message for a reply/forward"); |
| return; |
| } |
| ContentValues cv = new ContentValues(); |
| cv.put(MessageColumns.FLAGS, msg.mFlags | flag); |
| updateMessageSync(messageId, cv); |
| } |
| }); |
| } |
| |
| /** |
| * Set/clear the favorite status of a message from UI thread |
| * |
| * @param messageId the message to update |
| * @param isFavorite the new value for the isFavorite flag |
| * @return the EmailAsyncTask created |
| */ |
| public EmailAsyncTask<Void, Void, Void> setMessageFavorite(final long messageId, |
| final boolean isFavorite) { |
| return EmailAsyncTask.runAsyncParallel(new Runnable() { |
| @Override |
| public void run() { |
| setMessageBooleanSync(messageId, EmailContent.MessageColumns.FLAG_FAVORITE, |
| isFavorite); |
| }}); |
| } |
| /** |
| * Set/clear the favorite status of a message |
| * |
| * @param messageId the message to update |
| * @param isFavorite the new value for the isFavorite flag |
| */ |
| public void setMessageFavoriteSync(long messageId, boolean isFavorite) { |
| setMessageBooleanSync(messageId, EmailContent.MessageColumns.FLAG_FAVORITE, isFavorite); |
| } |
| |
| /** |
| * Set/clear boolean columns of a message |
| * |
| * @param messageId the message to update |
| * @param columnName the column to update |
| * @param columnValue the new value for the column |
| */ |
| private void setMessageBooleanSync(long messageId, String columnName, boolean columnValue) { |
| ContentValues cv = new ContentValues(); |
| cv.put(columnName, columnValue); |
| updateMessageSync(messageId, cv); |
| } |
| |
| |
| private static final HashMap<Long, SearchParams> sSearchParamsMap = |
| new HashMap<Long, SearchParams>(); |
| |
| public void searchMore(long accountId) throws MessagingException { |
| SearchParams params = sSearchParamsMap.get(accountId); |
| if (params == null) return; |
| params.mOffset += params.mLimit; |
| searchMessages(accountId, params); |
| } |
| |
| /** |
| * Search for messages on the (IMAP) server; do not call this on the UI thread! |
| * @param accountId the id of the account to be searched |
| * @param searchParams the parameters for this search |
| * @throws MessagingException |
| */ |
| public int searchMessages(final long accountId, final SearchParams searchParams) |
| throws MessagingException { |
| // Find/create our search mailbox |
| Mailbox searchMailbox = getSearchMailbox(accountId); |
| if (searchMailbox == null) return 0; |
| final long searchMailboxId = searchMailbox.mId; |
| // Save this away (per account) |
| sSearchParamsMap.put(accountId, searchParams); |
| |
| if (searchParams.mOffset == 0) { |
| // Delete existing contents of search mailbox |
| ContentResolver resolver = mContext.getContentResolver(); |
| resolver.delete(Message.CONTENT_URI, Message.MAILBOX_KEY + "=" + searchMailboxId, |
| null); |
| ContentValues cv = new ContentValues(); |
| // For now, use the actual query as the name of the mailbox |
| cv.put(Mailbox.DISPLAY_NAME, searchParams.mFilter); |
| resolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, searchMailboxId), |
| cv, null, null); |
| } |
| |
| IEmailService service = getServiceForAccount(accountId); |
| if (service != null) { |
| // Service implementation |
| try { |
| return service.searchMessages(accountId, searchParams, searchMailboxId); |
| } catch (RemoteException e) { |
| // TODO Change exception handling to be consistent with however this method |
| // is implemented for other protocols |
| Log.e("searchMessages", "RemoteException", e); |
| return 0; |
| } |
| } else { |
| // This is the actual mailbox we'll be searching |
| Mailbox actualMailbox = Mailbox.restoreMailboxWithId(mContext, searchParams.mMailboxId); |
| if (actualMailbox == null) { |
| Log.e(Logging.LOG_TAG, "Unable to find mailbox " + searchParams.mMailboxId |
| + " to search in with " + searchParams); |
| return 0; |
| } |
| // Do the search |
| if (Email.DEBUG) { |
| Log.d(Logging.LOG_TAG, "Search: " + searchParams.mFilter); |
| } |
| return mLegacyController.searchMailbox(accountId, searchParams, searchMailboxId); |
| } |
| } |
| |
| /** |
| * Respond to a meeting invitation. |
| * |
| * @param messageId the id of the invitation being responded to |
| * @param response the code representing the response to the invitation |
| */ |
| public void sendMeetingResponse(final long messageId, final int response) { |
| // Split here for target type (Service or MessagingController) |
| IEmailService service = getServiceForMessage(messageId); |
| if (service != null) { |
| // Service implementation |
| try { |
| service.sendMeetingResponse(messageId, response); |
| } catch (RemoteException e) { |
| // TODO Change exception handling to be consistent with however this method |
| // is implemented for other protocols |
| Log.e("onDownloadAttachment", "RemoteException", e); |
| } |
| } |
| } |
| |
| /** |
| * Request that an attachment be loaded. It will be stored at a location controlled |
| * by the AttachmentProvider. |
| * |
| * @param attachmentId the attachment to load |
| * @param messageId the owner message |
| * @param accountId the owner account |
| */ |
| public void loadAttachment(final long attachmentId, final long messageId, |
| final long accountId) { |
| Attachment attachInfo = Attachment.restoreAttachmentWithId(mProviderContext, attachmentId); |
| if (attachInfo == null) { |
| return; |
| } |
| |
| if (Utility.attachmentExists(mProviderContext, attachInfo)) { |
| // The attachment has already been downloaded, so we will just "pretend" to download it |
| // This presumably is for POP3 messages |
| synchronized (mListeners) { |
| for (Result listener : mListeners) { |
| listener.loadAttachmentCallback(null, accountId, messageId, attachmentId, 0); |
| } |
| for (Result listener : mListeners) { |
| listener.loadAttachmentCallback(null, accountId, messageId, attachmentId, 100); |
| } |
| } |
| return; |
| } |
| |
| // Flag the attachment as needing download at the user's request |
| ContentValues cv = new ContentValues(); |
| cv.put(Attachment.FLAGS, attachInfo.mFlags | Attachment.FLAG_DOWNLOAD_USER_REQUEST); |
| attachInfo.update(mProviderContext, cv); |
| } |
| |
| /** |
| * For a given message id, return a service proxy if applicable, or null. |
| * |
| * @param messageId the message of interest |
| * @result service proxy, or null if n/a |
| */ |
| private IEmailService getServiceForMessage(long messageId) { |
| // TODO make this more efficient, caching the account, smaller lookup here, etc. |
| Message message = Message.restoreMessageWithId(mProviderContext, messageId); |
| if (message == null) { |
| return null; |
| } |
| return getServiceForAccount(message.mAccountKey); |
| } |
| |
| /** |
| * For a given account id, return a service proxy if applicable, or null. |
| * |
| * @param accountId the message of interest |
| * @result service proxy, or null if n/a |
| */ |
| private IEmailService getServiceForAccount(long accountId) { |
| if (isMessagingController(accountId)) return null; |
| return getExchangeEmailService(); |
| } |
| |
| private IEmailService getExchangeEmailService() { |
| return EmailServiceUtils.getExchangeService(mContext, mServiceCallback); |
| } |
| |
| /** |
| * Simple helper to determine if legacy MessagingController should be used |
| */ |
| public boolean isMessagingController(Account account) { |
| if (account == null) return false; |
| return isMessagingController(account.mId); |
| } |
| |
| public boolean isMessagingController(long accountId) { |
| Boolean isLegacyController = mLegacyControllerMap.get(accountId); |
| if (isLegacyController == null) { |
| String protocol = Account.getProtocol(mProviderContext, accountId); |
| isLegacyController = ("pop3".equals(protocol) || "imap".equals(protocol)); |
| mLegacyControllerMap.put(accountId, isLegacyController); |
| } |
| return isLegacyController; |
| } |
| |
| /** |
| * Delete an account. |
| */ |
| public void deleteAccount(final long accountId) { |
| EmailAsyncTask.runAsyncParallel(new Runnable() { |
| @Override |
| public void run() { |
| deleteAccountSync(accountId, mProviderContext); |
| } |
| }); |
| } |
| |
| /** |
| * Delete an account synchronously. |
| */ |
| public void deleteAccountSync(long accountId, Context context) { |
| try { |
| mLegacyControllerMap.remove(accountId); |
| // Get the account URI. |
| final Account account = Account.restoreAccountWithId(context, accountId); |
| if (account == null) { |
| return; // Already deleted? |
| } |
| |
| // Delete account data, attachments, PIM data, etc. |
| deleteSyncedDataSync(accountId); |
| |
| // Now delete the account itself |
| Uri uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId); |
| context.getContentResolver().delete(uri, null, null); |
| |
| // For unit tests, don't run backup, security, and ui pieces. |
| if (mInUnitTests) { |
| return; |
| } |
| |
| // Clean up |
| AccountBackupRestore.backup(context); |
| SecurityPolicy.getInstance(context).reducePolicies(); |
| Email.setServicesEnabledSync(context); |
| Email.setNotifyUiAccountsChanged(true); |
| MailService.actionReschedule(context); |
| } catch (Exception e) { |
| Log.w(Logging.LOG_TAG, "Exception while deleting account", e); |
| } |
| } |
| |
| /** |
| * Delete all synced data, but don't delete the actual account. This is used when security |
| * policy requirements are not met, and we don't want to reveal any synced data, but we do |
| * wish to keep the account configured (e.g. to accept remote wipe commands). |
| * |
| * The only mailbox not deleted is the account mailbox (if any) |
| * Also, clear the sync keys on the remaining account, since the data is gone. |
| * |
| * SYNCHRONOUS - do not call from UI thread. |
| * |
| * @param accountId The account to wipe. |
| */ |
| public void deleteSyncedDataSync(long accountId) { |
| try { |
| // Delete synced attachments |
| AttachmentUtilities.deleteAllAccountAttachmentFiles(mProviderContext, |
| accountId); |
| |
| // Delete synced email, leaving only an empty inbox. We do this in two phases: |
| // 1. Delete all non-inbox mailboxes (which will delete all of their messages) |
| // 2. Delete all remaining messages (which will be the inbox messages) |
| ContentResolver resolver = mProviderContext.getContentResolver(); |
| String[] accountIdArgs = new String[] { Long.toString(accountId) }; |
| resolver.delete(Mailbox.CONTENT_URI, |
| MAILBOXES_FOR_ACCOUNT_EXCEPT_ACCOUNT_MAILBOX_SELECTION, |
| accountIdArgs); |
| resolver.delete(Message.CONTENT_URI, MESSAGES_FOR_ACCOUNT_SELECTION, accountIdArgs); |
| |
| // Delete sync keys on remaining items |
| ContentValues cv = new ContentValues(); |
| cv.putNull(Account.SYNC_KEY); |
| resolver.update(Account.CONTENT_URI, cv, Account.ID_SELECTION, accountIdArgs); |
| cv.clear(); |
| cv.putNull(Mailbox.SYNC_KEY); |
| resolver.update(Mailbox.CONTENT_URI, cv, |
| MAILBOXES_FOR_ACCOUNT_SELECTION, accountIdArgs); |
| |
| // Delete PIM data (contacts, calendar), stop syncs, etc. if applicable |
| IEmailService service = getServiceForAccount(accountId); |
| if (service != null) { |
| service.deleteAccountPIMData(accountId); |
| } |
| } catch (Exception e) { |
| Log.w(Logging.LOG_TAG, "Exception while deleting account synced data", e); |
| } |
| } |
| |
| /** |
| * Simple callback for synchronous commands. For many commands, this can be largely ignored |
| * and the result is observed via provider cursors. The callback will *not* necessarily be |
| * made from the UI thread, so you may need further handlers to safely make UI updates. |
| */ |
| public static abstract class Result { |
| private volatile boolean mRegistered; |
| |
| protected void setRegistered(boolean registered) { |
| mRegistered = registered; |
| } |
| |
| protected final boolean isRegistered() { |
| return mRegistered; |
| } |
| |
| /** |
| * Callback for updateMailboxList |
| * |
| * @param result If null, the operation completed without error |
| * @param accountId The account being operated on |
| * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete |
| */ |
| public void updateMailboxListCallback(MessagingException result, long accountId, |
| int progress) { |
| } |
| |
| /** |
| * Callback for updateMailbox. Note: This looks a lot like checkMailCallback, but |
| * it's a separate call used only by UI's, so we can keep things separate. |
| * |
| * @param result If null, the operation completed without error |
| * @param accountId The account being operated on |
| * @param mailboxId The mailbox being operated on |
| * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete |
| * @param numNewMessages the number of new messages delivered |
| */ |
| public void updateMailboxCallback(MessagingException result, long accountId, |
| long mailboxId, int progress, int numNewMessages, ArrayList<Long> addedMessages) { |
| } |
| |
| /** |
| * Callback for loadMessageForView |
| * |
| * @param result if null, the attachment completed - if non-null, terminating with failure |
| * @param messageId the message which contains the attachment |
| * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete |
| */ |
| public void loadMessageForViewCallback(MessagingException result, long accountId, |
| long messageId, int progress) { |
| } |
| |
| /** |
| * Callback for loadAttachment |
| * |
| * @param result if null, the attachment completed - if non-null, terminating with failure |
| * @param messageId the message which contains the attachment |
| * @param attachmentId the attachment being loaded |
| * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete |
| */ |
| public void loadAttachmentCallback(MessagingException result, long accountId, |
| long messageId, long attachmentId, int progress) { |
| } |
| |
| /** |
| * Callback for checkmail. Note: This looks a lot like updateMailboxCallback, but |
| * it's a separate call used only by the automatic checker service, so we can keep |
| * things separate. |
| * |
| * @param result If null, the operation completed without error |
| * @param accountId The account being operated on |
| * @param mailboxId The mailbox being operated on (may be unknown at start) |
| * @param progress 0 for "starting", no updates, 100 for complete |
| * @param tag the same tag that was passed to serviceCheckMail() |
| */ |
| public void serviceCheckMailCallback(MessagingException result, long accountId, |
| long mailboxId, int progress, long tag) { |
| } |
| |
| /** |
| * Callback for sending pending messages. This will be called once to start the |
| * group, multiple times for messages, and once to complete the group. |
| * |
| * Unfortunately this callback works differently on SMTP and EAS. |
| * |
| * On SMTP: |
| * |
| * First, we get this. |
| * result == null, messageId == -1, progress == 0: start batch send |
| * |
| * Then we get these callbacks per message. |
| * (Exchange backend may skip "start sending one message".) |
| * result == null, messageId == xx, progress == 0: start sending one message |
| * result == xxxx, messageId == xx, progress == 0; failed sending one message |
| * |
| * Finally we get this. |
| * result == null, messageId == -1, progres == 100; finish sending batch |
| * |
| * On EAS: Almost same as above, except: |
| * |
| * - There's no first ("start batch send") callback. |
| * - accountId is always -1. |
| * |
| * @param result If null, the operation completed without error |
| * @param accountId The account being operated on |
| * @param messageId The being sent (may be unknown at start) |
| * @param progress 0 for "starting", 100 for complete |
| */ |
| public void sendMailCallback(MessagingException result, long accountId, |
| long messageId, int progress) { |
| } |
| } |
| |
| /** |
| * Bridge to intercept {@link MessageRetrievalListener#loadAttachmentProgress} and |
| * pass down to {@link Result}. |
| */ |
| public class MessageRetrievalListenerBridge implements MessageRetrievalListener { |
| private final long mMessageId; |
| private final long mAttachmentId; |
| private final long mAccountId; |
| |
| public MessageRetrievalListenerBridge(long messageId, long attachmentId) { |
| mMessageId = messageId; |
| mAttachmentId = attachmentId; |
| mAccountId = Account.getAccountIdForMessageId(mProviderContext, mMessageId); |
| } |
| |
| @Override |
| public void loadAttachmentProgress(int progress) { |
| synchronized (mListeners) { |
| for (Result listener : mListeners) { |
| listener.loadAttachmentCallback(null, mAccountId, mMessageId, mAttachmentId, |
| progress); |
| } |
| } |
| } |
| |
| @Override |
| public void messageRetrieved(com.android.emailcommon.mail.Message message) { |
| } |
| } |
| |
| /** |
| * Support for receiving callbacks from MessagingController and dealing with UI going |
| * out of scope. |
| */ |
| public class LegacyListener extends MessagingListener { |
| public LegacyListener() { |
| } |
| |
| @Override |
| public void listFoldersStarted(long accountId) { |
| synchronized (mListeners) { |
| for (Result l : mListeners) { |
| l.updateMailboxListCallback(null, accountId, 0); |
| } |
| } |
| } |
| |
| @Override |
| public void listFoldersFailed(long accountId, String message) { |
| synchronized (mListeners) { |
| for (Result l : mListeners) { |
| l.updateMailboxListCallback(new MessagingException(message), accountId, 0); |
| } |
| } |
| } |
| |
| @Override |
| public void listFoldersFinished(long accountId) { |
| synchronized (mListeners) { |
| for (Result l : mListeners) { |
| l.updateMailboxListCallback(null, accountId, 100); |
| } |
| } |
| } |
| |
| @Override |
| public void synchronizeMailboxStarted(long accountId, long mailboxId) { |
| synchronized (mListeners) { |
| for (Result l : mListeners) { |
| l.updateMailboxCallback(null, accountId, mailboxId, 0, 0, null); |
| } |
| } |
| } |
| |
| @Override |
| public void synchronizeMailboxFinished(long accountId, long mailboxId, |
| int totalMessagesInMailbox, int numNewMessages, ArrayList<Long> addedMessages) { |
| synchronized (mListeners) { |
| for (Result l : mListeners) { |
| l.updateMailboxCallback(null, accountId, mailboxId, 100, numNewMessages, |
| addedMessages); |
| } |
| } |
| } |
| |
| @Override |
| public void synchronizeMailboxFailed(long accountId, long mailboxId, Exception e) { |
| MessagingException me; |
| if (e instanceof MessagingException) { |
| me = (MessagingException) e; |
| } else { |
| me = new MessagingException(e.toString()); |
| } |
| synchronized (mListeners) { |
| for (Result l : mListeners) { |
| l.updateMailboxCallback(me, accountId, mailboxId, 0, 0, null); |
| } |
| } |
| } |
| |
| @Override |
| public void checkMailStarted(Context context, long accountId, long tag) { |
| synchronized (mListeners) { |
| for (Result l : mListeners) { |
| l.serviceCheckMailCallback(null, accountId, -1, 0, tag); |
| } |
| } |
| } |
| |
| @Override |
| public void checkMailFinished(Context context, long accountId, long folderId, long tag) { |
| synchronized (mListeners) { |
| for (Result l : mListeners) { |
| l.serviceCheckMailCallback(null, accountId, folderId, 100, tag); |
| } |
| } |
| } |
| |
| @Override |
| public void loadMessageForViewStarted(long messageId) { |
| final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId); |
| synchronized (mListeners) { |
| for (Result listener : mListeners) { |
| listener.loadMessageForViewCallback(null, accountId, messageId, 0); |
| } |
| } |
| } |
| |
| @Override |
| public void loadMessageForViewFinished(long messageId) { |
| final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId); |
| synchronized (mListeners) { |
| for (Result listener : mListeners) { |
| listener.loadMessageForViewCallback(null, accountId, messageId, 100); |
| } |
| } |
| } |
| |
| @Override |
| public void loadMessageForViewFailed(long messageId, String message) { |
| final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId); |
| synchronized (mListeners) { |
| for (Result listener : mListeners) { |
| listener.loadMessageForViewCallback(new MessagingException(message), |
| accountId, messageId, 0); |
| } |
| } |
| } |
| |
| @Override |
| public void loadAttachmentStarted(long accountId, long messageId, long attachmentId, |
| boolean requiresDownload) { |
| try { |
| mCallbackProxy.loadAttachmentStatus(messageId, attachmentId, |
| EmailServiceStatus.IN_PROGRESS, 0); |
| } catch (RemoteException e) { |
| } |
| synchronized (mListeners) { |
| for (Result listener : mListeners) { |
| listener.loadAttachmentCallback(null, accountId, messageId, attachmentId, 0); |
| } |
| } |
| } |
| |
| @Override |
| public void loadAttachmentFinished(long accountId, long messageId, long attachmentId) { |
| try { |
| mCallbackProxy.loadAttachmentStatus(messageId, attachmentId, |
| EmailServiceStatus.SUCCESS, 100); |
| } catch (RemoteException e) { |
| } |
| synchronized (mListeners) { |
| for (Result listener : mListeners) { |
| listener.loadAttachmentCallback(null, accountId, messageId, attachmentId, 100); |
| } |
| } |
| } |
| |
| @Override |
| public void loadAttachmentFailed(long accountId, long messageId, long attachmentId, |
| MessagingException me, boolean background) { |
| try { |
| // If the cause of the MessagingException is an IOException, we send a status of |
| // CONNECTION_ERROR; in this case, AttachmentDownloadService will try again to |
| // download the attachment. Otherwise, the error is considered non-recoverable. |
| int status = EmailServiceStatus.ATTACHMENT_NOT_FOUND; |
| if (me != null && me.getCause() instanceof IOException) { |
| status = EmailServiceStatus.CONNECTION_ERROR; |
| } |
| mCallbackProxy.loadAttachmentStatus(messageId, attachmentId, status, 0); |
| } catch (RemoteException e) { |
| } |
| synchronized (mListeners) { |
| for (Result listener : mListeners) { |
| // TODO We are overloading the exception here. The UI listens for this |
| // callback and displays a toast if the exception is not null. Since we |
| // want to avoid displaying toast for background operations, we force |
| // the exception to be null. This needs to be re-worked so the UI will |
| // only receive (or at least pays attention to) responses for requests |
| // it explicitly cares about. Then we would not need to overload the |
| // exception parameter. |
| listener.loadAttachmentCallback(background ? null : me, accountId, messageId, |
| attachmentId, 0); |
| } |
| } |
| } |
| |
| @Override |
| synchronized public void sendPendingMessagesStarted(long accountId, long messageId) { |
| synchronized (mListeners) { |
| for (Result listener : mListeners) { |
| listener.sendMailCallback(null, accountId, messageId, 0); |
| } |
| } |
| } |
| |
| @Override |
| synchronized public void sendPendingMessagesCompleted(long accountId) { |
| synchronized (mListeners) { |
| for (Result listener : mListeners) { |
| listener.sendMailCallback(null, accountId, -1, 100); |
| } |
| } |
| } |
| |
| @Override |
| synchronized public void sendPendingMessagesFailed(long accountId, long messageId, |
| Exception reason) { |
| MessagingException me; |
| if (reason instanceof MessagingException) { |
| me = (MessagingException) reason; |
| } else { |
| me = new MessagingException(reason.toString()); |
| } |
| synchronized (mListeners) { |
| for (Result listener : mListeners) { |
| listener.sendMailCallback(me, accountId, messageId, 0); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Service callback for service operations |
| */ |
| private class ServiceCallback extends IEmailServiceCallback.Stub { |
| |
| private final static boolean DEBUG_FAIL_DOWNLOADS = false; // do not check in "true" |
| |
| public void loadAttachmentStatus(long messageId, long attachmentId, int statusCode, |
| int progress) { |
| MessagingException result = mapStatusToException(statusCode); |
| switch (statusCode) { |
| case EmailServiceStatus.SUCCESS: |
| progress = 100; |
| break; |
| case EmailServiceStatus.IN_PROGRESS: |
| if (DEBUG_FAIL_DOWNLOADS && progress > 75) { |
| result = new MessagingException( |
| String.valueOf(EmailServiceStatus.CONNECTION_ERROR)); |
| } |
| // discard progress reports that look like sentinels |
| if (progress < 0 || progress >= 100) { |
| return; |
| } |
| break; |
| } |
| final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId); |
| synchronized (mListeners) { |
| for (Result listener : mListeners) { |
| listener.loadAttachmentCallback(result, accountId, messageId, attachmentId, |
| progress); |
| } |
| } |
| } |
| |
| /** |
| * Note, this is an incomplete implementation of this callback, because we are |
| * not getting things back from Service in quite the same way as from MessagingController. |
| * However, this is sufficient for basic "progress=100" notification that message send |
| * has just completed. |
| */ |
| public void sendMessageStatus(long messageId, String subject, int statusCode, |
| int progress) { |
| long accountId = -1; // This should be in the callback |
| MessagingException result = mapStatusToException(statusCode); |
| switch (statusCode) { |
| case EmailServiceStatus.SUCCESS: |
| progress = 100; |
| break; |
| case EmailServiceStatus.IN_PROGRESS: |
| // discard progress reports that look like sentinels |
| if (progress < 0 || progress >= 100) { |
| return; |
| } |
| break; |
| } |
| synchronized(mListeners) { |
| for (Result listener : mListeners) { |
| listener.sendMailCallback(result, accountId, messageId, progress); |
| } |
| } |
| } |
| |
| public void syncMailboxListStatus(long accountId, int statusCode, int progress) { |
| MessagingException result = mapStatusToException(statusCode); |
| switch (statusCode) { |
| case EmailServiceStatus.SUCCESS: |
| progress = 100; |
| break; |
| case EmailServiceStatus.IN_PROGRESS: |
| // discard progress reports that look like sentinels |
| if (progress < 0 || progress >= 100) { |
| return; |
| } |
| break; |
| } |
| synchronized(mListeners) { |
| for (Result listener : mListeners) { |
| listener.updateMailboxListCallback(result, accountId, progress); |
| } |
| } |
| } |
| |
| public void syncMailboxStatus(long mailboxId, int statusCode, int progress) { |
| MessagingException result = mapStatusToException(statusCode); |
| switch (statusCode) { |
| case EmailServiceStatus.SUCCESS: |
| progress = 100; |
| break; |
| case EmailServiceStatus.IN_PROGRESS: |
| // discard progress reports that look like sentinels |
| if (progress < 0 || progress >= 100) { |
| return; |
| } |
| break; |
| } |
| // TODO should pass this back instead of looking it up here |
| Mailbox mbx = Mailbox.restoreMailboxWithId(mProviderContext, mailboxId); |
| // The mailbox could have disappeared if the server commanded it |
| if (mbx == null) return; |
| long accountId = mbx.mAccountKey; |
| synchronized(mListeners) { |
| for (Result listener : mListeners) { |
| listener.updateMailboxCallback(result, accountId, mailboxId, progress, 0, null); |
| } |
| } |
| } |
| |
| private MessagingException mapStatusToException(int statusCode) { |
| switch (statusCode) { |
| case EmailServiceStatus.SUCCESS: |
| case EmailServiceStatus.IN_PROGRESS: |
| // Don't generate error if the account is uninitialized |
| case EmailServiceStatus.ACCOUNT_UNINITIALIZED: |
| return null; |
| |
| case EmailServiceStatus.LOGIN_FAILED: |
| return new AuthenticationFailedException(""); |
| |
| case EmailServiceStatus.CONNECTION_ERROR: |
| return new MessagingException(MessagingException.IOERROR); |
| |
| case EmailServiceStatus.SECURITY_FAILURE: |
| return new MessagingException(MessagingException.SECURITY_POLICIES_REQUIRED); |
| |
| case EmailServiceStatus.ACCESS_DENIED: |
| return new MessagingException(MessagingException.ACCESS_DENIED); |
| |
| case EmailServiceStatus.ATTACHMENT_NOT_FOUND: |
| return new MessagingException(MessagingException.ATTACHMENT_NOT_FOUND); |
| |
| case EmailServiceStatus.CLIENT_CERTIFICATE_ERROR: |
| return new MessagingException(MessagingException.CLIENT_CERTIFICATE_ERROR); |
| |
| case EmailServiceStatus.MESSAGE_NOT_FOUND: |
| case EmailServiceStatus.FOLDER_NOT_DELETED: |
| case EmailServiceStatus.FOLDER_NOT_RENAMED: |
| case EmailServiceStatus.FOLDER_NOT_CREATED: |
| case EmailServiceStatus.REMOTE_EXCEPTION: |
| // TODO: define exception code(s) & UI string(s) for server-side errors |
| default: |
| return new MessagingException(String.valueOf(statusCode)); |
| } |
| } |
| |
| @Override |
| public void loadMessageStatus(long messageId, int statusCode, int progress) |
| throws RemoteException { |
| } |
| } |
| |
| private interface ServiceCallbackWrapper { |
| public void call(IEmailServiceCallback cb) throws RemoteException; |
| } |
| |
| /** |
| * Proxy that can be used to broadcast service callbacks; we currently use this only for |
| * loadAttachment callbacks |
| */ |
| private final IEmailServiceCallback.Stub mCallbackProxy = |
| new IEmailServiceCallback.Stub() { |
| |
| /** |
| * Broadcast a callback to the everyone that's registered |
| * |
| * @param wrapper the ServiceCallbackWrapper used in the broadcast |
| */ |
| private synchronized void broadcastCallback(ServiceCallbackWrapper wrapper) { |
| if (sCallbackList != null) { |
| // Call everyone on our callback list |
| // Exceptions can be safely ignored |
| int count = sCallbackList.beginBroadcast(); |
| for (int i = 0; i < count; i++) { |
| try { |
| wrapper.call(sCallbackList.getBroadcastItem(i)); |
| } catch (RemoteException e) { |
| } |
| } |
| sCallbackList.finishBroadcast(); |
| } |
| } |
| |
| public void loadAttachmentStatus(final long messageId, final long attachmentId, |
| final int status, final int progress) { |
| broadcastCallback(new ServiceCallbackWrapper() { |
| @Override |
| public void call(IEmailServiceCallback cb) throws RemoteException { |
| cb.loadAttachmentStatus(messageId, attachmentId, status, progress); |
| } |
| }); |
| } |
| |
| @Override |
| public void sendMessageStatus(long messageId, String subject, int statusCode, int progress){ |
| } |
| |
| @Override |
| public void syncMailboxListStatus(long accountId, int statusCode, int progress) { |
| } |
| |
| @Override |
| public void syncMailboxStatus(long mailboxId, int statusCode, int progress) { |
| } |
| |
| @Override |
| public void loadMessageStatus(long messageId, int statusCode, int progress) |
| throws RemoteException { |
| } |
| }; |
| |
| public static class ControllerService extends Service { |
| /** |
| * Create our EmailService implementation here. For now, only loadAttachment is supported; |
| * the intention, however, is to move more functionality to the service interface |
| */ |
| private final IEmailService.Stub mBinder = new IEmailService.Stub() { |
| |
| public Bundle validate(HostAuth hostAuth) { |
| return null; |
| } |
| |
| public Bundle autoDiscover(String userName, String password) { |
| return null; |
| } |
| |
| public void startSync(long mailboxId, boolean userRequest) { |
| } |
| |
| public void stopSync(long mailboxId) { |
| } |
| |
| public void loadAttachment(long attachmentId, boolean background) |
| throws RemoteException { |
| Attachment att = Attachment.restoreAttachmentWithId(ControllerService.this, |
| attachmentId); |
| if (att != null) { |
| if (Email.DEBUG) { |
| Log.d(TAG, "loadAttachment " + attachmentId + ": " + att.mFileName); |
| } |
| Message msg = Message.restoreMessageWithId(ControllerService.this, |
| att.mMessageKey); |
| if (msg != null) { |
| // If the message is a forward and the attachment needs downloading, we need |
| // to retrieve the message from the source, rather than from the message |
| // itself |
| if ((msg.mFlags & Message.FLAG_TYPE_FORWARD) != 0) { |
| String[] cols = Utility.getRowColumns(ControllerService.this, |
| Body.CONTENT_URI, BODY_SOURCE_KEY_PROJECTION, WHERE_MESSAGE_KEY, |
| new String[] {Long.toString(msg.mId)}); |
| if (cols != null) { |
| msg = Message.restoreMessageWithId(ControllerService.this, |
| Long.parseLong(cols[BODY_SOURCE_KEY_COLUMN])); |
| if (msg == null) { |
| // TODO: We can try restoring from the deleted table here... |
| return; |
| } |
| } |
| } |
| MessagingController legacyController = sInstance.mLegacyController; |
| LegacyListener legacyListener = sInstance.mLegacyListener; |
| legacyController.loadAttachment(msg.mAccountKey, msg.mId, msg.mMailboxKey, |
| attachmentId, legacyListener, background); |
| } else { |
| // Send back the specific error status for this case |
| sInstance.mCallbackProxy.loadAttachmentStatus(att.mMessageKey, attachmentId, |
| EmailServiceStatus.MESSAGE_NOT_FOUND, 0); |
| } |
| } |
| } |
| |
| public void updateFolderList(long accountId) { |
| } |
| |
| public void hostChanged(long accountId) { |
| } |
| |
| public void setLogging(int flags) { |
| } |
| |
| public void sendMeetingResponse(long messageId, int response) { |
| } |
| |
| public void loadMore(long messageId) { |
| } |
| |
| // The following three methods are not implemented in this version |
| public boolean createFolder(long accountId, String name) { |
| return false; |
| } |
| |
| public boolean deleteFolder(long accountId, String name) { |
| return false; |
| } |
| |
| public boolean renameFolder(long accountId, String oldName, String newName) { |
| return false; |
| } |
| |
| public void setCallback(IEmailServiceCallback cb) { |
| sCallbackList.register(cb); |
| } |
| |
| public void deleteAccountPIMData(long accountId) { |
| } |
| |
| public int searchMessages(long accountId, SearchParams searchParams, |
| long destMailboxId) { |
| return 0; |
| } |
| |
| @Override |
| public int getApiLevel() { |
| return Api.LEVEL; |
| } |
| |
| @Override |
| public void sendMail(long accountId) throws RemoteException { |
| } |
| }; |
| |
| @Override |
| public IBinder onBind(Intent intent) { |
| return mBinder; |
| } |
| } |
| } |