Home | History | Annotate | Download | only in providers
      1 /**
      2  * Copyright (c) 2011, Google Inc.
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *     http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.mail.providers;
     18 
     19 import android.content.ContentResolver;
     20 import android.content.ContentValues;
     21 import android.content.Context;
     22 import android.database.Cursor;
     23 import android.net.Uri;
     24 import android.os.Parcel;
     25 import android.os.Parcelable;
     26 import android.text.TextUtils;
     27 
     28 import com.android.emailcommon.internet.MimeUtility;
     29 import com.android.emailcommon.mail.MessagingException;
     30 import com.android.emailcommon.mail.Part;
     31 import com.android.mail.browse.MessageAttachmentBar;
     32 import com.android.mail.providers.UIProvider.AttachmentColumns;
     33 import com.android.mail.providers.UIProvider.AttachmentDestination;
     34 import com.android.mail.providers.UIProvider.AttachmentRendition;
     35 import com.android.mail.providers.UIProvider.AttachmentState;
     36 import com.android.mail.providers.UIProvider.AttachmentType;
     37 import com.android.mail.utils.LogTag;
     38 import com.android.mail.utils.LogUtils;
     39 import com.android.mail.utils.MimeType;
     40 import com.android.mail.utils.Utils;
     41 import com.google.common.collect.Lists;
     42 
     43 import org.apache.commons.io.IOUtils;
     44 import org.json.JSONArray;
     45 import org.json.JSONException;
     46 import org.json.JSONObject;
     47 
     48 import java.io.FileNotFoundException;
     49 import java.io.IOException;
     50 import java.io.InputStream;
     51 import java.io.OutputStream;
     52 import java.util.Collection;
     53 import java.util.List;
     54 
     55 public class Attachment implements Parcelable {
     56     public static final int MAX_ATTACHMENT_PREVIEWS = 2;
     57     public static final String LOG_TAG = LogTag.getLogTag();
     58     /**
     59      * Workaround for b/8070022 so that appending a null partId to the end of a
     60      * uri wouldn't remove the trailing backslash
     61      */
     62     public static final String EMPTY_PART_ID = "empty";
     63 
     64     // Indicates that this is a dummy placeholder attachment.
     65     public static final int FLAG_DUMMY_ATTACHMENT = 1<<10;
     66 
     67     /**
     68      * Part id of the attachment.
     69      */
     70     public String partId;
     71 
     72     /**
     73      * Attachment file name. See {@link AttachmentColumns#NAME} Use {@link #setName(String)}.
     74      */
     75     private String name;
     76 
     77     /**
     78      * Attachment size in bytes. See {@link AttachmentColumns#SIZE}.
     79      */
     80     public int size;
     81 
     82     /**
     83      * The provider-generated URI for this Attachment. Must be globally unique.
     84      * For local attachments generated by the Compose UI prior to send/save,
     85      * this field will be null.
     86      *
     87      * @see AttachmentColumns#URI
     88      */
     89     public Uri uri;
     90 
     91     /**
     92      * MIME type of the file. Use {@link #getContentType()} and {@link #setContentType(String)}.
     93      *
     94      * @see AttachmentColumns#CONTENT_TYPE
     95      */
     96     private String contentType;
     97     private String inferredContentType;
     98 
     99     /**
    100      * Use {@link #setState(int)}
    101      *
    102      * @see AttachmentColumns#STATE
    103      */
    104     public int state;
    105 
    106     /**
    107      * @see AttachmentColumns#DESTINATION
    108      */
    109     public int destination;
    110 
    111     /**
    112      * @see AttachmentColumns#DOWNLOADED_SIZE
    113      */
    114     public int downloadedSize;
    115 
    116     /**
    117      * Shareable, openable uri for this attachment
    118      * <p>
    119      * content:// Gmail.getAttachmentDefaultUri() if origin is SERVER_ATTACHMENT
    120      * <p>
    121      * content:// uri pointing to the content to be uploaded if origin is
    122      * LOCAL_FILE
    123      * <p>
    124      * file:// uri pointing to an EXTERNAL apk file. The package manager only
    125      * handles file:// uris not content:// uris. We do the same workaround in
    126      * {@link MessageAttachmentBar#onClick(android.view.View)} and
    127      * UiProvider#getUiAttachmentsCursorForUIAttachments().
    128      *
    129      * @see AttachmentColumns#CONTENT_URI
    130      */
    131     public Uri contentUri;
    132 
    133     /**
    134      * Might be null.
    135      *
    136      * @see AttachmentColumns#THUMBNAIL_URI
    137      */
    138     public Uri thumbnailUri;
    139 
    140     /**
    141      * Might be null.
    142      *
    143      * @see AttachmentColumns#PREVIEW_INTENT_URI
    144      */
    145     public Uri previewIntentUri;
    146 
    147     /**
    148      * The visibility type of this attachment.
    149      *
    150      * @see AttachmentColumns#TYPE
    151      */
    152     public int type;
    153 
    154     public int flags;
    155 
    156     /**
    157      * Might be null. JSON string.
    158      *
    159      * @see AttachmentColumns#PROVIDER_DATA
    160      */
    161     public String providerData;
    162 
    163     /**
    164      * Streamable mime type of the attachment in case it's a virtual file.
    165      *
    166      * Might be null. If null, then the default type (contentType) is assumed
    167      * to be streamable.
    168      */
    169     public String virtualMimeType;
    170 
    171     private transient Uri mIdentifierUri;
    172 
    173     /**
    174      * True if this attachment can be downloaded again.
    175      */
    176     private boolean supportsDownloadAgain;
    177 
    178 
    179     public Attachment() {
    180     }
    181 
    182     public Attachment(Parcel in) {
    183         name = in.readString();
    184         size = in.readInt();
    185         uri = in.readParcelable(null);
    186         contentType = in.readString();
    187         state = in.readInt();
    188         destination = in.readInt();
    189         downloadedSize = in.readInt();
    190         contentUri = in.readParcelable(null);
    191         thumbnailUri = in.readParcelable(null);
    192         previewIntentUri = in.readParcelable(null);
    193         providerData = in.readString();
    194         supportsDownloadAgain = in.readInt() == 1;
    195         type = in.readInt();
    196         flags = in.readInt();
    197         virtualMimeType = in.readString();
    198     }
    199 
    200     public Attachment(Cursor cursor) {
    201         if (cursor == null) {
    202             return;
    203         }
    204 
    205         name = cursor.getString(cursor.getColumnIndex(AttachmentColumns.NAME));
    206         size = cursor.getInt(cursor.getColumnIndex(AttachmentColumns.SIZE));
    207         uri = Uri.parse(cursor.getString(cursor.getColumnIndex(AttachmentColumns.URI)));
    208         contentType = cursor.getString(cursor.getColumnIndex(AttachmentColumns.CONTENT_TYPE));
    209         state = cursor.getInt(cursor.getColumnIndex(AttachmentColumns.STATE));
    210         destination = cursor.getInt(cursor.getColumnIndex(AttachmentColumns.DESTINATION));
    211         downloadedSize = cursor.getInt(cursor.getColumnIndex(AttachmentColumns.DOWNLOADED_SIZE));
    212         contentUri = parseOptionalUri(
    213                 cursor.getString(cursor.getColumnIndex(AttachmentColumns.CONTENT_URI)));
    214         thumbnailUri = parseOptionalUri(
    215                 cursor.getString(cursor.getColumnIndex(AttachmentColumns.THUMBNAIL_URI)));
    216         previewIntentUri = parseOptionalUri(
    217                 cursor.getString(cursor.getColumnIndex(AttachmentColumns.PREVIEW_INTENT_URI)));
    218         providerData = cursor.getString(cursor.getColumnIndex(AttachmentColumns.PROVIDER_DATA));
    219         supportsDownloadAgain = cursor.getInt(
    220                 cursor.getColumnIndex(AttachmentColumns.SUPPORTS_DOWNLOAD_AGAIN)) == 1;
    221         type = cursor.getInt(cursor.getColumnIndex(AttachmentColumns.TYPE));
    222         flags = cursor.getInt(cursor.getColumnIndex(AttachmentColumns.FLAGS));
    223         virtualMimeType = cursor.getString(cursor.getColumnIndex(AttachmentColumns.VIRTUAL_MIME_TYPE));
    224     }
    225 
    226     public Attachment(JSONObject srcJson) {
    227         name = srcJson.optString(AttachmentColumns.NAME, null);
    228         size = srcJson.optInt(AttachmentColumns.SIZE);
    229         uri = parseOptionalUri(srcJson, AttachmentColumns.URI);
    230         contentType = srcJson.optString(AttachmentColumns.CONTENT_TYPE, null);
    231         state = srcJson.optInt(AttachmentColumns.STATE);
    232         destination = srcJson.optInt(AttachmentColumns.DESTINATION);
    233         downloadedSize = srcJson.optInt(AttachmentColumns.DOWNLOADED_SIZE);
    234         contentUri = parseOptionalUri(srcJson, AttachmentColumns.CONTENT_URI);
    235         thumbnailUri = parseOptionalUri(srcJson, AttachmentColumns.THUMBNAIL_URI);
    236         previewIntentUri = parseOptionalUri(srcJson, AttachmentColumns.PREVIEW_INTENT_URI);
    237         providerData = srcJson.optString(AttachmentColumns.PROVIDER_DATA);
    238         supportsDownloadAgain = srcJson.optBoolean(AttachmentColumns.SUPPORTS_DOWNLOAD_AGAIN, true);
    239         type = srcJson.optInt(AttachmentColumns.TYPE);
    240         flags = srcJson.optInt(AttachmentColumns.FLAGS);
    241         virtualMimeType = srcJson.optString(AttachmentColumns.VIRTUAL_MIME_TYPE, null);
    242     }
    243 
    244     /**
    245      * Constructor for use when creating attachments in eml files.
    246      */
    247     public Attachment(Context context, Part part, Uri emlFileUri, String messageId, String cid,
    248                       boolean inline) {
    249         try {
    250             // Transfer fields from mime format to provider format
    251             final String contentTypeHeader = MimeUtility.unfoldAndDecode(part.getContentType());
    252             name = MimeUtility.getHeaderParameter(contentTypeHeader, "name");
    253             if (name == null) {
    254                 final String contentDisposition =
    255                         MimeUtility.unfoldAndDecode(part.getDisposition());
    256                 name = MimeUtility.getHeaderParameter(contentDisposition, "filename");
    257             }
    258 
    259             // Prevent passing in a file path as part of the name.
    260             if (name != null) {
    261                 name = name.replace('/', '_');
    262             }
    263 
    264             contentType = MimeType.inferMimeType(name, part.getMimeType());
    265             uri = EmlAttachmentProvider.getAttachmentUri(emlFileUri, messageId, cid);
    266             contentUri = uri;
    267             thumbnailUri = uri;
    268             previewIntentUri = null;
    269             state = AttachmentState.SAVED;
    270             providerData = null;
    271             supportsDownloadAgain = false;
    272             destination = AttachmentDestination.CACHE;
    273             type = inline ? AttachmentType.INLINE_CURRENT_MESSAGE : AttachmentType.STANDARD;
    274             partId = cid;
    275             flags = 0;
    276             virtualMimeType = null;
    277 
    278             // insert attachment into content provider so that we can open the file
    279             final ContentResolver resolver = context.getContentResolver();
    280             resolver.insert(uri, toContentValues());
    281 
    282             // save the file in the cache
    283             try {
    284                 final InputStream in = part.getBody().getInputStream();
    285                 final OutputStream out = resolver.openOutputStream(uri, "rwt");
    286                 size = IOUtils.copy(in, out);
    287                 downloadedSize = size;
    288                 in.close();
    289                 out.close();
    290             } catch (FileNotFoundException e) {
    291                 LogUtils.e(LOG_TAG, e, "Error in writing attachment to cache");
    292             } catch (IOException e) {
    293                 LogUtils.e(LOG_TAG, e, "Error in writing attachment to cache");
    294             }
    295             // perform a second insert to put the updated size and downloaded size values in
    296             resolver.insert(uri, toContentValues());
    297         } catch (MessagingException e) {
    298             LogUtils.e(LOG_TAG, e, "Error parsing eml attachment");
    299         }
    300     }
    301 
    302     /**
    303      * Create an attachment from a {@link ContentValues} object.
    304      * The keys should be {@link AttachmentColumns}.
    305      */
    306     public Attachment(ContentValues values) {
    307         name = values.getAsString(AttachmentColumns.NAME);
    308         size = values.getAsInteger(AttachmentColumns.SIZE);
    309         uri = parseOptionalUri(values.getAsString(AttachmentColumns.URI));
    310         contentType = values.getAsString(AttachmentColumns.CONTENT_TYPE);
    311         state = values.getAsInteger(AttachmentColumns.STATE);
    312         destination = values.getAsInteger(AttachmentColumns.DESTINATION);
    313         downloadedSize = values.getAsInteger(AttachmentColumns.DOWNLOADED_SIZE);
    314         contentUri = parseOptionalUri(values.getAsString(AttachmentColumns.CONTENT_URI));
    315         thumbnailUri = parseOptionalUri(values.getAsString(AttachmentColumns.THUMBNAIL_URI));
    316         previewIntentUri =
    317                 parseOptionalUri(values.getAsString(AttachmentColumns.PREVIEW_INTENT_URI));
    318         providerData = values.getAsString(AttachmentColumns.PROVIDER_DATA);
    319         supportsDownloadAgain = values.getAsBoolean(AttachmentColumns.SUPPORTS_DOWNLOAD_AGAIN);
    320         type = values.getAsInteger(AttachmentColumns.TYPE);
    321         flags = values.getAsInteger(AttachmentColumns.FLAGS);
    322         partId = values.getAsString(AttachmentColumns.CONTENT_ID);
    323         virtualMimeType = values.getAsString(AttachmentColumns.VIRTUAL_MIME_TYPE);
    324     }
    325 
    326     /**
    327      * Returns the various attachment fields in a {@link ContentValues} object.
    328      * The keys for each field should be {@link AttachmentColumns}.
    329      */
    330     public ContentValues toContentValues() {
    331         final ContentValues values = new ContentValues(12);
    332 
    333         values.put(AttachmentColumns.NAME, name);
    334         values.put(AttachmentColumns.SIZE, size);
    335         values.put(AttachmentColumns.URI, uri.toString());
    336         values.put(AttachmentColumns.CONTENT_TYPE, contentType);
    337         values.put(AttachmentColumns.STATE, state);
    338         values.put(AttachmentColumns.DESTINATION, destination);
    339         values.put(AttachmentColumns.DOWNLOADED_SIZE, downloadedSize);
    340         values.put(AttachmentColumns.CONTENT_URI, contentUri.toString());
    341         values.put(AttachmentColumns.THUMBNAIL_URI, thumbnailUri.toString());
    342         values.put(AttachmentColumns.PREVIEW_INTENT_URI,
    343                 previewIntentUri == null ? null : previewIntentUri.toString());
    344         values.put(AttachmentColumns.PROVIDER_DATA, providerData);
    345         values.put(AttachmentColumns.SUPPORTS_DOWNLOAD_AGAIN, supportsDownloadAgain);
    346         values.put(AttachmentColumns.TYPE, type);
    347         values.put(AttachmentColumns.FLAGS, flags);
    348         values.put(AttachmentColumns.CONTENT_ID, partId);
    349         values.put(AttachmentColumns.VIRTUAL_MIME_TYPE, virtualMimeType);
    350 
    351         return values;
    352     }
    353 
    354     @Override
    355     public void writeToParcel(Parcel dest, int flags) {
    356         dest.writeString(name);
    357         dest.writeInt(size);
    358         dest.writeParcelable(uri, flags);
    359         dest.writeString(contentType);
    360         dest.writeInt(state);
    361         dest.writeInt(destination);
    362         dest.writeInt(downloadedSize);
    363         dest.writeParcelable(contentUri, flags);
    364         dest.writeParcelable(thumbnailUri, flags);
    365         dest.writeParcelable(previewIntentUri, flags);
    366         dest.writeString(providerData);
    367         dest.writeInt(supportsDownloadAgain ? 1 : 0);
    368         dest.writeInt(type);
    369         dest.writeInt(flags);
    370         dest.writeString(virtualMimeType);
    371     }
    372 
    373     public JSONObject toJSON() throws JSONException {
    374         final JSONObject result = new JSONObject();
    375 
    376         result.put(AttachmentColumns.NAME, name);
    377         result.put(AttachmentColumns.SIZE, size);
    378         result.put(AttachmentColumns.URI, stringify(uri));
    379         result.put(AttachmentColumns.CONTENT_TYPE, contentType);
    380         result.put(AttachmentColumns.STATE, state);
    381         result.put(AttachmentColumns.DESTINATION, destination);
    382         result.put(AttachmentColumns.DOWNLOADED_SIZE, downloadedSize);
    383         result.put(AttachmentColumns.CONTENT_URI, stringify(contentUri));
    384         result.put(AttachmentColumns.THUMBNAIL_URI, stringify(thumbnailUri));
    385         result.put(AttachmentColumns.PREVIEW_INTENT_URI, stringify(previewIntentUri));
    386         result.put(AttachmentColumns.PROVIDER_DATA, providerData);
    387         result.put(AttachmentColumns.SUPPORTS_DOWNLOAD_AGAIN, supportsDownloadAgain);
    388         result.put(AttachmentColumns.TYPE, type);
    389         result.put(AttachmentColumns.FLAGS, flags);
    390         result.put(AttachmentColumns.VIRTUAL_MIME_TYPE, virtualMimeType);
    391 
    392         return result;
    393     }
    394 
    395     @Override
    396     public String toString() {
    397         try {
    398             final JSONObject jsonObject = toJSON();
    399             // Add some additional fields that are helpful when debugging issues
    400             jsonObject.put("partId", partId);
    401             if (providerData != null) {
    402                 try {
    403                     // pretty print the provider data
    404                     jsonObject.put(AttachmentColumns.PROVIDER_DATA, new JSONObject(providerData));
    405                 } catch (JSONException e) {
    406                     LogUtils.e(LOG_TAG, e, "JSONException when adding provider data");
    407                 }
    408             }
    409             return jsonObject.toString(4);
    410         } catch (JSONException e) {
    411             LogUtils.e(LOG_TAG, e, "JSONException in toString");
    412             return super.toString();
    413         }
    414     }
    415 
    416     private static String stringify(Object object) {
    417         return object != null ? object.toString() : null;
    418     }
    419 
    420     protected static Uri parseOptionalUri(String uriString) {
    421         return uriString == null ? null : Uri.parse(uriString);
    422     }
    423 
    424     protected static Uri parseOptionalUri(JSONObject srcJson, String key) {
    425         final String uriStr = srcJson.optString(key, null);
    426         return uriStr == null ? null : Uri.parse(uriStr);
    427     }
    428 
    429     @Override
    430     public int describeContents() {
    431         return 0;
    432     }
    433 
    434     public boolean isPresentLocally() {
    435         return state == AttachmentState.SAVED;
    436     }
    437 
    438     public boolean canSave() {
    439         return !isSavedToExternal() && !isInstallable();
    440     }
    441 
    442     public boolean canShare() {
    443         return isPresentLocally() && contentUri != null;
    444     }
    445 
    446     public boolean isDownloading() {
    447         return state == AttachmentState.DOWNLOADING || state == AttachmentState.PAUSED;
    448     }
    449 
    450     public boolean isSavedToExternal() {
    451         return state == AttachmentState.SAVED && destination == AttachmentDestination.EXTERNAL;
    452     }
    453 
    454     public boolean isInstallable() {
    455         return MimeType.isInstallable(getContentType());
    456     }
    457 
    458     public boolean shouldShowProgress() {
    459         return (state == AttachmentState.DOWNLOADING || state == AttachmentState.PAUSED)
    460                 && size > 0 && downloadedSize > 0 && downloadedSize <= size;
    461     }
    462 
    463     public boolean isDownloadFailed() {
    464         return state == AttachmentState.FAILED;
    465     }
    466 
    467     public boolean isDownloadFinishedOrFailed() {
    468         return state == AttachmentState.FAILED || state == AttachmentState.SAVED;
    469     }
    470 
    471     public boolean supportsDownloadAgain() {
    472         return supportsDownloadAgain;
    473     }
    474 
    475     public boolean canPreview() {
    476         return previewIntentUri != null;
    477     }
    478 
    479     /**
    480      * Returns a stable identifier URI for this attachment. TODO: make the uri
    481      * field stable, and put provider-specific opaque bits and bobs elsewhere
    482      */
    483     public Uri getIdentifierUri() {
    484         if (Utils.isEmpty(mIdentifierUri)) {
    485             mIdentifierUri = Utils.isEmpty(uri) ?
    486                     (Utils.isEmpty(contentUri) ? Uri.EMPTY : contentUri)
    487                     : uri.buildUpon().clearQuery().build();
    488         }
    489         return mIdentifierUri;
    490     }
    491 
    492     public String getContentType() {
    493         if (TextUtils.isEmpty(inferredContentType)) {
    494             inferredContentType = MimeType.inferMimeType(name, contentType);
    495         }
    496         return inferredContentType;
    497     }
    498 
    499     public Uri getUriForRendition(int rendition) {
    500         final Uri uri;
    501         switch (rendition) {
    502             case AttachmentRendition.BEST:
    503                 uri = contentUri;
    504                 break;
    505             case AttachmentRendition.SIMPLE:
    506                 uri = thumbnailUri;
    507                 break;
    508             default:
    509                 throw new IllegalArgumentException("invalid rendition: " + rendition);
    510         }
    511         return uri;
    512     }
    513 
    514     public void setContentType(String contentType) {
    515         if (!TextUtils.equals(this.contentType, contentType)) {
    516             this.inferredContentType = null;
    517             this.contentType = contentType;
    518         }
    519     }
    520 
    521     public String getName() {
    522         return name;
    523     }
    524 
    525     public boolean setName(String name) {
    526         if (!TextUtils.equals(this.name, name)) {
    527             this.inferredContentType = null;
    528             this.name = name;
    529             return true;
    530         }
    531         return false;
    532     }
    533 
    534     /**
    535      * Sets the attachment state. Side effect: sets downloadedSize
    536      */
    537     public void setState(int state) {
    538         this.state = state;
    539         if (state == AttachmentState.FAILED || state == AttachmentState.NOT_SAVED) {
    540             this.downloadedSize = 0;
    541         }
    542     }
    543 
    544     /**
    545      * @return {@code true} if the attachment is an inline attachment
    546      * that appears in the body of the message content (including possibly
    547      * quoted text).
    548      */
    549     public boolean isInlineAttachment() {
    550         return type != UIProvider.AttachmentType.STANDARD;
    551     }
    552 
    553     @Override
    554     public boolean equals(final Object o) {
    555         if (this == o) {
    556             return true;
    557         }
    558         if (o == null || getClass() != o.getClass()) {
    559             return false;
    560         }
    561 
    562         final Attachment that = (Attachment) o;
    563 
    564         if (destination != that.destination) {
    565             return false;
    566         }
    567         if (downloadedSize != that.downloadedSize) {
    568             return false;
    569         }
    570         if (size != that.size) {
    571             return false;
    572         }
    573         if (state != that.state) {
    574             return false;
    575         }
    576         if (supportsDownloadAgain != that.supportsDownloadAgain) {
    577             return false;
    578         }
    579         if (type != that.type) {
    580             return false;
    581         }
    582         if (contentType != null ? !contentType.equals(that.contentType)
    583                 : that.contentType != null) {
    584             return false;
    585         }
    586         if (contentUri != null ? !contentUri.equals(that.contentUri) : that.contentUri != null) {
    587             return false;
    588         }
    589         if (name != null ? !name.equals(that.name) : that.name != null) {
    590             return false;
    591         }
    592         if (partId != null ? !partId.equals(that.partId) : that.partId != null) {
    593             return false;
    594         }
    595         if (previewIntentUri != null ? !previewIntentUri.equals(that.previewIntentUri)
    596                 : that.previewIntentUri != null) {
    597             return false;
    598         }
    599         if (providerData != null ? !providerData.equals(that.providerData)
    600                 : that.providerData != null) {
    601             return false;
    602         }
    603         if (thumbnailUri != null ? !thumbnailUri.equals(that.thumbnailUri)
    604                 : that.thumbnailUri != null) {
    605             return false;
    606         }
    607         if (uri != null ? !uri.equals(that.uri) : that.uri != null) {
    608             return false;
    609         }
    610 
    611         return true;
    612     }
    613 
    614     @Override
    615     public int hashCode() {
    616         int result = partId != null ? partId.hashCode() : 0;
    617         result = 31 * result + (name != null ? name.hashCode() : 0);
    618         result = 31 * result + size;
    619         result = 31 * result + (uri != null ? uri.hashCode() : 0);
    620         result = 31 * result + (contentType != null ? contentType.hashCode() : 0);
    621         result = 31 * result + state;
    622         result = 31 * result + destination;
    623         result = 31 * result + downloadedSize;
    624         result = 31 * result + (contentUri != null ? contentUri.hashCode() : 0);
    625         result = 31 * result + (thumbnailUri != null ? thumbnailUri.hashCode() : 0);
    626         result = 31 * result + (previewIntentUri != null ? previewIntentUri.hashCode() : 0);
    627         result = 31 * result + type;
    628         result = 31 * result + (providerData != null ? providerData.hashCode() : 0);
    629         result = 31 * result + (supportsDownloadAgain ? 1 : 0);
    630         return result;
    631     }
    632 
    633     public static String toJSONArray(Collection<? extends Attachment> attachments) {
    634         if (attachments == null) {
    635             return null;
    636         }
    637         final JSONArray result = new JSONArray();
    638         try {
    639             for (Attachment attachment : attachments) {
    640                 result.put(attachment.toJSON());
    641             }
    642         } catch (JSONException e) {
    643             throw new IllegalArgumentException(e);
    644         }
    645         return result.toString();
    646     }
    647 
    648     public static List<Attachment> fromJSONArray(String jsonArrayStr) {
    649         final List<Attachment> results = Lists.newArrayList();
    650         if (jsonArrayStr != null) {
    651             try {
    652                 final JSONArray arr = new JSONArray(jsonArrayStr);
    653 
    654                 for (int i = 0; i < arr.length(); i++) {
    655                     results.add(new Attachment(arr.getJSONObject(i)));
    656                 }
    657 
    658             } catch (JSONException e) {
    659                 throw new IllegalArgumentException(e);
    660             }
    661         }
    662         return results;
    663     }
    664 
    665     private static final String SERVER_ATTACHMENT = "SERVER_ATTACHMENT";
    666     private static final String LOCAL_FILE = "LOCAL_FILE";
    667 
    668     public String toJoinedString() {
    669         return TextUtils.join(UIProvider.ATTACHMENT_INFO_DELIMITER, Lists.newArrayList(
    670                 partId == null ? "" : partId,
    671                 name == null ? ""
    672                         : name.replaceAll("[" + UIProvider.ATTACHMENT_INFO_DELIMITER
    673                                 + UIProvider.ATTACHMENT_INFO_SEPARATOR + "]", ""),
    674                 getContentType(),
    675                 String.valueOf(size),
    676                 getContentType(),
    677                 contentUri != null ? SERVER_ATTACHMENT : LOCAL_FILE,
    678                 contentUri != null ? contentUri.toString() : "",
    679                 "" /* cachedFileUri */,
    680                 String.valueOf(type)));
    681     }
    682 
    683     /**
    684      * For use with {@link UIProvider.ConversationColumns#ATTACHMENT_PREVIEW_STATES}.
    685      *
    686      * @param previewStates The packed int describing the states of multiple attachments.
    687      * @param attachmentIndex The index of the attachment to update.
    688      * @param rendition The rendition of that attachment to update.
    689      * @param downloaded Whether that specific rendition is downloaded.
    690      * @return A packed int describing the updated downloaded states of the multiple attachments.
    691      */
    692     public static int updatePreviewStates(int previewStates, int attachmentIndex, int rendition,
    693             boolean downloaded) {
    694         // find the bit that describes that specific attachment index and rendition
    695         int shift = attachmentIndex * 2 + rendition;
    696         int mask = 1 << shift;
    697         // update the packed int at that bit
    698         if (downloaded) {
    699             // turns that bit into a 1
    700             return previewStates | mask;
    701         } else {
    702             // turns that bit into a 0
    703             return previewStates & ~mask;
    704         }
    705     }
    706 
    707     /**
    708      * For use with {@link UIProvider.ConversationColumns#ATTACHMENT_PREVIEW_STATES}.
    709      *
    710      * @param previewStates The packed int describing the states of multiple attachments.
    711      * @param attachmentIndex The index of the attachment.
    712      * @param rendition The rendition of the attachment.
    713      * @return The downloaded state of that particular rendition of that particular attachment.
    714      */
    715     public static boolean getPreviewState(int previewStates, int attachmentIndex, int rendition) {
    716         // find the bit that describes that specific attachment index
    717         int shift = attachmentIndex * 2;
    718         int mask = 1 << shift;
    719 
    720         if (rendition == AttachmentRendition.SIMPLE) {
    721             // implicit shift of 0 finds the SIMPLE rendition bit
    722             return (previewStates & mask) != 0;
    723         } else if (rendition == AttachmentRendition.BEST) {
    724             // shift of 1 finds the BEST rendition bit
    725             return (previewStates & (mask << 1)) != 0;
    726         } else {
    727             return false;
    728         }
    729     }
    730 
    731     public static final Creator<Attachment> CREATOR = new Creator<Attachment>() {
    732             @Override
    733         public Attachment createFromParcel(Parcel source) {
    734             return new Attachment(source);
    735         }
    736 
    737             @Override
    738         public Attachment[] newArray(int size) {
    739             return new Attachment[size];
    740         }
    741     };
    742 }
    743