summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--AndroidManifest.xml9
-rw-r--r--res/values/strings.xml3
-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
13 files changed, 701 insertions, 17 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 91a8b3c7e..a615facb4 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -146,6 +146,14 @@
<grant-uri-permission android:pathPattern=".*" />
</provider>
+ <provider
+ android:authorities="@string/eml_attachment_provider"
+ android:multiprocess="false"
+ android:exported="false"
+ android:name="com.android.mail.providers.EmlAttachmentProvider" >
+ <grant-uri-permission android:pathPattern=".*" />
+ </provider>
+
<!-- The android:name is the name of the Provider class which is stored in
UnifiedEmail, and has package name com.android.mail.providers and the class is
called SuggestionsProvider. The authority name is specified in the MailAppProvider
@@ -170,6 +178,7 @@
android:permission="android.permission.BIND_REMOTEVIEWS"
android:exported="false" />
<service android:name=".MailLogService"/>
+ <service android:name=".browse.EmlTempFileDeletionService" />
</application>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 6cc5cec8c..09e554bae 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -944,4 +944,7 @@
<!-- Button, "Add account" in the preference screen [CHAR LIMIT=30] -->
<string name="add_account">Add account</string>
+
+ <!-- Content Provider Authority for Eml Attachments -->
+ <string name="eml_attachment_provider" translatable="false">com.android.mail.provider.eml.attachment</string>
</resources>
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() {}
}