2012-06-28 19:16:59 +00:00
|
|
|
/*
|
|
|
|
* Copyright (C) 2012 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.provider;
|
|
|
|
|
2014-05-12 18:33:43 +00:00
|
|
|
import android.annotation.TargetApi;
|
2012-06-28 19:16:59 +00:00
|
|
|
import android.content.ContentUris;
|
|
|
|
import android.content.ContentValues;
|
|
|
|
import android.content.Context;
|
|
|
|
import android.database.Cursor;
|
|
|
|
import android.net.Uri;
|
2014-05-12 18:33:43 +00:00
|
|
|
import android.os.ParcelFileDescriptor;
|
2012-06-28 19:16:59 +00:00
|
|
|
|
|
|
|
import com.android.email.LegacyConversions;
|
|
|
|
import com.android.emailcommon.Logging;
|
|
|
|
import com.android.emailcommon.internet.MimeUtility;
|
|
|
|
import com.android.emailcommon.mail.Message;
|
|
|
|
import com.android.emailcommon.mail.MessagingException;
|
|
|
|
import com.android.emailcommon.mail.Part;
|
|
|
|
import com.android.emailcommon.provider.Account;
|
|
|
|
import com.android.emailcommon.provider.EmailContent;
|
2013-09-21 00:34:09 +00:00
|
|
|
import com.android.emailcommon.provider.EmailContent.Attachment;
|
2012-06-28 19:16:59 +00:00
|
|
|
import com.android.emailcommon.provider.EmailContent.MessageColumns;
|
|
|
|
import com.android.emailcommon.provider.EmailContent.SyncColumns;
|
|
|
|
import com.android.emailcommon.provider.Mailbox;
|
|
|
|
import com.android.emailcommon.utility.ConversionUtilities;
|
2013-05-26 04:32:32 +00:00
|
|
|
import com.android.mail.utils.LogUtils;
|
2014-05-12 18:33:43 +00:00
|
|
|
import com.android.mail.utils.Utils;
|
2012-06-28 19:16:59 +00:00
|
|
|
|
2015-05-01 19:35:23 +00:00
|
|
|
import java.io.InputStream;
|
2012-06-28 19:16:59 +00:00
|
|
|
import java.io.IOException;
|
|
|
|
import java.util.ArrayList;
|
|
|
|
|
|
|
|
public class Utilities {
|
|
|
|
/**
|
|
|
|
* Copy one downloaded message (which may have partially-loaded sections)
|
|
|
|
* 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
|
|
|
|
* @param folder the mailbox it will be stored into
|
|
|
|
* @param loadStatus when complete, the message will be marked with this status (e.g.
|
|
|
|
* EmailContent.Message.LOADED)
|
|
|
|
*/
|
|
|
|
public static void copyOneMessageToProvider(Context context, Message message, Account account,
|
|
|
|
Mailbox folder, int loadStatus) {
|
|
|
|
EmailContent.Message localMessage = null;
|
|
|
|
Cursor c = null;
|
|
|
|
try {
|
|
|
|
c = context.getContentResolver().query(
|
|
|
|
EmailContent.Message.CONTENT_URI,
|
|
|
|
EmailContent.Message.CONTENT_PROJECTION,
|
|
|
|
EmailContent.MessageColumns.ACCOUNT_KEY + "=?" +
|
2012-08-02 17:53:40 +00:00
|
|
|
" AND " + MessageColumns.MAILBOX_KEY + "=?" +
|
|
|
|
" AND " + SyncColumns.SERVER_ID + "=?",
|
|
|
|
new String[] {
|
2012-06-28 19:16:59 +00:00
|
|
|
String.valueOf(account.mId),
|
|
|
|
String.valueOf(folder.mId),
|
|
|
|
String.valueOf(message.getUid())
|
|
|
|
},
|
|
|
|
null);
|
2012-08-02 17:53:40 +00:00
|
|
|
if (c == null) {
|
|
|
|
return;
|
|
|
|
} else if (c.moveToNext()) {
|
2014-05-08 20:07:54 +00:00
|
|
|
localMessage = EmailContent.getContent(context, c, EmailContent.Message.class);
|
2012-08-02 17:53:40 +00:00
|
|
|
} else {
|
|
|
|
localMessage = new EmailContent.Message();
|
2012-06-28 19:16:59 +00:00
|
|
|
}
|
2012-08-02 17:53:40 +00:00
|
|
|
localMessage.mMailboxKey = folder.mId;
|
|
|
|
localMessage.mAccountKey = account.mId;
|
|
|
|
copyOneMessageToProvider(context, message, localMessage, loadStatus);
|
2012-06-28 19:16:59 +00:00
|
|
|
} finally {
|
|
|
|
if (c != null) {
|
|
|
|
c.close();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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 static void copyOneMessageToProvider(Context context, Message message,
|
|
|
|
EmailContent.Message localMessage, int loadStatus) {
|
|
|
|
try {
|
2012-08-02 17:53:40 +00:00
|
|
|
EmailContent.Body body = null;
|
|
|
|
if (localMessage.mId != EmailContent.Message.NO_MESSAGE) {
|
|
|
|
body = EmailContent.Body.restoreBodyWithMessageId(context, localMessage.mId);
|
|
|
|
}
|
2012-06-28 19:16:59 +00:00
|
|
|
if (body == null) {
|
|
|
|
body = new EmailContent.Body();
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
// Copy the fields that are available into the message object
|
|
|
|
LegacyConversions.updateMessageFields(localMessage, message,
|
|
|
|
localMessage.mAccountKey, localMessage.mMailboxKey);
|
|
|
|
|
|
|
|
// Now process body parts & attachments
|
|
|
|
ArrayList<Part> viewables = new ArrayList<Part>();
|
|
|
|
ArrayList<Part> attachments = new ArrayList<Part>();
|
|
|
|
MimeUtility.collectParts(message, viewables, attachments);
|
|
|
|
|
2015-05-01 19:35:23 +00:00
|
|
|
// Don't close the viewables attachment InputStream yet
|
2013-06-04 22:08:18 +00:00
|
|
|
final ConversionUtilities.BodyFieldData data =
|
2015-05-01 19:35:23 +00:00
|
|
|
ConversionUtilities.parseBodyFields(viewables, false);
|
2013-06-04 22:08:18 +00:00
|
|
|
|
|
|
|
// set body and local message values
|
|
|
|
localMessage.setFlags(data.isQuotedReply, data.isQuotedForward);
|
|
|
|
localMessage.mSnippet = data.snippet;
|
|
|
|
body.mTextContent = data.textContent;
|
|
|
|
body.mHtmlContent = data.htmlContent;
|
2012-06-28 19:16:59 +00:00
|
|
|
|
|
|
|
// Commit the message & body to the local store immediately
|
|
|
|
saveOrUpdate(localMessage, context);
|
2012-08-02 17:53:40 +00:00
|
|
|
body.mMessageKey = localMessage.mId;
|
2012-06-28 19:16:59 +00:00
|
|
|
saveOrUpdate(body, context);
|
|
|
|
|
|
|
|
// process (and save) attachments
|
2013-07-30 00:02:27 +00:00
|
|
|
if (loadStatus != EmailContent.Message.FLAG_LOADED_PARTIAL
|
|
|
|
&& loadStatus != EmailContent.Message.FLAG_LOADED_UNKNOWN) {
|
|
|
|
// TODO(pwestbro): What should happen with unknown status?
|
2012-08-02 17:53:40 +00:00
|
|
|
LegacyConversions.updateAttachments(context, localMessage, attachments);
|
2014-02-21 19:14:48 +00:00
|
|
|
LegacyConversions.updateInlineAttachments(context, localMessage, viewables);
|
2012-08-02 17:53:40 +00:00
|
|
|
} else {
|
|
|
|
EmailContent.Attachment att = new EmailContent.Attachment();
|
2013-09-21 00:34:09 +00:00
|
|
|
// Since we haven't actually loaded the attachment, we're just putting
|
|
|
|
// a dummy placeholder here. When the user taps on it, we'll load the attachment
|
|
|
|
// for real.
|
Fix attachments in search results
b/11294681
We had some really broken logic about handling search
results.
In IMAP search, we would request, in a single pass,
FLAGS, ENVELOPE, STRUCTURE, and BODY_SANE. BODY_SANE means
the first N bytes of message content, whether it be from
the message text or attachments. This is different from how
sync works: In sync, we get FLAGS and ENVELOPE in one pass,
and in a later pass get STRUCTURE and first body part text
for each message.
If the total size of the message exceeded the maximum limit
for BODY_SANE, then we'd mark the message as partial, which
would cause us to create a dummy attachment in copyMessageToProvider().
This is a weird solution to the problem of POP messages not
being completely loaded, because in POP message body and
attachments can't be requested separately, so the dummy attachment
just signified that we needed to fetch more data.
This system fails completely on IMAP, because just fetching the
rest of the body will not get you the attachments.
But even if that code is disabled, attachments in search results
still didn't work properly. For reasons I don't yet understand,
if we requet both STRUCTURE and BODY_SANE at the same time, either
we don't received the full attachment metadata, or we ignore it, and
only use the attachments whose contents could actually fit in the
limit imposed by BODY_SANE. So attachments that didn't fit,
or didn't completely fit, would either be missing or corrupt
and unretriveable.
So, end result: It's not clear why we were trying to load
BODY_SANE all in one pass, unlike how it works for sync.
In fact, the way sync does it now makes a lot of sense: We
load FLAGS and ENVELOPE data (small) and put the in the DB
immediately so they can be displayed. In the second pass we
load the (potentially large) structure and message body. If this
is the right solution for sync, it's probably the right solution
for search. So now, that's what we do.
There is cleanup I'd like to do post MR1: Some code is duplicated
between sync and search that could be consolidated, but we're in
low risk mode now so I only changed search code.
Change-Id: I11475e290cda04b91f76d38ba952679e8e8964d5
2013-11-06 05:19:41 +00:00
|
|
|
// TODO: This is not a great way to model this. What we're saying is, we don't
|
|
|
|
// have the complete message, without paying any attention to what we do have.
|
|
|
|
// Did the main body exceed the maximum initial size? If so, we really might
|
|
|
|
// not have any attachments at all, and we just need a button somewhere that
|
|
|
|
// says "load the rest of the message".
|
|
|
|
// Or, what if we were able to load some, but not all of the attachments?
|
|
|
|
// Then we should ideally not be dropping the data we have on the floor.
|
|
|
|
// Also, what behavior we have here really should be based on what protocol
|
|
|
|
// we're dealing with. If it's POP, then we don't actually know how many
|
|
|
|
// attachments we have until we've loaded the complete message.
|
|
|
|
// If it's IMAP, we should know that, and we should load all attachment
|
|
|
|
// metadata we can get, regardless of whether or not we have the complete
|
|
|
|
// message body.
|
2013-09-21 00:34:09 +00:00
|
|
|
att.mFileName = "";
|
2012-08-02 17:53:40 +00:00
|
|
|
att.mSize = message.getSize();
|
|
|
|
att.mMimeType = "text/plain";
|
|
|
|
att.mMessageKey = localMessage.mId;
|
|
|
|
att.mAccountKey = localMessage.mAccountKey;
|
2013-09-21 00:34:09 +00:00
|
|
|
att.mFlags = Attachment.FLAG_DUMMY_ATTACHMENT;
|
2012-08-02 17:53:40 +00:00
|
|
|
att.save(context);
|
|
|
|
localMessage.mFlagAttachment = true;
|
|
|
|
}
|
2012-06-28 19:16:59 +00:00
|
|
|
|
2015-05-01 19:35:23 +00:00
|
|
|
// Close any parts that may still be open
|
|
|
|
for (final Part part : viewables) {
|
|
|
|
if (part.getBody() == null) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
InputStream is = part.getBody().getInputStream();
|
|
|
|
if (is != null) {
|
|
|
|
is.close();
|
|
|
|
}
|
2015-06-09 10:53:07 +00:00
|
|
|
} catch (IOException | MessagingException io) {
|
2015-05-01 19:35:23 +00:00
|
|
|
// Ignore
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-06-28 19:16:59 +00:00
|
|
|
// One last update of message with two updated flags
|
|
|
|
localMessage.mFlagLoaded = loadStatus;
|
|
|
|
|
|
|
|
ContentValues cv = new ContentValues();
|
|
|
|
cv.put(EmailContent.MessageColumns.FLAG_ATTACHMENT, localMessage.mFlagAttachment);
|
|
|
|
cv.put(EmailContent.MessageColumns.FLAG_LOADED, localMessage.mFlagLoaded);
|
|
|
|
Uri uri = ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI,
|
|
|
|
localMessage.mId);
|
|
|
|
context.getContentResolver().update(uri, cv, null, null);
|
|
|
|
|
|
|
|
} catch (MessagingException me) {
|
2013-05-26 04:32:32 +00:00
|
|
|
LogUtils.e(Logging.LOG_TAG, "Error while copying downloaded message." + me);
|
2012-06-28 19:16:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
} catch (RuntimeException rte) {
|
2013-05-26 04:32:32 +00:00
|
|
|
LogUtils.e(Logging.LOG_TAG, "Error while storing downloaded message." + rte.toString());
|
2012-06-28 19:16:59 +00:00
|
|
|
} catch (IOException ioe) {
|
2013-05-26 04:32:32 +00:00
|
|
|
LogUtils.e(Logging.LOG_TAG, "Error while storing attachment." + ioe.toString());
|
2012-06-28 19:16:59 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public static void saveOrUpdate(EmailContent content, Context context) {
|
|
|
|
if (content.isSaved()) {
|
|
|
|
content.update(context, content.toContentValues());
|
|
|
|
} else {
|
|
|
|
content.save(context);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-05-12 18:33:43 +00:00
|
|
|
/**
|
|
|
|
* Converts a string representing a file mode, such as "rw", into a bitmask suitable for use
|
|
|
|
* with {@link android.os.ParcelFileDescriptor#open}.
|
|
|
|
* <p>
|
|
|
|
* @param mode The string representation of the file mode.
|
|
|
|
* @return A bitmask representing the given file mode.
|
|
|
|
* @throws IllegalArgumentException if the given string does not match a known file mode.
|
|
|
|
*/
|
|
|
|
@TargetApi(19)
|
|
|
|
public static int parseMode(String mode) {
|
|
|
|
if (Utils.isRunningKitkatOrLater()) {
|
|
|
|
return ParcelFileDescriptor.parseMode(mode);
|
|
|
|
}
|
|
|
|
final int modeBits;
|
|
|
|
if ("r".equals(mode)) {
|
|
|
|
modeBits = ParcelFileDescriptor.MODE_READ_ONLY;
|
|
|
|
} else if ("w".equals(mode) || "wt".equals(mode)) {
|
|
|
|
modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY
|
|
|
|
| ParcelFileDescriptor.MODE_CREATE
|
|
|
|
| ParcelFileDescriptor.MODE_TRUNCATE;
|
|
|
|
} else if ("wa".equals(mode)) {
|
|
|
|
modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY
|
|
|
|
| ParcelFileDescriptor.MODE_CREATE
|
|
|
|
| ParcelFileDescriptor.MODE_APPEND;
|
|
|
|
} else if ("rw".equals(mode)) {
|
|
|
|
modeBits = ParcelFileDescriptor.MODE_READ_WRITE
|
|
|
|
| ParcelFileDescriptor.MODE_CREATE;
|
|
|
|
} else if ("rwt".equals(mode)) {
|
|
|
|
modeBits = ParcelFileDescriptor.MODE_READ_WRITE
|
|
|
|
| ParcelFileDescriptor.MODE_CREATE
|
|
|
|
| ParcelFileDescriptor.MODE_TRUNCATE;
|
|
|
|
} else {
|
|
|
|
throw new IllegalArgumentException("Bad mode '" + mode + "'");
|
|
|
|
}
|
|
|
|
return modeBits;
|
|
|
|
}
|
2012-06-28 19:16:59 +00:00
|
|
|
}
|