1/*
2 * Copyright (C) 2008 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.ContentProvider;
20import android.content.ContentUris;
21import android.content.ContentValues;
22import android.content.Context;
23import android.content.pm.PackageManager;
24import android.database.Cursor;
25import android.database.MatrixCursor;
26import android.graphics.Bitmap;
27import android.graphics.BitmapFactory;
28import android.net.Uri;
29import android.os.Binder;
30import android.os.ParcelFileDescriptor;
31
32import com.android.emailcommon.Logging;
33import com.android.emailcommon.internet.MimeUtility;
34import com.android.emailcommon.provider.EmailContent;
35import com.android.emailcommon.provider.EmailContent.Attachment;
36import com.android.emailcommon.provider.EmailContent.AttachmentColumns;
37import com.android.emailcommon.utility.AttachmentUtilities;
38import com.android.emailcommon.utility.AttachmentUtilities.Columns;
39import com.android.mail.utils.LogUtils;
40import com.android.mail.utils.MatrixCursorWithCachedColumns;
41
42import java.io.File;
43import java.io.FileNotFoundException;
44import java.io.FileOutputStream;
45import java.io.IOException;
46import java.io.InputStream;
47import java.util.List;
48
49/*
50 * A simple ContentProvider that allows file access to Email's attachments.
51 *
52 * The URI scheme is as follows.  For raw file access:
53 *   content://com.android.mail.attachmentprovider/acct#/attach#/RAW
54 *
55 * And for access to thumbnails:
56 *   content://com.android.mail.attachmentprovider/acct#/attach#/THUMBNAIL/width#/height#
57 *
58 * The on-disk (storage) schema is as follows.
59 *
60 * Attachments are stored at:  <database-path>/account#.db_att/item#
61 * Thumbnails are stored at:   <cache-path>/thmb_account#_item#
62 *
63 * Using the standard application context, account #10 and attachment # 20, this would be:
64 *      /data/data/com.android.email/databases/10.db_att/20
65 *      /data/data/com.android.email/cache/thmb_10_20
66 */
67public class AttachmentProvider extends ContentProvider {
68
69    private static final String[] MIME_TYPE_PROJECTION = new String[] {
70            AttachmentColumns.MIME_TYPE, AttachmentColumns.FILENAME };
71    private static final int MIME_TYPE_COLUMN_MIME_TYPE = 0;
72    private static final int MIME_TYPE_COLUMN_FILENAME = 1;
73
74    private static final String[] PROJECTION_QUERY = new String[] { AttachmentColumns.FILENAME,
75            AttachmentColumns.SIZE, AttachmentColumns.CONTENT_URI };
76
77    @Override
78    public boolean onCreate() {
79        /*
80         * We use the cache dir as a temporary directory (since Android doesn't give us one) so
81         * on startup we'll clean up any .tmp files from the last run.
82         */
83
84        final File[] files = getContext().getCacheDir().listFiles();
85        if (files != null) {
86            for (File file : files) {
87                final String filename = file.getName();
88                if (filename.endsWith(".tmp") || filename.startsWith("thmb_")) {
89                    file.delete();
90                }
91            }
92        }
93        return true;
94    }
95
96    /**
97     * Returns the mime type for a given attachment.  There are three possible results:
98     *  - If thumbnail Uri, always returns "image/png" (even if there's no attachment)
99     *  - If the attachment does not exist, returns null
100     *  - Returns the mime type of the attachment
101     */
102    @Override
103    public String getType(Uri uri) {
104        long callingId = Binder.clearCallingIdentity();
105        try {
106            List<String> segments = uri.getPathSegments();
107            String id = segments.get(1);
108            String format = segments.get(2);
109            if (AttachmentUtilities.FORMAT_THUMBNAIL.equals(format)) {
110                return "image/png";
111            } else {
112                uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, Long.parseLong(id));
113                Cursor c = getContext().getContentResolver().query(uri, MIME_TYPE_PROJECTION, null,
114                        null, null);
115                try {
116                    if (c.moveToFirst()) {
117                        String mimeType = c.getString(MIME_TYPE_COLUMN_MIME_TYPE);
118                        String fileName = c.getString(MIME_TYPE_COLUMN_FILENAME);
119                        mimeType = AttachmentUtilities.inferMimeType(fileName, mimeType);
120                        return mimeType;
121                    }
122                } finally {
123                    c.close();
124                }
125                return null;
126            }
127        } finally {
128            Binder.restoreCallingIdentity(callingId);
129        }
130    }
131
132    /**
133     * Open an attachment file.  There are two "formats" - "raw", which returns an actual file,
134     * and "thumbnail", which attempts to generate a thumbnail image.
135     *
136     * Thumbnails are cached for easy space recovery and cleanup.
137     *
138     * TODO:  The thumbnail format returns null for its failure cases, instead of throwing
139     * FileNotFoundException, and should be fixed for consistency.
140     *
141     *  @throws FileNotFoundException
142     */
143    @Override
144    public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
145        // If this is a write, the caller must have the EmailProvider permission, which is
146        // based on signature only
147        if (mode.equals("w")) {
148            Context context = getContext();
149            if (context.checkCallingOrSelfPermission(EmailContent.PROVIDER_PERMISSION)
150                    != PackageManager.PERMISSION_GRANTED) {
151                throw new FileNotFoundException();
152            }
153            List<String> segments = uri.getPathSegments();
154            String accountId = segments.get(0);
155            String id = segments.get(1);
156            File saveIn =
157                AttachmentUtilities.getAttachmentDirectory(context, Long.parseLong(accountId));
158            if (!saveIn.exists()) {
159                saveIn.mkdirs();
160            }
161            File newFile = new File(saveIn, id);
162            return ParcelFileDescriptor.open(
163                    newFile, ParcelFileDescriptor.MODE_READ_WRITE |
164                        ParcelFileDescriptor.MODE_CREATE | ParcelFileDescriptor.MODE_TRUNCATE);
165        }
166        long callingId = Binder.clearCallingIdentity();
167        try {
168            List<String> segments = uri.getPathSegments();
169            final long accountId = Long.parseLong(segments.get(0));
170            final long id = Long.parseLong(segments.get(1));
171            String format = segments.get(2);
172            if (AttachmentUtilities.FORMAT_THUMBNAIL.equals(format)) {
173                int width = Integer.parseInt(segments.get(3));
174                int height = Integer.parseInt(segments.get(4));
175                String filename = "thmb_" + accountId + "_" + id;
176                File dir = getContext().getCacheDir();
177                File file = new File(dir, filename);
178                if (!file.exists()) {
179                    Uri attachmentUri = AttachmentUtilities.getAttachmentUri(accountId, id);
180                    Cursor c = query(attachmentUri,
181                            new String[] { Columns.DATA }, null, null, null);
182                    if (c != null) {
183                        try {
184                            if (c.moveToFirst()) {
185                                attachmentUri = Uri.parse(c.getString(0));
186                            } else {
187                                return null;
188                            }
189                        } finally {
190                            c.close();
191                        }
192                    }
193                    String type = getContext().getContentResolver().getType(attachmentUri);
194                    try {
195                        InputStream in =
196                            getContext().getContentResolver().openInputStream(attachmentUri);
197                        Bitmap thumbnail = createThumbnail(type, in);
198                        if (thumbnail == null) {
199                            return null;
200                        }
201                        thumbnail = Bitmap.createScaledBitmap(thumbnail, width, height, true);
202                        FileOutputStream out = new FileOutputStream(file);
203                        thumbnail.compress(Bitmap.CompressFormat.PNG, 100, out);
204                        out.close();
205                        in.close();
206                    } catch (IOException ioe) {
207                        LogUtils.d(Logging.LOG_TAG, "openFile/thumbnail failed with " +
208                                ioe.getMessage());
209                        return null;
210                    } catch (OutOfMemoryError oome) {
211                        LogUtils.d(Logging.LOG_TAG, "openFile/thumbnail failed with " +
212                                oome.getMessage());
213                        return null;
214                    }
215                }
216                return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
217            }
218            else {
219                return ParcelFileDescriptor.open(
220                        new File(getContext().getDatabasePath(accountId + ".db_att"),
221                                String.valueOf(id)),
222                        ParcelFileDescriptor.MODE_READ_ONLY);
223            }
224        } catch (NumberFormatException e) {
225            LogUtils.e(Logging.LOG_TAG,
226                    "AttachmentProvider.openFile: Failed to open as id is not a long");
227            return null;
228        } finally {
229            Binder.restoreCallingIdentity(callingId);
230        }
231    }
232
233    @Override
234    public int delete(Uri uri, String arg1, String[] arg2) {
235        return 0;
236    }
237
238    @Override
239    public Uri insert(Uri uri, ContentValues values) {
240        return null;
241    }
242
243    /**
244     * Returns a cursor based on the data in the attachments table, or null if the attachment
245     * is not recorded in the table.
246     *
247     * Supports REST Uri only, for a single row - selection, selection args, and sortOrder are
248     * ignored (non-null values should probably throw an exception....)
249     */
250    @Override
251    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
252            String sortOrder) {
253        long callingId = Binder.clearCallingIdentity();
254        try {
255            if (projection == null) {
256                projection =
257                    new String[] {
258                        Columns._ID,
259                        Columns.DATA,
260                };
261            }
262
263            List<String> segments = uri.getPathSegments();
264            String accountId = segments.get(0);
265            String id = segments.get(1);
266            String format = segments.get(2);
267            String name = null;
268            int size = -1;
269            String contentUri = null;
270
271            uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, Long.parseLong(id));
272            Cursor c = getContext().getContentResolver().query(uri, PROJECTION_QUERY,
273                    null, null, null);
274            try {
275                if (c.moveToFirst()) {
276                    name = c.getString(0);
277                    size = c.getInt(1);
278                    contentUri = c.getString(2);
279                } else {
280                    return null;
281                }
282            } finally {
283                c.close();
284            }
285
286            MatrixCursor ret = new MatrixCursorWithCachedColumns(projection);
287            Object[] values = new Object[projection.length];
288            for (int i = 0, count = projection.length; i < count; i++) {
289                String column = projection[i];
290                if (Columns._ID.equals(column)) {
291                    values[i] = id;
292                }
293                else if (Columns.DATA.equals(column)) {
294                    values[i] = contentUri;
295                }
296                else if (Columns.DISPLAY_NAME.equals(column)) {
297                    values[i] = name;
298                }
299                else if (Columns.SIZE.equals(column)) {
300                    values[i] = size;
301                }
302            }
303            ret.addRow(values);
304            return ret;
305        } finally {
306            Binder.restoreCallingIdentity(callingId);
307        }
308    }
309
310    @Override
311    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
312        return 0;
313    }
314
315    private static Bitmap createThumbnail(String type, InputStream data) {
316        if(MimeUtility.mimeTypeMatches(type, "image/*")) {
317            return createImageThumbnail(data);
318        }
319        return null;
320    }
321
322    private static Bitmap createImageThumbnail(InputStream data) {
323        try {
324            Bitmap bitmap = BitmapFactory.decodeStream(data);
325            return bitmap;
326        } catch (OutOfMemoryError oome) {
327            LogUtils.d(Logging.LOG_TAG, "createImageThumbnail failed with " + oome.getMessage());
328            return null;
329        } catch (Exception e) {
330            LogUtils.d(Logging.LOG_TAG, "createImageThumbnail failed with " + e.getMessage());
331            return null;
332        }
333    }
334
335    /**
336     * Need this to suppress warning in unit tests.
337     */
338    @Override
339    public void shutdown() {
340        // Don't call super.shutdown(), which emits a warning...
341    }
342}
343