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