diff options
| author | Tony Mantler <nicoya@google.com> | 2014-05-08 13:07:54 -0700 |
|---|---|---|
| committer | Tony Mantler <nicoya@google.com> | 2014-05-08 15:50:57 -0700 |
| commit | 2f288864b621cfb5aee44eda27df463460d33dd3 (patch) | |
| tree | 6bcee481806c2e5a890c7fbd0051c203dfda5623 /src | |
| parent | 7366516bb08c59739c84b1c9b4effd14c1a561fc (diff) | |
| download | android_packages_apps_Email-2f288864b621cfb5aee44eda27df463460d33dd3.tar.gz android_packages_apps_Email-2f288864b621cfb5aee44eda27df463460d33dd3.tar.bz2 android_packages_apps_Email-2f288864b621cfb5aee44eda27df463460d33dd3.zip | |
Fetch email bodies using ContentResolver#openInputStream
This brings us further along the path to storing email bodies
outside of the database.
Change-Id: I96296114ade0d561df724878ed92999306bcd176
Diffstat (limited to 'src')
| -rw-r--r-- | src/com/android/email/provider/EmailMessageCursor.java | 121 | ||||
| -rw-r--r-- | src/com/android/email/provider/EmailProvider.java | 132 | ||||
| -rw-r--r-- | src/com/android/email/provider/Utilities.java | 2 | ||||
| -rw-r--r-- | src/com/android/email/service/ImapService.java | 4 |
4 files changed, 132 insertions, 127 deletions
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<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; - } } 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<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; 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); |
