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
17package com.android.messaging.datamodel.data;
18
19import android.content.ContentValues;
20import android.database.Cursor;
21import android.database.sqlite.SQLiteStatement;
22import android.graphics.Rect;
23import android.net.Uri;
24import android.os.Parcel;
25import android.os.Parcelable;
26import android.text.TextUtils;
27
28import com.android.messaging.Factory;
29import com.android.messaging.datamodel.DatabaseHelper;
30import com.android.messaging.datamodel.DatabaseHelper.PartColumns;
31import com.android.messaging.datamodel.DatabaseWrapper;
32import com.android.messaging.datamodel.MediaScratchFileProvider;
33import com.android.messaging.datamodel.MessagingContentProvider;
34import com.android.messaging.datamodel.action.UpdateMessagePartSizeAction;
35import com.android.messaging.datamodel.media.ImageRequest;
36import com.android.messaging.sms.MmsUtils;
37import com.android.messaging.util.Assert;
38import com.android.messaging.util.Assert.DoesNotRunOnMainThread;
39import com.android.messaging.util.ContentType;
40import com.android.messaging.util.GifTranscoder;
41import com.android.messaging.util.ImageUtils;
42import com.android.messaging.util.LogUtil;
43import com.android.messaging.util.SafeAsyncTask;
44import com.android.messaging.util.UriUtil;
45
46import java.util.Arrays;
47import 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 */
53public 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