New attachment download support for Controller (IMAP/POP3)

* Supports download via AttachmentDownloadService

Change-Id: I66143a79b99dcdbd307524ba0b81227f09a00e4a
This commit is contained in:
Marc Blank 2010-08-18 08:50:45 -07:00
parent 09fd4d0a18
commit 7894ee82b3
8 changed files with 376 additions and 156 deletions

View File

@ -276,6 +276,12 @@
>
</service>
<service
android:name=".Controller"
android:enabled="false"
>
</service>
<service
android:name=".service.AttachmentDownloadService"
android:enabled="false"

View File

@ -19,11 +19,13 @@ 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.mail.Folder.MessageRetrievalListener;
import com.android.email.mail.store.Pop3Store.Pop3Message;
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.Body;
import com.android.email.provider.EmailContent.Mailbox;
import com.android.email.provider.EmailContent.MailboxColumns;
import com.android.email.provider.EmailContent.Message;
@ -32,17 +34,21 @@ import com.android.email.service.EmailServiceStatus;
import com.android.email.service.IEmailService;
import com.android.email.service.IEmailServiceCallback;
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.text.TextUtils;
import android.util.Log;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
@ -52,10 +58,11 @@ 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.
* 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 {
public class Controller extends Service {
private static final String TAG = "Controller";
private static Controller sInstance;
private final Context mContext;
private Context mProviderContext;
@ -77,22 +84,31 @@ public class Controller {
MailboxColumns.TYPE + "=" + Mailbox.TYPE_ATTACHMENT;
private static final String WHERE_MAILBOX_KEY = MessageColumns.MAILBOX_KEY + "=?";
private static String[] MESSAGEID_TO_ACCOUNTID_PROJECTION = new String[] {
private static final 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 final int MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID = 1;
private static String[] MESSAGEID_TO_MAILBOXID_PROJECTION = new String[] {
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[] MESSAGEID_TO_MAILBOXID_PROJECTION = new String[] {
EmailContent.RECORD_ID,
EmailContent.MessageColumns.MAILBOX_KEY
};
private static int MESSAGEID_TO_MAILBOXID_COLUMN_MAILBOXID = 1;
private static final int MESSAGEID_TO_MAILBOXID_COLUMN_MAILBOXID = 1;
// Service callbacks as set up via setCallback
private static RemoteCallbackList<IEmailServiceCallback> sCallbackList =
new RemoteCallbackList<IEmailServiceCallback>();
protected Controller(Context _context) {
mContext = _context.getApplicationContext();
mProviderContext = _context;
mLegacyController = MessagingController.getInstance(mProviderContext);
mLegacyController = MessagingController.getInstance(mProviderContext, this);
mLegacyController.addListener(mLegacyListener);
}
@ -106,6 +122,29 @@ public class Controller {
mLegacyController.removeListener(mLegacyListener);
}
/**
* As a Service, Controller needs this no-argument constructor, but only because there is
* another constructor defined for the class. In the typical case for a Service, there are
* no defined constructors, so the default constructor is used when the Service is instantiated
* by ServiceManager (initialization would be performed in onCreate or onStartCommand, etc.)
*
* Because of this, the Controller Service, when bound, creates a second instance of the class,
* i.e. in addition to the "singleton" created/returned by getInstance. This is unfortunate,
* but not disruptive, as the Service Controller instance references members of sInstance (the
* previously-singleton Controller); it's perhaps best to think of the Service instance as a
* delegate.
*
* TODO: Have Controller behave more like a real Service. This means that the lifecycle of
* the Service (and thus the singleton instance) should be managed by ServiceManager (as happens
* with AttachmentDownloadService and MailService), its initialization should be handled in
* onCreate(), etc. When this is done (and it should be relatively simple), we will be back
* to a true singleton
*/
public Controller() {
mContext = mProviderContext = this;
mLegacyController = null;
}
/**
* Gets or creates the singleton instance of Controller.
*/
@ -159,12 +198,6 @@ public class Controller {
}
}
private boolean isActiveResultCallback(Result listener) {
synchronized (mListeners) {
return mListeners.contains(listener);
}
}
/**
* Delete all Messages that live in the attachment mailbox
*/
@ -504,37 +537,28 @@ public class Controller {
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);
if (account == null) {
return;
}
final long sentboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_SENT);
Utility.runAsync(new Runnable() {
public void run() {
mLegacyController.sendPendingMessages(account, sentboxId, mLegacyListener);
}
});
sendPendingMessages(accountId);
}
private void sendPendingMessagesSmtp(long accountId) {
// for IMAP & POP only, (attempt to) send the message now
final EmailContent.Account account =
EmailContent.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 (-1 for all accounts)
* @param callback
* @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
@ -557,17 +581,7 @@ public class Controller {
}
} else {
// MessagingController implementation
final EmailContent.Account account =
EmailContent.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);
}
});
sendPendingMessagesSmtp(accountId);
}
}
@ -846,12 +860,10 @@ public class Controller {
public void loadAttachment(final long attachmentId, final long messageId, final long mailboxId,
final long accountId) {
File saveToFile = AttachmentProvider.getAttachmentFilename(mProviderContext,
accountId, attachmentId);
Attachment attachInfo = Attachment.restoreAttachmentWithId(mProviderContext, attachmentId);
if (saveToFile.exists() && attachInfo.mContentUri != null) {
if (Utility.attachmentExists(mProviderContext, accountId, 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, messageId, attachmentId, 0);
@ -863,27 +875,10 @@ public class Controller {
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
Utility.runAsync(new Runnable() {
public void run() {
mLegacyController.loadAttachment(accountId, messageId, mailboxId, attachmentId,
mLegacyListener);
}
});
}
// 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(mContext, cv);
}
/**
@ -1080,7 +1075,13 @@ public class Controller {
* Support for receiving callbacks from MessagingController and dealing with UI going
* out of scope.
*/
private class LegacyListener extends MessagingListener {
public class LegacyListener extends MessagingListener implements MessageRetrievalListener {
public LegacyListener(long messageId, long attachmentId) {
super(messageId, attachmentId);
}
public LegacyListener() {
}
@Override
public void listFoldersStarted(long accountId) {
@ -1192,6 +1193,11 @@ public class Controller {
@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, messageId, attachmentId, 0);
@ -1201,6 +1207,11 @@ public class Controller {
@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, messageId, attachmentId, 100);
@ -1209,12 +1220,31 @@ public class Controller {
}
@Override
public void loadAttachmentFailed(long accountId, long messageId, long attachmentId,
String reason) {
public void loadAttachmentProgress(int progress) {
synchronized (mListeners) {
for (Result listener : mListeners) {
listener.loadAttachmentCallback(new MessagingException(reason),
messageId, attachmentId, 0);
listener.loadAttachmentCallback(null, messageId, attachmentId, progress);
}
}
}
@Override
public void loadAttachmentFailed(long accountId, long messageId, long attachmentId,
MessagingException me) {
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.getCause() instanceof IOException) {
status = EmailServiceStatus.CONNECTION_ERROR;
}
mCallbackProxy.loadAttachmentStatus(messageId, attachmentId, status, 0);
} catch (RemoteException e) {
}
synchronized (mListeners) {
for (Result listener : mListeners) {
listener.loadAttachmentCallback(me, messageId, attachmentId, 0);
}
}
}
@ -1252,6 +1282,10 @@ public class Controller {
}
}
}
@Override
public void messageRetrieved(com.android.email.mail.Message message) {
}
}
/**
@ -1294,9 +1328,6 @@ public class Controller {
*/
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) {
@ -1310,8 +1341,6 @@ public class Controller {
}
break;
}
// Log.d(Email.LOG_TAG, "result=" + result + " messageId=" + messageId
// + " progress=" + progress);
synchronized(mListeners) {
for (Result listener : mListeners) {
listener.sendMailCallback(result, accountId, messageId, progress);
@ -1352,9 +1381,7 @@ public class Controller {
}
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;
@ -1393,4 +1420,154 @@ public class Controller {
}
}
}
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)
throws RemoteException {
}
@Override
public void syncMailboxListStatus(long accountId, int statusCode, int progress)
throws RemoteException {
}
@Override
public void syncMailboxStatus(long mailboxId, int statusCode, int progress)
throws RemoteException {
}
};
/**
* 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(String protocol, String host, String userName, String password,
int port, boolean ssl, boolean trustCertificates) throws RemoteException {
return null;
}
public Bundle autoDiscover(String userName, String password) throws RemoteException {
return null;
}
public void startSync(long mailboxId) throws RemoteException {
}
public void stopSync(long mailboxId) throws RemoteException {
}
public void loadAttachment(long attachmentId, String destinationFile,
String contentUriString) throws RemoteException {
if (Email.DEBUG) {
Log.d(TAG, "loadAttachment: " + attachmentId + " to " + destinationFile);
}
Attachment att = Attachment.restoreAttachmentWithId(Controller.this, attachmentId);
if (att != null) {
Message msg = Message.restoreMessageWithId(Controller.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(Controller.this, Body.CONTENT_URI,
BODY_SOURCE_KEY_PROJECTION, WHERE_MESSAGE_KEY,
new String[] {Long.toString(msg.mId)});
if (cols != null) {
msg = Message.restoreMessageWithId(Controller.this,
Long.parseLong(cols[BODY_SOURCE_KEY_COLUMN]));
if (msg == null) {
// TODO: We can try restoring from the deleted table at this point...
return;
}
}
}
MessagingController legacyController = sInstance.mLegacyController;
LegacyListener legacyListener = sInstance.mLegacyListener;
legacyController.loadAttachment(msg.mAccountKey, msg.mId, msg.mMailboxKey,
attachmentId, legacyListener);
}
}
}
public void updateFolderList(long accountId) throws RemoteException {
}
public void hostChanged(long accountId) throws RemoteException {
}
public void setLogging(int on) throws RemoteException {
}
public void sendMeetingResponse(long messageId, int response) throws RemoteException {
}
public void loadMore(long messageId) throws RemoteException {
}
// The following three methods are not implemented in this version
public boolean createFolder(long accountId, String name) throws RemoteException {
return false;
}
public boolean deleteFolder(long accountId, String name) throws RemoteException {
return false;
}
public boolean renameFolder(long accountId, String oldName, String newName)
throws RemoteException {
return false;
}
public void setCallback(IEmailServiceCallback cb) throws RemoteException {
sCallbackList.register(cb);
}
};
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
}

View File

@ -16,6 +16,8 @@
package com.android.email;
import com.android.email.mail.MessagingException;
import android.content.Context;
import java.util.Set;
@ -169,6 +171,14 @@ public class GroupMessagingListener extends MessagingListener {
}
}
@Override
synchronized public void loadAttachmentProgress(
int progress) {
for (MessagingListener l : mListeners) {
l.loadAttachmentProgress(progress);
}
}
@Override
synchronized public void loadAttachmentFinished(
long accountId,
@ -184,9 +194,9 @@ public class GroupMessagingListener extends MessagingListener {
long accountId,
long messageId,
long attachmentId,
String reason) {
MessagingException me) {
for (MessagingListener l : mListeners) {
l.loadAttachmentFailed(accountId, messageId, attachmentId, reason);
l.loadAttachmentFailed(accountId, messageId, attachmentId, me);
}
}

View File

@ -50,7 +50,6 @@ import android.net.Uri;
import android.os.Process;
import android.util.Log;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
@ -122,10 +121,11 @@ public class MessagingController implements Runnable {
private final GroupMessagingListener mListeners = new GroupMessagingListener();
private boolean mBusy;
private final Context mContext;
private final Controller mController;
protected MessagingController(Context _context) {
protected MessagingController(Context _context, Controller _controller) {
mContext = _context.getApplicationContext();
mController = _controller;
mThread = new Thread(this);
mThread.start();
}
@ -134,9 +134,10 @@ public class MessagingController implements Runnable {
* Gets or creates the singleton instance of MessagingController. Application is used to
* provide a Context to classes that need it.
*/
public synchronized static MessagingController getInstance(Context _context) {
public synchronized static MessagingController getInstance(Context _context,
Controller _controller) {
if (sInstance == null) {
sInstance = new MessagingController(_context);
sInstance = new MessagingController(_context, _controller);
}
return sInstance;
}
@ -636,6 +637,10 @@ public class MessagingController implements Runnable {
"Error while storing downloaded message." + e.toString());
}
}
@Override
public void loadAttachmentProgress(int progress) {
}
});
}
@ -768,6 +773,10 @@ public class MessagingController implements Runnable {
copyOneMessageToProvider(message, account, folder,
EmailContent.Message.FLAG_LOADED_COMPLETE);
}
@Override
public void loadAttachmentProgress(int progress) {
}
});
// 14. Download large messages. We ask the server to give us the message structure,
@ -1816,16 +1825,14 @@ public class MessagingController implements Runnable {
public void run() {
try {
//1. Check if the attachment is already here and return early in that case
File saveToFile = AttachmentProvider.getAttachmentFilename(mContext, accountId,
attachmentId);
Attachment attachment =
Attachment.restoreAttachmentWithId(mContext, attachmentId);
if (attachment == null) {
mListeners.loadAttachmentFailed(accountId, messageId, attachmentId,
"Attachment is null");
new MessagingException("The attachment is null"));
return;
}
if (saveToFile.exists() && attachment.mContentUri != null) {
if (Utility.attachmentExists(mContext, accountId, attachment)) {
mListeners.loadAttachmentFinished(accountId, messageId, attachmentId);
return;
}
@ -1841,14 +1848,11 @@ public class MessagingController implements Runnable {
if (account == null || mailbox == null || message == null) {
mListeners.loadAttachmentFailed(accountId, messageId, attachmentId,
"Account, mailbox, message or attachment are null");
new MessagingException(
"Account, mailbox, message or attachment are null"));
return;
}
// Pruning. Policy is to have one downloaded attachment at a time,
// per account, to reduce disk storage pressure.
pruneCachedAttachments(accountId);
Store remoteStore =
Store.getInstance(account.getStoreUri(mContext), mContext, null);
Folder remoteFolder = remoteStore.getFolder(mailbox.mDisplayName);
@ -1879,7 +1883,14 @@ public class MessagingController implements Runnable {
// 4. Now ask for the attachment to be fetched
FetchProfile fp = new FetchProfile();
fp.add(storePart);
remoteFolder.fetch(new Message[] { storeMessage }, fp, null);
remoteFolder.fetch(new Message[] { storeMessage }, fp,
mController.new LegacyListener(messageId, attachmentId));
// If we failed to load the attachment, throw an Exception here, so that
// AttachmentDownloadService knows that we failed
if (storePart.getBody() == null) {
throw new MessagingException("Attachment not loaded.");
}
// 5. Save the downloaded file and update the attachment as necessary
LegacyConversions.saveAttachmentBody(mContext, storePart, attachment,
@ -1890,57 +1901,13 @@ public class MessagingController implements Runnable {
}
catch (MessagingException me) {
if (Email.LOGD) Log.v(Email.LOG_TAG, "", me);
mListeners.loadAttachmentFailed(accountId, messageId, attachmentId,
me.getMessage());
mListeners.loadAttachmentFailed(accountId, messageId, attachmentId, me);
} catch (IOException ioe) {
Log.e(Email.LOG_TAG, "Error while storing attachment." + ioe.toString());
}
}});
}
/**
* Erase all stored attachments for a given account. Rules:
* 1. All files in attachment directory are up for deletion
* 2. If filename does not match an known attachment id, it's deleted
* 3. If the attachment has location data (implying that it's reloadable), it's deleted
*/
/* package */ void pruneCachedAttachments(long accountId) {
ContentResolver resolver = mContext.getContentResolver();
File cacheDir = AttachmentProvider.getAttachmentDirectory(mContext, accountId);
File[] fileList = cacheDir.listFiles();
// fileList can be null if the directory doesn't exist or if there's an IOException
if (fileList == null) return;
for (File file : fileList) {
if (file.exists()) {
long id;
try {
// the name of the file == the attachment id
id = Long.valueOf(file.getName());
Uri uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, id);
Cursor c = resolver.query(uri, PRUNE_ATTACHMENT_PROJECTION, null, null, null);
try {
if (c.moveToNext()) {
// if there is no way to reload the attachment, don't delete it
if (c.getString(0) == null) {
continue;
}
}
} finally {
c.close();
}
// Clear the content URI field since we're losing the attachment
resolver.update(uri, PRUNE_ATTACHMENT_CV, null, null);
} catch (NumberFormatException nfe) {
// ignore filename != number error, and just delete it anyway
}
// This file can be safely deleted
if (!file.delete()) {
file.deleteOnExit();
}
}
}
}
/**
* Attempt to send any messages that are sitting in the Outbox.
* @param account
@ -1996,6 +1963,14 @@ public class MessagingController implements Runnable {
try {
messageId = c.getLong(0);
mListeners.sendPendingMessagesStarted(account.mId, messageId);
// Don't send messages with unloaded attachments
if (Utility.hasUnloadedAttachments(mContext, messageId)) {
if (Email.DEBUG) {
Log.d(Email.LOG_TAG, "Can't send #" + messageId +
"; unloaded attachments");
}
continue;
}
sender.sendMessage(messageId);
} catch (MessagingException me) {
// report error for this message, but keep trying others
@ -2006,6 +1981,15 @@ public class MessagingController implements Runnable {
Uri syncedUri =
ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, messageId);
if (requireMoveMessageToSentFolder) {
// If this is a forwarded message and it has attachments, delete them, as they
// duplicate information found elsewhere (on the server). This saves storage.
EmailContent.Message msg =
EmailContent.Message.restoreMessageWithId(mContext, messageId);
if (msg != null &&
((msg.mFlags & EmailContent.Message.FLAG_TYPE_FORWARD) != 0)) {
AttachmentProvider.deleteAllAttachmentFiles(mContext, account.mId,
messageId);
}
resolver.update(syncedUri, moveToSentValues, null, null);
} else {
AttachmentProvider.deleteAllAttachmentFiles(mContext, account.mId, messageId);

View File

@ -16,6 +16,8 @@
package com.android.email;
import com.android.email.mail.MessagingException;
import android.content.Context;
/**
@ -26,6 +28,17 @@ import android.content.Context;
* changes in this class.
*/
public class MessagingListener {
long messageId = -1;
long attachmentId = -1;
public MessagingListener(long _messageId, long _attachmentId) {
messageId = _messageId;
attachmentId = _attachmentId;
}
public MessagingListener() {
}
public void listFoldersStarted(long accountId) {
}
@ -81,6 +94,8 @@ public class MessagingListener {
boolean requiresDownload) {
}
public void loadAttachmentProgress(int progress) {}
public void loadAttachmentFinished(
long accountId,
long messageId,
@ -91,7 +106,7 @@ public class MessagingListener {
long accountId,
long messageId,
long attachmentId,
String reason) {
MessagingException me) {
}
/**

View File

@ -48,6 +48,7 @@ public abstract class Folder {
*/
public interface MessageRetrievalListener {
public void messageRetrieved(Message message);
public void loadAttachmentProgress(int progress);
}
/**

View File

@ -21,6 +21,7 @@ import com.android.email.Preferences;
import com.android.email.Utility;
import com.android.email.VendorPolicyLoader;
import com.android.email.mail.AuthenticationFailedException;
import com.android.email.mail.Body;
import com.android.email.mail.CertificateValidationException;
import com.android.email.mail.FetchProfile;
import com.android.email.mail.Flag;
@ -30,6 +31,7 @@ import com.android.email.mail.MessagingException;
import com.android.email.mail.Part;
import com.android.email.mail.Store;
import com.android.email.mail.Transport;
import com.android.email.mail.internet.BinaryTempFileBody;
import com.android.email.mail.internet.MimeBodyPart;
import com.android.email.mail.internet.MimeHeader;
import com.android.email.mail.internet.MimeMessage;
@ -59,6 +61,7 @@ import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
@ -98,6 +101,8 @@ public class ImapStore extends Store {
// Always check in FALSE
private static final boolean DEBUG_FORCE_SEND_ID = false;
private static final int COPY_BUFFER_SIZE = 16*1024;
private static final Flag[] PERMANENT_FLAGS = { Flag.DELETED, Flag.SEEN, Flag.FLAGGED };
private final Context mContext;
@ -995,9 +1000,8 @@ public class ImapStore extends Store {
// decodeBody creates BinaryTempFileBody, but we could avoid this
// if we implement ImapStringBody.
// (We'll need to share a temp file. Protect it with a ref-count.)
fetchPart.setBody(MimeUtility.decodeBody(
bodyStream,
contentTransferEncoding));
fetchPart.setBody(decodeBody(bodyStream, contentTransferEncoding,
fetchPart.getSize(), listener));
}
if (listener != null) {
@ -1012,6 +1016,30 @@ public class ImapStore extends Store {
}
}
/**
* Removes any content transfer encoding from the stream and returns a Body.
* This code is taken/condensed from MimeUtility.decodeBody
*/
private Body decodeBody(InputStream in, String contentTransferEncoding, int size,
MessageRetrievalListener listener) throws IOException {
// Get a properly wrapped input stream
in = MimeUtility.getInputStreamForContentTransferEncoding(in, contentTransferEncoding);
BinaryTempFileBody tempBody = new BinaryTempFileBody();
OutputStream out = tempBody.getOutputStream();
byte[] buffer = new byte[COPY_BUFFER_SIZE];
int n = 0;
int count = 0;
while (-1 != (n = in.read(buffer))) {
out.write(buffer, 0, n);
count += n;
if (listener != null) {
listener.loadAttachmentProgress(count * 100 / size);
}
}
out.close();
return tempBody;
}
@Override
public Flag[] getPermanentFlags() throws MessagingException {
return PERMANENT_FLAGS;

View File

@ -507,8 +507,7 @@ public class AttachmentDownloadService extends Service implements Runnable {
if (protocol.equals("eas")) {
serviceClass = SyncManager.class;
} else {
// Uncomment this when Controller has been made a Service
//serviceClass = Controller.class;
serviceClass = com.android.email.Controller.class;
}
mAccountServiceMap.put(accountId, serviceClass);
}