From f678a18b69c95ef1a448cb5cb7cd3699be7c5423 Mon Sep 17 00:00:00 2001 From: Tony Mantler Date: Tue, 15 Apr 2014 11:27:23 -0700 Subject: [PATCH] Bypass the cursor window for email bodies b/11787468 Change-Id: Iba5faa5b825357144d07ec4dfcf010c6af50496d --- .../email/provider/EmailMessageCursor.java | 115 ++++++++++++++++++ .../android/email/provider/EmailProvider.java | 39 +++++- 2 files changed, 150 insertions(+), 4 deletions(-) create mode 100644 src/com/android/email/provider/EmailMessageCursor.java diff --git a/src/com/android/email/provider/EmailMessageCursor.java b/src/com/android/email/provider/EmailMessageCursor.java new file mode 100644 index 000000000..150f5ed57 --- /dev/null +++ b/src/com/android/email/provider/EmailMessageCursor.java @@ -0,0 +1,115 @@ +/* + * 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; +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; + +/** + * 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. + */ +public class EmailMessageCursor extends CursorWrapper { + private final SparseArray mTextParts; + private final SparseArray mHtmlParts; + private final int mTextColumnIndex; + private final int mHtmlColumnIndex; + + public EmailMessageCursor(final Cursor cursor, final SQLiteDatabase db, final String htmlColumn, + final String textColumn) { + super(cursor); + mHtmlColumnIndex = cursor.getColumnIndex(htmlColumn); + mTextColumnIndex = cursor.getColumnIndex(textColumn); + final int cursorSize = cursor.getCount(); + mHtmlParts = new SparseArray(cursorSize); + mTextParts = new SparseArray(cursorSize); + + 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 int position = cursor.getPosition(); + final long rowId = cursor.getLong(cursor.getColumnIndex(BaseColumns._ID)); + htmlSql.bindLong(1, rowId); + textSql.bindLong(1, rowId); + try { + if (mHtmlColumnIndex != -1) { + final String underlyingHtmlString = htmlSql.simpleQueryForString(); + mHtmlParts.put(position, underlyingHtmlString); + } + if (mTextColumnIndex != -1) { + final String underlyingTextString = textSql.simpleQueryForString(); + mTextParts.put(position, underlyingTextString); + } + } catch (final SQLiteDoneException e) { + LogUtils.d(LogUtils.TAG, e, "Done"); + } + } + cursor.moveToPosition(-1); + } + + @Override + public String getString(final int columnIndex) { + if (columnIndex == mHtmlColumnIndex) { + return mHtmlParts.get(getPosition()); + } else if (columnIndex == mTextColumnIndex) { + return mTextParts.get(getPosition()); + } + 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); + } + } +} diff --git a/src/com/android/email/provider/EmailProvider.java b/src/com/android/email/provider/EmailProvider.java index c86d071b4..c36109470 100644 --- a/src/com/android/email/provider/EmailProvider.java +++ b/src/com/android/email/provider/EmailProvider.java @@ -1273,7 +1273,6 @@ public class EmailProvider extends ContentProvider { case MESSAGE_STATE_CHANGE: return db.query(MessageStateChange.TABLE_NAME, projection, selection, selectionArgs, null, null, sortOrder, limit); - case BODY: case MESSAGE: case UPDATED_MESSAGE: case DELETED_MESSAGE: @@ -1289,7 +1288,35 @@ public class EmailProvider extends ContentProvider { case QUICK_RESPONSE: c = uiQuickResponse(projection); break; - case BODY_ID: + case BODY: + case BODY_ID: { + final ProjectionMap map = new ProjectionMap.Builder() + .addAll(projection) + .build(); + final ContentValues cv = new ContentValues(2); + cv.put(BodyColumns.HTML_CONTENT, ""); // Loaded in EmailMessageCursor + cv.put(BodyColumns.TEXT_CONTENT, ""); // Loaded in EmailMessageCursor + final StringBuilder sb = genSelect(map, projection, cv); + sb.append(" FROM ").append(Body.TABLE_NAME); + if (match == BODY_ID) { + id = uri.getPathSegments().get(1); + sb.append(" WHERE ").append(whereWithId(id, selection)); + } else if (!TextUtils.isEmpty(selection)) { + sb.append(" WHERE ").append(selection); + } + if (!TextUtils.isEmpty(sortOrder)) { + sb.append(" ORDER BY ").append(sortOrder); + } + if (!TextUtils.isEmpty(limit)) { + sb.append(" LIMIT ").append(limit); + } + c = db.rawQuery(sb.toString(), selectionArgs); + if (c != null) { + c = new EmailMessageCursor(c, db, BodyColumns.HTML_CONTENT, + BodyColumns.TEXT_CONTENT); + } + break; + } case MESSAGE_ID: case DELETED_MESSAGE_ID: case UPDATED_MESSAGE_ID: @@ -2356,8 +2383,8 @@ public class EmailProvider extends ContentProvider { .add(UIProvider.MessageColumns.BCC, MessageColumns.BCC_LIST) .add(UIProvider.MessageColumns.REPLY_TO, MessageColumns.REPLY_TO_LIST) .add(UIProvider.MessageColumns.DATE_RECEIVED_MS, MessageColumns.TIMESTAMP) - .add(UIProvider.MessageColumns.BODY_HTML, BodyColumns.HTML_CONTENT) - .add(UIProvider.MessageColumns.BODY_TEXT, BodyColumns.TEXT_CONTENT) + .add(UIProvider.MessageColumns.BODY_HTML, "") // Loaded in EmailMessageCursor + .add(UIProvider.MessageColumns.BODY_TEXT, "") // Loaded in EmailMessageCursor .add(UIProvider.MessageColumns.REF_MESSAGE_ID, "0") .add(UIProvider.MessageColumns.DRAFT_TYPE, NOT_A_DRAFT_STRING) .add(UIProvider.MessageColumns.APPEND_REF_MESSAGE_CONTENT, "0") @@ -4267,6 +4294,10 @@ public class EmailProvider extends ContentProvider { } else { c = db.rawQuery(sql, new String[] {id}); } + if (c != null) { + c = new EmailMessageCursor(c, db, UIProvider.MessageColumns.BODY_HTML, + UIProvider.MessageColumns.BODY_TEXT); + } notifyUri = UIPROVIDER_MESSAGE_NOTIFIER.buildUpon().appendPath(id).build(); break; case UI_ATTACHMENTS: