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