1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.email.provider;
18
19import android.content.ContentResolver;
20import android.content.Context;
21import android.database.Cursor;
22import android.database.CursorWrapper;
23import android.net.Uri;
24import android.provider.BaseColumns;
25import android.util.SparseArray;
26
27import com.android.emailcommon.provider.EmailContent.Body;
28import com.android.mail.utils.HtmlSanitizer;
29import com.android.mail.utils.LogUtils;
30
31import org.apache.commons.io.IOUtils;
32
33import java.io.IOException;
34import java.io.InputStream;
35
36/**
37 * This class wraps a cursor for the purpose of bypassing the CursorWindow object for the
38 * potentially over-sized body content fields. The CursorWindow has a hard limit of 2MB and so a
39 * large email message can exceed that limit and cause the cursor to fail to load.
40 *
41 * To get around this, we load null values in those columns, and then in this wrapper we directly
42 * load the content from the provider, skipping the cursor window.
43 *
44 * This will still potentially blow up if this cursor gets wrapped in a CrossProcessCursorWrapper
45 * which uses a CursorWindow to shuffle results between processes. Since we're only using this for
46 * passing a cursor back to UnifiedEmail this shouldn't be an issue.
47 */
48public class EmailMessageCursor extends CursorWrapper {
49
50    private final SparseArray<String> mTextParts;
51    private final SparseArray<String> mHtmlParts;
52    private final int mTextColumnIndex;
53    private final int mHtmlColumnIndex;
54
55    public EmailMessageCursor(final Context c, final Cursor cursor, final String htmlColumn,
56            final String textColumn) {
57        super(cursor);
58        mHtmlColumnIndex = cursor.getColumnIndex(htmlColumn);
59        mTextColumnIndex = cursor.getColumnIndex(textColumn);
60        final int cursorSize = cursor.getCount();
61        mHtmlParts = new SparseArray<String>(cursorSize);
62        mTextParts = new SparseArray<String>(cursorSize);
63
64        final ContentResolver cr = c.getContentResolver();
65
66        while (cursor.moveToNext()) {
67            final int position = cursor.getPosition();
68            final long messageId = cursor.getLong(cursor.getColumnIndex(BaseColumns._ID));
69            try {
70                if (mHtmlColumnIndex != -1) {
71                    final Uri htmlUri = Body.getBodyHtmlUriForMessageWithId(messageId);
72                    final InputStream in = cr.openInputStream(htmlUri);
73                    final String underlyingHtmlString;
74                    try {
75                        underlyingHtmlString = IOUtils.toString(in);
76                    } finally {
77                        in.close();
78                    }
79                    final String sanitizedHtml = HtmlSanitizer.sanitizeHtml(underlyingHtmlString);
80                    mHtmlParts.put(position, sanitizedHtml);
81                }
82            } catch (final IOException e) {
83                LogUtils.v(LogUtils.TAG, e, "Did not find html body for message %d", messageId);
84            }
85            try {
86                if (mTextColumnIndex != -1) {
87                    final Uri textUri = Body.getBodyTextUriForMessageWithId(messageId);
88                    final InputStream in = cr.openInputStream(textUri);
89                    final String underlyingTextString;
90                    try {
91                        underlyingTextString = IOUtils.toString(in);
92                    } finally {
93                        in.close();
94                    }
95                    mTextParts.put(position, underlyingTextString);
96                }
97            } catch (final IOException e) {
98                LogUtils.v(LogUtils.TAG, e, "Did not find text body for message %d", messageId);
99            }
100        }
101        cursor.moveToPosition(-1);
102    }
103
104    @Override
105    public String getString(final int columnIndex) {
106        if (columnIndex == mHtmlColumnIndex) {
107            return mHtmlParts.get(getPosition());
108        } else if (columnIndex == mTextColumnIndex) {
109            return mTextParts.get(getPosition());
110        }
111        return super.getString(columnIndex);
112    }
113
114    @Override
115    public int getType(int columnIndex) {
116        if (columnIndex == mHtmlColumnIndex || columnIndex == mTextColumnIndex) {
117            // Need to force this, otherwise we might fall through to some other get*() method
118            // instead of getString() if the underlying cursor has other ideas about this content
119            return FIELD_TYPE_STRING;
120        } else {
121            return super.getType(columnIndex);
122        }
123    }
124}
125