| /* |
| * 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.ContentValues; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.res.AssetFileDescriptor; |
| import android.content.res.Resources; |
| import android.database.Cursor; |
| import android.database.sqlite.SQLiteDatabase; |
| import android.database.sqlite.SQLiteException; |
| import android.media.MediaMetadataRetriever; |
| import android.net.Uri; |
| import android.os.Bundle; |
| import android.provider.Settings; |
| import android.provider.Telephony; |
| import android.provider.Telephony.Mms; |
| import android.provider.Telephony.Sms; |
| import android.provider.Telephony.Threads; |
| import android.telephony.SmsManager; |
| import android.telephony.SmsMessage; |
| import android.text.TextUtils; |
| import android.text.util.Rfc822Token; |
| import android.text.util.Rfc822Tokenizer; |
| |
| import com.android.messaging.Factory; |
| import com.android.messaging.R; |
| import com.android.messaging.datamodel.MediaScratchFileProvider; |
| import com.android.messaging.datamodel.action.DownloadMmsAction; |
| import com.android.messaging.datamodel.action.SendMessageAction; |
| import com.android.messaging.datamodel.data.MessageData; |
| import com.android.messaging.datamodel.data.MessagePartData; |
| import com.android.messaging.datamodel.data.ParticipantData; |
| import com.android.messaging.mmslib.InvalidHeaderValueException; |
| import com.android.messaging.mmslib.MmsException; |
| import com.android.messaging.mmslib.SqliteWrapper; |
| import com.android.messaging.mmslib.pdu.CharacterSets; |
| import com.android.messaging.mmslib.pdu.EncodedStringValue; |
| import com.android.messaging.mmslib.pdu.GenericPdu; |
| import com.android.messaging.mmslib.pdu.NotificationInd; |
| import com.android.messaging.mmslib.pdu.PduBody; |
| import com.android.messaging.mmslib.pdu.PduComposer; |
| import com.android.messaging.mmslib.pdu.PduHeaders; |
| import com.android.messaging.mmslib.pdu.PduParser; |
| import com.android.messaging.mmslib.pdu.PduPart; |
| import com.android.messaging.mmslib.pdu.PduPersister; |
| import com.android.messaging.mmslib.pdu.RetrieveConf; |
| import com.android.messaging.mmslib.pdu.SendConf; |
| import com.android.messaging.mmslib.pdu.SendReq; |
| import com.android.messaging.sms.SmsSender.SendResult; |
| import com.android.messaging.util.Assert; |
| import com.android.messaging.util.BugleGservices; |
| import com.android.messaging.util.BugleGservicesKeys; |
| import com.android.messaging.util.BuglePrefs; |
| import com.android.messaging.util.ContentType; |
| import com.android.messaging.util.DebugUtils; |
| import com.android.messaging.util.EmailAddress; |
| import com.android.messaging.util.ImageUtils; |
| import com.android.messaging.util.ImageUtils.ImageResizer; |
| 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.base.Joiner; |
| |
| import java.io.BufferedOutputStream; |
| import java.io.File; |
| import java.io.FileNotFoundException; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.UnsupportedEncodingException; |
| import java.util.ArrayList; |
| import java.util.Calendar; |
| import java.util.GregorianCalendar; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Set; |
| import java.util.UUID; |
| |
| /** |
| * Utils for sending sms/mms messages. |
| */ |
| public class MmsUtils { |
| private static final String TAG = LogUtil.BUGLE_TAG; |
| |
| public static final boolean DEFAULT_DELIVERY_REPORT_MODE = false; |
| public static final boolean DEFAULT_READ_REPORT_MODE = false; |
| public static final long DEFAULT_EXPIRY_TIME_IN_SECONDS = 7 * 24 * 60 * 60; |
| public static final int DEFAULT_PRIORITY = PduHeaders.PRIORITY_NORMAL; |
| |
| public static final int MAX_SMS_RETRY = 3; |
| |
| /** |
| * MMS request succeeded |
| */ |
| public static final int MMS_REQUEST_SUCCEEDED = 0; |
| /** |
| * MMS request failed with a transient error and can be retried automatically |
| */ |
| public static final int MMS_REQUEST_AUTO_RETRY = 1; |
| /** |
| * MMS request failed with an error and can be retried manually |
| */ |
| public static final int MMS_REQUEST_MANUAL_RETRY = 2; |
| /** |
| * MMS request failed with a specific error and should not be retried |
| */ |
| public static final int MMS_REQUEST_NO_RETRY = 3; |
| |
| public static final String getRequestStatusDescription(final int status) { |
| switch (status) { |
| case MMS_REQUEST_SUCCEEDED: |
| return "SUCCEEDED"; |
| case MMS_REQUEST_AUTO_RETRY: |
| return "AUTO_RETRY"; |
| case MMS_REQUEST_MANUAL_RETRY: |
| return "MANUAL_RETRY"; |
| case MMS_REQUEST_NO_RETRY: |
| return "NO_RETRY"; |
| default: |
| return String.valueOf(status) + " (check MmsUtils)"; |
| } |
| } |
| |
| public static final int PDU_HEADER_VALUE_UNDEFINED = 0; |
| |
| private static final int DEFAULT_DURATION = 5000; //ms |
| |
| // amount of space to leave in a MMS for text and overhead. |
| private static final int MMS_MAX_SIZE_SLOP = 1024; |
| public static final long INVALID_TIMESTAMP = 0L; |
| private static String[] sNoSubjectStrings; |
| |
| public static class MmsInfo { |
| public Uri mUri; |
| public int mMessageSize; |
| public PduBody mPduBody; |
| } |
| |
| // Sync all remote messages apart from drafts |
| private static final String REMOTE_SMS_SELECTION = String.format( |
| Locale.US, |
| "(%s IN (%d, %d, %d, %d, %d))", |
| Sms.TYPE, |
| Sms.MESSAGE_TYPE_INBOX, |
| Sms.MESSAGE_TYPE_OUTBOX, |
| Sms.MESSAGE_TYPE_QUEUED, |
| Sms.MESSAGE_TYPE_FAILED, |
| Sms.MESSAGE_TYPE_SENT); |
| |
| private static final String REMOTE_MMS_SELECTION = String.format( |
| Locale.US, |
| "((%s IN (%d, %d, %d, %d)) AND (%s IN (%d, %d, %d)))", |
| Mms.MESSAGE_BOX, |
| Mms.MESSAGE_BOX_INBOX, |
| Mms.MESSAGE_BOX_OUTBOX, |
| Mms.MESSAGE_BOX_SENT, |
| Mms.MESSAGE_BOX_FAILED, |
| Mms.MESSAGE_TYPE, |
| PduHeaders.MESSAGE_TYPE_SEND_REQ, |
| PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND, |
| PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF); |
| |
| /** |
| * Type selection for importing sms messages. |
| * |
| * @return The SQL selection for importing sms messages |
| */ |
| public static String getSmsTypeSelectionSql() { |
| return REMOTE_SMS_SELECTION; |
| } |
| |
| /** |
| * Type selection for importing mms messages. |
| * |
| * @return The SQL selection for importing mms messages. This selects the message type, |
| * not including the selection on timestamp. |
| */ |
| public static String getMmsTypeSelectionSql() { |
| return REMOTE_MMS_SELECTION; |
| } |
| |
| // SMIL spec: http://www.w3.org/TR/SMIL3 |
| |
| private static final String sSmilImagePart = |
| "<par dur=\"" + DEFAULT_DURATION + "ms\">" + |
| "<img src=\"%s\" region=\"Image\" />" + |
| "</par>"; |
| |
| private static final String sSmilVideoPart = |
| "<par dur=\"%2$dms\">" + |
| "<video src=\"%1$s\" dur=\"%2$dms\" region=\"Image\" />" + |
| "</par>"; |
| |
| private static final String sSmilAudioPart = |
| "<par dur=\"%2$dms\">" + |
| "<audio src=\"%1$s\" dur=\"%2$dms\" />" + |
| "</par>"; |
| |
| private static final String sSmilTextPart = |
| "<par dur=\"" + DEFAULT_DURATION + "ms\">" + |
| "<text src=\"%s\" region=\"Text\" />" + |
| "</par>"; |
| |
| private static final String sSmilPart = |
| "<par dur=\"" + DEFAULT_DURATION + "ms\">" + |
| "<ref src=\"%s\" />" + |
| "</par>"; |
| |
| private static final String sSmilTextOnly = |
| "<smil>" + |
| "<head>" + |
| "<layout>" + |
| "<root-layout/>" + |
| "<region id=\"Text\" top=\"0\" left=\"0\" " |
| + "height=\"100%%\" width=\"100%%\"/>" + |
| "</layout>" + |
| "</head>" + |
| "<body>" + |
| "%s" + // constructed body goes here |
| "</body>" + |
| "</smil>"; |
| |
| private static final String sSmilVisualAttachmentsOnly = |
| "<smil>" + |
| "<head>" + |
| "<layout>" + |
| "<root-layout/>" + |
| "<region id=\"Image\" fit=\"meet\" top=\"0\" left=\"0\" " |
| + "height=\"100%%\" width=\"100%%\"/>" + |
| "</layout>" + |
| "</head>" + |
| "<body>" + |
| "%s" + // constructed body goes here |
| "</body>" + |
| "</smil>"; |
| |
| private static final String sSmilVisualAttachmentsWithText = |
| "<smil>" + |
| "<head>" + |
| "<layout>" + |
| "<root-layout/>" + |
| "<region id=\"Image\" fit=\"meet\" top=\"0\" left=\"0\" " |
| + "height=\"80%%\" width=\"100%%\"/>" + |
| "<region id=\"Text\" top=\"80%%\" left=\"0\" height=\"20%%\" " |
| + "width=\"100%%\"/>" + |
| "</layout>" + |
| "</head>" + |
| "<body>" + |
| "%s" + // constructed body goes here |
| "</body>" + |
| "</smil>"; |
| |
| private static final String sSmilNonVisualAttachmentsOnly = |
| "<smil>" + |
| "<head>" + |
| "<layout>" + |
| "<root-layout/>" + |
| "</layout>" + |
| "</head>" + |
| "<body>" + |
| "%s" + // constructed body goes here |
| "</body>" + |
| "</smil>"; |
| |
| private static final String sSmilNonVisualAttachmentsWithText = sSmilTextOnly; |
| |
| public static final String MMS_DUMP_PREFIX = "mmsdump-"; |
| public static final String SMS_DUMP_PREFIX = "smsdump-"; |
| |
| public static final int MIN_VIDEO_BYTES_PER_SECOND = 4 * 1024; |
| public static final int MIN_IMAGE_BYTE_SIZE = 16 * 1024; |
| public static final int MAX_VIDEO_ATTACHMENT_COUNT = 1; |
| |
| public static MmsInfo makePduBody(final Context context, final MessageData message, |
| final int subId) { |
| final PduBody pb = new PduBody(); |
| |
| // Compute data size requirements for this message: count up images and total size of |
| // non-image attachments. |
| int totalLength = 0; |
| int countImage = 0; |
| for (final MessagePartData part : message.getParts()) { |
| if (part.isAttachment()) { |
| final String contentType = part.getContentType(); |
| if (ContentType.isImageType(contentType)) { |
| countImage++; |
| } else if (ContentType.isVCardType(contentType)) { |
| totalLength += getDataLength(context, part.getContentUri()); |
| } else { |
| totalLength += getMediaFileSize(part.getContentUri()); |
| } |
| } |
| } |
| final long minSize = countImage * MIN_IMAGE_BYTE_SIZE; |
| final int byteBudget = MmsConfig.get(subId).getMaxMessageSize() - totalLength |
| - MMS_MAX_SIZE_SLOP; |
| final double budgetFactor = |
| minSize > 0 ? Math.max(1.0, byteBudget / ((double) minSize)) : 1; |
| final int bytesPerImage = (int) (budgetFactor * MIN_IMAGE_BYTE_SIZE); |
| final int widthLimit = MmsConfig.get(subId).getMaxImageWidth(); |
| final int heightLimit = MmsConfig.get(subId).getMaxImageHeight(); |
| |
| // Actually add the attachments, shrinking images appropriately. |
| int index = 0; |
| totalLength = 0; |
| boolean hasVisualAttachment = false; |
| boolean hasNonVisualAttachment = false; |
| boolean hasText = false; |
| final StringBuilder smilBody = new StringBuilder(); |
| for (final MessagePartData part : message.getParts()) { |
| String srcName; |
| if (part.isAttachment()) { |
| String contentType = part.getContentType(); |
| final String extension = ContentType.getExtensionFromMimeType(contentType); |
| if (ContentType.isImageType(contentType)) { |
| if (extension != null) { |
| srcName = String.format("image%06d.%s", index, extension); |
| } else { |
| // There's a good chance that if we selected the image from our media picker |
| // the content type is image/*. Fix the content type here for gifs so that |
| // we only need to open the input stream once. All other gif vs static image |
| // checks will only have to do a string comparison which is much cheaper. |
| final boolean isGif = ImageUtils.isGif(contentType, part.getContentUri()); |
| contentType = isGif ? ContentType.IMAGE_GIF : contentType; |
| srcName = String.format(isGif ? "image%06d.gif" : "image%06d.jpg", index); |
| } |
| smilBody.append(String.format(sSmilImagePart, srcName)); |
| totalLength += addPicturePart(context, pb, index, part, |
| widthLimit, heightLimit, bytesPerImage, srcName, contentType); |
| hasVisualAttachment = true; |
| } else if (ContentType.isVideoType(contentType)) { |
| srcName = String.format("video%06d.%s", index, |
| extension != null ? extension : "mp4"); |
| final int length = addVideoPart(context, pb, part, srcName); |
| totalLength += length; |
| smilBody.append(String.format(sSmilVideoPart, srcName, |
| getMediaDurationMs(context, part, DEFAULT_DURATION))); |
| hasVisualAttachment = true; |
| } else if (ContentType.isVCardType(contentType)) { |
| srcName = String.format("contact%06d.vcf", index); |
| totalLength += addVCardPart(context, pb, part, srcName); |
| smilBody.append(String.format(sSmilPart, srcName)); |
| hasNonVisualAttachment = true; |
| } else if (ContentType.isAudioType(contentType)) { |
| srcName = String.format("recording%06d.%s", |
| index, extension != null ? extension : "amr"); |
| totalLength += addOtherPart(context, pb, part, srcName); |
| final int duration = getMediaDurationMs(context, part, -1); |
| Assert.isTrue(duration != -1); |
| smilBody.append(String.format(sSmilAudioPart, srcName, duration)); |
| hasNonVisualAttachment = true; |
| } else { |
| srcName = String.format("other%06d.dat", index); |
| totalLength += addOtherPart(context, pb, part, srcName); |
| smilBody.append(String.format(sSmilPart, srcName)); |
| } |
| index++; |
| } |
| if (!TextUtils.isEmpty(part.getText())) { |
| hasText = true; |
| } |
| } |
| |
| if (hasText) { |
| final String srcName = String.format("text.%06d.txt", index); |
| final String text = message.getMessageText(); |
| totalLength += addTextPart(context, pb, text, srcName); |
| |
| // Append appropriate SMIL to the body. |
| smilBody.append(String.format(sSmilTextPart, srcName)); |
| } |
| |
| final String smilTemplate = getSmilTemplate(hasVisualAttachment, |
| hasNonVisualAttachment, hasText); |
| addSmilPart(pb, smilTemplate, smilBody.toString()); |
| |
| final MmsInfo mmsInfo = new MmsInfo(); |
| mmsInfo.mPduBody = pb; |
| mmsInfo.mMessageSize = totalLength; |
| |
| return mmsInfo; |
| } |
| |
| private static int getMediaDurationMs(final Context context, final MessagePartData part, |
| final int defaultDurationMs) { |
| Assert.notNull(context); |
| Assert.notNull(part); |
| Assert.isTrue(ContentType.isAudioType(part.getContentType()) || |
| ContentType.isVideoType(part.getContentType())); |
| |
| final MediaMetadataRetrieverWrapper retriever = new MediaMetadataRetrieverWrapper(); |
| try { |
| retriever.setDataSource(part.getContentUri()); |
| return retriever.extractInteger( |
| MediaMetadataRetriever.METADATA_KEY_DURATION, defaultDurationMs); |
| } catch (final IOException e) { |
| LogUtil.i(LogUtil.BUGLE_TAG, "Error extracting duration from " + part.getContentUri(), e); |
| return defaultDurationMs; |
| } finally { |
| retriever.release(); |
| } |
| } |
| |
| private static void setPartContentLocationAndId(final PduPart part, final String srcName) { |
| // Set Content-Location. |
| part.setContentLocation(srcName.getBytes()); |
| |
| // Set Content-Id. |
| final int index = srcName.lastIndexOf("."); |
| final String contentId = (index == -1) ? srcName : srcName.substring(0, index); |
| part.setContentId(contentId.getBytes()); |
| } |
| |
| private static int addTextPart(final Context context, final PduBody pb, |
| final String text, final String srcName) { |
| final PduPart part = new PduPart(); |
| |
| // Set Charset if it's a text media. |
| part.setCharset(CharacterSets.UTF_8); |
| |
| // Set Content-Type. |
| part.setContentType(ContentType.TEXT_PLAIN.getBytes()); |
| |
| // Set Content-Location. |
| setPartContentLocationAndId(part, srcName); |
| |
| part.setData(text.getBytes()); |
| |
| pb.addPart(part); |
| |
| return part.getData().length; |
| } |
| |
| private static int addPicturePart(final Context context, final PduBody pb, final int index, |
| final MessagePartData messagePart, int widthLimit, int heightLimit, |
| final int maxPartSize, final String srcName, final String contentType) { |
| final Uri imageUri = messagePart.getContentUri(); |
| final int width = messagePart.getWidth(); |
| final int height = messagePart.getHeight(); |
| |
| // Swap the width and height limits to match the orientation of the image so we scale the |
| // picture as little as possible. |
| if ((height > width) != (heightLimit > widthLimit)) { |
| final int temp = widthLimit; |
| widthLimit = heightLimit; |
| heightLimit = temp; |
| } |
| |
| final int orientation = ImageUtils.getOrientation(context, imageUri); |
| int imageSize = getDataLength(context, imageUri); |
| if (imageSize <= 0) { |
| LogUtil.e(TAG, "Can't get image", new Exception()); |
| return 0; |
| } |
| |
| if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { |
| LogUtil.v(TAG, "addPicturePart size: " + imageSize + " width: " |
| + width + " widthLimit: " + widthLimit |
| + " height: " + height |
| + " heightLimit: " + heightLimit); |
| } |
| |
| PduPart part; |
| // Check if we're already within the limits - in which case we don't need to resize. |
| // The size can be zero here, even when the media has content. See the comment in |
| // MediaModel.initMediaSize. Sometimes it'll compute zero and it's costly to read the |
| // whole stream to compute the size. When we call getResizedImageAsPart(), we'll correctly |
| // set the size. |
| if (imageSize <= maxPartSize && |
| width <= widthLimit && |
| height <= heightLimit && |
| (orientation == android.media.ExifInterface.ORIENTATION_UNDEFINED || |
| orientation == android.media.ExifInterface.ORIENTATION_NORMAL)) { |
| if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { |
| LogUtil.v(TAG, "addPicturePart - already sized"); |
| } |
| part = new PduPart(); |
| part.setDataUri(imageUri); |
| part.setContentType(contentType.getBytes()); |
| } else { |
| part = getResizedImageAsPart(widthLimit, heightLimit, maxPartSize, |
| width, height, orientation, imageUri, context, contentType); |
| if (part == null) { |
| final OutOfMemoryError e = new OutOfMemoryError(); |
| LogUtil.e(TAG, "Can't resize image: not enough memory?", e); |
| throw e; |
| } |
| imageSize = part.getData().length; |
| } |
| |
| setPartContentLocationAndId(part, srcName); |
| |
| pb.addPart(index, part); |
| |
| if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { |
| LogUtil.v(TAG, "addPicturePart size: " + imageSize); |
| } |
| |
| return imageSize; |
| } |
| |
| private static void addPartForUri(final Context context, final PduBody pb, |
| final String srcName, final Uri uri, final String contentType) { |
| final PduPart part = new PduPart(); |
| part.setDataUri(uri); |
| part.setContentType(contentType.getBytes()); |
| |
| setPartContentLocationAndId(part, srcName); |
| |
| pb.addPart(part); |
| } |
| |
| private static int addVCardPart(final Context context, final PduBody pb, |
| final MessagePartData messagePart, final String srcName) { |
| final Uri vcardUri = messagePart.getContentUri(); |
| final String contentType = messagePart.getContentType(); |
| final int vcardSize = getDataLength(context, vcardUri); |
| if (vcardSize <= 0) { |
| LogUtil.e(TAG, "Can't get vcard", new Exception()); |
| return 0; |
| } |
| |
| addPartForUri(context, pb, srcName, vcardUri, contentType); |
| |
| if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { |
| LogUtil.v(TAG, "addVCardPart size: " + vcardSize); |
| } |
| |
| return vcardSize; |
| } |
| |
| /** |
| * Add video part recompressing video if necessary. If recompression fails, part is not |
| * added. |
| */ |
| private static int addVideoPart(final Context context, final PduBody pb, |
| final MessagePartData messagePart, final String srcName) { |
| final Uri attachmentUri = messagePart.getContentUri(); |
| String contentType = messagePart.getContentType(); |
| |
| if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { |
| LogUtil.v(TAG, "addPart attachmentUrl: " + attachmentUri.toString()); |
| } |
| |
| if (TextUtils.isEmpty(contentType)) { |
| contentType = ContentType.VIDEO_3G2; |
| } |
| |
| addPartForUri(context, pb, srcName, attachmentUri, contentType); |
| return (int) getMediaFileSize(attachmentUri); |
| } |
| |
| private static int addOtherPart(final Context context, final PduBody pb, |
| final MessagePartData messagePart, final String srcName) { |
| final Uri attachmentUri = messagePart.getContentUri(); |
| final String contentType = messagePart.getContentType(); |
| |
| if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { |
| LogUtil.v(TAG, "addPart attachmentUrl: " + attachmentUri.toString()); |
| } |
| |
| final int dataSize = (int) getMediaFileSize(attachmentUri); |
| |
| addPartForUri(context, pb, srcName, attachmentUri, contentType); |
| |
| return dataSize; |
| } |
| |
| private static void addSmilPart(final PduBody pb, final String smilTemplate, |
| final String smilBody) { |
| final PduPart smilPart = new PduPart(); |
| smilPart.setContentId("smil".getBytes()); |
| smilPart.setContentLocation("smil.xml".getBytes()); |
| smilPart.setContentType(ContentType.APP_SMIL.getBytes()); |
| final String smil = String.format(smilTemplate, smilBody); |
| smilPart.setData(smil.getBytes()); |
| pb.addPart(0, smilPart); |
| } |
| |
| private static String getSmilTemplate(final boolean hasVisualAttachments, |
| final boolean hasNonVisualAttachments, final boolean hasText) { |
| if (hasVisualAttachments) { |
| return hasText ? sSmilVisualAttachmentsWithText : sSmilVisualAttachmentsOnly; |
| } |
| if (hasNonVisualAttachments) { |
| return hasText ? sSmilNonVisualAttachmentsWithText : sSmilNonVisualAttachmentsOnly; |
| } |
| return sSmilTextOnly; |
| } |
| |
| private static int getDataLength(final Context context, final Uri uri) { |
| InputStream is = null; |
| try { |
| is = context.getContentResolver().openInputStream(uri); |
| try { |
| return is == null ? 0 : is.available(); |
| } catch (final IOException e) { |
| LogUtil.e(TAG, "getDataLength couldn't stream: " + uri, e); |
| } |
| } catch (final FileNotFoundException e) { |
| LogUtil.e(TAG, "getDataLength couldn't open: " + uri, e); |
| } finally { |
| if (is != null) { |
| try { |
| is.close(); |
| } catch (final IOException e) { |
| LogUtil.e(TAG, "getDataLength couldn't close: " + uri, e); |
| } |
| } |
| } |
| return 0; |
| } |
| |
| /** |
| * Returns {@code true} if group mms is turned on, |
| * {@code false} otherwise. |
| * |
| * For the group mms feature to be enabled, the following must be true: |
| * 1. the feature is enabled in mms_config.xml (currently on by default) |
| * 2. the feature is enabled in the SMS settings page |
| * |
| * @return true if group mms is supported |
| */ |
| public static boolean groupMmsEnabled(final int subId) { |
| final Context context = Factory.get().getApplicationContext(); |
| final Resources resources = context.getResources(); |
| final BuglePrefs prefs = BuglePrefs.getSubscriptionPrefs(subId); |
| final String groupMmsKey = resources.getString(R.string.group_mms_pref_key); |
| final boolean groupMmsEnabledDefault = resources.getBoolean(R.bool.group_mms_pref_default); |
| final boolean groupMmsPrefOn = prefs.getBoolean(groupMmsKey, groupMmsEnabledDefault); |
| return MmsConfig.get(subId).getGroupMmsEnabled() && groupMmsPrefOn; |
| } |
| |
| /** |
| * Get a version of this image resized to fit the given dimension and byte-size limits. Note |
| * that the content type of the resulting PduPart may not be the same as the content type of |
| * this UriImage; always call {@link PduPart#getContentType()} to get the new content type. |
| * |
| * @param widthLimit The width limit, in pixels |
| * @param heightLimit The height limit, in pixels |
| * @param byteLimit The binary size limit, in bytes |
| * @param width The image width, in pixels |
| * @param height The image height, in pixels |
| * @param orientation Orientation constant from ExifInterface for rotating or flipping the |
| * image |
| * @param imageUri Uri to the image data |
| * @param context Needed to open the image |
| * @return A new PduPart containing the resized image data |
| */ |
| private static PduPart getResizedImageAsPart(final int widthLimit, |
| final int heightLimit, final int byteLimit, final int width, final int height, |
| final int orientation, final Uri imageUri, final Context context, final String contentType) { |
| final PduPart part = new PduPart(); |
| |
| final byte[] data = ImageResizer.getResizedImageData(width, height, orientation, |
| widthLimit, heightLimit, byteLimit, imageUri, context, contentType); |
| if (data == null) { |
| if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { |
| LogUtil.v(TAG, "Resize image failed."); |
| } |
| return null; |
| } |
| |
| part.setData(data); |
| // Any static images will be compressed into a jpeg |
| final String contentTypeOfResizedImage = ImageUtils.isGif(contentType, imageUri) |
| ? ContentType.IMAGE_GIF : ContentType.IMAGE_JPEG; |
| part.setContentType(contentTypeOfResizedImage.getBytes()); |
| |
| return part; |
| } |
| |
| /** |
| * Get media file size |
| */ |
| public static long getMediaFileSize(final Uri uri) { |
| final Context context = Factory.get().getApplicationContext(); |
| AssetFileDescriptor fd = null; |
| try { |
| fd = context.getContentResolver().openAssetFileDescriptor(uri, "r"); |
| if (fd != null) { |
| return fd.getParcelFileDescriptor().getStatSize(); |
| } |
| } catch (final FileNotFoundException e) { |
| LogUtil.e(TAG, "MmsUtils.getMediaFileSize: cound not find media file: " + e, e); |
| } finally { |
| if (fd != null) { |
| try { |
| fd.close(); |
| } catch (final IOException e) { |
| LogUtil.e(TAG, "MmsUtils.getMediaFileSize: failed to close " + e, e); |
| } |
| } |
| } |
| return 0L; |
| } |
| |
| // Code for extracting the actual phone numbers for the participants in a conversation, |
| // given a thread id. |
| |
| private static final Uri ALL_THREADS_URI = |
| Threads.CONTENT_URI.buildUpon().appendQueryParameter("simple", "true").build(); |
| |
| private static final String[] RECIPIENTS_PROJECTION = { |
| Threads._ID, |
| Threads.RECIPIENT_IDS |
| }; |
| |
| private static final int RECIPIENT_IDS = 1; |
| |
| public static List<String> getRecipientsByThread(final long threadId) { |
| final String spaceSepIds = getRawRecipientIdsForThread(threadId); |
| if (!TextUtils.isEmpty(spaceSepIds)) { |
| final Context context = Factory.get().getApplicationContext(); |
| return getAddresses(context, spaceSepIds); |
| } |
| return null; |
| } |
| |
| // NOTE: There are phones on which you can't get the recipients from the thread id for SMS |
| // until you have a message in the conversation! |
| public static String getRawRecipientIdsForThread(final long threadId) { |
| if (threadId <= 0) { |
| return null; |
| } |
| final Context context = Factory.get().getApplicationContext(); |
| final ContentResolver cr = context.getContentResolver(); |
| final Cursor thread = cr.query( |
| ALL_THREADS_URI, |
| RECIPIENTS_PROJECTION, "_id=?", new String[] { String.valueOf(threadId) }, null); |
| if (thread != null) { |
| try { |
| if (thread.moveToFirst()) { |
| // recipientIds will be a space-separated list of ids into the |
| // canonical addresses table. |
| return thread.getString(RECIPIENT_IDS); |
| } |
| } finally { |
| thread.close(); |
| } |
| } |
| return null; |
| } |
| |
| private static final Uri SINGLE_CANONICAL_ADDRESS_URI = |
| Uri.parse("content://mms-sms/canonical-address"); |
| |
| private static List<String> getAddresses(final Context context, final String spaceSepIds) { |
| final List<String> numbers = new ArrayList<String>(); |
| final String[] ids = spaceSepIds.split(" "); |
| for (final String id : ids) { |
| long longId; |
| |
| try { |
| longId = Long.parseLong(id); |
| if (longId < 0) { |
| LogUtil.e(TAG, "MmsUtils.getAddresses: invalid id " + longId); |
| continue; |
| } |
| } catch (final NumberFormatException ex) { |
| LogUtil.e(TAG, "MmsUtils.getAddresses: invalid id. " + ex, ex); |
| // skip this id |
| continue; |
| } |
| |
| // TODO: build a single query where we get all the addresses at once. |
| Cursor c = null; |
| try { |
| c = context.getContentResolver().query( |
| ContentUris.withAppendedId(SINGLE_CANONICAL_ADDRESS_URI, longId), |
| null, null, null, null); |
| } catch (final Exception e) { |
| LogUtil.e(TAG, "MmsUtils.getAddresses: query failed for id " + longId, e); |
| } |
| if (c != null) { |
| try { |
| if (c.moveToFirst()) { |
| final String number = c.getString(0); |
| if (!TextUtils.isEmpty(number)) { |
| numbers.add(number); |
| } else { |
| LogUtil.w(TAG, "Canonical MMS/SMS address is empty for id: " + longId); |
| } |
| } |
| } finally { |
| c.close(); |
| } |
| } |
| } |
| if (numbers.isEmpty()) { |
| LogUtil.w(TAG, "No MMS addresses found from ids string [" + spaceSepIds + "]"); |
| } |
| return numbers; |
| } |
| |
| // Get telephony SMS thread ID |
| public static long getOrCreateSmsThreadId(final Context context, final String dest) { |
| // use destinations to determine threadId |
| final Set<String> recipients = new HashSet<String>(); |
| recipients.add(dest); |
| try { |
| return MmsSmsUtils.Threads.getOrCreateThreadId(context, recipients); |
| } catch (final IllegalArgumentException e) { |
| LogUtil.e(TAG, "MmsUtils: getting thread id failed: " + e); |
| return -1; |
| } |
| } |
| |
| // Get telephony SMS thread ID |
| public static long getOrCreateThreadId(final Context context, final List<String> dests) { |
| if (dests == null || dests.size() == 0) { |
| return -1; |
| } |
| // use destinations to determine threadId |
| final Set<String> recipients = new HashSet<String>(dests); |
| try { |
| return MmsSmsUtils.Threads.getOrCreateThreadId(context, recipients); |
| } catch (final IllegalArgumentException e) { |
| LogUtil.e(TAG, "MmsUtils: getting thread id failed: " + e); |
| return -1; |
| } |
| } |
| |
| /** |
| * Add an SMS to the given URI with thread_id specified. |
| * |
| * @param resolver the content resolver to use |
| * @param uri the URI to add the message to |
| * @param subId subId for the receiving sim |
| * @param address the address of the sender |
| * @param body the body of the message |
| * @param subject the psuedo-subject of the message |
| * @param date the timestamp for the message |
| * @param read true if the message has been read, false if not |
| * @param threadId the thread_id of the message |
| * @return the URI for the new message |
| */ |
| private static Uri addMessageToUri(final ContentResolver resolver, |
| final Uri uri, final int subId, final String address, final String body, |
| final String subject, final Long date, final boolean read, final boolean seen, |
| final int status, final int type, final long threadId) { |
| final ContentValues values = new ContentValues(7); |
| |
| values.put(Telephony.Sms.ADDRESS, address); |
| if (date != null) { |
| values.put(Telephony.Sms.DATE, date); |
| } |
| values.put(Telephony.Sms.READ, read ? 1 : 0); |
| values.put(Telephony.Sms.SEEN, seen ? 1 : 0); |
| values.put(Telephony.Sms.SUBJECT, subject); |
| values.put(Telephony.Sms.BODY, body); |
| if (OsUtil.isAtLeastL_MR1()) { |
| values.put(Telephony.Sms.SUBSCRIPTION_ID, subId); |
| } |
| if (status != Telephony.Sms.STATUS_NONE) { |
| values.put(Telephony.Sms.STATUS, status); |
| } |
| if (type != Telephony.Sms.MESSAGE_TYPE_ALL) { |
| values.put(Telephony.Sms.TYPE, type); |
| } |
| if (threadId != -1L) { |
| values.put(Telephony.Sms.THREAD_ID, threadId); |
| } |
| return resolver.insert(uri, values); |
| } |
| |
| // Insert an SMS message to telephony |
| public static Uri insertSmsMessage(final Context context, final Uri uri, final int subId, |
| final String dest, final String text, final long timestamp, final int status, |
| final int type, final long threadId) { |
| Uri response = null; |
| try { |
| response = addMessageToUri(context.getContentResolver(), uri, subId, dest, |
| text, null /* subject */, timestamp, true /* read */, |
| true /* seen */, status, type, threadId); |
| if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { |
| LogUtil.d(TAG, "Mmsutils: Inserted SMS message into telephony (type = " + type + ")" |
| + ", uri: " + response); |
| } |
| } catch (final SQLiteException e) { |
| LogUtil.e(TAG, "MmsUtils: persist sms message failure " + e, e); |
| } catch (final IllegalArgumentException e) { |
| LogUtil.e(TAG, "MmsUtils: persist sms message failure " + e, e); |
| } |
| return response; |
| } |
| |
| // Update SMS message type in telephony; returns true if it succeeded. |
| public static boolean updateSmsMessageSendingStatus(final Context context, final Uri uri, |
| final int type, final long date) { |
| try { |
| final ContentResolver resolver = context.getContentResolver(); |
| final ContentValues values = new ContentValues(2); |
| |
| values.put(Telephony.Sms.TYPE, type); |
| values.put(Telephony.Sms.DATE, date); |
| final int cnt = resolver.update(uri, values, null, null); |
| if (cnt == 1) { |
| if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { |
| LogUtil.d(TAG, "Mmsutils: Updated sending SMS " + uri + "; type = " + type |
| + ", date = " + date + " (millis since epoch)"); |
| } |
| return true; |
| } |
| } catch (final SQLiteException e) { |
| LogUtil.e(TAG, "MmsUtils: update sms message failure " + e, e); |
| } catch (final IllegalArgumentException e) { |
| LogUtil.e(TAG, "MmsUtils: update sms message failure " + e, e); |
| } |
| return false; |
| } |
| |
| // Persist a sent MMS message in telephony |
| private static Uri insertSendReq(final Context context, final GenericPdu pdu, final int subId, |
| final String subPhoneNumber) { |
| final PduPersister persister = PduPersister.getPduPersister(context); |
| Uri uri = null; |
| try { |
| // Persist the PDU |
| uri = persister.persist( |
| pdu, |
| Mms.Sent.CONTENT_URI, |
| subId, |
| subPhoneNumber, |
| null/*preOpenedFiles*/); |
| // Update mms table to reflect sent messages are always seen and read |
| final ContentValues values = new ContentValues(1); |
| values.put(Mms.READ, 1); |
| values.put(Mms.SEEN, 1); |
| SqliteWrapper.update(context, context.getContentResolver(), uri, values, null, null); |
| } catch (final MmsException e) { |
| LogUtil.e(TAG, "MmsUtils: persist mms sent message failure " + e, e); |
| } |
| return uri; |
| } |
| |
| // Persist a received MMS message in telephony |
| public static Uri insertReceivedMmsMessage(final Context context, |
| final RetrieveConf retrieveConf, final int subId, final String subPhoneNumber, |
| final long receivedTimestampInSeconds, final long expiry, final String transactionId) { |
| final PduPersister persister = PduPersister.getPduPersister(context); |
| Uri uri = null; |
| try { |
| uri = persister.persist( |
| retrieveConf, |
| Mms.Inbox.CONTENT_URI, |
| subId, |
| subPhoneNumber, |
| null/*preOpenedFiles*/); |
| |
| final ContentValues values = new ContentValues(3); |
| // Update mms table with local time instead of PDU time |
| values.put(Mms.DATE, receivedTimestampInSeconds); |
| // Also update the transaction id and the expiry from NotificationInd so that |
| // wap push dedup would work even after the wap push is deleted. |
| values.put(Mms.TRANSACTION_ID, transactionId); |
| values.put(Mms.EXPIRY, expiry); |
| SqliteWrapper.update(context, context.getContentResolver(), uri, values, null, null); |
| if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { |
| LogUtil.d(TAG, "MmsUtils: Inserted MMS message into telephony, uri: " + uri); |
| } |
| } catch (final MmsException e) { |
| LogUtil.e(TAG, "MmsUtils: persist mms received message failure " + e, e); |
| // Just returns empty uri to RetrieveMmsRequest, which triggers a permanent failure |
| } catch (final SQLiteException e) { |
| LogUtil.e(TAG, "MmsUtils: update mms received message failure " + e, e); |
| // Time update failure is ignored. |
| } |
| return uri; |
| } |
| |
| // Update MMS message type in telephony; returns true if it succeeded. |
| public static boolean updateMmsMessageSendingStatus(final Context context, final Uri uri, |
| final int box, final long timestampInMillis) { |
| try { |
| final ContentResolver resolver = context.getContentResolver(); |
| final ContentValues values = new ContentValues(); |
| |
| final long timestampInSeconds = timestampInMillis / 1000L; |
| values.put(Telephony.Mms.MESSAGE_BOX, box); |
| values.put(Telephony.Mms.DATE, timestampInSeconds); |
| final int cnt = resolver.update(uri, values, null, null); |
| if (cnt == 1) { |
| if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { |
| LogUtil.d(TAG, "Mmsutils: Updated sending MMS " + uri + "; box = " + box |
| + ", date = " + timestampInSeconds + " (secs since epoch)"); |
| } |
| return true; |
| } |
| } catch (final SQLiteException e) { |
| LogUtil.e(TAG, "MmsUtils: update mms message failure " + e, e); |
| } catch (final IllegalArgumentException e) { |
| LogUtil.e(TAG, "MmsUtils: update mms message failure " + e, e); |
| } |
| return false; |
| } |
| |
| /** |
| * Parse values from a received sms message |
| * |
| * @param context |
| * @param msgs The received sms message content |
| * @param error The received sms error |
| * @return Parsed values from the message |
| */ |
| public static ContentValues parseReceivedSmsMessage( |
| final Context context, final SmsMessage[] msgs, final int error) { |
| final SmsMessage sms = msgs[0]; |
| final ContentValues values = new ContentValues(); |
| |
| values.put(Sms.ADDRESS, sms.getDisplayOriginatingAddress()); |
| values.put(Sms.BODY, buildMessageBodyFromPdus(msgs)); |
| if (MmsUtils.hasSmsDateSentColumn()) { |
| // TODO:: The boxing here seems unnecessary. |
| values.put(Sms.DATE_SENT, Long.valueOf(sms.getTimestampMillis())); |
| } |
| values.put(Sms.PROTOCOL, sms.getProtocolIdentifier()); |
| if (sms.getPseudoSubject().length() > 0) { |
| values.put(Sms.SUBJECT, sms.getPseudoSubject()); |
| } |
| values.put(Sms.REPLY_PATH_PRESENT, sms.isReplyPathPresent() ? 1 : 0); |
| values.put(Sms.SERVICE_CENTER, sms.getServiceCenterAddress()); |
| // Error code |
| values.put(Sms.ERROR_CODE, error); |
| |
| return values; |
| } |
| |
| // Some providers send formfeeds in their messages. Convert those formfeeds to newlines. |
| private static String replaceFormFeeds(final String s) { |
| return s == null ? "" : s.replace('\f', '\n'); |
| } |
| |
| // Parse the message body from message PDUs |
| private static String buildMessageBodyFromPdus(final SmsMessage[] msgs) { |
| if (msgs.length == 1) { |
| // There is only one part, so grab the body directly. |
| return replaceFormFeeds(msgs[0].getDisplayMessageBody()); |
| } else { |
| // Build up the body from the parts. |
| final StringBuilder body = new StringBuilder(); |
| for (final SmsMessage msg : msgs) { |
| try { |
| // getDisplayMessageBody() can NPE if mWrappedMessage inside is null. |
| body.append(msg.getDisplayMessageBody()); |
| } catch (final NullPointerException e) { |
| // Nothing to do |
| } |
| } |
| return replaceFormFeeds(body.toString()); |
| } |
| } |
| |
| // Parse the message date |
| public static Long getMessageDate(final SmsMessage sms, long now) { |
| // Use now for the timestamp to avoid confusion with clock |
| // drift between the handset and the SMSC. |
| // Check to make sure the system is giving us a non-bogus time. |
| final Calendar buildDate = new GregorianCalendar(2011, 8, 18); // 18 Sep 2011 |
| final Calendar nowDate = new GregorianCalendar(); |
| nowDate.setTimeInMillis(now); |
| if (nowDate.before(buildDate)) { |
| // It looks like our system clock isn't set yet because the current time right now |
| // is before an arbitrary time we made this build. Instead of inserting a bogus |
| // receive time in this case, use the timestamp of when the message was sent. |
| now = sms.getTimestampMillis(); |
| } |
| return now; |
| } |
| |
| /** |
| * cleanseMmsSubject will take a subject that's says, "<Subject: no subject>", and return |
| * a null string. Otherwise it will return the original subject string. |
| * @param resources So the function can grab string resources |
| * @param subject the raw subject |
| * @return |
| */ |
| public static String cleanseMmsSubject(final Resources resources, final String subject) { |
| if (TextUtils.isEmpty(subject)) { |
| return null; |
| } |
| if (sNoSubjectStrings == null) { |
| sNoSubjectStrings = |
| resources.getStringArray(R.array.empty_subject_strings); |
| } |
| for (final String noSubjectString : sNoSubjectStrings) { |
| if (subject.equalsIgnoreCase(noSubjectString)) { |
| return null; |
| } |
| } |
| return subject; |
| } |
| |
| /** |
| * @return Whether to auto retrieve MMS |
| */ |
| public static boolean allowMmsAutoRetrieve(final int subId) { |
| final Context context = Factory.get().getApplicationContext(); |
| final Resources resources = context.getResources(); |
| final BuglePrefs prefs = BuglePrefs.getSubscriptionPrefs(subId); |
| final boolean autoRetrieve = prefs.getBoolean( |
| resources.getString(R.string.auto_retrieve_mms_pref_key), |
| resources.getBoolean(R.bool.auto_retrieve_mms_pref_default)); |
| if (autoRetrieve) { |
| final boolean autoRetrieveInRoaming = prefs.getBoolean( |
| resources.getString(R.string.auto_retrieve_mms_when_roaming_pref_key), |
| resources.getBoolean(R.bool.auto_retrieve_mms_when_roaming_pref_default)); |
| final PhoneUtils phoneUtils = PhoneUtils.get(subId); |
| if ((autoRetrieveInRoaming && phoneUtils.isDataRoamingEnabled()) |
| || !phoneUtils.isRoaming()) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Parse the message row id from a message Uri. |
| * |
| * @param messageUri The input Uri |
| * @return The message row id if valid, otherwise -1 |
| */ |
| public static long parseRowIdFromMessageUri(final Uri messageUri) { |
| try { |
| if (messageUri != null) { |
| return ContentUris.parseId(messageUri); |
| } |
| } catch (final UnsupportedOperationException e) { |
| // Nothing to do |
| } catch (final NumberFormatException e) { |
| // Nothing to do |
| } |
| return -1; |
| } |
| |
| public static SmsMessage getSmsMessageFromDeliveryReport(final Intent intent) { |
| final byte[] pdu = intent.getByteArrayExtra("pdu"); |
| final String format = intent.getStringExtra("format"); |
| return OsUtil.isAtLeastM() |
| ? SmsMessage.createFromPdu(pdu, format) |
| : SmsMessage.createFromPdu(pdu); |
| } |
| |
| /** |
| * Update the status and date_sent column of sms message in telephony provider |
| * |
| * @param smsMessageUri |
| * @param status |
| * @param timeSentInMillis |
| */ |
| public static void updateSmsStatusAndDateSent(final Uri smsMessageUri, final int status, |
| final long timeSentInMillis) { |
| if (smsMessageUri == null) { |
| return; |
| } |
| final ContentValues values = new ContentValues(); |
| values.put(Sms.STATUS, status); |
| if (MmsUtils.hasSmsDateSentColumn()) { |
| values.put(Sms.DATE_SENT, timeSentInMillis); |
| } |
| final ContentResolver resolver = Factory.get().getApplicationContext().getContentResolver(); |
| resolver.update(smsMessageUri, values, null/*where*/, null/*selectionArgs*/); |
| } |
| |
| /** |
| * Get the SQL selection statement for matching messages with media. |
| * |
| * Example for MMS part table: |
| * "((ct LIKE 'image/%') |
| * OR (ct LIKE 'video/%') |
| * OR (ct LIKE 'audio/%') |
| * OR (ct='application/ogg')) |
| * |
| * @param contentTypeColumn The content-type column name |
| * @return The SQL selection statement for matching media types: image, video, audio |
| */ |
| public static String getMediaTypeSelectionSql(final String contentTypeColumn) { |
| return String.format( |
| Locale.US, |
| "((%s LIKE '%s') OR (%s LIKE '%s') OR (%s LIKE '%s') OR (%s='%s'))", |
| contentTypeColumn, |
| "image/%", |
| contentTypeColumn, |
| "video/%", |
| contentTypeColumn, |
| "audio/%", |
| contentTypeColumn, |
| ContentType.AUDIO_OGG); |
| } |
| |
| // Max number of operands per SQL query for deleting SMS messages |
| public static final int MAX_IDS_PER_QUERY = 128; |
| |
| /** |
| * Delete MMS messages with media parts. |
| * |
| * Because the telephony provider constraints, we can't use JOIN and delete messages in one |
| * shot. We have to do a query first and then batch delete the messages based on IDs. |
| * |
| * @return The count of messages deleted. |
| */ |
| public static int deleteMediaMessages() { |
| // Do a query first |
| // |
| // The WHERE clause has two parts: |
| // The first part is to select the exact same types of MMS messages as when we import them |
| // (so that we don't delete messages that are not in local database) |
| // The second part is to select MMS with media parts, including image, video and audio |
| final String selection = String.format( |
| Locale.US, |
| "%s AND (%s IN (SELECT %s FROM part WHERE %s))", |
| getMmsTypeSelectionSql(), |
| Mms._ID, |
| Mms.Part.MSG_ID, |
| getMediaTypeSelectionSql(Mms.Part.CONTENT_TYPE)); |
| final ContentResolver resolver = Factory.get().getApplicationContext().getContentResolver(); |
| final Cursor cursor = resolver.query(Mms.CONTENT_URI, |
| new String[]{ Mms._ID }, |
| selection, |
| null/*selectionArgs*/, |
| null/*sortOrder*/); |
| int deleted = 0; |
| if (cursor != null) { |
| final long[] messageIds = new long[cursor.getCount()]; |
| try { |
| int i = 0; |
| while (cursor.moveToNext()) { |
| messageIds[i++] = cursor.getLong(0); |
| } |
| } finally { |
| cursor.close(); |
| } |
| final int totalIds = messageIds.length; |
| if (totalIds > 0) { |
| // Batch delete the messages using IDs |
| // We don't want to send all IDs at once since there is a limit on SQL statement |
| for (int start = 0; start < totalIds; start += MAX_IDS_PER_QUERY) { |
| final int end = Math.min(start + MAX_IDS_PER_QUERY, totalIds); // excluding |
| final int count = end - start; |
| final String batchSelection = String.format( |
| Locale.US, |
| "%s IN %s", |
| Mms._ID, |
| getSqlInOperand(count)); |
| final String[] batchSelectionArgs = |
| getSqlInOperandArgs(messageIds, start, count); |
| final int deletedForBatch = resolver.delete( |
| Mms.CONTENT_URI, |
| batchSelection, |
| batchSelectionArgs); |
| if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { |
| LogUtil.d(TAG, "deleteMediaMessages: deleting IDs = " |
| + Joiner.on(',').skipNulls().join(batchSelectionArgs) |
| + ", deleted = " + deletedForBatch); |
| } |
| deleted += deletedForBatch; |
| } |
| } |
| } |
| return deleted; |
| } |
| |
| /** |
| * Get the (?,?,...) thing for the SQL IN operator by a count |
| * |
| * @param count |
| * @return |
| */ |
| public static String getSqlInOperand(final int count) { |
| if (count <= 0) { |
| return null; |
| } |
| final StringBuilder sb = new StringBuilder(); |
| sb.append("(?"); |
| for (int i = 0; i < count - 1; i++) { |
| sb.append(",?"); |
| } |
| sb.append(")"); |
| return sb.toString(); |
| } |
| |
| /** |
| * Get the args for SQL IN operator from a long ID array |
| * |
| * @param ids The original long id array |
| * @param start Start of the ids to fill the args |
| * @param count Number of ids to pack |
| * @return The long array with the id args |
| */ |
| private static String[] getSqlInOperandArgs( |
| final long[] ids, final int start, final int count) { |
| if (count <= 0) { |
| return null; |
| } |
| final String[] args = new String[count]; |
| for (int i = 0; i < count; i++) { |
| args[i] = Long.toString(ids[start + i]); |
| } |
| return args; |
| } |
| |
| /** |
| * Delete SMS and MMS messages that are earlier than a specific timestamp |
| * |
| * @param cutOffTimestampInMillis The cut-off timestamp |
| * @return Total number of messages deleted. |
| */ |
| public static int deleteMessagesOlderThan(final long cutOffTimestampInMillis) { |
| int deleted = 0; |
| final ContentResolver resolver = Factory.get().getApplicationContext().getContentResolver(); |
| // Delete old SMS |
| final String smsSelection = String.format( |
| Locale.US, |
| "%s AND (%s<=%d)", |
| getSmsTypeSelectionSql(), |
| Sms.DATE, |
| cutOffTimestampInMillis); |
| deleted += resolver.delete(Sms.CONTENT_URI, smsSelection, null/*selectionArgs*/); |
| // Delete old MMS |
| final String mmsSelection = String.format( |
| Locale.US, |
| "%s AND (%s<=%d)", |
| getMmsTypeSelectionSql(), |
| Mms.DATE, |
| cutOffTimestampInMillis / 1000L); |
| deleted += resolver.delete(Mms.CONTENT_URI, mmsSelection, null/*selectionArgs*/); |
| return deleted; |
| } |
| |
| /** |
| * Update the read status of SMS/MMS messages by thread and timestamp |
| * |
| * @param threadId The thread of sms/mms to change |
| * @param timestampInMillis Change the status before this timestamp |
| */ |
| public static void updateSmsReadStatus(final long threadId, final long timestampInMillis) { |
| final ContentResolver resolver = Factory.get().getApplicationContext().getContentResolver(); |
| final ContentValues values = new ContentValues(); |
| values.put("read", 1); |
| values.put("seen", 1); /* If you read it you saw it */ |
| final String smsSelection = String.format( |
| Locale.US, |
| "%s=%d AND %s<=%d AND %s=0", |
| Sms.THREAD_ID, |
| threadId, |
| Sms.DATE, |
| timestampInMillis, |
| Sms.READ); |
| resolver.update( |
| Sms.CONTENT_URI, |
| values, |
| smsSelection, |
| null/*selectionArgs*/); |
| final String mmsSelection = String.format( |
| Locale.US, |
| "%s=%d AND %s<=%d AND %s=0", |
| Mms.THREAD_ID, |
| threadId, |
| Mms.DATE, |
| timestampInMillis / 1000L, |
| Mms.READ); |
| resolver.update( |
| Mms.CONTENT_URI, |
| values, |
| mmsSelection, |
| null/*selectionArgs*/); |
| } |
| |
| /** |
| * Update the read status of a single MMS message by its URI |
| * |
| * @param mmsUri |
| * @param read |
| */ |
| public static void updateReadStatusForMmsMessage(final Uri mmsUri, final boolean read) { |
| final ContentResolver resolver = Factory.get().getApplicationContext().getContentResolver(); |
| final ContentValues values = new ContentValues(); |
| values.put(Mms.READ, read ? 1 : 0); |
| resolver.update(mmsUri, values, null/*where*/, null/*selectionArgs*/); |
| } |
| |
| public static class AttachmentInfo { |
| public String mUrl; |
| public String mContentType; |
| public int mWidth; |
| public int mHeight; |
| } |
| |
| /** |
| * Convert byte array to Java String using a charset name |
| * |
| * @param bytes |
| * @param charsetName |
| * @return |
| */ |
| public static String bytesToString(final byte[] bytes, final String charsetName) { |
| if (bytes == null) { |
| return null; |
| } |
| try { |
| return new String(bytes, charsetName); |
| } catch (final UnsupportedEncodingException e) { |
| LogUtil.e(TAG, "MmsUtils.bytesToString: " + e, e); |
| return new String(bytes); |
| } |
| } |
| |
| /** |
| * Convert a Java String to byte array using a charset name |
| * |
| * @param string |
| * @param charsetName |
| * @return |
| */ |
| public static byte[] stringToBytes(final String string, final String charsetName) { |
| if (string == null) { |
| return null; |
| } |
| try { |
| return string.getBytes(charsetName); |
| } catch (final UnsupportedEncodingException e) { |
| LogUtil.e(TAG, "MmsUtils.stringToBytes: " + e, e); |
| return string.getBytes(); |
| } |
| } |
| |
| private static final String[] TEST_DATE_SENT_PROJECTION = new String[] { Sms.DATE_SENT }; |
| private static Boolean sHasSmsDateSentColumn = null; |
| /** |
| * Check if date_sent column exists on ICS and above devices. We need to do a test |
| * query to figure that out since on some ICS+ devices, somehow the date_sent column does |
| * not exist. http://b/17629135 tracks the associated compliance test. |
| * |
| * @return Whether "date_sent" column exists in sms table |
| */ |
| public static boolean hasSmsDateSentColumn() { |
| if (sHasSmsDateSentColumn == null) { |
| Cursor cursor = null; |
| try { |
| final Context context = Factory.get().getApplicationContext(); |
| final ContentResolver resolver = context.getContentResolver(); |
| cursor = SqliteWrapper.query( |
| context, |
| resolver, |
| Sms.CONTENT_URI, |
| TEST_DATE_SENT_PROJECTION, |
| null/*selection*/, |
| null/*selectionArgs*/, |
| Sms.DATE_SENT + " ASC LIMIT 1"); |
| sHasSmsDateSentColumn = true; |
| } catch (final SQLiteException e) { |
| LogUtil.w(TAG, "date_sent in sms table does not exist", e); |
| sHasSmsDateSentColumn = false; |
| } finally { |
| if (cursor != null) { |
| cursor.close(); |
| } |
| } |
| } |
| return sHasSmsDateSentColumn; |
| } |
| |
| private static final String[] TEST_CARRIERS_PROJECTION = |
| new String[] { Telephony.Carriers.MMSC }; |
| private static Boolean sUseSystemApn = null; |
| /** |
| * Check if we can access the APN data in the Telephony provider. Access was restricted in |
| * JB MR1 (and some JB MR2) devices. If we can't access the APN, we have to fall back and use |
| * a private table in our own app. |
| * |
| * @return Whether we can access the system APN table |
| */ |
| public static boolean useSystemApnTable() { |
| if (sUseSystemApn == null) { |
| Cursor cursor = null; |
| try { |
| final Context context = Factory.get().getApplicationContext(); |
| final ContentResolver resolver = context.getContentResolver(); |
| cursor = SqliteWrapper.query( |
| context, |
| resolver, |
| Telephony.Carriers.CONTENT_URI, |
| TEST_CARRIERS_PROJECTION, |
| null/*selection*/, |
| null/*selectionArgs*/, |
| null); |
| sUseSystemApn = true; |
| } catch (final SecurityException e) { |
| LogUtil.w(TAG, "Can't access system APN, using internal table", e); |
| sUseSystemApn = false; |
| } finally { |
| if (cursor != null) { |
| cursor.close(); |
| } |
| } |
| } |
| return sUseSystemApn; |
| } |
| |
| // For the internal debugger only |
| public static void setUseSystemApnTable(final boolean turnOn) { |
| if (!turnOn) { |
| // We're not turning on to the system table. Instead, we're using our internal table. |
| final int osVersion = OsUtil.getApiVersion(); |
| if (osVersion != android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) { |
| // We're turning on local APNs on a device where we wouldn't normally have the |
| // local APN table. Build it here. |
| |
| final SQLiteDatabase database = ApnDatabase.getApnDatabase().getWritableDatabase(); |
| |
| // Do we already have the table? |
| Cursor cursor = null; |
| try { |
| cursor = database.query(ApnDatabase.APN_TABLE, |
| ApnDatabase.APN_PROJECTION, |
| null, null, null, null, null, null); |
| } catch (final Exception e) { |
| // Apparently there's no table, create it now. |
| ApnDatabase.forceBuildAndLoadApnTables(); |
| } finally { |
| if (cursor != null) { |
| cursor.close(); |
| } |
| } |
| } |
| } |
| sUseSystemApn = turnOn; |
| } |
| |
| /** |
| * Checks if we should dump sms, based on both the setting and the global debug |
| * flag |
| * |
| * @return if dump sms is enabled |
| */ |
| public static boolean isDumpSmsEnabled() { |
| if (!DebugUtils.isDebugEnabled()) { |
| return false; |
| } |
| return getDumpSmsOrMmsPref(R.string.dump_sms_pref_key, R.bool.dump_sms_pref_default); |
| } |
| |
| /** |
| * Checks if we should dump mms, based on both the setting and the global debug |
| * flag |
| * |
| * @return if dump mms is enabled |
| */ |
| public static boolean isDumpMmsEnabled() { |
| if (!DebugUtils.isDebugEnabled()) { |
| return false; |
| } |
| return getDumpSmsOrMmsPref(R.string.dump_mms_pref_key, R.bool.dump_mms_pref_default); |
| } |
| |
| /** |
| * Load the value of dump sms or mms setting preference |
| */ |
| private static boolean getDumpSmsOrMmsPref(final int prefKeyRes, final int defaultKeyRes) { |
| final Context context = Factory.get().getApplicationContext(); |
| final Resources resources = context.getResources(); |
| final BuglePrefs prefs = BuglePrefs.getApplicationPrefs(); |
| final String key = resources.getString(prefKeyRes); |
| final boolean defaultValue = resources.getBoolean(defaultKeyRes); |
| return prefs.getBoolean(key, defaultValue); |
| } |
| |
| public static final Uri MMS_PART_CONTENT_URI = Uri.parse("content://mms/part"); |
| |
| /** |
| * Load MMS from telephony |
| * |
| * @param mmsUri The MMS pdu Uri |
| * @return A memory copy of the MMS pdu including parts (but not addresses) |
| */ |
| public static DatabaseMessages.MmsMessage loadMms(final Uri mmsUri) { |
| final Context context = Factory.get().getApplicationContext(); |
| final ContentResolver resolver = context.getContentResolver(); |
| DatabaseMessages.MmsMessage mms = null; |
| Cursor cursor = null; |
| // Load pdu first |
| try { |
| cursor = SqliteWrapper.query(context, resolver, |
| mmsUri, |
| DatabaseMessages.MmsMessage.getProjection(), |
| null/*selection*/, null/*selectionArgs*/, null/*sortOrder*/); |
| if (cursor != null && cursor.moveToFirst()) { |
| mms = DatabaseMessages.MmsMessage.get(cursor); |
| } |
| } catch (final SQLiteException e) { |
| LogUtil.e(TAG, "MmsLoader: query pdu failure: " + e, e); |
| } finally { |
| if (cursor != null) { |
| cursor.close(); |
| } |
| } |
| if (mms == null) { |
| return null; |
| } |
| // Load parts except SMIL |
| // TODO: we may need to load SMIL part in the future. |
| final long rowId = MmsUtils.parseRowIdFromMessageUri(mmsUri); |
| final String selection = String.format( |
| Locale.US, |
| "%s != '%s' AND %s = ?", |
| Mms.Part.CONTENT_TYPE, |
| ContentType.APP_SMIL, |
| Mms.Part.MSG_ID); |
| cursor = null; |
| try { |
| cursor = SqliteWrapper.query(context, resolver, |
| MMS_PART_CONTENT_URI, |
| DatabaseMessages.MmsPart.PROJECTION, |
| selection, |
| new String[] { Long.toString(rowId) }, |
| null/*sortOrder*/); |
| if (cursor != null) { |
| while (cursor.moveToNext()) { |
| mms.addPart(DatabaseMessages.MmsPart.get(cursor, true/*loadMedia*/)); |
| } |
| } |
| } catch (final SQLiteException e) { |
| LogUtil.e(TAG, "MmsLoader: query parts failure: " + e, e); |
| } finally { |
| if (cursor != null) { |
| cursor.close(); |
| } |
| } |
| return mms; |
| } |
| |
| /** |
| * Get the sender of an MMS message |
| * |
| * @param recipients The recipient list of the message |
| * @param mmsUri The pdu uri of the MMS |
| * @return The sender phone number of the MMS |
| */ |
| public static String getMmsSender(final List<String> recipients, final String mmsUri) { |
| final Context context = Factory.get().getApplicationContext(); |
| // We try to avoid the database query. |
| // If this is a 1v1 conv., then the other party is the sender |
| if (recipients != null && recipients.size() == 1) { |
| return recipients.get(0); |
| } |
| // Otherwise, we have to query the MMS addr table for sender address |
| // This should only be done for a received group mms message |
| final Cursor cursor = SqliteWrapper.query( |
| context, |
| context.getContentResolver(), |
| Uri.withAppendedPath(Uri.parse(mmsUri), "addr"), |
| new String[] { Mms.Addr.ADDRESS, Mms.Addr.CHARSET }, |
| Mms.Addr.TYPE + "=" + PduHeaders.FROM, |
| null/*selectionArgs*/, |
| null/*sortOrder*/); |
| if (cursor != null) { |
| try { |
| if (cursor.moveToFirst()) { |
| return DatabaseMessages.MmsAddr.get(cursor); |
| } |
| } finally { |
| cursor.close(); |
| } |
| } |
| return null; |
| } |
| |
| public static int bugleStatusForMms(final boolean isOutgoing, final boolean isNotification, |
| final int messageBox) { |
| int bugleStatus = MessageData.BUGLE_STATUS_UNKNOWN; |
| // For a message we sync either |
| if (isOutgoing) { |
| if (messageBox == Mms.MESSAGE_BOX_OUTBOX || messageBox == Mms.MESSAGE_BOX_FAILED) { |
| // Not sent counts as failed and available for manual resend |
| bugleStatus = MessageData.BUGLE_STATUS_OUTGOING_FAILED; |
| } else { |
| // Otherwise outgoing message is complete |
| bugleStatus = MessageData.BUGLE_STATUS_OUTGOING_COMPLETE; |
| } |
| } else if (isNotification) { |
| // Incoming MMS notifications we sync count as failed and available for manual download |
| bugleStatus = MessageData.BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD; |
| } else { |
| // Other incoming MMS messages are complete |
| bugleStatus = MessageData.BUGLE_STATUS_INCOMING_COMPLETE; |
| } |
| return bugleStatus; |
| } |
| |
| public static MessageData createMmsMessage(final DatabaseMessages.MmsMessage mms, |
| final String conversationId, final String participantId, final String selfId, |
| final int bugleStatus) { |
| Assert.notNull(mms); |
| final boolean isNotification = (mms.mMmsMessageType == |
| PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND); |
| final int rawMmsStatus = (bugleStatus < MessageData.BUGLE_STATUS_FIRST_INCOMING |
| ? mms.mRetrieveStatus : mms.mResponseStatus); |
| |
| final MessageData message = MessageData.createMmsMessage(mms.getUri(), |
| participantId, selfId, conversationId, isNotification, bugleStatus, |
| mms.mContentLocation, mms.mTransactionId, mms.mPriority, mms.mSubject, |
| mms.mSeen, mms.mRead, mms.getSize(), rawMmsStatus, |
| mms.mExpiryInMillis, mms.mSentTimestampInMillis, mms.mTimestampInMillis); |
| |
| for (final DatabaseMessages.MmsPart part : mms.mParts) { |
| final MessagePartData messagePart = MmsUtils.createMmsMessagePart(part); |
| // Import media and text parts (skip SMIL and others) |
| if (messagePart != null) { |
| message.addPart(messagePart); |
| } |
| } |
| |
| if (!message.getParts().iterator().hasNext()) { |
| message.addPart(MessagePartData.createEmptyMessagePart()); |
| } |
| |
| return message; |
| } |
| |
| public static MessagePartData createMmsMessagePart(final DatabaseMessages.MmsPart part) { |
| MessagePartData messagePart = null; |
| if (part.isText()) { |
| final int mmsTextLengthLimit = |
| BugleGservices.get().getInt(BugleGservicesKeys.MMS_TEXT_LIMIT, |
| BugleGservicesKeys.MMS_TEXT_LIMIT_DEFAULT); |
| String text = part.mText; |
| if (text != null && text.length() > mmsTextLengthLimit) { |
| // Limit the text to a reasonable value. We ran into a situation where a vcard |
| // with a photo was sent as plain text. The massive amount of text caused the |
| // app to hang, ANR, and eventually crash in native text code. |
| text = text.substring(0, mmsTextLengthLimit); |
| } |
| messagePart = MessagePartData.createTextMessagePart(text); |
| } else if (part.isMedia()) { |
| messagePart = MessagePartData.createMediaMessagePart(part.mContentType, |
| part.getDataUri(), MessagePartData.UNSPECIFIED_SIZE, |
| MessagePartData.UNSPECIFIED_SIZE); |
| } |
| return messagePart; |
| } |
| |
| public static class StatusPlusUri { |
| // The request status to be as the result of the operation |
| // e.g. MMS_REQUEST_MANUAL_RETRY |
| public final int status; |
| // The raw telephony status |
| public final int rawStatus; |
| // The raw telephony URI |
| public final Uri uri; |
| // The operation result code from system api invocation (sent by system) |
| // or mapped from internal exception (sent by app) |
| public final int resultCode; |
| |
| public StatusPlusUri(final int status, final int rawStatus, final Uri uri) { |
| this.status = status; |
| this.rawStatus = rawStatus; |
| this.uri = uri; |
| resultCode = MessageData.UNKNOWN_RESULT_CODE; |
| } |
| |
| public StatusPlusUri(final int status, final int rawStatus, final Uri uri, |
| final int resultCode) { |
| this.status = status; |
| this.rawStatus = rawStatus; |
| this.uri = uri; |
| this.resultCode = resultCode; |
| } |
| } |
| |
| public static class SendReqResp { |
| public SendReq mSendReq; |
| public SendConf mSendConf; |
| |
| public SendReqResp(final SendReq sendReq, final SendConf sendConf) { |
| mSendReq = sendReq; |
| mSendConf = sendConf; |
| } |
| } |
| |
| /** |
| * Returned when sending/downloading MMS via platform APIs. In that case, we have to wait to |
| * receive the pending intent to determine status. |
| */ |
| public static final StatusPlusUri STATUS_PENDING = new StatusPlusUri(-1, -1, null); |
| |
| public static StatusPlusUri downloadMmsMessage(final Context context, final Uri notificationUri, |
| final int subId, final String subPhoneNumber, final String transactionId, |
| final String contentLocation, final boolean autoDownload, |
| final long receivedTimestampInSeconds, final long expiry, Bundle extras) { |
| if (TextUtils.isEmpty(contentLocation)) { |
| LogUtil.e(TAG, "MmsUtils: Download from empty content location URL"); |
| return new StatusPlusUri( |
| MMS_REQUEST_NO_RETRY, MessageData.RAW_TELEPHONY_STATUS_UNDEFINED, null); |
| } |
| if (!isMmsDataAvailable(subId)) { |
| LogUtil.e(TAG, |
| "MmsUtils: failed to download message, no data available"); |
| return new StatusPlusUri(MMS_REQUEST_MANUAL_RETRY, |
| MessageData.RAW_TELEPHONY_STATUS_UNDEFINED, |
| null, |
| SmsManager.MMS_ERROR_NO_DATA_NETWORK); |
| } |
| int status = MMS_REQUEST_MANUAL_RETRY; |
| try { |
| RetrieveConf retrieveConf = null; |
| if (DebugUtils.isDebugEnabled() && |
| MediaScratchFileProvider |
| .isMediaScratchSpaceUri(Uri.parse(contentLocation))) { |
| if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { |
| LogUtil.d(TAG, "MmsUtils: Reading MMS from dump file: " + contentLocation); |
| } |
| final String fileName = Uri.parse(contentLocation).getPathSegments().get(1); |
| final byte[] data = DebugUtils.receiveFromDumpFile(fileName); |
| retrieveConf = receiveFromDumpFile(data); |
| } else { |
| if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { |
| LogUtil.d(TAG, "MmsUtils: Downloading MMS via MMS lib API; notification " |
| + "message: " + notificationUri); |
| } |
| if (OsUtil.isAtLeastL_MR1()) { |
| if (subId < 0) { |
| LogUtil.e(TAG, "MmsUtils: Incoming MMS came from unknown SIM"); |
| throw new MmsFailureException(MMS_REQUEST_NO_RETRY, |
| "Message from unknown SIM"); |
| } |
| } else { |
| Assert.isTrue(subId == ParticipantData.DEFAULT_SELF_SUB_ID); |
| } |
| if (extras == null) { |
| extras = new Bundle(); |
| } |
| extras.putParcelable(DownloadMmsAction.EXTRA_NOTIFICATION_URI, notificationUri); |
| extras.putInt(DownloadMmsAction.EXTRA_SUB_ID, subId); |
| extras.putString(DownloadMmsAction.EXTRA_SUB_PHONE_NUMBER, subPhoneNumber); |
| extras.putString(DownloadMmsAction.EXTRA_TRANSACTION_ID, transactionId); |
| extras.putString(DownloadMmsAction.EXTRA_CONTENT_LOCATION, contentLocation); |
| extras.putBoolean(DownloadMmsAction.EXTRA_AUTO_DOWNLOAD, autoDownload); |
| extras.putLong(DownloadMmsAction.EXTRA_RECEIVED_TIMESTAMP, |
| receivedTimestampInSeconds); |
| extras.putLong(DownloadMmsAction.EXTRA_EXPIRY, expiry); |
| |
| MmsSender.downloadMms(context, subId, contentLocation, extras); |
| return STATUS_PENDING; // Download happens asynchronously; no status to return |
| } |
| return insertDownloadedMessageAndSendResponse(context, notificationUri, subId, |
| subPhoneNumber, transactionId, contentLocation, autoDownload, |
| receivedTimestampInSeconds, expiry, retrieveConf); |
| |
| } catch (final MmsFailureException e) { |
| LogUtil.e(TAG, "MmsUtils: failed to download message " + notificationUri, e); |
| status = e.retryHint; |
| } catch (final InvalidHeaderValueException e) { |
| LogUtil.e(TAG, "MmsUtils: failed to download message " + notificationUri, e); |
| } |
| return new StatusPlusUri(status, PDU_HEADER_VALUE_UNDEFINED, null); |
| } |
| |
| public static StatusPlusUri insertDownloadedMessageAndSendResponse(final Context context, |
| final Uri notificationUri, final int subId, final String subPhoneNumber, |
| final String transactionId, final String contentLocation, |
| final boolean autoDownload, final long receivedTimestampInSeconds, |
| final long expiry, final RetrieveConf retrieveConf) { |
| final byte[] notificationTransactionId = stringToBytes(transactionId, "UTF-8"); |
| Uri messageUri = null; |
| final int status; |
| final int retrieveStatus = retrieveConf.getRetrieveStatus(); |
| |
| if (retrieveStatus == PduHeaders.RETRIEVE_STATUS_OK) { |
| status = MMS_REQUEST_SUCCEEDED; |
| } else if (retrieveStatus >= PduHeaders.RETRIEVE_STATUS_ERROR_TRANSIENT_FAILURE && |
| retrieveStatus < PduHeaders.RETRIEVE_STATUS_ERROR_PERMANENT_FAILURE) { |
| status = MMS_REQUEST_AUTO_RETRY; |
| } else { |
| // else not meant to retry download |
| status = MMS_REQUEST_NO_RETRY; |
| LogUtil.e(TAG, "MmsUtils: failed to retrieve message; retrieveStatus: " |
| + retrieveStatus); |
| } |
| final ContentValues values = new ContentValues(1); |
| values.put(Mms.RETRIEVE_STATUS, retrieveConf.getRetrieveStatus()); |
| SqliteWrapper.update(context, context.getContentResolver(), |
| notificationUri, values, null, null); |
| |
| if (status == MMS_REQUEST_SUCCEEDED) { |
| // Send response of the notification |
| if (autoDownload) { |
| sendNotifyResponseForMmsDownload( |
| context, |
| subId, |
| notificationTransactionId, |
| contentLocation, |
| PduHeaders.STATUS_RETRIEVED); |
| } else { |
| sendAcknowledgeForMmsDownload( |
| context, subId, retrieveConf.getTransactionId(), contentLocation); |
| } |
| |
| // Insert downloaded message into telephony |
| final Uri inboxUri = MmsUtils.insertReceivedMmsMessage(context, retrieveConf, subId, |
| subPhoneNumber, receivedTimestampInSeconds, expiry, transactionId); |
| messageUri = ContentUris.withAppendedId(Mms.CONTENT_URI, ContentUris.parseId(inboxUri)); |
| } |
| // Do nothing for MMS_REQUEST_AUTO_RETRY and MMS_REQUEST_NO_RETRY. |
| |
| return new StatusPlusUri(status, retrieveStatus, messageUri); |
| } |
| |
| /** |
| * Send response for MMS download - catches and ignores errors |
| */ |
| public static void sendNotifyResponseForMmsDownload(final Context context, final int subId, |
| final byte[] transactionId, final String contentLocation, final int status) { |
| try { |
| if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { |
| LogUtil.d(TAG, "MmsUtils: Sending M-NotifyResp.ind for received MMS, status: " |
| + String.format("0x%X", status)); |
| } |
| if (contentLocation == null) { |
| LogUtil.w(TAG, "MmsUtils: Can't send NotifyResp; contentLocation is null"); |
| return; |
| } |
| if (transactionId == null) { |
| LogUtil.w(TAG, "MmsUtils: Can't send NotifyResp; transaction id is null"); |
| return; |
| } |
| if (!isMmsDataAvailable(subId)) { |
| LogUtil.w(TAG, "MmsUtils: Can't send NotifyResp; no data available"); |
| return; |
| } |
| MmsSender.sendNotifyResponseForMmsDownload( |
| context, subId, transactionId, contentLocation, status); |
| } catch (final MmsFailureException e) { |
| LogUtil.e(TAG, "sendNotifyResponseForMmsDownload: failed to retrieve message " + e, e); |
| } catch (final InvalidHeaderValueException e) { |
| LogUtil.e(TAG, "sendNotifyResponseForMmsDownload: failed to retrieve message " + e, e); |
| } |
| } |
| |
| /** |
| * Send acknowledge for mms download - catched and ignores errors |
| */ |
| public static void sendAcknowledgeForMmsDownload(final Context context, final int subId, |
| final byte[] transactionId, final String contentLocation) { |
| try { |
| if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { |
| LogUtil.d(TAG, "MmsUtils: Sending M-Acknowledge.ind for received MMS"); |
| } |
| if (contentLocation == null) { |
| LogUtil.w(TAG, "MmsUtils: Can't send AckInd; contentLocation is null"); |
| return; |
| } |
| if (transactionId == null) { |
| LogUtil.w(TAG, "MmsUtils: Can't send AckInd; transaction id is null"); |
| return; |
| } |
| if (!isMmsDataAvailable(subId)) { |
| LogUtil.w(TAG, "MmsUtils: Can't send AckInd; no data available"); |
| return; |
| } |
| MmsSender.sendAcknowledgeForMmsDownload(context, subId, transactionId, contentLocation); |
| } catch (final MmsFailureException e) { |
| LogUtil.e(TAG, "sendAcknowledgeForMmsDownload: failed to retrieve message " + e, e); |
| } catch (final InvalidHeaderValueException e) { |
| LogUtil.e(TAG, "sendAcknowledgeForMmsDownload: failed to retrieve message " + e, e); |
| } |
| } |
| |
| /** |
| * Try parsing a PDU without knowing the carrier. This is useful for importing |
| * MMS or storing draft when carrier info is not available |
| * |
| * @param data The PDU data |
| * @return Parsed PDU, null if failed to parse |
| */ |
| private static GenericPdu parsePduForAnyCarrier(final byte[] data) { |
| GenericPdu pdu = null; |
| try { |
| pdu = (new PduParser(data, true/*parseContentDisposition*/)).parse(); |
| } catch (final RuntimeException e) { |
| LogUtil.d(TAG, "parsePduForAnyCarrier: Failed to parse PDU with content disposition", |
| e); |
| } |
| if (pdu == null) { |
| try { |
| pdu = (new PduParser(data, false/*parseContentDisposition*/)).parse(); |
| } catch (final RuntimeException e) { |
| LogUtil.d(TAG, |
| "parsePduForAnyCarrier: Failed to parse PDU without content disposition", |
| e); |
| } |
| } |
| return pdu; |
| } |
| |
| private static RetrieveConf receiveFromDumpFile(final byte[] data) throws MmsFailureException { |
| final GenericPdu pdu = parsePduForAnyCarrier(data); |
| if (pdu == null || !(pdu instanceof RetrieveConf)) { |
| LogUtil.e(TAG, "receiveFromDumpFile: Parsing retrieved PDU failure"); |
| throw new MmsFailureException(MMS_REQUEST_MANUAL_RETRY, "Failed reading dump file"); |
| } |
| return (RetrieveConf) pdu; |
| } |
| |
| private static boolean isMmsDataAvailable(final int subId) { |
| if (OsUtil.isAtLeastL_MR1()) { |
| // L_MR1 above may support sending mms via wifi |
| return true; |
| } |
| final PhoneUtils phoneUtils = PhoneUtils.get(subId); |
| return !phoneUtils.isAirplaneModeOn() && phoneUtils.isMobileDataEnabled(); |
| } |
| |
| private static boolean isSmsDataAvailable(final int subId) { |
| if (OsUtil.isAtLeastL_MR1()) { |
| // L_MR1 above may support sending sms via wifi |
| return true; |
| } |
| final PhoneUtils phoneUtils = PhoneUtils.get(subId); |
| return !phoneUtils.isAirplaneModeOn(); |
| } |
| |
| public static StatusPlusUri sendMmsMessage(final Context context, final int subId, |
| final Uri messageUri, final Bundle extras) { |
| int status = MMS_REQUEST_MANUAL_RETRY; |
| int rawStatus = MessageData.RAW_TELEPHONY_STATUS_UNDEFINED; |
| if (!isMmsDataAvailable(subId)) { |
| LogUtil.w(TAG, "MmsUtils: failed to send message, no data available"); |
| return new StatusPlusUri(MMS_REQUEST_MANUAL_RETRY, |
| MessageData.RAW_TELEPHONY_STATUS_UNDEFINED, |
| messageUri, |
| SmsManager.MMS_ERROR_NO_DATA_NETWORK); |
| } |
| final PduPersister persister = PduPersister.getPduPersister(context); |
| try { |
| final SendReq sendReq = (SendReq) persister.load(messageUri); |
| if (sendReq == null) { |
| LogUtil.w(TAG, "MmsUtils: Sending MMS was deleted; uri = " + messageUri); |
| return new StatusPlusUri(MMS_REQUEST_NO_RETRY, |
| MessageData.RAW_TELEPHONY_STATUS_UNDEFINED, messageUri); |
| } |
| if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { |
| LogUtil.d(TAG, String.format("MmsUtils: Sending MMS, message uri: %s", messageUri)); |
| } |
| extras.putInt(SendMessageAction.KEY_SUB_ID, subId); |
| MmsSender.sendMms(context, subId, messageUri, sendReq, extras); |
| return STATUS_PENDING; |
| } catch (final MmsFailureException e) { |
| status = e.retryHint; |
| rawStatus = e.rawStatus; |
| LogUtil.e(TAG, "MmsUtils: failed to send message " + e, e); |
| } catch (final InvalidHeaderValueException e) { |
| LogUtil.e(TAG, "MmsUtils: failed to send message " + e, e); |
| } catch (final IllegalArgumentException e) { |
| LogUtil.e(TAG, "MmsUtils: invalid message to send " + e, e); |
| } catch (final MmsException e) { |
| LogUtil.e(TAG, "MmsUtils: failed to send message " + e, e); |
| } |
| // If we get here, some exception occurred |
| return new StatusPlusUri(status, rawStatus, messageUri); |
| } |
| |
| public static StatusPlusUri updateSentMmsMessageStatus(final Context context, |
| final Uri messageUri, final SendConf sendConf) { |
| final int status; |
| final int respStatus = sendConf.getResponseStatus(); |
| |
| final ContentValues values = new ContentValues(2); |
| values.put(Mms.RESPONSE_STATUS, respStatus); |
| final byte[] messageId = sendConf.getMessageId(); |
| if (messageId != null && messageId.length > 0) { |
| values.put(Mms.MESSAGE_ID, PduPersister.toIsoString(messageId)); |
| } |
| SqliteWrapper.update(context, context.getContentResolver(), |
| messageUri, values, null, null); |
| if (respStatus == PduHeaders.RESPONSE_STATUS_OK) { |
| status = MMS_REQUEST_SUCCEEDED; |
| } else if (respStatus >= PduHeaders.RESPONSE_STATUS_ERROR_TRANSIENT_FAILURE |
| && respStatus < PduHeaders.RESPONSE_STATUS_ERROR_PERMANENT_FAILURE) { |
| // Only RESPONSE_STATUS_ERROR_TRANSIENT_FAILURE and RESPONSE_STATUS_ERROR_TRANSIENT |
| // _NETWORK_PROBLEM are used in the M-Send.conf. But for others transient failures |
| // including reserved values for future purposes, it should work same as transient |
| // failure always. (OMA-MMS-ENC-V1_2, 7.2.37. X-Mms-Response-Status field) |
| status = MMS_REQUEST_AUTO_RETRY; |
| } else { |
| // else permanent failure |
| status = MMS_REQUEST_MANUAL_RETRY; |
| LogUtil.e(TAG, "MmsUtils: failed to send message; respStatus = " |
| + String.format("0x%X", respStatus)); |
| } |
| return new StatusPlusUri(status, respStatus, messageUri); |
| } |
| |
| public static void clearMmsStatus(final Context context, final Uri uri) { |
| // Messaging application can leave invalid values in STATUS field of M-Notification.ind |
| // messages. Take this opportunity to clear it. |
| // Downloading status just kept in local db and not reflected into telephony. |
| final ContentValues values = new ContentValues(1); |
| values.putNull(Mms.STATUS); |
| SqliteWrapper.update(context, context.getContentResolver(), |
| uri, values, null, null); |
| } |
| |
| // Selection for dedup algorithm: |
| // ((m_type=NOTIFICATION_IND) OR (m_type=RETRIEVE_CONF)) AND (exp>NOW)) AND (t_id=xxxxxx) |
| // i.e. If it is NotificationInd or RetrieveConf and not expired |
| // AND transaction id is the input id |
| private static final String DUP_NOTIFICATION_QUERY_SELECTION = |
| "((" + Mms.MESSAGE_TYPE + "=?) OR (" + Mms.MESSAGE_TYPE + "=?)) AND (" |
| + Mms.EXPIRY + ">?) AND (" + Mms.TRANSACTION_ID + "=?)"; |
| |
| private static final int MAX_RETURN = 32; |
| private static String[] getDupNotifications(final Context context, final NotificationInd nInd) { |
| final byte[] rawTransactionId = nInd.getTransactionId(); |
| if (rawTransactionId != null) { |
| // dedup algorithm |
| String selection = DUP_NOTIFICATION_QUERY_SELECTION; |
| final long nowSecs = System.currentTimeMillis() / 1000; |
| String[] selectionArgs = new String[] { |
| Integer.toString(PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND), |
| Integer.toString(PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF), |
| Long.toString(nowSecs), |
| new String(rawTransactionId) |
| }; |
| |
| Cursor cursor = null; |
| try { |
| cursor = SqliteWrapper.query( |
| context, context.getContentResolver(), |
| Mms.CONTENT_URI, new String[] { Mms._ID }, |
| selection, selectionArgs, null); |
| final int dupCount = cursor.getCount(); |
| if (dupCount > 0) { |
| // We already received the same notification before. |
| // Don't want to return too many dups. It is only for debugging. |
| final int returnCount = dupCount < MAX_RETURN ? dupCount : MAX_RETURN; |
| final String[] dups = new String[returnCount]; |
| for (int i = 0; cursor.moveToNext() && i < returnCount; i++) { |
| dups[i] = cursor.getString(0); |
| } |
| return dups; |
| } |
| } catch (final SQLiteException e) { |
| LogUtil.e(TAG, "query failure: " + e, e); |
| } finally { |
| cursor.close(); |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Try parse the address using RFC822 format. If it fails to parse, then return the |
| * original address |
| * |
| * @param address The MMS ind sender address to parse |
| * @return The real address. If in RFC822 format, returns the correct email. |
| */ |
| private static String parsePotentialRfc822EmailAddress(final String address) { |
| if (address == null || !address.contains("@") || !address.contains("<")) { |
| return address; |
| } |
| final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(address); |
| if (tokens != null && tokens.length > 0) { |
| for (final Rfc822Token token : tokens) { |
| if (token != null && !TextUtils.isEmpty(token.getAddress())) { |
| return token.getAddress(); |
| } |
| } |
| } |
| return address; |
| } |
| |
| public static DatabaseMessages.MmsMessage processReceivedPdu(final Context context, |
| final byte[] pushData, final int subId, final String subPhoneNumber) { |
| // Parse data |
| |
| // Insert placeholder row to telephony and local db |
| // Get raw PDU push-data from the message and parse it |
| final PduParser parser = new PduParser(pushData, |
| MmsConfig.get(subId).getSupportMmsContentDisposition()); |
| final GenericPdu pdu = parser.parse(); |
| |
| if (null == pdu) { |
| LogUtil.e(TAG, "Invalid PUSH data"); |
| return null; |
| } |
| |
| final PduPersister p = PduPersister.getPduPersister(context); |
| final int type = pdu.getMessageType(); |
| |
| Uri messageUri = null; |
| switch (type) { |
| case PduHeaders.MESSAGE_TYPE_DELIVERY_IND: |
| case PduHeaders.MESSAGE_TYPE_READ_ORIG_IND: { |
| // TODO: Should this be commented out? |
| // threadId = findThreadId(context, pdu, type); |
| // if (threadId == -1) { |
| // // The associated SendReq isn't found, therefore skip |
| // // processing this PDU. |
| // break; |
| // } |
| |
| // Uri uri = p.persist(pdu, Inbox.CONTENT_URI, true, |
| // MessagingPreferenceActivity.getIsGroupMmsEnabled(mContext), null); |
| // // Update thread ID for ReadOrigInd & DeliveryInd. |
| // ContentValues values = new ContentValues(1); |
| // values.put(Mms.THREAD_ID, threadId); |
| // SqliteWrapper.update(mContext, cr, uri, values, null, null); |
| LogUtil.w(TAG, "Received unsupported WAP Push, type=" + type); |
| break; |
| } |
| case PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND: { |
| final NotificationInd nInd = (NotificationInd) pdu; |
| |
| if (MmsConfig.get(subId).getTransIdEnabled()) { |
| final byte [] contentLocationTemp = nInd.getContentLocation(); |
| if ('=' == contentLocationTemp[contentLocationTemp.length - 1]) { |
| final byte [] transactionIdTemp = nInd.getTransactionId(); |
| final byte [] contentLocationWithId = |
| new byte [contentLocationTemp.length |
| + transactionIdTemp.length]; |
| System.arraycopy(contentLocationTemp, 0, contentLocationWithId, |
| 0, contentLocationTemp.length); |
| System.arraycopy(transactionIdTemp, 0, contentLocationWithId, |
| contentLocationTemp.length, transactionIdTemp.length); |
| nInd.setContentLocation(contentLocationWithId); |
| } |
| } |
| final String[] dups = getDupNotifications(context, nInd); |
| if (dups == null) { |
| // TODO: Do we handle Rfc822 Email Addresses? |
| //final String contentLocation = |
| // MmsUtils.bytesToString(nInd.getContentLocation(), "UTF-8"); |
| //final byte[] transactionId = nInd.getTransactionId(); |
| //final long messageSize = nInd.getMessageSize(); |
| //final long expiry = nInd.getExpiry(); |
| //final String transactionIdString = |
| // MmsUtils.bytesToString(transactionId, "UTF-8"); |
| |
| //final EncodedStringValue fromEncoded = nInd.getFrom(); |
| // An mms ind received from email address will have from address shown as |
| // "John Doe <johndoe@foobar.com>" but the actual received message will only |
| // have the email address. So let's try to parse the RFC822 format to get the |
| // real email. Otherwise we will create two conversations for the MMS |
| // notification and the actual MMS message if auto retrieve is disabled. |
| //final String from = parsePotentialRfc822EmailAddress( |
| // fromEncoded != null ? fromEncoded.getString() : null); |
| |
| Uri inboxUri = null; |
| try { |
| inboxUri = p.persist(pdu, Mms.Inbox.CONTENT_URI, subId, subPhoneNumber, |
| null); |
| messageUri = ContentUris.withAppendedId(Mms.CONTENT_URI, |
| ContentUris.parseId(inboxUri)); |
| } catch (final MmsException e) { |
| LogUtil.e(TAG, "Failed to save the data from PUSH: type=" + type, e); |
| } |
| } else { |
| LogUtil.w(TAG, "Received WAP Push is a dup: " + Joiner.on(',').join(dups)); |
| if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { |
| LogUtil.w(TAG, "Dup Transaction Id=" + new String(nInd.getTransactionId())); |
| } |
| } |
| break; |
| } |
| default: |
| LogUtil.e(TAG, "Received unrecognized WAP Push, type=" + type); |
| } |
| |
| DatabaseMessages.MmsMessage mms = null; |
| if (messageUri != null) { |
| mms = MmsUtils.loadMms(messageUri); |
| } |
| return mms; |
| } |
| |
| public static Uri insertSendingMmsMessage(final Context context, final List<String> recipients, |
| final MessageData content, final int subId, final String subPhoneNumber, |
| final long timestamp) { |
| final SendReq sendReq = createMmsSendReq( |
| context, subId, recipients.toArray(new String[recipients.size()]), content, |
| DEFAULT_DELIVERY_REPORT_MODE, |
| DEFAULT_READ_REPORT_MODE, |
| DEFAULT_EXPIRY_TIME_IN_SECONDS, |
| DEFAULT_PRIORITY, |
| timestamp); |
| Uri messageUri = null; |
| if (sendReq != null) { |
| final Uri outboxUri = MmsUtils.insertSendReq(context, sendReq, subId, subPhoneNumber); |
| if (outboxUri != null) { |
| messageUri = ContentUris.withAppendedId(Telephony.Mms.CONTENT_URI, |
| ContentUris.parseId(outboxUri)); |
| if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { |
| LogUtil.d(TAG, "Mmsutils: Inserted sending MMS message into telephony, uri: " |
| + outboxUri); |
| } |
| } else { |
| LogUtil.e(TAG, "insertSendingMmsMessage: failed to persist message into telephony"); |
| } |
| } |
| return messageUri; |
| } |
| |
| public static MessageData readSendingMmsMessage(final Uri messageUri, |
| final String conversationId, final String participantId, final String selfId) { |
| MessageData message = null; |
| if (messageUri != null) { |
| final DatabaseMessages.MmsMessage mms = MmsUtils.loadMms(messageUri); |
| |
| // Make sure that the message has not been deleted from the Telephony DB |
| if (mms != null) { |
| // Transform the message |
| message = MmsUtils.createMmsMessage(mms, conversationId, participantId, selfId, |
| MessageData.BUGLE_STATUS_OUTGOING_RESENDING); |
| } |
| } |
| return message; |
| } |
| |
| /** |
| * Create an MMS message with subject, text and image |
| * |
| * @return Both the M-Send.req and the M-Send.conf for processing in the caller |
| * @throws MmsException |
| */ |
| private static SendReq createMmsSendReq(final Context context, final int subId, |
| final String[] recipients, final MessageData message, |
| final boolean requireDeliveryReport, final boolean requireReadReport, |
| final long expiryTime, final int priority, final long timestampMillis) { |
| Assert.notNull(context); |
| if (recipients == null || recipients.length < 1) { |
| throw new IllegalArgumentException("MMS sendReq no recipient"); |
| } |
| |
| // Make a copy so we don't propagate changes to recipients to outside of this method |
| final String[] recipientsCopy = new String[recipients.length]; |
| // Don't send phone number as is since some received phone number is malformed |
| // for sending. We need to strip the separators. |
| for (int i = 0; i < recipients.length; i++) { |
| final String recipient = recipients[i]; |
| if (EmailAddress.isValidEmail(recipients[i])) { |
| // Don't do stripping for emails |
| recipientsCopy[i] = recipient; |
| } else { |
| recipientsCopy[i] = stripPhoneNumberSeparators(recipient); |
| } |
| } |
| |
| SendReq sendReq = null; |
| try { |
| sendReq = createSendReq(context, subId, recipientsCopy, |
| message, requireDeliveryReport, |
| requireReadReport, expiryTime, priority, timestampMillis); |
| } catch (final InvalidHeaderValueException e) { |
| LogUtil.e(TAG, "InvalidHeaderValue creating sendReq PDU"); |
| } catch (final OutOfMemoryError e) { |
| LogUtil.e(TAG, "Out of memory error creating sendReq PDU"); |
| } |
| return sendReq; |
| } |
| |
| /** |
| * Stripping out the invalid characters in a phone number before sending |
| * MMS. We only keep alphanumeric and '*', '#', '+'. |
| */ |
| private static String stripPhoneNumberSeparators(final String phoneNumber) { |
| if (phoneNumber == null) { |
| return null; |
| } |
| final int len = phoneNumber.length(); |
| final StringBuilder ret = new StringBuilder(len); |
| for (int i = 0; i < len; i++) { |
| final char c = phoneNumber.charAt(i); |
| if (Character.isLetterOrDigit(c) || c == '+' || c == '*' || c == '#') { |
| ret.append(c); |
| } |
| } |
| return ret.toString(); |
| } |
| |
| /** |
| * Create M-Send.req for the MMS message to be sent. |
| * |
| * @return the M-Send.req |
| * @throws InvalidHeaderValueException if there is any error in parsing the input |
| */ |
| static SendReq createSendReq(final Context context, final int subId, |
| final String[] recipients, final MessageData message, |
| final boolean requireDeliveryReport, |
| final boolean requireReadReport, final long expiryTime, final int priority, |
| final long timestampMillis) |
| throws InvalidHeaderValueException { |
| final SendReq req = new SendReq(); |
| // From, per spec |
| final String lineNumber = PhoneUtils.get(subId).getCanonicalForSelf(true/*allowOverride*/); |
| if (!TextUtils.isEmpty(lineNumber)) { |
| req.setFrom(new EncodedStringValue(lineNumber)); |
| } |
| // To |
| final EncodedStringValue[] encodedNumbers = EncodedStringValue.encodeStrings(recipients); |
| if (encodedNumbers != null) { |
| req.setTo(encodedNumbers); |
| } |
| // Subject |
| if (!TextUtils.isEmpty(message.getMmsSubject())) { |
| req.setSubject(new EncodedStringValue(message.getMmsSubject())); |
| } |
| // Date |
| req.setDate(timestampMillis / 1000L); |
| // Body |
| final MmsInfo bodyInfo = MmsUtils.makePduBody(context, message, subId); |
| req.setBody(bodyInfo.mPduBody); |
| // Message size |
| req.setMessageSize(bodyInfo.mMessageSize); |
| // Message class |
| req.setMessageClass(PduHeaders.MESSAGE_CLASS_PERSONAL_STR.getBytes()); |
| // Expiry |
| req.setExpiry(expiryTime); |
| // Priority |
| req.setPriority(priority); |
| // Delivery report |
| req.setDeliveryReport(requireDeliveryReport ? PduHeaders.VALUE_YES : PduHeaders.VALUE_NO); |
| // Read report |
| req.setReadReport(requireReadReport ? PduHeaders.VALUE_YES : PduHeaders.VALUE_NO); |
| return req; |
| } |
| |
| public static boolean isDeliveryReportRequired(final int subId) { |
| if (!MmsConfig.get(subId).getSMSDeliveryReportsEnabled()) { |
| return false; |
| } |
| final Context context = Factory.get().getApplicationContext(); |
| final Resources res = context.getResources(); |
| final BuglePrefs prefs = BuglePrefs.getSubscriptionPrefs(subId); |
| final String deliveryReportKey = res.getString(R.string.delivery_reports_pref_key); |
| final boolean defaultValue = res.getBoolean(R.bool.delivery_reports_pref_default); |
| return prefs.getBoolean(deliveryReportKey, defaultValue); |
| } |
| |
| public static int sendSmsMessage(final String recipient, final String messageText, |
| final Uri requestUri, final int subId, |
| final String smsServiceCenter, final boolean requireDeliveryReport) { |
| if (!isSmsDataAvailable(subId)) { |
| LogUtil.w(TAG, "MmsUtils: can't send SMS without radio"); |
| return MMS_REQUEST_MANUAL_RETRY; |
| } |
| final Context context = Factory.get().getApplicationContext(); |
| int status = MMS_REQUEST_MANUAL_RETRY; |
| try { |
| // Send a single message |
| final SendResult result = SmsSender.sendMessage( |
| context, |
| subId, |
| recipient, |
| messageText, |
| smsServiceCenter, |
| requireDeliveryReport, |
| requestUri); |
| if (!result.hasPending()) { |
| // not timed out, check failures |
| final int failureLevel = result.getHighestFailureLevel(); |
| switch (failureLevel) { |
| case SendResult.FAILURE_LEVEL_NONE: |
| status = MMS_REQUEST_SUCCEEDED; |
| break; |
| case SendResult.FAILURE_LEVEL_TEMPORARY: |
| status = MMS_REQUEST_AUTO_RETRY; |
| LogUtil.e(TAG, "MmsUtils: SMS temporary failure"); |
| break; |
| case SendResult.FAILURE_LEVEL_PERMANENT: |
| LogUtil.e(TAG, "MmsUtils: SMS permanent failure"); |
| break; |
| } |
| } else { |
| // Timed out |
| LogUtil.e(TAG, "MmsUtils: sending SMS timed out"); |
| } |
| } catch (final Exception e) { |
| LogUtil.e(TAG, "MmsUtils: failed to send SMS " + e, e); |
| } |
| return status; |
| } |
| |
| /** |
| * Delete SMS and MMS messages in a particular thread |
| * |
| * @return the number of messages deleted |
| */ |
| public static int deleteThread(final long threadId, final long cutOffTimestampInMillis) { |
| final ContentResolver resolver = Factory.get().getApplicationContext().getContentResolver(); |
| final Uri threadUri = ContentUris.withAppendedId(Telephony.Threads.CONTENT_URI, threadId); |
| if (cutOffTimestampInMillis < Long.MAX_VALUE) { |
| return resolver.delete(threadUri, Sms.DATE + "<=?", |
| new String[] { Long.toString(cutOffTimestampInMillis) }); |
| } else { |
| return resolver.delete(threadUri, null /* smsSelection */, null /* selectionArgs */); |
| } |
| } |
| |
| /** |
| * Delete single SMS and MMS message |
| * |
| * @return number of rows deleted (should be 1 or 0) |
| */ |
| public static int deleteMessage(final Uri messageUri) { |
| final ContentResolver resolver = Factory.get().getApplicationContext().getContentResolver(); |
| return resolver.delete(messageUri, null /* selection */, null /* selectionArgs */); |
| } |
| |
| public static byte[] createDebugNotificationInd(final String fileName) { |
| byte[] pduData = null; |
| try { |
| final Context context = Factory.get().getApplicationContext(); |
| // Load the message file |
| final byte[] data = DebugUtils.receiveFromDumpFile(fileName); |
| final RetrieveConf retrieveConf = receiveFromDumpFile(data); |
| // Create the notification |
| final NotificationInd notification = new NotificationInd(); |
| final long expiry = System.currentTimeMillis() / 1000 + 600; |
| notification.setTransactionId(fileName.getBytes()); |
| notification.setMmsVersion(retrieveConf.getMmsVersion()); |
| notification.setFrom(retrieveConf.getFrom()); |
| notification.setSubject(retrieveConf.getSubject()); |
| notification.setExpiry(expiry); |
| notification.setMessageSize(data.length); |
| notification.setMessageClass(retrieveConf.getMessageClass()); |
| |
| final Uri.Builder builder = MediaScratchFileProvider.getUriBuilder(); |
| builder.appendPath(fileName); |
| final Uri contentLocation = builder.build(); |
| notification.setContentLocation(contentLocation.toString().getBytes()); |
| |
| // Serialize |
| pduData = new PduComposer(context, notification).make(); |
| if (pduData == null || pduData.length < 1) { |
| throw new IllegalArgumentException("Empty or zero length PDU data"); |
| } |
| } catch (final MmsFailureException e) { |
| // Nothing to do |
| } catch (final InvalidHeaderValueException e) { |
| // Nothing to do |
| } |
| return pduData; |
| } |
| |
| public static int mapRawStatusToErrorResourceId(final int bugleStatus, final int rawStatus) { |
| int stringResId = R.string.message_status_send_failed; |
| switch (rawStatus) { |
| case PduHeaders.RESPONSE_STATUS_ERROR_SERVICE_DENIED: |
| case PduHeaders.RESPONSE_STATUS_ERROR_PERMANENT_SERVICE_DENIED: |
| //case PduHeaders.RESPONSE_STATUS_ERROR_PERMANENT_REPLY_CHARGING_LIMITATIONS_NOT_MET: |
| //case PduHeaders.RESPONSE_STATUS_ERROR_PERMANENT_REPLY_CHARGING_REQUEST_NOT_ACCEPTED: |
| //case PduHeaders.RESPONSE_STATUS_ERROR_PERMANENT_REPLY_CHARGING_FORWARDING_DENIED: |
| //case PduHeaders.RESPONSE_STATUS_ERROR_PERMANENT_REPLY_CHARGING_NOT_SUPPORTED: |
| //case PduHeaders.RESPONSE_STATUS_ERROR_PERMANENT_ADDRESS_HIDING_NOT_SUPPORTED: |
| //case PduHeaders.RESPONSE_STATUS_ERROR_PERMANENT_LACK_OF_PREPAID: |
| stringResId = R.string.mms_failure_outgoing_service; |
| break; |
| case PduHeaders.RESPONSE_STATUS_ERROR_SENDING_ADDRESS_UNRESOLVED: |
| case PduHeaders.RESPONSE_STATUS_ERROR_TRANSIENT_SENDNG_ADDRESS_UNRESOLVED: |
| case PduHeaders.RESPONSE_STATUS_ERROR_PERMANENT_SENDING_ADDRESS_UNRESOLVED: |
| stringResId = R.string.mms_failure_outgoing_address; |
| break; |
| case PduHeaders.RESPONSE_STATUS_ERROR_MESSAGE_FORMAT_CORRUPT: |
| case PduHeaders.RESPONSE_STATUS_ERROR_PERMANENT_MESSAGE_FORMAT_CORRUPT: |
| stringResId = R.string.mms_failure_outgoing_corrupt; |
| break; |
| case PduHeaders.RESPONSE_STATUS_ERROR_CONTENT_NOT_ACCEPTED: |
| case PduHeaders.RESPONSE_STATUS_ERROR_PERMANENT_CONTENT_NOT_ACCEPTED: |
| stringResId = R.string.mms_failure_outgoing_content; |
| break; |
| case PduHeaders.RESPONSE_STATUS_ERROR_UNSUPPORTED_MESSAGE: |
| //case PduHeaders.RESPONSE_STATUS_ERROR_MESSAGE_NOT_FOUND: |
| //case PduHeaders.RESPONSE_STATUS_ERROR_TRANSIENT_MESSAGE_NOT_FOUND: |
| stringResId = R.string.mms_failure_outgoing_unsupported; |
| break; |
| case MessageData.RAW_TELEPHONY_STATUS_MESSAGE_TOO_BIG: |
| stringResId = R.string.mms_failure_outgoing_too_large; |
| break; |
| } |
| return stringResId; |
| } |
| |
| /** |
| * Dump the raw MMS data into a file |
| * |
| * @param rawPdu The raw pdu data |
| * @param pdu The parsed pdu, used to construct a dump file name |
| */ |
| public static void dumpPdu(final byte[] rawPdu, final GenericPdu pdu) { |
| if (rawPdu == null || rawPdu.length < 1) { |
| return; |
| } |
| final String dumpFileName = MmsUtils.MMS_DUMP_PREFIX + getDumpFileId(pdu); |
| final File dumpFile = DebugUtils.getDebugFile(dumpFileName, true); |
| if (dumpFile != null) { |
| try { |
| final FileOutputStream fos = new FileOutputStream(dumpFile); |
| final BufferedOutputStream bos = new BufferedOutputStream(fos); |
| try { |
| bos.write(rawPdu); |
| bos.flush(); |
| } finally { |
| bos.close(); |
| } |
| DebugUtils.ensureReadable(dumpFile); |
| } catch (final IOException e) { |
| LogUtil.e(TAG, "dumpPdu: " + e, e); |
| } |
| } |
| } |
| |
| /** |
| * Get the dump file id based on the parsed PDU |
| * 1. Use message id if not empty |
| * 2. Use transaction id if message id is empty |
| * 3. If all above is empty, use random UUID |
| * |
| * @param pdu the parsed PDU |
| * @return the id of the dump file |
| */ |
| private static String getDumpFileId(final GenericPdu pdu) { |
| String fileId = null; |
| if (pdu != null && pdu instanceof RetrieveConf) { |
| final RetrieveConf retrieveConf = (RetrieveConf) pdu; |
| if (retrieveConf.getMessageId() != null) { |
| fileId = new String(retrieveConf.getMessageId()); |
| } else if (retrieveConf.getTransactionId() != null) { |
| fileId = new String(retrieveConf.getTransactionId()); |
| } |
| } |
| if (TextUtils.isEmpty(fileId)) { |
| fileId = UUID.randomUUID().toString(); |
| } |
| return fileId; |
| } |
| } |