blob: 63d8faeda156e5d942db9af84dfe2dc48fc6a7ef [file] [log] [blame]
/*
* 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;
}
}
}