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 */ 105 protected MessagePartData() { 106 this(null, null, UNSPECIFIED_SIZE, UNSPECIFIED_SIZE); 107 } 108 109 /** 110 * Create a populated text message part 111 */ 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 */ 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 */ 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 */ 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 */ 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 */ 159 public static MessagePartData createTextMessagePart(final String messageText) { 160 return new MessagePartData(messageText); 161 } 162 163 /** 164 * Create a "media" message part 165 */ 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 */ 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 */ 184 public static MessagePartData createEmptyMessagePart() { 185 return new MessagePartData(""); 186 } 187 188 /** 189 * Creates a new message part reading from the cursor 190 */ 191 public static MessagePartData createFromCursor(final Cursor cursor) { 192 final MessagePartData part = new MessagePartData(); 193 part.bind(cursor); 194 return part; 195 } 196 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 */ 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 */ 216 public void updateMessageId(final String messageId) { 217 Assert.isTrue(TextUtils.isEmpty(messageId) || TextUtils.isEmpty(mMessageId)); 218 mMessageId = messageId; 219 } 220 221 protected static String getMessageId(final Cursor cursor) { 222 return cursor.getString(INDEX_MESSAGE_ID); 223 } 224 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 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 */ 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 275 public final String getPartId() { 276 return mPartId; 277 } 278 279 public final String getMessageId() { 280 return mMessageId; 281 } 282 283 public final String getText() { 284 return mText; 285 } 286 287 public final Uri getContentUri() { 288 return mContentUri; 289 } 290 291 public boolean isAttachment() { 292 return mContentUri != null; 293 } 294 295 public boolean isText() { 296 return ContentType.isTextType(mContentType); 297 } 298 299 public boolean isImage() { 300 return ContentType.isImageType(mContentType); 301 } 302 303 public boolean isMedia() { 304 return ContentType.isMediaType(mContentType); 305 } 306 307 public boolean isVCard() { 308 return ContentType.isVCardType(mContentType); 309 } 310 311 public boolean isAudio() { 312 return ContentType.isAudioType(mContentType); 313 } 314 315 public boolean isVideo() { 316 return ContentType.isVideoType(mContentType); 317 } 318 319 public final String getContentType() { 320 return mContentType; 321 } 322 323 public final int getWidth() { 324 return mWidth; 325 } 326 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 */ 335 public boolean getSinglePartOnly() { 336 return mSinglePartOnly; 337 } 338 339 @Override 340 public int describeContents() { 341 return 0; 342 } 343 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 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 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 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 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 */ 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 */ 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 */ 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 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 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 */ 527 public boolean isSinglePartOnly() { 528 return mSinglePartOnly; 529 } 530 531 public void setSinglePartOnly(final boolean isSinglePartOnly) { 532 mSinglePartOnly = isSinglePartOnly; 533 } 534 } 535