summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xemailcommon/src/com/android/emailcommon/provider/EmailContent.java19
-rw-r--r--src/com/android/email/provider/DBHelper.java157
-rw-r--r--src/com/android/email/provider/EmailMessageCursor.java58
-rw-r--r--src/com/android/email/provider/EmailProvider.java308
-rw-r--r--src/com/android/email/provider/Utilities.java39
5 files changed, 410 insertions, 171 deletions
diff --git a/emailcommon/src/com/android/emailcommon/provider/EmailContent.java b/emailcommon/src/com/android/emailcommon/provider/EmailContent.java
index 697db269e..a10e91db8 100755
--- a/emailcommon/src/com/android/emailcommon/provider/EmailContent.java
+++ b/emailcommon/src/com/android/emailcommon/provider/EmailContent.java
@@ -574,20 +574,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 {
diff --git a/src/com/android/email/provider/DBHelper.java b/src/com/android/email/provider/DBHelper.java
index 40bebfb1d..22ce40d76 100644
--- a/src/com/android/email/provider/DBHelper.java
+++ b/src/com/android/email/provider/DBHelper.java
@@ -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) {
- }
+ 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");
}
- 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;
+ }
+
+ @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");
}
- 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;
+ }
+
+ 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");
}
- if (oldVersion == 8) {
- // Move to Email2 version
- oldVersion = 100;
+ }
+
+ /**
+ * 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);
diff --git a/src/com/android/email/provider/EmailMessageCursor.java b/src/com/android/email/provider/EmailMessageCursor.java
index 7e455edab..a734bbe63 100644
--- a/src/com/android/email/provider/EmailMessageCursor.java
+++ b/src/com/android/email/provider/EmailMessageCursor.java
@@ -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);
diff --git a/src/com/android/email/provider/EmailProvider.java b/src/com/android/email/provider/EmailProvider.java
index d43ccae97..acd04b034 100644
--- a/src/com/android/email/provider/EmailProvider.java
+++ b/src/com/android/email/provider/EmailProvider.java
@@ -41,7 +41,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;
@@ -54,7 +53,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;
@@ -131,6 +129,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;
@@ -141,13 +140,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;
/**
@@ -334,10 +326,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 + '=';
@@ -757,9 +751,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();
@@ -866,13 +885,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:
@@ -1845,7 +1881,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:
@@ -1966,8 +2001,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.
@@ -1975,8 +2042,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:
@@ -2088,30 +2197,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);
+ /**
+ * 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);
+ }
+ }
+ }
- public Thread newThread(Runnable r) {
- return new Thread(r, "EmailProviderOpenFile #" + mCount.getAndIncrement());
+ /**
+ * Writes a message body file to disk
+ *
+ * @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 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();
+ }
}
- };
+ }
/**
- * An {@link java.util.concurrent.Executor} that executes tasks which feed text and html email
- * bodies into streams.
+ * Returns a {@link java.io.File} object pointing to the body content file for the message
*
- * 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 locate
+ * @param ext "html" or "txt"
+ * @return File ready for operating upon
*/
- private static final Executor OPEN_FILE_EXECUTOR = new ThreadPoolExecutor(1 /* corePoolSize */,
- 5 /* maxPoolSize */, 1 /* keepAliveTime */, TimeUnit.SECONDS,
- sPoolWorkQueue, sThreadFactory);
+ 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)
@@ -2141,71 +2306,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");
@@ -4455,7 +4565,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();
diff --git a/src/com/android/email/provider/Utilities.java b/src/com/android/email/provider/Utilities.java
index aaf7875a7..c3b7ec93a 100644
--- a/src/com/android/email/provider/Utilities.java
+++ b/src/com/android/email/provider/Utilities.java
@@ -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;
+ }
}