AttachmentProvider.java revision 3f1ac4da947f426775c9546f2e37206f58ce1a6e
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.email.mail.internet.MimeUtility;
20import com.android.email.provider.EmailContent.Attachment;
21import com.android.email.provider.EmailContent.AttachmentColumns;
22
23import android.content.ContentProvider;
24import android.content.ContentResolver;
25import android.content.ContentUris;
26import android.content.ContentValues;
27import android.content.Context;
28import android.database.Cursor;
29import android.database.MatrixCursor;
30import android.graphics.Bitmap;
31import android.graphics.BitmapFactory;
32import android.net.Uri;
33import android.os.ParcelFileDescriptor;
34
35import java.io.File;
36import java.io.FileNotFoundException;
37import java.io.FileOutputStream;
38import java.io.IOException;
39import java.io.InputStream;
40import java.util.List;
41
42/*
43 * A simple ContentProvider that allows file access to Email's attachments.
44 *
45 * The URI scheme is as follows.  For raw file access:
46 *   content://com.android.email.attachmentprovider/acct#/attach#/RAW
47 *
48 * And for access to thumbnails:
49 *   content://com.android.email.attachmentprovider/acct#/attach#/THUMBNAIL/width#/height#
50 *
51 * The on-disk (storage) schema is as follows.
52 *
53 * Attachments are stored at:  <database-path>/account#.db_att/item#
54 * Thumbnails are stored at:   <cache-path>/thmb_account#_item#
55 *
56 * Using the standard application context, account #10 and attachment # 20, this would be:
57 *      /data/data/com.android.email/databases/10.db_att/20
58 *      /data/data/com.android.email/cache/thmb_10_20
59 */
60public class AttachmentProvider extends ContentProvider {
61
62    public static final String AUTHORITY = "com.android.email.attachmentprovider";
63    public static final Uri CONTENT_URI = Uri.parse( "content://" + AUTHORITY);
64
65    private static final String FORMAT_RAW = "RAW";
66    private static final String FORMAT_THUMBNAIL = "THUMBNAIL";
67
68    public static class AttachmentProviderColumns {
69        public static final String _ID = "_id";
70        public static final String DATA = "_data";
71        public static final String DISPLAY_NAME = "_display_name";
72        public static final String SIZE = "_size";
73    }
74
75    private String[] PROJECTION_MIME_TYPE = new String[] { AttachmentColumns.MIME_TYPE };
76    private String[] PROJECTION_QUERY = new String[] { AttachmentColumns.FILENAME,
77            AttachmentColumns.SIZE, AttachmentColumns.CONTENT_URI };
78
79    public static Uri getAttachmentUri(long accountId, long id) {
80        return CONTENT_URI.buildUpon()
81                .appendPath(Long.toString(accountId))
82                .appendPath(Long.toString(id))
83                .appendPath(FORMAT_RAW)
84                .build();
85    }
86
87    public static Uri getAttachmentThumbnailUri(long accountId, long id,
88            int width, int height) {
89        return CONTENT_URI.buildUpon()
90                .appendPath(Long.toString(accountId))
91                .appendPath(Long.toString(id))
92                .appendPath(FORMAT_THUMBNAIL)
93                .appendPath(Integer.toString(width))
94                .appendPath(Integer.toString(height))
95                .build();
96    }
97
98    /**
99     * Return the filename for a given attachment.  This should be used by any code that is
100     * going to *write* attachments.
101     *
102     * This does not create or write the file, or even the directories.  It simply builds
103     * the filename that should be used.
104     */
105    public static File getAttachmentFilename(Context context, long accountId, long attachmentId) {
106        return new File(getAttachmentDirectory(context, accountId), Long.toString(attachmentId));
107    }
108
109    /**
110     * Return the directory for a given attachment.  This should be used by any code that is
111     * going to *write* attachments.
112     *
113     * This does not create or write the directory.  It simply builds the pathname that should be
114     * used.
115     */
116    public static File getAttachmentDirectory(Context context, long accountId) {
117        return context.getDatabasePath(accountId + ".db_att");
118    }
119
120    @Override
121    public boolean onCreate() {
122        /*
123         * We use the cache dir as a temporary directory (since Android doesn't give us one) so
124         * on startup we'll clean up any .tmp files from the last run.
125         */
126        File[] files = getContext().getCacheDir().listFiles();
127        for (File file : files) {
128            String filename = file.getName();
129            if (filename.endsWith(".tmp") || filename.startsWith("thmb_")) {
130                file.delete();
131            }
132        }
133        return true;
134    }
135
136    /**
137     * Returns the mime type for a given attachment.  There are three possible results:
138     *  - If thumbnail Uri, always returns "image/png" (even if there's no attachment)
139     *  - If the attachment does not exist, returns null
140     *  - Returns the mime type of the attachment
141     */
142    @Override
143    public String getType(Uri uri) {
144        List<String> segments = uri.getPathSegments();
145        String accountId = segments.get(0);
146        String id = segments.get(1);
147        String format = segments.get(2);
148        if (FORMAT_THUMBNAIL.equals(format)) {
149            return "image/png";
150        } else {
151            uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, Long.parseLong(id));
152            Cursor c = getContext().getContentResolver().query(uri, PROJECTION_MIME_TYPE,
153                    null, null, null);
154            try {
155                if (c.moveToFirst()) {
156                    return c.getString(0);
157                }
158            } finally {
159                c.close();
160            }
161            return null;
162        }
163    }
164
165    /**
166     * Open an attachment file.  There are two "modes" - "raw", which returns an actual file,
167     * and "thumbnail", which attempts to generate a thumbnail image.
168     *
169     * Thumbnails are cached for easy space recovery and cleanup.
170     *
171     * TODO:  The thumbnail mode returns null for its failure cases, instead of throwing
172     * FileNotFoundException, and should be fixed for consistency.
173     *
174     *  @throws FileNotFoundException
175     */
176    @Override
177    public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
178        List<String> segments = uri.getPathSegments();
179        String accountId = segments.get(0);
180        String id = segments.get(1);
181        String format = segments.get(2);
182        if (FORMAT_THUMBNAIL.equals(format)) {
183            int width = Integer.parseInt(segments.get(3));
184            int height = Integer.parseInt(segments.get(4));
185            String filename = "thmb_" + accountId + "_" + id;
186            File dir = getContext().getCacheDir();
187            File file = new File(dir, filename);
188            if (!file.exists()) {
189                Uri attachmentUri = getAttachmentUri(Long.parseLong(accountId), Long.parseLong(id));
190                Cursor c = query(attachmentUri,
191                        new String[] { AttachmentProviderColumns.DATA }, null, null, null);
192                if (c != null) {
193                    try {
194                        if (c.moveToFirst()) {
195                            attachmentUri = Uri.parse(c.getString(0));
196                        } else {
197                            return null;
198                        }
199                    } finally {
200                        c.close();
201                    }
202                }
203                String type = getContext().getContentResolver().getType(attachmentUri);
204                try {
205                    InputStream in =
206                        getContext().getContentResolver().openInputStream(attachmentUri);
207                    Bitmap thumbnail = createThumbnail(type, in);
208                    thumbnail = Bitmap.createScaledBitmap(thumbnail, width, height, true);
209                    FileOutputStream out = new FileOutputStream(file);
210                    thumbnail.compress(Bitmap.CompressFormat.PNG, 100, out);
211                    out.close();
212                    in.close();
213                }
214                catch (IOException ioe) {
215                    return null;
216                }
217            }
218            return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
219        }
220        else {
221            return ParcelFileDescriptor.open(
222                    new File(getContext().getDatabasePath(accountId + ".db_att"), id),
223                    ParcelFileDescriptor.MODE_READ_ONLY);
224        }
225    }
226
227    @Override
228    public int delete(Uri uri, String arg1, String[] arg2) {
229        return 0;
230    }
231
232    @Override
233    public Uri insert(Uri uri, ContentValues values) {
234        return null;
235    }
236
237    /**
238     * Returns a cursor based on the data in the attachments table, or null if the attachment
239     * is not recorded in the table.
240     *
241     * Supports REST Uri only, for a single row - selection, selection args, and sortOrder are
242     * ignored (non-null values should probably throw an exception....)
243     */
244    @Override
245    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
246            String sortOrder) {
247        if (projection == null) {
248            projection =
249                new String[] {
250                    AttachmentProviderColumns._ID,
251                    AttachmentProviderColumns.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 (AttachmentProviderColumns._ID.equals(column)) {
283                values[i] = id;
284            }
285            else if (AttachmentProviderColumns.DATA.equals(column)) {
286                values[i] = contentUri;
287            }
288            else if (AttachmentProviderColumns.DISPLAY_NAME.equals(column)) {
289                values[i] = name;
290            }
291            else if (AttachmentProviderColumns.SIZE.equals(column)) {
292                values[i] = size;
293            }
294        }
295        ret.addRow(values);
296        return ret;
297    }
298
299    @Override
300    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
301        return 0;
302    }
303
304    private Bitmap createThumbnail(String type, InputStream data) {
305        if(MimeUtility.mimeTypeMatches(type, "image/*")) {
306            return createImageThumbnail(data);
307        }
308        return null;
309    }
310
311    private Bitmap createImageThumbnail(InputStream data) {
312        try {
313            Bitmap bitmap = BitmapFactory.decodeStream(data);
314            return bitmap;
315        }
316        catch (OutOfMemoryError oome) {
317            /*
318             * Improperly downloaded images, corrupt bitmaps and the like can commonly
319             * cause OOME due to invalid allocation sizes. We're happy with a null bitmap in
320             * that case. If the system is really out of memory we'll know about it soon
321             * enough.
322             */
323            return null;
324        }
325        catch (Exception e) {
326            return null;
327        }
328    }
329    /**
330     * Resolve attachment id to content URI.  Returns the resolved content URI (from the attachment
331     * DB) or, if not found, simply returns the incoming value.
332     *
333     * @param attachmentUri
334     * @return resolved content URI
335     *
336     * TODO:  Throws an SQLite exception on a missing DB file (e.g. unknown URI) instead of just
337     * returning the incoming uri, as it should.
338     */
339    public static Uri resolveAttachmentIdToContentUri(ContentResolver resolver, Uri attachmentUri) {
340        Cursor c = resolver.query(attachmentUri,
341                new String[] { AttachmentProvider.AttachmentProviderColumns.DATA },
342                null, null, null);
343        if (c != null) {
344            try {
345                if (c.moveToFirst()) {
346                    return Uri.parse(c.getString(0));
347                }
348            } finally {
349                c.close();
350            }
351        }
352        return attachmentUri;
353    }
354}
355