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