Move email bodies to files

Change-Id: Icfd0c4ab2ad25cc02b45cf41e7a205c17948ef2c
This commit is contained in:
Tony Mantler 2014-05-12 11:33:43 -07:00
parent bca4f9fcfb
commit 7525feb244
5 changed files with 414 additions and 175 deletions

View File

@ -578,20 +578,29 @@ public abstract class EmailContent {
0, 0L);
}
public static Uri getBodyTextUriForMessageWithId(long messageId) {
return EmailContent.CONTENT_URI.buildUpon()
.appendPath("bodyText").appendPath(Long.toString(messageId)).build();
}
public static Uri getBodyHtmlUriForMessageWithId(long messageId) {
return EmailContent.CONTENT_URI.buildUpon()
.appendPath("bodyHtml").appendPath(Long.toString(messageId)).build();
}
public static String restoreBodyTextWithMessageId(Context context, long messageId) {
return readBodyFromProvider(context, EmailContent.CONTENT_URI.buildUpon()
.appendPath("bodyText").appendPath(Long.toString(messageId)).toString());
return readBodyFromProvider(context,
getBodyTextUriForMessageWithId(messageId).toString());
}
public static String restoreBodyHtmlWithMessageId(Context context, long messageId) {
return readBodyFromProvider(context, EmailContent.CONTENT_URI.buildUpon()
.appendPath("bodyHtml").appendPath(Long.toString(messageId)).toString());
return readBodyFromProvider(context,
getBodyHtmlUriForMessageWithId(messageId).toString());
}
private static String readBodyFromProvider(final Context context, final String uri) {
String content = null;
try {
final InputStream bodyInput =
context.getContentResolver().openInputStream(Uri.parse(uri));
try {

View File

@ -23,7 +23,9 @@ import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDoneException;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteStatement;
import android.provider.BaseColumns;
import android.provider.CalendarContract;
import android.provider.ContactsContract;
@ -61,6 +63,9 @@ import com.android.mail.utils.LogUtils;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Map;
public final class DBHelper {
@ -189,7 +194,9 @@ public final class DBHelper {
// Version 6: Adding Body.mIntroText column
// Version 7/8: Adding quoted text start pos
// Version 8 is last Email1 version
public static final int BODY_DATABASE_VERSION = 100;
// Version 100 is the first Email2 version
// Version 101: Move body contents to external files
public static final int BODY_DATABASE_VERSION = 101;
/*
* Internal helper method for index creation.
@ -577,6 +584,7 @@ public final class DBHelper {
createHostAuthTable(db);
}
@SuppressWarnings("deprecation")
static void createMailboxTable(SQLiteDatabase db) {
String s = " (" + MailboxColumns._ID + " integer primary key autoincrement, "
+ MailboxColumns.DISPLAY_NAME + " text, "
@ -661,6 +669,7 @@ public final class DBHelper {
db.execSQL("create table " + QuickResponse.TABLE_NAME + s);
}
@SuppressWarnings("deprecation")
static void createBodyTable(SQLiteDatabase db) {
String s = " (" + BodyColumns._ID + " integer primary key autoincrement, "
+ BodyColumns.MESSAGE_KEY + " integer, "
@ -676,44 +685,115 @@ public final class DBHelper {
db.execSQL(createIndex(Body.TABLE_NAME, BodyColumns.MESSAGE_KEY));
}
static void upgradeBodyTable(SQLiteDatabase db, int oldVersion, int newVersion) {
if (oldVersion < 5) {
try {
db.execSQL("drop table " + Body.TABLE_NAME);
createBodyTable(db);
oldVersion = 5;
} catch (SQLException e) {
}
}
if (oldVersion == 5) {
try {
db.execSQL("alter table " + Body.TABLE_NAME
+ " add " + BodyColumns.INTRO_TEXT + " text");
} catch (SQLException e) {
// Shouldn't be needed unless we're debugging and interrupt the process
LogUtils.w(TAG, "Exception upgrading EmailProviderBody.db from v5 to v6", e);
}
oldVersion = 6;
}
if (oldVersion == 6 || oldVersion == 7) {
try {
db.execSQL("alter table " + Body.TABLE_NAME
+ " add " + BodyColumns.QUOTED_TEXT_START_POS + " integer");
} catch (SQLException e) {
// Shouldn't be needed unless we're debugging and interrupt the process
LogUtils.w(TAG, "Exception upgrading EmailProviderBody.db from v6 to v8", e);
}
oldVersion = 8;
}
if (oldVersion == 8) {
// Move to Email2 version
oldVersion = 100;
private static void upgradeBodyToVersion5(final SQLiteDatabase db) {
try {
db.execSQL("drop table " + Body.TABLE_NAME);
createBodyTable(db);
} catch (final SQLException e) {
// Shouldn't be needed unless we're debugging and interrupt the process
LogUtils.w(TAG, e, "Exception upgrading EmailProviderBody.db from <v5");
}
}
@SuppressWarnings("deprecation")
private static void upgradeBodyFromVersion5ToVersion6(final SQLiteDatabase db) {
try {
db.execSQL("alter table " + Body.TABLE_NAME
+ " add " + BodyColumns.INTRO_TEXT + " text");
} catch (final SQLException e) {
// Shouldn't be needed unless we're debugging and interrupt the process
LogUtils.w(TAG, e, "Exception upgrading EmailProviderBody.db from v5 to v6");
}
}
private static void upgradeBodyFromVersion6ToVersion8(final SQLiteDatabase db) {
try {
db.execSQL("alter table " + Body.TABLE_NAME
+ " add " + BodyColumns.QUOTED_TEXT_START_POS + " integer");
} catch (final SQLException e) {
// Shouldn't be needed unless we're debugging and interrupt the process
LogUtils.w(TAG, e, "Exception upgrading EmailProviderBody.db from v6 to v8");
}
}
/**
* This upgrade migrates email bodies out of the database and into individual files.
*/
private static void upgradeBodyFromVersion100ToVersion101(final Context context,
final SQLiteDatabase db) {
try {
// We can't read the body parts through the cursor because they might be over 2MB
final String projection[] = { BodyColumns.MESSAGE_KEY };
final Cursor cursor = db.query(Body.TABLE_NAME, projection,
null, null, null, null, null);
if (cursor == null) {
throw new IllegalStateException("Could not read body table for upgrade");
}
final SQLiteStatement htmlSql = db.compileStatement(
"SELECT " + BodyColumns.HTML_CONTENT +
" FROM " + Body.TABLE_NAME +
" WHERE " + BodyColumns.MESSAGE_KEY + "=?"
);
final SQLiteStatement textSql = db.compileStatement(
"SELECT " + BodyColumns.TEXT_CONTENT +
" FROM " + Body.TABLE_NAME +
" WHERE " + BodyColumns.MESSAGE_KEY + "=?"
);
while (cursor.moveToNext()) {
final long messageId = cursor.getLong(0);
htmlSql.bindLong(1, messageId);
try {
final String htmlString = htmlSql.simpleQueryForString();
if (!TextUtils.isEmpty(htmlString)) {
final File htmlFile = EmailProvider.getBodyFile(context, messageId, "html");
final FileWriter w = new FileWriter(htmlFile);
try {
w.write(htmlString);
} finally {
w.close();
}
}
} catch (final SQLiteDoneException e) {
LogUtils.v(LogUtils.TAG, e, "Done with the HTML column");
}
textSql.bindLong(1, messageId);
try {
final String textString = textSql.simpleQueryForString();
if (!TextUtils.isEmpty(textString)) {
final File textFile = EmailProvider.getBodyFile(context, messageId, "txt");
final FileWriter w = new FileWriter(textFile);
try {
w.write(textString);
} finally {
w.close();
}
}
} catch (final SQLiteDoneException e) {
LogUtils.v(LogUtils.TAG, e, "Done with the text column");
}
}
db.execSQL("update " + Body.TABLE_NAME +
" set " + BodyColumns.HTML_CONTENT + "=NULL,"
+ BodyColumns.TEXT_CONTENT + "=NULL");
} catch (final SQLException e) {
// Shouldn't be needed unless we're debugging and interrupt the process
LogUtils.w(TAG, e, "Exception upgrading EmailProviderBody.db from v100 to v101");
} catch (final IOException e) {
throw new RuntimeException(e);
}
}
protected static class BodyDatabaseHelper extends SQLiteOpenHelper {
final Context mContext;
BodyDatabaseHelper(Context context, String name) {
super(context, name, null, BODY_DATABASE_VERSION);
mContext = context;
}
@Override
@ -723,8 +803,19 @@ public final class DBHelper {
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
upgradeBodyTable(db, oldVersion, newVersion);
public void onUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) {
if (oldVersion < 5) {
upgradeBodyToVersion5(db);
}
if (oldVersion < 6) {
upgradeBodyFromVersion5ToVersion6(db);
}
if (oldVersion < 8) {
upgradeBodyFromVersion6ToVersion8(db);
}
if (oldVersion < 101) {
upgradeBodyFromVersion100ToVersion101(mContext, db);
}
}
@Override
@ -742,7 +833,7 @@ public final class DBHelper {
}
protected static class DatabaseHelper extends SQLiteOpenHelper {
Context mContext;
final Context mContext;
DatabaseHelper(Context context, String name) {
super(context, name, null, DATABASE_VERSION);

View File

@ -16,34 +16,33 @@
package com.android.email.provider;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.database.CursorWrapper;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDoneException;
import android.database.sqlite.SQLiteStatement;
import android.net.Uri;
import android.provider.BaseColumns;
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 org.apache.commons.io.IOUtils;
import java.io.IOException;
import java.io.InputStream;
/**
* 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
* large email message can exceed that limit and cause the cursor to fail to load.
*
* To get around this, we load null values in those columns, and then in this wrapper we directly
* load the content from the DB, skipping the cursor window.
* load the content from the provider, skipping the cursor window.
*
* This will still potentially blow up if this cursor gets wrapped in a CrossProcessCursorWrapper
* which uses a CursorWindow to shuffle results between processes. This is currently only done in
* Exchange, and only for outgoing mail, so hopefully users never type more than 2MB of email on
* their device.
*
* 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.
* which uses a CursorWindow to shuffle results between processes. Since we're only using this for
* passing a cursor back to UnifiedEmail this shouldn't be an issue.
*/
public class EmailMessageCursor extends CursorWrapper {
@ -52,7 +51,7 @@ public class EmailMessageCursor extends CursorWrapper {
private final int mTextColumnIndex;
private final int mHtmlColumnIndex;
public EmailMessageCursor(final Cursor cursor, final SQLiteDatabase db, final String htmlColumn,
public EmailMessageCursor(final Context c, final Cursor cursor, final String htmlColumn,
final String textColumn) {
super(cursor);
mHtmlColumnIndex = cursor.getColumnIndex(htmlColumn);
@ -61,39 +60,30 @@ public class EmailMessageCursor extends CursorWrapper {
mHtmlParts = new SparseArray<String>(cursorSize);
mTextParts = new SparseArray<String>(cursorSize);
// 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 " + BodyColumns.MESSAGE_KEY + "=?"
);
final SQLiteStatement textSql = db.compileStatement(
"SELECT " + BodyColumns.TEXT_CONTENT +
" FROM " + Body.TABLE_NAME +
" WHERE " + BodyColumns.MESSAGE_KEY + "=?"
);
final ContentResolver cr = c.getContentResolver();
while (cursor.moveToNext()) {
final int position = cursor.getPosition();
final long rowId = cursor.getLong(cursor.getColumnIndex(BaseColumns._ID));
htmlSql.bindLong(1, rowId);
final long messageId = cursor.getLong(cursor.getColumnIndex(BaseColumns._ID));
try {
if (mHtmlColumnIndex != -1) {
final String underlyingHtmlString = htmlSql.simpleQueryForString();
final Uri htmlUri = Body.getBodyHtmlUriForMessageWithId(messageId);
final InputStream in = cr.openInputStream(htmlUri);
final String underlyingHtmlString = IOUtils.toString(in);
mHtmlParts.put(position, underlyingHtmlString);
}
} catch (final SQLiteDoneException e) {
LogUtils.d(LogUtils.TAG, e, "Done with the HTML column");
} catch (final IOException e) {
LogUtils.v(LogUtils.TAG, e, "Did not find html body for message %d", messageId);
}
textSql.bindLong(1, rowId);
try {
if (mTextColumnIndex != -1) {
final String underlyingTextString = textSql.simpleQueryForString();
final Uri textUri = Body.getBodyTextUriForMessageWithId(messageId);
final InputStream in = cr.openInputStream(textUri);
final String underlyingTextString = IOUtils.toString(in);
mTextParts.put(position, underlyingTextString);
}
} catch (final SQLiteDoneException e) {
LogUtils.d(LogUtils.TAG, e, "Done with the text column");
} catch (final IOException e) {
LogUtils.v(LogUtils.TAG, e, "Did not find text body for message %d", messageId);
}
}
cursor.moveToPosition(-1);

View File

@ -40,7 +40,6 @@ 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;
@ -53,7 +52,6 @@ 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;
@ -129,6 +127,7 @@ import com.google.common.collect.Sets;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
@ -138,13 +137,6 @@ 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;
/**
@ -329,10 +321,12 @@ public class EmailProvider extends ContentProvider {
Message.DELETED_TABLE_NAME + " select * from " + Message.TABLE_NAME + " where " +
BaseColumns._ID + '=';
private static final String ORPHAN_BODY_MESSAGE_ID_SELECT =
"select " + BodyColumns.MESSAGE_KEY + " from " + Body.TABLE_NAME +
" except select " + BaseColumns._ID + " from " + Message.TABLE_NAME;
private static final String DELETE_ORPHAN_BODIES = "delete from " + Body.TABLE_NAME +
" where " + BodyColumns.MESSAGE_KEY + " in " + "(select " + BodyColumns.MESSAGE_KEY +
" from " + Body.TABLE_NAME + " except select " + BaseColumns._ID + " from " +
Message.TABLE_NAME + ')';
" where " + BodyColumns.MESSAGE_KEY + " in " + '(' + ORPHAN_BODY_MESSAGE_ID_SELECT + ')';
private static final String DELETE_BODY = "delete from " + Body.TABLE_NAME +
" where " + BodyColumns.MESSAGE_KEY + '=';
@ -743,9 +737,34 @@ public class EmailProvider extends ContentProvider {
if (messageDeletion) {
if (match == MESSAGE_ID) {
// Delete the Body record associated with the deleted message
final ContentValues emptyValues = new ContentValues(2);
emptyValues.putNull(BodyColumns.HTML_CONTENT);
emptyValues.putNull(BodyColumns.TEXT_CONTENT);
final long messageId = Long.valueOf(id);
try {
writeBodyFiles(context, messageId, emptyValues);
} catch (final IllegalStateException e) {
LogUtils.v(LogUtils.TAG, e, "Exception while deleting bodies");
}
db.execSQL(DELETE_BODY + id);
} else {
// Delete any orphaned Body records
final Cursor orphans = db.rawQuery(ORPHAN_BODY_MESSAGE_ID_SELECT, null);
try {
final ContentValues emptyValues = new ContentValues(2);
emptyValues.putNull(BodyColumns.HTML_CONTENT);
emptyValues.putNull(BodyColumns.TEXT_CONTENT);
while (orphans.moveToNext()) {
final long messageId = orphans.getLong(0);
try {
writeBodyFiles(context, messageId, emptyValues);
} catch (final IllegalStateException e) {
LogUtils.v(LogUtils.TAG, e, "Exception while deleting bodies");
}
}
} finally {
orphans.close();
}
db.execSQL(DELETE_ORPHAN_BODIES);
}
db.setTransactionSuccessful();
@ -852,13 +871,30 @@ public class EmailProvider extends ContentProvider {
try {
switch (match) {
case BODY:
final ContentValues dbValues = new ContentValues(values);
// Prune out the content we don't want in the DB
dbValues.remove(BodyColumns.HTML_CONTENT);
dbValues.remove(BodyColumns.TEXT_CONTENT);
// TODO: move this to the message table
longId = db.insert(Body.TABLE_NAME, "foo", dbValues);
resultUri = ContentUris.withAppendedId(uri, longId);
// Write content to the filesystem where appropriate
// This will look less ugly once the body table is folded into the message table
// and we can just use longId instead
if (!values.containsKey(BodyColumns.MESSAGE_KEY)) {
throw new IllegalArgumentException(
"Cannot insert body without MESSAGE_KEY");
}
final long messageId = values.getAsLong(BodyColumns.MESSAGE_KEY);
writeBodyFiles(getContext(), messageId, values);
break;
// NOTE: It is NOT legal for production code to insert directly into UPDATED_MESSAGE
// or DELETED_MESSAGE; see the comment below for details
case UPDATED_MESSAGE:
case DELETED_MESSAGE:
case MESSAGE:
decodeEmailAddresses(values);
case BODY:
case ATTACHMENT:
case MAILBOX:
case ACCOUNT:
@ -1832,7 +1868,6 @@ public class EmailProvider extends ContentProvider {
case SYNCED_MESSAGE_ID:
case UPDATED_MESSAGE_ID:
case MESSAGE_ID:
case BODY_ID:
case ATTACHMENT_ID:
case MAILBOX_ID:
case ACCOUNT_ID:
@ -1953,8 +1988,40 @@ public class EmailProvider extends ContentProvider {
restartPushForAccount(context, db, values, id);
}
break;
case BODY:
result = db.update(tableName, values, selection, selectionArgs);
case BODY_ID: {
final ContentValues updateValues = new ContentValues(values);
updateValues.remove(BodyColumns.HTML_CONTENT);
updateValues.remove(BodyColumns.TEXT_CONTENT);
result = db.update(tableName, updateValues, whereWithId(id, selection),
selectionArgs);
if (values.containsKey(BodyColumns.HTML_CONTENT) ||
values.containsKey(BodyColumns.TEXT_CONTENT)) {
final long messageId;
if (values.containsKey(BodyColumns.MESSAGE_KEY)) {
messageId = values.getAsLong(BodyColumns.MESSAGE_KEY);
} else {
final long bodyId = Long.parseLong(id);
final SQLiteStatement sql = db.compileStatement(
"select " + BodyColumns.MESSAGE_KEY +
" from " + Body.TABLE_NAME +
" where " + BodyColumns._ID + "=" + Long
.toString(bodyId)
);
messageId = sql.simpleQueryForLong();
}
writeBodyFiles(context, messageId, values);
}
break;
}
case BODY: {
final ContentValues updateValues = new ContentValues(values);
updateValues.remove(BodyColumns.HTML_CONTENT);
updateValues.remove(BodyColumns.TEXT_CONTENT);
result = db.update(tableName, updateValues, selection, selectionArgs);
if (result == 0 && selection.equals(Body.SELECTION_BY_MESSAGE_KEY)) {
// TODO: This is a hack. Notably, the selection equality test above
// is hokey at best.
@ -1962,8 +2029,50 @@ public class EmailProvider extends ContentProvider {
final ContentValues insertValues = new ContentValues(values);
insertValues.put(BodyColumns.MESSAGE_KEY, selectionArgs[0]);
insert(Body.CONTENT_URI, insertValues);
} else {
// possibly need to write new body values
if (values.containsKey(BodyColumns.HTML_CONTENT) ||
values.containsKey(BodyColumns.TEXT_CONTENT)) {
final long messageIds[];
if (values.containsKey(BodyColumns.MESSAGE_KEY)) {
messageIds = new long[] {values.getAsLong(BodyColumns.MESSAGE_KEY)};
} else if (values.containsKey(BodyColumns._ID)) {
final long bodyId = values.getAsLong(BodyColumns._ID);
final SQLiteStatement sql = db.compileStatement(
"select " + BodyColumns.MESSAGE_KEY +
" from " + Body.TABLE_NAME +
" where " + BodyColumns._ID + "=" + Long
.toString(bodyId)
);
messageIds = new long[] {sql.simpleQueryForLong()};
} else {
final String proj[] = {BodyColumns.MESSAGE_KEY};
final Cursor c = db.query(Body.TABLE_NAME, proj,
selection, selectionArgs,
null, null, null);
try {
final int count = c.getCount();
if (count == 0) {
throw new IllegalStateException("Can't find body record");
}
messageIds = new long[count];
int i = 0;
while (c.moveToNext()) {
messageIds[i++] = c.getLong(0);
}
} finally {
c.close();
}
}
// This is probably overkill
for (int i = 0; i < messageIds.length; i++) {
final long messageId = messageIds[i];
writeBodyFiles(context, messageId, values);
}
}
}
break;
}
case MESSAGE:
decodeEmailAddresses(values);
case UPDATED_MESSAGE:
@ -2095,30 +2204,86 @@ 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());
/**
* Writes message bodies to disk, read from a set of ContentValues
*
* @param c Context for finding files
* @param messageId id of message to write body for
* @param cv {@link ContentValues} containing {@link BodyColumns#HTML_CONTENT} and/or
* {@link BodyColumns#TEXT_CONTENT}. Inserting a null or empty value will delete the
* associated text or html body file
* @throws IllegalStateException
*/
private static void writeBodyFiles(final Context c, final long messageId,
final ContentValues cv) throws IllegalStateException {
if (cv.containsKey(BodyColumns.HTML_CONTENT)) {
final String htmlContent = cv.getAsString(BodyColumns.HTML_CONTENT);
try {
writeBodyFile(c, messageId, "html", htmlContent);
} catch (final IOException e) {
throw new IllegalStateException("IOException while writing html body " +
"for message id " + Long.toString(messageId), e);
}
}
};
if (cv.containsKey(BodyColumns.TEXT_CONTENT)) {
final String textContent = cv.getAsString(BodyColumns.TEXT_CONTENT);
try {
writeBodyFile(c, messageId, "txt", textContent);
} catch (final IOException e) {
throw new IllegalStateException("IOException while writing text body " +
"for message id " + Long.toString(messageId), e);
}
}
}
/**
* An {@link java.util.concurrent.Executor} that executes tasks which feed text and html email
* bodies into streams.
* Writes a message body file to disk
*
* 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.
* @param c Context for finding files dir
* @param messageId id of message to write body for
* @param ext "html" or "txt"
* @param content Body content to write to file, or null/empty to delete file
* @throws IOException
*/
private static final Executor OPEN_FILE_EXECUTOR = new ThreadPoolExecutor(1 /* corePoolSize */,
5 /* maxPoolSize */, 1 /* keepAliveTime */, TimeUnit.SECONDS,
sPoolWorkQueue, sThreadFactory);
private static void writeBodyFile(final Context c, final long messageId, final String ext,
final String content) throws IOException {
final File textFile = getBodyFile(c, messageId, ext);
if (TextUtils.isEmpty(content)) {
if (!textFile.delete()) {
LogUtils.v(LogUtils.TAG, "did not delete text body for %d", messageId);
}
} else {
final FileWriter w = new FileWriter(textFile);
try {
w.write(content);
} finally {
w.close();
}
}
}
/**
* Returns a {@link java.io.File} object pointing to the body content file for the message
*
* @param c Context for finding files dir
* @param messageId id of message to locate
* @param ext "html" or "txt"
* @return File ready for operating upon
*/
protected static File getBodyFile(final Context c, final long messageId, final String ext)
throws FileNotFoundException {
if (!TextUtils.equals(ext, "html") && !TextUtils.equals(ext, "txt")) {
throw new IllegalArgumentException("ext must be one of 'html' or 'txt'");
}
long l1 = messageId / 100 % 100;
long l2 = messageId % 100;
final File dir = new File(c.getFilesDir(),
"body/" + Long.toString(l1) + "/" + Long.toString(l2) + "/");
if (!dir.isDirectory() && !dir.mkdirs()) {
throw new FileNotFoundException("Could not create directory for body file");
}
return new File(dir, Long.toString(messageId) + "." + ext);
}
@Override
public ParcelFileDescriptor openFile(final Uri uri, final String mode)
@ -2148,71 +2313,16 @@ 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 + "=?");
}
case BODY_HTML: {
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;
return ParcelFileDescriptor.open(getBodyFile(getContext(), messageKey, "html"),
Utilities.parseMode(mode));
}
case BODY_TEXT:{
final long messageKey = Long.valueOf(uri.getLastPathSegment());
return ParcelFileDescriptor.open(getBodyFile(getContext(), messageKey, "txt"),
Utilities.parseMode(mode));
}
}
throw new FileNotFoundException("unable to open file");
@ -4462,7 +4572,7 @@ public class EmailProvider extends ContentProvider {
c = db.rawQuery(sql, new String[] {id});
}
if (c != null) {
c = new EmailMessageCursor(c, db, UIProvider.MessageColumns.BODY_HTML,
c = new EmailMessageCursor(getContext(), c, UIProvider.MessageColumns.BODY_HTML,
UIProvider.MessageColumns.BODY_TEXT);
}
notifyUri = UIPROVIDER_MESSAGE_NOTIFIER.buildUpon().appendPath(id).build();

View File

@ -16,11 +16,13 @@
package com.android.email.provider;
import android.annotation.TargetApi;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import com.android.email.LegacyConversions;
import com.android.emailcommon.Logging;
@ -36,6 +38,7 @@ import com.android.emailcommon.provider.EmailContent.SyncColumns;
import com.android.emailcommon.provider.Mailbox;
import com.android.emailcommon.utility.ConversionUtilities;
import com.android.mail.utils.LogUtils;
import com.android.mail.utils.Utils;
import java.io.IOException;
import java.util.ArrayList;
@ -192,4 +195,40 @@ public class Utilities {
}
}
/**
* 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;
}
}