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
17package com.android.mail.providers;
18
19import android.content.ContentResolver;
20import android.content.ContentValues;
21import android.content.Context;
22import android.database.Cursor;
23import android.net.Uri;
24import android.os.Parcel;
25import android.os.Parcelable;
26import android.text.TextUtils;
27
28import com.android.emailcommon.internet.MimeUtility;
29import com.android.emailcommon.mail.MessagingException;
30import com.android.emailcommon.mail.Part;
31import com.android.mail.browse.MessageAttachmentBar;
32import com.android.mail.providers.UIProvider.AttachmentColumns;
33import com.android.mail.providers.UIProvider.AttachmentDestination;
34import com.android.mail.providers.UIProvider.AttachmentRendition;
35import com.android.mail.providers.UIProvider.AttachmentState;
36import com.android.mail.providers.UIProvider.AttachmentType;
37import com.android.mail.utils.LogTag;
38import com.android.mail.utils.LogUtils;
39import com.android.mail.utils.MimeType;
40import com.android.mail.utils.Utils;
41import com.google.common.collect.Lists;
42
43import org.apache.commons.io.IOUtils;
44import org.json.JSONArray;
45import org.json.JSONException;
46import org.json.JSONObject;
47
48import java.io.FileNotFoundException;
49import java.io.IOException;
50import java.io.InputStream;
51import java.io.OutputStream;
52import java.util.Collection;
53import java.util.List;
54
55public 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    private transient Uri mIdentifierUri;
164
165    /**
166     * True if this attachment can be downloaded again.
167     */
168    private boolean supportsDownloadAgain;
169
170
171    public Attachment() {
172    }
173
174    public Attachment(Parcel in) {
175        name = in.readString();
176        size = in.readInt();
177        uri = in.readParcelable(null);
178        contentType = in.readString();
179        state = in.readInt();
180        destination = in.readInt();
181        downloadedSize = in.readInt();
182        contentUri = in.readParcelable(null);
183        thumbnailUri = in.readParcelable(null);
184        previewIntentUri = in.readParcelable(null);
185        providerData = in.readString();
186        supportsDownloadAgain = in.readInt() == 1;
187        type = in.readInt();
188        flags = in.readInt();
189    }
190
191    public Attachment(Cursor cursor) {
192        if (cursor == null) {
193            return;
194        }
195
196        name = cursor.getString(cursor.getColumnIndex(AttachmentColumns.NAME));
197        size = cursor.getInt(cursor.getColumnIndex(AttachmentColumns.SIZE));
198        uri = Uri.parse(cursor.getString(cursor.getColumnIndex(AttachmentColumns.URI)));
199        contentType = cursor.getString(cursor.getColumnIndex(AttachmentColumns.CONTENT_TYPE));
200        state = cursor.getInt(cursor.getColumnIndex(AttachmentColumns.STATE));
201        destination = cursor.getInt(cursor.getColumnIndex(AttachmentColumns.DESTINATION));
202        downloadedSize = cursor.getInt(cursor.getColumnIndex(AttachmentColumns.DOWNLOADED_SIZE));
203        contentUri = parseOptionalUri(
204                cursor.getString(cursor.getColumnIndex(AttachmentColumns.CONTENT_URI)));
205        thumbnailUri = parseOptionalUri(
206                cursor.getString(cursor.getColumnIndex(AttachmentColumns.THUMBNAIL_URI)));
207        previewIntentUri = parseOptionalUri(
208                cursor.getString(cursor.getColumnIndex(AttachmentColumns.PREVIEW_INTENT_URI)));
209        providerData = cursor.getString(cursor.getColumnIndex(AttachmentColumns.PROVIDER_DATA));
210        supportsDownloadAgain = cursor.getInt(
211                cursor.getColumnIndex(AttachmentColumns.SUPPORTS_DOWNLOAD_AGAIN)) == 1;
212        type = cursor.getInt(cursor.getColumnIndex(AttachmentColumns.TYPE));
213        flags = cursor.getInt(cursor.getColumnIndex(AttachmentColumns.FLAGS));
214    }
215
216    public Attachment(JSONObject srcJson) {
217        name = srcJson.optString(AttachmentColumns.NAME, null);
218        size = srcJson.optInt(AttachmentColumns.SIZE);
219        uri = parseOptionalUri(srcJson, AttachmentColumns.URI);
220        contentType = srcJson.optString(AttachmentColumns.CONTENT_TYPE, null);
221        state = srcJson.optInt(AttachmentColumns.STATE);
222        destination = srcJson.optInt(AttachmentColumns.DESTINATION);
223        downloadedSize = srcJson.optInt(AttachmentColumns.DOWNLOADED_SIZE);
224        contentUri = parseOptionalUri(srcJson, AttachmentColumns.CONTENT_URI);
225        thumbnailUri = parseOptionalUri(srcJson, AttachmentColumns.THUMBNAIL_URI);
226        previewIntentUri = parseOptionalUri(srcJson, AttachmentColumns.PREVIEW_INTENT_URI);
227        providerData = srcJson.optString(AttachmentColumns.PROVIDER_DATA);
228        supportsDownloadAgain = srcJson.optBoolean(AttachmentColumns.SUPPORTS_DOWNLOAD_AGAIN, true);
229        type = srcJson.optInt(AttachmentColumns.TYPE);
230        flags = srcJson.optInt(AttachmentColumns.FLAGS);
231    }
232
233    /**
234     * Constructor for use when creating attachments in eml files.
235     */
236    public Attachment(Context context, Part part, Uri emlFileUri, String messageId, String cid,
237                      boolean inline) {
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, cid);
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 = inline ? AttachmentType.INLINE_CURRENT_MESSAGE : AttachmentType.STANDARD;
258            partId = cid;
259            flags = 0;
260
261            // insert attachment into content provider so that we can open the file
262            final ContentResolver resolver = context.getContentResolver();
263            resolver.insert(uri, toContentValues());
264
265            // save the file in the cache
266            try {
267                final InputStream in = part.getBody().getInputStream();
268                final OutputStream out = resolver.openOutputStream(uri, "rwt");
269                size = IOUtils.copy(in, out);
270                downloadedSize = size;
271                in.close();
272                out.close();
273            } catch (FileNotFoundException e) {
274                LogUtils.e(LOG_TAG, e, "Error in writing attachment to cache");
275            } catch (IOException e) {
276                LogUtils.e(LOG_TAG, e, "Error in writing attachment to cache");
277            }
278            // perform a second insert to put the updated size and downloaded size values in
279            resolver.insert(uri, toContentValues());
280        } catch (MessagingException e) {
281            LogUtils.e(LOG_TAG, e, "Error parsing eml attachment");
282        }
283    }
284
285    /**
286     * Create an attachment from a {@link ContentValues} object.
287     * The keys should be {@link AttachmentColumns}.
288     */
289    public Attachment(ContentValues values) {
290        name = values.getAsString(AttachmentColumns.NAME);
291        size = values.getAsInteger(AttachmentColumns.SIZE);
292        uri = parseOptionalUri(values.getAsString(AttachmentColumns.URI));
293        contentType = values.getAsString(AttachmentColumns.CONTENT_TYPE);
294        state = values.getAsInteger(AttachmentColumns.STATE);
295        destination = values.getAsInteger(AttachmentColumns.DESTINATION);
296        downloadedSize = values.getAsInteger(AttachmentColumns.DOWNLOADED_SIZE);
297        contentUri = parseOptionalUri(values.getAsString(AttachmentColumns.CONTENT_URI));
298        thumbnailUri = parseOptionalUri(values.getAsString(AttachmentColumns.THUMBNAIL_URI));
299        previewIntentUri =
300                parseOptionalUri(values.getAsString(AttachmentColumns.PREVIEW_INTENT_URI));
301        providerData = values.getAsString(AttachmentColumns.PROVIDER_DATA);
302        supportsDownloadAgain = values.getAsBoolean(AttachmentColumns.SUPPORTS_DOWNLOAD_AGAIN);
303        type = values.getAsInteger(AttachmentColumns.TYPE);
304        flags = values.getAsInteger(AttachmentColumns.FLAGS);
305        partId = values.getAsString(AttachmentColumns.CONTENT_ID);
306    }
307
308    /**
309     * Returns the various attachment fields in a {@link ContentValues} object.
310     * The keys for each field should be {@link AttachmentColumns}.
311     */
312    public ContentValues toContentValues() {
313        final ContentValues values = new ContentValues(12);
314
315        values.put(AttachmentColumns.NAME, name);
316        values.put(AttachmentColumns.SIZE, size);
317        values.put(AttachmentColumns.URI, uri.toString());
318        values.put(AttachmentColumns.CONTENT_TYPE, contentType);
319        values.put(AttachmentColumns.STATE, state);
320        values.put(AttachmentColumns.DESTINATION, destination);
321        values.put(AttachmentColumns.DOWNLOADED_SIZE, downloadedSize);
322        values.put(AttachmentColumns.CONTENT_URI, contentUri.toString());
323        values.put(AttachmentColumns.THUMBNAIL_URI, thumbnailUri.toString());
324        values.put(AttachmentColumns.PREVIEW_INTENT_URI,
325                previewIntentUri == null ? null : previewIntentUri.toString());
326        values.put(AttachmentColumns.PROVIDER_DATA, providerData);
327        values.put(AttachmentColumns.SUPPORTS_DOWNLOAD_AGAIN, supportsDownloadAgain);
328        values.put(AttachmentColumns.TYPE, type);
329        values.put(AttachmentColumns.FLAGS, flags);
330        values.put(AttachmentColumns.CONTENT_ID, partId);
331
332        return values;
333    }
334
335    @Override
336    public void writeToParcel(Parcel dest, int flags) {
337        dest.writeString(name);
338        dest.writeInt(size);
339        dest.writeParcelable(uri, flags);
340        dest.writeString(contentType);
341        dest.writeInt(state);
342        dest.writeInt(destination);
343        dest.writeInt(downloadedSize);
344        dest.writeParcelable(contentUri, flags);
345        dest.writeParcelable(thumbnailUri, flags);
346        dest.writeParcelable(previewIntentUri, flags);
347        dest.writeString(providerData);
348        dest.writeInt(supportsDownloadAgain ? 1 : 0);
349        dest.writeInt(type);
350        dest.writeInt(flags);
351    }
352
353    public JSONObject toJSON() throws JSONException {
354        final JSONObject result = new JSONObject();
355
356        result.put(AttachmentColumns.NAME, name);
357        result.put(AttachmentColumns.SIZE, size);
358        result.put(AttachmentColumns.URI, stringify(uri));
359        result.put(AttachmentColumns.CONTENT_TYPE, contentType);
360        result.put(AttachmentColumns.STATE, state);
361        result.put(AttachmentColumns.DESTINATION, destination);
362        result.put(AttachmentColumns.DOWNLOADED_SIZE, downloadedSize);
363        result.put(AttachmentColumns.CONTENT_URI, stringify(contentUri));
364        result.put(AttachmentColumns.THUMBNAIL_URI, stringify(thumbnailUri));
365        result.put(AttachmentColumns.PREVIEW_INTENT_URI, stringify(previewIntentUri));
366        result.put(AttachmentColumns.PROVIDER_DATA, providerData);
367        result.put(AttachmentColumns.SUPPORTS_DOWNLOAD_AGAIN, supportsDownloadAgain);
368        result.put(AttachmentColumns.TYPE, type);
369        result.put(AttachmentColumns.FLAGS, flags);
370
371        return result;
372    }
373
374    @Override
375    public String toString() {
376        try {
377            final JSONObject jsonObject = toJSON();
378            // Add some additional fields that are helpful when debugging issues
379            jsonObject.put("partId", partId);
380            if (providerData != null) {
381                try {
382                    // pretty print the provider data
383                    jsonObject.put(AttachmentColumns.PROVIDER_DATA, new JSONObject(providerData));
384                } catch (JSONException e) {
385                    LogUtils.e(LOG_TAG, e, "JSONException when adding provider data");
386                }
387            }
388            return jsonObject.toString(4);
389        } catch (JSONException e) {
390            LogUtils.e(LOG_TAG, e, "JSONException in toString");
391            return super.toString();
392        }
393    }
394
395    private static String stringify(Object object) {
396        return object != null ? object.toString() : null;
397    }
398
399    protected static Uri parseOptionalUri(String uriString) {
400        return uriString == null ? null : Uri.parse(uriString);
401    }
402
403    protected static Uri parseOptionalUri(JSONObject srcJson, String key) {
404        final String uriStr = srcJson.optString(key, null);
405        return uriStr == null ? null : Uri.parse(uriStr);
406    }
407
408    @Override
409    public int describeContents() {
410        return 0;
411    }
412
413    public boolean isPresentLocally() {
414        return state == AttachmentState.SAVED;
415    }
416
417    public boolean canSave() {
418        return !isSavedToExternal() && !isInstallable();
419    }
420
421    public boolean canShare() {
422        return isPresentLocally() && contentUri != null;
423    }
424
425    public boolean isDownloading() {
426        return state == AttachmentState.DOWNLOADING || state == AttachmentState.PAUSED;
427    }
428
429    public boolean isSavedToExternal() {
430        return state == AttachmentState.SAVED && destination == AttachmentDestination.EXTERNAL;
431    }
432
433    public boolean isInstallable() {
434        return MimeType.isInstallable(getContentType());
435    }
436
437    public boolean shouldShowProgress() {
438        return (state == AttachmentState.DOWNLOADING || state == AttachmentState.PAUSED)
439                && size > 0 && downloadedSize > 0 && downloadedSize <= size;
440    }
441
442    public boolean isDownloadFailed() {
443        return state == AttachmentState.FAILED;
444    }
445
446    public boolean isDownloadFinishedOrFailed() {
447        return state == AttachmentState.FAILED || state == AttachmentState.SAVED;
448    }
449
450    public boolean supportsDownloadAgain() {
451        return supportsDownloadAgain;
452    }
453
454    public boolean canPreview() {
455        return previewIntentUri != null;
456    }
457
458    /**
459     * Returns a stable identifier URI for this attachment. TODO: make the uri
460     * field stable, and put provider-specific opaque bits and bobs elsewhere
461     */
462    public Uri getIdentifierUri() {
463        if (Utils.isEmpty(mIdentifierUri)) {
464            mIdentifierUri = Utils.isEmpty(uri) ?
465                    (Utils.isEmpty(contentUri) ? Uri.EMPTY : contentUri)
466                    : uri.buildUpon().clearQuery().build();
467        }
468        return mIdentifierUri;
469    }
470
471    public String getContentType() {
472        if (TextUtils.isEmpty(inferredContentType)) {
473            inferredContentType = MimeType.inferMimeType(name, contentType);
474        }
475        return inferredContentType;
476    }
477
478    public Uri getUriForRendition(int rendition) {
479        final Uri uri;
480        switch (rendition) {
481            case AttachmentRendition.BEST:
482                uri = contentUri;
483                break;
484            case AttachmentRendition.SIMPLE:
485                uri = thumbnailUri;
486                break;
487            default:
488                throw new IllegalArgumentException("invalid rendition: " + rendition);
489        }
490        return uri;
491    }
492
493    public void setContentType(String contentType) {
494        if (!TextUtils.equals(this.contentType, contentType)) {
495            this.inferredContentType = null;
496            this.contentType = contentType;
497        }
498    }
499
500    public String getName() {
501        return name;
502    }
503
504    public boolean setName(String name) {
505        if (!TextUtils.equals(this.name, name)) {
506            this.inferredContentType = null;
507            this.name = name;
508            return true;
509        }
510        return false;
511    }
512
513    /**
514     * Sets the attachment state. Side effect: sets downloadedSize
515     */
516    public void setState(int state) {
517        this.state = state;
518        if (state == AttachmentState.FAILED || state == AttachmentState.NOT_SAVED) {
519            this.downloadedSize = 0;
520        }
521    }
522
523    /**
524     * @return {@code true} if the attachment is an inline attachment
525     * that appears in the body of the message content (including possibly
526     * quoted text).
527     */
528    public boolean isInlineAttachment() {
529        return type != UIProvider.AttachmentType.STANDARD;
530    }
531
532    @Override
533    public boolean equals(final Object o) {
534        if (this == o) {
535            return true;
536        }
537        if (o == null || getClass() != o.getClass()) {
538            return false;
539        }
540
541        final Attachment that = (Attachment) o;
542
543        if (destination != that.destination) {
544            return false;
545        }
546        if (downloadedSize != that.downloadedSize) {
547            return false;
548        }
549        if (size != that.size) {
550            return false;
551        }
552        if (state != that.state) {
553            return false;
554        }
555        if (supportsDownloadAgain != that.supportsDownloadAgain) {
556            return false;
557        }
558        if (type != that.type) {
559            return false;
560        }
561        if (contentType != null ? !contentType.equals(that.contentType)
562                : that.contentType != null) {
563            return false;
564        }
565        if (contentUri != null ? !contentUri.equals(that.contentUri) : that.contentUri != null) {
566            return false;
567        }
568        if (name != null ? !name.equals(that.name) : that.name != null) {
569            return false;
570        }
571        if (partId != null ? !partId.equals(that.partId) : that.partId != null) {
572            return false;
573        }
574        if (previewIntentUri != null ? !previewIntentUri.equals(that.previewIntentUri)
575                : that.previewIntentUri != null) {
576            return false;
577        }
578        if (providerData != null ? !providerData.equals(that.providerData)
579                : that.providerData != null) {
580            return false;
581        }
582        if (thumbnailUri != null ? !thumbnailUri.equals(that.thumbnailUri)
583                : that.thumbnailUri != null) {
584            return false;
585        }
586        if (uri != null ? !uri.equals(that.uri) : that.uri != null) {
587            return false;
588        }
589
590        return true;
591    }
592
593    @Override
594    public int hashCode() {
595        int result = partId != null ? partId.hashCode() : 0;
596        result = 31 * result + (name != null ? name.hashCode() : 0);
597        result = 31 * result + size;
598        result = 31 * result + (uri != null ? uri.hashCode() : 0);
599        result = 31 * result + (contentType != null ? contentType.hashCode() : 0);
600        result = 31 * result + state;
601        result = 31 * result + destination;
602        result = 31 * result + downloadedSize;
603        result = 31 * result + (contentUri != null ? contentUri.hashCode() : 0);
604        result = 31 * result + (thumbnailUri != null ? thumbnailUri.hashCode() : 0);
605        result = 31 * result + (previewIntentUri != null ? previewIntentUri.hashCode() : 0);
606        result = 31 * result + type;
607        result = 31 * result + (providerData != null ? providerData.hashCode() : 0);
608        result = 31 * result + (supportsDownloadAgain ? 1 : 0);
609        return result;
610    }
611
612    public static String toJSONArray(Collection<? extends Attachment> attachments) {
613        if (attachments == null) {
614            return null;
615        }
616        final JSONArray result = new JSONArray();
617        try {
618            for (Attachment attachment : attachments) {
619                result.put(attachment.toJSON());
620            }
621        } catch (JSONException e) {
622            throw new IllegalArgumentException(e);
623        }
624        return result.toString();
625    }
626
627    public static List<Attachment> fromJSONArray(String jsonArrayStr) {
628        final List<Attachment> results = Lists.newArrayList();
629        if (jsonArrayStr != null) {
630            try {
631                final JSONArray arr = new JSONArray(jsonArrayStr);
632
633                for (int i = 0; i < arr.length(); i++) {
634                    results.add(new Attachment(arr.getJSONObject(i)));
635                }
636
637            } catch (JSONException e) {
638                throw new IllegalArgumentException(e);
639            }
640        }
641        return results;
642    }
643
644    private static final String SERVER_ATTACHMENT = "SERVER_ATTACHMENT";
645    private static final String LOCAL_FILE = "LOCAL_FILE";
646
647    public String toJoinedString() {
648        return TextUtils.join(UIProvider.ATTACHMENT_INFO_DELIMITER, Lists.newArrayList(
649                partId == null ? "" : partId,
650                name == null ? ""
651                        : name.replaceAll("[" + UIProvider.ATTACHMENT_INFO_DELIMITER
652                                + UIProvider.ATTACHMENT_INFO_SEPARATOR + "]", ""),
653                getContentType(),
654                String.valueOf(size),
655                getContentType(),
656                contentUri != null ? SERVER_ATTACHMENT : LOCAL_FILE,
657                contentUri != null ? contentUri.toString() : "",
658                "" /* cachedFileUri */,
659                String.valueOf(type)));
660    }
661
662    /**
663     * For use with {@link UIProvider.ConversationColumns#ATTACHMENT_PREVIEW_STATES}.
664     *
665     * @param previewStates The packed int describing the states of multiple attachments.
666     * @param attachmentIndex The index of the attachment to update.
667     * @param rendition The rendition of that attachment to update.
668     * @param downloaded Whether that specific rendition is downloaded.
669     * @return A packed int describing the updated downloaded states of the multiple attachments.
670     */
671    public static int updatePreviewStates(int previewStates, int attachmentIndex, int rendition,
672            boolean downloaded) {
673        // find the bit that describes that specific attachment index and rendition
674        int shift = attachmentIndex * 2 + rendition;
675        int mask = 1 << shift;
676        // update the packed int at that bit
677        if (downloaded) {
678            // turns that bit into a 1
679            return previewStates | mask;
680        } else {
681            // turns that bit into a 0
682            return previewStates & ~mask;
683        }
684    }
685
686    /**
687     * For use with {@link UIProvider.ConversationColumns#ATTACHMENT_PREVIEW_STATES}.
688     *
689     * @param previewStates The packed int describing the states of multiple attachments.
690     * @param attachmentIndex The index of the attachment.
691     * @param rendition The rendition of the attachment.
692     * @return The downloaded state of that particular rendition of that particular attachment.
693     */
694    public static boolean getPreviewState(int previewStates, int attachmentIndex, int rendition) {
695        // find the bit that describes that specific attachment index
696        int shift = attachmentIndex * 2;
697        int mask = 1 << shift;
698
699        if (rendition == AttachmentRendition.SIMPLE) {
700            // implicit shift of 0 finds the SIMPLE rendition bit
701            return (previewStates & mask) != 0;
702        } else if (rendition == AttachmentRendition.BEST) {
703            // shift of 1 finds the BEST rendition bit
704            return (previewStates & (mask << 1)) != 0;
705        } else {
706            return false;
707        }
708    }
709
710    public static final Creator<Attachment> CREATOR = new Creator<Attachment>() {
711            @Override
712        public Attachment createFromParcel(Parcel source) {
713            return new Attachment(source);
714        }
715
716            @Override
717        public Attachment[] newArray(int size) {
718            return new Attachment[size];
719        }
720    };
721}
722