summaryrefslogtreecommitdiffstats
path: root/src/com
diff options
context:
space:
mode:
authorAndrew Sapperstein <asapperstein@google.com>2013-06-21 11:26:49 -0700
committerAndrew Sapperstein <asapperstein@google.com>2013-06-24 11:40:19 -0700
commit7434e800d4313a227120ca36bd95683752a7879f (patch)
tree987bd2fdb11b5e2cda31e3613df3f195c7ccd926 /src/com
parent48ccbc53ef90bf6420f831f63e6243008e02a346 (diff)
downloadandroid_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.java2
-rw-r--r--src/com/android/mail/browse/ConversationMessage.java6
-rw-r--r--src/com/android/mail/browse/EmlMessageLoader.java24
-rw-r--r--src/com/android/mail/browse/EmlTempFileDeletionService.java45
-rw-r--r--src/com/android/mail/browse/MessageAttachmentBar.java3
-rw-r--r--src/com/android/mail/browse/MessageAttachmentTile.java2
-rw-r--r--src/com/android/mail/perf/Timer.java5
-rw-r--r--src/com/android/mail/providers/Attachment.java123
-rw-r--r--src/com/android/mail/providers/EmlAttachmentProvider.java466
-rw-r--r--src/com/android/mail/providers/Message.java21
-rw-r--r--src/com/android/mail/providers/UIProvider.java9
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() {}
}