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 172 public Attachment() { 173 } 174 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 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 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 */ 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 */ 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 */ 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 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 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 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 392 private static String stringify(Object object) { 393 return object != null ? object.toString() : null; 394 } 395 396 protected static Uri parseOptionalUri(String uriString) { 397 return uriString == null ? null : Uri.parse(uriString); 398 } 399 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 406 public int describeContents() { 407 return 0; 408 } 409 410 public boolean isPresentLocally() { 411 return state == AttachmentState.SAVED; 412 } 413 414 public boolean canSave() { 415 return !isSavedToExternal() && !isInstallable() && !MimeType.isBlocked(getContentType()); 416 } 417 418 public boolean canShare() { 419 return isPresentLocally() && contentUri != null; 420 } 421 422 public boolean isDownloading() { 423 return state == AttachmentState.DOWNLOADING || state == AttachmentState.PAUSED; 424 } 425 426 public boolean isSavedToExternal() { 427 return state == AttachmentState.SAVED && destination == AttachmentDestination.EXTERNAL; 428 } 429 430 public boolean isInstallable() { 431 return MimeType.isInstallable(getContentType()); 432 } 433 434 public boolean shouldShowProgress() { 435 return (state == AttachmentState.DOWNLOADING || state == AttachmentState.PAUSED) 436 && size > 0 && downloadedSize > 0 && downloadedSize <= size; 437 } 438 439 public boolean isDownloadFailed() { 440 return state == AttachmentState.FAILED; 441 } 442 443 public boolean isDownloadFinishedOrFailed() { 444 return state == AttachmentState.FAILED || state == AttachmentState.SAVED; 445 } 446 447 public boolean supportsDownloadAgain() { 448 return supportsDownloadAgain; 449 } 450 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 */ 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 468 public String getContentType() { 469 if (TextUtils.isEmpty(inferredContentType)) { 470 inferredContentType = MimeType.inferMimeType(name, contentType); 471 } 472 return inferredContentType; 473 } 474 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 490 public void setContentType(String contentType) { 491 if (!TextUtils.equals(this.contentType, contentType)) { 492 this.inferredContentType = null; 493 this.contentType = contentType; 494 } 495 } 496 497 public String getName() { 498 return name; 499 } 500 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 */ 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 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 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 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 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 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 */ 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 */ 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