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