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