1202 lines
49 KiB
Java
1202 lines
49 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 com.android.email.mail.AuthenticationFailedException;
|
|
import com.android.email.mail.MessagingException;
|
|
import com.android.email.mail.Store;
|
|
import com.android.email.provider.AttachmentProvider;
|
|
import com.android.email.provider.EmailContent;
|
|
import com.android.email.provider.EmailContent.Account;
|
|
import com.android.email.provider.EmailContent.Attachment;
|
|
import com.android.email.provider.EmailContent.Mailbox;
|
|
import com.android.email.provider.EmailContent.MailboxColumns;
|
|
import com.android.email.provider.EmailContent.Message;
|
|
import com.android.email.provider.EmailContent.MessageColumns;
|
|
import com.android.email.service.EmailServiceStatus;
|
|
import com.android.email.service.IEmailService;
|
|
import com.android.email.service.IEmailServiceCallback;
|
|
|
|
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.RemoteException;
|
|
import android.util.Log;
|
|
|
|
import java.io.File;
|
|
import java.util.HashSet;
|
|
|
|
/**
|
|
* New central controller/dispatcher for Email activities that may require remote operations.
|
|
* Handles disambiguating between legacy MessagingController operations and newer provider/sync
|
|
* based code.
|
|
*/
|
|
public class Controller {
|
|
|
|
static Controller sInstance;
|
|
private Context mContext;
|
|
private Context mProviderContext;
|
|
private MessagingController mLegacyController;
|
|
private LegacyListener mLegacyListener = new LegacyListener();
|
|
private ServiceCallback mServiceCallback = new ServiceCallback();
|
|
private HashSet<Result> mListeners = new HashSet<Result>();
|
|
|
|
private static String[] MESSAGEID_TO_ACCOUNTID_PROJECTION = new String[] {
|
|
EmailContent.RECORD_ID,
|
|
EmailContent.MessageColumns.ACCOUNT_KEY
|
|
};
|
|
private static int MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID = 1;
|
|
|
|
private static String[] MESSAGEID_TO_MAILBOXID_PROJECTION = new String[] {
|
|
EmailContent.RECORD_ID,
|
|
EmailContent.MessageColumns.MAILBOX_KEY
|
|
};
|
|
private static int MESSAGEID_TO_MAILBOXID_COLUMN_MAILBOXID = 1;
|
|
|
|
protected Controller(Context _context) {
|
|
mContext = _context;
|
|
mProviderContext = _context;
|
|
mLegacyController = MessagingController.getInstance(mContext);
|
|
mLegacyController.addListener(mLegacyListener);
|
|
}
|
|
|
|
/**
|
|
* Gets or creates the singleton instance of Controller.
|
|
* @param _context The context that will be used for all underlying system access
|
|
*/
|
|
public synchronized static Controller getInstance(Context _context) {
|
|
if (sInstance == null) {
|
|
sInstance = new Controller(_context);
|
|
}
|
|
return sInstance;
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
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) {
|
|
mListeners.remove(listener);
|
|
}
|
|
}
|
|
|
|
private boolean isActiveResultCallback(Result listener) {
|
|
synchronized (mListeners) {
|
|
return mListeners.contains(listener);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Enable/disable logging for external sync services
|
|
*
|
|
* Generally this should be called by anybody who changes Email.DEBUG
|
|
*/
|
|
public void serviceLogging(int debugEnabled) {
|
|
IEmailService service = ExchangeUtils.getExchangeEmailService(mContext, mServiceCallback);
|
|
try {
|
|
service.setLogging(debugEnabled);
|
|
} catch (RemoteException e) {
|
|
// TODO Change exception handling to be consistent with however this method
|
|
// is implemented for other protocols
|
|
Log.d("updateMailboxList", "RemoteException" + e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Request a remote update of mailboxes for an account.
|
|
*
|
|
* TODO: Clean up threading in MessagingController cases (or perhaps here in Controller)
|
|
*/
|
|
public void updateMailboxList(final long accountId, final Result callback) {
|
|
|
|
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
|
|
new Thread() {
|
|
@Override
|
|
public void run() {
|
|
mLegacyController.listFolders(accountId, mLegacyListener);
|
|
}
|
|
}.start();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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,
|
|
final Result callback) {
|
|
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);
|
|
callback.serviceCheckMailCallback(null, accountId, mailboxId, 100, 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
|
|
new Thread() {
|
|
@Override
|
|
public void run() {
|
|
mLegacyController.checkMail(accountId, tag, mLegacyListener);
|
|
}
|
|
}.start();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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, final Result callback) {
|
|
|
|
IEmailService service = getServiceForAccount(accountId);
|
|
if (service != null) {
|
|
// Service implementation
|
|
try {
|
|
service.startSync(mailboxId);
|
|
} 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
|
|
new Thread() {
|
|
@Override
|
|
public void run() {
|
|
// TODO shouldn't be passing fully-build accounts & mailboxes into APIs
|
|
Account account =
|
|
EmailContent.Account.restoreAccountWithId(mProviderContext, accountId);
|
|
Mailbox mailbox =
|
|
EmailContent.Mailbox.restoreMailboxWithId(mProviderContext, mailboxId);
|
|
mLegacyController.synchronizeMailbox(account, mailbox, mLegacyListener);
|
|
}
|
|
}.start();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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, final Result callback) {
|
|
|
|
// 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(Email.LOG_TAG, "Unexpected loadMessageForView() for service-based message.");
|
|
synchronized (mListeners) {
|
|
for (Result listener : mListeners) {
|
|
listener.loadMessageForViewCallback(null, messageId, 100);
|
|
}
|
|
}
|
|
} else {
|
|
// MessagingController implementation
|
|
new Thread() {
|
|
@Override
|
|
public void run() {
|
|
mLegacyController.loadMessageForView(messageId, mLegacyListener);
|
|
}
|
|
}.start();
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
|
|
/**
|
|
* @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 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.
|
|
*
|
|
* @param mailboxType the mailbox type
|
|
* @return the resource string corresponding to the mailbox type, empty if not found.
|
|
*/
|
|
/* package */ String getMailboxServerName(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 ? mContext.getString(resId) : "";
|
|
}
|
|
|
|
/**
|
|
* Create a mailbox given the account and mailboxType.
|
|
* TODO: Does this need to be signaled explicitly to the sync engines?
|
|
*/
|
|
/* package */ long createMailbox(long accountId, int mailboxType) {
|
|
if (accountId < 0 || mailboxType < 0) {
|
|
String mes = "Invalid arguments " + accountId + ' ' + mailboxType;
|
|
Log.e(Email.LOG_TAG, mes);
|
|
throw new RuntimeException(mes);
|
|
}
|
|
Mailbox box = new Mailbox();
|
|
box.mAccountKey = accountId;
|
|
box.mType = mailboxType;
|
|
box.mSyncInterval = EmailContent.Account.CHECK_INTERVAL_NEVER;
|
|
box.mFlagVisible = true;
|
|
box.mDisplayName = getMailboxServerName(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
|
|
* - trigger send for POP/IMAP
|
|
* @param messageId the id of the message to send
|
|
*/
|
|
public void sendMessage(long messageId, long accountId) {
|
|
ContentResolver resolver = mProviderContext.getContentResolver();
|
|
if (accountId == -1) {
|
|
accountId = lookupAccountForMessage(messageId);
|
|
}
|
|
if (accountId == -1) {
|
|
// probably the message was not found
|
|
if (Email.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(EmailContent.Message.CONTENT_URI, messageId);
|
|
resolver.update(uri, cv, null, null);
|
|
|
|
// Split here for target type (Service or MessagingController)
|
|
IEmailService service = getServiceForMessage(messageId);
|
|
if (service != null) {
|
|
// We just need to be sure the callback is installed, if this is the first call
|
|
// to the service.
|
|
try {
|
|
service.setCallback(mServiceCallback);
|
|
} catch (RemoteException re) {
|
|
// OK - not a critical callback here
|
|
}
|
|
} else {
|
|
// for IMAP & POP only, (attempt to) send the message now
|
|
final EmailContent.Account account =
|
|
EmailContent.Account.restoreAccountWithId(mProviderContext, accountId);
|
|
final long sentboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_SENT);
|
|
new Thread() {
|
|
@Override
|
|
public void run() {
|
|
mLegacyController.sendPendingMessages(account, sentboxId, mLegacyListener);
|
|
}
|
|
}.start();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Try to send all pending messages for a given account
|
|
*
|
|
* @param accountId the account for which to send messages (-1 for all accounts)
|
|
* @param callback
|
|
*/
|
|
public void sendPendingMessages(long accountId, Result callback) {
|
|
// 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);
|
|
} 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
|
|
final EmailContent.Account account =
|
|
EmailContent.Account.restoreAccountWithId(mProviderContext, accountId);
|
|
final long sentboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_SENT);
|
|
new Thread() {
|
|
@Override
|
|
public void run() {
|
|
mLegacyController.sendPendingMessages(account, sentboxId, mLegacyListener);
|
|
}
|
|
}.start();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reset visible limits for all accounts.
|
|
* For each account:
|
|
* look up limit
|
|
* write limit into all mailboxes for that account
|
|
*/
|
|
public void resetVisibleLimits() {
|
|
new Thread() {
|
|
@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);
|
|
Account account = Account.restoreAccountWithId(mProviderContext, accountId);
|
|
if (account != null) {
|
|
Store.StoreInfo info = Store.StoreInfo.getStoreInfo(
|
|
account.getStoreUri(mProviderContext), mContext);
|
|
if (info != null && info.mVisibleLimitDefault > 0) {
|
|
int limit = info.mVisibleLimitDefault;
|
|
ContentValues cv = new ContentValues();
|
|
cv.put(MailboxColumns.VISIBLE_LIMIT, limit);
|
|
resolver.update(Mailbox.CONTENT_URI, cv,
|
|
MailboxColumns.ACCOUNT_KEY + "=?",
|
|
new String[] { Long.toString(accountId) });
|
|
}
|
|
}
|
|
}
|
|
} finally {
|
|
c.close();
|
|
}
|
|
}
|
|
}.start();
|
|
}
|
|
|
|
/**
|
|
* Increase the load count for a given mailbox, and trigger a refresh. Applies only to
|
|
* IMAP and POP.
|
|
*
|
|
* @param mailboxId the mailbox
|
|
* @param callback
|
|
*/
|
|
public void loadMoreMessages(final long mailboxId, Result callback) {
|
|
new Thread() {
|
|
@Override
|
|
public void run() {
|
|
Mailbox mailbox = Mailbox.restoreMailboxWithId(mProviderContext, mailboxId);
|
|
if (mailbox == null) {
|
|
return;
|
|
}
|
|
Account account = Account.restoreAccountWithId(mProviderContext,
|
|
mailbox.mAccountKey);
|
|
if (account == null) {
|
|
return;
|
|
}
|
|
Store.StoreInfo info = Store.StoreInfo.getStoreInfo(
|
|
account.getStoreUri(mProviderContext), mContext);
|
|
if (info != null && info.mVisibleLimitIncrement > 0) {
|
|
// 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, info.mVisibleLimitIncrement);
|
|
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 += info.mVisibleLimitIncrement;
|
|
mLegacyController.synchronizeMailbox(account, mailbox, mLegacyListener);
|
|
}
|
|
}
|
|
}.start();
|
|
}
|
|
|
|
/**
|
|
* @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);
|
|
}
|
|
|
|
/**
|
|
* Delete a single message by moving it to the trash, or deleting it from the trash
|
|
*
|
|
* 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".
|
|
* @param accountId The id of the message's account, or -1 if not known by caller
|
|
*
|
|
* TODO: Move out of UI thread
|
|
* TODO: "get account a for message m" should be a utility
|
|
* TODO: "get mailbox of type n for account a" should be a utility
|
|
*/
|
|
public void deleteMessage(long messageId, long accountId) {
|
|
ContentResolver resolver = mProviderContext.getContentResolver();
|
|
|
|
// 1. Look up acct# for message we're deleting
|
|
if (accountId == -1) {
|
|
accountId = lookupAccountForMessage(messageId);
|
|
}
|
|
if (accountId == -1) {
|
|
return;
|
|
}
|
|
|
|
// 2. Confirm that there is a trash mailbox available. If not, create one
|
|
long trashMailboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_TRASH);
|
|
|
|
// 3. Are we moving to trash or deleting? It depends on where the message currently sits.
|
|
long sourceMailboxId = -1;
|
|
Cursor c = resolver.query(EmailContent.Message.CONTENT_URI,
|
|
MESSAGEID_TO_MAILBOXID_PROJECTION, EmailContent.RECORD_ID + "=?",
|
|
new String[] { Long.toString(messageId) }, null);
|
|
try {
|
|
sourceMailboxId = c.moveToFirst()
|
|
? c.getLong(MESSAGEID_TO_MAILBOXID_COLUMN_MAILBOXID)
|
|
: -1;
|
|
} finally {
|
|
c.close();
|
|
}
|
|
|
|
// 4. Drop non-essential data for the message (e.g. attachment files)
|
|
AttachmentProvider.deleteAllAttachmentFiles(mProviderContext, accountId, messageId);
|
|
|
|
Uri uri = ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, messageId);
|
|
|
|
// 5. Perform "delete" as appropriate
|
|
if (sourceMailboxId == trashMailboxId) {
|
|
// 5a. Delete from trash
|
|
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);
|
|
}
|
|
|
|
// 6. Service runs automatically, MessagingController needs a kick
|
|
Account account = Account.restoreAccountWithId(mProviderContext, accountId);
|
|
if (isMessagingController(account)) {
|
|
final long syncAccountId = accountId;
|
|
new Thread() {
|
|
@Override
|
|
public void run() {
|
|
mLegacyController.processPendingActions(syncAccountId);
|
|
}
|
|
}.start();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set/clear the unread status of a message
|
|
*
|
|
* TODO db ops should not be in this thread. queue it up.
|
|
*
|
|
* @param messageId the message to update
|
|
* @param isRead the new value for the isRead flag
|
|
*/
|
|
public void setMessageRead(final long messageId, boolean isRead) {
|
|
ContentValues cv = new ContentValues();
|
|
cv.put(EmailContent.MessageColumns.FLAG_READ, isRead);
|
|
Uri uri = ContentUris.withAppendedId(
|
|
EmailContent.Message.SYNCED_CONTENT_URI, messageId);
|
|
mProviderContext.getContentResolver().update(uri, cv, null, null);
|
|
|
|
// Service runs automatically, MessagingController needs a kick
|
|
final Message message = Message.restoreMessageWithId(mProviderContext, messageId);
|
|
Account account = Account.restoreAccountWithId(mProviderContext, message.mAccountKey);
|
|
if (isMessagingController(account)) {
|
|
new Thread() {
|
|
@Override
|
|
public void run() {
|
|
mLegacyController.processPendingActions(message.mAccountKey);
|
|
}
|
|
}.start();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set/clear the favorite status of a message
|
|
*
|
|
* TODO db ops should not be in this thread. queue it up.
|
|
*
|
|
* @param messageId the message to update
|
|
* @param isFavorite the new value for the isFavorite flag
|
|
*/
|
|
public void setMessageFavorite(final long messageId, boolean isFavorite) {
|
|
ContentValues cv = new ContentValues();
|
|
cv.put(EmailContent.MessageColumns.FLAG_FAVORITE, isFavorite);
|
|
Uri uri = ContentUris.withAppendedId(
|
|
EmailContent.Message.SYNCED_CONTENT_URI, messageId);
|
|
mProviderContext.getContentResolver().update(uri, cv, null, null);
|
|
|
|
// Service runs automatically, MessagingController needs a kick
|
|
final Message message = Message.restoreMessageWithId(mProviderContext, messageId);
|
|
Account account = Account.restoreAccountWithId(mProviderContext, message.mAccountKey);
|
|
if (isMessagingController(account)) {
|
|
new Thread() {
|
|
@Override
|
|
public void run() {
|
|
mLegacyController.processPendingActions(message.mAccountKey);
|
|
}
|
|
}.start();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
* @callback the Controller callback by which results will be reported (currently not defined)
|
|
*/
|
|
public void sendMeetingResponse(final long messageId, final int response,
|
|
final Result callback) {
|
|
// 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 mailboxId the owner mailbox
|
|
* @param accountId the owner account
|
|
* @param callback the Controller callback by which results will be reported
|
|
*/
|
|
public void loadAttachment(final long attachmentId, final long messageId, final long mailboxId,
|
|
final long accountId, final Result callback) {
|
|
|
|
File saveToFile = AttachmentProvider.getAttachmentFilename(mProviderContext,
|
|
accountId, attachmentId);
|
|
Attachment attachInfo = Attachment.restoreAttachmentWithId(mProviderContext, attachmentId);
|
|
|
|
if (saveToFile.exists() && attachInfo.mContentUri != null) {
|
|
// The attachment has already been downloaded, so we will just "pretend" to download it
|
|
synchronized (mListeners) {
|
|
for (Result listener : mListeners) {
|
|
listener.loadAttachmentCallback(null, messageId, attachmentId, 0);
|
|
}
|
|
for (Result listener : mListeners) {
|
|
listener.loadAttachmentCallback(null, messageId, attachmentId, 100);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Split here for target type (Service or MessagingController)
|
|
IEmailService service = getServiceForMessage(messageId);
|
|
if (service != null) {
|
|
// Service implementation
|
|
try {
|
|
service.loadAttachment(attachInfo.mId, saveToFile.getAbsolutePath(),
|
|
AttachmentProvider.getAttachmentUri(accountId, attachmentId).toString());
|
|
} catch (RemoteException e) {
|
|
// TODO Change exception handling to be consistent with however this method
|
|
// is implemented for other protocols
|
|
Log.e("onDownloadAttachment", "RemoteException", e);
|
|
}
|
|
} else {
|
|
// MessagingController implementation
|
|
new Thread() {
|
|
@Override
|
|
public void run() {
|
|
mLegacyController.loadAttachment(accountId, messageId, mailboxId, attachmentId,
|
|
mLegacyListener);
|
|
}
|
|
}.start();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
return getServiceForAccount(message.mAccountKey);
|
|
}
|
|
|
|
/**
|
|
* For a given account id, return a service proxy if applicable, or null.
|
|
*
|
|
* TODO this should use a cache because we'll be doing this a lot
|
|
*
|
|
* @param accountId the message of interest
|
|
* @result service proxy, or null if n/a
|
|
*/
|
|
private IEmailService getServiceForAccount(long accountId) {
|
|
// TODO make this more efficient, caching the account, MUCH smaller lookup here, etc.
|
|
Account account = EmailContent.Account.restoreAccountWithId(mProviderContext, accountId);
|
|
if (account == null || isMessagingController(account)) {
|
|
return null;
|
|
} else {
|
|
return ExchangeUtils.getExchangeEmailService(mContext, mServiceCallback);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Simple helper to determine if legacy MessagingController should be used
|
|
*
|
|
* TODO this should not require a full account, just an accountId
|
|
* TODO this should use a cache because we'll be doing this a lot
|
|
*/
|
|
public boolean isMessagingController(EmailContent.Account account) {
|
|
if (account == null) return false;
|
|
Store.StoreInfo info =
|
|
Store.StoreInfo.getStoreInfo(account.getStoreUri(mProviderContext), mContext);
|
|
// This null happens in testing.
|
|
if (info == null) {
|
|
return false;
|
|
}
|
|
String scheme = info.mScheme;
|
|
|
|
return ("pop3".equals(scheme) || "imap".equals(scheme));
|
|
}
|
|
|
|
/**
|
|
* 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 interface Result {
|
|
/**
|
|
* 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);
|
|
|
|
/**
|
|
* 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 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 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.
|
|
*
|
|
* @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);
|
|
}
|
|
|
|
/**
|
|
* Support for receiving callbacks from MessagingController and dealing with UI going
|
|
* out of scope.
|
|
*/
|
|
private class LegacyListener extends MessagingListener {
|
|
|
|
@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);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void synchronizeMailboxFinished(long accountId, long mailboxId,
|
|
int totalMessagesInMailbox, int numNewMessages) {
|
|
synchronized (mListeners) {
|
|
for (Result l : mListeners) {
|
|
l.updateMailboxCallback(null, accountId, mailboxId, 100, numNewMessages);
|
|
}
|
|
}
|
|
}
|
|
|
|
@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);
|
|
}
|
|
}
|
|
}
|
|
|
|
@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) {
|
|
synchronized (mListeners) {
|
|
for (Result listener : mListeners) {
|
|
listener.loadMessageForViewCallback(null, messageId, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void loadMessageForViewFinished(long messageId) {
|
|
synchronized (mListeners) {
|
|
for (Result listener : mListeners) {
|
|
listener.loadMessageForViewCallback(null, messageId, 100);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void loadMessageForViewFailed(long messageId, String message) {
|
|
synchronized (mListeners) {
|
|
for (Result listener : mListeners) {
|
|
listener.loadMessageForViewCallback(new MessagingException(message),
|
|
messageId, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void loadAttachmentStarted(long accountId, long messageId, long attachmentId,
|
|
boolean requiresDownload) {
|
|
synchronized (mListeners) {
|
|
for (Result listener : mListeners) {
|
|
listener.loadAttachmentCallback(null, messageId, attachmentId, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void loadAttachmentFinished(long accountId, long messageId, long attachmentId) {
|
|
synchronized (mListeners) {
|
|
for (Result listener : mListeners) {
|
|
listener.loadAttachmentCallback(null, messageId, attachmentId, 100);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void loadAttachmentFailed(long accountId, long messageId, long attachmentId,
|
|
String reason) {
|
|
synchronized (mListeners) {
|
|
for (Result listener : mListeners) {
|
|
listener.loadAttachmentCallback(new MessagingException(reason),
|
|
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;
|
|
}
|
|
synchronized (mListeners) {
|
|
for (Result listener : mListeners) {
|
|
listener.loadAttachmentCallback(result, 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) {
|
|
// Log.d(Email.LOG_TAG, "sendMessageStatus: messageId=" + messageId
|
|
// + " statusCode=" + statusCode + " progress=" + progress);
|
|
// Log.d(Email.LOG_TAG, "sendMessageStatus: subject=" + subject);
|
|
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;
|
|
}
|
|
// Log.d(Email.LOG_TAG, "result=" + result + " messageId=" + messageId
|
|
// + " progress=" + progress);
|
|
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 where do we get "number of new messages" as well?
|
|
// TODO should pass this back instead of looking it up here
|
|
// TODO smaller projection
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
private MessagingException mapStatusToException(int statusCode) {
|
|
switch (statusCode) {
|
|
case EmailServiceStatus.SUCCESS:
|
|
case EmailServiceStatus.IN_PROGRESS:
|
|
return null;
|
|
|
|
case EmailServiceStatus.LOGIN_FAILED:
|
|
return new AuthenticationFailedException("");
|
|
|
|
case EmailServiceStatus.CONNECTION_ERROR:
|
|
return new MessagingException(MessagingException.IOERROR);
|
|
|
|
case EmailServiceStatus.MESSAGE_NOT_FOUND:
|
|
case EmailServiceStatus.ATTACHMENT_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));
|
|
}
|
|
}
|
|
}
|
|
}
|