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