com.chen.mail.providers.Attachment.java Source code

Java tutorial

Introduction

Here is the source code for com.chen.mail.providers.Attachment.java

Source

/**
 * 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.chen.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.chen.emailcommon.internet.MimeUtility;
import com.chen.emailcommon.mail.MessagingException;
import com.chen.emailcommon.mail.Part;
import com.chen.mail.browse.MessageAttachmentBar;
import com.chen.mail.utils.LogTag;
import com.chen.mail.utils.LogUtils;
import com.chen.mail.utils.MimeType;
import com.chen.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 UIProvider.AttachmentColumns#NAME} Use {@link #setName(String)}.
     */
    private String name;

    /**
     * Attachment size in bytes. See {@link UIProvider.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 UIProvider.AttachmentColumns#URI
     */
    public Uri uri;

    /**
     * MIME type of the file. Use {@link #getContentType()} and {@link #setContentType(String)}.
     *
     * @see UIProvider.AttachmentColumns#CONTENT_TYPE
     */
    private String contentType;
    private String inferredContentType;

    /**
     * Use {@link #setState(int)}
     *
     * @see UIProvider.AttachmentColumns#STATE
     */
    public int state;

    /**
     * @see UIProvider.AttachmentColumns#DESTINATION
     */
    public int destination;

    /**
     * @see UIProvider.AttachmentColumns#DOWNLOADED_SIZE
     */
    public int downloadedSize;

    /**
     * Shareable, openable uri for this attachment
     * <p>
     * content:// Gmail.getAttachmentDefaultUri() if origin is SERVER_ATTACHMENT
     * <p>
     * content:// uri pointing to the content to be uploaded if origin is
     * LOCAL_FILE
     * <p>
     * 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 UIProvider.AttachmentColumns#CONTENT_URI
     */
    public Uri contentUri;

    /**
     * Might be null.
     *
     * @see UIProvider.AttachmentColumns#THUMBNAIL_URI
     */
    public Uri thumbnailUri;

    /**
     * Might be null.
     *
     * @see UIProvider.AttachmentColumns#PREVIEW_INTENT_URI
     */
    public Uri previewIntentUri;

    /**
     * The visibility type of this attachment.
     *
     * @see UIProvider.AttachmentColumns#TYPE
     */
    public int type;

    public int flags;

    /**
     * Might be null. JSON string.
     *
     * @see UIProvider.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(UIProvider.AttachmentColumns.NAME));
        size = cursor.getInt(cursor.getColumnIndex(UIProvider.AttachmentColumns.SIZE));
        uri = Uri.parse(cursor.getString(cursor.getColumnIndex(UIProvider.AttachmentColumns.URI)));
        contentType = cursor.getString(cursor.getColumnIndex(UIProvider.AttachmentColumns.CONTENT_TYPE));
        state = cursor.getInt(cursor.getColumnIndex(UIProvider.AttachmentColumns.STATE));
        destination = cursor.getInt(cursor.getColumnIndex(UIProvider.AttachmentColumns.DESTINATION));
        downloadedSize = cursor.getInt(cursor.getColumnIndex(UIProvider.AttachmentColumns.DOWNLOADED_SIZE));
        contentUri = parseOptionalUri(
                cursor.getString(cursor.getColumnIndex(UIProvider.AttachmentColumns.CONTENT_URI)));
        thumbnailUri = parseOptionalUri(
                cursor.getString(cursor.getColumnIndex(UIProvider.AttachmentColumns.THUMBNAIL_URI)));
        previewIntentUri = parseOptionalUri(
                cursor.getString(cursor.getColumnIndex(UIProvider.AttachmentColumns.PREVIEW_INTENT_URI)));
        providerData = cursor.getString(cursor.getColumnIndex(UIProvider.AttachmentColumns.PROVIDER_DATA));
        supportsDownloadAgain = cursor
                .getInt(cursor.getColumnIndex(UIProvider.AttachmentColumns.SUPPORTS_DOWNLOAD_AGAIN)) == 1;
        type = cursor.getInt(cursor.getColumnIndex(UIProvider.AttachmentColumns.TYPE));
        flags = cursor.getInt(cursor.getColumnIndex(UIProvider.AttachmentColumns.FLAGS));
    }

    public Attachment(JSONObject srcJson) {
        name = srcJson.optString(UIProvider.AttachmentColumns.NAME, null);
        size = srcJson.optInt(UIProvider.AttachmentColumns.SIZE);
        uri = parseOptionalUri(srcJson, UIProvider.AttachmentColumns.URI);
        contentType = srcJson.optString(UIProvider.AttachmentColumns.CONTENT_TYPE, null);
        state = srcJson.optInt(UIProvider.AttachmentColumns.STATE);
        destination = srcJson.optInt(UIProvider.AttachmentColumns.DESTINATION);
        downloadedSize = srcJson.optInt(UIProvider.AttachmentColumns.DOWNLOADED_SIZE);
        contentUri = parseOptionalUri(srcJson, UIProvider.AttachmentColumns.CONTENT_URI);
        thumbnailUri = parseOptionalUri(srcJson, UIProvider.AttachmentColumns.THUMBNAIL_URI);
        previewIntentUri = parseOptionalUri(srcJson, UIProvider.AttachmentColumns.PREVIEW_INTENT_URI);
        providerData = srcJson.optString(UIProvider.AttachmentColumns.PROVIDER_DATA);
        supportsDownloadAgain = srcJson.optBoolean(UIProvider.AttachmentColumns.SUPPORTS_DOWNLOAD_AGAIN, true);
        type = srcJson.optInt(UIProvider.AttachmentColumns.TYPE);
        flags = srcJson.optInt(UIProvider.AttachmentColumns.FLAGS);
    }

    /**
     * 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 = UIProvider.AttachmentState.SAVED;
            providerData = null;
            supportsDownloadAgain = false;
            destination = UIProvider.AttachmentDestination.CACHE;
            type = UIProvider.AttachmentType.STANDARD;
            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 UIProvider.AttachmentColumns}.
     */
    public Attachment(ContentValues values) {
        name = values.getAsString(UIProvider.AttachmentColumns.NAME);
        size = values.getAsInteger(UIProvider.AttachmentColumns.SIZE);
        uri = parseOptionalUri(values.getAsString(UIProvider.AttachmentColumns.URI));
        contentType = values.getAsString(UIProvider.AttachmentColumns.CONTENT_TYPE);
        state = values.getAsInteger(UIProvider.AttachmentColumns.STATE);
        destination = values.getAsInteger(UIProvider.AttachmentColumns.DESTINATION);
        downloadedSize = values.getAsInteger(UIProvider.AttachmentColumns.DOWNLOADED_SIZE);
        contentUri = parseOptionalUri(values.getAsString(UIProvider.AttachmentColumns.CONTENT_URI));
        thumbnailUri = parseOptionalUri(values.getAsString(UIProvider.AttachmentColumns.THUMBNAIL_URI));
        previewIntentUri = parseOptionalUri(values.getAsString(UIProvider.AttachmentColumns.PREVIEW_INTENT_URI));
        providerData = values.getAsString(UIProvider.AttachmentColumns.PROVIDER_DATA);
        supportsDownloadAgain = values.getAsBoolean(UIProvider.AttachmentColumns.SUPPORTS_DOWNLOAD_AGAIN);
        type = values.getAsInteger(UIProvider.AttachmentColumns.TYPE);
        flags = values.getAsInteger(UIProvider.AttachmentColumns.FLAGS);
    }

    /**
     * Returns the various attachment fields in a {@link ContentValues} object.
     * The keys for each field should be {@link UIProvider.AttachmentColumns}.
     */
    public ContentValues toContentValues() {
        final ContentValues values = new ContentValues(12);

        values.put(UIProvider.AttachmentColumns.NAME, name);
        values.put(UIProvider.AttachmentColumns.SIZE, size);
        values.put(UIProvider.AttachmentColumns.URI, uri.toString());
        values.put(UIProvider.AttachmentColumns.CONTENT_TYPE, contentType);
        values.put(UIProvider.AttachmentColumns.STATE, state);
        values.put(UIProvider.AttachmentColumns.DESTINATION, destination);
        values.put(UIProvider.AttachmentColumns.DOWNLOADED_SIZE, downloadedSize);
        values.put(UIProvider.AttachmentColumns.CONTENT_URI, contentUri.toString());
        values.put(UIProvider.AttachmentColumns.THUMBNAIL_URI, thumbnailUri.toString());
        values.put(UIProvider.AttachmentColumns.PREVIEW_INTENT_URI,
                previewIntentUri == null ? null : previewIntentUri.toString());
        values.put(UIProvider.AttachmentColumns.PROVIDER_DATA, providerData);
        values.put(UIProvider.AttachmentColumns.SUPPORTS_DOWNLOAD_AGAIN, supportsDownloadAgain);
        values.put(UIProvider.AttachmentColumns.TYPE, type);
        values.put(UIProvider.AttachmentColumns.FLAGS, flags);

        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(UIProvider.AttachmentColumns.NAME, name);
        result.put(UIProvider.AttachmentColumns.SIZE, size);
        result.put(UIProvider.AttachmentColumns.URI, stringify(uri));
        result.put(UIProvider.AttachmentColumns.CONTENT_TYPE, contentType);
        result.put(UIProvider.AttachmentColumns.STATE, state);
        result.put(UIProvider.AttachmentColumns.DESTINATION, destination);
        result.put(UIProvider.AttachmentColumns.DOWNLOADED_SIZE, downloadedSize);
        result.put(UIProvider.AttachmentColumns.CONTENT_URI, stringify(contentUri));
        result.put(UIProvider.AttachmentColumns.THUMBNAIL_URI, stringify(thumbnailUri));
        result.put(UIProvider.AttachmentColumns.PREVIEW_INTENT_URI, stringify(previewIntentUri));
        result.put(UIProvider.AttachmentColumns.PROVIDER_DATA, providerData);
        result.put(UIProvider.AttachmentColumns.SUPPORTS_DOWNLOAD_AGAIN, supportsDownloadAgain);
        result.put(UIProvider.AttachmentColumns.TYPE, type);
        result.put(UIProvider.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(UIProvider.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 == UIProvider.AttachmentState.SAVED;
    }

    public boolean canSave() {
        return !isSavedToExternal() && !isInstallable() && !MimeType.isBlocked(getContentType());
    }

    public boolean canShare() {
        return isPresentLocally() && contentUri != null;
    }

    public boolean isDownloading() {
        return state == UIProvider.AttachmentState.DOWNLOADING || state == UIProvider.AttachmentState.PAUSED;
    }

    public boolean isSavedToExternal() {
        return state == UIProvider.AttachmentState.SAVED
                && destination == UIProvider.AttachmentDestination.EXTERNAL;
    }

    public boolean isInstallable() {
        return MimeType.isInstallable(getContentType());
    }

    public boolean shouldShowProgress() {
        return (state == UIProvider.AttachmentState.DOWNLOADING || state == UIProvider.AttachmentState.PAUSED)
                && size > 0 && downloadedSize > 0 && downloadedSize <= size;
    }

    public boolean isDownloadFailed() {
        return state == UIProvider.AttachmentState.FAILED;
    }

    public boolean isDownloadFinishedOrFailed() {
        return state == UIProvider.AttachmentState.FAILED || state == UIProvider.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 UIProvider.AttachmentRendition.BEST:
            uri = contentUri;
            break;
        case UIProvider.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 == UIProvider.AttachmentState.FAILED || state == UIProvider.AttachmentState.NOT_SAVED) {
            this.downloadedSize = 0;
        }
    }

    @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<? extends Attachment> 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<Attachment> fromJSONArray(String jsonArrayStr) {
        final List<Attachment> 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 == UIProvider.AttachmentRendition.SIMPLE) {
            // implicit shift of 0 finds the SIMPLE rendition bit
            return (previewStates & mask) != 0;
        } else if (rendition == UIProvider.AttachmentRendition.BEST) {
            // shift of 1 finds the BEST rendition bit
            return (previewStates & (mask << 1)) != 0;
        } else {
            return false;
        }
    }

    public static final Creator<Attachment> CREATOR = new Creator<Attachment>() {
        @Override
        public Attachment createFromParcel(Parcel source) {
            return new Attachment(source);
        }

        @Override
        public Attachment[] newArray(int size) {
            return new Attachment[size];
        }
    };
}