Home | History | Annotate | Download | only in sms
      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 {
     63         public abstract int getProtocol();
     64         public abstract String getUri();
     65         public abstract long getTimestampInMillis();
     66 
     67         @Override
     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
     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 
    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 
    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          */
    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          */
    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
    187         public String getUri() {
    188             return mUri;
    189         }
    190 
    191         public int getSubId() {
    192             return mSubId;
    193         }
    194 
    195         @Override
    196         public int getProtocol() {
    197             return MessageData.PROTOCOL_SMS;
    198         }
    199 
    200         @Override
    201         public long getTimestampInMillis() {
    202             return mTimestampInMillis;
    203         }
    204 
    205         @Override
    206         public int describeContents() {
    207             return 0;
    208         }
    209 
    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
    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 
    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 
    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          */
    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          */
    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          */
    408         public void addPart(final MmsPart part) {
    409             mParts.add(part);
    410         }
    411 
    412         public List<MmsPart> getParts() {
    413             return mParts;
    414         }
    415 
    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          */
    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
    445         public String getUri() {
    446             return mUri;
    447         }
    448 
    449         public long getId() {
    450             return mRowId;
    451         }
    452 
    453         public int getSubId() {
    454             return mSubId;
    455         }
    456 
    457         @Override
    458         public int getProtocol() {
    459             return MessageData.PROTOCOL_MMS;
    460         }
    461 
    462         @Override
    463         public long getTimestampInMillis() {
    464             return mTimestampInMillis;
    465         }
    466 
    467         @Override
    468         public int describeContents() {
    469             return 0;
    470         }
    471 
    472         public void setSender(final String sender) {
    473             mSender = sender;
    474         }
    475 
    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
    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 
    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          */
    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          */
    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          */
    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          */
    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          */
    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          */
    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          */
    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          */
    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 
    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 
    811         public boolean isMedia() {
    812             return ContentType.isImageType(mContentType)
    813                     || ContentType.isVideoType(mContentType)
    814                     || ContentType.isAudioType(mContentType)
    815                     || ContentType.isVCardType(mContentType);
    816         }
    817 
    818         public boolean isImage() {
    819             return ContentType.isImageType(mContentType);
    820         }
    821 
    822         public Uri getDataUri() {
    823             return Uri.parse("content://mms/part/" + mRowId);
    824         }
    825 
    826         @Override
    827         public int describeContents() {
    828             return 0;
    829         }
    830 
    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
    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 
    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
    890         public int getProtocol() {
    891             return mProtocol;
    892         }
    893 
    894         @Override
    895         public long getTimestampInMillis() {
    896             return mTimestamp;
    897         }
    898 
    899         @Override
    900         public String getUri() {
    901             return mUri;
    902         }
    903 
    904         public long getLocalId() {
    905             return mLocalId;
    906         }
    907 
    908         public String getConversationId() {
    909             return mConversationId;
    910         }
    911 
    912         @Override
    913         public int describeContents() {
    914             return 0;
    915         }
    916 
    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
    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 
    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      */
    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      */
    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