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 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 */ Attachment(ContentValues values)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 */ toContentValues()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 writeToParcel(Parcel dest, int flags)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 toJSON()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 toString()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 stringify(Object object)411 private static String stringify(Object object) { 412 return object != null ? object.toString() : null; 413 } 414 parseOptionalUri(String uriString)415 protected static Uri parseOptionalUri(String uriString) { 416 return uriString == null ? null : Uri.parse(uriString); 417 } 418 parseOptionalUri(JSONObject srcJson, String key)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 describeContents()425 public int describeContents() { 426 return 0; 427 } 428 isPresentLocally()429 public boolean isPresentLocally() { 430 return state == AttachmentState.SAVED; 431 } 432 canSave()433 public boolean canSave() { 434 return !isSavedToExternal() && !isInstallable(); 435 } 436 canShare()437 public boolean canShare() { 438 return isPresentLocally() && contentUri != null; 439 } 440 isDownloading()441 public boolean isDownloading() { 442 return state == AttachmentState.DOWNLOADING || state == AttachmentState.PAUSED; 443 } 444 isSavedToExternal()445 public boolean isSavedToExternal() { 446 return state == AttachmentState.SAVED && destination == AttachmentDestination.EXTERNAL; 447 } 448 isInstallable()449 public boolean isInstallable() { 450 return MimeType.isInstallable(getContentType()); 451 } 452 shouldShowProgress()453 public boolean shouldShowProgress() { 454 return (state == AttachmentState.DOWNLOADING || state == AttachmentState.PAUSED) 455 && size > 0 && downloadedSize > 0 && downloadedSize <= size; 456 } 457 isDownloadFailed()458 public boolean isDownloadFailed() { 459 return state == AttachmentState.FAILED; 460 } 461 isDownloadFinishedOrFailed()462 public boolean isDownloadFinishedOrFailed() { 463 return state == AttachmentState.FAILED || state == AttachmentState.SAVED; 464 } 465 supportsDownloadAgain()466 public boolean supportsDownloadAgain() { 467 return supportsDownloadAgain; 468 } 469 canPreview()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 */ getIdentifierUri()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 getContentType()487 public String getContentType() { 488 if (TextUtils.isEmpty(inferredContentType)) { 489 inferredContentType = MimeType.inferMimeType(name, contentType); 490 } 491 return inferredContentType; 492 } 493 getUriForRendition(int rendition)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 setContentType(String contentType)509 public void setContentType(String contentType) { 510 if (!TextUtils.equals(this.contentType, contentType)) { 511 this.inferredContentType = null; 512 this.contentType = contentType; 513 } 514 } 515 getName()516 public String getName() { 517 return name; 518 } 519 setName(String name)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 */ setState(int state)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 */ isInlineAttachment()544 public boolean isInlineAttachment() { 545 return type != UIProvider.AttachmentType.STANDARD; 546 } 547 548 @Override equals(final Object o)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 hashCode()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 toJSONArray(Collection<? extends Attachment> attachments)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 fromJSONArray(String jsonArrayStr)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 toJoinedString()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 */ updatePreviewStates(int previewStates, int attachmentIndex, int rendition, boolean downloaded)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 */ getPreviewState(int previewStates, int attachmentIndex, int rendition)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