2014-04-15 18:27:23 +00:00
|
|
|
/*
|
|
|
|
* Copyright (C) 2014 The Android Open Source Project
|
|
|
|
*
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
*
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
*
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
* limitations under the License.
|
|
|
|
*/
|
|
|
|
|
|
|
|
package com.android.email.provider;
|
|
|
|
|
|
|
|
import android.database.Cursor;
|
|
|
|
import android.database.CursorWrapper;
|
|
|
|
import android.database.sqlite.SQLiteDatabase;
|
|
|
|
import android.database.sqlite.SQLiteDoneException;
|
|
|
|
import android.database.sqlite.SQLiteStatement;
|
2014-04-16 21:21:35 +00:00
|
|
|
import android.os.AsyncTask;
|
|
|
|
import android.os.Bundle;
|
|
|
|
import android.os.ParcelFileDescriptor;
|
|
|
|
import android.os.ParcelFileDescriptor.AutoCloseOutputStream;
|
2014-04-15 18:27:23 +00:00
|
|
|
import android.provider.BaseColumns;
|
2014-04-16 21:21:35 +00:00
|
|
|
import android.text.TextUtils;
|
2014-04-15 18:27:23 +00:00
|
|
|
import android.util.SparseArray;
|
|
|
|
|
|
|
|
import com.android.emailcommon.provider.EmailContent.Body;
|
|
|
|
import com.android.emailcommon.provider.EmailContent.BodyColumns;
|
|
|
|
import com.android.mail.utils.LogUtils;
|
|
|
|
|
2014-04-16 21:21:35 +00:00
|
|
|
import java.io.IOException;
|
|
|
|
|
2014-04-15 18:27:23 +00:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*
|
|
|
|
* 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.
|
2014-04-21 23:43:35 +00:00
|
|
|
*
|
|
|
|
* 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.
|
2014-04-15 18:27:23 +00:00
|
|
|
*/
|
|
|
|
public class EmailMessageCursor extends CursorWrapper {
|
|
|
|
private final SparseArray<String> mTextParts;
|
|
|
|
private final SparseArray<String> mHtmlParts;
|
|
|
|
private final int mTextColumnIndex;
|
|
|
|
private final int mHtmlColumnIndex;
|
2014-04-21 23:43:35 +00:00
|
|
|
private final boolean mFromUiQuery;
|
2014-04-15 18:27:23 +00:00
|
|
|
|
|
|
|
public EmailMessageCursor(final Cursor cursor, final SQLiteDatabase db, final String htmlColumn,
|
2014-04-21 23:43:35 +00:00
|
|
|
final String textColumn, final boolean fromUiQuery) {
|
2014-04-15 18:27:23 +00:00
|
|
|
super(cursor);
|
2014-04-21 23:43:35 +00:00
|
|
|
mFromUiQuery = fromUiQuery;
|
2014-04-15 18:27:23 +00:00
|
|
|
mHtmlColumnIndex = cursor.getColumnIndex(htmlColumn);
|
|
|
|
mTextColumnIndex = cursor.getColumnIndex(textColumn);
|
|
|
|
final int cursorSize = cursor.getCount();
|
|
|
|
mHtmlParts = new SparseArray<String>(cursorSize);
|
|
|
|
mTextParts = new SparseArray<String>(cursorSize);
|
|
|
|
|
2014-04-21 23:43:35 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2014-04-15 18:27:23 +00:00
|
|
|
final SQLiteStatement htmlSql = db.compileStatement(
|
|
|
|
"SELECT " + BodyColumns.HTML_CONTENT +
|
|
|
|
" FROM " + Body.TABLE_NAME +
|
2014-04-21 23:43:35 +00:00
|
|
|
" WHERE " + rowIdColumn + "=?"
|
2014-04-15 18:27:23 +00:00
|
|
|
);
|
2014-04-21 23:43:35 +00:00
|
|
|
|
2014-04-15 18:27:23 +00:00
|
|
|
final SQLiteStatement textSql = db.compileStatement(
|
|
|
|
"SELECT " + BodyColumns.TEXT_CONTENT +
|
|
|
|
" FROM " + Body.TABLE_NAME +
|
2014-04-21 23:43:35 +00:00
|
|
|
" WHERE " + rowIdColumn + "=?"
|
2014-04-15 18:27:23 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
while (cursor.moveToNext()) {
|
|
|
|
final int position = cursor.getPosition();
|
|
|
|
final long rowId = cursor.getLong(cursor.getColumnIndex(BaseColumns._ID));
|
|
|
|
htmlSql.bindLong(1, rowId);
|
|
|
|
try {
|
|
|
|
if (mHtmlColumnIndex != -1) {
|
|
|
|
final String underlyingHtmlString = htmlSql.simpleQueryForString();
|
|
|
|
mHtmlParts.put(position, underlyingHtmlString);
|
|
|
|
}
|
2014-04-21 23:43:35 +00:00
|
|
|
} catch (final SQLiteDoneException e) {
|
|
|
|
LogUtils.d(LogUtils.TAG, e, "Done with the HTML column");
|
|
|
|
}
|
|
|
|
textSql.bindLong(1, rowId);
|
|
|
|
try {
|
2014-04-15 18:27:23 +00:00
|
|
|
if (mTextColumnIndex != -1) {
|
|
|
|
final String underlyingTextString = textSql.simpleQueryForString();
|
|
|
|
mTextParts.put(position, underlyingTextString);
|
|
|
|
}
|
|
|
|
} catch (final SQLiteDoneException e) {
|
2014-04-21 23:43:35 +00:00
|
|
|
LogUtils.d(LogUtils.TAG, e, "Done with the text column");
|
2014-04-15 18:27:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
cursor.moveToPosition(-1);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public String getString(final int columnIndex) {
|
2014-04-21 23:43:35 +00:00
|
|
|
if (mFromUiQuery) {
|
2014-04-16 21:21:35 +00:00
|
|
|
if (columnIndex == mHtmlColumnIndex) {
|
|
|
|
return mHtmlParts.get(getPosition());
|
|
|
|
} else if (columnIndex == mTextColumnIndex) {
|
|
|
|
return mTextParts.get(getPosition());
|
|
|
|
}
|
2014-04-15 18:27:23 +00:00
|
|
|
}
|
|
|
|
return super.getString(columnIndex);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public int getType(int columnIndex) {
|
|
|
|
if (columnIndex == mHtmlColumnIndex || columnIndex == mTextColumnIndex) {
|
|
|
|
// Need to force this, otherwise we might fall through to some other get*() method
|
|
|
|
// instead of getString() if the underlying cursor has other ideas about this content
|
|
|
|
return FIELD_TYPE_STRING;
|
|
|
|
} else {
|
|
|
|
return super.getType(columnIndex);
|
|
|
|
}
|
|
|
|
}
|
2014-04-16 21:21:35 +00:00
|
|
|
|
|
|
|
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(AsyncTask.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;
|
|
|
|
}
|
2014-04-15 18:27:23 +00:00
|
|
|
}
|