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