diff --git a/emailcommon/src/com/android/emailcommon/provider/EmailContent.java b/emailcommon/src/com/android/emailcommon/provider/EmailContent.java index 2e0749791..f42b0a4e5 100755 --- a/emailcommon/src/com/android/emailcommon/provider/EmailContent.java +++ b/emailcommon/src/com/android/emailcommon/provider/EmailContent.java @@ -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 getContent(Cursor cursor, Class klass) { + static public T getContent(final Context context, final Cursor cursor, + final Class 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(); diff --git a/emailcommon/src/com/android/emailcommon/provider/Mailbox.java b/emailcommon/src/com/android/emailcommon/provider/Mailbox.java index 6aad55947..952a73147 100644 --- a/emailcommon/src/com/android/emailcommon/provider/Mailbox.java +++ b/emailcommon/src/com/android/emailcommon/provider/Mailbox.java @@ -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); } diff --git a/emailsync/src/com/android/emailsync/SyncManager.java b/emailsync/src/com/android/emailsync/SyncManager.java index a3ac17bcc..195a0ec6b 100644 --- a/emailsync/src/com/android/emailsync/SyncManager.java +++ b/emailsync/src/com/android/emailsync/SyncManager.java @@ -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; diff --git a/src/com/android/email/provider/EmailMessageCursor.java b/src/com/android/email/provider/EmailMessageCursor.java index d2a1a6cb6..7e455edab 100644 --- a/src/com/android/email/provider/EmailMessageCursor.java +++ b/src/com/android/email/provider/EmailMessageCursor.java @@ -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 sPoolWorkQueue = - new LinkedBlockingQueue(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 mTextParts; private final SparseArray 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(cursorSize); mTextParts = new SparseArray(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() { - @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; - } } diff --git a/src/com/android/email/provider/EmailProvider.java b/src/com/android/email/provider/EmailProvider.java index b86bf11e0..6add748ba 100644 --- a/src/com/android/email/provider/EmailProvider.java +++ b/src/com/android/email/provider/EmailProvider.java @@ -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 sPoolWorkQueue = + new LinkedBlockingQueue(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() { + @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; diff --git a/src/com/android/email/provider/Utilities.java b/src/com/android/email/provider/Utilities.java index 289be5bf9..aaf7875a7 100644 --- a/src/com/android/email/provider/Utilities.java +++ b/src/com/android/email/provider/Utilities.java @@ -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(); } diff --git a/src/com/android/email/service/ImapService.java b/src/com/android/email/service/ImapService.java index d1444d1ad..9efed3be9 100644 --- a/src/com/android/email/service/ImapService.java +++ b/src/com/android/email/service/ImapService.java @@ -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); diff --git a/tests/src/com/android/email/LegacyConversionsTests.java b/tests/src/com/android/email/LegacyConversionsTests.java index 1a44a0851..82f333ae7 100644 --- a/tests/src/com/android/email/LegacyConversionsTests.java +++ b/tests/src/com/android/email/LegacyConversionsTests.java @@ -115,7 +115,8 @@ public class LegacyConversionsTests extends ProviderTestCase2 { 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); diff --git a/tests/src/com/android/email/provider/ProviderTests.java b/tests/src/com/android/email/provider/ProviderTests.java index 5bf5850f4..a84d97709 100644 --- a/tests/src/com/android/email/provider/ProviderTests.java +++ b/tests/src/com/android/email/provider/ProviderTests.java @@ -420,7 +420,7 @@ public class ProviderTests extends ProviderTestCase2 { 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 { 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 { 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 { 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 { 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 { 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();