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.sms;
18 
19 import android.content.ContentResolver;
20 import android.content.ContentUris;
21 import android.content.Context;
22 import android.content.res.AssetFileDescriptor;
23 import android.database.Cursor;
24 import android.graphics.Bitmap;
25 import android.graphics.BitmapFactory;
26 import android.media.MediaMetadataRetriever;
27 import android.net.Uri;
28 import android.os.Parcel;
29 import android.os.Parcelable;
30 import android.provider.Telephony.Mms;
31 import android.provider.Telephony.Sms;
32 import android.text.TextUtils;
33 import android.util.Log;
34 import android.webkit.MimeTypeMap;
35 
36 import com.android.messaging.Factory;
37 import com.android.messaging.datamodel.data.MessageData;
38 import com.android.messaging.datamodel.media.VideoThumbnailRequest;
39 import com.android.messaging.mmslib.pdu.CharacterSets;
40 import com.android.messaging.util.Assert;
41 import com.android.messaging.util.ContentType;
42 import com.android.messaging.util.LogUtil;
43 import com.android.messaging.util.MediaMetadataRetrieverWrapper;
44 import com.android.messaging.util.OsUtil;
45 import com.android.messaging.util.PhoneUtils;
46 import com.google.common.collect.Lists;
47 
48 import java.io.ByteArrayOutputStream;
49 import java.io.FileNotFoundException;
50 import java.io.IOException;
51 import java.io.InputStream;
52 import java.io.UnsupportedEncodingException;
53 import java.util.ArrayList;
54 import java.util.List;
55 
56 /**
57  * Class contains various SMS/MMS database entities from telephony provider
58  */
59 public class DatabaseMessages {
60     private static final String TAG = LogUtil.BUGLE_TAG;
61 
62     public abstract static class DatabaseMessage {
getProtocol()63         public abstract int getProtocol();
getUri()64         public abstract String getUri();
getTimestampInMillis()65         public abstract long getTimestampInMillis();
66 
67         @Override
equals(final Object other)68         public boolean equals(final Object other) {
69             if (other == null || !(other instanceof DatabaseMessage)) {
70                 return false;
71             }
72             final DatabaseMessage otherDbMsg = (DatabaseMessage) other;
73             // No need to check timestamp since we only need this when we compare
74             // messages at the same timestamp
75             return TextUtils.equals(getUri(), otherDbMsg.getUri());
76         }
77 
78         @Override
hashCode()79         public int hashCode() {
80             // No need to check timestamp since we only need this when we compare
81             // messages at the same timestamp
82             return getUri().hashCode();
83         }
84     }
85 
86     /**
87      * SMS message
88      */
89     public static class SmsMessage extends DatabaseMessage implements Parcelable {
90         private static int sIota = 0;
91         public static final int INDEX_ID = sIota++;
92         public static final int INDEX_TYPE = sIota++;
93         public static final int INDEX_ADDRESS = sIota++;
94         public static final int INDEX_BODY = sIota++;
95         public static final int INDEX_DATE = sIota++;
96         public static final int INDEX_THREAD_ID = sIota++;
97         public static final int INDEX_STATUS = sIota++;
98         public static final int INDEX_READ = sIota++;
99         public static final int INDEX_SEEN = sIota++;
100         public static final int INDEX_DATE_SENT = sIota++;
101         public static final int INDEX_SUB_ID = sIota++;
102 
103         private static String[] sProjection;
104 
getProjection()105         public static String[] getProjection() {
106             if (sProjection == null) {
107                 String[] projection = new String[] {
108                         Sms._ID,
109                         Sms.TYPE,
110                         Sms.ADDRESS,
111                         Sms.BODY,
112                         Sms.DATE,
113                         Sms.THREAD_ID,
114                         Sms.STATUS,
115                         Sms.READ,
116                         Sms.SEEN,
117                         Sms.DATE_SENT,
118                         Sms.SUBSCRIPTION_ID,
119                     };
120                 if (!MmsUtils.hasSmsDateSentColumn()) {
121                     projection[INDEX_DATE_SENT] = Sms.DATE;
122                 }
123                 if (!OsUtil.isAtLeastL_MR1()) {
124                     Assert.equals(INDEX_SUB_ID, projection.length - 1);
125                     String[] withoutSubId = new String[projection.length - 1];
126                     System.arraycopy(projection, 0, withoutSubId, 0, withoutSubId.length);
127                     projection = withoutSubId;
128                 }
129 
130                 sProjection = projection;
131             }
132 
133             return sProjection;
134         }
135 
136         public String mUri;
137         public String mAddress;
138         public String mBody;
139         private long mRowId;
140         public long mTimestampInMillis;
141         public long mTimestampSentInMillis;
142         public int mType;
143         public long mThreadId;
144         public int mStatus;
145         public boolean mRead;
146         public boolean mSeen;
147         public int mSubId;
148 
SmsMessage()149         private SmsMessage() {
150         }
151 
152         /**
153          * Load from a cursor of a query that returns the SMS to import
154          *
155          * @param cursor
156          */
load(final Cursor cursor)157         private void load(final Cursor cursor) {
158             mRowId = cursor.getLong(INDEX_ID);
159             mAddress = cursor.getString(INDEX_ADDRESS);
160             mBody = cursor.getString(INDEX_BODY);
161             mTimestampInMillis = cursor.getLong(INDEX_DATE);
162             // Before ICS, there is no "date_sent" so use copy of "date" value
163             mTimestampSentInMillis = cursor.getLong(INDEX_DATE_SENT);
164             mType = cursor.getInt(INDEX_TYPE);
165             mThreadId = cursor.getLong(INDEX_THREAD_ID);
166             mStatus = cursor.getInt(INDEX_STATUS);
167             mRead = cursor.getInt(INDEX_READ) == 0 ? false : true;
168             mSeen = cursor.getInt(INDEX_SEEN) == 0 ? false : true;
169             mUri = ContentUris.withAppendedId(Sms.CONTENT_URI, mRowId).toString();
170             mSubId = PhoneUtils.getDefault().getSubIdFromTelephony(cursor, INDEX_SUB_ID);
171         }
172 
173         /**
174          * Get a new SmsMessage by loading from the cursor of a query
175          * that returns the SMS to import
176          *
177          * @param cursor
178          * @return
179          */
get(final Cursor cursor)180         public static SmsMessage get(final Cursor cursor) {
181             final SmsMessage msg = new SmsMessage();
182             msg.load(cursor);
183             return msg;
184         }
185 
186         @Override
getUri()187         public String getUri() {
188             return mUri;
189         }
190 
getSubId()191         public int getSubId() {
192             return mSubId;
193         }
194 
195         @Override
getProtocol()196         public int getProtocol() {
197             return MessageData.PROTOCOL_SMS;
198         }
199 
200         @Override
getTimestampInMillis()201         public long getTimestampInMillis() {
202             return mTimestampInMillis;
203         }
204 
205         @Override
describeContents()206         public int describeContents() {
207             return 0;
208         }
209 
SmsMessage(final Parcel in)210         private SmsMessage(final Parcel in) {
211             mUri = in.readString();
212             mRowId = in.readLong();
213             mTimestampInMillis = in.readLong();
214             mTimestampSentInMillis = in.readLong();
215             mType = in.readInt();
216             mThreadId = in.readLong();
217             mStatus = in.readInt();
218             mRead = in.readInt() != 0;
219             mSeen = in.readInt() != 0;
220             mSubId = in.readInt();
221 
222             // SMS specific
223             mAddress = in.readString();
224             mBody = in.readString();
225         }
226 
227         public static final Parcelable.Creator<SmsMessage> CREATOR
228                 = new Parcelable.Creator<SmsMessage>() {
229             @Override
230             public SmsMessage createFromParcel(final Parcel in) {
231                 return new SmsMessage(in);
232             }
233 
234             @Override
235             public SmsMessage[] newArray(final int size) {
236                 return new SmsMessage[size];
237             }
238         };
239 
240         @Override
writeToParcel(final Parcel out, final int flags)241         public void writeToParcel(final Parcel out, final int flags) {
242             out.writeString(mUri);
243             out.writeLong(mRowId);
244             out.writeLong(mTimestampInMillis);
245             out.writeLong(mTimestampSentInMillis);
246             out.writeInt(mType);
247             out.writeLong(mThreadId);
248             out.writeInt(mStatus);
249             out.writeInt(mRead ? 1 : 0);
250             out.writeInt(mSeen ? 1 : 0);
251             out.writeInt(mSubId);
252 
253             // SMS specific
254             out.writeString(mAddress);
255             out.writeString(mBody);
256         }
257     }
258 
259     /**
260      * MMS message
261      */
262     public static class MmsMessage extends DatabaseMessage implements Parcelable {
263         private static int sIota = 0;
264         public static final int INDEX_ID = sIota++;
265         public static final int INDEX_MESSAGE_BOX = sIota++;
266         public static final int INDEX_SUBJECT = sIota++;
267         public static final int INDEX_SUBJECT_CHARSET = sIota++;
268         public static final int INDEX_MESSAGE_SIZE = sIota++;
269         public static final int INDEX_DATE = sIota++;
270         public static final int INDEX_DATE_SENT = sIota++;
271         public static final int INDEX_THREAD_ID = sIota++;
272         public static final int INDEX_PRIORITY = sIota++;
273         public static final int INDEX_STATUS = sIota++;
274         public static final int INDEX_READ = sIota++;
275         public static final int INDEX_SEEN = sIota++;
276         public static final int INDEX_CONTENT_LOCATION = sIota++;
277         public static final int INDEX_TRANSACTION_ID = sIota++;
278         public static final int INDEX_MESSAGE_TYPE = sIota++;
279         public static final int INDEX_EXPIRY = sIota++;
280         public static final int INDEX_RESPONSE_STATUS = sIota++;
281         public static final int INDEX_RETRIEVE_STATUS = sIota++;
282         public static final int INDEX_SUB_ID = sIota++;
283 
284         private static String[] sProjection;
285 
getProjection()286         public static String[] getProjection() {
287             if (sProjection == null) {
288                 String[] projection = new String[] {
289                     Mms._ID,
290                     Mms.MESSAGE_BOX,
291                     Mms.SUBJECT,
292                     Mms.SUBJECT_CHARSET,
293                     Mms.MESSAGE_SIZE,
294                     Mms.DATE,
295                     Mms.DATE_SENT,
296                     Mms.THREAD_ID,
297                     Mms.PRIORITY,
298                     Mms.STATUS,
299                     Mms.READ,
300                     Mms.SEEN,
301                     Mms.CONTENT_LOCATION,
302                     Mms.TRANSACTION_ID,
303                     Mms.MESSAGE_TYPE,
304                     Mms.EXPIRY,
305                     Mms.RESPONSE_STATUS,
306                     Mms.RETRIEVE_STATUS,
307                     Mms.SUBSCRIPTION_ID,
308                 };
309 
310                 if (!OsUtil.isAtLeastL_MR1()) {
311                     Assert.equals(INDEX_SUB_ID, projection.length - 1);
312                     String[] withoutSubId = new String[projection.length - 1];
313                     System.arraycopy(projection, 0, withoutSubId, 0, withoutSubId.length);
314                     projection = withoutSubId;
315                 }
316 
317                 sProjection = projection;
318             }
319 
320             return sProjection;
321         }
322 
323         public String mUri;
324         private long mRowId;
325         public int mType;
326         public String mSubject;
327         public int mSubjectCharset;
328         private long mSize;
329         public long mTimestampInMillis;
330         public long mSentTimestampInMillis;
331         public long mThreadId;
332         public int mPriority;
333         public int mStatus;
334         public boolean mRead;
335         public boolean mSeen;
336         public String mContentLocation;
337         public String mTransactionId;
338         public int mMmsMessageType;
339         public long mExpiryInMillis;
340         public int mSubId;
341         public String mSender;
342         public int mResponseStatus;
343         public int mRetrieveStatus;
344 
345         public List<MmsPart> mParts = Lists.newArrayList();
346         private boolean mPartsProcessed = false;
347 
MmsMessage()348         private MmsMessage() {
349         }
350 
351         /**
352          * Load from a cursor of a query that returns the MMS to import
353          *
354          * @param cursor
355          */
load(final Cursor cursor)356         public void load(final Cursor cursor) {
357             mRowId = cursor.getLong(INDEX_ID);
358             mType = cursor.getInt(INDEX_MESSAGE_BOX);
359             mSubject = cursor.getString(INDEX_SUBJECT);
360             mSubjectCharset = cursor.getInt(INDEX_SUBJECT_CHARSET);
361             if (!TextUtils.isEmpty(mSubject)) {
362                 // PduPersister stores the subject using ISO_8859_1
363                 // Let's load it using that encoding and convert it back to its original
364                 // See PduPersister.persist and PduPersister.toIsoString
365                 // (Refer to bug b/11162476)
366                 mSubject = getDecodedString(
367                         getStringBytes(mSubject, CharacterSets.ISO_8859_1), mSubjectCharset);
368             }
369             mSize = cursor.getLong(INDEX_MESSAGE_SIZE);
370             // MMS db times are in seconds
371             mTimestampInMillis = cursor.getLong(INDEX_DATE) * 1000;
372             mSentTimestampInMillis = cursor.getLong(INDEX_DATE_SENT) * 1000;
373             mThreadId = cursor.getLong(INDEX_THREAD_ID);
374             mPriority = cursor.getInt(INDEX_PRIORITY);
375             mStatus = cursor.getInt(INDEX_STATUS);
376             mRead = cursor.getInt(INDEX_READ) == 0 ? false : true;
377             mSeen = cursor.getInt(INDEX_SEEN) == 0 ? false : true;
378             mContentLocation = cursor.getString(INDEX_CONTENT_LOCATION);
379             mTransactionId = cursor.getString(INDEX_TRANSACTION_ID);
380             mMmsMessageType = cursor.getInt(INDEX_MESSAGE_TYPE);
381             mExpiryInMillis = cursor.getLong(INDEX_EXPIRY) * 1000;
382             mResponseStatus = cursor.getInt(INDEX_RESPONSE_STATUS);
383             mRetrieveStatus = cursor.getInt(INDEX_RETRIEVE_STATUS);
384             // Clear all parts in case we reuse this object
385             mParts.clear();
386             mPartsProcessed = false;
387             mUri = ContentUris.withAppendedId(Mms.CONTENT_URI, mRowId).toString();
388             mSubId = PhoneUtils.getDefault().getSubIdFromTelephony(cursor, INDEX_SUB_ID);
389         }
390 
391         /**
392          * Get a new MmsMessage by loading from the cursor of a query
393          * that returns the MMS to import
394          *
395          * @param cursor
396          * @return
397          */
get(final Cursor cursor)398         public static MmsMessage get(final Cursor cursor) {
399             final MmsMessage msg = new MmsMessage();
400             msg.load(cursor);
401             return msg;
402         }
403         /**
404          * Add a loaded MMS part
405          *
406          * @param part
407          */
addPart(final MmsPart part)408         public void addPart(final MmsPart part) {
409             mParts.add(part);
410         }
411 
getParts()412         public List<MmsPart> getParts() {
413             return mParts;
414         }
415 
getSize()416         public long getSize() {
417             if (!mPartsProcessed) {
418                 processParts();
419             }
420             return mSize;
421         }
422 
423         /**
424          * Process loaded MMS parts to obtain the combined text, the combined attachment url,
425          * the combined content type and the combined size.
426          */
processParts()427         private void processParts() {
428             if (mPartsProcessed) {
429                 return;
430             }
431             mPartsProcessed = true;
432             // Remember the width and height of the first media part
433             // These are needed when building attachment list
434             long sizeOfParts = 0L;
435             for (final MmsPart part : mParts) {
436                 sizeOfParts += part.mSize;
437             }
438             if (mSize <= 0) {
439                 mSize = mSubject != null ? mSubject.getBytes().length : 0L;
440                 mSize += sizeOfParts;
441             }
442         }
443 
444         @Override
getUri()445         public String getUri() {
446             return mUri;
447         }
448 
getId()449         public long getId() {
450             return mRowId;
451         }
452 
getSubId()453         public int getSubId() {
454             return mSubId;
455         }
456 
457         @Override
getProtocol()458         public int getProtocol() {
459             return MessageData.PROTOCOL_MMS;
460         }
461 
462         @Override
getTimestampInMillis()463         public long getTimestampInMillis() {
464             return mTimestampInMillis;
465         }
466 
467         @Override
describeContents()468         public int describeContents() {
469             return 0;
470         }
471 
setSender(final String sender)472         public void setSender(final String sender) {
473             mSender = sender;
474         }
475 
MmsMessage(final Parcel in)476         private MmsMessage(final Parcel in) {
477             mUri = in.readString();
478             mRowId = in.readLong();
479             mTimestampInMillis = in.readLong();
480             mSentTimestampInMillis = in.readLong();
481             mType = in.readInt();
482             mThreadId = in.readLong();
483             mStatus = in.readInt();
484             mRead = in.readInt() != 0;
485             mSeen = in.readInt() != 0;
486             mSubId = in.readInt();
487 
488             // MMS specific
489             mSubject = in.readString();
490             mContentLocation = in.readString();
491             mTransactionId = in.readString();
492             mSender = in.readString();
493 
494             mSize = in.readLong();
495             mExpiryInMillis = in.readLong();
496 
497             mSubjectCharset = in.readInt();
498             mPriority = in.readInt();
499             mMmsMessageType = in.readInt();
500             mResponseStatus = in.readInt();
501             mRetrieveStatus = in.readInt();
502 
503             final int nParts = in.readInt();
504             mParts = new ArrayList<MmsPart>();
505             mPartsProcessed = false;
506             for (int i = 0; i < nParts; i++) {
507                 mParts.add((MmsPart) in.readParcelable(getClass().getClassLoader()));
508             }
509         }
510 
511         public static final Parcelable.Creator<MmsMessage> CREATOR
512                 = new Parcelable.Creator<MmsMessage>() {
513             @Override
514             public MmsMessage createFromParcel(final Parcel in) {
515                 return new MmsMessage(in);
516             }
517 
518             @Override
519             public MmsMessage[] newArray(final int size) {
520                 return new MmsMessage[size];
521             }
522         };
523 
524         @Override
writeToParcel(final Parcel out, final int flags)525         public void writeToParcel(final Parcel out, final int flags) {
526             out.writeString(mUri);
527             out.writeLong(mRowId);
528             out.writeLong(mTimestampInMillis);
529             out.writeLong(mSentTimestampInMillis);
530             out.writeInt(mType);
531             out.writeLong(mThreadId);
532             out.writeInt(mStatus);
533             out.writeInt(mRead ? 1 : 0);
534             out.writeInt(mSeen ? 1 : 0);
535             out.writeInt(mSubId);
536 
537             out.writeString(mSubject);
538             out.writeString(mContentLocation);
539             out.writeString(mTransactionId);
540             out.writeString(mSender);
541 
542             out.writeLong(mSize);
543             out.writeLong(mExpiryInMillis);
544 
545             out.writeInt(mSubjectCharset);
546             out.writeInt(mPriority);
547             out.writeInt(mMmsMessageType);
548             out.writeInt(mResponseStatus);
549             out.writeInt(mRetrieveStatus);
550 
551             out.writeInt(mParts.size());
552             for (final MmsPart part : mParts) {
553                 out.writeParcelable(part, 0);
554             }
555         }
556     }
557 
558     /**
559      * Part of an MMS message
560      */
561     public static class MmsPart implements Parcelable {
562         public static final String[] PROJECTION = new String[] {
563             Mms.Part._ID,
564             Mms.Part.MSG_ID,
565             Mms.Part.CHARSET,
566             Mms.Part.CONTENT_TYPE,
567             Mms.Part.TEXT,
568         };
569         private static int sIota = 0;
570         public static final int INDEX_ID = sIota++;
571         public static final int INDEX_MSG_ID = sIota++;
572         public static final int INDEX_CHARSET = sIota++;
573         public static final int INDEX_CONTENT_TYPE = sIota++;
574         public static final int INDEX_TEXT = sIota++;
575 
576         public String mUri;
577         public long mRowId;
578         public long mMessageId;
579         public String mContentType;
580         public String mText;
581         public int mCharset;
582         private int mWidth;
583         private int mHeight;
584         public long mSize;
585 
MmsPart()586         private MmsPart() {
587         }
588 
589         /**
590          * Load from a cursor of a query that returns the MMS part to import
591          *
592          * @param cursor
593          */
load(final Cursor cursor, final boolean loadMedia)594         public void load(final Cursor cursor, final boolean loadMedia) {
595             mRowId = cursor.getLong(INDEX_ID);
596             mMessageId = cursor.getLong(INDEX_MSG_ID);
597             mContentType = cursor.getString(INDEX_CONTENT_TYPE);
598             mText = cursor.getString(INDEX_TEXT);
599             mCharset = cursor.getInt(INDEX_CHARSET);
600             mWidth = 0;
601             mHeight = 0;
602             mSize = 0;
603             if (isMedia()) {
604                 // For importing we don't load media since performance is critical
605                 // For loading when we receive mms, we do load media to get enough
606                 // information of the media file
607                 if (loadMedia) {
608                     if (ContentType.isImageType(mContentType)) {
609                         loadImage();
610                     } else if (ContentType.isVideoType(mContentType)) {
611                         loadVideo();
612                     } // No need to load audio for parsing
613                     mSize = MmsUtils.getMediaFileSize(getDataUri());
614                 }
615             } else {
616                 // Load text if not media type
617                 loadText();
618             }
619             mUri = Uri.withAppendedPath(Mms.CONTENT_URI, cursor.getString(INDEX_ID)).toString();
620         }
621 
622         /**
623          * Get content type from file extension
624          */
extractContentType(final Context context, final Uri uri)625         private static String extractContentType(final Context context, final Uri uri) {
626             final String path = uri.getPath();
627             final MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
628             String extension = MimeTypeMap.getFileExtensionFromUrl(path);
629             if (TextUtils.isEmpty(extension)) {
630                 // getMimeTypeFromExtension() doesn't handle spaces in filenames nor can it handle
631                 // urlEncoded strings. Let's try one last time at finding the extension.
632                 final int dotPos = path.lastIndexOf('.');
633                 if (0 <= dotPos) {
634                     extension = path.substring(dotPos + 1);
635                 }
636             }
637             return mimeTypeMap.getMimeTypeFromExtension(extension);
638         }
639 
640         /**
641          * Get text of a text part
642          */
loadText()643         private void loadText() {
644             byte[] data = null;
645             if (isEmbeddedTextType()) {
646                 // Embedded text, get from the "text" column
647                 if (!TextUtils.isEmpty(mText)) {
648                     data = getStringBytes(mText, mCharset);
649                 }
650             } else {
651                 // Not embedded, load from disk
652                 final ContentResolver resolver =
653                         Factory.get().getApplicationContext().getContentResolver();
654                 final Uri uri = getDataUri();
655                 InputStream is = null;
656                 final ByteArrayOutputStream baos = new ByteArrayOutputStream();
657                 try {
658                     is = resolver.openInputStream(uri);
659                     final byte[] buffer = new byte[256];
660                     int len = is.read(buffer);
661                     while (len >= 0) {
662                         baos.write(buffer, 0, len);
663                         len = is.read(buffer);
664                     }
665                 } catch (final IOException e) {
666                     LogUtil.e(TAG,
667                             "DatabaseMessages.MmsPart: loading text from file failed: " + e, e);
668                 } finally {
669                     if (is != null) {
670                         try {
671                             is.close();
672                         } catch (final IOException e) {
673                             LogUtil.e(TAG, "DatabaseMessages.MmsPart: close file failed: " + e, e);
674                         }
675                     }
676                 }
677                 data = baos.toByteArray();
678             }
679             if (data != null && data.length > 0) {
680                 mSize = data.length;
681                 mText = getDecodedString(data, mCharset);
682             }
683         }
684 
685         /**
686          * Load image file of an image part and parse the dimensions and type
687          */
loadImage()688         private void loadImage() {
689             final Context context = Factory.get().getApplicationContext();
690             final ContentResolver resolver = context.getContentResolver();
691             final Uri uri = getDataUri();
692             // We have to get the width and height of the image -- they're needed when adding
693             // an attachment in bugle.
694             InputStream is = null;
695             try {
696                 is = resolver.openInputStream(uri);
697                 final BitmapFactory.Options opt = new BitmapFactory.Options();
698                 opt.inJustDecodeBounds = true;
699                 BitmapFactory.decodeStream(is, null, opt);
700                 mContentType = opt.outMimeType;
701                 mWidth = opt.outWidth;
702                 mHeight = opt.outHeight;
703                 if (TextUtils.isEmpty(mContentType)) {
704                     // BitmapFactory couldn't figure out the image type. That's got to be a bad
705                     // sign, but see if we can figure it out from the file extension.
706                     mContentType = extractContentType(context, uri);
707                 }
708             } catch (final FileNotFoundException e) {
709                 LogUtil.e(TAG, "DatabaseMessages.MmsPart.loadImage: file not found", e);
710             } finally {
711                 if (is != null) {
712                     try {
713                         is.close();
714                     } catch (final IOException e) {
715                         Log.e(TAG, "IOException caught while closing stream", e);
716                     }
717                 }
718             }
719         }
720 
721         /**
722          * Load video file of a video part and parse the dimensions and type
723          */
loadVideo()724         private void loadVideo() {
725             // This is a coarse check, and should not be applied to outgoing messages. However,
726             // currently, this does not cause any problems.
727             if (!VideoThumbnailRequest.shouldShowIncomingVideoThumbnails()) {
728                 return;
729             }
730             final Uri uri = getDataUri();
731             final MediaMetadataRetrieverWrapper retriever = new MediaMetadataRetrieverWrapper();
732             try {
733                 retriever.setDataSource(uri);
734                 // FLAG: This inadvertently fixes a problem with phone receiving audio
735                 // messages on some carrier. We should handle this in a less accidental way so that
736                 // we don't break it again. (The carrier changes the content type in the wrapper
737                 // in-transit from audio/mp4 to video/3gpp without changing the data)
738                 // Also note: There is a bug in some OEM device where mmr returns
739                 // video/ffmpeg for image files.  That shouldn't happen here but be aware.
740                 mContentType =
741                         retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE);
742                 final Bitmap bitmap = retriever.getFrameAtTime(-1);
743                 if (bitmap != null) {
744                     mWidth = bitmap.getWidth();
745                     mHeight = bitmap.getHeight();
746                 } else {
747                     // Get here if it's not actually video (see above)
748                     LogUtil.i(LogUtil.BUGLE_TAG, "loadVideo: Got null bitmap from " + uri);
749                 }
750             } catch (IOException e) {
751                 LogUtil.i(LogUtil.BUGLE_TAG, "Error extracting metadata from " + uri, e);
752             } finally {
753                 retriever.release();
754             }
755         }
756 
757         /**
758          * Get media file size
759          */
getMediaFileSize()760         private long getMediaFileSize() {
761             final Context context = Factory.get().getApplicationContext();
762             final Uri uri = getDataUri();
763             AssetFileDescriptor fd = null;
764             try {
765                 fd = context.getContentResolver().openAssetFileDescriptor(uri, "r");
766                 if (fd != null) {
767                     return fd.getParcelFileDescriptor().getStatSize();
768                 }
769             } catch (final FileNotFoundException e) {
770                 LogUtil.e(TAG, "DatabaseMessages.MmsPart: cound not find media file: " + e, e);
771             } finally {
772                 if (fd != null) {
773                     try {
774                         fd.close();
775                     } catch (final IOException e) {
776                         LogUtil.e(TAG, "DatabaseMessages.MmsPart: failed to close " + e, e);
777                     }
778                 }
779             }
780             return 0L;
781         }
782 
783         /**
784          * @return If the type is a text type that stores text embedded (i.e. in db table)
785          */
isEmbeddedTextType()786         private boolean isEmbeddedTextType() {
787             return ContentType.TEXT_PLAIN.equals(mContentType)
788                     || ContentType.APP_SMIL.equals(mContentType)
789                     || ContentType.TEXT_HTML.equals(mContentType);
790         }
791 
792         /**
793          * Get an instance of the MMS part from the part table cursor
794          *
795          * @param cursor
796          * @param loadMedia Whether to load the media file of the part
797          * @return
798          */
get(final Cursor cursor, final boolean loadMedia)799         public static MmsPart get(final Cursor cursor, final boolean loadMedia) {
800             final MmsPart part = new MmsPart();
801             part.load(cursor, loadMedia);
802             return part;
803         }
804 
isText()805         public boolean isText() {
806             return ContentType.TEXT_PLAIN.equals(mContentType)
807                     || ContentType.TEXT_HTML.equals(mContentType)
808                     || ContentType.APP_WAP_XHTML.equals(mContentType);
809         }
810 
isMedia()811         public boolean isMedia() {
812             return ContentType.isImageType(mContentType)
813                     || ContentType.isVideoType(mContentType)
814                     || ContentType.isAudioType(mContentType)
815                     || ContentType.isVCardType(mContentType);
816         }
817 
isImage()818         public boolean isImage() {
819             return ContentType.isImageType(mContentType);
820         }
821 
getDataUri()822         public Uri getDataUri() {
823             return Uri.parse("content://mms/part/" + mRowId);
824         }
825 
826         @Override
describeContents()827         public int describeContents() {
828             return 0;
829         }
830 
MmsPart(final Parcel in)831         private MmsPart(final Parcel in) {
832             mUri = in.readString();
833             mRowId = in.readLong();
834             mMessageId = in.readLong();
835             mContentType = in.readString();
836             mText = in.readString();
837             mCharset = in.readInt();
838             mWidth = in.readInt();
839             mHeight = in.readInt();
840             mSize = in.readLong();
841         }
842 
843         public static final Parcelable.Creator<MmsPart> CREATOR
844                 = new Parcelable.Creator<MmsPart>() {
845             @Override
846             public MmsPart createFromParcel(final Parcel in) {
847                 return new MmsPart(in);
848             }
849 
850             @Override
851             public MmsPart[] newArray(final int size) {
852                 return new MmsPart[size];
853             }
854         };
855 
856         @Override
writeToParcel(final Parcel out, final int flags)857         public void writeToParcel(final Parcel out, final int flags) {
858             out.writeString(mUri);
859             out.writeLong(mRowId);
860             out.writeLong(mMessageId);
861             out.writeString(mContentType);
862             out.writeString(mText);
863             out.writeInt(mCharset);
864             out.writeInt(mWidth);
865             out.writeInt(mHeight);
866             out.writeLong(mSize);
867         }
868     }
869 
870     /**
871      * This class provides the same DatabaseMessage interface over a local SMS db message
872      */
873     public static class LocalDatabaseMessage extends DatabaseMessage implements Parcelable {
874         private final int mProtocol;
875         private final String mUri;
876         private final long mTimestamp;
877         private final long mLocalId;
878         private final String mConversationId;
879 
LocalDatabaseMessage(final long localId, final int protocol, final String uri, final long timestamp, final String conversationId)880         public LocalDatabaseMessage(final long localId, final int protocol, final String uri,
881                 final long timestamp, final String conversationId) {
882             mLocalId = localId;
883             mProtocol = protocol;
884             mUri = uri;
885             mTimestamp = timestamp;
886             mConversationId = conversationId;
887         }
888 
889         @Override
getProtocol()890         public int getProtocol() {
891             return mProtocol;
892         }
893 
894         @Override
getTimestampInMillis()895         public long getTimestampInMillis() {
896             return mTimestamp;
897         }
898 
899         @Override
getUri()900         public String getUri() {
901             return mUri;
902         }
903 
getLocalId()904         public long getLocalId() {
905             return mLocalId;
906         }
907 
getConversationId()908         public String getConversationId() {
909             return mConversationId;
910         }
911 
912         @Override
describeContents()913         public int describeContents() {
914             return 0;
915         }
916 
LocalDatabaseMessage(final Parcel in)917         private LocalDatabaseMessage(final Parcel in) {
918             mUri = in.readString();
919             mConversationId = in.readString();
920             mLocalId = in.readLong();
921             mTimestamp = in.readLong();
922             mProtocol = in.readInt();
923         }
924 
925         public static final Parcelable.Creator<LocalDatabaseMessage> CREATOR
926                 = new Parcelable.Creator<LocalDatabaseMessage>() {
927             @Override
928             public LocalDatabaseMessage createFromParcel(final Parcel in) {
929                 return new LocalDatabaseMessage(in);
930             }
931 
932             @Override
933             public LocalDatabaseMessage[] newArray(final int size) {
934                 return new LocalDatabaseMessage[size];
935             }
936         };
937 
938         @Override
writeToParcel(final Parcel out, final int flags)939         public void writeToParcel(final Parcel out, final int flags) {
940             out.writeString(mUri);
941             out.writeString(mConversationId);
942             out.writeLong(mLocalId);
943             out.writeLong(mTimestamp);
944             out.writeInt(mProtocol);
945         }
946     }
947 
948     /**
949      * Address for MMS message
950      */
951     public static class MmsAddr {
952         public static final String[] PROJECTION = new String[] {
953             Mms.Addr.ADDRESS,
954             Mms.Addr.CHARSET,
955         };
956         private static int sIota = 0;
957         public static final int INDEX_ADDRESS = sIota++;
958         public static final int INDEX_CHARSET = sIota++;
959 
get(final Cursor cursor)960         public static String get(final Cursor cursor) {
961             final int charset = cursor.getInt(INDEX_CHARSET);
962             // PduPersister stores the addresses using ISO_8859_1
963             // Let's load it using that encoding and convert it back to its original
964             // See PduPersister.persistAddress
965             return getDecodedString(
966                     getStringBytes(cursor.getString(INDEX_ADDRESS), CharacterSets.ISO_8859_1),
967                     charset);
968         }
969     }
970 
971     /**
972      * Decoded string by character set
973      */
getDecodedString(final byte[] data, final int charset)974     public static String getDecodedString(final byte[] data, final int charset)  {
975         if (CharacterSets.ANY_CHARSET == charset) {
976             return new String(data); // system default encoding.
977         } else {
978             try {
979                 final String name = CharacterSets.getMimeName(charset);
980                 return new String(data, name);
981             } catch (final UnsupportedEncodingException e) {
982                 try {
983                     return new String(data, CharacterSets.MIMENAME_ISO_8859_1);
984                 } catch (final UnsupportedEncodingException exception) {
985                     return new String(data); // system default encoding.
986                 }
987             }
988         }
989     }
990 
991     /**
992      * Unpack a given String into a byte[].
993      */
getStringBytes(final String data, final int charset)994     public static byte[] getStringBytes(final String data, final int charset) {
995         if (CharacterSets.ANY_CHARSET == charset) {
996             return data.getBytes();
997         } else {
998             try {
999                 final String name = CharacterSets.getMimeName(charset);
1000                 return data.getBytes(name);
1001             } catch (final UnsupportedEncodingException e) {
1002                 return data.getBytes();
1003             }
1004         }
1005     }
1006 }
1007