| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.messaging.datamodel.data; |
| |
| import android.content.ContentValues; |
| import android.database.Cursor; |
| import android.database.sqlite.SQLiteStatement; |
| import android.graphics.Rect; |
| import android.net.Uri; |
| import android.os.Parcel; |
| import android.os.Parcelable; |
| import android.text.TextUtils; |
| |
| import com.android.messaging.Factory; |
| import com.android.messaging.datamodel.DatabaseHelper; |
| import com.android.messaging.datamodel.DatabaseHelper.PartColumns; |
| import com.android.messaging.datamodel.DatabaseWrapper; |
| import com.android.messaging.datamodel.MediaScratchFileProvider; |
| import com.android.messaging.datamodel.MessagingContentProvider; |
| import com.android.messaging.datamodel.action.UpdateMessagePartSizeAction; |
| import com.android.messaging.datamodel.media.ImageRequest; |
| import com.android.messaging.sms.MmsUtils; |
| import com.android.messaging.util.Assert; |
| import com.android.messaging.util.Assert.DoesNotRunOnMainThread; |
| import com.android.messaging.util.ContentType; |
| import com.android.messaging.util.GifTranscoder; |
| import com.android.messaging.util.ImageUtils; |
| import com.android.messaging.util.LogUtil; |
| import com.android.messaging.util.SafeAsyncTask; |
| import com.android.messaging.util.UriUtil; |
| |
| import java.util.Arrays; |
| import java.util.concurrent.TimeUnit; |
| |
| /** |
| * Represents a single message part. Messages consist of one or more parts which may contain |
| * either text or media. |
| */ |
| public class MessagePartData implements Parcelable { |
| public static final int UNSPECIFIED_SIZE = MessagingContentProvider.UNSPECIFIED_SIZE; |
| |
| public static final String[] ACCEPTABLE_GALLERY_MEDIA_TYPES = |
| new String[] { |
| // Acceptable image types |
| ContentType.IMAGE_JPEG, ContentType.IMAGE_JPG, ContentType.IMAGE_PNG, |
| ContentType.IMAGE_GIF, ContentType.IMAGE_WBMP, ContentType.IMAGE_X_MS_BMP, |
| // Acceptable video types |
| ContentType.VIDEO_3GP, ContentType.VIDEO_3GPP, ContentType.VIDEO_3G2, |
| ContentType.VIDEO_H263, ContentType.VIDEO_M4V, ContentType.VIDEO_MP4, |
| ContentType.VIDEO_MPEG, ContentType.VIDEO_MPEG4, ContentType.VIDEO_WEBM, |
| // Acceptable audio types |
| ContentType.AUDIO_MP3, ContentType.AUDIO_MP4, ContentType.AUDIO_MIDI, |
| ContentType.AUDIO_MID, ContentType.AUDIO_AMR, ContentType.AUDIO_X_WAV, |
| ContentType.AUDIO_AAC, ContentType.AUDIO_X_MIDI, ContentType.AUDIO_X_MID, |
| ContentType.AUDIO_X_MP3 |
| }; |
| |
| private static final String[] sProjection = { |
| PartColumns._ID, |
| PartColumns.MESSAGE_ID, |
| PartColumns.TEXT, |
| PartColumns.CONTENT_URI, |
| PartColumns.CONTENT_TYPE, |
| PartColumns.WIDTH, |
| PartColumns.HEIGHT, |
| }; |
| |
| private static final int INDEX_ID = 0; |
| private static final int INDEX_MESSAGE_ID = 1; |
| private static final int INDEX_TEXT = 2; |
| private static final int INDEX_CONTENT_URI = 3; |
| private static final int INDEX_CONTENT_TYPE = 4; |
| private static final int INDEX_WIDTH = 5; |
| private static final int INDEX_HEIGHT = 6; |
| // This isn't part of the projection |
| private static final int INDEX_CONVERSATION_ID = 7; |
| |
| // SQL statement to insert a "complete" message part row (columns based on projection above). |
| private static final String INSERT_MESSAGE_PART_SQL = |
| "INSERT INTO " + DatabaseHelper.PARTS_TABLE + " ( " |
| + TextUtils.join(",", Arrays.copyOfRange(sProjection, 1, INDEX_CONVERSATION_ID)) |
| + ", " + PartColumns.CONVERSATION_ID |
| + ") VALUES (?, ?, ?, ?, ?, ?, ?)"; |
| |
| // Used for stuff that's ignored or arbitrarily compressed. |
| private static final long NO_MINIMUM_SIZE = 0; |
| |
| private String mPartId; |
| private String mMessageId; |
| private String mText; |
| private Uri mContentUri; |
| private String mContentType; |
| private int mWidth; |
| private int mHeight; |
| // This kind of part can only be attached once and with no other attachment |
| private boolean mSinglePartOnly; |
| |
| /** Transient data: true if destroy was already called */ |
| private boolean mDestroyed; |
| |
| /** |
| * Create an "empty" message part |
| */ |
| protected MessagePartData() { |
| this(null, null, UNSPECIFIED_SIZE, UNSPECIFIED_SIZE); |
| } |
| |
| /** |
| * Create a populated text message part |
| */ |
| protected MessagePartData(final String messageText) { |
| this(null, messageText, ContentType.TEXT_PLAIN, null, UNSPECIFIED_SIZE, UNSPECIFIED_SIZE, |
| false /*singlePartOnly*/); |
| } |
| |
| /** |
| * Create a populated attachment message part |
| */ |
| protected MessagePartData(final String contentType, final Uri contentUri, |
| final int width, final int height) { |
| this(null, null, contentType, contentUri, width, height, false /*singlePartOnly*/); |
| } |
| |
| /** |
| * Create a populated attachment message part, with additional caption text |
| */ |
| protected MessagePartData(final String messageText, final String contentType, |
| final Uri contentUri, final int width, final int height) { |
| this(null, messageText, contentType, contentUri, width, height, false /*singlePartOnly*/); |
| } |
| |
| /** |
| * Create a populated attachment message part, with additional caption text, single part only |
| */ |
| protected MessagePartData(final String messageText, final String contentType, |
| final Uri contentUri, final int width, final int height, final boolean singlePartOnly) { |
| this(null, messageText, contentType, contentUri, width, height, singlePartOnly); |
| } |
| |
| /** |
| * Create a populated message part |
| */ |
| private MessagePartData(final String messageId, final String messageText, |
| final String contentType, final Uri contentUri, final int width, final int height, |
| final boolean singlePartOnly) { |
| mMessageId = messageId; |
| mText = messageText; |
| mContentType = contentType; |
| mContentUri = contentUri; |
| mWidth = width; |
| mHeight = height; |
| mSinglePartOnly = singlePartOnly; |
| } |
| |
| /** |
| * Create a "text" message part |
| */ |
| public static MessagePartData createTextMessagePart(final String messageText) { |
| return new MessagePartData(messageText); |
| } |
| |
| /** |
| * Create a "media" message part |
| */ |
| public static MessagePartData createMediaMessagePart(final String contentType, |
| final Uri contentUri, final int width, final int height) { |
| return new MessagePartData(contentType, contentUri, width, height); |
| } |
| |
| /** |
| * Create a "media" message part with caption |
| */ |
| public static MessagePartData createMediaMessagePart(final String caption, |
| final String contentType, final Uri contentUri, final int width, final int height) { |
| return new MessagePartData(null, caption, contentType, contentUri, width, height, |
| false /*singlePartOnly*/ |
| ); |
| } |
| |
| /** |
| * Create an empty "text" message part |
| */ |
| public static MessagePartData createEmptyMessagePart() { |
| return new MessagePartData(""); |
| } |
| |
| /** |
| * Creates a new message part reading from the cursor |
| */ |
| public static MessagePartData createFromCursor(final Cursor cursor) { |
| final MessagePartData part = new MessagePartData(); |
| part.bind(cursor); |
| return part; |
| } |
| |
| public static String[] getProjection() { |
| return sProjection; |
| } |
| |
| /** |
| * Updates the part id. |
| * Can be used to reset the partId just prior to persisting (which will assign a new partId) |
| * or can be called on a part that does not yet have a valid part id to set it. |
| */ |
| public void updatePartId(final String partId) { |
| Assert.isTrue(TextUtils.isEmpty(partId) || TextUtils.isEmpty(mPartId)); |
| mPartId = partId; |
| } |
| |
| /** |
| * Updates the messageId for the part. |
| * Can be used to reset the messageId prior to persisting (which will assign a new messageId) |
| * or can be called on a part that does not yet have a valid messageId to set it. |
| */ |
| public void updateMessageId(final String messageId) { |
| Assert.isTrue(TextUtils.isEmpty(messageId) || TextUtils.isEmpty(mMessageId)); |
| mMessageId = messageId; |
| } |
| |
| protected static String getMessageId(final Cursor cursor) { |
| return cursor.getString(INDEX_MESSAGE_ID); |
| } |
| |
| protected void bind(final Cursor cursor) { |
| mPartId = cursor.getString(INDEX_ID); |
| mMessageId = cursor.getString(INDEX_MESSAGE_ID); |
| mText = cursor.getString(INDEX_TEXT); |
| mContentUri = UriUtil.uriFromString(cursor.getString(INDEX_CONTENT_URI)); |
| mContentType = cursor.getString(INDEX_CONTENT_TYPE); |
| mWidth = cursor.getInt(INDEX_WIDTH); |
| mHeight = cursor.getInt(INDEX_HEIGHT); |
| } |
| |
| public final void populate(final ContentValues values) { |
| // Must have a valid messageId on a part |
| Assert.isTrue(!TextUtils.isEmpty(mMessageId)); |
| values.put(PartColumns.MESSAGE_ID, mMessageId); |
| values.put(PartColumns.TEXT, mText); |
| values.put(PartColumns.CONTENT_URI, UriUtil.stringFromUri(mContentUri)); |
| values.put(PartColumns.CONTENT_TYPE, mContentType); |
| if (mWidth != UNSPECIFIED_SIZE) { |
| values.put(PartColumns.WIDTH, mWidth); |
| } |
| if (mHeight != UNSPECIFIED_SIZE) { |
| values.put(PartColumns.HEIGHT, mHeight); |
| } |
| } |
| |
| /** |
| * Note this is not thread safe so callers need to make sure they own the wrapper + statements |
| * while they call this and use the returned value. |
| */ |
| public SQLiteStatement getInsertStatement(final DatabaseWrapper db, |
| final String conversationId) { |
| final SQLiteStatement insert = db.getStatementInTransaction( |
| DatabaseWrapper.INDEX_INSERT_MESSAGE_PART, INSERT_MESSAGE_PART_SQL); |
| insert.clearBindings(); |
| insert.bindString(INDEX_MESSAGE_ID, mMessageId); |
| if (mText != null) { |
| insert.bindString(INDEX_TEXT, mText); |
| } |
| if (mContentUri != null) { |
| insert.bindString(INDEX_CONTENT_URI, mContentUri.toString()); |
| } |
| if (mContentType != null) { |
| insert.bindString(INDEX_CONTENT_TYPE, mContentType); |
| } |
| insert.bindLong(INDEX_WIDTH, mWidth); |
| insert.bindLong(INDEX_HEIGHT, mHeight); |
| insert.bindString(INDEX_CONVERSATION_ID, conversationId); |
| return insert; |
| } |
| |
| public final String getPartId() { |
| return mPartId; |
| } |
| |
| public final String getMessageId() { |
| return mMessageId; |
| } |
| |
| public final String getText() { |
| return mText; |
| } |
| |
| public final Uri getContentUri() { |
| return mContentUri; |
| } |
| |
| public boolean isAttachment() { |
| return mContentUri != null; |
| } |
| |
| public boolean isText() { |
| return ContentType.isTextType(mContentType); |
| } |
| |
| public boolean isImage() { |
| return ContentType.isImageType(mContentType); |
| } |
| |
| public boolean isMedia() { |
| return ContentType.isMediaType(mContentType); |
| } |
| |
| public boolean isVCard() { |
| return ContentType.isVCardType(mContentType); |
| } |
| |
| public boolean isAudio() { |
| return ContentType.isAudioType(mContentType); |
| } |
| |
| public boolean isVideo() { |
| return ContentType.isVideoType(mContentType); |
| } |
| |
| public final String getContentType() { |
| return mContentType; |
| } |
| |
| public final int getWidth() { |
| return mWidth; |
| } |
| |
| public final int getHeight() { |
| return mHeight; |
| } |
| |
| public static boolean isSupportedMediaType(final String contentType) { |
| return ContentType.isVCardType(contentType) |
| || Arrays.asList(ACCEPTABLE_GALLERY_MEDIA_TYPES).contains(contentType); |
| } |
| |
| /** |
| * |
| * @return true if this part can only exist by itself, with no other attachments |
| */ |
| public boolean getSinglePartOnly() { |
| return mSinglePartOnly; |
| } |
| |
| @Override |
| public int describeContents() { |
| return 0; |
| } |
| |
| protected MessagePartData(final Parcel in) { |
| mMessageId = in.readString(); |
| mText = in.readString(); |
| mContentUri = UriUtil.uriFromString(in.readString()); |
| mContentType = in.readString(); |
| mWidth = in.readInt(); |
| mHeight = in.readInt(); |
| } |
| |
| @Override |
| public void writeToParcel(final Parcel dest, final int flags) { |
| Assert.isTrue(!mDestroyed); |
| dest.writeString(mMessageId); |
| dest.writeString(mText); |
| dest.writeString(UriUtil.stringFromUri(mContentUri)); |
| dest.writeString(mContentType); |
| dest.writeInt(mWidth); |
| dest.writeInt(mHeight); |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (this == o) { |
| return true; |
| } |
| |
| if (!(o instanceof MessagePartData)) { |
| return false; |
| } |
| |
| MessagePartData lhs = (MessagePartData) o; |
| return mWidth == lhs.mWidth && mHeight == lhs.mHeight && |
| TextUtils.equals(mMessageId, lhs.mMessageId) && |
| TextUtils.equals(mText, lhs.mText) && |
| TextUtils.equals(mContentType, lhs.mContentType) && |
| (mContentUri == null ? lhs.mContentUri == null |
| : mContentUri.equals(lhs.mContentUri)); |
| } |
| |
| @Override public int hashCode() { |
| int result = 17; |
| result = 31 * result + mWidth; |
| result = 31 * result + mHeight; |
| result = 31 * result + (mMessageId == null ? 0 : mMessageId.hashCode()); |
| result = 31 * result + (mText == null ? 0 : mText.hashCode()); |
| result = 31 * result + (mContentType == null ? 0 : mContentType.hashCode()); |
| result = 31 * result + (mContentUri == null ? 0 : mContentUri.hashCode()); |
| return result; |
| } |
| |
| public static final Parcelable.Creator<MessagePartData> CREATOR |
| = new Parcelable.Creator<MessagePartData>() { |
| @Override |
| public MessagePartData createFromParcel(final Parcel in) { |
| return new MessagePartData(in); |
| } |
| |
| @Override |
| public MessagePartData[] newArray(final int size) { |
| return new MessagePartData[size]; |
| } |
| }; |
| |
| protected Uri shouldDestroy() { |
| // We should never double-destroy. |
| Assert.isTrue(!mDestroyed); |
| mDestroyed = true; |
| Uri contentUri = mContentUri; |
| mContentUri = null; |
| mContentType = null; |
| // Only destroy the image if it's staged in our scratch space. |
| if (!MediaScratchFileProvider.isMediaScratchSpaceUri(contentUri)) { |
| contentUri = null; |
| } |
| return contentUri; |
| } |
| |
| /** |
| * If application owns content associated with this part delete it (on background thread) |
| */ |
| public void destroyAsync() { |
| final Uri contentUri = shouldDestroy(); |
| if (contentUri != null) { |
| SafeAsyncTask.executeOnThreadPool(new Runnable() { |
| @Override |
| public void run() { |
| Factory.get().getApplicationContext().getContentResolver().delete( |
| contentUri, null, null); |
| } |
| }); |
| } |
| } |
| |
| /** |
| * If application owns content associated with this part delete it |
| */ |
| public void destroySync() { |
| final Uri contentUri = shouldDestroy(); |
| if (contentUri != null) { |
| Factory.get().getApplicationContext().getContentResolver().delete( |
| contentUri, null, null); |
| } |
| } |
| |
| /** |
| * If this is an image part, decode the image header and potentially save the size to the db. |
| */ |
| public void decodeAndSaveSizeIfImage(final boolean saveToStorage) { |
| if (isImage()) { |
| final Rect imageSize = ImageUtils.decodeImageBounds( |
| Factory.get().getApplicationContext(), mContentUri); |
| if (imageSize.width() != ImageRequest.UNSPECIFIED_SIZE && |
| imageSize.height() != ImageRequest.UNSPECIFIED_SIZE) { |
| mWidth = imageSize.width(); |
| mHeight = imageSize.height(); |
| if (saveToStorage) { |
| UpdateMessagePartSizeAction.updateSize(mPartId, mWidth, mHeight); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Computes the minimum size that this MessagePartData could be compressed/downsampled/encoded |
| * before sending to meet the maximum message size imposed by the carriers. This is used to |
| * determine right before sending a message whether a message could possibly be sent. If not |
| * then the user is given a chance to unselect some/all of the attachments. |
| * |
| * TODO: computing the minimum size could be expensive. Should we cache the |
| * computed value in db to be retrieved later? |
| * |
| * @return the carrier-independent minimum size, in bytes. |
| */ |
| @DoesNotRunOnMainThread |
| public long getMinimumSizeInBytesForSending() { |
| Assert.isNotMainThread(); |
| if (!isAttachment()) { |
| // No limit is imposed on non-attachment part (i.e. plain text), so treat it as zero. |
| return NO_MINIMUM_SIZE; |
| } else if (isImage()) { |
| // GIFs are resized by the native transcoder (exposed by GifTranscoder). |
| if (ImageUtils.isGif(mContentType, mContentUri)) { |
| final long originalImageSize = UriUtil.getContentSize(mContentUri); |
| // Wish we could save the size here, but we don't have a part id yet |
| decodeAndSaveSizeIfImage(false /* saveToStorage */); |
| return GifTranscoder.canBeTranscoded(mWidth, mHeight) ? |
| GifTranscoder.estimateFileSizeAfterTranscode(originalImageSize) |
| : originalImageSize; |
| } |
| // Other images should be arbitrarily resized by ImageResizer before sending. |
| return MmsUtils.MIN_IMAGE_BYTE_SIZE; |
| } else if (isMedia()) { |
| // We can't compress attachments except images. |
| return UriUtil.getContentSize(mContentUri); |
| } else { |
| // This is some unknown media type that we don't know how to handle. Log an error |
| // and try sending it anyway. |
| LogUtil.e(LogUtil.BUGLE_DATAMODEL_TAG, "Unknown attachment type " + getContentType()); |
| return NO_MINIMUM_SIZE; |
| } |
| } |
| |
| @Override |
| public String toString() { |
| if (isText()) { |
| return LogUtil.sanitizePII(getText()); |
| } else { |
| return getContentType() + " (" + getContentUri() + ")"; |
| } |
| } |
| |
| /** |
| * |
| * @return true if this part can only exist by itself, with no other attachments |
| */ |
| public boolean isSinglePartOnly() { |
| return mSinglePartOnly; |
| } |
| |
| public void setSinglePartOnly(final boolean isSinglePartOnly) { |
| mSinglePartOnly = isSinglePartOnly; |
| } |
| } |