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