/** * Copyright (c) 2011, Google Inc. * * 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.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; import com.android.mail.providers.UIProvider.AttachmentRendition; import com.android.mail.providers.UIProvider.AttachmentState; import com.android.mail.providers.UIProvider.AttachmentType; 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.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; public class Attachment implements Parcelable { public static final int MAX_ATTACHMENT_PREVIEWS = 2; public static final String LOG_TAG = LogTag.getLogTag(); /** * Workaround for b/8070022 so that appending a null partId to the end of a * uri wouldn't remove the trailing backslash */ public static final String EMPTY_PART_ID = "empty"; // Indicates that this is a dummy placeholder attachment. public static final int FLAG_DUMMY_ATTACHMENT = 1<<10; /** * Part id of the attachment. */ public String partId; /** * Attachment file name. See {@link AttachmentColumns#NAME} Use {@link #setName(String)}. */ private String name; /** * Attachment size in bytes. See {@link AttachmentColumns#SIZE}. */ public int size; /** * The provider-generated URI for this Attachment. Must be globally unique. * For local attachments generated by the Compose UI prior to send/save, * this field will be null. * * @see AttachmentColumns#URI */ public Uri uri; /** * MIME type of the file. Use {@link #getContentType()} and {@link #setContentType(String)}. * * @see AttachmentColumns#CONTENT_TYPE */ private String contentType; private String inferredContentType; /** * Use {@link #setState(int)} * * @see AttachmentColumns#STATE */ public int state; /** * @see AttachmentColumns#DESTINATION */ public int destination; /** * @see AttachmentColumns#DOWNLOADED_SIZE */ public int downloadedSize; /** * Shareable, openable uri for this attachment *

* content:// Gmail.getAttachmentDefaultUri() if origin is SERVER_ATTACHMENT *

* content:// uri pointing to the content to be uploaded if origin is * LOCAL_FILE *

* file:// uri pointing to an EXTERNAL apk file. The package manager only * handles file:// uris not content:// uris. We do the same workaround in * {@link MessageAttachmentBar#onClick(android.view.View)} and * UiProvider#getUiAttachmentsCursorForUIAttachments(). * * @see AttachmentColumns#CONTENT_URI */ public Uri contentUri; /** * Might be null. * * @see AttachmentColumns#THUMBNAIL_URI */ public Uri thumbnailUri; /** * Might be null. * * @see AttachmentColumns#PREVIEW_INTENT_URI */ public Uri previewIntentUri; /** * The visibility type of this attachment. * * @see AttachmentColumns#TYPE */ public int type; public int flags; /** * Might be null. JSON string. * * @see AttachmentColumns#PROVIDER_DATA */ public String providerData; private transient Uri mIdentifierUri; /** * True if this attachment can be downloaded again. */ private boolean supportsDownloadAgain; public Attachment() { } public Attachment(Parcel in) { name = in.readString(); size = in.readInt(); uri = in.readParcelable(null); contentType = in.readString(); state = in.readInt(); destination = in.readInt(); downloadedSize = in.readInt(); contentUri = in.readParcelable(null); thumbnailUri = in.readParcelable(null); previewIntentUri = in.readParcelable(null); providerData = in.readString(); supportsDownloadAgain = in.readInt() == 1; type = in.readInt(); flags = in.readInt(); } public Attachment(Cursor cursor) { if (cursor == null) { return; } name = cursor.getString(cursor.getColumnIndex(AttachmentColumns.NAME)); size = cursor.getInt(cursor.getColumnIndex(AttachmentColumns.SIZE)); uri = Uri.parse(cursor.getString(cursor.getColumnIndex(AttachmentColumns.URI))); contentType = cursor.getString(cursor.getColumnIndex(AttachmentColumns.CONTENT_TYPE)); state = cursor.getInt(cursor.getColumnIndex(AttachmentColumns.STATE)); destination = cursor.getInt(cursor.getColumnIndex(AttachmentColumns.DESTINATION)); downloadedSize = cursor.getInt(cursor.getColumnIndex(AttachmentColumns.DOWNLOADED_SIZE)); contentUri = parseOptionalUri( cursor.getString(cursor.getColumnIndex(AttachmentColumns.CONTENT_URI))); thumbnailUri = parseOptionalUri( cursor.getString(cursor.getColumnIndex(AttachmentColumns.THUMBNAIL_URI))); 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; type = cursor.getInt(cursor.getColumnIndex(AttachmentColumns.TYPE)); flags = cursor.getInt(cursor.getColumnIndex(AttachmentColumns.FLAGS)); } public Attachment(JSONObject srcJson) { name = srcJson.optString(AttachmentColumns.NAME, null); size = srcJson.optInt(AttachmentColumns.SIZE); uri = parseOptionalUri(srcJson, AttachmentColumns.URI); contentType = srcJson.optString(AttachmentColumns.CONTENT_TYPE, null); state = srcJson.optInt(AttachmentColumns.STATE); destination = srcJson.optInt(AttachmentColumns.DESTINATION); downloadedSize = srcJson.optInt(AttachmentColumns.DOWNLOADED_SIZE); contentUri = parseOptionalUri(srcJson, AttachmentColumns.CONTENT_URI); 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); type = srcJson.optInt(AttachmentColumns.TYPE); flags = srcJson.optInt(AttachmentColumns.FLAGS); } /** * Constructor for use when creating attachments in eml files. */ public Attachment(Context context, Part part, Uri emlFileUri, String messageId, String cid, boolean inline) { 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, cid); contentUri = uri; thumbnailUri = uri; previewIntentUri = null; state = AttachmentState.SAVED; providerData = null; supportsDownloadAgain = false; destination = AttachmentDestination.CACHE; type = inline ? AttachmentType.INLINE_CURRENT_MESSAGE : AttachmentType.STANDARD; partId = cid; flags = 0; // 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); type = values.getAsInteger(AttachmentColumns.TYPE); flags = values.getAsInteger(AttachmentColumns.FLAGS); partId = values.getAsString(AttachmentColumns.CONTENT_ID); } /** * 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); values.put(AttachmentColumns.TYPE, type); values.put(AttachmentColumns.FLAGS, flags); values.put(AttachmentColumns.CONTENT_ID, partId); return values; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(name); dest.writeInt(size); dest.writeParcelable(uri, flags); dest.writeString(contentType); dest.writeInt(state); dest.writeInt(destination); dest.writeInt(downloadedSize); dest.writeParcelable(contentUri, flags); dest.writeParcelable(thumbnailUri, flags); dest.writeParcelable(previewIntentUri, flags); dest.writeString(providerData); dest.writeInt(supportsDownloadAgain ? 1 : 0); dest.writeInt(type); dest.writeInt(flags); } public JSONObject toJSON() throws JSONException { final JSONObject result = new JSONObject(); result.put(AttachmentColumns.NAME, name); result.put(AttachmentColumns.SIZE, size); result.put(AttachmentColumns.URI, stringify(uri)); result.put(AttachmentColumns.CONTENT_TYPE, contentType); result.put(AttachmentColumns.STATE, state); result.put(AttachmentColumns.DESTINATION, destination); result.put(AttachmentColumns.DOWNLOADED_SIZE, downloadedSize); result.put(AttachmentColumns.CONTENT_URI, stringify(contentUri)); 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); result.put(AttachmentColumns.TYPE, type); result.put(AttachmentColumns.FLAGS, flags); return result; } @Override public String toString() { try { final JSONObject jsonObject = toJSON(); // Add some additional fields that are helpful when debugging issues jsonObject.put("partId", partId); if (providerData != null) { try { // pretty print the provider data jsonObject.put(AttachmentColumns.PROVIDER_DATA, new JSONObject(providerData)); } catch (JSONException e) { LogUtils.e(LOG_TAG, e, "JSONException when adding provider data"); } } return jsonObject.toString(4); } catch (JSONException e) { LogUtils.e(LOG_TAG, e, "JSONException in toString"); return super.toString(); } } private static String stringify(Object object) { return object != null ? object.toString() : null; } protected static Uri parseOptionalUri(String uriString) { return uriString == null ? null : Uri.parse(uriString); } protected static Uri parseOptionalUri(JSONObject srcJson, String key) { final String uriStr = srcJson.optString(key, null); return uriStr == null ? null : Uri.parse(uriStr); } @Override public int describeContents() { return 0; } public boolean isPresentLocally() { return state == AttachmentState.SAVED; } public boolean canSave() { return !isSavedToExternal() && !isInstallable(); } public boolean canShare() { return isPresentLocally() && contentUri != null; } public boolean isDownloading() { return state == AttachmentState.DOWNLOADING || state == AttachmentState.PAUSED; } public boolean isSavedToExternal() { return state == AttachmentState.SAVED && destination == AttachmentDestination.EXTERNAL; } public boolean isInstallable() { return MimeType.isInstallable(getContentType()); } public boolean shouldShowProgress() { return (state == AttachmentState.DOWNLOADING || state == AttachmentState.PAUSED) && size > 0 && downloadedSize > 0 && downloadedSize <= size; } public boolean isDownloadFailed() { return state == AttachmentState.FAILED; } public boolean isDownloadFinishedOrFailed() { return state == AttachmentState.FAILED || state == AttachmentState.SAVED; } public boolean supportsDownloadAgain() { return supportsDownloadAgain; } public boolean canPreview() { return previewIntentUri != null; } /** * Returns a stable identifier URI for this attachment. TODO: make the uri * field stable, and put provider-specific opaque bits and bobs elsewhere */ public Uri getIdentifierUri() { if (Utils.isEmpty(mIdentifierUri)) { mIdentifierUri = Utils.isEmpty(uri) ? (Utils.isEmpty(contentUri) ? Uri.EMPTY : contentUri) : uri.buildUpon().clearQuery().build(); } return mIdentifierUri; } public String getContentType() { if (TextUtils.isEmpty(inferredContentType)) { inferredContentType = MimeType.inferMimeType(name, contentType); } return inferredContentType; } public Uri getUriForRendition(int rendition) { final Uri uri; switch (rendition) { case AttachmentRendition.BEST: uri = contentUri; break; case AttachmentRendition.SIMPLE: uri = thumbnailUri; break; default: throw new IllegalArgumentException("invalid rendition: " + rendition); } return uri; } public void setContentType(String contentType) { if (!TextUtils.equals(this.contentType, contentType)) { this.inferredContentType = null; this.contentType = contentType; } } public String getName() { return name; } public boolean setName(String name) { if (!TextUtils.equals(this.name, name)) { this.inferredContentType = null; this.name = name; return true; } return false; } /** * Sets the attachment state. Side effect: sets downloadedSize */ public void setState(int state) { this.state = state; if (state == AttachmentState.FAILED || state == AttachmentState.NOT_SAVED) { this.downloadedSize = 0; } } /** * @return {@code true} if the attachment is an inline attachment * that appears in the body of the message content (including possibly * quoted text). */ public boolean isInlineAttachment() { return type != UIProvider.AttachmentType.STANDARD; } @Override public boolean equals(final Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } final Attachment that = (Attachment) o; if (destination != that.destination) { return false; } if (downloadedSize != that.downloadedSize) { return false; } if (size != that.size) { return false; } if (state != that.state) { return false; } if (supportsDownloadAgain != that.supportsDownloadAgain) { return false; } if (type != that.type) { return false; } if (contentType != null ? !contentType.equals(that.contentType) : that.contentType != null) { return false; } if (contentUri != null ? !contentUri.equals(that.contentUri) : that.contentUri != null) { return false; } if (name != null ? !name.equals(that.name) : that.name != null) { return false; } if (partId != null ? !partId.equals(that.partId) : that.partId != null) { return false; } if (previewIntentUri != null ? !previewIntentUri.equals(that.previewIntentUri) : that.previewIntentUri != null) { return false; } if (providerData != null ? !providerData.equals(that.providerData) : that.providerData != null) { return false; } if (thumbnailUri != null ? !thumbnailUri.equals(that.thumbnailUri) : that.thumbnailUri != null) { return false; } if (uri != null ? !uri.equals(that.uri) : that.uri != null) { return false; } return true; } @Override public int hashCode() { int result = partId != null ? partId.hashCode() : 0; result = 31 * result + (name != null ? name.hashCode() : 0); result = 31 * result + size; result = 31 * result + (uri != null ? uri.hashCode() : 0); result = 31 * result + (contentType != null ? contentType.hashCode() : 0); result = 31 * result + state; result = 31 * result + destination; result = 31 * result + downloadedSize; result = 31 * result + (contentUri != null ? contentUri.hashCode() : 0); result = 31 * result + (thumbnailUri != null ? thumbnailUri.hashCode() : 0); result = 31 * result + (previewIntentUri != null ? previewIntentUri.hashCode() : 0); result = 31 * result + type; result = 31 * result + (providerData != null ? providerData.hashCode() : 0); result = 31 * result + (supportsDownloadAgain ? 1 : 0); return result; } public static String toJSONArray(Collection attachments) { if (attachments == null) { return null; } final JSONArray result = new JSONArray(); try { for (Attachment attachment : attachments) { result.put(attachment.toJSON()); } } catch (JSONException e) { throw new IllegalArgumentException(e); } return result.toString(); } public static List fromJSONArray(String jsonArrayStr) { final List results = Lists.newArrayList(); if (jsonArrayStr != null) { try { final JSONArray arr = new JSONArray(jsonArrayStr); for (int i = 0; i < arr.length(); i++) { results.add(new Attachment(arr.getJSONObject(i))); } } catch (JSONException e) { throw new IllegalArgumentException(e); } } return results; } private static final String SERVER_ATTACHMENT = "SERVER_ATTACHMENT"; private static final String LOCAL_FILE = "LOCAL_FILE"; public String toJoinedString() { return TextUtils.join(UIProvider.ATTACHMENT_INFO_DELIMITER, Lists.newArrayList( partId == null ? "" : partId, name == null ? "" : name.replaceAll("[" + UIProvider.ATTACHMENT_INFO_DELIMITER + UIProvider.ATTACHMENT_INFO_SEPARATOR + "]", ""), getContentType(), String.valueOf(size), getContentType(), contentUri != null ? SERVER_ATTACHMENT : LOCAL_FILE, contentUri != null ? contentUri.toString() : "", "" /* cachedFileUri */, String.valueOf(type))); } /** * For use with {@link UIProvider.ConversationColumns#ATTACHMENT_PREVIEW_STATES}. * * @param previewStates The packed int describing the states of multiple attachments. * @param attachmentIndex The index of the attachment to update. * @param rendition The rendition of that attachment to update. * @param downloaded Whether that specific rendition is downloaded. * @return A packed int describing the updated downloaded states of the multiple attachments. */ public static int updatePreviewStates(int previewStates, int attachmentIndex, int rendition, boolean downloaded) { // find the bit that describes that specific attachment index and rendition int shift = attachmentIndex * 2 + rendition; int mask = 1 << shift; // update the packed int at that bit if (downloaded) { // turns that bit into a 1 return previewStates | mask; } else { // turns that bit into a 0 return previewStates & ~mask; } } /** * For use with {@link UIProvider.ConversationColumns#ATTACHMENT_PREVIEW_STATES}. * * @param previewStates The packed int describing the states of multiple attachments. * @param attachmentIndex The index of the attachment. * @param rendition The rendition of the attachment. * @return The downloaded state of that particular rendition of that particular attachment. */ public static boolean getPreviewState(int previewStates, int attachmentIndex, int rendition) { // find the bit that describes that specific attachment index int shift = attachmentIndex * 2; int mask = 1 << shift; if (rendition == AttachmentRendition.SIMPLE) { // implicit shift of 0 finds the SIMPLE rendition bit return (previewStates & mask) != 0; } else if (rendition == AttachmentRendition.BEST) { // shift of 1 finds the BEST rendition bit return (previewStates & (mask << 1)) != 0; } else { return false; } } public static final Creator CREATOR = new Creator() { @Override public Attachment createFromParcel(Parcel source) { return new Attachment(source); } @Override public Attachment[] newArray(int size) { return new Attachment[size]; } }; }