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