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