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            String accountId = segments.get(0);
170            String id = 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.
180                        getAttachmentUri(Long.parseLong(accountId), Long.parseLong(id));
181                    Cursor c = query(attachmentUri,
182                            new String[] { Columns.DATA }, null, null, null);
183                    if (c != null) {
184                        try {
185                            if (c.moveToFirst()) {
186                                attachmentUri = Uri.parse(c.getString(0));
187                            } else {
188                                return null;
189                            }
190                        } finally {
191                            c.close();
192                        }
193                    }
194                    String type = getContext().getContentResolver().getType(attachmentUri);
195                    try {
196                        InputStream in =
197                            getContext().getContentResolver().openInputStream(attachmentUri);
198                        Bitmap thumbnail = createThumbnail(type, in);
199                        if (thumbnail == null) {
200                            return null;
201                        }
202                        thumbnail = Bitmap.createScaledBitmap(thumbnail, width, height, true);
203                        FileOutputStream out = new FileOutputStream(file);
204                        thumbnail.compress(Bitmap.CompressFormat.PNG, 100, out);
205                        out.close();
206                        in.close();
207                    } catch (IOException ioe) {
208                        LogUtils.d(Logging.LOG_TAG, "openFile/thumbnail failed with " +
209                                ioe.getMessage());
210                        return null;
211                    } catch (OutOfMemoryError oome) {
212                        LogUtils.d(Logging.LOG_TAG, "openFile/thumbnail failed with " +
213                                oome.getMessage());
214                        return null;
215                    }
216                }
217                return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
218            }
219            else {
220                return ParcelFileDescriptor.open(
221                        new File(getContext().getDatabasePath(accountId + ".db_att"), id),
222                        ParcelFileDescriptor.MODE_READ_ONLY);
223            }
224        } finally {
225            Binder.restoreCallingIdentity(callingId);
226        }
227    }
228
229    @Override
230    public int delete(Uri uri, String arg1, String[] arg2) {
231        return 0;
232    }
233
234    @Override
235    public Uri insert(Uri uri, ContentValues values) {
236        return null;
237    }
238
239    /**
240     * Returns a cursor based on the data in the attachments table, or null if the attachment
241     * is not recorded in the table.
242     *
243     * Supports REST Uri only, for a single row - selection, selection args, and sortOrder are
244     * ignored (non-null values should probably throw an exception....)
245     */
246    @Override
247    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
248            String sortOrder) {
249        long callingId = Binder.clearCallingIdentity();
250        try {
251            if (projection == null) {
252                projection =
253                    new String[] {
254                        Columns._ID,
255                        Columns.DATA,
256                };
257            }
258
259            List<String> segments = uri.getPathSegments();
260            String accountId = segments.get(0);
261            String id = segments.get(1);
262            String format = segments.get(2);
263            String name = null;
264            int size = -1;
265            String contentUri = null;
266
267            uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, Long.parseLong(id));
268            Cursor c = getContext().getContentResolver().query(uri, PROJECTION_QUERY,
269                    null, null, null);
270            try {
271                if (c.moveToFirst()) {
272                    name = c.getString(0);
273                    size = c.getInt(1);
274                    contentUri = c.getString(2);
275                } else {
276                    return null;
277                }
278            } finally {
279                c.close();
280            }
281
282            MatrixCursor ret = new MatrixCursorWithCachedColumns(projection);
283            Object[] values = new Object[projection.length];
284            for (int i = 0, count = projection.length; i < count; i++) {
285                String column = projection[i];
286                if (Columns._ID.equals(column)) {
287                    values[i] = id;
288                }
289                else if (Columns.DATA.equals(column)) {
290                    values[i] = contentUri;
291                }
292                else if (Columns.DISPLAY_NAME.equals(column)) {
293                    values[i] = name;
294                }
295                else if (Columns.SIZE.equals(column)) {
296                    values[i] = size;
297                }
298            }
299            ret.addRow(values);
300            return ret;
301        } finally {
302            Binder.restoreCallingIdentity(callingId);
303        }
304    }
305
306    @Override
307    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
308        return 0;
309    }
310
311    private static Bitmap createThumbnail(String type, InputStream data) {
312        if(MimeUtility.mimeTypeMatches(type, "image/*")) {
313            return createImageThumbnail(data);
314        }
315        return null;
316    }
317
318    private static Bitmap createImageThumbnail(InputStream data) {
319        try {
320            Bitmap bitmap = BitmapFactory.decodeStream(data);
321            return bitmap;
322        } catch (OutOfMemoryError oome) {
323            LogUtils.d(Logging.LOG_TAG, "createImageThumbnail failed with " + oome.getMessage());
324            return null;
325        } catch (Exception e) {
326            LogUtils.d(Logging.LOG_TAG, "createImageThumbnail failed with " + e.getMessage());
327            return null;
328        }
329    }
330
331    /**
332     * Need this to suppress warning in unit tests.
333     */
334    @Override
335    public void shutdown() {
336        // Don't call super.shutdown(), which emits a warning...
337    }
338}
339