Fetch email bodies using ContentResolver#openInputStream

This brings us further along the path to storing email bodies
outside of the database.

Change-Id: I96296114ade0d561df724878ed92999306bcd176
This commit is contained in:
Tony Mantler 2014-05-08 13:07:54 -07:00
parent 7366516bb0
commit 2f288864b6
9 changed files with 197 additions and 233 deletions

View File

@ -28,12 +28,9 @@ import android.database.ContentObservable;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.Looper;
import android.os.Parcel;
import android.os.ParcelFileDescriptor;
import android.os.ParcelFileDescriptor.AutoCloseInputStream;
import android.os.Parcelable;
import android.os.RemoteException;
import android.provider.BaseColumns;
@ -49,8 +46,8 @@ import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.io.IOUtils;
import java.io.File;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
@ -134,7 +131,13 @@ public abstract class EmailContent {
// Write the Content into a ContentValues container
public abstract ContentValues toContentValues();
// Read the Content from a ContentCursor
public abstract void restore (Cursor cursor);
public abstract void restore(Cursor cursor);
// Same as above, with the addition of a context to retrieve extra content.
// Body uses this to fetch the email body html/text from the provider bypassing the cursor
// Not always safe to call on the UI thread.
public void restore(Context context, Cursor cursor) {
restore(cursor);
}
public static String EMAIL_PACKAGE_NAME;
@ -239,7 +242,7 @@ public abstract class EmailContent {
if (c == null) throw new ProviderUnavailableException();
try {
if (c.moveToFirst()) {
final T content = getContent(c, klass);
final T content = getContent(context, c, klass);
if (observer != null) {
content.registerObserver(context, observer);
}
@ -351,11 +354,12 @@ public abstract class EmailContent {
// The Content sub class must have a no-arg constructor
static public <T extends EmailContent> T getContent(Cursor cursor, Class<T> klass) {
static public <T extends EmailContent> T getContent(final Context context, final Cursor cursor,
final Class<T> klass) {
try {
T content = klass.newInstance();
content.mId = cursor.getLong(0);
content.restore(cursor);
content.restore(context, cursor);
return content;
} catch (IllegalAccessException e) {
e.printStackTrace();
@ -433,10 +437,14 @@ public abstract class EmailContent {
public interface BodyColumns extends BaseColumns {
// Foreign key to the message corresponding to this body
public static final String MESSAGE_KEY = "messageKey";
// The html content itself
// The html content itself, not returned on query
public static final String HTML_CONTENT = "htmlContent";
// The plain text content itself
// The html content URI, for ContentResolver#openFileDescriptor()
public static final String HTML_CONTENT_URI = "htmlContentUri";
// The plain text content itself, not returned on query
public static final String TEXT_CONTENT = "textContent";
// The text content URI, for ContentResolver#openFileDescriptor()
public static final String TEXT_CONTENT_URI = "textContentUri";
// Replied-to or forwarded body (in html form)
@Deprecated
public static final String HTML_REPLY = "htmlReply";
@ -465,40 +473,22 @@ public abstract class EmailContent {
CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/body");
}
/**
* Following values are for EmailMessageCursor
*/
// Value is an int specifying the row to get
public static final String RESPOND_COMMAND_GET_HTML_PIPE = "EmailMessageCursor.getHtmlPipe";
public static final String RESPOND_COMMAND_GET_TEXT_PIPE = "EmailMessageCursor.getTextPipe";
// Value returned is a ParcelFileDescriptor pipe, or null if no content is present
public static final String RESPOND_RESULT_HTML_PIPE_KEY = "EmailMessageCursor.htmlPipe";
public static final String RESPOND_RESULT_TEXT_PIPE_KEY = "EmailMessageCursor.textPipe";
public static final String[] CONTENT_PROJECTION = new String[] {
BaseColumns._ID,
BodyColumns.MESSAGE_KEY,
BodyColumns.HTML_CONTENT,
BodyColumns.TEXT_CONTENT,
BodyColumns.HTML_CONTENT_URI,
BodyColumns.TEXT_CONTENT_URI,
BodyColumns.SOURCE_MESSAGE_KEY,
BodyColumns.QUOTED_TEXT_START_POS
};
public static final int CONTENT_ID_COLUMN = 0;
public static final int CONTENT_MESSAGE_KEY_COLUMN = 1;
public static final int CONTENT_HTML_CONTENT_COLUMN = 2;
public static final int CONTENT_TEXT_CONTENT_COLUMN = 3;
public static final int CONTENT_HTML_URI_COLUMN = 2;
public static final int CONTENT_TEXT_URI_COLUMN = 3;
public static final int CONTENT_SOURCE_KEY_COLUMN = 4;
public static final int CONTENT_QUOTED_TEXT_START_POS_COLUMN = 5;
public static final String[] COMMON_PROJECTION_TEXT = new String[] {
BaseColumns._ID, BodyColumns.TEXT_CONTENT
};
public static final String[] COMMON_PROJECTION_HTML = new String[] {
BaseColumns._ID, BodyColumns.HTML_CONTENT
};
public static final int COMMON_PROJECTION_COLUMN_TEXT = 1;
private static final String[] PROJECTION_SOURCE_KEY =
new String[] {BaseColumns._ID, BodyColumns.SOURCE_MESSAGE_KEY};
@ -533,10 +523,10 @@ public abstract class EmailContent {
* @param cursor a cursor which must NOT be null
* @return the Body as restored from the cursor
*/
private static Body restoreBodyWithCursor(Cursor cursor) {
private static Body restoreBodyWithCursor(final Context context, final Cursor cursor) {
try {
if (cursor.moveToFirst()) {
return getContent(cursor, Body.class);
return getContent(context, cursor, Body.class);
} else {
return null;
}
@ -545,20 +535,12 @@ public abstract class EmailContent {
}
}
public static Body restoreBodyWithId(Context context, long id) {
Uri u = ContentUris.withAppendedId(Body.CONTENT_URI, id);
Cursor c = context.getContentResolver().query(u, Body.CONTENT_PROJECTION,
null, null, null);
if (c == null) throw new ProviderUnavailableException();
return restoreBodyWithCursor(c);
}
public static Body restoreBodyWithMessageId(Context context, long messageId) {
Cursor c = context.getContentResolver().query(Body.CONTENT_URI,
Body.CONTENT_PROJECTION, BodyColumns.MESSAGE_KEY + "=?",
new String[] {Long.toString(messageId)}, null);
if (c == null) throw new ProviderUnavailableException();
return restoreBodyWithCursor(c);
return restoreBodyWithCursor(context, c);
}
/**
@ -596,72 +578,48 @@ public abstract class EmailContent {
0, 0L);
}
private static String restoreTextWithMessageId(Context context, long messageId,
String[] projection) {
Cursor c = context.getContentResolver().query(Body.CONTENT_URI, projection,
BodyColumns.MESSAGE_KEY + "=?", new String[] {Long.toString(messageId)}, null);
if (c == null) throw new ProviderUnavailableException();
try {
if (c.moveToFirst()) {
return c.getString(COMMON_PROJECTION_COLUMN_TEXT);
} else {
return null;
}
} finally {
c.close();
}
}
public static String restoreBodyTextWithMessageId(Context context, long messageId) {
return restoreTextWithMessageId(context, messageId, Body.COMMON_PROJECTION_TEXT);
return readBodyFromProvider(context, EmailContent.CONTENT_URI.buildUpon()
.appendPath("bodyText").appendPath(Long.toString(messageId)).toString());
}
public static String restoreBodyHtmlWithMessageId(Context context, long messageId) {
return restoreTextWithMessageId(context, messageId, Body.COMMON_PROJECTION_HTML);
return readBodyFromProvider(context, EmailContent.CONTENT_URI.buildUpon()
.appendPath("bodyHtml").appendPath(Long.toString(messageId)).toString());
}
private static String readBodyFromPipe(ParcelFileDescriptor d) {
final AutoCloseInputStream htmlInput = new AutoCloseInputStream(d);
private static String readBodyFromProvider(final Context context, final String uri) {
String content = null;
try {
content = IOUtils.toString(htmlInput, "utf8");
} catch (final IOException e) {
LogUtils.e(LogUtils.TAG, e, "IOError while reading message body");
content = null;
} finally {
final InputStream bodyInput =
context.getContentResolver().openInputStream(Uri.parse(uri));
try {
htmlInput.close();
} catch (final IOException e) {
LogUtils.e(LogUtils.TAG, e, "IOError while closing message body");
content = IOUtils.toString(bodyInput);
} finally {
bodyInput.close();
}
} catch (final IOException e) {
LogUtils.v(LogUtils.TAG, e, "Exception while reading body content");
}
return content;
}
@Override
public void restore(Cursor cursor) {
public void restore(final Cursor cursor) {
throw new UnsupportedOperationException("Must have context to restore Body object");
}
@Override
public void restore(final Context context, final Cursor cursor) {
warnIfUiThread();
mBaseUri = EmailContent.Body.CONTENT_URI;
mMessageKey = cursor.getLong(CONTENT_MESSAGE_KEY_COLUMN);
// These get overwritten below if we find a file descriptor in the respond() call,
// but we'll keep this here in case we want to construct a matrix cursor or something
// to build a Body object from.
mHtmlContent = cursor.getString(CONTENT_HTML_CONTENT_COLUMN);
mTextContent = cursor.getString(CONTENT_TEXT_CONTENT_COLUMN);
final int rowId = cursor.getPosition();
final Bundle command = new Bundle(2);
command.putInt(RESPOND_COMMAND_GET_HTML_PIPE, rowId);
command.putInt(RESPOND_COMMAND_GET_TEXT_PIPE, rowId);
final Bundle response = cursor.respond(command);
final ParcelFileDescriptor htmlDescriptor =
response.getParcelable(RESPOND_RESULT_HTML_PIPE_KEY);
if (htmlDescriptor != null) {
mHtmlContent = readBodyFromPipe(htmlDescriptor);
}
final ParcelFileDescriptor textDescriptor =
response.getParcelable(RESPOND_RESULT_TEXT_PIPE_KEY);
if (textDescriptor != null) {
mTextContent = readBodyFromPipe(textDescriptor);
}
mHtmlContent = readBodyFromProvider(context, cursor.getString(CONTENT_HTML_URI_COLUMN));
mTextContent = readBodyFromProvider(context, cursor.getString(CONTENT_TEXT_URI_COLUMN));
mSourceKey = cursor.getLong(CONTENT_SOURCE_KEY_COLUMN);
mQuotedTextStartPos = cursor.getInt(CONTENT_QUOTED_TEXT_START_POS_COLUMN);
}
@ -1314,10 +1272,10 @@ public abstract class EmailContent {
public void setFlags(boolean quotedReply, boolean quotedForward) {
// Set message flags as well
if (quotedReply || quotedForward) {
mFlags &= ~EmailContent.Message.FLAG_TYPE_MASK;
mFlags &= ~Message.FLAG_TYPE_MASK;
mFlags |= quotedReply
? EmailContent.Message.FLAG_TYPE_REPLY
: EmailContent.Message.FLAG_TYPE_FORWARD;
? Message.FLAG_TYPE_REPLY
: Message.FLAG_TYPE_FORWARD;
}
}
}
@ -1647,7 +1605,7 @@ public abstract class EmailContent {
}
public Attachment(Parcel in) {
mBaseUri = EmailContent.Attachment.CONTENT_URI;
mBaseUri = Attachment.CONTENT_URI;
mId = in.readLong();
mFileName = in.readString();
mMimeType = in.readString();

View File

@ -520,7 +520,7 @@ public class Mailbox extends EmailContent implements EmailContent.MailboxColumns
try {
Mailbox mailbox = null;
if (c.moveToFirst()) {
mailbox = getContent(c, Mailbox.class);
mailbox = getContent(context, c, Mailbox.class);
if (c.moveToNext()) {
LogUtils.w(Logging.LOG_TAG, "Multiple mailboxes named \"%s\"", path);
}

View File

@ -1869,11 +1869,11 @@ public abstract class SyncManager extends Service implements Runnable {
// Otherwise, we use the sync interval
long syncInterval = c.getInt(Mailbox.CONTENT_SYNC_INTERVAL_COLUMN);
if (syncInterval == Mailbox.CHECK_INTERVAL_PUSH) {
Mailbox m = EmailContent.getContent(c, Mailbox.class);
Mailbox m = EmailContent.getContent(this, c, Mailbox.class);
requestSync(m, SYNC_PUSH, null);
} else if (mailboxType == Mailbox.TYPE_OUTBOX) {
if (hasSendableMessages(c)) {
Mailbox m = EmailContent.getContent(c, Mailbox.class);
Mailbox m = EmailContent.getContent(this, c, Mailbox.class);
startServiceThread(getServiceForMailbox(this, m));
}
} else if (syncInterval > 0 && syncInterval <= ONE_DAY_MINUTES) {
@ -1883,7 +1883,7 @@ public abstract class SyncManager extends Service implements Runnable {
long toNextSync = syncInterval*MINUTES - sinceLastSync;
String name = c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN);
if (toNextSync <= 0) {
Mailbox m = EmailContent.getContent(c, Mailbox.class);
Mailbox m = EmailContent.getContent(this, c, Mailbox.class);
requestSync(m, SYNC_SCHEDULED, null);
} else if (toNextSync < nextWait) {
nextWait = toNextSync;

View File

@ -21,27 +21,13 @@ import android.database.CursorWrapper;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDoneException;
import android.database.sqlite.SQLiteStatement;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.os.ParcelFileDescriptor.AutoCloseOutputStream;
import android.provider.BaseColumns;
import android.text.TextUtils;
import android.util.SparseArray;
import com.android.emailcommon.provider.EmailContent.Body;
import com.android.emailcommon.provider.EmailContent.BodyColumns;
import com.android.mail.utils.LogUtils;
import java.io.IOException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* This class wraps a cursor for the purpose of bypassing the CursorWindow object for the
* potentially over-sized body content fields. The CursorWindow has a hard limit of 2MB and so a
@ -58,74 +44,34 @@ import java.util.concurrent.atomic.AtomicInteger;
* If we want to address that issue fully, we need to return the body through a
* ParcelFileDescriptor or some other mechanism that doesn't involve passing the data through a
* CursorWindow.
*
* The fromUiQuery param indicates that this EmailMessageCursor object was created from uiQuery().
* This is significant because we know that the body content fields will be retrieved within
* the same process as the provider so we can proceed w/o having to worry about any cross
* process marshalling issues. Secondly, if the request is made from a uiQuery, the _id column
* of the cursor will be a Message._id. If this call is made outside if the uiQuery(), than the
* _id column is actually Body._id so we need to proceed accordingly.
*/
public class EmailMessageCursor extends CursorWrapper {
private static final BlockingQueue<Runnable> sPoolWorkQueue =
new LinkedBlockingQueue<Runnable>(128);
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
public Thread newThread(Runnable r) {
return new Thread(r, "EmailMessageCursor #" + mCount.getAndIncrement());
}
};
/**
* An {@link Executor} that executes tasks which feed text and html email bodies into streams.
*
* It is important that this Executor is private to this class since we don't want to risk
* sharing a common Executor with Threads that *read* from the stream. If that were to happen
* it is possible for all Threads in the Executor to be blocked reads and thus starvation
* occurs.
*/
private static final Executor THREAD_POOL_EXECUTOR
= new ThreadPoolExecutor(1, 5, 1, TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);
private final SparseArray<String> mTextParts;
private final SparseArray<String> mHtmlParts;
private final int mTextColumnIndex;
private final int mHtmlColumnIndex;
private final boolean mFromUiQuery;
public EmailMessageCursor(final Cursor cursor, final SQLiteDatabase db, final String htmlColumn,
final String textColumn, final boolean fromUiQuery) {
final String textColumn) {
super(cursor);
mFromUiQuery = fromUiQuery;
mHtmlColumnIndex = cursor.getColumnIndex(htmlColumn);
mTextColumnIndex = cursor.getColumnIndex(textColumn);
final int cursorSize = cursor.getCount();
mHtmlParts = new SparseArray<String>(cursorSize);
mTextParts = new SparseArray<String>(cursorSize);
final String rowIdColumn;
if (fromUiQuery) {
// In the UI query, the _id column is the id in the message table so it is
// messageKey in the Body table.
rowIdColumn = BodyColumns.MESSAGE_KEY;
} else {
// In the non-UI query, the _id column is the id in the Body table.
rowIdColumn = BaseColumns._ID;
}
// TODO: Load this from the provider instead of duplicating the loading code here
final SQLiteStatement htmlSql = db.compileStatement(
"SELECT " + BodyColumns.HTML_CONTENT +
" FROM " + Body.TABLE_NAME +
" WHERE " + rowIdColumn + "=?"
" WHERE " + BodyColumns.MESSAGE_KEY + "=?"
);
final SQLiteStatement textSql = db.compileStatement(
"SELECT " + BodyColumns.TEXT_CONTENT +
" FROM " + Body.TABLE_NAME +
" WHERE " + rowIdColumn + "=?"
" WHERE " + BodyColumns.MESSAGE_KEY + "=?"
);
while (cursor.moveToNext()) {
@ -155,12 +101,10 @@ public class EmailMessageCursor extends CursorWrapper {
@Override
public String getString(final int columnIndex) {
if (mFromUiQuery) {
if (columnIndex == mHtmlColumnIndex) {
return mHtmlParts.get(getPosition());
} else if (columnIndex == mTextColumnIndex) {
return mTextParts.get(getPosition());
}
if (columnIndex == mHtmlColumnIndex) {
return mHtmlParts.get(getPosition());
} else if (columnIndex == mTextColumnIndex) {
return mTextParts.get(getPosition());
}
return super.getString(columnIndex);
}
@ -175,53 +119,4 @@ public class EmailMessageCursor extends CursorWrapper {
return super.getType(columnIndex);
}
}
private static ParcelFileDescriptor createPipeAndFillAsync(final String contents) {
try {
final ParcelFileDescriptor descriptors[] = ParcelFileDescriptor.createPipe();
final ParcelFileDescriptor readDescriptor = descriptors[0];
final ParcelFileDescriptor writeDescriptor = descriptors[1];
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
final AutoCloseOutputStream outStream =
new AutoCloseOutputStream(writeDescriptor);
try {
outStream.write(contents.getBytes("utf8"));
} catch (final IOException e) {
LogUtils.e(LogUtils.TAG, e, "IOException while writing to body pipe");
} finally {
try {
outStream.close();
} catch (final IOException e) {
LogUtils.e(LogUtils.TAG, e, "IOException while closing body pipe");
}
}
return null;
}
}.executeOnExecutor(THREAD_POOL_EXECUTOR);
return readDescriptor;
} catch (final IOException e) {
LogUtils.e(LogUtils.TAG, e, "IOException while creating body pipe");
return null;
}
}
@Override
public Bundle respond(Bundle extras) {
final int htmlRow = extras.getInt(Body.RESPOND_COMMAND_GET_HTML_PIPE, -1);
final int textRow = extras.getInt(Body.RESPOND_COMMAND_GET_TEXT_PIPE, -1);
final Bundle b = new Bundle(2);
if (htmlRow >= 0 && !TextUtils.isEmpty(mHtmlParts.get(htmlRow))) {
b.putParcelable(Body.RESPOND_RESULT_HTML_PIPE_KEY,
createPipeAndFillAsync(mHtmlParts.get(htmlRow)));
}
if (textRow >= 0 && !TextUtils.isEmpty(mTextParts.get(textRow))) {
b.putParcelable(Body.RESPOND_RESULT_TEXT_PIPE_KEY,
createPipeAndFillAsync(mTextParts.get(textRow)));
}
return b;
}
}

View File

@ -40,7 +40,9 @@ import android.database.DatabaseUtils;
import android.database.MatrixCursor;
import android.database.MergeCursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDoneException;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteStatement;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Binder;
@ -51,6 +53,7 @@ import android.os.Handler.Callback;
import android.os.Looper;
import android.os.Parcel;
import android.os.ParcelFileDescriptor;
import android.os.ParcelFileDescriptor.AutoCloseOutputStream;
import android.os.RemoteException;
import android.provider.BaseColumns;
import android.text.TextUtils;
@ -126,6 +129,7 @@ import com.google.common.collect.Sets;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
@ -134,6 +138,13 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
/**
@ -267,6 +278,8 @@ public class EmailProvider extends ContentProvider {
private static final int BODY_BASE = 0xA000;
private static final int BODY = BODY_BASE;
private static final int BODY_ID = BODY_BASE + 1;
private static final int BODY_HTML = BODY_BASE + 2;
private static final int BODY_TEXT = BODY_BASE + 3;
private static final int CREDENTIAL_BASE = 0xB000;
private static final int CREDENTIAL = CREDENTIAL_BASE;
@ -1067,6 +1080,10 @@ public class EmailProvider extends ContentProvider {
sURIMatcher.addURI(EmailContent.AUTHORITY, "body", BODY);
// A specific mail body
sURIMatcher.addURI(EmailContent.AUTHORITY, "body/#", BODY_ID);
// A specific HTML body part, for openFile
sURIMatcher.addURI(EmailContent.AUTHORITY, "bodyHtml/#", BODY_HTML);
// A specific text body part, for openFile
sURIMatcher.addURI(EmailContent.AUTHORITY, "bodyText/#", BODY_TEXT);
// All hostauth records
sURIMatcher.addURI(EmailContent.AUTHORITY, "hostauth", HOSTAUTH);
@ -1299,9 +1316,18 @@ public class EmailProvider extends ContentProvider {
final ProjectionMap map = new ProjectionMap.Builder()
.addAll(projection)
.build();
if (map.containsKey(BodyColumns.HTML_CONTENT) ||
map.containsKey(BodyColumns.TEXT_CONTENT)) {
throw new IllegalArgumentException(
"Body content cannot be returned in the cursor");
}
final ContentValues cv = new ContentValues(2);
cv.put(BodyColumns.HTML_CONTENT, ""); // Loaded in EmailMessageCursor
cv.put(BodyColumns.TEXT_CONTENT, ""); // Loaded in EmailMessageCursor
cv.put(BodyColumns.HTML_CONTENT_URI, "@" + uriWithColumn("bodyHtml",
BodyColumns.MESSAGE_KEY));
cv.put(BodyColumns.TEXT_CONTENT_URI, "@" + uriWithColumn("bodyText",
BodyColumns.MESSAGE_KEY));
final StringBuilder sb = genSelect(map, projection, cv);
sb.append(" FROM ").append(Body.TABLE_NAME);
if (match == BODY_ID) {
@ -1317,13 +1343,6 @@ public class EmailProvider extends ContentProvider {
sb.append(" LIMIT ").append(limit);
}
c = db.rawQuery(sb.toString(), selectionArgs);
if (c != null) {
// We don't want to deliver the body contents inline here because we might
// be sending this cursor to the Exchange process, and we'll blow out the
// CursorWindow if there's a large message body.
c = new EmailMessageCursor(c, db, BodyColumns.HTML_CONTENT,
BodyColumns.TEXT_CONTENT, false);
}
break;
}
case MESSAGE_ID:
@ -2076,8 +2095,34 @@ public class EmailProvider extends ContentProvider {
return result;
}
// TODO: remove this when we move message bodies to actual files
private static final BlockingQueue<Runnable> sPoolWorkQueue =
new LinkedBlockingQueue<Runnable>(128);
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
public Thread newThread(Runnable r) {
return new Thread(r, "EmailProviderOpenFile #" + mCount.getAndIncrement());
}
};
/**
* An {@link java.util.concurrent.Executor} that executes tasks which feed text and html email
* bodies into streams.
*
* It is important that this Executor is private to this class since we don't want to risk
* sharing a common Executor with Threads that *read* from the stream. If that were to happen
* it is possible for all Threads in the Executor to be blocked reads and thus starvation
* occurs.
*/
private static final Executor OPEN_FILE_EXECUTOR = new ThreadPoolExecutor(1 /* corePoolSize */,
5 /* maxPoolSize */, 1 /* keepAliveTime */, TimeUnit.SECONDS,
sPoolWorkQueue, sThreadFactory);
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
public ParcelFileDescriptor openFile(final Uri uri, final String mode)
throws FileNotFoundException {
if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
LogUtils.d(TAG, "EmailProvider.openFile: %s", LogUtils.contentUriToString(TAG, uri));
}
@ -2103,6 +2148,71 @@ public class EmailProvider extends ContentProvider {
}
}
break;
case BODY_HTML:
case BODY_TEXT:
final ParcelFileDescriptor descriptors[];
try {
descriptors = ParcelFileDescriptor.createPipe();
} catch (final IOException e) {
throw new FileNotFoundException();
}
final ParcelFileDescriptor readDescriptor = descriptors[0];
final ParcelFileDescriptor writeDescriptor = descriptors[1];
final SQLiteDatabase db = getDatabase(getContext());
final SQLiteStatement sql;
if (match == BODY_HTML) {
sql = db.compileStatement(
"SELECT " + BodyColumns.HTML_CONTENT +
" FROM " + Body.TABLE_NAME +
" WHERE " + BodyColumns.MESSAGE_KEY + "=?");
} else { // BODY_TEXT
sql = db.compileStatement(
"SELECT " + BodyColumns.TEXT_CONTENT +
" FROM " + Body.TABLE_NAME +
" WHERE " + BodyColumns.MESSAGE_KEY + "=?");
}
final long messageKey = Long.valueOf(uri.getLastPathSegment());
sql.bindLong(1, messageKey);
final String contents;
try {
contents = sql.simpleQueryForString();
} catch (final SQLiteDoneException e) {
LogUtils.v(LogUtils.TAG, e,
"Done exception while reading %s body for message %d",
match == BODY_HTML ? "html" : "text", messageKey);
throw new FileNotFoundException();
}
if (TextUtils.isEmpty(contents)) {
throw new FileNotFoundException("Body field is empty");
}
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
final AutoCloseOutputStream outStream =
new AutoCloseOutputStream(writeDescriptor);
try {
outStream.write(contents.getBytes("utf8"));
} catch (final IOException e) {
LogUtils.e(LogUtils.TAG, e,
"IOException while writing to body pipe");
} finally {
try {
outStream.close();
} catch (final IOException e) {
LogUtils.e(LogUtils.TAG, e,
"IOException while closing body pipe");
}
}
return null;
}
}.executeOnExecutor(OPEN_FILE_EXECUTOR);
return readDescriptor;
// break;
}
throw new FileNotFoundException("unable to open file");
@ -4353,7 +4463,7 @@ public class EmailProvider extends ContentProvider {
}
if (c != null) {
c = new EmailMessageCursor(c, db, UIProvider.MessageColumns.BODY_HTML,
UIProvider.MessageColumns.BODY_TEXT, true /* deliverColumnsInline */);
UIProvider.MessageColumns.BODY_TEXT);
}
notifyUri = UIPROVIDER_MESSAGE_NOTIFIER.buildUpon().appendPath(id).build();
break;

View File

@ -71,7 +71,7 @@ public class Utilities {
if (c == null) {
return;
} else if (c.moveToNext()) {
localMessage = EmailContent.getContent(c, EmailContent.Message.class);
localMessage = EmailContent.getContent(context, c, EmailContent.Message.class);
} else {
localMessage = new EmailContent.Message();
}

View File

@ -824,7 +824,7 @@ public class ImapService extends Service {
// loop through messages marked as deleted
while (deletes.moveToNext()) {
EmailContent.Message oldMessage =
EmailContent.getContent(deletes, EmailContent.Message.class);
EmailContent.getContent(context, deletes, EmailContent.Message.class);
if (oldMessage != null) {
lastMessageId = oldMessage.mId;
@ -961,7 +961,7 @@ public class ImapService extends Service {
boolean changeAnswered = false;
EmailContent.Message oldMessage =
EmailContent.getContent(updates, EmailContent.Message.class);
EmailContent.getContent(context, updates, EmailContent.Message.class);
lastMessageId = oldMessage.mId;
EmailContent.Message newMessage =
EmailContent.Message.restoreMessageWithId(context, oldMessage.mId);

View File

@ -115,7 +115,8 @@ public class LegacyConversionsTests extends ProviderTestCase2<EmailProvider> {
try {
assertEquals(2, c.getCount());
while (c.moveToNext()) {
Attachment attachment = Attachment.getContent(c, Attachment.class);
Attachment attachment =
Attachment.getContent(mProviderContext, c, Attachment.class);
if ("100".equals(attachment.mLocation)) {
checkAttachment("attachment1Part", attachments.get(0), attachment,
localMessage.mAccountKey);

View File

@ -420,7 +420,7 @@ public class ProviderTests extends ProviderTestCase2<EmailProvider> {
new String[] {String.valueOf(messageId)}, null);
int numBodies = c.getCount();
assertTrue("at most one body", numBodies < 2);
return c.moveToFirst() ? EmailContent.getContent(c, Body.class) : null;
return c.moveToFirst() ? EmailContent.getContent(mMockContext, c, Body.class) : null;
} finally {
c.close();
}
@ -515,7 +515,7 @@ public class ProviderTests extends ProviderTestCase2<EmailProvider> {
assertEquals(3, numAtts);
int i = 0;
while (c.moveToNext()) {
Attachment actual = EmailContent.getContent(c, Attachment.class);
Attachment actual = EmailContent.getContent(mMockContext, c, Attachment.class);
ProviderTestUtils.assertAttachmentEqual("save-message3", atts.get(i), actual);
i++;
}
@ -567,7 +567,7 @@ public class ProviderTests extends ProviderTestCase2<EmailProvider> {
assertEquals(3, numAtts);
int i = 0;
while (c.moveToNext()) {
Attachment actual = EmailContent.getContent(c, Attachment.class);
Attachment actual = EmailContent.getContent(mMockContext, c, Attachment.class);
ProviderTestUtils.assertAttachmentEqual("save-message4", atts.get(i), actual);
i++;
}
@ -1292,7 +1292,7 @@ public class ProviderTests extends ProviderTestCase2<EmailProvider> {
cr.query(Message.UPDATED_CONTENT_URI, Message.CONTENT_PROJECTION, null, null, null);
try {
assertTrue(c.moveToFirst());
Message originalMessage = EmailContent.getContent(c, Message.class);
Message originalMessage = EmailContent.getContent(mMockContext, c, Message.class);
// make sure this has the original value
assertEquals("from message2", originalMessage.mFrom);
// Should only be one
@ -1600,10 +1600,10 @@ public class ProviderTests extends ProviderTestCase2<EmailProvider> {
try {
c.moveToFirst();
Attachment a1Get = EmailContent.getContent(c, Attachment.class);
Attachment a1Get = EmailContent.getContent(mMockContext, c, Attachment.class);
ProviderTestUtils.assertAttachmentEqual("getAttachByUri-1", a1, a1Get);
c.moveToNext();
Attachment a2Get = EmailContent.getContent(c, Attachment.class);
Attachment a2Get = EmailContent.getContent(mMockContext, c, Attachment.class);
ProviderTestUtils.assertAttachmentEqual("getAttachByUri-2", a2, a2Get);
} finally {
c.close();
@ -1638,10 +1638,10 @@ public class ProviderTests extends ProviderTestCase2<EmailProvider> {
try {
c.moveToFirst();
Attachment a3Get = EmailContent.getContent(c, Attachment.class);
Attachment a3Get = EmailContent.getContent(mMockContext, c, Attachment.class);
ProviderTestUtils.assertAttachmentEqual("getAttachByUri-3", a3, a3Get);
c.moveToNext();
Attachment a4Get = EmailContent.getContent(c, Attachment.class);
Attachment a4Get = EmailContent.getContent(mMockContext, c, Attachment.class);
ProviderTestUtils.assertAttachmentEqual("getAttachByUri-4", a4, a4Get);
} finally {
c.close();