Home | History | Annotate | Download | only in data
      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