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