• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
Attachment()172     public Attachment() {
173     }
174 
Attachment(Parcel in)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 
Attachment(Cursor cursor)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 
Attachment(JSONObject srcJson)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      */
Attachment(Context context, Part part, Uri emlFileUri, String messageId, String partId)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      */
Attachment(ContentValues values)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      */
toContentValues()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
writeToParcel(Parcel dest, int flags)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 
toJSON()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
toString()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 
stringify(Object object)392     private static String stringify(Object object) {
393         return object != null ? object.toString() : null;
394     }
395 
parseOptionalUri(String uriString)396     protected static Uri parseOptionalUri(String uriString) {
397         return uriString == null ? null : Uri.parse(uriString);
398     }
399 
parseOptionalUri(JSONObject srcJson, String key)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
describeContents()406     public int describeContents() {
407         return 0;
408     }
409 
isPresentLocally()410     public boolean isPresentLocally() {
411         return state == AttachmentState.SAVED;
412     }
413 
canSave()414     public boolean canSave() {
415         return !isSavedToExternal() && !isInstallable() && !MimeType.isBlocked(getContentType());
416     }
417 
canShare()418     public boolean canShare() {
419         return isPresentLocally() && contentUri != null;
420     }
421 
isDownloading()422     public boolean isDownloading() {
423         return state == AttachmentState.DOWNLOADING || state == AttachmentState.PAUSED;
424     }
425 
isSavedToExternal()426     public boolean isSavedToExternal() {
427         return state == AttachmentState.SAVED && destination == AttachmentDestination.EXTERNAL;
428     }
429 
isInstallable()430     public boolean isInstallable() {
431         return MimeType.isInstallable(getContentType());
432     }
433 
shouldShowProgress()434     public boolean shouldShowProgress() {
435         return (state == AttachmentState.DOWNLOADING || state == AttachmentState.PAUSED)
436                 && size > 0 && downloadedSize > 0 && downloadedSize <= size;
437     }
438 
isDownloadFailed()439     public boolean isDownloadFailed() {
440         return state == AttachmentState.FAILED;
441     }
442 
isDownloadFinishedOrFailed()443     public boolean isDownloadFinishedOrFailed() {
444         return state == AttachmentState.FAILED || state == AttachmentState.SAVED;
445     }
446 
supportsDownloadAgain()447     public boolean supportsDownloadAgain() {
448         return supportsDownloadAgain;
449     }
450 
canPreview()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      */
getIdentifierUri()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 
getContentType()468     public String getContentType() {
469         if (TextUtils.isEmpty(inferredContentType)) {
470             inferredContentType = MimeType.inferMimeType(name, contentType);
471         }
472         return inferredContentType;
473     }
474 
getUriForRendition(int rendition)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 
setContentType(String contentType)490     public void setContentType(String contentType) {
491         if (!TextUtils.equals(this.contentType, contentType)) {
492             this.inferredContentType = null;
493             this.contentType = contentType;
494         }
495     }
496 
getName()497     public String getName() {
498         return name;
499     }
500 
setName(String name)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      */
setState(int state)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
equals(final Object o)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
hashCode()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 
toJSONArray(Collection<? extends Attachment> attachments)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 
fromJSONArray(String jsonArrayStr)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 
toJoinedString()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      */
updatePreviewStates(int previewStates, int attachmentIndex, int rendition, boolean downloaded)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      */
getPreviewState(int previewStates, int attachmentIndex, int rendition)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