blob: a857a69c34cfebfc3d375ca15181206748674e3a [file] [log] [blame]
/*
* Copyright (C) 2013 Google Inc.
* Licensed to The Android Open Source Project.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.mail.providers;
import android.app.DownloadManager;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.os.SystemClock;
import com.android.ex.photo.provider.PhotoContract;
import com.android.mail.R;
import com.android.mail.utils.LogTag;
import com.android.mail.utils.LogUtils;
import com.android.mail.utils.MimeType;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
import java.util.Map;
/**
* A {@link ContentProvider} for attachments created from eml files.
* Supports all of the semantics (query/insert/update/delete/openFile)
* of the regular attachment provider.
*
* One major difference is that all attachment info is stored in memory (with the
* exception of the attachment raw data which is stored in the cache). When
* the process is killed, all of the attachments disappear if they still
* exist.
*/
public class EmlAttachmentProvider extends ContentProvider {
private static final String LOG_TAG = LogTag.getLogTag();
private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
private static boolean sUrisAddedToMatcher = false;
private static final int ATTACHMENT_LIST = 0;
private static final int ATTACHMENT = 1;
/**
* The buffer size used to copy data from cache to sd card.
*/
private static final int BUFFER_SIZE = 4096;
/** Any IO reads should be limited to this timeout */
private static final long READ_TIMEOUT = 3600 * 1000;
private static Uri BASE_URI;
private DownloadManager mDownloadManager;
/**
* Map that contains a mapping from an attachment list uri to a list of uris.
*/
private Map<Uri, List<Uri>> mUriListMap;
/**
* Map that contains a mapping from an attachment uri to an {@link Attachment} object.
*/
private Map<Uri, Attachment> mUriAttachmentMap;
@Override
public boolean onCreate() {
final String authority =
getContext().getResources().getString(R.string.eml_attachment_provider);
BASE_URI = new Uri.Builder().scheme("content").authority(authority).build();
if (!sUrisAddedToMatcher) {
sUrisAddedToMatcher = true;
sUriMatcher.addURI(authority, "*/*", ATTACHMENT_LIST);
sUriMatcher.addURI(authority, "*/*/#", ATTACHMENT);
}
mDownloadManager =
(DownloadManager) getContext().getSystemService(Context.DOWNLOAD_SERVICE);
mUriListMap = Maps.newHashMap();
mUriAttachmentMap = Maps.newHashMap();
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
final int match = sUriMatcher.match(uri);
// ignore other projections
final MatrixCursor cursor = new MatrixCursor(UIProvider.ATTACHMENT_PROJECTION);
switch (match) {
case ATTACHMENT_LIST:
final List<String> contentTypeQueryParameters =
uri.getQueryParameters(PhotoContract.ContentTypeParameters.CONTENT_TYPE);
uri = uri.buildUpon().clearQuery().build();
final List<Uri> attachmentUris = mUriListMap.get(uri);
for (final Uri attachmentUri : attachmentUris) {
addRow(cursor, attachmentUri, contentTypeQueryParameters);
}
cursor.setNotificationUri(getContext().getContentResolver(), uri);
break;
case ATTACHMENT:
addRow(cursor, mUriAttachmentMap.get(uri));
cursor.setNotificationUri(
getContext().getContentResolver(), getListUriFromAttachmentUri(uri));
break;
default:
break;
}
return cursor;
}
@Override
public String getType(Uri uri) {
final int match = sUriMatcher.match(uri);
switch (match) {
case ATTACHMENT:
return mUriAttachmentMap.get(uri).getContentType();
default:
return null;
}
}
@Override
public Uri insert(Uri uri, ContentValues values) {
final Uri listUri = getListUriFromAttachmentUri(uri);
// add mapping from uri to attachment
if (mUriAttachmentMap.put(uri, new Attachment(values)) == null) {
// only add uri to list if the list
// get list of attachment uris, creating if necessary
List<Uri> list = mUriListMap.get(listUri);
if (list == null) {
list = Lists.newArrayList();
mUriListMap.put(listUri, list);
}
list.add(uri);
}
return uri;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
final int match = sUriMatcher.match(uri);
switch (match) {
case ATTACHMENT_LIST:
// remove from list mapping
final List<Uri> attachmentUris = mUriListMap.remove(uri);
// delete each file and remove each element from the mapping
for (final Uri attachmentUri : attachmentUris) {
mUriAttachmentMap.remove(attachmentUri);
}
deleteDirectory(getCacheFileDirectory(uri));
// return rows affected
return attachmentUris.size();
default:
return 0;
}
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
final int match = sUriMatcher.match(uri);
switch (match) {
case ATTACHMENT:
return copyAttachment(uri, values);
default:
return 0;
}
}
/**
* Adds a row to the cursor for the attachment at the specific attachment {@link Uri}
* if the attachment's mime type matches one of the query parameters.
*
* Matching is defined to be starting with one of the query parameters. If no
* parameters exist, all rows are added.
*/
private void addRow(MatrixCursor cursor, Uri uri,
List<String> contentTypeQueryParameters) {
final Attachment attachment = mUriAttachmentMap.get(uri);
if (contentTypeQueryParameters != null && !contentTypeQueryParameters.isEmpty()) {
for (final String type : contentTypeQueryParameters) {
if (attachment.getContentType().startsWith(type)) {
addRow(cursor, attachment);
return;
}
}
} else {
addRow(cursor, attachment);
}
}
/**
* Adds a new row to the cursor for the specific attachment.
*/
private static void addRow(MatrixCursor cursor, Attachment attachment) {
cursor.newRow()
.add(attachment.getName()) // displayName
.add(attachment.size) // size
.add(attachment.uri) // uri
.add(attachment.getContentType()) // contentType
.add(attachment.state) // state
.add(attachment.destination) // destination
.add(attachment.downloadedSize) // downloadedSize
.add(attachment.contentUri) // contentUri
.add(attachment.thumbnailUri) // thumbnailUri
.add(attachment.previewIntentUri) // previewIntentUri
.add(attachment.providerData) // providerData
.add(attachment.supportsDownloadAgain() ? 1 : 0); // supportsDownloadAgain
}
/**
* Copies an attachment at the specified {@link Uri}
* from cache to the external downloads directory (usually the sd card).
* @return the number of attachments affected. Should be 1 or 0.
*/
private int copyAttachment(Uri uri, ContentValues values) {
final Integer newState = values.getAsInteger(UIProvider.AttachmentColumns.STATE);
final Integer newDestination =
values.getAsInteger(UIProvider.AttachmentColumns.DESTINATION);
if (newState == null && newDestination == null) {
return 0;
}
final int destination = newDestination != null ?
newDestination.intValue() : UIProvider.AttachmentDestination.CACHE;
final boolean saveToSd =
destination == UIProvider.AttachmentDestination.EXTERNAL;
final Attachment attachment = mUriAttachmentMap.get(uri);
// 1. check if already saved to sd (via uri save to sd)
// and return if so (we shouldn't ever be here)
// if the call was not to save to sd or already saved to sd, just bail out
if (!saveToSd || attachment.isSavedToExternal()) {
return 0;
}
// 2. copy file
final String oldFilePath = getFilePath(uri);
// update the destination before getting the new file path
// otherwise it will just point to the old location.
attachment.destination = UIProvider.AttachmentDestination.EXTERNAL;
final String newFilePath = getFilePath(uri);
InputStream inputStream = null;
OutputStream outputStream = null;
try {
try {
inputStream = new FileInputStream(oldFilePath);
} catch (FileNotFoundException e) {
LogUtils.e(LOG_TAG, "File not found for file %s", oldFilePath);
return 0;
}
try {
outputStream = new FileOutputStream(newFilePath);
} catch (FileNotFoundException e) {
LogUtils.e(LOG_TAG, "File not found for file %s", newFilePath);
return 0;
}
try {
final long now = SystemClock.elapsedRealtime();
final byte data[] = new byte[BUFFER_SIZE];
int size = 0;
while (true) {
final int len = inputStream.read(data);
if (len != -1) {
outputStream.write(data, 0, len);
size += len;
} else {
break;
}
if (SystemClock.elapsedRealtime() - now > READ_TIMEOUT) {
throw new IOException("Timed out copying attachment.");
}
}
// if the attachment is an APK, change contentUri to be a direct file uri
if (MimeType.isInstallable(attachment.getContentType())) {
attachment.contentUri = Uri.parse("file://" + newFilePath);
}
// 3. add file to download manager
try {
// TODO - make a better description
final String description = attachment.getName();
mDownloadManager.addCompletedDownload(attachment.getName(),
description, true, attachment.getContentType(),
newFilePath, size, false);
}
catch (IllegalArgumentException e) {
// Even if we cannot save the download to the downloads app,
// (likely due to a bad mimeType), we still want to save it.
LogUtils.e(LOG_TAG, e, "Failed to save download to Downloads app.");
}
final Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
intent.setData(Uri.parse("file://" + newFilePath));
getContext().sendBroadcast(intent);
// 4. delete old file
new File(oldFilePath).delete();
} catch (IOException e) {
// Error writing file, delete partial file
LogUtils.e(LOG_TAG, e, "Cannot write to file %s", newFilePath);
new File(newFilePath).delete();
}
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
} catch (IOException e) {
}
try {
if (outputStream != null) {
outputStream.close();
}
} catch (IOException e) {
}
}
// 5. notify that the list of attachments has changed so the UI will update
getContext().getContentResolver().notifyChange(
getListUriFromAttachmentUri(uri), null, false);
return 1;
}
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
final String filePath = getFilePath(uri);
final int fileMode;
if ("rwt".equals(mode)) {
fileMode = ParcelFileDescriptor.MODE_READ_WRITE |
ParcelFileDescriptor.MODE_TRUNCATE |
ParcelFileDescriptor.MODE_CREATE;
} else if ("rw".equals(mode)) {
fileMode = ParcelFileDescriptor.MODE_READ_WRITE | ParcelFileDescriptor.MODE_CREATE;
} else {
fileMode = ParcelFileDescriptor.MODE_READ_ONLY;
}
return ParcelFileDescriptor.open(new File(filePath), fileMode);
}
/**
* Returns an attachment list uri for an eml file at the given uri
* with the given message id.
*/
public static Uri getAttachmentsListUri(Uri emlFileUri, String messageId) {
return BASE_URI.buildUpon().appendPath(Integer.toString(emlFileUri.hashCode()))
.appendPath(messageId).build();
}
/**
* Returns an attachment list uri for the specific attachment uri passed.
*/
public static Uri getListUriFromAttachmentUri(Uri uri) {
final List<String> segments = uri.getPathSegments();
return BASE_URI.buildUpon()
.appendPath(segments.get(0)).appendPath(segments.get(1)).build();
}
/**
* Returns an attachment uri for an attachment from the given eml file uri with
* the given message id and part id.
*/
public static Uri getAttachmentUri(Uri emlFileUri, String messageId, String partId) {
return BASE_URI.buildUpon().appendPath(Integer.toString(emlFileUri.hashCode()))
.appendPath(messageId).appendPath(partId).build();
}
/**
* Returns the absolute file path for the attachment at the given uri.
*/
private String getFilePath(Uri uri) {
final Attachment attachment = mUriAttachmentMap.get(uri);
final boolean saveToSd =
attachment.destination == UIProvider.AttachmentDestination.EXTERNAL;
final String pathStart = (saveToSd) ?
Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOWNLOADS).getAbsolutePath() : getCacheDir();
// we want the root of the downloads directory if the attachment is
// saved to external (or we're saving to external)
final String directoryPath = (saveToSd) ? pathStart : pathStart + uri.getEncodedPath();
final File directory = new File(directoryPath);
if (!directory.exists()) {
directory.mkdirs();
}
return directoryPath + "/" + attachment.getName();
}
/**
* Returns the root directory for the attachments for the specific uri.
*/
private String getCacheFileDirectory(Uri uri) {
return getCacheDir() + "/" + Uri.encode(uri.getPathSegments().get(0));
}
/**
* Returns the cache directory for eml attachment files.
*/
private String getCacheDir() {
return getContext().getCacheDir().getAbsolutePath().concat("/eml");
}
/**
* Recursively delete the directory at the passed file path.
*/
private void deleteDirectory(String cacheFileDirectory) {
recursiveDelete(new File(cacheFileDirectory));
}
/**
* Recursively deletes a file or directory.
*/
private void recursiveDelete(File file) {
if (file.isDirectory()) {
final File[] children = file.listFiles();
for (final File child : children) {
recursiveDelete(child);
}
}
file.delete();
}
}