/* * 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 CREATOR = new Parcelable.Creator() { @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 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 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(); mPartsProcessed = false; for (int i = 0; i < nParts; i++) { mParts.add((MmsPart) in.readParcelable(getClass().getClassLoader())); } } public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { @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 CREATOR = new Parcelable.Creator() { @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 CREATOR = new Parcelable.Creator() { @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(); } } } }