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 private transient Uri mIdentifierUri; 164 165 /** 166 * True if this attachment can be downloaded again. 167 */ 168 private boolean supportsDownloadAgain; 169 170 Attachment()171 public Attachment() { 172 } 173 Attachment(Parcel in)174 public Attachment(Parcel in) { 175 name = in.readString(); 176 size = in.readInt(); 177 uri = in.readParcelable(null); 178 contentType = in.readString(); 179 state = in.readInt(); 180 destination = in.readInt(); 181 downloadedSize = in.readInt(); 182 contentUri = in.readParcelable(null); 183 thumbnailUri = in.readParcelable(null); 184 previewIntentUri = in.readParcelable(null); 185 providerData = in.readString(); 186 supportsDownloadAgain = in.readInt() == 1; 187 type = in.readInt(); 188 flags = in.readInt(); 189 } 190 Attachment(Cursor cursor)191 public Attachment(Cursor cursor) { 192 if (cursor == null) { 193 return; 194 } 195 196 name = cursor.getString(cursor.getColumnIndex(AttachmentColumns.NAME)); 197 size = cursor.getInt(cursor.getColumnIndex(AttachmentColumns.SIZE)); 198 uri = Uri.parse(cursor.getString(cursor.getColumnIndex(AttachmentColumns.URI))); 199 contentType = cursor.getString(cursor.getColumnIndex(AttachmentColumns.CONTENT_TYPE)); 200 state = cursor.getInt(cursor.getColumnIndex(AttachmentColumns.STATE)); 201 destination = cursor.getInt(cursor.getColumnIndex(AttachmentColumns.DESTINATION)); 202 downloadedSize = cursor.getInt(cursor.getColumnIndex(AttachmentColumns.DOWNLOADED_SIZE)); 203 contentUri = parseOptionalUri( 204 cursor.getString(cursor.getColumnIndex(AttachmentColumns.CONTENT_URI))); 205 thumbnailUri = parseOptionalUri( 206 cursor.getString(cursor.getColumnIndex(AttachmentColumns.THUMBNAIL_URI))); 207 previewIntentUri = parseOptionalUri( 208 cursor.getString(cursor.getColumnIndex(AttachmentColumns.PREVIEW_INTENT_URI))); 209 providerData = cursor.getString(cursor.getColumnIndex(AttachmentColumns.PROVIDER_DATA)); 210 supportsDownloadAgain = cursor.getInt( 211 cursor.getColumnIndex(AttachmentColumns.SUPPORTS_DOWNLOAD_AGAIN)) == 1; 212 type = cursor.getInt(cursor.getColumnIndex(AttachmentColumns.TYPE)); 213 flags = cursor.getInt(cursor.getColumnIndex(AttachmentColumns.FLAGS)); 214 } 215 Attachment(JSONObject srcJson)216 public Attachment(JSONObject srcJson) { 217 name = srcJson.optString(AttachmentColumns.NAME, null); 218 size = srcJson.optInt(AttachmentColumns.SIZE); 219 uri = parseOptionalUri(srcJson, AttachmentColumns.URI); 220 contentType = srcJson.optString(AttachmentColumns.CONTENT_TYPE, null); 221 state = srcJson.optInt(AttachmentColumns.STATE); 222 destination = srcJson.optInt(AttachmentColumns.DESTINATION); 223 downloadedSize = srcJson.optInt(AttachmentColumns.DOWNLOADED_SIZE); 224 contentUri = parseOptionalUri(srcJson, AttachmentColumns.CONTENT_URI); 225 thumbnailUri = parseOptionalUri(srcJson, AttachmentColumns.THUMBNAIL_URI); 226 previewIntentUri = parseOptionalUri(srcJson, AttachmentColumns.PREVIEW_INTENT_URI); 227 providerData = srcJson.optString(AttachmentColumns.PROVIDER_DATA); 228 supportsDownloadAgain = srcJson.optBoolean(AttachmentColumns.SUPPORTS_DOWNLOAD_AGAIN, true); 229 type = srcJson.optInt(AttachmentColumns.TYPE); 230 flags = srcJson.optInt(AttachmentColumns.FLAGS); 231 } 232 233 /** 234 * Constructor for use when creating attachments in eml files. 235 */ Attachment(Context context, Part part, Uri emlFileUri, String messageId, String cid, boolean inline)236 public Attachment(Context context, Part part, Uri emlFileUri, String messageId, String cid, 237 boolean inline) { 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, cid); 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 = inline ? AttachmentType.INLINE_CURRENT_MESSAGE : AttachmentType.STANDARD; 258 partId = cid; 259 flags = 0; 260 261 // insert attachment into content provider so that we can open the file 262 final ContentResolver resolver = context.getContentResolver(); 263 resolver.insert(uri, toContentValues()); 264 265 // save the file in the cache 266 try { 267 final InputStream in = part.getBody().getInputStream(); 268 final OutputStream out = resolver.openOutputStream(uri, "rwt"); 269 size = IOUtils.copy(in, out); 270 downloadedSize = size; 271 in.close(); 272 out.close(); 273 } catch (FileNotFoundException e) { 274 LogUtils.e(LOG_TAG, e, "Error in writing attachment to cache"); 275 } catch (IOException e) { 276 LogUtils.e(LOG_TAG, e, "Error in writing attachment to cache"); 277 } 278 // perform a second insert to put the updated size and downloaded size values in 279 resolver.insert(uri, toContentValues()); 280 } catch (MessagingException e) { 281 LogUtils.e(LOG_TAG, e, "Error parsing eml attachment"); 282 } 283 } 284 285 /** 286 * Create an attachment from a {@link ContentValues} object. 287 * The keys should be {@link AttachmentColumns}. 288 */ Attachment(ContentValues values)289 public Attachment(ContentValues values) { 290 name = values.getAsString(AttachmentColumns.NAME); 291 size = values.getAsInteger(AttachmentColumns.SIZE); 292 uri = parseOptionalUri(values.getAsString(AttachmentColumns.URI)); 293 contentType = values.getAsString(AttachmentColumns.CONTENT_TYPE); 294 state = values.getAsInteger(AttachmentColumns.STATE); 295 destination = values.getAsInteger(AttachmentColumns.DESTINATION); 296 downloadedSize = values.getAsInteger(AttachmentColumns.DOWNLOADED_SIZE); 297 contentUri = parseOptionalUri(values.getAsString(AttachmentColumns.CONTENT_URI)); 298 thumbnailUri = parseOptionalUri(values.getAsString(AttachmentColumns.THUMBNAIL_URI)); 299 previewIntentUri = 300 parseOptionalUri(values.getAsString(AttachmentColumns.PREVIEW_INTENT_URI)); 301 providerData = values.getAsString(AttachmentColumns.PROVIDER_DATA); 302 supportsDownloadAgain = values.getAsBoolean(AttachmentColumns.SUPPORTS_DOWNLOAD_AGAIN); 303 type = values.getAsInteger(AttachmentColumns.TYPE); 304 flags = values.getAsInteger(AttachmentColumns.FLAGS); 305 partId = values.getAsString(AttachmentColumns.CONTENT_ID); 306 } 307 308 /** 309 * Returns the various attachment fields in a {@link ContentValues} object. 310 * The keys for each field should be {@link AttachmentColumns}. 311 */ toContentValues()312 public ContentValues toContentValues() { 313 final ContentValues values = new ContentValues(12); 314 315 values.put(AttachmentColumns.NAME, name); 316 values.put(AttachmentColumns.SIZE, size); 317 values.put(AttachmentColumns.URI, uri.toString()); 318 values.put(AttachmentColumns.CONTENT_TYPE, contentType); 319 values.put(AttachmentColumns.STATE, state); 320 values.put(AttachmentColumns.DESTINATION, destination); 321 values.put(AttachmentColumns.DOWNLOADED_SIZE, downloadedSize); 322 values.put(AttachmentColumns.CONTENT_URI, contentUri.toString()); 323 values.put(AttachmentColumns.THUMBNAIL_URI, thumbnailUri.toString()); 324 values.put(AttachmentColumns.PREVIEW_INTENT_URI, 325 previewIntentUri == null ? null : previewIntentUri.toString()); 326 values.put(AttachmentColumns.PROVIDER_DATA, providerData); 327 values.put(AttachmentColumns.SUPPORTS_DOWNLOAD_AGAIN, supportsDownloadAgain); 328 values.put(AttachmentColumns.TYPE, type); 329 values.put(AttachmentColumns.FLAGS, flags); 330 values.put(AttachmentColumns.CONTENT_ID, partId); 331 332 return values; 333 } 334 335 @Override writeToParcel(Parcel dest, int flags)336 public void writeToParcel(Parcel dest, int flags) { 337 dest.writeString(name); 338 dest.writeInt(size); 339 dest.writeParcelable(uri, flags); 340 dest.writeString(contentType); 341 dest.writeInt(state); 342 dest.writeInt(destination); 343 dest.writeInt(downloadedSize); 344 dest.writeParcelable(contentUri, flags); 345 dest.writeParcelable(thumbnailUri, flags); 346 dest.writeParcelable(previewIntentUri, flags); 347 dest.writeString(providerData); 348 dest.writeInt(supportsDownloadAgain ? 1 : 0); 349 dest.writeInt(type); 350 dest.writeInt(flags); 351 } 352 toJSON()353 public JSONObject toJSON() throws JSONException { 354 final JSONObject result = new JSONObject(); 355 356 result.put(AttachmentColumns.NAME, name); 357 result.put(AttachmentColumns.SIZE, size); 358 result.put(AttachmentColumns.URI, stringify(uri)); 359 result.put(AttachmentColumns.CONTENT_TYPE, contentType); 360 result.put(AttachmentColumns.STATE, state); 361 result.put(AttachmentColumns.DESTINATION, destination); 362 result.put(AttachmentColumns.DOWNLOADED_SIZE, downloadedSize); 363 result.put(AttachmentColumns.CONTENT_URI, stringify(contentUri)); 364 result.put(AttachmentColumns.THUMBNAIL_URI, stringify(thumbnailUri)); 365 result.put(AttachmentColumns.PREVIEW_INTENT_URI, stringify(previewIntentUri)); 366 result.put(AttachmentColumns.PROVIDER_DATA, providerData); 367 result.put(AttachmentColumns.SUPPORTS_DOWNLOAD_AGAIN, supportsDownloadAgain); 368 result.put(AttachmentColumns.TYPE, type); 369 result.put(AttachmentColumns.FLAGS, flags); 370 371 return result; 372 } 373 374 @Override toString()375 public String toString() { 376 try { 377 final JSONObject jsonObject = toJSON(); 378 // Add some additional fields that are helpful when debugging issues 379 jsonObject.put("partId", partId); 380 if (providerData != null) { 381 try { 382 // pretty print the provider data 383 jsonObject.put(AttachmentColumns.PROVIDER_DATA, new JSONObject(providerData)); 384 } catch (JSONException e) { 385 LogUtils.e(LOG_TAG, e, "JSONException when adding provider data"); 386 } 387 } 388 return jsonObject.toString(4); 389 } catch (JSONException e) { 390 LogUtils.e(LOG_TAG, e, "JSONException in toString"); 391 return super.toString(); 392 } 393 } 394 stringify(Object object)395 private static String stringify(Object object) { 396 return object != null ? object.toString() : null; 397 } 398 parseOptionalUri(String uriString)399 protected static Uri parseOptionalUri(String uriString) { 400 return uriString == null ? null : Uri.parse(uriString); 401 } 402 parseOptionalUri(JSONObject srcJson, String key)403 protected static Uri parseOptionalUri(JSONObject srcJson, String key) { 404 final String uriStr = srcJson.optString(key, null); 405 return uriStr == null ? null : Uri.parse(uriStr); 406 } 407 408 @Override describeContents()409 public int describeContents() { 410 return 0; 411 } 412 isPresentLocally()413 public boolean isPresentLocally() { 414 return state == AttachmentState.SAVED; 415 } 416 canSave()417 public boolean canSave() { 418 return !isSavedToExternal() && !isInstallable(); 419 } 420 canShare()421 public boolean canShare() { 422 return isPresentLocally() && contentUri != null; 423 } 424 isDownloading()425 public boolean isDownloading() { 426 return state == AttachmentState.DOWNLOADING || state == AttachmentState.PAUSED; 427 } 428 isSavedToExternal()429 public boolean isSavedToExternal() { 430 return state == AttachmentState.SAVED && destination == AttachmentDestination.EXTERNAL; 431 } 432 isInstallable()433 public boolean isInstallable() { 434 return MimeType.isInstallable(getContentType()); 435 } 436 shouldShowProgress()437 public boolean shouldShowProgress() { 438 return (state == AttachmentState.DOWNLOADING || state == AttachmentState.PAUSED) 439 && size > 0 && downloadedSize > 0 && downloadedSize <= size; 440 } 441 isDownloadFailed()442 public boolean isDownloadFailed() { 443 return state == AttachmentState.FAILED; 444 } 445 isDownloadFinishedOrFailed()446 public boolean isDownloadFinishedOrFailed() { 447 return state == AttachmentState.FAILED || state == AttachmentState.SAVED; 448 } 449 supportsDownloadAgain()450 public boolean supportsDownloadAgain() { 451 return supportsDownloadAgain; 452 } 453 canPreview()454 public boolean canPreview() { 455 return previewIntentUri != null; 456 } 457 458 /** 459 * Returns a stable identifier URI for this attachment. TODO: make the uri 460 * field stable, and put provider-specific opaque bits and bobs elsewhere 461 */ getIdentifierUri()462 public Uri getIdentifierUri() { 463 if (Utils.isEmpty(mIdentifierUri)) { 464 mIdentifierUri = Utils.isEmpty(uri) ? 465 (Utils.isEmpty(contentUri) ? Uri.EMPTY : contentUri) 466 : uri.buildUpon().clearQuery().build(); 467 } 468 return mIdentifierUri; 469 } 470 getContentType()471 public String getContentType() { 472 if (TextUtils.isEmpty(inferredContentType)) { 473 inferredContentType = MimeType.inferMimeType(name, contentType); 474 } 475 return inferredContentType; 476 } 477 getUriForRendition(int rendition)478 public Uri getUriForRendition(int rendition) { 479 final Uri uri; 480 switch (rendition) { 481 case AttachmentRendition.BEST: 482 uri = contentUri; 483 break; 484 case AttachmentRendition.SIMPLE: 485 uri = thumbnailUri; 486 break; 487 default: 488 throw new IllegalArgumentException("invalid rendition: " + rendition); 489 } 490 return uri; 491 } 492 setContentType(String contentType)493 public void setContentType(String contentType) { 494 if (!TextUtils.equals(this.contentType, contentType)) { 495 this.inferredContentType = null; 496 this.contentType = contentType; 497 } 498 } 499 getName()500 public String getName() { 501 return name; 502 } 503 setName(String name)504 public boolean setName(String name) { 505 if (!TextUtils.equals(this.name, name)) { 506 this.inferredContentType = null; 507 this.name = name; 508 return true; 509 } 510 return false; 511 } 512 513 /** 514 * Sets the attachment state. Side effect: sets downloadedSize 515 */ setState(int state)516 public void setState(int state) { 517 this.state = state; 518 if (state == AttachmentState.FAILED || state == AttachmentState.NOT_SAVED) { 519 this.downloadedSize = 0; 520 } 521 } 522 523 /** 524 * @return {@code true} if the attachment is an inline attachment 525 * that appears in the body of the message content (including possibly 526 * quoted text). 527 */ isInlineAttachment()528 public boolean isInlineAttachment() { 529 return type != UIProvider.AttachmentType.STANDARD; 530 } 531 532 @Override equals(final Object o)533 public boolean equals(final Object o) { 534 if (this == o) { 535 return true; 536 } 537 if (o == null || getClass() != o.getClass()) { 538 return false; 539 } 540 541 final Attachment that = (Attachment) o; 542 543 if (destination != that.destination) { 544 return false; 545 } 546 if (downloadedSize != that.downloadedSize) { 547 return false; 548 } 549 if (size != that.size) { 550 return false; 551 } 552 if (state != that.state) { 553 return false; 554 } 555 if (supportsDownloadAgain != that.supportsDownloadAgain) { 556 return false; 557 } 558 if (type != that.type) { 559 return false; 560 } 561 if (contentType != null ? !contentType.equals(that.contentType) 562 : that.contentType != null) { 563 return false; 564 } 565 if (contentUri != null ? !contentUri.equals(that.contentUri) : that.contentUri != null) { 566 return false; 567 } 568 if (name != null ? !name.equals(that.name) : that.name != null) { 569 return false; 570 } 571 if (partId != null ? !partId.equals(that.partId) : that.partId != null) { 572 return false; 573 } 574 if (previewIntentUri != null ? !previewIntentUri.equals(that.previewIntentUri) 575 : that.previewIntentUri != null) { 576 return false; 577 } 578 if (providerData != null ? !providerData.equals(that.providerData) 579 : that.providerData != null) { 580 return false; 581 } 582 if (thumbnailUri != null ? !thumbnailUri.equals(that.thumbnailUri) 583 : that.thumbnailUri != null) { 584 return false; 585 } 586 if (uri != null ? !uri.equals(that.uri) : that.uri != null) { 587 return false; 588 } 589 590 return true; 591 } 592 593 @Override hashCode()594 public int hashCode() { 595 int result = partId != null ? partId.hashCode() : 0; 596 result = 31 * result + (name != null ? name.hashCode() : 0); 597 result = 31 * result + size; 598 result = 31 * result + (uri != null ? uri.hashCode() : 0); 599 result = 31 * result + (contentType != null ? contentType.hashCode() : 0); 600 result = 31 * result + state; 601 result = 31 * result + destination; 602 result = 31 * result + downloadedSize; 603 result = 31 * result + (contentUri != null ? contentUri.hashCode() : 0); 604 result = 31 * result + (thumbnailUri != null ? thumbnailUri.hashCode() : 0); 605 result = 31 * result + (previewIntentUri != null ? previewIntentUri.hashCode() : 0); 606 result = 31 * result + type; 607 result = 31 * result + (providerData != null ? providerData.hashCode() : 0); 608 result = 31 * result + (supportsDownloadAgain ? 1 : 0); 609 return result; 610 } 611 toJSONArray(Collection<? extends Attachment> attachments)612 public static String toJSONArray(Collection<? extends Attachment> attachments) { 613 if (attachments == null) { 614 return null; 615 } 616 final JSONArray result = new JSONArray(); 617 try { 618 for (Attachment attachment : attachments) { 619 result.put(attachment.toJSON()); 620 } 621 } catch (JSONException e) { 622 throw new IllegalArgumentException(e); 623 } 624 return result.toString(); 625 } 626 fromJSONArray(String jsonArrayStr)627 public static List<Attachment> fromJSONArray(String jsonArrayStr) { 628 final List<Attachment> results = Lists.newArrayList(); 629 if (jsonArrayStr != null) { 630 try { 631 final JSONArray arr = new JSONArray(jsonArrayStr); 632 633 for (int i = 0; i < arr.length(); i++) { 634 results.add(new Attachment(arr.getJSONObject(i))); 635 } 636 637 } catch (JSONException e) { 638 throw new IllegalArgumentException(e); 639 } 640 } 641 return results; 642 } 643 644 private static final String SERVER_ATTACHMENT = "SERVER_ATTACHMENT"; 645 private static final String LOCAL_FILE = "LOCAL_FILE"; 646 toJoinedString()647 public String toJoinedString() { 648 return TextUtils.join(UIProvider.ATTACHMENT_INFO_DELIMITER, Lists.newArrayList( 649 partId == null ? "" : partId, 650 name == null ? "" 651 : name.replaceAll("[" + UIProvider.ATTACHMENT_INFO_DELIMITER 652 + UIProvider.ATTACHMENT_INFO_SEPARATOR + "]", ""), 653 getContentType(), 654 String.valueOf(size), 655 getContentType(), 656 contentUri != null ? SERVER_ATTACHMENT : LOCAL_FILE, 657 contentUri != null ? contentUri.toString() : "", 658 "" /* cachedFileUri */, 659 String.valueOf(type))); 660 } 661 662 /** 663 * For use with {@link UIProvider.ConversationColumns#ATTACHMENT_PREVIEW_STATES}. 664 * 665 * @param previewStates The packed int describing the states of multiple attachments. 666 * @param attachmentIndex The index of the attachment to update. 667 * @param rendition The rendition of that attachment to update. 668 * @param downloaded Whether that specific rendition is downloaded. 669 * @return A packed int describing the updated downloaded states of the multiple attachments. 670 */ updatePreviewStates(int previewStates, int attachmentIndex, int rendition, boolean downloaded)671 public static int updatePreviewStates(int previewStates, int attachmentIndex, int rendition, 672 boolean downloaded) { 673 // find the bit that describes that specific attachment index and rendition 674 int shift = attachmentIndex * 2 + rendition; 675 int mask = 1 << shift; 676 // update the packed int at that bit 677 if (downloaded) { 678 // turns that bit into a 1 679 return previewStates | mask; 680 } else { 681 // turns that bit into a 0 682 return previewStates & ~mask; 683 } 684 } 685 686 /** 687 * For use with {@link UIProvider.ConversationColumns#ATTACHMENT_PREVIEW_STATES}. 688 * 689 * @param previewStates The packed int describing the states of multiple attachments. 690 * @param attachmentIndex The index of the attachment. 691 * @param rendition The rendition of the attachment. 692 * @return The downloaded state of that particular rendition of that particular attachment. 693 */ getPreviewState(int previewStates, int attachmentIndex, int rendition)694 public static boolean getPreviewState(int previewStates, int attachmentIndex, int rendition) { 695 // find the bit that describes that specific attachment index 696 int shift = attachmentIndex * 2; 697 int mask = 1 << shift; 698 699 if (rendition == AttachmentRendition.SIMPLE) { 700 // implicit shift of 0 finds the SIMPLE rendition bit 701 return (previewStates & mask) != 0; 702 } else if (rendition == AttachmentRendition.BEST) { 703 // shift of 1 finds the BEST rendition bit 704 return (previewStates & (mask << 1)) != 0; 705 } else { 706 return false; 707 } 708 } 709 710 public static final Creator<Attachment> CREATOR = new Creator<Attachment>() { 711 @Override 712 public Attachment createFromParcel(Parcel source) { 713 return new Attachment(source); 714 } 715 716 @Override 717 public Attachment[] newArray(int size) { 718 return new Attachment[size]; 719 } 720 }; 721 } 722