Move email bodies to files
Change-Id: Icfd0c4ab2ad25cc02b45cf41e7a205c17948ef2c
This commit is contained in:
parent
bca4f9fcfb
commit
7525feb244
@ -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 {
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user