| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.messaging.sms; |
| |
| import android.content.ContentResolver; |
| import android.content.ContentUris; |
| import android.content.Context; |
| import android.content.res.AssetFileDescriptor; |
| import android.database.Cursor; |
| import android.graphics.Bitmap; |
| import android.graphics.BitmapFactory; |
| import android.media.MediaMetadataRetriever; |
| import android.net.Uri; |
| import android.os.Parcel; |
| import android.os.Parcelable; |
| import android.provider.Telephony.Mms; |
| import android.provider.Telephony.Sms; |
| import android.text.TextUtils; |
| import android.util.Log; |
| import android.webkit.MimeTypeMap; |
| |
| import com.android.messaging.Factory; |
| import com.android.messaging.datamodel.data.MessageData; |
| import com.android.messaging.datamodel.media.VideoThumbnailRequest; |
| import com.android.messaging.mmslib.pdu.CharacterSets; |
| import com.android.messaging.util.Assert; |
| import com.android.messaging.util.ContentType; |
| import com.android.messaging.util.LogUtil; |
| import com.android.messaging.util.MediaMetadataRetrieverWrapper; |
| import com.android.messaging.util.OsUtil; |
| import com.android.messaging.util.PhoneUtils; |
| import com.google.common.collect.Lists; |
| |
| import java.io.ByteArrayOutputStream; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.UnsupportedEncodingException; |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| /** |
| * Class contains various SMS/MMS database entities from telephony provider |
| */ |
| public class DatabaseMessages { |
| private static final String TAG = LogUtil.BUGLE_TAG; |
| |
| public abstract static class DatabaseMessage { |
| public abstract int getProtocol(); |
| public abstract String getUri(); |
| public abstract long getTimestampInMillis(); |
| |
| @Override |
| public boolean equals(final Object other) { |
| if (other == null || !(other instanceof DatabaseMessage)) { |
| return false; |
| } |
| final DatabaseMessage otherDbMsg = (DatabaseMessage) other; |
| // No need to check timestamp since we only need this when we compare |
| // messages at the same timestamp |
| return TextUtils.equals(getUri(), otherDbMsg.getUri()); |
| } |
| |
| @Override |
| public int hashCode() { |
| // No need to check timestamp since we only need this when we compare |
| // messages at the same timestamp |
| return getUri().hashCode(); |
| } |
| } |
| |
| /** |
| * SMS message |
| */ |
| public static class SmsMessage extends DatabaseMessage implements Parcelable { |
| private static int sIota = 0; |
| public static final int INDEX_ID = sIota++; |
| public static final int INDEX_TYPE = sIota++; |
| public static final int INDEX_ADDRESS = sIota++; |
| public static final int INDEX_BODY = sIota++; |
| public static final int INDEX_DATE = sIota++; |
| public static final int INDEX_THREAD_ID = sIota++; |
| public static final int INDEX_STATUS = sIota++; |
| public static final int INDEX_READ = sIota++; |
| public static final int INDEX_SEEN = sIota++; |
| public static final int INDEX_DATE_SENT = sIota++; |
| public static final int INDEX_SUB_ID = sIota++; |
| |
| private static String[] sProjection; |
| |
| public static String[] getProjection() { |
| if (sProjection == null) { |
| String[] projection = new String[] { |
| Sms._ID, |
| Sms.TYPE, |
| Sms.ADDRESS, |
| Sms.BODY, |
| Sms.DATE, |
| Sms.THREAD_ID, |
| Sms.STATUS, |
| Sms.READ, |
| Sms.SEEN, |
| Sms.DATE_SENT, |
| Sms.SUBSCRIPTION_ID, |
| }; |
| if (!MmsUtils.hasSmsDateSentColumn()) { |
| projection[INDEX_DATE_SENT] = Sms.DATE; |
| } |
| if (!OsUtil.isAtLeastL_MR1()) { |
| Assert.equals(INDEX_SUB_ID, projection.length - 1); |
| String[] withoutSubId = new String[projection.length - 1]; |
| System.arraycopy(projection, 0, withoutSubId, 0, withoutSubId.length); |
| projection = withoutSubId; |
| } |
| |
| sProjection = projection; |
| } |
| |
| return sProjection; |
| } |
| |
| public String mUri; |
| public String mAddress; |
| public String mBody; |
| private long mRowId; |
| public long mTimestampInMillis; |
| public long mTimestampSentInMillis; |
| public int mType; |
| public long mThreadId; |
| public int mStatus; |
| public boolean mRead; |
| public boolean mSeen; |
| public int mSubId; |
| |
| private SmsMessage() { |
| } |
| |
| /** |
| * Load from a cursor of a query that returns the SMS to import |
| * |
| * @param cursor |
| */ |
| private void load(final Cursor cursor) { |
| mRowId = cursor.getLong(INDEX_ID); |
| mAddress = cursor.getString(INDEX_ADDRESS); |
| mBody = cursor.getString(INDEX_BODY); |
| mTimestampInMillis = cursor.getLong(INDEX_DATE); |
| // Before ICS, there is no "date_sent" so use copy of "date" value |
| mTimestampSentInMillis = cursor.getLong(INDEX_DATE_SENT); |
| mType = cursor.getInt(INDEX_TYPE); |
| mThreadId = cursor.getLong(INDEX_THREAD_ID); |
| mStatus = cursor.getInt(INDEX_STATUS); |
| mRead = cursor.getInt(INDEX_READ) == 0 ? false : true; |
| mSeen = cursor.getInt(INDEX_SEEN) == 0 ? false : true; |
| mUri = ContentUris.withAppendedId(Sms.CONTENT_URI, mRowId).toString(); |
| mSubId = PhoneUtils.getDefault().getSubIdFromTelephony(cursor, INDEX_SUB_ID); |
| } |
| |
| /** |
| * Get a new SmsMessage by loading from the cursor of a query |
| * that returns the SMS to import |
| * |
| * @param cursor |
| * @return |
| */ |
| public static SmsMessage get(final Cursor cursor) { |
| final SmsMessage msg = new SmsMessage(); |
| msg.load(cursor); |
| return msg; |
| } |
| |
| @Override |
| public String getUri() { |
| return mUri; |
| } |
| |
| public int getSubId() { |
| return mSubId; |
| } |
| |
| @Override |
| public int getProtocol() { |
| return MessageData.PROTOCOL_SMS; |
| } |
| |
| @Override |
| public long getTimestampInMillis() { |
| return mTimestampInMillis; |
| } |
| |
| @Override |
| public int describeContents() { |
| return 0; |
| } |
| |
| private SmsMessage(final Parcel in) { |
| mUri = in.readString(); |
| mRowId = in.readLong(); |
| mTimestampInMillis = in.readLong(); |
| mTimestampSentInMillis = in.readLong(); |
| mType = in.readInt(); |
| mThreadId = in.readLong(); |
| mStatus = in.readInt(); |
| mRead = in.readInt() != 0; |
| mSeen = in.readInt() != 0; |
| mSubId = in.readInt(); |
| |
| // SMS specific |
| mAddress = in.readString(); |
| mBody = in.readString(); |
| } |
| |
| public static final Parcelable.Creator<SmsMessage> CREATOR |
| = new Parcelable.Creator<SmsMessage>() { |
| @Override |
| public SmsMessage createFromParcel(final Parcel in) { |
| return new SmsMessage(in); |
| } |
| |
| @Override |
| public SmsMessage[] newArray(final int size) { |
| return new SmsMessage[size]; |
| } |
| }; |
| |
| @Override |
| public void writeToParcel(final Parcel out, final int flags) { |
| out.writeString(mUri); |
| out.writeLong(mRowId); |
| out.writeLong(mTimestampInMillis); |
| out.writeLong(mTimestampSentInMillis); |
| out.writeInt(mType); |
| out.writeLong(mThreadId); |
| out.writeInt(mStatus); |
| out.writeInt(mRead ? 1 : 0); |
| out.writeInt(mSeen ? 1 : 0); |
| out.writeInt(mSubId); |
| |
| // SMS specific |
| out.writeString(mAddress); |
| out.writeString(mBody); |
| } |
| } |
| |
| /** |
| * MMS message |
| */ |
| public static class MmsMessage extends DatabaseMessage implements Parcelable { |
| private static int sIota = 0; |
| public static final int INDEX_ID = sIota++; |
| public static final int INDEX_MESSAGE_BOX = sIota++; |
| public static final int INDEX_SUBJECT = sIota++; |
| public static final int INDEX_SUBJECT_CHARSET = sIota++; |
| public static final int INDEX_MESSAGE_SIZE = sIota++; |
| public static final int INDEX_DATE = sIota++; |
| public static final int INDEX_DATE_SENT = sIota++; |
| public static final int INDEX_THREAD_ID = sIota++; |
| public static final int INDEX_PRIORITY = sIota++; |
| public static final int INDEX_STATUS = sIota++; |
| public static final int INDEX_READ = sIota++; |
| public static final int INDEX_SEEN = sIota++; |
| public static final int INDEX_CONTENT_LOCATION = sIota++; |
| public static final int INDEX_TRANSACTION_ID = sIota++; |
| public static final int INDEX_MESSAGE_TYPE = sIota++; |
| public static final int INDEX_EXPIRY = sIota++; |
| public static final int INDEX_RESPONSE_STATUS = sIota++; |
| public static final int INDEX_RETRIEVE_STATUS = sIota++; |
| public static final int INDEX_SUB_ID = sIota++; |
| |
| private static String[] sProjection; |
| |
| public static String[] getProjection() { |
| if (sProjection == null) { |
| String[] projection = new String[] { |
| Mms._ID, |
| Mms.MESSAGE_BOX, |
| Mms.SUBJECT, |
| Mms.SUBJECT_CHARSET, |
| Mms.MESSAGE_SIZE, |
| Mms.DATE, |
| Mms.DATE_SENT, |
| Mms.THREAD_ID, |
| Mms.PRIORITY, |
| Mms.STATUS, |
| Mms.READ, |
| Mms.SEEN, |
| Mms.CONTENT_LOCATION, |
| Mms.TRANSACTION_ID, |
| Mms.MESSAGE_TYPE, |
| Mms.EXPIRY, |
| Mms.RESPONSE_STATUS, |
| Mms.RETRIEVE_STATUS, |
| Mms.SUBSCRIPTION_ID, |
| }; |
| |
| if (!OsUtil.isAtLeastL_MR1()) { |
| Assert.equals(INDEX_SUB_ID, projection.length - 1); |
| String[] withoutSubId = new String[projection.length - 1]; |
| System.arraycopy(projection, 0, withoutSubId, 0, withoutSubId.length); |
| projection = withoutSubId; |
| } |
| |
| sProjection = projection; |
| } |
| |
| return sProjection; |
| } |
| |
| public String mUri; |
| private long mRowId; |
| public int mType; |
| public String mSubject; |
| public int mSubjectCharset; |
| private long mSize; |
| public long mTimestampInMillis; |
| public long mSentTimestampInMillis; |
| public long mThreadId; |
| public int mPriority; |
| public int mStatus; |
| public boolean mRead; |
| public boolean mSeen; |
| public String mContentLocation; |
| public String mTransactionId; |
| public int mMmsMessageType; |
| public long mExpiryInMillis; |
| public int mSubId; |
| public String mSender; |
| public int mResponseStatus; |
| public int mRetrieveStatus; |
| |
| public List<MmsPart> mParts = Lists.newArrayList(); |
| private boolean mPartsProcessed = false; |
| |
| private MmsMessage() { |
| } |
| |
| /** |
| * Load from a cursor of a query that returns the MMS to import |
| * |
| * @param cursor |
| */ |
| public void load(final Cursor cursor) { |
| mRowId = cursor.getLong(INDEX_ID); |
| mType = cursor.getInt(INDEX_MESSAGE_BOX); |
| mSubject = cursor.getString(INDEX_SUBJECT); |
| mSubjectCharset = cursor.getInt(INDEX_SUBJECT_CHARSET); |
| if (!TextUtils.isEmpty(mSubject)) { |
| // PduPersister stores the subject using ISO_8859_1 |
| // Let's load it using that encoding and convert it back to its original |
| // See PduPersister.persist and PduPersister.toIsoString |
| // (Refer to bug b/11162476) |
| mSubject = getDecodedString( |
| getStringBytes(mSubject, CharacterSets.ISO_8859_1), mSubjectCharset); |
| } |
| mSize = cursor.getLong(INDEX_MESSAGE_SIZE); |
| // MMS db times are in seconds |
| mTimestampInMillis = cursor.getLong(INDEX_DATE) * 1000; |
| mSentTimestampInMillis = cursor.getLong(INDEX_DATE_SENT) * 1000; |
| mThreadId = cursor.getLong(INDEX_THREAD_ID); |
| mPriority = cursor.getInt(INDEX_PRIORITY); |
| mStatus = cursor.getInt(INDEX_STATUS); |
| mRead = cursor.getInt(INDEX_READ) == 0 ? false : true; |
| mSeen = cursor.getInt(INDEX_SEEN) == 0 ? false : true; |
| mContentLocation = cursor.getString(INDEX_CONTENT_LOCATION); |
| mTransactionId = cursor.getString(INDEX_TRANSACTION_ID); |
| mMmsMessageType = cursor.getInt(INDEX_MESSAGE_TYPE); |
| mExpiryInMillis = cursor.getLong(INDEX_EXPIRY) * 1000; |
| mResponseStatus = cursor.getInt(INDEX_RESPONSE_STATUS); |
| mRetrieveStatus = cursor.getInt(INDEX_RETRIEVE_STATUS); |
| // Clear all parts in case we reuse this object |
| mParts.clear(); |
| mPartsProcessed = false; |
| mUri = ContentUris.withAppendedId(Mms.CONTENT_URI, mRowId).toString(); |
| mSubId = PhoneUtils.getDefault().getSubIdFromTelephony(cursor, INDEX_SUB_ID); |
| } |
| |
| /** |
| * Get a new MmsMessage by loading from the cursor of a query |
| * that returns the MMS to import |
| * |
| * @param cursor |
| * @return |
| */ |
| public static MmsMessage get(final Cursor cursor) { |
| final MmsMessage msg = new MmsMessage(); |
| msg.load(cursor); |
| return msg; |
| } |
| /** |
| * Add a loaded MMS part |
| * |
| * @param part |
| */ |
| public void addPart(final MmsPart part) { |
| mParts.add(part); |
| } |
| |
| public List<MmsPart> getParts() { |
| return mParts; |
| } |
| |
| public long getSize() { |
| if (!mPartsProcessed) { |
| processParts(); |
| } |
| return mSize; |
| } |
| |
| /** |
| * Process loaded MMS parts to obtain the combined text, the combined attachment url, |
| * the combined content type and the combined size. |
| */ |
| private void processParts() { |
| if (mPartsProcessed) { |
| return; |
| } |
| mPartsProcessed = true; |
| // Remember the width and height of the first media part |
| // These are needed when building attachment list |
| long sizeOfParts = 0L; |
| for (final MmsPart part : mParts) { |
| sizeOfParts += part.mSize; |
| } |
| if (mSize <= 0) { |
| mSize = mSubject != null ? mSubject.getBytes().length : 0L; |
| mSize += sizeOfParts; |
| } |
| } |
| |
| @Override |
| public String getUri() { |
| return mUri; |
| } |
| |
| public long getId() { |
| return mRowId; |
| } |
| |
| public int getSubId() { |
| return mSubId; |
| } |
| |
| @Override |
| public int getProtocol() { |
| return MessageData.PROTOCOL_MMS; |
| } |
| |
| @Override |
| public long getTimestampInMillis() { |
| return mTimestampInMillis; |
| } |
| |
| @Override |
| public int describeContents() { |
| return 0; |
| } |
| |
| public void setSender(final String sender) { |
| mSender = sender; |
| } |
| |
| private MmsMessage(final Parcel in) { |
| mUri = in.readString(); |
| mRowId = in.readLong(); |
| mTimestampInMillis = in.readLong(); |
| mSentTimestampInMillis = in.readLong(); |
| mType = in.readInt(); |
| mThreadId = in.readLong(); |
| mStatus = in.readInt(); |
| mRead = in.readInt() != 0; |
| mSeen = in.readInt() != 0; |
| mSubId = in.readInt(); |
| |
| // MMS specific |
| mSubject = in.readString(); |
| mContentLocation = in.readString(); |
| mTransactionId = in.readString(); |
| mSender = in.readString(); |
| |
| mSize = in.readLong(); |
| mExpiryInMillis = in.readLong(); |
| |
| mSubjectCharset = in.readInt(); |
| mPriority = in.readInt(); |
| mMmsMessageType = in.readInt(); |
| mResponseStatus = in.readInt(); |
| mRetrieveStatus = in.readInt(); |
| |
| final int nParts = in.readInt(); |
| mParts = new ArrayList<MmsPart>(); |
| mPartsProcessed = false; |
| for (int i = 0; i < nParts; i++) { |
| mParts.add((MmsPart) in.readParcelable(getClass().getClassLoader())); |
| } |
| } |
| |
| public static final Parcelable.Creator<MmsMessage> CREATOR |
| = new Parcelable.Creator<MmsMessage>() { |
| @Override |
| public MmsMessage createFromParcel(final Parcel in) { |
| return new MmsMessage(in); |
| } |
| |
| @Override |
| public MmsMessage[] newArray(final int size) { |
| return new MmsMessage[size]; |
| } |
| }; |
| |
| @Override |
| public void writeToParcel(final Parcel out, final int flags) { |
| out.writeString(mUri); |
| out.writeLong(mRowId); |
| out.writeLong(mTimestampInMillis); |
| out.writeLong(mSentTimestampInMillis); |
| out.writeInt(mType); |
| out.writeLong(mThreadId); |
| out.writeInt(mStatus); |
| out.writeInt(mRead ? 1 : 0); |
| out.writeInt(mSeen ? 1 : 0); |
| out.writeInt(mSubId); |
| |
| out.writeString(mSubject); |
| out.writeString(mContentLocation); |
| out.writeString(mTransactionId); |
| out.writeString(mSender); |
| |
| out.writeLong(mSize); |
| out.writeLong(mExpiryInMillis); |
| |
| out.writeInt(mSubjectCharset); |
| out.writeInt(mPriority); |
| out.writeInt(mMmsMessageType); |
| out.writeInt(mResponseStatus); |
| out.writeInt(mRetrieveStatus); |
| |
| out.writeInt(mParts.size()); |
| for (final MmsPart part : mParts) { |
| out.writeParcelable(part, 0); |
| } |
| } |
| } |
| |
| /** |
| * Part of an MMS message |
| */ |
| public static class MmsPart implements Parcelable { |
| public static final String[] PROJECTION = new String[] { |
| Mms.Part._ID, |
| Mms.Part.MSG_ID, |
| Mms.Part.CHARSET, |
| Mms.Part.CONTENT_TYPE, |
| Mms.Part.TEXT, |
| }; |
| private static int sIota = 0; |
| public static final int INDEX_ID = sIota++; |
| public static final int INDEX_MSG_ID = sIota++; |
| public static final int INDEX_CHARSET = sIota++; |
| public static final int INDEX_CONTENT_TYPE = sIota++; |
| public static final int INDEX_TEXT = sIota++; |
| |
| public String mUri; |
| public long mRowId; |
| public long mMessageId; |
| public String mContentType; |
| public String mText; |
| public int mCharset; |
| private int mWidth; |
| private int mHeight; |
| public long mSize; |
| |
| private MmsPart() { |
| } |
| |
| /** |
| * Load from a cursor of a query that returns the MMS part to import |
| * |
| * @param cursor |
| */ |
| public void load(final Cursor cursor, final boolean loadMedia) { |
| mRowId = cursor.getLong(INDEX_ID); |
| mMessageId = cursor.getLong(INDEX_MSG_ID); |
| mContentType = cursor.getString(INDEX_CONTENT_TYPE); |
| mText = cursor.getString(INDEX_TEXT); |
| mCharset = cursor.getInt(INDEX_CHARSET); |
| mWidth = 0; |
| mHeight = 0; |
| mSize = 0; |
| if (isMedia()) { |
| // For importing we don't load media since performance is critical |
| // For loading when we receive mms, we do load media to get enough |
| // information of the media file |
| if (loadMedia) { |
| if (ContentType.isImageType(mContentType)) { |
| loadImage(); |
| } else if (ContentType.isVideoType(mContentType)) { |
| loadVideo(); |
| } // No need to load audio for parsing |
| mSize = MmsUtils.getMediaFileSize(getDataUri()); |
| } |
| } else { |
| // Load text if not media type |
| loadText(); |
| } |
| mUri = Uri.withAppendedPath(Mms.CONTENT_URI, cursor.getString(INDEX_ID)).toString(); |
| } |
| |
| /** |
| * Get content type from file extension |
| */ |
| private static String extractContentType(final Context context, final Uri uri) { |
| final String path = uri.getPath(); |
| final MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton(); |
| String extension = MimeTypeMap.getFileExtensionFromUrl(path); |
| if (TextUtils.isEmpty(extension)) { |
| // getMimeTypeFromExtension() doesn't handle spaces in filenames nor can it handle |
| // urlEncoded strings. Let's try one last time at finding the extension. |
| final int dotPos = path.lastIndexOf('.'); |
| if (0 <= dotPos) { |
| extension = path.substring(dotPos + 1); |
| } |
| } |
| return mimeTypeMap.getMimeTypeFromExtension(extension); |
| } |
| |
| /** |
| * Get text of a text part |
| */ |
| private void loadText() { |
| byte[] data = null; |
| if (isEmbeddedTextType()) { |
| // Embedded text, get from the "text" column |
| if (!TextUtils.isEmpty(mText)) { |
| data = getStringBytes(mText, mCharset); |
| } |
| } else { |
| // Not embedded, load from disk |
| final ContentResolver resolver = |
| Factory.get().getApplicationContext().getContentResolver(); |
| final Uri uri = getDataUri(); |
| InputStream is = null; |
| final ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
| try { |
| is = resolver.openInputStream(uri); |
| final byte[] buffer = new byte[256]; |
| int len = is.read(buffer); |
| while (len >= 0) { |
| baos.write(buffer, 0, len); |
| len = is.read(buffer); |
| } |
| } catch (final IOException e) { |
| LogUtil.e(TAG, |
| "DatabaseMessages.MmsPart: loading text from file failed: " + e, e); |
| } finally { |
| if (is != null) { |
| try { |
| is.close(); |
| } catch (final IOException e) { |
| LogUtil.e(TAG, "DatabaseMessages.MmsPart: close file failed: " + e, e); |
| } |
| } |
| } |
| data = baos.toByteArray(); |
| } |
| if (data != null && data.length > 0) { |
| mSize = data.length; |
| mText = getDecodedString(data, mCharset); |
| } |
| } |
| |
| /** |
| * Load image file of an image part and parse the dimensions and type |
| */ |
| private void loadImage() { |
| final Context context = Factory.get().getApplicationContext(); |
| final ContentResolver resolver = context.getContentResolver(); |
| final Uri uri = getDataUri(); |
| // We have to get the width and height of the image -- they're needed when adding |
| // an attachment in bugle. |
| InputStream is = null; |
| try { |
| is = resolver.openInputStream(uri); |
| final BitmapFactory.Options opt = new BitmapFactory.Options(); |
| opt.inJustDecodeBounds = true; |
| BitmapFactory.decodeStream(is, null, opt); |
| mContentType = opt.outMimeType; |
| mWidth = opt.outWidth; |
| mHeight = opt.outHeight; |
| if (TextUtils.isEmpty(mContentType)) { |
| // BitmapFactory couldn't figure out the image type. That's got to be a bad |
| // sign, but see if we can figure it out from the file extension. |
| mContentType = extractContentType(context, uri); |
| } |
| } catch (final FileNotFoundException e) { |
| LogUtil.e(TAG, "DatabaseMessages.MmsPart.loadImage: file not found", e); |
| } finally { |
| if (is != null) { |
| try { |
| is.close(); |
| } catch (final IOException e) { |
| Log.e(TAG, "IOException caught while closing stream", e); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Load video file of a video part and parse the dimensions and type |
| */ |
| private void loadVideo() { |
| // This is a coarse check, and should not be applied to outgoing messages. However, |
| // currently, this does not cause any problems. |
| if (!VideoThumbnailRequest.shouldShowIncomingVideoThumbnails()) { |
| return; |
| } |
| final Uri uri = getDataUri(); |
| final MediaMetadataRetrieverWrapper retriever = new MediaMetadataRetrieverWrapper(); |
| try { |
| retriever.setDataSource(uri); |
| // FLAG: This inadvertently fixes a problem with phone receiving audio |
| // messages on some carrier. We should handle this in a less accidental way so that |
| // we don't break it again. (The carrier changes the content type in the wrapper |
| // in-transit from audio/mp4 to video/3gpp without changing the data) |
| // Also note: There is a bug in some OEM device where mmr returns |
| // video/ffmpeg for image files. That shouldn't happen here but be aware. |
| mContentType = |
| retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE); |
| final Bitmap bitmap = retriever.getFrameAtTime(-1); |
| if (bitmap != null) { |
| mWidth = bitmap.getWidth(); |
| mHeight = bitmap.getHeight(); |
| } else { |
| // Get here if it's not actually video (see above) |
| LogUtil.i(LogUtil.BUGLE_TAG, "loadVideo: Got null bitmap from " + uri); |
| } |
| } catch (IOException e) { |
| LogUtil.i(LogUtil.BUGLE_TAG, "Error extracting metadata from " + uri, e); |
| } finally { |
| retriever.release(); |
| } |
| } |
| |
| /** |
| * Get media file size |
| */ |
| private long getMediaFileSize() { |
| final Context context = Factory.get().getApplicationContext(); |
| final Uri uri = getDataUri(); |
| AssetFileDescriptor fd = null; |
| try { |
| fd = context.getContentResolver().openAssetFileDescriptor(uri, "r"); |
| if (fd != null) { |
| return fd.getParcelFileDescriptor().getStatSize(); |
| } |
| } catch (final FileNotFoundException e) { |
| LogUtil.e(TAG, "DatabaseMessages.MmsPart: cound not find media file: " + e, e); |
| } finally { |
| if (fd != null) { |
| try { |
| fd.close(); |
| } catch (final IOException e) { |
| LogUtil.e(TAG, "DatabaseMessages.MmsPart: failed to close " + e, e); |
| } |
| } |
| } |
| return 0L; |
| } |
| |
| /** |
| * @return If the type is a text type that stores text embedded (i.e. in db table) |
| */ |
| private boolean isEmbeddedTextType() { |
| return ContentType.TEXT_PLAIN.equals(mContentType) |
| || ContentType.APP_SMIL.equals(mContentType) |
| || ContentType.TEXT_HTML.equals(mContentType); |
| } |
| |
| /** |
| * Get an instance of the MMS part from the part table cursor |
| * |
| * @param cursor |
| * @param loadMedia Whether to load the media file of the part |
| * @return |
| */ |
| public static MmsPart get(final Cursor cursor, final boolean loadMedia) { |
| final MmsPart part = new MmsPart(); |
| part.load(cursor, loadMedia); |
| return part; |
| } |
| |
| public boolean isText() { |
| return ContentType.TEXT_PLAIN.equals(mContentType) |
| || ContentType.TEXT_HTML.equals(mContentType) |
| || ContentType.APP_WAP_XHTML.equals(mContentType); |
| } |
| |
| public boolean isMedia() { |
| return ContentType.isImageType(mContentType) |
| || ContentType.isVideoType(mContentType) |
| || ContentType.isAudioType(mContentType) |
| || ContentType.isVCardType(mContentType); |
| } |
| |
| public boolean isImage() { |
| return ContentType.isImageType(mContentType); |
| } |
| |
| public Uri getDataUri() { |
| return Uri.parse("content://mms/part/" + mRowId); |
| } |
| |
| @Override |
| public int describeContents() { |
| return 0; |
| } |
| |
| private MmsPart(final Parcel in) { |
| mUri = in.readString(); |
| mRowId = in.readLong(); |
| mMessageId = in.readLong(); |
| mContentType = in.readString(); |
| mText = in.readString(); |
| mCharset = in.readInt(); |
| mWidth = in.readInt(); |
| mHeight = in.readInt(); |
| mSize = in.readLong(); |
| } |
| |
| public static final Parcelable.Creator<MmsPart> CREATOR |
| = new Parcelable.Creator<MmsPart>() { |
| @Override |
| public MmsPart createFromParcel(final Parcel in) { |
| return new MmsPart(in); |
| } |
| |
| @Override |
| public MmsPart[] newArray(final int size) { |
| return new MmsPart[size]; |
| } |
| }; |
| |
| @Override |
| public void writeToParcel(final Parcel out, final int flags) { |
| out.writeString(mUri); |
| out.writeLong(mRowId); |
| out.writeLong(mMessageId); |
| out.writeString(mContentType); |
| out.writeString(mText); |
| out.writeInt(mCharset); |
| out.writeInt(mWidth); |
| out.writeInt(mHeight); |
| out.writeLong(mSize); |
| } |
| } |
| |
| /** |
| * This class provides the same DatabaseMessage interface over a local SMS db message |
| */ |
| public static class LocalDatabaseMessage extends DatabaseMessage implements Parcelable { |
| private final int mProtocol; |
| private final String mUri; |
| private final long mTimestamp; |
| private final long mLocalId; |
| private final String mConversationId; |
| |
| public LocalDatabaseMessage(final long localId, final int protocol, final String uri, |
| final long timestamp, final String conversationId) { |
| mLocalId = localId; |
| mProtocol = protocol; |
| mUri = uri; |
| mTimestamp = timestamp; |
| mConversationId = conversationId; |
| } |
| |
| @Override |
| public int getProtocol() { |
| return mProtocol; |
| } |
| |
| @Override |
| public long getTimestampInMillis() { |
| return mTimestamp; |
| } |
| |
| @Override |
| public String getUri() { |
| return mUri; |
| } |
| |
| public long getLocalId() { |
| return mLocalId; |
| } |
| |
| public String getConversationId() { |
| return mConversationId; |
| } |
| |
| @Override |
| public int describeContents() { |
| return 0; |
| } |
| |
| private LocalDatabaseMessage(final Parcel in) { |
| mUri = in.readString(); |
| mConversationId = in.readString(); |
| mLocalId = in.readLong(); |
| mTimestamp = in.readLong(); |
| mProtocol = in.readInt(); |
| } |
| |
| public static final Parcelable.Creator<LocalDatabaseMessage> CREATOR |
| = new Parcelable.Creator<LocalDatabaseMessage>() { |
| @Override |
| public LocalDatabaseMessage createFromParcel(final Parcel in) { |
| return new LocalDatabaseMessage(in); |
| } |
| |
| @Override |
| public LocalDatabaseMessage[] newArray(final int size) { |
| return new LocalDatabaseMessage[size]; |
| } |
| }; |
| |
| @Override |
| public void writeToParcel(final Parcel out, final int flags) { |
| out.writeString(mUri); |
| out.writeString(mConversationId); |
| out.writeLong(mLocalId); |
| out.writeLong(mTimestamp); |
| out.writeInt(mProtocol); |
| } |
| } |
| |
| /** |
| * Address for MMS message |
| */ |
| public static class MmsAddr { |
| public static final String[] PROJECTION = new String[] { |
| Mms.Addr.ADDRESS, |
| Mms.Addr.CHARSET, |
| }; |
| private static int sIota = 0; |
| public static final int INDEX_ADDRESS = sIota++; |
| public static final int INDEX_CHARSET = sIota++; |
| |
| public static String get(final Cursor cursor) { |
| final int charset = cursor.getInt(INDEX_CHARSET); |
| // PduPersister stores the addresses using ISO_8859_1 |
| // Let's load it using that encoding and convert it back to its original |
| // See PduPersister.persistAddress |
| return getDecodedString( |
| getStringBytes(cursor.getString(INDEX_ADDRESS), CharacterSets.ISO_8859_1), |
| charset); |
| } |
| } |
| |
| /** |
| * Decoded string by character set |
| */ |
| public static String getDecodedString(final byte[] data, final int charset) { |
| if (CharacterSets.ANY_CHARSET == charset) { |
| return new String(data); // system default encoding. |
| } else { |
| try { |
| final String name = CharacterSets.getMimeName(charset); |
| return new String(data, name); |
| } catch (final UnsupportedEncodingException e) { |
| try { |
| return new String(data, CharacterSets.MIMENAME_ISO_8859_1); |
| } catch (final UnsupportedEncodingException exception) { |
| return new String(data); // system default encoding. |
| } |
| } |
| } |
| } |
| |
| /** |
| * Unpack a given String into a byte[]. |
| */ |
| public static byte[] getStringBytes(final String data, final int charset) { |
| if (CharacterSets.ANY_CHARSET == charset) { |
| return data.getBytes(); |
| } else { |
| try { |
| final String name = CharacterSets.getMimeName(charset); |
| return data.getBytes(name); |
| } catch (final UnsupportedEncodingException e) { |
| return data.getBytes(); |
| } |
| } |
| } |
| } |