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 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 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 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 */ 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 */ 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 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 */ 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 */ 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 */ 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 */ 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 */ 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) { 493 return getCacheDir() + "/" + Uri.encode(uri.getPathSegments().get(1)); 494 } 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 } 524