replicant-packages_apps_Email/src/com/android/email/Controller.java

1412 lines
57 KiB
Java

/*
* 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.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
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.provider.Utilities;
import com.android.email.service.EmailServiceUtils;
import com.android.emailcommon.Logging;
import com.android.emailcommon.mail.AuthenticationFailedException;
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.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.EmailServiceProxy;
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 Controller sInstance;
private final Context mContext;
private Context mProviderContext;
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 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;
}
/**
* 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;
}
/**
* 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
Utilities.copyOneMessageToProvider(mProviderContext, pop3Message, msg,
Message.FLAG_LOADED_COMPLETE);
// 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.
*/
@SuppressWarnings("deprecation")
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 {
throw new IllegalStateException("No service for updateMailboxList?");
}
}
});
}
/**
* 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 {
throw new IllegalStateException("No service for loadMessageForView?");
}
}
/**
* 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)
EmailServiceProxy service = getServiceForMessage(messageId);
if (service.isRemote()) {
// 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 remote service message.");
final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId);
synchronized (mListeners) {
for (Result listener : mListeners) {
listener.loadMessageForViewCallback(null, accountId, messageId, 100);
}
}
} else {
try {
service.loadMore(messageId);
} catch (RemoteException e) {
}
}
}
/**
* 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 sendPendingMessages(long accountId) {
EmailServiceProxy service =
EmailServiceUtils.getServiceForAccount(mContext, null, accountId);
try {
service.sendMail(accountId);
} catch (RemoteException e) {
}
}
/**
* Reset visible limits for all accounts.
* For each account:
* look up limit
* write limit into all mailboxes for that account
*/
@SuppressWarnings("deprecation")
public void resetVisibleLimits() {
Utility.runAsync(new Runnable() {
@Override
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() {
@Override
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() {
@Override
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() {
@Override
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);
}
}
/**
* 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() {
@Override
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);
}
}
}
});
}
/**
* 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);
}
/**
* 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() {
@Override
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;
}
private EmailServiceProxy getServiceForAccount(long accountId) {
return EmailServiceUtils.getServiceForAccount(mContext, mCallbackProxy, accountId);
}
/**
* 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 EmailServiceProxy 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);
}
/**
* 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);
} 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) {
}
}
/**
* Service callback for service operations
*/
private class ServiceCallback extends IEmailServiceCallback.Stub {
@Override
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:
// 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.
*/
@Override
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);
}
}
}
/**
* 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.
*/
@Override
public void loadMessageStatus(long messageId, 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.loadMessageForViewCallback(result, accountId, messageId, progress);
}
}
}
@Override
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);
}
}
}
@Override
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));
}
}
}
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();
}
}
@Override
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 syncMailboxListStatus(long accountId, int statusCode, int progress)
throws RemoteException {
}
@Override
public void syncMailboxStatus(long mailboxId, int statusCode, int progress)
throws RemoteException {
}
@Override
public void sendMessageStatus(long messageId, String subject, int statusCode, int progress)
throws RemoteException {
}
@Override
public void loadMessageStatus(long messageId, int statusCode, int progress)
throws RemoteException {
}
};
}