diff options
-rwxr-xr-x | emailcommon/src/com/android/emailcommon/provider/EmailContent.java | 19 | ||||
-rw-r--r-- | src/com/android/email/provider/DBHelper.java | 157 | ||||
-rw-r--r-- | src/com/android/email/provider/EmailMessageCursor.java | 58 | ||||
-rw-r--r-- | src/com/android/email/provider/EmailProvider.java | 308 | ||||
-rw-r--r-- | src/com/android/email/provider/Utilities.java | 39 |
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; + } } |