Fetch email bodies using ContentResolver#openInputStream
This brings us further along the path to storing email bodies outside of the database. Change-Id: I96296114ade0d561df724878ed92999306bcd176
This commit is contained in:
parent
7366516bb0
commit
2f288864b6
@ -28,12 +28,9 @@ import android.database.ContentObservable;
|
||||
import android.database.ContentObserver;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.os.Looper;
|
||||
import android.os.Parcel;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.os.ParcelFileDescriptor.AutoCloseInputStream;
|
||||
import android.os.Parcelable;
|
||||
import android.os.RemoteException;
|
||||
import android.provider.BaseColumns;
|
||||
@ -49,8 +46,8 @@ import com.google.common.annotations.VisibleForTesting;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayList;
|
||||
|
||||
@ -134,7 +131,13 @@ public abstract class EmailContent {
|
||||
// Write the Content into a ContentValues container
|
||||
public abstract ContentValues toContentValues();
|
||||
// Read the Content from a ContentCursor
|
||||
public abstract void restore (Cursor cursor);
|
||||
public abstract void restore(Cursor cursor);
|
||||
// Same as above, with the addition of a context to retrieve extra content.
|
||||
// Body uses this to fetch the email body html/text from the provider bypassing the cursor
|
||||
// Not always safe to call on the UI thread.
|
||||
public void restore(Context context, Cursor cursor) {
|
||||
restore(cursor);
|
||||
}
|
||||
|
||||
|
||||
public static String EMAIL_PACKAGE_NAME;
|
||||
@ -239,7 +242,7 @@ public abstract class EmailContent {
|
||||
if (c == null) throw new ProviderUnavailableException();
|
||||
try {
|
||||
if (c.moveToFirst()) {
|
||||
final T content = getContent(c, klass);
|
||||
final T content = getContent(context, c, klass);
|
||||
if (observer != null) {
|
||||
content.registerObserver(context, observer);
|
||||
}
|
||||
@ -351,11 +354,12 @@ public abstract class EmailContent {
|
||||
|
||||
|
||||
// The Content sub class must have a no-arg constructor
|
||||
static public <T extends EmailContent> T getContent(Cursor cursor, Class<T> klass) {
|
||||
static public <T extends EmailContent> T getContent(final Context context, final Cursor cursor,
|
||||
final Class<T> klass) {
|
||||
try {
|
||||
T content = klass.newInstance();
|
||||
content.mId = cursor.getLong(0);
|
||||
content.restore(cursor);
|
||||
content.restore(context, cursor);
|
||||
return content;
|
||||
} catch (IllegalAccessException e) {
|
||||
e.printStackTrace();
|
||||
@ -433,10 +437,14 @@ public abstract class EmailContent {
|
||||
public interface BodyColumns extends BaseColumns {
|
||||
// Foreign key to the message corresponding to this body
|
||||
public static final String MESSAGE_KEY = "messageKey";
|
||||
// The html content itself
|
||||
// The html content itself, not returned on query
|
||||
public static final String HTML_CONTENT = "htmlContent";
|
||||
// The plain text content itself
|
||||
// The html content URI, for ContentResolver#openFileDescriptor()
|
||||
public static final String HTML_CONTENT_URI = "htmlContentUri";
|
||||
// The plain text content itself, not returned on query
|
||||
public static final String TEXT_CONTENT = "textContent";
|
||||
// The text content URI, for ContentResolver#openFileDescriptor()
|
||||
public static final String TEXT_CONTENT_URI = "textContentUri";
|
||||
// Replied-to or forwarded body (in html form)
|
||||
@Deprecated
|
||||
public static final String HTML_REPLY = "htmlReply";
|
||||
@ -465,40 +473,22 @@ public abstract class EmailContent {
|
||||
CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/body");
|
||||
}
|
||||
|
||||
/**
|
||||
* Following values are for EmailMessageCursor
|
||||
*/
|
||||
// Value is an int specifying the row to get
|
||||
public static final String RESPOND_COMMAND_GET_HTML_PIPE = "EmailMessageCursor.getHtmlPipe";
|
||||
public static final String RESPOND_COMMAND_GET_TEXT_PIPE = "EmailMessageCursor.getTextPipe";
|
||||
// Value returned is a ParcelFileDescriptor pipe, or null if no content is present
|
||||
public static final String RESPOND_RESULT_HTML_PIPE_KEY = "EmailMessageCursor.htmlPipe";
|
||||
public static final String RESPOND_RESULT_TEXT_PIPE_KEY = "EmailMessageCursor.textPipe";
|
||||
|
||||
public static final String[] CONTENT_PROJECTION = new String[] {
|
||||
BaseColumns._ID,
|
||||
BodyColumns.MESSAGE_KEY,
|
||||
BodyColumns.HTML_CONTENT,
|
||||
BodyColumns.TEXT_CONTENT,
|
||||
BodyColumns.HTML_CONTENT_URI,
|
||||
BodyColumns.TEXT_CONTENT_URI,
|
||||
BodyColumns.SOURCE_MESSAGE_KEY,
|
||||
BodyColumns.QUOTED_TEXT_START_POS
|
||||
};
|
||||
|
||||
public static final int CONTENT_ID_COLUMN = 0;
|
||||
public static final int CONTENT_MESSAGE_KEY_COLUMN = 1;
|
||||
public static final int CONTENT_HTML_CONTENT_COLUMN = 2;
|
||||
public static final int CONTENT_TEXT_CONTENT_COLUMN = 3;
|
||||
public static final int CONTENT_HTML_URI_COLUMN = 2;
|
||||
public static final int CONTENT_TEXT_URI_COLUMN = 3;
|
||||
public static final int CONTENT_SOURCE_KEY_COLUMN = 4;
|
||||
public static final int CONTENT_QUOTED_TEXT_START_POS_COLUMN = 5;
|
||||
|
||||
public static final String[] COMMON_PROJECTION_TEXT = new String[] {
|
||||
BaseColumns._ID, BodyColumns.TEXT_CONTENT
|
||||
};
|
||||
public static final String[] COMMON_PROJECTION_HTML = new String[] {
|
||||
BaseColumns._ID, BodyColumns.HTML_CONTENT
|
||||
};
|
||||
public static final int COMMON_PROJECTION_COLUMN_TEXT = 1;
|
||||
|
||||
private static final String[] PROJECTION_SOURCE_KEY =
|
||||
new String[] {BaseColumns._ID, BodyColumns.SOURCE_MESSAGE_KEY};
|
||||
|
||||
@ -533,10 +523,10 @@ public abstract class EmailContent {
|
||||
* @param cursor a cursor which must NOT be null
|
||||
* @return the Body as restored from the cursor
|
||||
*/
|
||||
private static Body restoreBodyWithCursor(Cursor cursor) {
|
||||
private static Body restoreBodyWithCursor(final Context context, final Cursor cursor) {
|
||||
try {
|
||||
if (cursor.moveToFirst()) {
|
||||
return getContent(cursor, Body.class);
|
||||
return getContent(context, cursor, Body.class);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
@ -545,20 +535,12 @@ public abstract class EmailContent {
|
||||
}
|
||||
}
|
||||
|
||||
public static Body restoreBodyWithId(Context context, long id) {
|
||||
Uri u = ContentUris.withAppendedId(Body.CONTENT_URI, id);
|
||||
Cursor c = context.getContentResolver().query(u, Body.CONTENT_PROJECTION,
|
||||
null, null, null);
|
||||
if (c == null) throw new ProviderUnavailableException();
|
||||
return restoreBodyWithCursor(c);
|
||||
}
|
||||
|
||||
public static Body restoreBodyWithMessageId(Context context, long messageId) {
|
||||
Cursor c = context.getContentResolver().query(Body.CONTENT_URI,
|
||||
Body.CONTENT_PROJECTION, BodyColumns.MESSAGE_KEY + "=?",
|
||||
new String[] {Long.toString(messageId)}, null);
|
||||
if (c == null) throw new ProviderUnavailableException();
|
||||
return restoreBodyWithCursor(c);
|
||||
return restoreBodyWithCursor(context, c);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -596,72 +578,48 @@ public abstract class EmailContent {
|
||||
0, 0L);
|
||||
}
|
||||
|
||||
private static String restoreTextWithMessageId(Context context, long messageId,
|
||||
String[] projection) {
|
||||
Cursor c = context.getContentResolver().query(Body.CONTENT_URI, projection,
|
||||
BodyColumns.MESSAGE_KEY + "=?", new String[] {Long.toString(messageId)}, null);
|
||||
if (c == null) throw new ProviderUnavailableException();
|
||||
try {
|
||||
if (c.moveToFirst()) {
|
||||
return c.getString(COMMON_PROJECTION_COLUMN_TEXT);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} finally {
|
||||
c.close();
|
||||
}
|
||||
}
|
||||
|
||||
public static String restoreBodyTextWithMessageId(Context context, long messageId) {
|
||||
return restoreTextWithMessageId(context, messageId, Body.COMMON_PROJECTION_TEXT);
|
||||
return readBodyFromProvider(context, EmailContent.CONTENT_URI.buildUpon()
|
||||
.appendPath("bodyText").appendPath(Long.toString(messageId)).toString());
|
||||
}
|
||||
|
||||
public static String restoreBodyHtmlWithMessageId(Context context, long messageId) {
|
||||
return restoreTextWithMessageId(context, messageId, Body.COMMON_PROJECTION_HTML);
|
||||
return readBodyFromProvider(context, EmailContent.CONTENT_URI.buildUpon()
|
||||
.appendPath("bodyHtml").appendPath(Long.toString(messageId)).toString());
|
||||
}
|
||||
|
||||
private static String readBodyFromPipe(ParcelFileDescriptor d) {
|
||||
final AutoCloseInputStream htmlInput = new AutoCloseInputStream(d);
|
||||
private static String readBodyFromProvider(final Context context, final String uri) {
|
||||
String content = null;
|
||||
try {
|
||||
content = IOUtils.toString(htmlInput, "utf8");
|
||||
} catch (final IOException e) {
|
||||
LogUtils.e(LogUtils.TAG, e, "IOError while reading message body");
|
||||
content = null;
|
||||
} finally {
|
||||
|
||||
final InputStream bodyInput =
|
||||
context.getContentResolver().openInputStream(Uri.parse(uri));
|
||||
try {
|
||||
htmlInput.close();
|
||||
} catch (final IOException e) {
|
||||
LogUtils.e(LogUtils.TAG, e, "IOError while closing message body");
|
||||
content = IOUtils.toString(bodyInput);
|
||||
} finally {
|
||||
bodyInput.close();
|
||||
}
|
||||
} catch (final IOException e) {
|
||||
LogUtils.v(LogUtils.TAG, e, "Exception while reading body content");
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void restore(Cursor cursor) {
|
||||
public void restore(final Cursor cursor) {
|
||||
throw new UnsupportedOperationException("Must have context to restore Body object");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void restore(final Context context, final Cursor cursor) {
|
||||
warnIfUiThread();
|
||||
mBaseUri = EmailContent.Body.CONTENT_URI;
|
||||
mMessageKey = cursor.getLong(CONTENT_MESSAGE_KEY_COLUMN);
|
||||
// These get overwritten below if we find a file descriptor in the respond() call,
|
||||
// but we'll keep this here in case we want to construct a matrix cursor or something
|
||||
// to build a Body object from.
|
||||
mHtmlContent = cursor.getString(CONTENT_HTML_CONTENT_COLUMN);
|
||||
mTextContent = cursor.getString(CONTENT_TEXT_CONTENT_COLUMN);
|
||||
final int rowId = cursor.getPosition();
|
||||
final Bundle command = new Bundle(2);
|
||||
command.putInt(RESPOND_COMMAND_GET_HTML_PIPE, rowId);
|
||||
command.putInt(RESPOND_COMMAND_GET_TEXT_PIPE, rowId);
|
||||
final Bundle response = cursor.respond(command);
|
||||
final ParcelFileDescriptor htmlDescriptor =
|
||||
response.getParcelable(RESPOND_RESULT_HTML_PIPE_KEY);
|
||||
if (htmlDescriptor != null) {
|
||||
mHtmlContent = readBodyFromPipe(htmlDescriptor);
|
||||
}
|
||||
final ParcelFileDescriptor textDescriptor =
|
||||
response.getParcelable(RESPOND_RESULT_TEXT_PIPE_KEY);
|
||||
if (textDescriptor != null) {
|
||||
mTextContent = readBodyFromPipe(textDescriptor);
|
||||
}
|
||||
mHtmlContent = readBodyFromProvider(context, cursor.getString(CONTENT_HTML_URI_COLUMN));
|
||||
mTextContent = readBodyFromProvider(context, cursor.getString(CONTENT_TEXT_URI_COLUMN));
|
||||
mSourceKey = cursor.getLong(CONTENT_SOURCE_KEY_COLUMN);
|
||||
mQuotedTextStartPos = cursor.getInt(CONTENT_QUOTED_TEXT_START_POS_COLUMN);
|
||||
}
|
||||
@ -1314,10 +1272,10 @@ public abstract class EmailContent {
|
||||
public void setFlags(boolean quotedReply, boolean quotedForward) {
|
||||
// Set message flags as well
|
||||
if (quotedReply || quotedForward) {
|
||||
mFlags &= ~EmailContent.Message.FLAG_TYPE_MASK;
|
||||
mFlags &= ~Message.FLAG_TYPE_MASK;
|
||||
mFlags |= quotedReply
|
||||
? EmailContent.Message.FLAG_TYPE_REPLY
|
||||
: EmailContent.Message.FLAG_TYPE_FORWARD;
|
||||
? Message.FLAG_TYPE_REPLY
|
||||
: Message.FLAG_TYPE_FORWARD;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1647,7 +1605,7 @@ public abstract class EmailContent {
|
||||
}
|
||||
|
||||
public Attachment(Parcel in) {
|
||||
mBaseUri = EmailContent.Attachment.CONTENT_URI;
|
||||
mBaseUri = Attachment.CONTENT_URI;
|
||||
mId = in.readLong();
|
||||
mFileName = in.readString();
|
||||
mMimeType = in.readString();
|
||||
|
@ -520,7 +520,7 @@ public class Mailbox extends EmailContent implements EmailContent.MailboxColumns
|
||||
try {
|
||||
Mailbox mailbox = null;
|
||||
if (c.moveToFirst()) {
|
||||
mailbox = getContent(c, Mailbox.class);
|
||||
mailbox = getContent(context, c, Mailbox.class);
|
||||
if (c.moveToNext()) {
|
||||
LogUtils.w(Logging.LOG_TAG, "Multiple mailboxes named \"%s\"", path);
|
||||
}
|
||||
|
@ -1869,11 +1869,11 @@ public abstract class SyncManager extends Service implements Runnable {
|
||||
// Otherwise, we use the sync interval
|
||||
long syncInterval = c.getInt(Mailbox.CONTENT_SYNC_INTERVAL_COLUMN);
|
||||
if (syncInterval == Mailbox.CHECK_INTERVAL_PUSH) {
|
||||
Mailbox m = EmailContent.getContent(c, Mailbox.class);
|
||||
Mailbox m = EmailContent.getContent(this, c, Mailbox.class);
|
||||
requestSync(m, SYNC_PUSH, null);
|
||||
} else if (mailboxType == Mailbox.TYPE_OUTBOX) {
|
||||
if (hasSendableMessages(c)) {
|
||||
Mailbox m = EmailContent.getContent(c, Mailbox.class);
|
||||
Mailbox m = EmailContent.getContent(this, c, Mailbox.class);
|
||||
startServiceThread(getServiceForMailbox(this, m));
|
||||
}
|
||||
} else if (syncInterval > 0 && syncInterval <= ONE_DAY_MINUTES) {
|
||||
@ -1883,7 +1883,7 @@ public abstract class SyncManager extends Service implements Runnable {
|
||||
long toNextSync = syncInterval*MINUTES - sinceLastSync;
|
||||
String name = c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN);
|
||||
if (toNextSync <= 0) {
|
||||
Mailbox m = EmailContent.getContent(c, Mailbox.class);
|
||||
Mailbox m = EmailContent.getContent(this, c, Mailbox.class);
|
||||
requestSync(m, SYNC_SCHEDULED, null);
|
||||
} else if (toNextSync < nextWait) {
|
||||
nextWait = toNextSync;
|
||||
|
@ -21,27 +21,13 @@ import android.database.CursorWrapper;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteDoneException;
|
||||
import android.database.sqlite.SQLiteStatement;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.os.ParcelFileDescriptor.AutoCloseOutputStream;
|
||||
import android.provider.BaseColumns;
|
||||
import android.text.TextUtils;
|
||||
import android.util.SparseArray;
|
||||
|
||||
import com.android.emailcommon.provider.EmailContent.Body;
|
||||
import com.android.emailcommon.provider.EmailContent.BodyColumns;
|
||||
import com.android.mail.utils.LogUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* This class wraps a cursor for the purpose of bypassing the CursorWindow object for the
|
||||
* potentially over-sized body content fields. The CursorWindow has a hard limit of 2MB and so a
|
||||
@ -58,74 +44,34 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||
* If we want to address that issue fully, we need to return the body through a
|
||||
* ParcelFileDescriptor or some other mechanism that doesn't involve passing the data through a
|
||||
* CursorWindow.
|
||||
*
|
||||
* The fromUiQuery param indicates that this EmailMessageCursor object was created from uiQuery().
|
||||
* This is significant because we know that the body content fields will be retrieved within
|
||||
* the same process as the provider so we can proceed w/o having to worry about any cross
|
||||
* process marshalling issues. Secondly, if the request is made from a uiQuery, the _id column
|
||||
* of the cursor will be a Message._id. If this call is made outside if the uiQuery(), than the
|
||||
* _id column is actually Body._id so we need to proceed accordingly.
|
||||
*/
|
||||
public class EmailMessageCursor extends CursorWrapper {
|
||||
|
||||
private static final BlockingQueue<Runnable> sPoolWorkQueue =
|
||||
new LinkedBlockingQueue<Runnable>(128);
|
||||
|
||||
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
|
||||
private final AtomicInteger mCount = new AtomicInteger(1);
|
||||
|
||||
public Thread newThread(Runnable r) {
|
||||
return new Thread(r, "EmailMessageCursor #" + mCount.getAndIncrement());
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* An {@link Executor} that executes tasks which feed text and html email bodies into streams.
|
||||
*
|
||||
* It is important that this Executor is private to this class since we don't want to risk
|
||||
* sharing a common Executor with Threads that *read* from the stream. If that were to happen
|
||||
* it is possible for all Threads in the Executor to be blocked reads and thus starvation
|
||||
* occurs.
|
||||
*/
|
||||
private static final Executor THREAD_POOL_EXECUTOR
|
||||
= new ThreadPoolExecutor(1, 5, 1, TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);
|
||||
|
||||
private final SparseArray<String> mTextParts;
|
||||
private final SparseArray<String> mHtmlParts;
|
||||
private final int mTextColumnIndex;
|
||||
private final int mHtmlColumnIndex;
|
||||
private final boolean mFromUiQuery;
|
||||
|
||||
public EmailMessageCursor(final Cursor cursor, final SQLiteDatabase db, final String htmlColumn,
|
||||
final String textColumn, final boolean fromUiQuery) {
|
||||
final String textColumn) {
|
||||
super(cursor);
|
||||
mFromUiQuery = fromUiQuery;
|
||||
mHtmlColumnIndex = cursor.getColumnIndex(htmlColumn);
|
||||
mTextColumnIndex = cursor.getColumnIndex(textColumn);
|
||||
final int cursorSize = cursor.getCount();
|
||||
mHtmlParts = new SparseArray<String>(cursorSize);
|
||||
mTextParts = new SparseArray<String>(cursorSize);
|
||||
|
||||
final String rowIdColumn;
|
||||
if (fromUiQuery) {
|
||||
// In the UI query, the _id column is the id in the message table so it is
|
||||
// messageKey in the Body table.
|
||||
rowIdColumn = BodyColumns.MESSAGE_KEY;
|
||||
} else {
|
||||
// In the non-UI query, the _id column is the id in the Body table.
|
||||
rowIdColumn = BaseColumns._ID;
|
||||
}
|
||||
|
||||
// TODO: Load this from the provider instead of duplicating the loading code here
|
||||
final SQLiteStatement htmlSql = db.compileStatement(
|
||||
"SELECT " + BodyColumns.HTML_CONTENT +
|
||||
" FROM " + Body.TABLE_NAME +
|
||||
" WHERE " + rowIdColumn + "=?"
|
||||
" WHERE " + BodyColumns.MESSAGE_KEY + "=?"
|
||||
);
|
||||
|
||||
final SQLiteStatement textSql = db.compileStatement(
|
||||
"SELECT " + BodyColumns.TEXT_CONTENT +
|
||||
" FROM " + Body.TABLE_NAME +
|
||||
" WHERE " + rowIdColumn + "=?"
|
||||
" WHERE " + BodyColumns.MESSAGE_KEY + "=?"
|
||||
);
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
@ -155,12 +101,10 @@ public class EmailMessageCursor extends CursorWrapper {
|
||||
|
||||
@Override
|
||||
public String getString(final int columnIndex) {
|
||||
if (mFromUiQuery) {
|
||||
if (columnIndex == mHtmlColumnIndex) {
|
||||
return mHtmlParts.get(getPosition());
|
||||
} else if (columnIndex == mTextColumnIndex) {
|
||||
return mTextParts.get(getPosition());
|
||||
}
|
||||
if (columnIndex == mHtmlColumnIndex) {
|
||||
return mHtmlParts.get(getPosition());
|
||||
} else if (columnIndex == mTextColumnIndex) {
|
||||
return mTextParts.get(getPosition());
|
||||
}
|
||||
return super.getString(columnIndex);
|
||||
}
|
||||
@ -175,53 +119,4 @@ public class EmailMessageCursor extends CursorWrapper {
|
||||
return super.getType(columnIndex);
|
||||
}
|
||||
}
|
||||
|
||||
private static ParcelFileDescriptor createPipeAndFillAsync(final String contents) {
|
||||
try {
|
||||
final ParcelFileDescriptor descriptors[] = ParcelFileDescriptor.createPipe();
|
||||
final ParcelFileDescriptor readDescriptor = descriptors[0];
|
||||
final ParcelFileDescriptor writeDescriptor = descriptors[1];
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
final AutoCloseOutputStream outStream =
|
||||
new AutoCloseOutputStream(writeDescriptor);
|
||||
try {
|
||||
outStream.write(contents.getBytes("utf8"));
|
||||
} catch (final IOException e) {
|
||||
LogUtils.e(LogUtils.TAG, e, "IOException while writing to body pipe");
|
||||
} finally {
|
||||
try {
|
||||
outStream.close();
|
||||
} catch (final IOException e) {
|
||||
LogUtils.e(LogUtils.TAG, e, "IOException while closing body pipe");
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}.executeOnExecutor(THREAD_POOL_EXECUTOR);
|
||||
return readDescriptor;
|
||||
} catch (final IOException e) {
|
||||
LogUtils.e(LogUtils.TAG, e, "IOException while creating body pipe");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle respond(Bundle extras) {
|
||||
final int htmlRow = extras.getInt(Body.RESPOND_COMMAND_GET_HTML_PIPE, -1);
|
||||
final int textRow = extras.getInt(Body.RESPOND_COMMAND_GET_TEXT_PIPE, -1);
|
||||
|
||||
final Bundle b = new Bundle(2);
|
||||
|
||||
if (htmlRow >= 0 && !TextUtils.isEmpty(mHtmlParts.get(htmlRow))) {
|
||||
b.putParcelable(Body.RESPOND_RESULT_HTML_PIPE_KEY,
|
||||
createPipeAndFillAsync(mHtmlParts.get(htmlRow)));
|
||||
}
|
||||
if (textRow >= 0 && !TextUtils.isEmpty(mTextParts.get(textRow))) {
|
||||
b.putParcelable(Body.RESPOND_RESULT_TEXT_PIPE_KEY,
|
||||
createPipeAndFillAsync(mTextParts.get(textRow)));
|
||||
}
|
||||
return b;
|
||||
}
|
||||
}
|
||||
|
@ -40,7 +40,9 @@ import android.database.DatabaseUtils;
|
||||
import android.database.MatrixCursor;
|
||||
import android.database.MergeCursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteDoneException;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
import android.database.sqlite.SQLiteStatement;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Binder;
|
||||
@ -51,6 +53,7 @@ import android.os.Handler.Callback;
|
||||
import android.os.Looper;
|
||||
import android.os.Parcel;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.os.ParcelFileDescriptor.AutoCloseOutputStream;
|
||||
import android.os.RemoteException;
|
||||
import android.provider.BaseColumns;
|
||||
import android.text.TextUtils;
|
||||
@ -126,6 +129,7 @@ import com.google.common.collect.Sets;
|
||||
import java.io.File;
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@ -134,6 +138,13 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
@ -267,6 +278,8 @@ public class EmailProvider extends ContentProvider {
|
||||
private static final int BODY_BASE = 0xA000;
|
||||
private static final int BODY = BODY_BASE;
|
||||
private static final int BODY_ID = BODY_BASE + 1;
|
||||
private static final int BODY_HTML = BODY_BASE + 2;
|
||||
private static final int BODY_TEXT = BODY_BASE + 3;
|
||||
|
||||
private static final int CREDENTIAL_BASE = 0xB000;
|
||||
private static final int CREDENTIAL = CREDENTIAL_BASE;
|
||||
@ -1067,6 +1080,10 @@ public class EmailProvider extends ContentProvider {
|
||||
sURIMatcher.addURI(EmailContent.AUTHORITY, "body", BODY);
|
||||
// A specific mail body
|
||||
sURIMatcher.addURI(EmailContent.AUTHORITY, "body/#", BODY_ID);
|
||||
// A specific HTML body part, for openFile
|
||||
sURIMatcher.addURI(EmailContent.AUTHORITY, "bodyHtml/#", BODY_HTML);
|
||||
// A specific text body part, for openFile
|
||||
sURIMatcher.addURI(EmailContent.AUTHORITY, "bodyText/#", BODY_TEXT);
|
||||
|
||||
// All hostauth records
|
||||
sURIMatcher.addURI(EmailContent.AUTHORITY, "hostauth", HOSTAUTH);
|
||||
@ -1299,9 +1316,18 @@ public class EmailProvider extends ContentProvider {
|
||||
final ProjectionMap map = new ProjectionMap.Builder()
|
||||
.addAll(projection)
|
||||
.build();
|
||||
if (map.containsKey(BodyColumns.HTML_CONTENT) ||
|
||||
map.containsKey(BodyColumns.TEXT_CONTENT)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Body content cannot be returned in the cursor");
|
||||
}
|
||||
|
||||
final ContentValues cv = new ContentValues(2);
|
||||
cv.put(BodyColumns.HTML_CONTENT, ""); // Loaded in EmailMessageCursor
|
||||
cv.put(BodyColumns.TEXT_CONTENT, ""); // Loaded in EmailMessageCursor
|
||||
cv.put(BodyColumns.HTML_CONTENT_URI, "@" + uriWithColumn("bodyHtml",
|
||||
BodyColumns.MESSAGE_KEY));
|
||||
cv.put(BodyColumns.TEXT_CONTENT_URI, "@" + uriWithColumn("bodyText",
|
||||
BodyColumns.MESSAGE_KEY));
|
||||
|
||||
final StringBuilder sb = genSelect(map, projection, cv);
|
||||
sb.append(" FROM ").append(Body.TABLE_NAME);
|
||||
if (match == BODY_ID) {
|
||||
@ -1317,13 +1343,6 @@ public class EmailProvider extends ContentProvider {
|
||||
sb.append(" LIMIT ").append(limit);
|
||||
}
|
||||
c = db.rawQuery(sb.toString(), selectionArgs);
|
||||
if (c != null) {
|
||||
// We don't want to deliver the body contents inline here because we might
|
||||
// be sending this cursor to the Exchange process, and we'll blow out the
|
||||
// CursorWindow if there's a large message body.
|
||||
c = new EmailMessageCursor(c, db, BodyColumns.HTML_CONTENT,
|
||||
BodyColumns.TEXT_CONTENT, false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MESSAGE_ID:
|
||||
@ -2076,8 +2095,34 @@ public class EmailProvider extends ContentProvider {
|
||||
return result;
|
||||
}
|
||||
|
||||
// TODO: remove this when we move message bodies to actual files
|
||||
private static final BlockingQueue<Runnable> sPoolWorkQueue =
|
||||
new LinkedBlockingQueue<Runnable>(128);
|
||||
|
||||
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
|
||||
private final AtomicInteger mCount = new AtomicInteger(1);
|
||||
|
||||
public Thread newThread(Runnable r) {
|
||||
return new Thread(r, "EmailProviderOpenFile #" + mCount.getAndIncrement());
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* An {@link java.util.concurrent.Executor} that executes tasks which feed text and html email
|
||||
* bodies into streams.
|
||||
*
|
||||
* It is important that this Executor is private to this class since we don't want to risk
|
||||
* sharing a common Executor with Threads that *read* from the stream. If that were to happen
|
||||
* it is possible for all Threads in the Executor to be blocked reads and thus starvation
|
||||
* occurs.
|
||||
*/
|
||||
private static final Executor OPEN_FILE_EXECUTOR = new ThreadPoolExecutor(1 /* corePoolSize */,
|
||||
5 /* maxPoolSize */, 1 /* keepAliveTime */, TimeUnit.SECONDS,
|
||||
sPoolWorkQueue, sThreadFactory);
|
||||
|
||||
@Override
|
||||
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
|
||||
public ParcelFileDescriptor openFile(final Uri uri, final String mode)
|
||||
throws FileNotFoundException {
|
||||
if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
|
||||
LogUtils.d(TAG, "EmailProvider.openFile: %s", LogUtils.contentUriToString(TAG, uri));
|
||||
}
|
||||
@ -2103,6 +2148,71 @@ public class EmailProvider extends ContentProvider {
|
||||
}
|
||||
}
|
||||
break;
|
||||
case BODY_HTML:
|
||||
case BODY_TEXT:
|
||||
final ParcelFileDescriptor descriptors[];
|
||||
try {
|
||||
descriptors = ParcelFileDescriptor.createPipe();
|
||||
} catch (final IOException e) {
|
||||
throw new FileNotFoundException();
|
||||
}
|
||||
final ParcelFileDescriptor readDescriptor = descriptors[0];
|
||||
final ParcelFileDescriptor writeDescriptor = descriptors[1];
|
||||
|
||||
final SQLiteDatabase db = getDatabase(getContext());
|
||||
final SQLiteStatement sql;
|
||||
|
||||
if (match == BODY_HTML) {
|
||||
sql = db.compileStatement(
|
||||
"SELECT " + BodyColumns.HTML_CONTENT +
|
||||
" FROM " + Body.TABLE_NAME +
|
||||
" WHERE " + BodyColumns.MESSAGE_KEY + "=?");
|
||||
} else { // BODY_TEXT
|
||||
sql = db.compileStatement(
|
||||
"SELECT " + BodyColumns.TEXT_CONTENT +
|
||||
" FROM " + Body.TABLE_NAME +
|
||||
" WHERE " + BodyColumns.MESSAGE_KEY + "=?");
|
||||
}
|
||||
|
||||
final long messageKey = Long.valueOf(uri.getLastPathSegment());
|
||||
sql.bindLong(1, messageKey);
|
||||
final String contents;
|
||||
try {
|
||||
contents = sql.simpleQueryForString();
|
||||
} catch (final SQLiteDoneException e) {
|
||||
LogUtils.v(LogUtils.TAG, e,
|
||||
"Done exception while reading %s body for message %d",
|
||||
match == BODY_HTML ? "html" : "text", messageKey);
|
||||
throw new FileNotFoundException();
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(contents)) {
|
||||
throw new FileNotFoundException("Body field is empty");
|
||||
}
|
||||
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
final AutoCloseOutputStream outStream =
|
||||
new AutoCloseOutputStream(writeDescriptor);
|
||||
try {
|
||||
outStream.write(contents.getBytes("utf8"));
|
||||
} catch (final IOException e) {
|
||||
LogUtils.e(LogUtils.TAG, e,
|
||||
"IOException while writing to body pipe");
|
||||
} finally {
|
||||
try {
|
||||
outStream.close();
|
||||
} catch (final IOException e) {
|
||||
LogUtils.e(LogUtils.TAG, e,
|
||||
"IOException while closing body pipe");
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}.executeOnExecutor(OPEN_FILE_EXECUTOR);
|
||||
return readDescriptor;
|
||||
// break;
|
||||
}
|
||||
|
||||
throw new FileNotFoundException("unable to open file");
|
||||
@ -4353,7 +4463,7 @@ public class EmailProvider extends ContentProvider {
|
||||
}
|
||||
if (c != null) {
|
||||
c = new EmailMessageCursor(c, db, UIProvider.MessageColumns.BODY_HTML,
|
||||
UIProvider.MessageColumns.BODY_TEXT, true /* deliverColumnsInline */);
|
||||
UIProvider.MessageColumns.BODY_TEXT);
|
||||
}
|
||||
notifyUri = UIPROVIDER_MESSAGE_NOTIFIER.buildUpon().appendPath(id).build();
|
||||
break;
|
||||
|
@ -71,7 +71,7 @@ public class Utilities {
|
||||
if (c == null) {
|
||||
return;
|
||||
} else if (c.moveToNext()) {
|
||||
localMessage = EmailContent.getContent(c, EmailContent.Message.class);
|
||||
localMessage = EmailContent.getContent(context, c, EmailContent.Message.class);
|
||||
} else {
|
||||
localMessage = new EmailContent.Message();
|
||||
}
|
||||
|
@ -824,7 +824,7 @@ public class ImapService extends Service {
|
||||
// loop through messages marked as deleted
|
||||
while (deletes.moveToNext()) {
|
||||
EmailContent.Message oldMessage =
|
||||
EmailContent.getContent(deletes, EmailContent.Message.class);
|
||||
EmailContent.getContent(context, deletes, EmailContent.Message.class);
|
||||
|
||||
if (oldMessage != null) {
|
||||
lastMessageId = oldMessage.mId;
|
||||
@ -961,7 +961,7 @@ public class ImapService extends Service {
|
||||
boolean changeAnswered = false;
|
||||
|
||||
EmailContent.Message oldMessage =
|
||||
EmailContent.getContent(updates, EmailContent.Message.class);
|
||||
EmailContent.getContent(context, updates, EmailContent.Message.class);
|
||||
lastMessageId = oldMessage.mId;
|
||||
EmailContent.Message newMessage =
|
||||
EmailContent.Message.restoreMessageWithId(context, oldMessage.mId);
|
||||
|
@ -115,7 +115,8 @@ public class LegacyConversionsTests extends ProviderTestCase2<EmailProvider> {
|
||||
try {
|
||||
assertEquals(2, c.getCount());
|
||||
while (c.moveToNext()) {
|
||||
Attachment attachment = Attachment.getContent(c, Attachment.class);
|
||||
Attachment attachment =
|
||||
Attachment.getContent(mProviderContext, c, Attachment.class);
|
||||
if ("100".equals(attachment.mLocation)) {
|
||||
checkAttachment("attachment1Part", attachments.get(0), attachment,
|
||||
localMessage.mAccountKey);
|
||||
|
@ -420,7 +420,7 @@ public class ProviderTests extends ProviderTestCase2<EmailProvider> {
|
||||
new String[] {String.valueOf(messageId)}, null);
|
||||
int numBodies = c.getCount();
|
||||
assertTrue("at most one body", numBodies < 2);
|
||||
return c.moveToFirst() ? EmailContent.getContent(c, Body.class) : null;
|
||||
return c.moveToFirst() ? EmailContent.getContent(mMockContext, c, Body.class) : null;
|
||||
} finally {
|
||||
c.close();
|
||||
}
|
||||
@ -515,7 +515,7 @@ public class ProviderTests extends ProviderTestCase2<EmailProvider> {
|
||||
assertEquals(3, numAtts);
|
||||
int i = 0;
|
||||
while (c.moveToNext()) {
|
||||
Attachment actual = EmailContent.getContent(c, Attachment.class);
|
||||
Attachment actual = EmailContent.getContent(mMockContext, c, Attachment.class);
|
||||
ProviderTestUtils.assertAttachmentEqual("save-message3", atts.get(i), actual);
|
||||
i++;
|
||||
}
|
||||
@ -567,7 +567,7 @@ public class ProviderTests extends ProviderTestCase2<EmailProvider> {
|
||||
assertEquals(3, numAtts);
|
||||
int i = 0;
|
||||
while (c.moveToNext()) {
|
||||
Attachment actual = EmailContent.getContent(c, Attachment.class);
|
||||
Attachment actual = EmailContent.getContent(mMockContext, c, Attachment.class);
|
||||
ProviderTestUtils.assertAttachmentEqual("save-message4", atts.get(i), actual);
|
||||
i++;
|
||||
}
|
||||
@ -1292,7 +1292,7 @@ public class ProviderTests extends ProviderTestCase2<EmailProvider> {
|
||||
cr.query(Message.UPDATED_CONTENT_URI, Message.CONTENT_PROJECTION, null, null, null);
|
||||
try {
|
||||
assertTrue(c.moveToFirst());
|
||||
Message originalMessage = EmailContent.getContent(c, Message.class);
|
||||
Message originalMessage = EmailContent.getContent(mMockContext, c, Message.class);
|
||||
// make sure this has the original value
|
||||
assertEquals("from message2", originalMessage.mFrom);
|
||||
// Should only be one
|
||||
@ -1600,10 +1600,10 @@ public class ProviderTests extends ProviderTestCase2<EmailProvider> {
|
||||
|
||||
try {
|
||||
c.moveToFirst();
|
||||
Attachment a1Get = EmailContent.getContent(c, Attachment.class);
|
||||
Attachment a1Get = EmailContent.getContent(mMockContext, c, Attachment.class);
|
||||
ProviderTestUtils.assertAttachmentEqual("getAttachByUri-1", a1, a1Get);
|
||||
c.moveToNext();
|
||||
Attachment a2Get = EmailContent.getContent(c, Attachment.class);
|
||||
Attachment a2Get = EmailContent.getContent(mMockContext, c, Attachment.class);
|
||||
ProviderTestUtils.assertAttachmentEqual("getAttachByUri-2", a2, a2Get);
|
||||
} finally {
|
||||
c.close();
|
||||
@ -1638,10 +1638,10 @@ public class ProviderTests extends ProviderTestCase2<EmailProvider> {
|
||||
|
||||
try {
|
||||
c.moveToFirst();
|
||||
Attachment a3Get = EmailContent.getContent(c, Attachment.class);
|
||||
Attachment a3Get = EmailContent.getContent(mMockContext, c, Attachment.class);
|
||||
ProviderTestUtils.assertAttachmentEqual("getAttachByUri-3", a3, a3Get);
|
||||
c.moveToNext();
|
||||
Attachment a4Get = EmailContent.getContent(c, Attachment.class);
|
||||
Attachment a4Get = EmailContent.getContent(mMockContext, c, Attachment.class);
|
||||
ProviderTestUtils.assertAttachmentEqual("getAttachByUri-4", a4, a4Get);
|
||||
} finally {
|
||||
c.close();
|
||||
|
Loading…
Reference in New Issue
Block a user