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