1 /* 2 * Copyright (C) 2015 The Android Open Source Project 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.messaging.datamodel.data; 18 19 import android.content.ContentValues; 20 import android.database.Cursor; 21 import android.database.sqlite.SQLiteStatement; 22 import android.graphics.Rect; 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.messaging.Factory; 29 import com.android.messaging.datamodel.DatabaseHelper; 30 import com.android.messaging.datamodel.DatabaseHelper.PartColumns; 31 import com.android.messaging.datamodel.DatabaseWrapper; 32 import com.android.messaging.datamodel.MediaScratchFileProvider; 33 import com.android.messaging.datamodel.MessagingContentProvider; 34 import com.android.messaging.datamodel.action.UpdateMessagePartSizeAction; 35 import com.android.messaging.datamodel.media.ImageRequest; 36 import com.android.messaging.sms.MmsUtils; 37 import com.android.messaging.util.Assert; 38 import com.android.messaging.util.Assert.DoesNotRunOnMainThread; 39 import com.android.messaging.util.ContentType; 40 import com.android.messaging.util.GifTranscoder; 41 import com.android.messaging.util.ImageUtils; 42 import com.android.messaging.util.LogUtil; 43 import com.android.messaging.util.SafeAsyncTask; 44 import com.android.messaging.util.UriUtil; 45 46 import java.util.Arrays; 47 import java.util.concurrent.TimeUnit; 48 49 /** 50 * Represents a single message part. Messages consist of one or more parts which may contain 51 * either text or media. 52 */ 53 public class MessagePartData implements Parcelable { 54 public static final int UNSPECIFIED_SIZE = MessagingContentProvider.UNSPECIFIED_SIZE; 55 56 public static final String[] ACCEPTABLE_GALLERY_MEDIA_TYPES = 57 new String[] { 58 // Acceptable image types 59 ContentType.IMAGE_JPEG, ContentType.IMAGE_JPG, ContentType.IMAGE_PNG, 60 ContentType.IMAGE_GIF, ContentType.IMAGE_WBMP, ContentType.IMAGE_X_MS_BMP, 61 // Acceptable video types 62 ContentType.VIDEO_3GP, ContentType.VIDEO_3GPP, ContentType.VIDEO_3G2, 63 ContentType.VIDEO_H263, ContentType.VIDEO_M4V, ContentType.VIDEO_MP4, 64 ContentType.VIDEO_MPEG, ContentType.VIDEO_MPEG4, ContentType.VIDEO_WEBM, 65 // Acceptable audio types 66 ContentType.AUDIO_MP3, ContentType.AUDIO_MP4, ContentType.AUDIO_MIDI, 67 ContentType.AUDIO_MID, ContentType.AUDIO_AMR, ContentType.AUDIO_X_WAV, 68 ContentType.AUDIO_AAC, ContentType.AUDIO_X_MIDI, ContentType.AUDIO_X_MID, 69 ContentType.AUDIO_X_MP3 70 }; 71 72 private static final String[] sProjection = { 73 PartColumns._ID, 74 PartColumns.MESSAGE_ID, 75 PartColumns.TEXT, 76 PartColumns.CONTENT_URI, 77 PartColumns.CONTENT_TYPE, 78 PartColumns.WIDTH, 79 PartColumns.HEIGHT, 80 }; 81 82 private static final int INDEX_ID = 0; 83 private static final int INDEX_MESSAGE_ID = 1; 84 private static final int INDEX_TEXT = 2; 85 private static final int INDEX_CONTENT_URI = 3; 86 private static final int INDEX_CONTENT_TYPE = 4; 87 private static final int INDEX_WIDTH = 5; 88 private static final int INDEX_HEIGHT = 6; 89 // This isn't part of the projection 90 private static final int INDEX_CONVERSATION_ID = 7; 91 92 // SQL statement to insert a "complete" message part row (columns based on projection above). 93 private static final String INSERT_MESSAGE_PART_SQL = 94 "INSERT INTO " + DatabaseHelper.PARTS_TABLE + " ( " 95 + TextUtils.join(",", Arrays.copyOfRange(sProjection, 1, INDEX_CONVERSATION_ID)) 96 + ", " + PartColumns.CONVERSATION_ID 97 + ") VALUES (?, ?, ?, ?, ?, ?, ?)"; 98 99 // Used for stuff that's ignored or arbitrarily compressed. 100 private static final long NO_MINIMUM_SIZE = 0; 101 102 private String mPartId; 103 private String mMessageId; 104 private String mText; 105 private Uri mContentUri; 106 private String mContentType; 107 private int mWidth; 108 private int mHeight; 109 // This kind of part can only be attached once and with no other attachment 110 private boolean mSinglePartOnly; 111 112 /** Transient data: true if destroy was already called */ 113 private boolean mDestroyed; 114 115 /** 116 * Create an "empty" message part 117 */ MessagePartData()118 protected MessagePartData() { 119 this(null, null, UNSPECIFIED_SIZE, UNSPECIFIED_SIZE); 120 } 121 122 /** 123 * Create a populated text message part 124 */ MessagePartData(final String messageText)125 protected MessagePartData(final String messageText) { 126 this(null, messageText, ContentType.TEXT_PLAIN, null, UNSPECIFIED_SIZE, UNSPECIFIED_SIZE, 127 false /*singlePartOnly*/); 128 } 129 130 /** 131 * Create a populated attachment message part 132 */ MessagePartData(final String contentType, final Uri contentUri, final int width, final int height)133 protected MessagePartData(final String contentType, final Uri contentUri, 134 final int width, final int height) { 135 this(null, null, contentType, contentUri, width, height, false /*singlePartOnly*/); 136 } 137 138 /** 139 * Create a populated attachment message part, with additional caption text 140 */ MessagePartData(final String messageText, final String contentType, final Uri contentUri, final int width, final int height)141 protected MessagePartData(final String messageText, final String contentType, 142 final Uri contentUri, final int width, final int height) { 143 this(null, messageText, contentType, contentUri, width, height, false /*singlePartOnly*/); 144 } 145 146 /** 147 * Create a populated attachment message part, with additional caption text, single part only 148 */ MessagePartData(final String messageText, final String contentType, final Uri contentUri, final int width, final int height, final boolean singlePartOnly)149 protected MessagePartData(final String messageText, final String contentType, 150 final Uri contentUri, final int width, final int height, final boolean singlePartOnly) { 151 this(null, messageText, contentType, contentUri, width, height, singlePartOnly); 152 } 153 154 /** 155 * Create a populated message part 156 */ MessagePartData(final String messageId, final String messageText, final String contentType, final Uri contentUri, final int width, final int height, final boolean singlePartOnly)157 private MessagePartData(final String messageId, final String messageText, 158 final String contentType, final Uri contentUri, final int width, final int height, 159 final boolean singlePartOnly) { 160 mMessageId = messageId; 161 mText = messageText; 162 mContentType = contentType; 163 mContentUri = contentUri; 164 mWidth = width; 165 mHeight = height; 166 mSinglePartOnly = singlePartOnly; 167 } 168 169 /** 170 * Create a "text" message part 171 */ createTextMessagePart(final String messageText)172 public static MessagePartData createTextMessagePart(final String messageText) { 173 return new MessagePartData(messageText); 174 } 175 176 /** 177 * Create a "media" message part 178 */ createMediaMessagePart(final String contentType, final Uri contentUri, final int width, final int height)179 public static MessagePartData createMediaMessagePart(final String contentType, 180 final Uri contentUri, final int width, final int height) { 181 return new MessagePartData(contentType, contentUri, width, height); 182 } 183 184 /** 185 * Create a "media" message part with caption 186 */ createMediaMessagePart(final String caption, final String contentType, final Uri contentUri, final int width, final int height)187 public static MessagePartData createMediaMessagePart(final String caption, 188 final String contentType, final Uri contentUri, final int width, final int height) { 189 return new MessagePartData(null, caption, contentType, contentUri, width, height, 190 false /*singlePartOnly*/ 191 ); 192 } 193 194 /** 195 * Create an empty "text" message part 196 */ createEmptyMessagePart()197 public static MessagePartData createEmptyMessagePart() { 198 return new MessagePartData(""); 199 } 200 201 /** 202 * Creates a new message part reading from the cursor 203 */ createFromCursor(final Cursor cursor)204 public static MessagePartData createFromCursor(final Cursor cursor) { 205 final MessagePartData part = new MessagePartData(); 206 part.bind(cursor); 207 return part; 208 } 209 getProjection()210 public static String[] getProjection() { 211 return sProjection; 212 } 213 214 /** 215 * Updates the part id. 216 * Can be used to reset the partId just prior to persisting (which will assign a new partId) 217 * or can be called on a part that does not yet have a valid part id to set it. 218 */ updatePartId(final String partId)219 public void updatePartId(final String partId) { 220 Assert.isTrue(TextUtils.isEmpty(partId) || TextUtils.isEmpty(mPartId)); 221 mPartId = partId; 222 } 223 224 /** 225 * Updates the messageId for the part. 226 * Can be used to reset the messageId prior to persisting (which will assign a new messageId) 227 * or can be called on a part that does not yet have a valid messageId to set it. 228 */ updateMessageId(final String messageId)229 public void updateMessageId(final String messageId) { 230 Assert.isTrue(TextUtils.isEmpty(messageId) || TextUtils.isEmpty(mMessageId)); 231 mMessageId = messageId; 232 } 233 getMessageId(final Cursor cursor)234 protected static String getMessageId(final Cursor cursor) { 235 return cursor.getString(INDEX_MESSAGE_ID); 236 } 237 bind(final Cursor cursor)238 protected void bind(final Cursor cursor) { 239 mPartId = cursor.getString(INDEX_ID); 240 mMessageId = cursor.getString(INDEX_MESSAGE_ID); 241 mText = cursor.getString(INDEX_TEXT); 242 mContentUri = UriUtil.uriFromString(cursor.getString(INDEX_CONTENT_URI)); 243 mContentType = cursor.getString(INDEX_CONTENT_TYPE); 244 mWidth = cursor.getInt(INDEX_WIDTH); 245 mHeight = cursor.getInt(INDEX_HEIGHT); 246 } 247 populate(final ContentValues values)248 public final void populate(final ContentValues values) { 249 // Must have a valid messageId on a part 250 Assert.isTrue(!TextUtils.isEmpty(mMessageId)); 251 values.put(PartColumns.MESSAGE_ID, mMessageId); 252 values.put(PartColumns.TEXT, mText); 253 values.put(PartColumns.CONTENT_URI, UriUtil.stringFromUri(mContentUri)); 254 values.put(PartColumns.CONTENT_TYPE, mContentType); 255 if (mWidth != UNSPECIFIED_SIZE) { 256 values.put(PartColumns.WIDTH, mWidth); 257 } 258 if (mHeight != UNSPECIFIED_SIZE) { 259 values.put(PartColumns.HEIGHT, mHeight); 260 } 261 } 262 263 /** 264 * Note this is not thread safe so callers need to make sure they own the wrapper + statements 265 * while they call this and use the returned value. 266 */ getInsertStatement(final DatabaseWrapper db, final String conversationId)267 public SQLiteStatement getInsertStatement(final DatabaseWrapper db, 268 final String conversationId) { 269 final SQLiteStatement insert = db.getStatementInTransaction( 270 DatabaseWrapper.INDEX_INSERT_MESSAGE_PART, INSERT_MESSAGE_PART_SQL); 271 insert.clearBindings(); 272 insert.bindString(INDEX_MESSAGE_ID, mMessageId); 273 if (mText != null) { 274 insert.bindString(INDEX_TEXT, mText); 275 } 276 if (mContentUri != null) { 277 insert.bindString(INDEX_CONTENT_URI, mContentUri.toString()); 278 } 279 if (mContentType != null) { 280 insert.bindString(INDEX_CONTENT_TYPE, mContentType); 281 } 282 insert.bindLong(INDEX_WIDTH, mWidth); 283 insert.bindLong(INDEX_HEIGHT, mHeight); 284 insert.bindString(INDEX_CONVERSATION_ID, conversationId); 285 return insert; 286 } 287 getPartId()288 public final String getPartId() { 289 return mPartId; 290 } 291 getMessageId()292 public final String getMessageId() { 293 return mMessageId; 294 } 295 getText()296 public final String getText() { 297 return mText; 298 } 299 getContentUri()300 public final Uri getContentUri() { 301 return mContentUri; 302 } 303 isAttachment()304 public boolean isAttachment() { 305 return mContentUri != null; 306 } 307 isText()308 public boolean isText() { 309 return ContentType.isTextType(mContentType); 310 } 311 isImage()312 public boolean isImage() { 313 return ContentType.isImageType(mContentType); 314 } 315 isMedia()316 public boolean isMedia() { 317 return ContentType.isMediaType(mContentType); 318 } 319 isVCard()320 public boolean isVCard() { 321 return ContentType.isVCardType(mContentType); 322 } 323 isAudio()324 public boolean isAudio() { 325 return ContentType.isAudioType(mContentType); 326 } 327 isVideo()328 public boolean isVideo() { 329 return ContentType.isVideoType(mContentType); 330 } 331 getContentType()332 public final String getContentType() { 333 return mContentType; 334 } 335 getWidth()336 public final int getWidth() { 337 return mWidth; 338 } 339 getHeight()340 public final int getHeight() { 341 return mHeight; 342 } 343 isSupportedMediaType(final String contentType)344 public static boolean isSupportedMediaType(final String contentType) { 345 return ContentType.isVCardType(contentType) 346 || Arrays.asList(ACCEPTABLE_GALLERY_MEDIA_TYPES).contains(contentType); 347 } 348 349 /** 350 * 351 * @return true if this part can only exist by itself, with no other attachments 352 */ getSinglePartOnly()353 public boolean getSinglePartOnly() { 354 return mSinglePartOnly; 355 } 356 357 @Override describeContents()358 public int describeContents() { 359 return 0; 360 } 361 MessagePartData(final Parcel in)362 protected MessagePartData(final Parcel in) { 363 mMessageId = in.readString(); 364 mText = in.readString(); 365 mContentUri = UriUtil.uriFromString(in.readString()); 366 mContentType = in.readString(); 367 mWidth = in.readInt(); 368 mHeight = in.readInt(); 369 } 370 371 @Override writeToParcel(final Parcel dest, final int flags)372 public void writeToParcel(final Parcel dest, final int flags) { 373 Assert.isTrue(!mDestroyed); 374 dest.writeString(mMessageId); 375 dest.writeString(mText); 376 dest.writeString(UriUtil.stringFromUri(mContentUri)); 377 dest.writeString(mContentType); 378 dest.writeInt(mWidth); 379 dest.writeInt(mHeight); 380 } 381 382 @Override equals(Object o)383 public boolean equals(Object o) { 384 if (this == o) { 385 return true; 386 } 387 388 if (!(o instanceof MessagePartData)) { 389 return false; 390 } 391 392 MessagePartData lhs = (MessagePartData) o; 393 return mWidth == lhs.mWidth && mHeight == lhs.mHeight && 394 TextUtils.equals(mMessageId, lhs.mMessageId) && 395 TextUtils.equals(mText, lhs.mText) && 396 TextUtils.equals(mContentType, lhs.mContentType) && 397 (mContentUri == null ? lhs.mContentUri == null 398 : mContentUri.equals(lhs.mContentUri)); 399 } 400 hashCode()401 @Override public int hashCode() { 402 int result = 17; 403 result = 31 * result + mWidth; 404 result = 31 * result + mHeight; 405 result = 31 * result + (mMessageId == null ? 0 : mMessageId.hashCode()); 406 result = 31 * result + (mText == null ? 0 : mText.hashCode()); 407 result = 31 * result + (mContentType == null ? 0 : mContentType.hashCode()); 408 result = 31 * result + (mContentUri == null ? 0 : mContentUri.hashCode()); 409 return result; 410 } 411 412 public static final Parcelable.Creator<MessagePartData> CREATOR 413 = new Parcelable.Creator<MessagePartData>() { 414 @Override 415 public MessagePartData createFromParcel(final Parcel in) { 416 return new MessagePartData(in); 417 } 418 419 @Override 420 public MessagePartData[] newArray(final int size) { 421 return new MessagePartData[size]; 422 } 423 }; 424 shouldDestroy()425 protected Uri shouldDestroy() { 426 // We should never double-destroy. 427 Assert.isTrue(!mDestroyed); 428 mDestroyed = true; 429 Uri contentUri = mContentUri; 430 mContentUri = null; 431 mContentType = null; 432 // Only destroy the image if it's staged in our scratch space. 433 if (!MediaScratchFileProvider.isMediaScratchSpaceUri(contentUri)) { 434 contentUri = null; 435 } 436 return contentUri; 437 } 438 439 /** 440 * If application owns content associated with this part delete it (on background thread) 441 */ destroyAsync()442 public void destroyAsync() { 443 final Uri contentUri = shouldDestroy(); 444 if (contentUri != null) { 445 SafeAsyncTask.executeOnThreadPool(new Runnable() { 446 @Override 447 public void run() { 448 Factory.get().getApplicationContext().getContentResolver().delete( 449 contentUri, null, null); 450 } 451 }); 452 } 453 } 454 455 /** 456 * If application owns content associated with this part delete it 457 */ destroySync()458 public void destroySync() { 459 final Uri contentUri = shouldDestroy(); 460 if (contentUri != null) { 461 Factory.get().getApplicationContext().getContentResolver().delete( 462 contentUri, null, null); 463 } 464 } 465 466 /** 467 * If this is an image part, decode the image header and potentially save the size to the db. 468 */ decodeAndSaveSizeIfImage(final boolean saveToStorage)469 public void decodeAndSaveSizeIfImage(final boolean saveToStorage) { 470 if (isImage()) { 471 final Rect imageSize = ImageUtils.decodeImageBounds( 472 Factory.get().getApplicationContext(), mContentUri); 473 if (imageSize.width() != ImageRequest.UNSPECIFIED_SIZE && 474 imageSize.height() != ImageRequest.UNSPECIFIED_SIZE) { 475 mWidth = imageSize.width(); 476 mHeight = imageSize.height(); 477 if (saveToStorage) { 478 UpdateMessagePartSizeAction.updateSize(mPartId, mWidth, mHeight); 479 } 480 } 481 } 482 } 483 484 /** 485 * Computes the minimum size that this MessagePartData could be compressed/downsampled/encoded 486 * before sending to meet the maximum message size imposed by the carriers. This is used to 487 * determine right before sending a message whether a message could possibly be sent. If not 488 * then the user is given a chance to unselect some/all of the attachments. 489 * 490 * TODO: computing the minimum size could be expensive. Should we cache the 491 * computed value in db to be retrieved later? 492 * 493 * @return the carrier-independent minimum size, in bytes. 494 */ 495 @DoesNotRunOnMainThread getMinimumSizeInBytesForSending()496 public long getMinimumSizeInBytesForSending() { 497 Assert.isNotMainThread(); 498 if (!isAttachment()) { 499 // No limit is imposed on non-attachment part (i.e. plain text), so treat it as zero. 500 return NO_MINIMUM_SIZE; 501 } else if (isImage()) { 502 // GIFs are resized by the native transcoder (exposed by GifTranscoder). 503 if (ImageUtils.isGif(mContentType, mContentUri)) { 504 final long originalImageSize = UriUtil.getContentSize(mContentUri); 505 // Wish we could save the size here, but we don't have a part id yet 506 decodeAndSaveSizeIfImage(false /* saveToStorage */); 507 return GifTranscoder.canBeTranscoded(mWidth, mHeight) ? 508 GifTranscoder.estimateFileSizeAfterTranscode(originalImageSize) 509 : originalImageSize; 510 } 511 // Other images should be arbitrarily resized by ImageResizer before sending. 512 return MmsUtils.MIN_IMAGE_BYTE_SIZE; 513 } else if (isMedia()) { 514 // We can't compress attachments except images. 515 return UriUtil.getContentSize(mContentUri); 516 } else { 517 // This is some unknown media type that we don't know how to handle. Log an error 518 // and try sending it anyway. 519 LogUtil.e(LogUtil.BUGLE_DATAMODEL_TAG, "Unknown attachment type " + getContentType()); 520 return NO_MINIMUM_SIZE; 521 } 522 } 523 524 @Override toString()525 public String toString() { 526 if (isText()) { 527 return LogUtil.sanitizePII(getText()); 528 } else { 529 return getContentType() + " (" + getContentUri() + ")"; 530 } 531 } 532 533 /** 534 * 535 * @return true if this part can only exist by itself, with no other attachments 536 */ isSinglePartOnly()537 public boolean isSinglePartOnly() { 538 return mSinglePartOnly; 539 } 540 setSinglePartOnly(final boolean isSinglePartOnly)541 public void setSinglePartOnly(final boolean isSinglePartOnly) { 542 mSinglePartOnly = isSinglePartOnly; 543 } 544 } 545