Home | History | Annotate | Download | only in providers
      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