diff --git a/AndroidManifest.xml b/AndroidManifest.xml index e982ec684..c19863d38 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -191,6 +191,12 @@ + + + + + + Message saved as draft. This attachment cannot be displayed. + + Opening message\u2026 Set up email diff --git a/src/com/android/email/Controller.java b/src/com/android/email/Controller.java index 8e2e73342..8de2e6e7a 100644 --- a/src/com/android/email/Controller.java +++ b/src/com/android/email/Controller.java @@ -19,6 +19,7 @@ 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.store.Pop3Store.Pop3Message; import com.android.email.provider.AttachmentProvider; import com.android.email.provider.EmailContent; import com.android.email.provider.EmailContent.Account; @@ -41,6 +42,9 @@ import android.os.RemoteException; import android.util.Log; import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; import java.util.HashSet; import java.util.concurrent.ConcurrentHashMap; @@ -59,6 +63,17 @@ public class Controller { private final ServiceCallback mServiceCallback = new ServiceCallback(); private final HashSet mListeners = new HashSet(); + + // 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 ATTACHMENT_MAILBOX_ACCOUNT_KEY = 0; + /*package*/ static final String ATTACHMENT_MAILBOX_SERVER_ID = "__attachment_mailbox__"; + /*package*/ static final String ATTACHMENT_MESSAGE_UID_PREFIX = "__attachment_message__"; + private static final String WHERE_TYPE_ATTACHMENT = + MailboxColumns.TYPE + "=" + Mailbox.TYPE_ATTACHMENT; + private static final String WHERE_MAILBOX_KEY = MessageColumns.MAILBOX_KEY + "=?"; + private static String[] MESSAGEID_TO_ACCOUNTID_PROJECTION = new String[] { EmailContent.RECORD_ID, EmailContent.MessageColumns.ACCOUNT_KEY @@ -83,7 +98,7 @@ public class Controller { protected Controller(Context _context) { mContext = _context.getApplicationContext(); mProviderContext = _context; - mLegacyController = MessagingController.getInstance(mContext); + mLegacyController = MessagingController.getInstance(mProviderContext); mLegacyController.addListener(mLegacyListener); } @@ -137,6 +152,89 @@ public class Controller { } } + /** + * 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 + AttachmentProvider.deleteAllMailboxAttachmentFiles(mProviderContext, 0, mailboxId); + resolver.delete(Message.CONTENT_URI, WHERE_MAILBOX_KEY, + new String[] {Long.toString(mailboxId)}); + } + } finally { + if (c != null) { + c.close(); + } + } + } + + /** + * Returns the attachment Mailbox (where we store eml attachment Emails), creating one + * if necessary + * @return the account's temporary Mailbox + */ + public Mailbox getAttachmentMailbox() { + Cursor c = mProviderContext.getContentResolver().query(Mailbox.CONTENT_URI, + Mailbox.CONTENT_PROJECTION, WHERE_TYPE_ATTACHMENT, null, null); + try { + if (c.moveToFirst()) { + return new Mailbox().restore(c); + } + } finally { + c.close(); + } + Mailbox m = new Mailbox(); + m.mAccountKey = ATTACHMENT_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; + } + + /** + * 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; + } + /** * Enable/disable logging for external sync services * diff --git a/src/com/android/email/MessagingController.java b/src/com/android/email/MessagingController.java index 87c57b285..769bc18ea 100644 --- a/src/com/android/email/MessagingController.java +++ b/src/com/android/email/MessagingController.java @@ -435,11 +435,11 @@ public class MessagingController implements Runnable { } } - private void saveOrUpdate(EmailContent content) { + private void saveOrUpdate(EmailContent content, Context context) { if (content.isSaved()) { - content.update(mContext, content.toContentValues()); + content.update(context, content.toContentValues()); } else { - content.save(mContext); + content.save(context); } } @@ -619,7 +619,7 @@ public class MessagingController implements Runnable { LegacyConversions.updateMessageFields(localMessage, message, account.mId, folder.mId); // Commit the message to the local store - saveOrUpdate(localMessage); + saveOrUpdate(localMessage, mContext); // Track the "new" ness of the downloaded message if (!message.isSet(Flag.SEEN)) { newMessages.add(message); @@ -916,7 +916,7 @@ public class MessagingController implements Runnable { /** * Copy one downloaded message (which may have partially-loaded sections) - * into a provider message + * into a newly created EmailProvider Message, given the account and mailbox * * @param message the remote message we've just downloaded * @param account the account it will be stored into @@ -924,47 +924,59 @@ public class MessagingController implements Runnable { * @param loadStatus when complete, the message will be marked with this status (e.g. * EmailContent.Message.LOADED) */ - private void copyOneMessageToProvider(Message message, EmailContent.Account account, + public void copyOneMessageToProvider(Message message, EmailContent.Account account, EmailContent.Mailbox folder, int loadStatus) { + EmailContent.Message localMessage = null; + Cursor c = null; try { - EmailContent.Message localMessage = null; - Cursor c = null; - try { - c = mContext.getContentResolver().query( - EmailContent.Message.CONTENT_URI, - EmailContent.Message.CONTENT_PROJECTION, - EmailContent.MessageColumns.ACCOUNT_KEY + "=?" + - " AND " + MessageColumns.MAILBOX_KEY + "=?" + - " AND " + SyncColumns.SERVER_ID + "=?", - new String[] { - String.valueOf(account.mId), - String.valueOf(folder.mId), - String.valueOf(message.getUid()) - }, - null); - if (c.moveToNext()) { - localMessage = EmailContent.getContent(c, EmailContent.Message.class); - } - } finally { - if (c != null) { - c.close(); - } + c = mContext.getContentResolver().query( + EmailContent.Message.CONTENT_URI, + EmailContent.Message.CONTENT_PROJECTION, + EmailContent.MessageColumns.ACCOUNT_KEY + "=?" + + " AND " + MessageColumns.MAILBOX_KEY + "=?" + + " AND " + SyncColumns.SERVER_ID + "=?", + new String[] { + String.valueOf(account.mId), + String.valueOf(folder.mId), + String.valueOf(message.getUid()) + }, + null); + if (c.moveToNext()) { + localMessage = EmailContent.getContent(c, EmailContent.Message.class); + localMessage.mMailboxKey = folder.mId; + localMessage.mAccountKey = account.mId; + copyOneMessageToProvider(message, localMessage, loadStatus, mContext); } - if (localMessage == null) { - Log.d(Email.LOG_TAG, "Could not retrieve message from db, UUID=" - + message.getUid()); - return; + } finally { + if (c != null) { + c.close(); } + } + } - EmailContent.Body body = EmailContent.Body.restoreBodyWithMessageId(mContext, + /** + * Copy one downloaded message (which may have partially-loaded sections) + * into an already-created EmailProvider Message + * + * @param message the remote message we've just downloaded + * @param localMessage the EmailProvider Message, already created + * @param loadStatus when complete, the message will be marked with this status (e.g. + * EmailContent.Message.LOADED) + * @param context the context to be used for EmailProvider + */ + public void copyOneMessageToProvider(Message message, EmailContent.Message localMessage, + int loadStatus, Context context) { + try { + + EmailContent.Body body = EmailContent.Body.restoreBodyWithMessageId(context, localMessage.mId); if (body == null) { body = new EmailContent.Body(); } try { // Copy the fields that are available into the message object - LegacyConversions.updateMessageFields(localMessage, message, account.mId, - folder.mId); + LegacyConversions.updateMessageFields(localMessage, message, + localMessage.mAccountKey, localMessage.mMailboxKey); // Now process body parts & attachments ArrayList viewables = new ArrayList(); @@ -974,11 +986,11 @@ public class MessagingController implements Runnable { LegacyConversions.updateBodyFields(body, localMessage, viewables); // Commit the message & body to the local store immediately - saveOrUpdate(localMessage); - saveOrUpdate(body); + saveOrUpdate(localMessage, context); + saveOrUpdate(body, context); // process (and save) attachments - LegacyConversions.updateAttachments(mContext, localMessage, + LegacyConversions.updateAttachments(context, localMessage, attachments, false); // One last update of message with two updated flags @@ -989,7 +1001,7 @@ public class MessagingController implements Runnable { cv.put(EmailContent.MessageColumns.FLAG_LOADED, localMessage.mFlagLoaded); Uri uri = ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, localMessage.mId); - mContext.getContentResolver().update(uri, cv, null, null); + context.getContentResolver().update(uri, cv, null, null); } catch (MessagingException me) { Log.e(Email.LOG_TAG, "Error while copying downloaded message." + me); diff --git a/src/com/android/email/activity/MessageView.java b/src/com/android/email/activity/MessageView.java index b2b8ff317..4826ff1e2 100644 --- a/src/com/android/email/activity/MessageView.java +++ b/src/com/android/email/activity/MessageView.java @@ -17,9 +17,9 @@ package com.android.email.activity; import com.android.email.Controller; +import com.android.email.ControllerResultUiThreadWrapper; import com.android.email.Email; import com.android.email.R; -import com.android.email.ControllerResultUiThreadWrapper; import com.android.email.Utility; import com.android.email.mail.Address; import com.android.email.mail.MeetingInfo; @@ -89,7 +89,10 @@ import java.util.regex.Pattern; public class MessageView extends Activity implements OnClickListener { private static final String EXTRA_MESSAGE_ID = "com.android.email.MessageView_message_id"; private static final String EXTRA_MAILBOX_ID = "com.android.email.MessageView_mailbox_id"; - /* package */ static final String EXTRA_DISABLE_REPLY = "com.android.email.MessageView_disable_reply"; + private static final String EXTRA_ORIGINAL_MAILBOX_ID = + "com.android.email.MessageView_original_mailbox_id"; + /* package */ static final String EXTRA_DISABLE_REPLY = + "com.android.email.MessageView_disable_reply"; // for saveInstanceState() private static final String STATE_MESSAGE_ID = "messageId"; @@ -141,6 +144,8 @@ public class MessageView extends Activity implements OnClickListener { private long mMailboxId; private Message mMessage; private long mWaitForLoadMessageId; + // Set to true for messages that are attachments + private boolean mAttachmentMessageFlag = false; private LoadMessageTask mLoadMessageTask; private LoadBodyTask mLoadBodyTask; @@ -384,6 +389,10 @@ public class MessageView extends Activity implements OnClickListener { mMessageContentView.destroy(); mMessageContentView = null; // the cursor was closed in onPause() + // If we're leaving a non-attachment message, delete any/all attachment messages + if (!mAttachmentMessageFlag) { + mController.deleteAttachmentMessages(); + } } private void onDelete() { @@ -875,7 +884,6 @@ public class MessageView extends Activity implements OnClickListener { * @param attachment A single attachment loaded from the provider */ private void addAttachment(Attachment attachment) { - AttachmentInfo attachmentInfo = new AttachmentInfo(); attachmentInfo.size = attachment.mSize; attachmentInfo.contentType = @@ -1056,18 +1064,41 @@ public class MessageView extends Activity implements OnClickListener { private long mId; private boolean mOkToFetch; + private Uri mIntentUri; /** * Special constructor to cache some local info */ public LoadMessageTask(long messageId, boolean okToFetch) { + mIntentUri = getIntent().getData(); mId = messageId; mOkToFetch = okToFetch; + mAttachmentMessageFlag = (mIntentUri != null); } + /** + * There will either be a Uri in the Intent (i.e. whose content is the Message to be + * loaded), or mId will be holding the id of the Message as stored in the provider. + * If we're loading via Uri, the Controller does the actual message parsing and storage, + * and we setup the message id and mailbox id based on the result; forward and reply are + * disabled for messages loaded via Uri + */ @Override protected Message doInBackground(Void... params) { - if (mId == Long.MIN_VALUE) { + // If we have a URI, then we were opened via an intent filter (e.g. an attachment that + // has a mime type that we can handle (e.g. message/rfc822). + if (mIntentUri != null) { + final Activity activity = MessageView.this; + // Put up a toast; this can take a little while... + Utility.showToast(activity, R.string.message_view_parse_message_toast); + Message msg = mController.loadMessageFromUri(mIntentUri); + if (msg == null) { + // Indicate that the attachment couldn't be loaded + Utility.showToast(activity, R.string.message_view_display_attachment_toast); + return null; + } + return msg; + } else if (mId == Long.MIN_VALUE) { return null; } return Message.restoreMessageWithId(MessageView.this, mId); @@ -1094,6 +1125,9 @@ public class MessageView extends Activity implements OnClickListener { } return; } + mMessageId = message.mId; + mMailboxId = message.mMailboxKey; + mDisableReplyAndForward = mIntentUri != null; reloadUiFromMessage(message, mOkToFetch); startPresenceCheck(); } diff --git a/src/com/android/email/mail/internet/MimeMessage.java b/src/com/android/email/mail/internet/MimeMessage.java index 67cc897cb..ee2ce9717 100644 --- a/src/com/android/email/mail/internet/MimeMessage.java +++ b/src/com/android/email/mail/internet/MimeMessage.java @@ -423,7 +423,7 @@ public class MimeMessage extends Message { return; } if (mExtendedHeader == null) { - mExtendedHeader = new MimeHeader(); + mExtendedHeader = new MimeHeader(); } mExtendedHeader.setHeader(name, END_OF_LINE.matcher(value).replaceAll("")); } @@ -433,7 +433,7 @@ public class MimeMessage extends Message { * * @param name Extended header name * @return header value - null if header does not exist - * @throws MessagingException + * @throws MessagingException */ public String getExtendedHeader(String name) throws MessagingException { if (mExtendedHeader == null) { diff --git a/src/com/android/email/mail/store/Pop3Store.java b/src/com/android/email/mail/store/Pop3Store.java index 9990fde5b..f5d65f36a 100644 --- a/src/com/android/email/mail/store/Pop3Store.java +++ b/src/com/android/email/mail/store/Pop3Store.java @@ -935,7 +935,7 @@ public class Pop3Store extends Store { } } - class Pop3Message extends MimeMessage { + public static class Pop3Message extends MimeMessage { public Pop3Message(String uid, Pop3Folder folder) throws MessagingException { mUid = uid; mFolder = folder; @@ -947,7 +947,7 @@ public class Pop3Store extends Store { } @Override - protected void parse(InputStream in) throws IOException, MessagingException { + public void parse(InputStream in) throws IOException, MessagingException { super.parse(in); } diff --git a/src/com/android/email/provider/EmailContent.java b/src/com/android/email/provider/EmailContent.java index 3a8b194e6..890102d82 100644 --- a/src/com/android/email/provider/EmailContent.java +++ b/src/com/android/email/provider/EmailContent.java @@ -2003,6 +2003,10 @@ public abstract class EmailContent { public static final int TYPE_TASKS = 0x43; public static final int TYPE_EAS_ACCOUNT_MAILBOX = 0x44; + public static final int TYPE_NOT_SYNCABLE = 0x100; + // A mailbox that holds Messages that are attachments + public static final int TYPE_ATTACHMENT = 0x101; + // Bit field flags public static final int FLAG_HAS_CHILDREN = 1<<0; public static final int FLAG_CHILDREN_VISIBLE = 1<<1; diff --git a/src/com/android/exchange/SyncManager.java b/src/com/android/exchange/SyncManager.java index 651cf6352..8c15fbf53 100644 --- a/src/com/android/exchange/SyncManager.java +++ b/src/com/android/exchange/SyncManager.java @@ -1737,7 +1737,6 @@ public class SyncManager extends Service implements Runnable { public void onCreate() { synchronized (sSyncLock) { alwaysLog("!!! EAS SyncManager, onCreate"); - // If we're in the process of shutting down, try again in 5 seconds if (sStop) { return; } @@ -2187,15 +2186,24 @@ public class SyncManager extends Service implements Runnable { serviceRequest(mailboxId, 5*SECONDS, reason); } + /** + * Return a boolean indicating whether the mailbox can be synced + * @param m the mailbox + * @return whether or not the mailbox can be synced + */ + static /*package*/ boolean isSyncable(Mailbox m) { + if (m == null || m.mType == Mailbox.TYPE_DRAFTS || m.mType == Mailbox.TYPE_OUTBOX || + m.mType >= Mailbox.TYPE_NOT_SYNCABLE) { + return false; + } + return true; + } + static public void serviceRequest(long mailboxId, long ms, int reason) { SyncManager syncManager = INSTANCE; if (syncManager == null) return; Mailbox m = Mailbox.restoreMailboxWithId(syncManager, mailboxId); - // Never allow manual start of Drafts or Outbox via serviceRequest - if (m == null || m.mType == Mailbox.TYPE_DRAFTS || m.mType == Mailbox.TYPE_OUTBOX) { - log("Ignoring serviceRequest for drafts/outbox/null mailbox"); - return; - } + if (!isSyncable(m)) return; try { AbstractSyncService service = syncManager.mServiceMap.get(mailboxId); if (service != null) { diff --git a/tests/src/com/android/email/ControllerProviderOpsTests.java b/tests/src/com/android/email/ControllerProviderOpsTests.java index abb6ed13c..30fd3e58d 100644 --- a/tests/src/com/android/email/ControllerProviderOpsTests.java +++ b/tests/src/com/android/email/ControllerProviderOpsTests.java @@ -16,16 +16,23 @@ package com.android.email; +import com.android.email.mail.MessagingException; +import com.android.email.mail.transport.Rfc822Output; import com.android.email.provider.EmailContent; import com.android.email.provider.EmailProvider; import com.android.email.provider.ProviderTestUtils; import com.android.email.provider.EmailContent.Account; +import com.android.email.provider.EmailContent.Body; import com.android.email.provider.EmailContent.Mailbox; import com.android.email.provider.EmailContent.Message; import android.content.Context; +import android.net.Uri; import android.test.ProviderTestCase2; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; import java.util.Locale; /** @@ -282,6 +289,63 @@ public class ControllerProviderOpsTests extends ProviderTestCase2 assertFalse(message1get.mFlagFavorite); } + public void testGetAndDeleteAttachmentMailbox() { + Controller ct = new TestController(mProviderContext, mContext); + Mailbox box = ct.getAttachmentMailbox(); + assertNotNull(box); + Mailbox anotherBox = ct.getAttachmentMailbox(); + assertNotNull(anotherBox); + // We should always get back the same Mailbox row + assertEquals(box.mId, anotherBox.mId); + // Add two messages to this mailbox + ProviderTestUtils.setupMessage("message1", 0, box.mId, false, true, + mProviderContext); + ProviderTestUtils.setupMessage("message2", 0, box.mId, false, true, + mProviderContext); + // Make sure we can find them where they are expected + assertEquals(2, EmailContent.count(mProviderContext, Message.CONTENT_URI, + Message.MAILBOX_KEY + "=?", new String[] {Long.toString(box.mId)})); + // Delete them + ct.deleteAttachmentMessages(); + // Make sure they're gone + assertEquals(0, EmailContent.count(mProviderContext, Message.CONTENT_URI, + Message.MAILBOX_KEY + "=?", new String[] {Long.toString(box.mId)})); + } + + public void testLoadMessageFromUri() throws IOException, MessagingException { + // Create a simple message + Message msg = new Message(); + String text = "This is some text"; + msg.mText = text; + String sender = "sender@host.com"; + msg.mFrom = sender; + // Save this away + msg.save(mProviderContext); + + // Write out the message in rfc822 format + File outputFile = File.createTempFile("message", "tmp", mContext.getFilesDir()); + assertNotNull(outputFile); + FileOutputStream outputStream = new FileOutputStream(outputFile); + Rfc822Output.writeTo(mProviderContext, msg.mId, outputStream, false, false); + outputStream.close(); + + // Load the message via Controller and a Uri + Controller ct = new TestController(mProviderContext, mContext); + Message loadedMsg = ct.loadMessageFromUri(Uri.fromFile(outputFile)); + + // Check server id, mailbox key, account key, and from + assertNotNull(loadedMsg); + assertTrue(loadedMsg.mServerId.startsWith(Controller.ATTACHMENT_MESSAGE_UID_PREFIX)); + Mailbox box = ct.getAttachmentMailbox(); + assertNotNull(box); + assertEquals(box.mId, loadedMsg.mMailboxKey); + assertEquals(0, loadedMsg.mAccountKey); + assertEquals(loadedMsg.mFrom, sender); + // Check body text + String loadedMsgText = Body.restoreBodyTextWithMessageId(mProviderContext, loadedMsg.mId); + assertEquals(text, loadedMsgText); + } + /** * TODO: releasing associated data (e.g. attachments, embedded images) */ diff --git a/tests/src/com/android/exchange/SyncManagerAccountTests.java b/tests/src/com/android/exchange/SyncManagerAccountTests.java index 3b67dc181..6a325b5ba 100644 --- a/tests/src/com/android/exchange/SyncManagerAccountTests.java +++ b/tests/src/com/android/exchange/SyncManagerAccountTests.java @@ -211,4 +211,24 @@ public class SyncManagerAccountTests extends AccountTestCase { assertEquals(0, errorMap.keySet().size()); } + public void testIsSyncable() { + Context context = mMockContext; + Account acct1 = ProviderTestUtils.setupAccount("acct1", true, context); + Mailbox box1 = ProviderTestUtils.setupMailbox("box1", acct1.mId, true, context, + Mailbox.TYPE_DRAFTS); + Mailbox box2 = ProviderTestUtils.setupMailbox("box2", acct1.mId, true, context, + Mailbox.TYPE_OUTBOX); + Mailbox box3 = ProviderTestUtils.setupMailbox("box2", acct1.mId, true, context, + Mailbox.TYPE_ATTACHMENT); + Mailbox box4 = ProviderTestUtils.setupMailbox("box2", acct1.mId, true, context, + Mailbox.TYPE_NOT_SYNCABLE + 64); + Mailbox box5 = ProviderTestUtils.setupMailbox("box2", acct1.mId, true, context, + Mailbox.TYPE_MAIL); + assertFalse(SyncManager.isSyncable(null)); + assertFalse(SyncManager.isSyncable(box1)); + assertFalse(SyncManager.isSyncable(box2)); + assertFalse(SyncManager.isSyncable(box3)); + assertFalse(SyncManager.isSyncable(box4)); + assertTrue(SyncManager.isSyncable(box5)); + } }