summaryrefslogtreecommitdiffstats
path: root/src/com/android/messaging/sms/MmsUtils.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/messaging/sms/MmsUtils.java')
-rw-r--r--src/com/android/messaging/sms/MmsUtils.java2747
1 files changed, 0 insertions, 2747 deletions
diff --git a/src/com/android/messaging/sms/MmsUtils.java b/src/com/android/messaging/sms/MmsUtils.java
deleted file mode 100644
index 913e9a6..0000000
--- a/src/com/android/messaging/sms/MmsUtils.java
+++ /dev/null
@@ -1,2747 +0,0 @@
-/*
- * 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.ConnectivityManager;
-import android.net.NetworkInfo;
-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();
- if (ContentType.isImageType(contentType)) {
- // 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.mp4", index);
- 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.amr", index);
- 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 String contentLocation) {
- 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(2);
- // Update mms table with local time instead of PDU time
- values.put(Mms.DATE, receivedTimestampInSeconds);
- // Also update the content location field from NotificationInd so that
- // wap push dedup would work even after the wap push is deleted
- values.put(Mms.CONTENT_LOCATION, contentLocation);
- 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 a semicolon separated list of phone numbers from a smsto: uri.
- public static String getSmsRecipients(final Uri uri) {
- String recipients = uri.getSchemeSpecificPart();
- final int pos = recipients.indexOf('?');
- if (pos != -1) {
- recipients = recipients.substring(0, pos);
- }
- recipients = replaceUnicodeDigits(recipients).replace(',', ';');
- return recipients;
- }
-
- // This function was lifted from Telephony.PhoneNumberUtils because it was @hide
- /**
- * Replace arabic/unicode digits with decimal digits.
- * @param number
- * the number to be normalized.
- * @return the replaced number.
- */
- private static String replaceUnicodeDigits(final String number) {
- final StringBuilder normalizedDigits = new StringBuilder(number.length());
- for (final char c : number.toCharArray()) {
- final int digit = Character.digit(c, 10);
- if (digit != -1) {
- normalizedDigits.append(digit);
- } else {
- normalizedDigits.append(c);
- }
- }
- return normalizedDigits.toString();
- }
-
- /**
- * @return Whether the data roaming is enabled
- */
- private static boolean isDataRoamingEnabled() {
- boolean dataRoamingEnabled = false;
- final ContentResolver cr = Factory.get().getApplicationContext().getContentResolver();
- if (OsUtil.isAtLeastJB_MR1()) {
- dataRoamingEnabled = (Settings.Global.getInt(cr, Settings.Global.DATA_ROAMING, 0) != 0);
- } else {
- dataRoamingEnabled = (Settings.System.getInt(cr, Settings.System.DATA_ROAMING, 0) != 0);
- }
- return dataRoamingEnabled;
- }
-
- /**
- * @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");
- return 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, 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);
-
- 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, 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 RetrieveConf retrieveConf) {
- final byte[] transactionIdBytes = stringToBytes(transactionId, "UTF-8");
- Uri messageUri = null;
- int status = MMS_REQUEST_MANUAL_RETRY;
- int retrieveStatus = PDU_HEADER_VALUE_UNDEFINED;
-
- 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, transactionIdBytes,
- contentLocation, PduHeaders.STATUS_RETRIEVED);
- } else {
- sendAcknowledgeForMmsDownload(context, subId, transactionIdBytes, contentLocation);
- }
-
- // Insert downloaded message into telephony
- final Uri inboxUri = MmsUtils.insertReceivedMmsMessage(context, retrieveConf, subId,
- subPhoneNumber, receivedTimestampInSeconds, contentLocation);
- messageUri = ContentUris.withAppendedId(Mms.CONTENT_URI, ContentUris.parseId(inboxUri));
- } else if (status == MMS_REQUEST_AUTO_RETRY) {
- // For a retry do nothing
- } else if (status == MMS_REQUEST_MANUAL_RETRY && autoDownload) {
- // Failure from autodownload - just treat like manual download
- sendNotifyResponseForMmsDownload(context, subId, transactionIdBytes,
- contentLocation, PduHeaders.STATUS_DEFERRED);
- }
- 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 boolean isMobileDataEnabled(final int subId) {
- final PhoneUtils phoneUtils = PhoneUtils.get(subId);
- return phoneUtils.isMobileDataEnabled();
- }
-
- public static boolean isAirplaneModeOn(final int subId) {
- 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) {
- int status = MMS_REQUEST_MANUAL_RETRY;
- 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_TRANSIENT_NETWORK_PROBLEM ||
- respStatus == PduHeaders.RESPONSE_STATUS_ERROR_TRANSIENT_PARTIAL_SUCCESS) {
- status = MMS_REQUEST_AUTO_RETRY;
- } else {
- // else permanent failure
- 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 new dedup algorithm:
- // ((m_type<>130) OR (exp>NOW)) AND (date>NOW-7d) AND (date<NOW+7d) AND (ct_l=xxxxxx)
- // i.e. If it is NotificationInd and not expired or not NotificationInd
- // AND message is received with +/- 7 days from now
- // AND content location is the input URL
- private static final String DUP_NOTIFICATION_QUERY_SELECTION =
- "((" + Mms.MESSAGE_TYPE + "<>?) OR (" + Mms.EXPIRY + ">?)) AND ("
- + Mms.DATE + ">?) AND (" + Mms.DATE + "<?) AND (" + Mms.CONTENT_LOCATION +
- "=?)";
- // Selection for old behavior: only checks NotificationInd and its content location
- private static final String DUP_NOTIFICATION_QUERY_SELECTION_OLD =
- "(" + Mms.MESSAGE_TYPE + "=?) AND (" + Mms.CONTENT_LOCATION + "=?)";
-
- private static final int MAX_RETURN = 32;
- private static String[] getDupNotifications(final Context context, final NotificationInd nInd) {
- final byte[] rawLocation = nInd.getContentLocation();
- if (rawLocation != null) {
- final String location = new String(rawLocation);
- // We can not be sure if the content location of an MMS is globally and historically
- // unique. So we limit the dedup time within the last 7 days
- // (or configured by gservices remotely). If the same content location shows up after
- // that, we will download regardless. Duplicated message is better than no message.
- String selection;
- String[] selectionArgs;
- final long timeLimit = BugleGservices.get().getLong(
- BugleGservicesKeys.MMS_WAP_PUSH_DEDUP_TIME_LIMIT_SECS,
- BugleGservicesKeys.MMS_WAP_PUSH_DEDUP_TIME_LIMIT_SECS_DEFAULT);
- if (timeLimit > 0) {
- // New dedup algorithm
- selection = DUP_NOTIFICATION_QUERY_SELECTION;
- final long nowSecs = System.currentTimeMillis() / 1000;
- final long timeLowerBoundSecs = nowSecs - timeLimit;
- // Need upper bound to protect against clock change so that a message has a time
- // stamp in the future
- final long timeUpperBoundSecs = nowSecs + timeLimit;
- selectionArgs = new String[] {
- Integer.toString(PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND),
- Long.toString(nowSecs),
- Long.toString(timeLowerBoundSecs),
- Long.toString(timeUpperBoundSecs),
- location
- };
- } else {
- // If time limit is 0, we revert back to old behavior in case the new
- // dedup algorithm behaves badly
- selection = DUP_NOTIFICATION_QUERY_SELECTION_OLD;
- selectionArgs = new String[] {
- Integer.toString(PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND),
- location
- };
- }
- 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 WAP Push url=" + new String(nInd.getContentLocation()));
- }
- }
- 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;
- }
-
- /**
- * The absence of a connection type.
- */
- public static final int TYPE_NONE = -1;
-
- public static int getConnectivityEventNetworkType(final Context context, final Intent intent) {
- final ConnectivityManager connMgr = (ConnectivityManager)
- context.getSystemService(Context.CONNECTIVITY_SERVICE);
- if (OsUtil.isAtLeastJB_MR1()) {
- return intent.getIntExtra(ConnectivityManager.EXTRA_NETWORK_TYPE, TYPE_NONE);
- } else {
- final NetworkInfo info = (NetworkInfo) intent.getParcelableExtra(
- ConnectivityManager.EXTRA_NETWORK_INFO);
- if (info != null) {
- return info.getType();
- }
- }
- return TYPE_NONE;
- }
-
- /**
- * 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;
- }
-}