diff options
author | Andrew Sapperstein <asapperstein@google.com> | 2013-06-21 11:26:49 -0700 |
---|---|---|
committer | Andrew Sapperstein <asapperstein@google.com> | 2013-06-24 11:40:19 -0700 |
commit | 7434e800d4313a227120ca36bd95683752a7879f (patch) | |
tree | 987bd2fdb11b5e2cda31e3613df3f195c7ccd926 /src/com | |
parent | 48ccbc53ef90bf6420f831f63e6243008e02a346 (diff) | |
download | android_packages_apps_UnifiedEmail-7434e800d4313a227120ca36bd95683752a7879f.tar.gz android_packages_apps_UnifiedEmail-7434e800d4313a227120ca36bd95683752a7879f.tar.bz2 android_packages_apps_UnifiedEmail-7434e800d4313a227120ca36bd95683752a7879f.zip |
Attachments in eml files.
It's kinda slow for large eml files
(because we have to parse the entire
file) but it works. Hooray.
Fixes b/6393073.
Change-Id: I2d7a87a484cd282a000a0905fe069f3ab45e2061
Diffstat (limited to 'src/com')
-rw-r--r-- | src/com/android/mail/browse/AttachmentActionHandler.java | 2 | ||||
-rw-r--r-- | src/com/android/mail/browse/ConversationMessage.java | 6 | ||||
-rw-r--r-- | src/com/android/mail/browse/EmlMessageLoader.java | 24 | ||||
-rw-r--r-- | src/com/android/mail/browse/EmlTempFileDeletionService.java | 45 | ||||
-rw-r--r-- | src/com/android/mail/browse/MessageAttachmentBar.java | 3 | ||||
-rw-r--r-- | src/com/android/mail/browse/MessageAttachmentTile.java | 2 | ||||
-rw-r--r-- | src/com/android/mail/perf/Timer.java | 5 | ||||
-rw-r--r-- | src/com/android/mail/providers/Attachment.java | 123 | ||||
-rw-r--r-- | src/com/android/mail/providers/EmlAttachmentProvider.java | 466 | ||||
-rw-r--r-- | src/com/android/mail/providers/Message.java | 21 | ||||
-rw-r--r-- | src/com/android/mail/providers/UIProvider.java | 9 |
11 files changed, 689 insertions, 17 deletions
diff --git a/src/com/android/mail/browse/AttachmentActionHandler.java b/src/com/android/mail/browse/AttachmentActionHandler.java index 9a95b1ec4..40a66db9b 100644 --- a/src/com/android/mail/browse/AttachmentActionHandler.java +++ b/src/com/android/mail/browse/AttachmentActionHandler.java @@ -104,7 +104,7 @@ public class AttachmentActionHandler { private void startDownloadingAttachment( Attachment attachment, int destination, int rendition, int additionalPriority, boolean delayDownload) { - final ContentValues params = new ContentValues(2); + final ContentValues params = new ContentValues(5); params.put(AttachmentColumns.STATE, AttachmentState.DOWNLOADING); params.put(AttachmentColumns.DESTINATION, destination); params.put(AttachmentContentValueKeys.RENDITION, rendition); diff --git a/src/com/android/mail/browse/ConversationMessage.java b/src/com/android/mail/browse/ConversationMessage.java index e66494f3d..e9aa875a1 100644 --- a/src/com/android/mail/browse/ConversationMessage.java +++ b/src/com/android/mail/browse/ConversationMessage.java @@ -17,6 +17,7 @@ package com.android.mail.browse; +import android.content.Context; import android.database.Cursor; import android.net.Uri; @@ -51,8 +52,9 @@ public final class ConversationMessage extends Message { super(cursor); } - public ConversationMessage(MimeMessage mimeMessage) throws MessagingException { - super(mimeMessage); + public ConversationMessage(Context context, MimeMessage mimeMessage, Uri emlFileUri) + throws MessagingException { + super(context, mimeMessage, emlFileUri); } public void setController(ConversationController controller) { diff --git a/src/com/android/mail/browse/EmlMessageLoader.java b/src/com/android/mail/browse/EmlMessageLoader.java index 09e5b6a84..0bacb5acf 100644 --- a/src/com/android/mail/browse/EmlMessageLoader.java +++ b/src/com/android/mail/browse/EmlMessageLoader.java @@ -7,7 +7,7 @@ * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 - *and + * * 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. @@ -20,6 +20,7 @@ package com.android.mail.browse; import android.content.AsyncTaskLoader; import android.content.ContentResolver; import android.content.Context; +import android.content.Intent; import android.net.Uri; import com.android.emailcommon.TempDirectory; @@ -28,6 +29,7 @@ import com.android.emailcommon.mail.MessagingException; import com.android.mail.utils.LogTag; import com.android.mail.utils.LogUtils; +import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; @@ -63,7 +65,7 @@ public class EmlMessageLoader extends AsyncTaskLoader<ConversationMessage> { final ConversationMessage convMessage; try { mimeMessage = new MimeMessage(stream); - convMessage = new ConversationMessage(mimeMessage); + convMessage = new ConversationMessage(context, mimeMessage, mEmlFileUri); } catch (IOException e) { LogUtils.e(LOG_TAG, e, "Could not read eml file"); return null; @@ -76,6 +78,15 @@ public class EmlMessageLoader extends AsyncTaskLoader<ConversationMessage> { } catch (IOException e) { return null; } + + // delete temp files created during parsing + final File[] cacheFiles = TempDirectory.getTempDirectory().listFiles(); + for (final File file : cacheFiles) { + if (file.getName().startsWith("body")) { + file.delete(); + } + } + } return convMessage; @@ -175,6 +186,13 @@ public class EmlMessageLoader extends AsyncTaskLoader<ConversationMessage> { * with an actively loaded data set. */ protected void onReleaseResources(ConversationMessage message) { - // DO NOTHING + // if this eml message had attachments, start a service to clean up the cache files + if (message.attachmentListUri != null) { + final Intent intent = new Intent(Intent.ACTION_DELETE); + intent.setClass(getContext(), EmlTempFileDeletionService.class); + intent.setData(message.attachmentListUri); + + getContext().startService(intent); + } } } diff --git a/src/com/android/mail/browse/EmlTempFileDeletionService.java b/src/com/android/mail/browse/EmlTempFileDeletionService.java new file mode 100644 index 000000000..71a915c51 --- /dev/null +++ b/src/com/android/mail/browse/EmlTempFileDeletionService.java @@ -0,0 +1,45 @@ +/* + * 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.browse; + +import android.app.IntentService; +import android.content.Intent; +import android.net.Uri; + +/** + * {@link IntentService} that cleans up temporary files in the cache for the eml viewer. + */ +public class EmlTempFileDeletionService extends IntentService { + + public EmlTempFileDeletionService() { + super("EmlTempFileDeletionService"); + } + + public EmlTempFileDeletionService(String name) { + super(name); + } + + @Override + protected void onHandleIntent(Intent intent) { + final String action = intent.getAction(); + if (Intent.ACTION_DELETE.equals(action)) { + final Uri uri = intent.getData(); + getContentResolver().delete(uri, null, null); + } + } +} diff --git a/src/com/android/mail/browse/MessageAttachmentBar.java b/src/com/android/mail/browse/MessageAttachmentBar.java index e61fdcf03..b1d26cc9b 100644 --- a/src/com/android/mail/browse/MessageAttachmentBar.java +++ b/src/com/android/mail/browse/MessageAttachmentBar.java @@ -261,7 +261,8 @@ public class MessageAttachmentBar extends FrameLayout implements OnClickListener private boolean shouldShowDownloadAgain() { // implies state == SAVED || state == FAILED - return mAttachment.isDownloadFinishedOrFailed(); + // and the attachment supports re-download + return mAttachment.supportsDownloadAgain() && mAttachment.isDownloadFinishedOrFailed(); } private boolean shouldShowOverflow() { diff --git a/src/com/android/mail/browse/MessageAttachmentTile.java b/src/com/android/mail/browse/MessageAttachmentTile.java index 7759f7f70..501189b75 100644 --- a/src/com/android/mail/browse/MessageAttachmentTile.java +++ b/src/com/android/mail/browse/MessageAttachmentTile.java @@ -174,7 +174,7 @@ public class MessageAttachmentTile extends AttachmentTile implements OnClickList getContext().startActivity(intent); } catch (ActivityNotFoundException e) { // couldn't find activity for View intent - LogUtils.e(LOG_TAG, "Coun't find Activity for intent", e); + LogUtils.e(LOG_TAG, "Couldn't find Activity for intent", e); } } diff --git a/src/com/android/mail/perf/Timer.java b/src/com/android/mail/perf/Timer.java index 668fdfd5d..917326175 100644 --- a/src/com/android/mail/perf/Timer.java +++ b/src/com/android/mail/perf/Timer.java @@ -15,12 +15,11 @@ */ package com.android.mail.perf; -import com.android.mail.utils.LogTag; -import com.android.mail.utils.LogUtils; - import android.os.Debug; import android.os.SystemClock; +import com.android.mail.utils.LogTag; +import com.android.mail.utils.LogUtils; import com.google.common.collect.Maps; import java.util.ArrayList; diff --git a/src/com/android/mail/providers/Attachment.java b/src/com/android/mail/providers/Attachment.java index 6fbef1c99..37ae4473d 100644 --- a/src/com/android/mail/providers/Attachment.java +++ b/src/com/android/mail/providers/Attachment.java @@ -16,15 +16,18 @@ package com.android.mail.providers; -import com.google.common.base.Objects; -import com.google.common.collect.Lists; - +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; +import com.android.emailcommon.internet.MimeUtility; +import com.android.emailcommon.mail.MessagingException; +import com.android.emailcommon.mail.Part; import com.android.mail.browse.MessageAttachmentBar; import com.android.mail.providers.UIProvider.AttachmentColumns; import com.android.mail.providers.UIProvider.AttachmentDestination; @@ -33,11 +36,18 @@ import com.android.mail.utils.LogTag; import com.android.mail.utils.LogUtils; import com.android.mail.utils.MimeType; import com.android.mail.utils.Utils; +import com.google.common.base.Objects; +import com.google.common.collect.Lists; +import org.apache.commons.io.IOUtils; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.util.Collection; import java.util.List; @@ -136,6 +146,11 @@ public class Attachment implements Parcelable { private transient Uri mIdentifierUri; + /** + * True if this attachment can be downloaded again. + */ + private boolean supportsDownloadAgain; + public Attachment() { } @@ -151,6 +166,7 @@ public class Attachment implements Parcelable { thumbnailUri = in.readParcelable(null); previewIntentUri = in.readParcelable(null); providerData = in.readString(); + supportsDownloadAgain = in.readInt() == 1; } public Attachment(Cursor cursor) { @@ -172,6 +188,8 @@ public class Attachment implements Parcelable { previewIntentUri = parseOptionalUri( cursor.getString(cursor.getColumnIndex(AttachmentColumns.PREVIEW_INTENT_URI))); providerData = cursor.getString(cursor.getColumnIndex(AttachmentColumns.PROVIDER_DATA)); + supportsDownloadAgain = cursor.getInt( + cursor.getColumnIndex(AttachmentColumns.SUPPORTS_DOWNLOAD_AGAIN)) == 1; } public Attachment(JSONObject srcJson) { @@ -186,6 +204,99 @@ public class Attachment implements Parcelable { thumbnailUri = parseOptionalUri(srcJson, AttachmentColumns.THUMBNAIL_URI); previewIntentUri = parseOptionalUri(srcJson, AttachmentColumns.PREVIEW_INTENT_URI); providerData = srcJson.optString(AttachmentColumns.PROVIDER_DATA); + supportsDownloadAgain = srcJson.optBoolean(AttachmentColumns.SUPPORTS_DOWNLOAD_AGAIN, true); + } + + /** + * Constructor for use when creating attachments in eml files. + */ + public Attachment(Context context, Part part, Uri emlFileUri, String messageId, String partId) { + try { + // Transfer fields from mime format to provider format + final String contentTypeHeader = MimeUtility.unfoldAndDecode(part.getContentType()); + name = MimeUtility.getHeaderParameter(contentTypeHeader, "name"); + if (name == null) { + final String contentDisposition = + MimeUtility.unfoldAndDecode(part.getDisposition()); + name = MimeUtility.getHeaderParameter(contentDisposition, "filename"); + } + + contentType = MimeType.inferMimeType(name, part.getMimeType()); + uri = EmlAttachmentProvider.getAttachmentUri(emlFileUri, messageId, partId); + contentUri = uri; + thumbnailUri = uri; + previewIntentUri = null; + state = AttachmentState.SAVED; + providerData = null; + supportsDownloadAgain = false; + destination = AttachmentDestination.CACHE; + + // insert attachment into content provider so that we can open the file + final ContentResolver resolver = context.getContentResolver(); + resolver.insert(uri, toContentValues()); + + // save the file in the cache + try { + final InputStream in = part.getBody().getInputStream(); + final OutputStream out = resolver.openOutputStream(uri, "rwt"); + size = IOUtils.copy(in, out); + downloadedSize = size; + in.close(); + out.close(); + } catch (FileNotFoundException e) { + LogUtils.e(LOG_TAG, e, "Error in writing attachment to cache"); + } catch (IOException e) { + LogUtils.e(LOG_TAG, e, "Error in writing attachment to cache"); + } + // perform a second insert to put the updated size and downloaded size values in + resolver.insert(uri, toContentValues()); + } catch (MessagingException e) { + LogUtils.e(LOG_TAG, e, "Error parsing eml attachment"); + } + } + + /** + * Create an attachment from a {@link ContentValues} object. + * The keys should be {@link AttachmentColumns}. + */ + public Attachment(ContentValues values) { + name = values.getAsString(AttachmentColumns.NAME); + size = values.getAsInteger(AttachmentColumns.SIZE); + uri = parseOptionalUri(values.getAsString(AttachmentColumns.URI)); + contentType = values.getAsString(AttachmentColumns.CONTENT_TYPE); + state = values.getAsInteger(AttachmentColumns.STATE); + destination = values.getAsInteger(AttachmentColumns.DESTINATION); + downloadedSize = values.getAsInteger(AttachmentColumns.DOWNLOADED_SIZE); + contentUri = parseOptionalUri(values.getAsString(AttachmentColumns.CONTENT_URI)); + thumbnailUri = parseOptionalUri(values.getAsString(AttachmentColumns.THUMBNAIL_URI)); + previewIntentUri = + parseOptionalUri(values.getAsString(AttachmentColumns.PREVIEW_INTENT_URI)); + providerData = values.getAsString(AttachmentColumns.PROVIDER_DATA); + supportsDownloadAgain = values.getAsBoolean(AttachmentColumns.SUPPORTS_DOWNLOAD_AGAIN); + } + + /** + * Returns the various attachment fields in a {@link ContentValues} object. + * The keys for each field should be {@link AttachmentColumns}. + */ + public ContentValues toContentValues() { + final ContentValues values = new ContentValues(12); + + values.put(AttachmentColumns.NAME, name); + values.put(AttachmentColumns.SIZE, size); + values.put(AttachmentColumns.URI, uri.toString()); + values.put(AttachmentColumns.CONTENT_TYPE, contentType); + values.put(AttachmentColumns.STATE, state); + values.put(AttachmentColumns.DESTINATION, destination); + values.put(AttachmentColumns.DOWNLOADED_SIZE, downloadedSize); + values.put(AttachmentColumns.CONTENT_URI, contentUri.toString()); + values.put(AttachmentColumns.THUMBNAIL_URI, thumbnailUri.toString()); + values.put(AttachmentColumns.PREVIEW_INTENT_URI, + previewIntentUri == null ? null : previewIntentUri.toString()); + values.put(AttachmentColumns.PROVIDER_DATA, providerData); + values.put(AttachmentColumns.SUPPORTS_DOWNLOAD_AGAIN, supportsDownloadAgain); + + return values; } @Override @@ -201,6 +312,7 @@ public class Attachment implements Parcelable { dest.writeParcelable(thumbnailUri, flags); dest.writeParcelable(previewIntentUri, flags); dest.writeString(providerData); + dest.writeInt(supportsDownloadAgain ? 1 : 0); } public JSONObject toJSON() throws JSONException { @@ -217,6 +329,7 @@ public class Attachment implements Parcelable { result.put(AttachmentColumns.THUMBNAIL_URI, stringify(thumbnailUri)); result.put(AttachmentColumns.PREVIEW_INTENT_URI, stringify(previewIntentUri)); result.put(AttachmentColumns.PROVIDER_DATA, providerData); + result.put(AttachmentColumns.SUPPORTS_DOWNLOAD_AGAIN, supportsDownloadAgain); return result; } @@ -294,6 +407,10 @@ public class Attachment implements Parcelable { return state == AttachmentState.FAILED || state == AttachmentState.SAVED; } + public boolean supportsDownloadAgain() { + return supportsDownloadAgain; + } + public boolean canPreview() { return previewIntentUri != null; } diff --git a/src/com/android/mail/providers/EmlAttachmentProvider.java b/src/com/android/mail/providers/EmlAttachmentProvider.java new file mode 100644 index 000000000..e0b08a3d0 --- /dev/null +++ b/src/com/android/mail/providers/EmlAttachmentProvider.java @@ -0,0 +1,466 @@ +/* + * 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.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 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."); + } + } + + // 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(); + } +} diff --git a/src/com/android/mail/providers/Message.java b/src/com/android/mail/providers/Message.java index cc9ea3f2c..0b71c69d6 100644 --- a/src/com/android/mail/providers/Message.java +++ b/src/com/android/mail/providers/Message.java @@ -18,6 +18,7 @@ package com.android.mail.providers; import android.content.AsyncQueryHandler; import android.content.ContentValues; +import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.Parcel; @@ -37,6 +38,7 @@ import com.android.emailcommon.utility.ConversionUtilities; import com.android.mail.providers.UIProvider.MessageColumns; import com.android.mail.utils.Utils; import com.google.common.base.Objects; +import com.google.common.collect.Lists; import java.util.ArrayList; import java.util.Collections; @@ -360,7 +362,8 @@ public class Message implements Parcelable { } } - public Message(MimeMessage mimeMessage) throws MessagingException { + public Message(Context context, MimeMessage mimeMessage, Uri emlFileUri) + throws MessagingException { // Set message header values. setFrom(com.android.emailcommon.mail.Address.pack(mimeMessage.getFrom())); setTo(com.android.emailcommon.mail.Address.pack(mimeMessage.getRecipients( @@ -395,7 +398,21 @@ public class Message implements Parcelable { snippet = data.snippet; bodyText = data.textContent; bodyHtml = data.htmlContent; - // TODO - attachments? + + // populate mAttachments + mAttachments = Lists.newArrayList(); + + int partId = 0; + final String messageId = mimeMessage.getMessageId(); + for (final Part attachmentPart : attachments) { + mAttachments.add(new Attachment(context, attachmentPart, + emlFileUri, messageId, Integer.toString(partId++))); + } + + hasAttachments = !mAttachments.isEmpty(); + + attachmentListUri = hasAttachments ? + EmlAttachmentProvider.getAttachmentsListUri(emlFileUri, messageId) : null; } public boolean isFlaggedReplied() { diff --git a/src/com/android/mail/providers/UIProvider.java b/src/com/android/mail/providers/UIProvider.java index 7fd379d7c..9c598e066 100644 --- a/src/com/android/mail/providers/UIProvider.java +++ b/src/com/android/mail/providers/UIProvider.java @@ -1721,7 +1721,8 @@ public class UIProvider { AttachmentColumns.CONTENT_URI, AttachmentColumns.THUMBNAIL_URI, AttachmentColumns.PREVIEW_INTENT_URI, - AttachmentColumns.PROVIDER_DATA + AttachmentColumns.PROVIDER_DATA, + AttachmentColumns.SUPPORTS_DOWNLOAD_AGAIN }; public static final int ATTACHMENT_NAME_COLUMN = 0; public static final int ATTACHMENT_SIZE_COLUMN = 1; @@ -1733,6 +1734,7 @@ public class UIProvider { public static final int ATTACHMENT_CONTENT_URI_COLUMN = 7; public static final int ATTACHMENT_THUMBNAIL_URI_COLUMN = 8; public static final int ATTACHMENT_PREVIEW_INTENT_COLUMN = 9; + public static final int ATTACHMENT_SUPPORTS_DOWNLOAD_AGAIN_COLUMN = 10; /** * Valid states for the {@link AttachmentColumns#STATE} column. @@ -1889,6 +1891,11 @@ public class UIProvider { */ public static final String PROVIDER_DATA = "providerData"; + /** + * This column tells whether this attachment supports the ability to be downloaded again. + */ + public static final String SUPPORTS_DOWNLOAD_AGAIN = "supportsDownloadAgain"; + private AttachmentColumns() {} } |