blob: f1c360be83cb9b7667df4cbc62b6316a59eeb082 [file] [log] [blame]
Andrew Sapperstein7434e802013-06-21 11:26:49 -07001/*
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
18package com.android.mail.providers;
19
20import android.app.DownloadManager;
21import android.content.ContentProvider;
James Lemieux934b1f42014-04-09 12:59:29 -070022import android.content.ContentResolver;
Andrew Sapperstein7434e802013-06-21 11:26:49 -070023import android.content.ContentValues;
24import android.content.Context;
25import android.content.Intent;
26import android.content.UriMatcher;
27import android.database.Cursor;
28import android.database.MatrixCursor;
29import android.net.Uri;
30import android.os.Environment;
31import android.os.ParcelFileDescriptor;
32import android.os.SystemClock;
James Lemieux934b1f42014-04-09 12:59:29 -070033import android.text.TextUtils;
Andrew Sapperstein7434e802013-06-21 11:26:49 -070034
35import com.android.ex.photo.provider.PhotoContract;
36import com.android.mail.R;
37import com.android.mail.utils.LogTag;
38import com.android.mail.utils.LogUtils;
Andrew Sapperstein13178962013-06-24 14:10:38 -070039import com.android.mail.utils.MimeType;
Andrew Sapperstein7434e802013-06-21 11:26:49 -070040import com.google.common.collect.Lists;
41import com.google.common.collect.Maps;
42
43import java.io.File;
44import java.io.FileInputStream;
45import java.io.FileNotFoundException;
46import java.io.FileOutputStream;
47import java.io.IOException;
48import java.io.InputStream;
49import java.io.OutputStream;
50import java.util.List;
51import 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 */
63public 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;
James Lemieux934b1f42014-04-09 12:59:29 -070071 private static final int ATTACHMENT_BY_CID = 2;
Andrew Sapperstein7434e802013-06-21 11:26:49 -070072
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
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;
James Lemieux934b1f42014-04-09 12:59:29 -0700104 sUriMatcher.addURI(authority, "attachments/*/*", ATTACHMENT_LIST);
105 sUriMatcher.addURI(authority, "attachment/*/*/#", ATTACHMENT);
106 sUriMatcher.addURI(authority, "attachmentByCid/*/*/*", ATTACHMENT_BY_CID);
Andrew Sapperstein7434e802013-06-21 11:26:49 -0700107 }
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
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);
James Lemieux934b1f42014-04-09 12:59:29 -0700123 final ContentResolver cr = getContext().getContentResolver();
Andrew Sapperstein7434e802013-06-21 11:26:49 -0700124
125 switch (match) {
James Lemieux934b1f42014-04-09 12:59:29 -0700126 case ATTACHMENT_LIST: {
Andrew Sapperstein7434e802013-06-21 11:26:49 -0700127 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 }
James Lemieux934b1f42014-04-09 12:59:29 -0700134 cursor.setNotificationUri(cr, uri);
Andrew Sapperstein7434e802013-06-21 11:26:49 -0700135 break;
James Lemieux934b1f42014-04-09 12:59:29 -0700136 }
137 case ATTACHMENT: {
Andrew Sapperstein7434e802013-06-21 11:26:49 -0700138 addRow(cursor, mUriAttachmentMap.get(uri));
James Lemieux934b1f42014-04-09 12:59:29 -0700139 cursor.setNotificationUri(cr, getListUriFromAttachmentUri(uri));
Andrew Sapperstein7434e802013-06-21 11:26:49 -0700140 break;
James Lemieux934b1f42014-04-09 12:59:29 -0700141 }
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 }
Andrew Sapperstein7434e802013-06-21 11:26:49 -0700163 default:
164 break;
165 }
166
167 return cursor;
168 }
169
170 @Override
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
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
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
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 */
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 */
Scott Kennedy3b965d72013-06-25 14:36:55 -0700259 private static void addRow(MatrixCursor cursor, Attachment attachment) {
Andrew Sapperstein7434e802013-06-21 11:26:49 -0700260 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
Andrew Sappersteine3077852013-12-11 18:04:25 -0800272 .add(attachment.supportsDownloadAgain() ? 1 : 0) // supportsDownloadAgain
273 .add(attachment.type) // type
James Lemieux934b1f42014-04-09 12:59:29 -0700274 .add(attachment.flags) // flags
275 .add(attachment.partId); // partId (same as RFC822 cid)
Andrew Sapperstein7434e802013-06-21 11:26:49 -0700276 }
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 */
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
Andrew Sapperstein13178962013-06-24 14:10:38 -0700349 // 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
Andrew Sapperstein7434e802013-06-21 11:26:49 -0700354 // 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
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 /**
James Lemieux934b1f42014-04-09 12:59:29 -0700420 * Returns an attachment list uri for the specific attachment uri passed.
Andrew Sapperstein7434e802013-06-21 11:26:49 -0700421 */
James Lemieux934b1f42014-04-09 12:59:29 -0700422 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();
Andrew Sapperstein7434e802013-06-21 11:26:49 -0700429 }
430
431 /**
James Lemieux934b1f42014-04-09 12:59:29 -0700432 * Returns an attachment list uri for an eml file at the given uri with the given message id.
Andrew Sapperstein7434e802013-06-21 11:26:49 -0700433 */
James Lemieux934b1f42014-04-09 12:59:29 -0700434 public static Uri getAttachmentsListUri(Uri emlFileUri, String messageId) {
Andrew Sapperstein7434e802013-06-21 11:26:49 -0700435 return BASE_URI.buildUpon()
James Lemieux934b1f42014-04-09 12:59:29 -0700436 .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 */
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();
Andrew Sapperstein7434e802013-06-21 11:26:49 -0700452 }
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 */
458 public static Uri getAttachmentUri(Uri emlFileUri, String messageId, String partId) {
James Lemieux934b1f42014-04-09 12:59:29 -0700459 return BASE_URI.buildUpon()
460 .appendPath("attachment")
461 .appendPath(Integer.toString(emlFileUri.hashCode()))
462 .appendPath(messageId)
463 .appendPath(partId)
464 .build();
Andrew Sapperstein7434e802013-06-21 11:26:49 -0700465 }
466
467 /**
468 * Returns the absolute file path for the attachment at the given uri.
469 */
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 */
492 private String getCacheFileDirectory(Uri uri) {
James Lemieux934b1f42014-04-09 12:59:29 -0700493 return getCacheDir() + "/" + Uri.encode(uri.getPathSegments().get(1));
Andrew Sapperstein7434e802013-06-21 11:26:49 -0700494 }
495
496 /**
497 * Returns the cache directory for eml attachment files.
498 */
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 */
506 private void deleteDirectory(String cacheFileDirectory) {
507 recursiveDelete(new File(cacheFileDirectory));
508 }
509
510 /**
511 * Recursively deletes a file or directory.
512 */
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}