1 /*
2  * Copyright (C) 2013 Google Inc.
3  * Licensed to The Android Open Source Project.
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 
18 package com.android.mail.providers;
19 
20 import android.app.DownloadManager;
21 import android.content.ContentProvider;
22 import android.content.ContentResolver;
23 import android.content.ContentValues;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.UriMatcher;
27 import android.database.Cursor;
28 import android.database.MatrixCursor;
29 import android.net.Uri;
30 import android.os.Environment;
31 import android.os.ParcelFileDescriptor;
32 import android.os.SystemClock;
33 import android.text.TextUtils;
34 
35 import com.android.ex.photo.provider.PhotoContract;
36 import com.android.mail.R;
37 import com.android.mail.utils.LogTag;
38 import com.android.mail.utils.LogUtils;
39 import com.android.mail.utils.MimeType;
40 import com.google.common.collect.Lists;
41 import com.google.common.collect.Maps;
42 
43 import java.io.File;
44 import java.io.FileInputStream;
45 import java.io.FileNotFoundException;
46 import java.io.FileOutputStream;
47 import java.io.IOException;
48 import java.io.InputStream;
49 import java.io.OutputStream;
50 import java.util.List;
51 import java.util.Map;
52 
53 /**
54  * A {@link ContentProvider} for attachments created from eml files.
55  * Supports all of the semantics (query/insert/update/delete/openFile)
56  * of the regular attachment provider.
57  *
58  * One major difference is that all attachment info is stored in memory (with the
59  * exception of the attachment raw data which is stored in the cache). When
60  * the process is killed, all of the attachments disappear if they still
61  * exist.
62  */
63 public class EmlAttachmentProvider extends ContentProvider {
64     private static final String LOG_TAG = LogTag.getLogTag();
65 
66     private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
67     private static boolean sUrisAddedToMatcher = false;
68 
69     private static final int ATTACHMENT_LIST = 0;
70     private static final int ATTACHMENT = 1;
71     private static final int ATTACHMENT_BY_CID = 2;
72 
73     /**
74      * The buffer size used to copy data from cache to sd card.
75      */
76     private static final int BUFFER_SIZE = 4096;
77 
78     /** Any IO reads should be limited to this timeout */
79     private static final long READ_TIMEOUT = 3600 * 1000;
80 
81     private static Uri BASE_URI;
82 
83     private DownloadManager mDownloadManager;
84 
85     /**
86      * Map that contains a mapping from an attachment list uri to a list of uris.
87      */
88     private Map<Uri, List<Uri>> mUriListMap;
89 
90     /**
91      * Map that contains a mapping from an attachment uri to an {@link Attachment} object.
92      */
93     private Map<Uri, Attachment> mUriAttachmentMap;
94 
95 
96     @Override
onCreate()97     public boolean onCreate() {
98         final String authority =
99                 getContext().getResources().getString(R.string.eml_attachment_provider);
100         BASE_URI = new Uri.Builder().scheme("content").authority(authority).build();
101 
102         if (!sUrisAddedToMatcher) {
103             sUrisAddedToMatcher = true;
104             sUriMatcher.addURI(authority, "attachments/*/*", ATTACHMENT_LIST);
105             sUriMatcher.addURI(authority, "attachment/*/*/#", ATTACHMENT);
106             sUriMatcher.addURI(authority, "attachmentByCid/*/*/*", ATTACHMENT_BY_CID);
107         }
108 
109         mDownloadManager =
110                 (DownloadManager) getContext().getSystemService(Context.DOWNLOAD_SERVICE);
111 
112         mUriListMap = Maps.newHashMap();
113         mUriAttachmentMap = Maps.newHashMap();
114         return true;
115     }
116 
117     @Override
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)118     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
119             String sortOrder) {
120         final int match = sUriMatcher.match(uri);
121         // ignore other projections
122         final MatrixCursor cursor = new MatrixCursor(UIProvider.ATTACHMENT_PROJECTION);
123         final ContentResolver cr = getContext().getContentResolver();
124 
125         switch (match) {
126             case ATTACHMENT_LIST: {
127                 final List<String> contentTypeQueryParameters =
128                         uri.getQueryParameters(PhotoContract.ContentTypeParameters.CONTENT_TYPE);
129                 uri = uri.buildUpon().clearQuery().build();
130                 final List<Uri> attachmentUris = mUriListMap.get(uri);
131                 for (final Uri attachmentUri : attachmentUris) {
132                     addRow(cursor, attachmentUri, contentTypeQueryParameters);
133                 }
134                 cursor.setNotificationUri(cr, uri);
135                 break;
136             }
137             case ATTACHMENT: {
138                 addRow(cursor, mUriAttachmentMap.get(uri));
139                 cursor.setNotificationUri(cr, getListUriFromAttachmentUri(uri));
140                 break;
141             }
142             case ATTACHMENT_BY_CID: {
143                 // form the attachment lists uri by clipping off the cid from the given uri
144                 final Uri attachmentsListUri = getListUriFromAttachmentUri(uri);
145                 final String cid = uri.getPathSegments().get(3);
146 
147                 // find all uris for the parent message
148                 final List<Uri> attachmentUris = mUriListMap.get(attachmentsListUri);
149 
150                 if (attachmentUris != null) {
151                     // find the attachment that contains the given cid
152                     for (Uri attachmentsUri : attachmentUris) {
153                         final Attachment attachment = mUriAttachmentMap.get(attachmentsUri);
154                         if (TextUtils.equals(cid, attachment.partId)) {
155                             addRow(cursor, attachment);
156                             cursor.setNotificationUri(cr, attachmentsListUri);
157                             break;
158                         }
159                     }
160                 }
161                 break;
162             }
163             default:
164                 break;
165         }
166 
167         return cursor;
168     }
169 
170     @Override
getType(Uri uri)171     public String getType(Uri uri) {
172         final int match = sUriMatcher.match(uri);
173         switch (match) {
174             case ATTACHMENT:
175                 return mUriAttachmentMap.get(uri).getContentType();
176             default:
177                 return null;
178         }
179     }
180 
181     @Override
insert(Uri uri, ContentValues values)182     public Uri insert(Uri uri, ContentValues values) {
183         final Uri listUri = getListUriFromAttachmentUri(uri);
184 
185         // add mapping from uri to attachment
186         if (mUriAttachmentMap.put(uri, new Attachment(values)) == null) {
187             // only add uri to list if the list
188             // get list of attachment uris, creating if necessary
189             List<Uri> list = mUriListMap.get(listUri);
190             if (list == null) {
191                 list = Lists.newArrayList();
192                 mUriListMap.put(listUri, list);
193             }
194 
195             list.add(uri);
196         }
197 
198         return uri;
199     }
200 
201     @Override
delete(Uri uri, String selection, String[] selectionArgs)202     public int delete(Uri uri, String selection, String[] selectionArgs) {
203         final int match = sUriMatcher.match(uri);
204         switch (match) {
205             case ATTACHMENT_LIST:
206                 // remove from list mapping
207                 final List<Uri> attachmentUris = mUriListMap.remove(uri);
208 
209                 // delete each file and remove each element from the mapping
210                 for (final Uri attachmentUri : attachmentUris) {
211                     mUriAttachmentMap.remove(attachmentUri);
212                 }
213 
214                 deleteDirectory(getCacheFileDirectory(uri));
215                 // return rows affected
216                 return attachmentUris.size();
217             default:
218                 return 0;
219         }
220     }
221 
222     @Override
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)223     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
224         final int match = sUriMatcher.match(uri);
225         switch (match) {
226             case ATTACHMENT:
227                 return copyAttachment(uri, values);
228             default:
229                 return 0;
230         }
231     }
232 
233     /**
234      * Adds a row to the cursor for the attachment at the specific attachment {@link Uri}
235      * if the attachment's mime type matches one of the query parameters.
236      *
237      * Matching is defined to be starting with one of the query parameters. If no
238      * parameters exist, all rows are added.
239      */
addRow(MatrixCursor cursor, Uri uri, List<String> contentTypeQueryParameters)240     private void addRow(MatrixCursor cursor, Uri uri,
241             List<String> contentTypeQueryParameters) {
242         final Attachment attachment = mUriAttachmentMap.get(uri);
243 
244         if (contentTypeQueryParameters != null && !contentTypeQueryParameters.isEmpty()) {
245             for (final String type : contentTypeQueryParameters) {
246                 if (attachment.getContentType().startsWith(type)) {
247                     addRow(cursor, attachment);
248                     return;
249                 }
250             }
251         } else {
252             addRow(cursor, attachment);
253         }
254     }
255 
256     /**
257      * Adds a new row to the cursor for the specific attachment.
258      */
addRow(MatrixCursor cursor, Attachment attachment)259     private static void addRow(MatrixCursor cursor, Attachment attachment) {
260         cursor.newRow()
261                 .add(attachment.getName())                          // displayName
262                 .add(attachment.size)                               // size
263                 .add(attachment.uri)                                // uri
264                 .add(attachment.getContentType())                   // contentType
265                 .add(attachment.state)                              // state
266                 .add(attachment.destination)                        // destination
267                 .add(attachment.downloadedSize)                     // downloadedSize
268                 .add(attachment.contentUri)                         // contentUri
269                 .add(attachment.thumbnailUri)                       // thumbnailUri
270                 .add(attachment.previewIntentUri)                   // previewIntentUri
271                 .add(attachment.providerData)                       // providerData
272                 .add(attachment.supportsDownloadAgain() ? 1 : 0)    // supportsDownloadAgain
273                 .add(attachment.type)                               // type
274                 .add(attachment.flags)                              // flags
275                 .add(attachment.partId);                            // partId (same as RFC822 cid)
276     }
277 
278     /**
279      * Copies an attachment at the specified {@link Uri}
280      * from cache to the external downloads directory (usually the sd card).
281      * @return the number of attachments affected. Should be 1 or 0.
282      */
copyAttachment(Uri uri, ContentValues values)283     private int copyAttachment(Uri uri, ContentValues values) {
284         final Integer newState = values.getAsInteger(UIProvider.AttachmentColumns.STATE);
285         final Integer newDestination =
286                 values.getAsInteger(UIProvider.AttachmentColumns.DESTINATION);
287         if (newState == null && newDestination == null) {
288             return 0;
289         }
290 
291         final int destination = newDestination != null ?
292                 newDestination.intValue() : UIProvider.AttachmentDestination.CACHE;
293         final boolean saveToSd =
294                 destination == UIProvider.AttachmentDestination.EXTERNAL;
295 
296         final Attachment attachment = mUriAttachmentMap.get(uri);
297 
298         // 1. check if already saved to sd (via uri save to sd)
299         // and return if so (we shouldn't ever be here)
300 
301         // if the call was not to save to sd or already saved to sd, just bail out
302         if (!saveToSd || attachment.isSavedToExternal()) {
303             return 0;
304         }
305 
306 
307         // 2. copy file
308         final String oldFilePath = getFilePath(uri);
309 
310         // update the destination before getting the new file path
311         // otherwise it will just point to the old location.
312         attachment.destination = UIProvider.AttachmentDestination.EXTERNAL;
313         final String newFilePath = getFilePath(uri);
314 
315         InputStream inputStream = null;
316         OutputStream outputStream = null;
317 
318         try {
319             try {
320                 inputStream = new FileInputStream(oldFilePath);
321             } catch (FileNotFoundException e) {
322                 LogUtils.e(LOG_TAG, "File not found for file %s", oldFilePath);
323                 return 0;
324             }
325             try {
326                 outputStream = new FileOutputStream(newFilePath);
327             } catch (FileNotFoundException e) {
328                 LogUtils.e(LOG_TAG, "File not found for file %s", newFilePath);
329                 return 0;
330             }
331             try {
332                 final long now = SystemClock.elapsedRealtime();
333                 final byte data[] = new byte[BUFFER_SIZE];
334                 int size = 0;
335                 while (true) {
336                     final int len = inputStream.read(data);
337                     if (len != -1) {
338                         outputStream.write(data, 0, len);
339 
340                         size += len;
341                     } else {
342                         break;
343                     }
344                     if (SystemClock.elapsedRealtime() - now > READ_TIMEOUT) {
345                         throw new IOException("Timed out copying attachment.");
346                     }
347                 }
348 
349                 // if the attachment is an APK, change contentUri to be a direct file uri
350                 if (MimeType.isInstallable(attachment.getContentType())) {
351                     attachment.contentUri = Uri.parse("file://" + newFilePath);
352                 }
353 
354                 // 3. add file to download manager
355 
356                 try {
357                     // TODO - make a better description
358                     final String description = attachment.getName();
359                     mDownloadManager.addCompletedDownload(attachment.getName(),
360                             description, true, attachment.getContentType(),
361                             newFilePath, size, false);
362                 }
363                 catch (IllegalArgumentException e) {
364                     // Even if we cannot save the download to the downloads app,
365                     // (likely due to a bad mimeType), we still want to save it.
366                     LogUtils.e(LOG_TAG, e, "Failed to save download to Downloads app.");
367                 }
368                 final Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
369                 intent.setData(Uri.parse("file://" + newFilePath));
370                 getContext().sendBroadcast(intent);
371 
372                 // 4. delete old file
373                 new File(oldFilePath).delete();
374             } catch (IOException e) {
375                 // Error writing file, delete partial file
376                 LogUtils.e(LOG_TAG, e, "Cannot write to file %s", newFilePath);
377                 new File(newFilePath).delete();
378             }
379         } finally {
380             try {
381                 if (inputStream != null) {
382                     inputStream.close();
383                 }
384             } catch (IOException e) {
385             }
386             try {
387                 if (outputStream != null) {
388                     outputStream.close();
389                 }
390             } catch (IOException e) {
391             }
392         }
393 
394         // 5. notify that the list of attachments has changed so the UI will update
395         getContext().getContentResolver().notifyChange(
396                 getListUriFromAttachmentUri(uri), null, false);
397         return 1;
398     }
399 
400     @Override
openFile(Uri uri, String mode)401     public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
402         final String filePath = getFilePath(uri);
403 
404         final int fileMode;
405 
406         if ("rwt".equals(mode)) {
407             fileMode = ParcelFileDescriptor.MODE_READ_WRITE |
408                     ParcelFileDescriptor.MODE_TRUNCATE |
409                     ParcelFileDescriptor.MODE_CREATE;
410         } else if ("rw".equals(mode)) {
411             fileMode = ParcelFileDescriptor.MODE_READ_WRITE | ParcelFileDescriptor.MODE_CREATE;
412         } else {
413             fileMode = ParcelFileDescriptor.MODE_READ_ONLY;
414         }
415 
416         return ParcelFileDescriptor.open(new File(filePath), fileMode);
417     }
418 
419     /**
420      * Returns an attachment list uri for the specific attachment uri passed.
421      */
getListUriFromAttachmentUri(Uri uri)422     private static Uri getListUriFromAttachmentUri(Uri uri) {
423         final List<String> segments = uri.getPathSegments();
424         return BASE_URI.buildUpon()
425                 .appendPath("attachments")
426                 .appendPath(segments.get(1))
427                 .appendPath(segments.get(2))
428                 .build();
429     }
430 
431     /**
432      * Returns an attachment list uri for an eml file at the given uri with the given message id.
433      */
getAttachmentsListUri(Uri emlFileUri, String messageId)434     public static Uri getAttachmentsListUri(Uri emlFileUri, String messageId) {
435         return BASE_URI.buildUpon()
436                 .appendPath("attachments")
437                 .appendPath(Integer.toString(emlFileUri.hashCode()))
438                 .appendPath(messageId)
439                 .build();
440     }
441 
442     /**
443      * Returns an attachment uri for an eml file at the given uri with the given message id.
444      * The consumer of this uri must append a specific CID to it to complete the uri.
445      */
getAttachmentByCidUri(Uri emlFileUri, String messageId)446     public static Uri getAttachmentByCidUri(Uri emlFileUri, String messageId) {
447         return BASE_URI.buildUpon()
448                 .appendPath("attachmentByCid")
449                 .appendPath(Integer.toString(emlFileUri.hashCode()))
450                 .appendPath(messageId)
451                 .build();
452     }
453 
454     /**
455      * Returns an attachment uri for an attachment from the given eml file uri with
456      * the given message id and part id.
457      */
getAttachmentUri(Uri emlFileUri, String messageId, String partId)458     public static Uri getAttachmentUri(Uri emlFileUri, String messageId, String partId) {
459         return BASE_URI.buildUpon()
460                 .appendPath("attachment")
461                 .appendPath(Integer.toString(emlFileUri.hashCode()))
462                 .appendPath(messageId)
463                 .appendPath(partId)
464                 .build();
465     }
466 
467     /**
468      * Returns the absolute file path for the attachment at the given uri.
469      */
getFilePath(Uri uri)470     private String getFilePath(Uri uri) {
471         final Attachment attachment = mUriAttachmentMap.get(uri);
472         final boolean saveToSd =
473                 attachment.destination == UIProvider.AttachmentDestination.EXTERNAL;
474         final String pathStart = (saveToSd) ?
475                 Environment.getExternalStoragePublicDirectory(
476                 Environment.DIRECTORY_DOWNLOADS).getAbsolutePath() : getCacheDir();
477 
478         // we want the root of the downloads directory if the attachment is
479         // saved to external (or we're saving to external)
480         final String directoryPath = (saveToSd) ? pathStart : pathStart + uri.getEncodedPath();
481 
482         final File directory = new File(directoryPath);
483         if (!directory.exists()) {
484             directory.mkdirs();
485         }
486         return directoryPath + "/" + attachment.getName();
487     }
488 
489     /**
490      * Returns the root directory for the attachments for the specific uri.
491      */
getCacheFileDirectory(Uri uri)492     private String getCacheFileDirectory(Uri uri) {
493         return getCacheDir() + "/" + Uri.encode(uri.getPathSegments().get(1));
494     }
495 
496     /**
497      * Returns the cache directory for eml attachment files.
498      */
getCacheDir()499     private String getCacheDir() {
500         return getContext().getCacheDir().getAbsolutePath().concat("/eml");
501     }
502 
503     /**
504      * Recursively delete the directory at the passed file path.
505      */
deleteDirectory(String cacheFileDirectory)506     private void deleteDirectory(String cacheFileDirectory) {
507         recursiveDelete(new File(cacheFileDirectory));
508     }
509 
510     /**
511      * Recursively deletes a file or directory.
512      */
recursiveDelete(File file)513     private void recursiveDelete(File file) {
514         if (file.isDirectory()) {
515             final File[] children = file.listFiles();
516             for (final File child : children) {
517                 recursiveDelete(child);
518             }
519         }
520 
521         file.delete();
522     }
523 }
524