Home | History | Annotate | Download | only in sms
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.messaging.sms;
     18 
     19 import android.content.ContentResolver;
     20 import android.content.ContentUris;
     21 import android.content.ContentValues;
     22 import android.content.Context;
     23 import android.content.Intent;
     24 import android.content.res.AssetFileDescriptor;
     25 import android.content.res.Resources;
     26 import android.database.Cursor;
     27 import android.database.sqlite.SQLiteDatabase;
     28 import android.database.sqlite.SQLiteException;
     29 import android.media.MediaMetadataRetriever;
     30 import android.net.ConnectivityManager;
     31 import android.net.NetworkInfo;
     32 import android.net.Uri;
     33 import android.os.Bundle;
     34 import android.provider.Settings;
     35 import android.provider.Telephony;
     36 import android.provider.Telephony.Mms;
     37 import android.provider.Telephony.Sms;
     38 import android.provider.Telephony.Threads;
     39 import android.telephony.SmsManager;
     40 import android.telephony.SmsMessage;
     41 import android.text.TextUtils;
     42 import android.text.util.Rfc822Token;
     43 import android.text.util.Rfc822Tokenizer;
     44 
     45 import com.android.messaging.Factory;
     46 import com.android.messaging.R;
     47 import com.android.messaging.datamodel.MediaScratchFileProvider;
     48 import com.android.messaging.datamodel.action.DownloadMmsAction;
     49 import com.android.messaging.datamodel.action.SendMessageAction;
     50 import com.android.messaging.datamodel.data.MessageData;
     51 import com.android.messaging.datamodel.data.MessagePartData;
     52 import com.android.messaging.datamodel.data.ParticipantData;
     53 import com.android.messaging.mmslib.InvalidHeaderValueException;
     54 import com.android.messaging.mmslib.MmsException;
     55 import com.android.messaging.mmslib.SqliteWrapper;
     56 import com.android.messaging.mmslib.pdu.CharacterSets;
     57 import com.android.messaging.mmslib.pdu.EncodedStringValue;
     58 import com.android.messaging.mmslib.pdu.GenericPdu;
     59 import com.android.messaging.mmslib.pdu.NotificationInd;
     60 import com.android.messaging.mmslib.pdu.PduBody;
     61 import com.android.messaging.mmslib.pdu.PduComposer;
     62 import com.android.messaging.mmslib.pdu.PduHeaders;
     63 import com.android.messaging.mmslib.pdu.PduParser;
     64 import com.android.messaging.mmslib.pdu.PduPart;
     65 import com.android.messaging.mmslib.pdu.PduPersister;
     66 import com.android.messaging.mmslib.pdu.RetrieveConf;
     67 import com.android.messaging.mmslib.pdu.SendConf;
     68 import com.android.messaging.mmslib.pdu.SendReq;
     69 import com.android.messaging.sms.SmsSender.SendResult;
     70 import com.android.messaging.util.Assert;
     71 import com.android.messaging.util.BugleGservices;
     72 import com.android.messaging.util.BugleGservicesKeys;
     73 import com.android.messaging.util.BuglePrefs;
     74 import com.android.messaging.util.ContentType;
     75 import com.android.messaging.util.DebugUtils;
     76 import com.android.messaging.util.EmailAddress;
     77 import com.android.messaging.util.ImageUtils;
     78 import com.android.messaging.util.ImageUtils.ImageResizer;
     79 import com.android.messaging.util.LogUtil;
     80 import com.android.messaging.util.MediaMetadataRetrieverWrapper;
     81 import com.android.messaging.util.OsUtil;
     82 import com.android.messaging.util.PhoneUtils;
     83 import com.google.common.base.Joiner;
     84 
     85 import java.io.BufferedOutputStream;
     86 import java.io.File;
     87 import java.io.FileNotFoundException;
     88 import java.io.FileOutputStream;
     89 import java.io.IOException;
     90 import java.io.InputStream;
     91 import java.io.UnsupportedEncodingException;
     92 import java.util.ArrayList;
     93 import java.util.Calendar;
     94 import java.util.GregorianCalendar;
     95 import java.util.HashSet;
     96 import java.util.List;
     97 import java.util.Locale;
     98 import java.util.Set;
     99 import java.util.UUID;
    100 
    101 /**
    102  * Utils for sending sms/mms messages.
    103  */
    104 public class MmsUtils {
    105     private static final String TAG = LogUtil.BUGLE_TAG;
    106 
    107     public static final boolean DEFAULT_DELIVERY_REPORT_MODE  = false;
    108     public static final boolean DEFAULT_READ_REPORT_MODE = false;
    109     public static final long DEFAULT_EXPIRY_TIME_IN_SECONDS = 7 * 24 * 60 * 60;
    110     public static final int DEFAULT_PRIORITY = PduHeaders.PRIORITY_NORMAL;
    111 
    112     public static final int MAX_SMS_RETRY = 3;
    113 
    114     /**
    115      * MMS request succeeded
    116      */
    117     public static final int MMS_REQUEST_SUCCEEDED = 0;
    118     /**
    119      * MMS request failed with a transient error and can be retried automatically
    120      */
    121     public static final int MMS_REQUEST_AUTO_RETRY = 1;
    122     /**
    123      * MMS request failed with an error and can be retried manually
    124      */
    125     public static final int MMS_REQUEST_MANUAL_RETRY = 2;
    126     /**
    127      * MMS request failed with a specific error and should not be retried
    128      */
    129     public static final int MMS_REQUEST_NO_RETRY = 3;
    130 
    131     public static final String getRequestStatusDescription(final int status) {
    132         switch (status) {
    133             case MMS_REQUEST_SUCCEEDED:
    134                 return "SUCCEEDED";
    135             case MMS_REQUEST_AUTO_RETRY:
    136                 return "AUTO_RETRY";
    137             case MMS_REQUEST_MANUAL_RETRY:
    138                 return "MANUAL_RETRY";
    139             case MMS_REQUEST_NO_RETRY:
    140                 return "NO_RETRY";
    141             default:
    142                 return String.valueOf(status) + " (check MmsUtils)";
    143         }
    144     }
    145 
    146     public static final int PDU_HEADER_VALUE_UNDEFINED = 0;
    147 
    148     private static final int DEFAULT_DURATION = 5000; //ms
    149 
    150     // amount of space to leave in a MMS for text and overhead.
    151     private static final int MMS_MAX_SIZE_SLOP = 1024;
    152     public static final long INVALID_TIMESTAMP = 0L;
    153     private static String[] sNoSubjectStrings;
    154 
    155     public static class MmsInfo {
    156         public Uri mUri;
    157         public int mMessageSize;
    158         public PduBody mPduBody;
    159     }
    160 
    161     // Sync all remote messages apart from drafts
    162     private static final String REMOTE_SMS_SELECTION = String.format(
    163             Locale.US,
    164             "(%s IN (%d, %d, %d, %d, %d))",
    165             Sms.TYPE,
    166             Sms.MESSAGE_TYPE_INBOX,
    167             Sms.MESSAGE_TYPE_OUTBOX,
    168             Sms.MESSAGE_TYPE_QUEUED,
    169             Sms.MESSAGE_TYPE_FAILED,
    170             Sms.MESSAGE_TYPE_SENT);
    171 
    172     private static final String REMOTE_MMS_SELECTION = String.format(
    173             Locale.US,
    174             "((%s IN (%d, %d, %d, %d)) AND (%s IN (%d, %d, %d)))",
    175             Mms.MESSAGE_BOX,
    176             Mms.MESSAGE_BOX_INBOX,
    177             Mms.MESSAGE_BOX_OUTBOX,
    178             Mms.MESSAGE_BOX_SENT,
    179             Mms.MESSAGE_BOX_FAILED,
    180             Mms.MESSAGE_TYPE,
    181             PduHeaders.MESSAGE_TYPE_SEND_REQ,
    182             PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND,
    183             PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF);
    184 
    185     /**
    186      * Type selection for importing sms messages.
    187      *
    188      * @return The SQL selection for importing sms messages
    189      */
    190     public static String getSmsTypeSelectionSql() {
    191         return REMOTE_SMS_SELECTION;
    192     }
    193 
    194     /**
    195      * Type selection for importing mms messages.
    196      *
    197      * @return The SQL selection for importing mms messages. This selects the message type,
    198      * not including the selection on timestamp.
    199      */
    200     public static String getMmsTypeSelectionSql() {
    201         return REMOTE_MMS_SELECTION;
    202     }
    203 
    204     // SMIL spec: http://www.w3.org/TR/SMIL3
    205 
    206     private static final String sSmilImagePart =
    207             "<par dur=\"" + DEFAULT_DURATION + "ms\">" +
    208                 "<img src=\"%s\" region=\"Image\" />" +
    209             "</par>";
    210 
    211     private static final String sSmilVideoPart =
    212             "<par dur=\"%2$dms\">" +
    213                 "<video src=\"%1$s\" dur=\"%2$dms\" region=\"Image\" />" +
    214             "</par>";
    215 
    216     private static final String sSmilAudioPart =
    217             "<par dur=\"%2$dms\">" +
    218                     "<audio src=\"%1$s\" dur=\"%2$dms\" />" +
    219             "</par>";
    220 
    221     private static final String sSmilTextPart =
    222             "<par dur=\"" + DEFAULT_DURATION + "ms\">" +
    223                 "<text src=\"%s\" region=\"Text\" />" +
    224             "</par>";
    225 
    226     private static final String sSmilPart =
    227             "<par dur=\"" + DEFAULT_DURATION + "ms\">" +
    228                 "<ref src=\"%s\" />" +
    229             "</par>";
    230 
    231     private static final String sSmilTextOnly =
    232             "<smil>" +
    233                 "<head>" +
    234                     "<layout>" +
    235                         "<root-layout/>" +
    236                         "<region id=\"Text\" top=\"0\" left=\"0\" "
    237                           + "height=\"100%%\" width=\"100%%\"/>" +
    238                     "</layout>" +
    239                 "</head>" +
    240                 "<body>" +
    241                        "%s" +  // constructed body goes here
    242                 "</body>" +
    243             "</smil>";
    244 
    245     private static final String sSmilVisualAttachmentsOnly =
    246             "<smil>" +
    247                 "<head>" +
    248                     "<layout>" +
    249                         "<root-layout/>" +
    250                         "<region id=\"Image\" fit=\"meet\" top=\"0\" left=\"0\" "
    251                           + "height=\"100%%\" width=\"100%%\"/>" +
    252                     "</layout>" +
    253                 "</head>" +
    254                 "<body>" +
    255                        "%s" +  // constructed body goes here
    256                 "</body>" +
    257             "</smil>";
    258 
    259     private static final String sSmilVisualAttachmentsWithText =
    260             "<smil>" +
    261                 "<head>" +
    262                     "<layout>" +
    263                         "<root-layout/>" +
    264                         "<region id=\"Image\" fit=\"meet\" top=\"0\" left=\"0\" "
    265                           + "height=\"80%%\" width=\"100%%\"/>" +
    266                         "<region id=\"Text\" top=\"80%%\" left=\"0\" height=\"20%%\" "
    267                           + "width=\"100%%\"/>" +
    268                     "</layout>" +
    269                 "</head>" +
    270                 "<body>" +
    271                        "%s" +  // constructed body goes here
    272                 "</body>" +
    273             "</smil>";
    274 
    275     private static final String sSmilNonVisualAttachmentsOnly =
    276             "<smil>" +
    277                 "<head>" +
    278                     "<layout>" +
    279                         "<root-layout/>" +
    280                     "</layout>" +
    281                 "</head>" +
    282                 "<body>" +
    283                        "%s" +  // constructed body goes here
    284                 "</body>" +
    285             "</smil>";
    286 
    287     private static final String sSmilNonVisualAttachmentsWithText = sSmilTextOnly;
    288 
    289     public static final String MMS_DUMP_PREFIX = "mmsdump-";
    290     public static final String SMS_DUMP_PREFIX = "smsdump-";
    291 
    292     public static final int MIN_VIDEO_BYTES_PER_SECOND = 4 * 1024;
    293     public static final int MIN_IMAGE_BYTE_SIZE = 16 * 1024;
    294     public static final int MAX_VIDEO_ATTACHMENT_COUNT = 1;
    295 
    296     public static MmsInfo makePduBody(final Context context, final MessageData message,
    297             final int subId) {
    298         final PduBody pb = new PduBody();
    299 
    300         // Compute data size requirements for this message: count up images and total size of
    301         // non-image attachments.
    302         int totalLength = 0;
    303         int countImage = 0;
    304         for (final MessagePartData part : message.getParts()) {
    305             if (part.isAttachment()) {
    306                 final String contentType = part.getContentType();
    307                 if (ContentType.isImageType(contentType)) {
    308                     countImage++;
    309                 } else if (ContentType.isVCardType(contentType)) {
    310                     totalLength += getDataLength(context, part.getContentUri());
    311                 } else {
    312                     totalLength += getMediaFileSize(part.getContentUri());
    313                 }
    314             }
    315         }
    316         final long minSize = countImage * MIN_IMAGE_BYTE_SIZE;
    317         final int byteBudget = MmsConfig.get(subId).getMaxMessageSize() - totalLength
    318                 - MMS_MAX_SIZE_SLOP;
    319         final double budgetFactor =
    320                 minSize > 0 ? Math.max(1.0, byteBudget / ((double) minSize)) : 1;
    321         final int bytesPerImage = (int) (budgetFactor * MIN_IMAGE_BYTE_SIZE);
    322         final int widthLimit = MmsConfig.get(subId).getMaxImageWidth();
    323         final int heightLimit = MmsConfig.get(subId).getMaxImageHeight();
    324 
    325         // Actually add the attachments, shrinking images appropriately.
    326         int index = 0;
    327         totalLength = 0;
    328         boolean hasVisualAttachment = false;
    329         boolean hasNonVisualAttachment = false;
    330         boolean hasText = false;
    331         final StringBuilder smilBody = new StringBuilder();
    332         for (final MessagePartData part : message.getParts()) {
    333             String srcName;
    334             if (part.isAttachment()) {
    335                 String contentType = part.getContentType();
    336                 if (ContentType.isImageType(contentType)) {
    337                     // There's a good chance that if we selected the image from our media picker the
    338                     // content type is image/*. Fix the content type here for gifs so that we only
    339                     // need to open the input stream once. All other gif vs static image checks will
    340                     // only have to do a string comparison which is much cheaper.
    341                     final boolean isGif = ImageUtils.isGif(contentType, part.getContentUri());
    342                     contentType = isGif ? ContentType.IMAGE_GIF : contentType;
    343                     srcName = String.format(isGif ? "image%06d.gif" : "image%06d.jpg", index);
    344                     smilBody.append(String.format(sSmilImagePart, srcName));
    345                     totalLength += addPicturePart(context, pb, index, part,
    346                             widthLimit, heightLimit, bytesPerImage, srcName, contentType);
    347                     hasVisualAttachment = true;
    348                 } else if (ContentType.isVideoType(contentType)) {
    349                     srcName = String.format("video%06d.mp4", index);
    350                     final int length = addVideoPart(context, pb, part, srcName);
    351                     totalLength += length;
    352                     smilBody.append(String.format(sSmilVideoPart, srcName,
    353                             getMediaDurationMs(context, part, DEFAULT_DURATION)));
    354                     hasVisualAttachment = true;
    355                 } else if (ContentType.isVCardType(contentType)) {
    356                     srcName = String.format("contact%06d.vcf", index);
    357                     totalLength += addVCardPart(context, pb, part, srcName);
    358                     smilBody.append(String.format(sSmilPart, srcName));
    359                     hasNonVisualAttachment = true;
    360                 } else if (ContentType.isAudioType(contentType)) {
    361                     srcName = String.format("recording%06d.amr", index);
    362                     totalLength += addOtherPart(context, pb, part, srcName);
    363                     final int duration = getMediaDurationMs(context, part, -1);
    364                     Assert.isTrue(duration != -1);
    365                     smilBody.append(String.format(sSmilAudioPart, srcName, duration));
    366                     hasNonVisualAttachment = true;
    367                 } else {
    368                     srcName = String.format("other%06d.dat", index);
    369                     totalLength += addOtherPart(context, pb, part, srcName);
    370                     smilBody.append(String.format(sSmilPart, srcName));
    371                 }
    372                 index++;
    373             }
    374             if (!TextUtils.isEmpty(part.getText())) {
    375                 hasText = true;
    376             }
    377         }
    378 
    379         if (hasText) {
    380             final String srcName = String.format("text.%06d.txt", index);
    381             final String text = message.getMessageText();
    382             totalLength += addTextPart(context, pb, text, srcName);
    383 
    384             // Append appropriate SMIL to the body.
    385             smilBody.append(String.format(sSmilTextPart, srcName));
    386         }
    387 
    388         final String smilTemplate = getSmilTemplate(hasVisualAttachment,
    389                 hasNonVisualAttachment, hasText);
    390         addSmilPart(pb, smilTemplate, smilBody.toString());
    391 
    392         final MmsInfo mmsInfo = new MmsInfo();
    393         mmsInfo.mPduBody = pb;
    394         mmsInfo.mMessageSize = totalLength;
    395 
    396         return mmsInfo;
    397     }
    398 
    399     private static int getMediaDurationMs(final Context context, final MessagePartData part,
    400             final int defaultDurationMs) {
    401         Assert.notNull(context);
    402         Assert.notNull(part);
    403         Assert.isTrue(ContentType.isAudioType(part.getContentType()) ||
    404                 ContentType.isVideoType(part.getContentType()));
    405 
    406         final MediaMetadataRetrieverWrapper retriever = new MediaMetadataRetrieverWrapper();
    407         try {
    408             retriever.setDataSource(part.getContentUri());
    409             return retriever.extractInteger(
    410                     MediaMetadataRetriever.METADATA_KEY_DURATION, defaultDurationMs);
    411         } catch (final IOException e) {
    412             LogUtil.i(LogUtil.BUGLE_TAG, "Error extracting duration from " + part.getContentUri(), e);
    413             return defaultDurationMs;
    414         } finally {
    415             retriever.release();
    416         }
    417     }
    418 
    419     private static void setPartContentLocationAndId(final PduPart part, final String srcName) {
    420         // Set Content-Location.
    421         part.setContentLocation(srcName.getBytes());
    422 
    423         // Set Content-Id.
    424         final int index = srcName.lastIndexOf(".");
    425         final String contentId = (index == -1) ? srcName : srcName.substring(0, index);
    426         part.setContentId(contentId.getBytes());
    427     }
    428 
    429     private static int addTextPart(final Context context, final PduBody pb,
    430             final String text, final String srcName) {
    431         final PduPart part = new PduPart();
    432 
    433         // Set Charset if it's a text media.
    434         part.setCharset(CharacterSets.UTF_8);
    435 
    436         // Set Content-Type.
    437         part.setContentType(ContentType.TEXT_PLAIN.getBytes());
    438 
    439         // Set Content-Location.
    440         setPartContentLocationAndId(part, srcName);
    441 
    442         part.setData(text.getBytes());
    443 
    444         pb.addPart(part);
    445 
    446         return part.getData().length;
    447     }
    448 
    449     private static int addPicturePart(final Context context, final PduBody pb, final int index,
    450             final MessagePartData messagePart, int widthLimit, int heightLimit,
    451             final int maxPartSize, final String srcName, final String contentType) {
    452         final Uri imageUri = messagePart.getContentUri();
    453         final int width = messagePart.getWidth();
    454         final int height = messagePart.getHeight();
    455 
    456         // Swap the width and height limits to match the orientation of the image so we scale the
    457         // picture as little as possible.
    458         if ((height > width) != (heightLimit > widthLimit)) {
    459             final int temp = widthLimit;
    460             widthLimit = heightLimit;
    461             heightLimit = temp;
    462         }
    463 
    464         final int orientation = ImageUtils.getOrientation(context, imageUri);
    465         int imageSize = getDataLength(context, imageUri);
    466         if (imageSize <= 0) {
    467             LogUtil.e(TAG, "Can't get image", new Exception());
    468             return 0;
    469         }
    470 
    471         if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
    472             LogUtil.v(TAG, "addPicturePart size: " + imageSize + " width: "
    473                     + width + " widthLimit: " + widthLimit
    474                     + " height: " + height
    475                     + " heightLimit: " + heightLimit);
    476         }
    477 
    478         PduPart part;
    479         // Check if we're already within the limits - in which case we don't need to resize.
    480         // The size can be zero here, even when the media has content. See the comment in
    481         // MediaModel.initMediaSize. Sometimes it'll compute zero and it's costly to read the
    482         // whole stream to compute the size. When we call getResizedImageAsPart(), we'll correctly
    483         // set the size.
    484         if (imageSize <= maxPartSize &&
    485                 width <= widthLimit &&
    486                 height <= heightLimit &&
    487                 (orientation == android.media.ExifInterface.ORIENTATION_UNDEFINED ||
    488                 orientation == android.media.ExifInterface.ORIENTATION_NORMAL)) {
    489             if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
    490                 LogUtil.v(TAG, "addPicturePart - already sized");
    491             }
    492             part = new PduPart();
    493             part.setDataUri(imageUri);
    494             part.setContentType(contentType.getBytes());
    495         } else {
    496             part = getResizedImageAsPart(widthLimit, heightLimit, maxPartSize,
    497                     width, height, orientation, imageUri, context, contentType);
    498             if (part == null) {
    499                 final OutOfMemoryError e = new OutOfMemoryError();
    500                 LogUtil.e(TAG, "Can't resize image: not enough memory?", e);
    501                 throw e;
    502             }
    503             imageSize = part.getData().length;
    504         }
    505 
    506         setPartContentLocationAndId(part, srcName);
    507 
    508         pb.addPart(index, part);
    509 
    510         if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
    511             LogUtil.v(TAG, "addPicturePart size: " + imageSize);
    512         }
    513 
    514         return imageSize;
    515     }
    516 
    517     private static void addPartForUri(final Context context, final PduBody pb,
    518             final String srcName, final Uri uri, final String contentType) {
    519         final PduPart part = new PduPart();
    520         part.setDataUri(uri);
    521         part.setContentType(contentType.getBytes());
    522 
    523         setPartContentLocationAndId(part, srcName);
    524 
    525         pb.addPart(part);
    526     }
    527 
    528     private static int addVCardPart(final Context context, final PduBody pb,
    529             final MessagePartData messagePart, final String srcName) {
    530         final Uri vcardUri = messagePart.getContentUri();
    531         final String contentType = messagePart.getContentType();
    532         final int vcardSize = getDataLength(context, vcardUri);
    533         if (vcardSize <= 0) {
    534             LogUtil.e(TAG, "Can't get vcard", new Exception());
    535             return 0;
    536         }
    537 
    538         addPartForUri(context, pb, srcName, vcardUri, contentType);
    539 
    540         if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
    541             LogUtil.v(TAG, "addVCardPart size: " + vcardSize);
    542         }
    543 
    544         return vcardSize;
    545     }
    546 
    547     /**
    548      * Add video part recompressing video if necessary.  If recompression fails, part is not
    549      * added.
    550      */
    551     private static int addVideoPart(final Context context, final PduBody pb,
    552             final MessagePartData messagePart, final String srcName) {
    553         final Uri attachmentUri = messagePart.getContentUri();
    554         String contentType = messagePart.getContentType();
    555 
    556         if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
    557             LogUtil.v(TAG, "addPart attachmentUrl: " + attachmentUri.toString());
    558         }
    559 
    560         if (TextUtils.isEmpty(contentType)) {
    561             contentType = ContentType.VIDEO_3G2;
    562         }
    563 
    564         addPartForUri(context, pb, srcName, attachmentUri, contentType);
    565         return (int) getMediaFileSize(attachmentUri);
    566     }
    567 
    568     private static int addOtherPart(final Context context, final PduBody pb,
    569             final MessagePartData messagePart, final String srcName) {
    570         final Uri attachmentUri = messagePart.getContentUri();
    571         final String contentType = messagePart.getContentType();
    572 
    573         if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
    574             LogUtil.v(TAG, "addPart attachmentUrl: " + attachmentUri.toString());
    575         }
    576 
    577         final int dataSize = (int) getMediaFileSize(attachmentUri);
    578 
    579         addPartForUri(context, pb, srcName, attachmentUri, contentType);
    580 
    581         return dataSize;
    582     }
    583 
    584     private static void addSmilPart(final PduBody pb, final String smilTemplate,
    585             final String smilBody) {
    586         final PduPart smilPart = new PduPart();
    587         smilPart.setContentId("smil".getBytes());
    588         smilPart.setContentLocation("smil.xml".getBytes());
    589         smilPart.setContentType(ContentType.APP_SMIL.getBytes());
    590         final String smil = String.format(smilTemplate, smilBody);
    591         smilPart.setData(smil.getBytes());
    592         pb.addPart(0, smilPart);
    593     }
    594 
    595     private static String getSmilTemplate(final boolean hasVisualAttachments,
    596             final boolean hasNonVisualAttachments, final boolean hasText) {
    597         if (hasVisualAttachments) {
    598             return hasText ? sSmilVisualAttachmentsWithText : sSmilVisualAttachmentsOnly;
    599         }
    600         if (hasNonVisualAttachments) {
    601             return hasText ? sSmilNonVisualAttachmentsWithText : sSmilNonVisualAttachmentsOnly;
    602         }
    603         return sSmilTextOnly;
    604     }
    605 
    606     private static int getDataLength(final Context context, final Uri uri) {
    607         InputStream is = null;
    608         try {
    609             is = context.getContentResolver().openInputStream(uri);
    610             try {
    611                 return is == null ? 0 : is.available();
    612             } catch (final IOException e) {
    613                 LogUtil.e(TAG, "getDataLength couldn't stream: " + uri, e);
    614             }
    615         } catch (final FileNotFoundException e) {
    616             LogUtil.e(TAG, "getDataLength couldn't open: " + uri, e);
    617         } finally {
    618             if (is != null) {
    619                 try {
    620                     is.close();
    621                 } catch (final IOException e) {
    622                     LogUtil.e(TAG, "getDataLength couldn't close: " + uri, e);
    623                 }
    624             }
    625         }
    626         return 0;
    627     }
    628 
    629     /**
    630      * Returns {@code true} if group mms is turned on,
    631      * {@code false} otherwise.
    632      *
    633      * For the group mms feature to be enabled, the following must be true:
    634      *  1. the feature is enabled in mms_config.xml (currently on by default)
    635      *  2. the feature is enabled in the SMS settings page
    636      *
    637      * @return true if group mms is supported
    638      */
    639     public static boolean groupMmsEnabled(final int subId) {
    640         final Context context = Factory.get().getApplicationContext();
    641         final Resources resources = context.getResources();
    642         final BuglePrefs prefs = BuglePrefs.getSubscriptionPrefs(subId);
    643         final String groupMmsKey = resources.getString(R.string.group_mms_pref_key);
    644         final boolean groupMmsEnabledDefault = resources.getBoolean(R.bool.group_mms_pref_default);
    645         final boolean groupMmsPrefOn = prefs.getBoolean(groupMmsKey, groupMmsEnabledDefault);
    646         return MmsConfig.get(subId).getGroupMmsEnabled() && groupMmsPrefOn;
    647     }
    648 
    649     /**
    650      * Get a version of this image resized to fit the given dimension and byte-size limits. Note
    651      * that the content type of the resulting PduPart may not be the same as the content type of
    652      * this UriImage; always call {@link PduPart#getContentType()} to get the new content type.
    653      *
    654      * @param widthLimit The width limit, in pixels
    655      * @param heightLimit The height limit, in pixels
    656      * @param byteLimit The binary size limit, in bytes
    657      * @param width The image width, in pixels
    658      * @param height The image height, in pixels
    659      * @param orientation Orientation constant from ExifInterface for rotating or flipping the
    660      *                    image
    661      * @param imageUri Uri to the image data
    662      * @param context Needed to open the image
    663      * @return A new PduPart containing the resized image data
    664      */
    665     private static PduPart getResizedImageAsPart(final int widthLimit,
    666             final int heightLimit, final int byteLimit, final int width, final int height,
    667             final int orientation, final Uri imageUri, final Context context, final String contentType) {
    668         final PduPart part = new PduPart();
    669 
    670         final byte[] data = ImageResizer.getResizedImageData(width, height, orientation,
    671                 widthLimit, heightLimit, byteLimit, imageUri, context, contentType);
    672         if (data == null) {
    673             if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
    674                 LogUtil.v(TAG, "Resize image failed.");
    675             }
    676             return null;
    677         }
    678 
    679         part.setData(data);
    680         // Any static images will be compressed into a jpeg
    681         final String contentTypeOfResizedImage = ImageUtils.isGif(contentType, imageUri)
    682                 ? ContentType.IMAGE_GIF : ContentType.IMAGE_JPEG;
    683         part.setContentType(contentTypeOfResizedImage.getBytes());
    684 
    685         return part;
    686     }
    687 
    688     /**
    689      * Get media file size
    690      */
    691     public static long getMediaFileSize(final Uri uri) {
    692         final Context context = Factory.get().getApplicationContext();
    693         AssetFileDescriptor fd = null;
    694         try {
    695             fd = context.getContentResolver().openAssetFileDescriptor(uri, "r");
    696             if (fd != null) {
    697                 return fd.getParcelFileDescriptor().getStatSize();
    698             }
    699         } catch (final FileNotFoundException e) {
    700             LogUtil.e(TAG, "MmsUtils.getMediaFileSize: cound not find media file: " + e, e);
    701         } finally {
    702             if (fd != null) {
    703                 try {
    704                     fd.close();
    705                 } catch (final IOException e) {
    706                     LogUtil.e(TAG, "MmsUtils.getMediaFileSize: failed to close " + e, e);
    707                 }
    708             }
    709         }
    710         return 0L;
    711     }
    712 
    713     // Code for extracting the actual phone numbers for the participants in a conversation,
    714     // given a thread id.
    715 
    716     private static final Uri ALL_THREADS_URI =
    717             Threads.CONTENT_URI.buildUpon().appendQueryParameter("simple", "true").build();
    718 
    719     private static final String[] RECIPIENTS_PROJECTION = {
    720         Threads._ID,
    721         Threads.RECIPIENT_IDS
    722     };
    723 
    724     private static final int RECIPIENT_IDS  = 1;
    725 
    726     public static List<String> getRecipientsByThread(final long threadId) {
    727         final String spaceSepIds = getRawRecipientIdsForThread(threadId);
    728         if (!TextUtils.isEmpty(spaceSepIds)) {
    729             final Context context = Factory.get().getApplicationContext();
    730             return getAddresses(context, spaceSepIds);
    731         }
    732         return null;
    733     }
    734 
    735     // NOTE: There are phones on which you can't get the recipients from the thread id for SMS
    736     // until you have a message in the conversation!
    737     public static String getRawRecipientIdsForThread(final long threadId) {
    738         if (threadId <= 0) {
    739             return null;
    740         }
    741         final Context context = Factory.get().getApplicationContext();
    742         final ContentResolver cr = context.getContentResolver();
    743         final Cursor thread = cr.query(
    744                 ALL_THREADS_URI,
    745                 RECIPIENTS_PROJECTION, "_id=?", new String[] { String.valueOf(threadId) }, null);
    746         if (thread != null) {
    747             try {
    748                 if (thread.moveToFirst()) {
    749                     // recipientIds will be a space-separated list of ids into the
    750                     // canonical addresses table.
    751                     return thread.getString(RECIPIENT_IDS);
    752                 }
    753             } finally {
    754                 thread.close();
    755             }
    756         }
    757         return null;
    758     }
    759 
    760     private static final Uri SINGLE_CANONICAL_ADDRESS_URI =
    761             Uri.parse("content://mms-sms/canonical-address");
    762 
    763     private static List<String> getAddresses(final Context context, final String spaceSepIds) {
    764         final List<String> numbers = new ArrayList<String>();
    765         final String[] ids = spaceSepIds.split(" ");
    766         for (final String id : ids) {
    767             long longId;
    768 
    769             try {
    770                 longId = Long.parseLong(id);
    771                 if (longId < 0) {
    772                     LogUtil.e(TAG, "MmsUtils.getAddresses: invalid id " + longId);
    773                     continue;
    774                 }
    775             } catch (final NumberFormatException ex) {
    776                 LogUtil.e(TAG, "MmsUtils.getAddresses: invalid id. " + ex, ex);
    777                 // skip this id
    778                 continue;
    779             }
    780 
    781             // TODO: build a single query where we get all the addresses at once.
    782             Cursor c = null;
    783             try {
    784                 c = context.getContentResolver().query(
    785                         ContentUris.withAppendedId(SINGLE_CANONICAL_ADDRESS_URI, longId),
    786                         null, null, null, null);
    787             } catch (final Exception e) {
    788                 LogUtil.e(TAG, "MmsUtils.getAddresses: query failed for id " + longId, e);
    789             }
    790             if (c != null) {
    791                 try {
    792                     if (c.moveToFirst()) {
    793                         final String number = c.getString(0);
    794                         if (!TextUtils.isEmpty(number)) {
    795                             numbers.add(number);
    796                         } else {
    797                             LogUtil.w(TAG, "Canonical MMS/SMS address is empty for id: " + longId);
    798                         }
    799                     }
    800                 } finally {
    801                     c.close();
    802                 }
    803             }
    804         }
    805         if (numbers.isEmpty()) {
    806             LogUtil.w(TAG, "No MMS addresses found from ids string [" + spaceSepIds + "]");
    807         }
    808         return numbers;
    809     }
    810 
    811     // Get telephony SMS thread ID
    812     public static long getOrCreateSmsThreadId(final Context context, final String dest) {
    813         // use destinations to determine threadId
    814         final Set<String> recipients = new HashSet<String>();
    815         recipients.add(dest);
    816         try {
    817             return MmsSmsUtils.Threads.getOrCreateThreadId(context, recipients);
    818         } catch (final IllegalArgumentException e) {
    819             LogUtil.e(TAG, "MmsUtils: getting thread id failed: " + e);
    820             return -1;
    821         }
    822     }
    823 
    824     // Get telephony SMS thread ID
    825     public static long getOrCreateThreadId(final Context context, final List<String> dests) {
    826         if (dests == null || dests.size() == 0) {
    827             return -1;
    828         }
    829         // use destinations to determine threadId
    830         final Set<String> recipients = new HashSet<String>(dests);
    831         try {
    832             return MmsSmsUtils.Threads.getOrCreateThreadId(context, recipients);
    833         } catch (final IllegalArgumentException e) {
    834             LogUtil.e(TAG, "MmsUtils: getting thread id failed: " + e);
    835             return -1;
    836         }
    837     }
    838 
    839     /**
    840      * Add an SMS to the given URI with thread_id specified.
    841      *
    842      * @param resolver the content resolver to use
    843      * @param uri the URI to add the message to
    844      * @param subId subId for the receiving sim
    845      * @param address the address of the sender
    846      * @param body the body of the message
    847      * @param subject the psuedo-subject of the message
    848      * @param date the timestamp for the message
    849      * @param read true if the message has been read, false if not
    850      * @param threadId the thread_id of the message
    851      * @return the URI for the new message
    852      */
    853     private static Uri addMessageToUri(final ContentResolver resolver,
    854             final Uri uri, final int subId, final String address, final String body,
    855             final String subject, final Long date, final boolean read, final boolean seen,
    856             final int status, final int type, final long threadId) {
    857         final ContentValues values = new ContentValues(7);
    858 
    859         values.put(Telephony.Sms.ADDRESS, address);
    860         if (date != null) {
    861             values.put(Telephony.Sms.DATE, date);
    862         }
    863         values.put(Telephony.Sms.READ, read ? 1 : 0);
    864         values.put(Telephony.Sms.SEEN, seen ? 1 : 0);
    865         values.put(Telephony.Sms.SUBJECT, subject);
    866         values.put(Telephony.Sms.BODY, body);
    867         if (OsUtil.isAtLeastL_MR1()) {
    868             values.put(Telephony.Sms.SUBSCRIPTION_ID, subId);
    869         }
    870         if (status != Telephony.Sms.STATUS_NONE) {
    871             values.put(Telephony.Sms.STATUS, status);
    872         }
    873         if (type != Telephony.Sms.MESSAGE_TYPE_ALL) {
    874             values.put(Telephony.Sms.TYPE, type);
    875         }
    876         if (threadId != -1L) {
    877             values.put(Telephony.Sms.THREAD_ID, threadId);
    878         }
    879         return resolver.insert(uri, values);
    880     }
    881 
    882     // Insert an SMS message to telephony
    883     public static Uri insertSmsMessage(final Context context, final Uri uri, final int subId,
    884             final String dest, final String text, final long timestamp, final int status,
    885             final int type, final long threadId) {
    886         Uri response = null;
    887         try {
    888             response = addMessageToUri(context.getContentResolver(), uri, subId, dest,
    889                     text, null /* subject */, timestamp, true /* read */,
    890                     true /* seen */, status, type, threadId);
    891             if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
    892                 LogUtil.d(TAG, "Mmsutils: Inserted SMS message into telephony (type = " + type + ")"
    893                         + ", uri: " + response);
    894             }
    895         } catch (final SQLiteException e) {
    896             LogUtil.e(TAG, "MmsUtils: persist sms message failure " + e, e);
    897         } catch (final IllegalArgumentException e) {
    898             LogUtil.e(TAG, "MmsUtils: persist sms message failure " + e, e);
    899         }
    900         return response;
    901     }
    902 
    903     // Update SMS message type in telephony; returns true if it succeeded.
    904     public static boolean updateSmsMessageSendingStatus(final Context context, final Uri uri,
    905             final int type, final long date) {
    906         try {
    907             final ContentResolver resolver = context.getContentResolver();
    908             final ContentValues values = new ContentValues(2);
    909 
    910             values.put(Telephony.Sms.TYPE, type);
    911             values.put(Telephony.Sms.DATE, date);
    912             final int cnt = resolver.update(uri, values, null, null);
    913             if (cnt == 1) {
    914                 if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
    915                     LogUtil.d(TAG, "Mmsutils: Updated sending SMS " + uri + "; type = " + type
    916                             + ", date = " + date + " (millis since epoch)");
    917                 }
    918                 return true;
    919             }
    920         } catch (final SQLiteException e) {
    921             LogUtil.e(TAG, "MmsUtils: update sms message failure " + e, e);
    922         } catch (final IllegalArgumentException e) {
    923             LogUtil.e(TAG, "MmsUtils: update sms message failure " + e, e);
    924         }
    925         return false;
    926     }
    927 
    928     // Persist a sent MMS message in telephony
    929     private static Uri insertSendReq(final Context context, final GenericPdu pdu, final int subId,
    930             final String subPhoneNumber) {
    931         final PduPersister persister = PduPersister.getPduPersister(context);
    932         Uri uri = null;
    933         try {
    934             // Persist the PDU
    935             uri = persister.persist(
    936                     pdu,
    937                     Mms.Sent.CONTENT_URI,
    938                     subId,
    939                     subPhoneNumber,
    940                     null/*preOpenedFiles*/);
    941             // Update mms table to reflect sent messages are always seen and read
    942             final ContentValues values = new ContentValues(1);
    943             values.put(Mms.READ, 1);
    944             values.put(Mms.SEEN, 1);
    945             SqliteWrapper.update(context, context.getContentResolver(), uri, values, null, null);
    946         } catch (final MmsException e) {
    947             LogUtil.e(TAG, "MmsUtils: persist mms sent message failure " + e, e);
    948         }
    949         return uri;
    950     }
    951 
    952     // Persist a received MMS message in telephony
    953     public static Uri insertReceivedMmsMessage(final Context context,
    954             final RetrieveConf retrieveConf, final int subId, final String subPhoneNumber,
    955             final long receivedTimestampInSeconds, final String contentLocation) {
    956         final PduPersister persister = PduPersister.getPduPersister(context);
    957         Uri uri = null;
    958         try {
    959             uri = persister.persist(
    960                     retrieveConf,
    961                     Mms.Inbox.CONTENT_URI,
    962                     subId,
    963                     subPhoneNumber,
    964                     null/*preOpenedFiles*/);
    965 
    966             final ContentValues values = new ContentValues(2);
    967             // Update mms table with local time instead of PDU time
    968             values.put(Mms.DATE, receivedTimestampInSeconds);
    969             // Also update the content location field from NotificationInd so that
    970             // wap push dedup would work even after the wap push is deleted
    971             values.put(Mms.CONTENT_LOCATION, contentLocation);
    972             SqliteWrapper.update(context, context.getContentResolver(), uri, values, null, null);
    973             if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
    974                 LogUtil.d(TAG, "MmsUtils: Inserted MMS message into telephony, uri: " + uri);
    975             }
    976         } catch (final MmsException e) {
    977             LogUtil.e(TAG, "MmsUtils: persist mms received message failure " + e, e);
    978             // Just returns empty uri to RetrieveMmsRequest, which triggers a permanent failure
    979         } catch (final SQLiteException e) {
    980             LogUtil.e(TAG, "MmsUtils: update mms received message failure " + e, e);
    981             // Time update failure is ignored.
    982         }
    983         return uri;
    984     }
    985 
    986     // Update MMS message type in telephony; returns true if it succeeded.
    987     public static boolean updateMmsMessageSendingStatus(final Context context, final Uri uri,
    988             final int box, final long timestampInMillis) {
    989         try {
    990             final ContentResolver resolver = context.getContentResolver();
    991             final ContentValues values = new ContentValues();
    992 
    993             final long timestampInSeconds = timestampInMillis / 1000L;
    994             values.put(Telephony.Mms.MESSAGE_BOX, box);
    995             values.put(Telephony.Mms.DATE, timestampInSeconds);
    996             final int cnt = resolver.update(uri, values, null, null);
    997             if (cnt == 1) {
    998                 if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
    999                     LogUtil.d(TAG, "Mmsutils: Updated sending MMS " + uri + "; box = " + box
   1000                             + ", date = " + timestampInSeconds + " (secs since epoch)");
   1001                 }
   1002                 return true;
   1003             }
   1004         } catch (final SQLiteException e) {
   1005             LogUtil.e(TAG, "MmsUtils: update mms message failure " + e, e);
   1006         } catch (final IllegalArgumentException e) {
   1007             LogUtil.e(TAG, "MmsUtils: update mms message failure " + e, e);
   1008         }
   1009         return false;
   1010     }
   1011 
   1012     /**
   1013      * Parse values from a received sms message
   1014      *
   1015      * @param context
   1016      * @param msgs The received sms message content
   1017      * @param error The received sms error
   1018      * @return Parsed values from the message
   1019      */
   1020     public static ContentValues parseReceivedSmsMessage(
   1021             final Context context, final SmsMessage[] msgs, final int error) {
   1022         final SmsMessage sms = msgs[0];
   1023         final ContentValues values = new ContentValues();
   1024 
   1025         values.put(Sms.ADDRESS, sms.getDisplayOriginatingAddress());
   1026         values.put(Sms.BODY, buildMessageBodyFromPdus(msgs));
   1027         if (MmsUtils.hasSmsDateSentColumn()) {
   1028             // TODO:: The boxing here seems unnecessary.
   1029             values.put(Sms.DATE_SENT, Long.valueOf(sms.getTimestampMillis()));
   1030         }
   1031         values.put(Sms.PROTOCOL, sms.getProtocolIdentifier());
   1032         if (sms.getPseudoSubject().length() > 0) {
   1033             values.put(Sms.SUBJECT, sms.getPseudoSubject());
   1034         }
   1035         values.put(Sms.REPLY_PATH_PRESENT, sms.isReplyPathPresent() ? 1 : 0);
   1036         values.put(Sms.SERVICE_CENTER, sms.getServiceCenterAddress());
   1037         // Error code
   1038         values.put(Sms.ERROR_CODE, error);
   1039 
   1040         return values;
   1041     }
   1042 
   1043     // Some providers send formfeeds in their messages. Convert those formfeeds to newlines.
   1044     private static String replaceFormFeeds(final String s) {
   1045         return s == null ? "" : s.replace('\f', '\n');
   1046     }
   1047 
   1048     // Parse the message body from message PDUs
   1049     private static String buildMessageBodyFromPdus(final SmsMessage[] msgs) {
   1050         if (msgs.length == 1) {
   1051             // There is only one part, so grab the body directly.
   1052             return replaceFormFeeds(msgs[0].getDisplayMessageBody());
   1053         } else {
   1054             // Build up the body from the parts.
   1055             final StringBuilder body = new StringBuilder();
   1056             for (final SmsMessage msg : msgs) {
   1057                 try {
   1058                     // getDisplayMessageBody() can NPE if mWrappedMessage inside is null.
   1059                     body.append(msg.getDisplayMessageBody());
   1060                 } catch (final NullPointerException e) {
   1061                     // Nothing to do
   1062                 }
   1063             }
   1064             return replaceFormFeeds(body.toString());
   1065         }
   1066     }
   1067 
   1068     // Parse the message date
   1069     public static Long getMessageDate(final SmsMessage sms, long now) {
   1070         // Use now for the timestamp to avoid confusion with clock
   1071         // drift between the handset and the SMSC.
   1072         // Check to make sure the system is giving us a non-bogus time.
   1073         final Calendar buildDate = new GregorianCalendar(2011, 8, 18);    // 18 Sep 2011
   1074         final Calendar nowDate = new GregorianCalendar();
   1075         nowDate.setTimeInMillis(now);
   1076         if (nowDate.before(buildDate)) {
   1077             // It looks like our system clock isn't set yet because the current time right now
   1078             // is before an arbitrary time we made this build. Instead of inserting a bogus
   1079             // receive time in this case, use the timestamp of when the message was sent.
   1080             now = sms.getTimestampMillis();
   1081         }
   1082         return now;
   1083     }
   1084 
   1085     /**
   1086      * cleanseMmsSubject will take a subject that's says, "<Subject: no subject>", and return
   1087      * a null string. Otherwise it will return the original subject string.
   1088      * @param resources So the function can grab string resources
   1089      * @param subject the raw subject
   1090      * @return
   1091      */
   1092     public static String cleanseMmsSubject(final Resources resources, final String subject) {
   1093         if (TextUtils.isEmpty(subject)) {
   1094             return null;
   1095         }
   1096         if (sNoSubjectStrings == null) {
   1097             sNoSubjectStrings =
   1098                     resources.getStringArray(R.array.empty_subject_strings);
   1099         }
   1100         for (final String noSubjectString : sNoSubjectStrings) {
   1101             if (subject.equalsIgnoreCase(noSubjectString)) {
   1102                 return null;
   1103             }
   1104         }
   1105         return subject;
   1106     }
   1107 
   1108     // return a semicolon separated list of phone numbers from a smsto: uri.
   1109     public static String getSmsRecipients(final Uri uri) {
   1110         String recipients = uri.getSchemeSpecificPart();
   1111         final int pos = recipients.indexOf('?');
   1112         if (pos != -1) {
   1113             recipients = recipients.substring(0, pos);
   1114         }
   1115         recipients = replaceUnicodeDigits(recipients).replace(',', ';');
   1116         return recipients;
   1117     }
   1118 
   1119     // This function was lifted from Telephony.PhoneNumberUtils because it was @hide
   1120     /**
   1121      * Replace arabic/unicode digits with decimal digits.
   1122      * @param number
   1123      *            the number to be normalized.
   1124      * @return the replaced number.
   1125      */
   1126     private static String replaceUnicodeDigits(final String number) {
   1127         final StringBuilder normalizedDigits = new StringBuilder(number.length());
   1128         for (final char c : number.toCharArray()) {
   1129             final int digit = Character.digit(c, 10);
   1130             if (digit != -1) {
   1131                 normalizedDigits.append(digit);
   1132             } else {
   1133                 normalizedDigits.append(c);
   1134             }
   1135         }
   1136         return normalizedDigits.toString();
   1137     }
   1138 
   1139     /**
   1140      * @return Whether the data roaming is enabled
   1141      */
   1142     private static boolean isDataRoamingEnabled() {
   1143         boolean dataRoamingEnabled = false;
   1144         final ContentResolver cr = Factory.get().getApplicationContext().getContentResolver();
   1145         if (OsUtil.isAtLeastJB_MR1()) {
   1146             dataRoamingEnabled = (Settings.Global.getInt(cr, Settings.Global.DATA_ROAMING, 0) != 0);
   1147         } else {
   1148             dataRoamingEnabled = (Settings.System.getInt(cr, Settings.System.DATA_ROAMING, 0) != 0);
   1149         }
   1150         return dataRoamingEnabled;
   1151     }
   1152 
   1153     /**
   1154      * @return Whether to auto retrieve MMS
   1155      */
   1156     public static boolean allowMmsAutoRetrieve(final int subId) {
   1157         final Context context = Factory.get().getApplicationContext();
   1158         final Resources resources = context.getResources();
   1159         final BuglePrefs prefs = BuglePrefs.getSubscriptionPrefs(subId);
   1160         final boolean autoRetrieve = prefs.getBoolean(
   1161                 resources.getString(R.string.auto_retrieve_mms_pref_key),
   1162                 resources.getBoolean(R.bool.auto_retrieve_mms_pref_default));
   1163         if (autoRetrieve) {
   1164             final boolean autoRetrieveInRoaming = prefs.getBoolean(
   1165                     resources.getString(R.string.auto_retrieve_mms_when_roaming_pref_key),
   1166                     resources.getBoolean(R.bool.auto_retrieve_mms_when_roaming_pref_default));
   1167             final PhoneUtils phoneUtils = PhoneUtils.get(subId);
   1168             if ((autoRetrieveInRoaming && phoneUtils.isDataRoamingEnabled())
   1169                     || !phoneUtils.isRoaming()) {
   1170                 return true;
   1171             }
   1172         }
   1173         return false;
   1174     }
   1175 
   1176     /**
   1177      * Parse the message row id from a message Uri.
   1178      *
   1179      * @param messageUri The input Uri
   1180      * @return The message row id if valid, otherwise -1
   1181      */
   1182     public static long parseRowIdFromMessageUri(final Uri messageUri) {
   1183         try {
   1184             if (messageUri != null) {
   1185                 return ContentUris.parseId(messageUri);
   1186             }
   1187         } catch (final UnsupportedOperationException e) {
   1188             // Nothing to do
   1189         } catch (final NumberFormatException e) {
   1190             // Nothing to do
   1191         }
   1192         return -1;
   1193     }
   1194 
   1195     public static SmsMessage getSmsMessageFromDeliveryReport(final Intent intent) {
   1196         final byte[] pdu = intent.getByteArrayExtra("pdu");
   1197         return SmsMessage.createFromPdu(pdu);
   1198     }
   1199 
   1200     /**
   1201      * Update the status and date_sent column of sms message in telephony provider
   1202      *
   1203      * @param smsMessageUri
   1204      * @param status
   1205      * @param timeSentInMillis
   1206      */
   1207     public static void updateSmsStatusAndDateSent(final Uri smsMessageUri, final int status,
   1208             final long timeSentInMillis) {
   1209         if (smsMessageUri == null) {
   1210             return;
   1211         }
   1212         final ContentValues values = new ContentValues();
   1213         values.put(Sms.STATUS, status);
   1214         if (MmsUtils.hasSmsDateSentColumn()) {
   1215             values.put(Sms.DATE_SENT, timeSentInMillis);
   1216         }
   1217         final ContentResolver resolver = Factory.get().getApplicationContext().getContentResolver();
   1218         resolver.update(smsMessageUri, values, null/*where*/, null/*selectionArgs*/);
   1219     }
   1220 
   1221     /**
   1222      * Get the SQL selection statement for matching messages with media.
   1223      *
   1224      * Example for MMS part table:
   1225      * "((ct LIKE 'image/%')
   1226      *   OR (ct LIKE 'video/%')
   1227      *   OR (ct LIKE 'audio/%')
   1228      *   OR (ct='application/ogg'))
   1229      *
   1230      * @param contentTypeColumn The content-type column name
   1231      * @return The SQL selection statement for matching media types: image, video, audio
   1232      */
   1233     public static String getMediaTypeSelectionSql(final String contentTypeColumn) {
   1234         return String.format(
   1235                 Locale.US,
   1236                 "((%s LIKE '%s') OR (%s LIKE '%s') OR (%s LIKE '%s') OR (%s='%s'))",
   1237                 contentTypeColumn,
   1238                 "image/%",
   1239                 contentTypeColumn,
   1240                 "video/%",
   1241                 contentTypeColumn,
   1242                 "audio/%",
   1243                 contentTypeColumn,
   1244                 ContentType.AUDIO_OGG);
   1245     }
   1246 
   1247     // Max number of operands per SQL query for deleting SMS messages
   1248     public static final int MAX_IDS_PER_QUERY = 128;
   1249 
   1250     /**
   1251      * Delete MMS messages with media parts.
   1252      *
   1253      * Because the telephony provider constraints, we can't use JOIN and delete messages in one
   1254      * shot. We have to do a query first and then batch delete the messages based on IDs.
   1255      *
   1256      * @return The count of messages deleted.
   1257      */
   1258     public static int deleteMediaMessages() {
   1259         // Do a query first
   1260         //
   1261         // The WHERE clause has two parts:
   1262         // The first part is to select the exact same types of MMS messages as when we import them
   1263         // (so that we don't delete messages that are not in local database)
   1264         // The second part is to select MMS with media parts, including image, video and audio
   1265         final String selection = String.format(
   1266                 Locale.US,
   1267                 "%s AND (%s IN (SELECT %s FROM part WHERE %s))",
   1268                 getMmsTypeSelectionSql(),
   1269                 Mms._ID,
   1270                 Mms.Part.MSG_ID,
   1271                 getMediaTypeSelectionSql(Mms.Part.CONTENT_TYPE));
   1272         final ContentResolver resolver = Factory.get().getApplicationContext().getContentResolver();
   1273         final Cursor cursor = resolver.query(Mms.CONTENT_URI,
   1274                 new String[]{ Mms._ID },
   1275                 selection,
   1276                 null/*selectionArgs*/,
   1277                 null/*sortOrder*/);
   1278         int deleted = 0;
   1279         if (cursor != null) {
   1280             final long[] messageIds = new long[cursor.getCount()];
   1281             try {
   1282                 int i = 0;
   1283                 while (cursor.moveToNext()) {
   1284                     messageIds[i++] = cursor.getLong(0);
   1285                 }
   1286             } finally {
   1287                 cursor.close();
   1288             }
   1289             final int totalIds = messageIds.length;
   1290             if (totalIds > 0) {
   1291                 // Batch delete the messages using IDs
   1292                 // We don't want to send all IDs at once since there is a limit on SQL statement
   1293                 for (int start = 0; start < totalIds; start += MAX_IDS_PER_QUERY) {
   1294                     final int end = Math.min(start + MAX_IDS_PER_QUERY, totalIds); // excluding
   1295                     final int count = end - start;
   1296                     final String batchSelection = String.format(
   1297                             Locale.US,
   1298                             "%s IN %s",
   1299                             Mms._ID,
   1300                             getSqlInOperand(count));
   1301                     final String[] batchSelectionArgs =
   1302                             getSqlInOperandArgs(messageIds, start, count);
   1303                     final int deletedForBatch = resolver.delete(
   1304                             Mms.CONTENT_URI,
   1305                             batchSelection,
   1306                             batchSelectionArgs);
   1307                     if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
   1308                         LogUtil.d(TAG, "deleteMediaMessages: deleting IDs = "
   1309                                 + Joiner.on(',').skipNulls().join(batchSelectionArgs)
   1310                                 + ", deleted = " + deletedForBatch);
   1311                     }
   1312                     deleted += deletedForBatch;
   1313                 }
   1314             }
   1315         }
   1316         return deleted;
   1317     }
   1318 
   1319     /**
   1320      * Get the (?,?,...) thing for the SQL IN operator by a count
   1321      *
   1322      * @param count
   1323      * @return
   1324      */
   1325     public static String getSqlInOperand(final int count) {
   1326         if (count <= 0) {
   1327             return null;
   1328         }
   1329         final StringBuilder sb = new StringBuilder();
   1330         sb.append("(?");
   1331         for (int i = 0; i < count - 1; i++) {
   1332             sb.append(",?");
   1333         }
   1334         sb.append(")");
   1335         return sb.toString();
   1336     }
   1337 
   1338     /**
   1339      * Get the args for SQL IN operator from a long ID array
   1340      *
   1341      * @param ids The original long id array
   1342      * @param start Start of the ids to fill the args
   1343      * @param count Number of ids to pack
   1344      * @return The long array with the id args
   1345      */
   1346     private static String[] getSqlInOperandArgs(
   1347             final long[] ids, final int start, final int count) {
   1348         if (count <= 0) {
   1349             return null;
   1350         }
   1351         final String[] args = new String[count];
   1352         for (int i = 0; i < count; i++) {
   1353             args[i] = Long.toString(ids[start + i]);
   1354         }
   1355         return args;
   1356     }
   1357 
   1358     /**
   1359      * Delete SMS and MMS messages that are earlier than a specific timestamp
   1360      *
   1361      * @param cutOffTimestampInMillis The cut-off timestamp
   1362      * @return Total number of messages deleted.
   1363      */
   1364     public static int deleteMessagesOlderThan(final long cutOffTimestampInMillis) {
   1365         int deleted = 0;
   1366         final ContentResolver resolver = Factory.get().getApplicationContext().getContentResolver();
   1367         // Delete old SMS
   1368         final String smsSelection = String.format(
   1369                 Locale.US,
   1370                 "%s AND (%s<=%d)",
   1371                 getSmsTypeSelectionSql(),
   1372                 Sms.DATE,
   1373                 cutOffTimestampInMillis);
   1374         deleted += resolver.delete(Sms.CONTENT_URI, smsSelection, null/*selectionArgs*/);
   1375         // Delete old MMS
   1376         final String mmsSelection = String.format(
   1377                 Locale.US,
   1378                 "%s AND (%s<=%d)",
   1379                 getMmsTypeSelectionSql(),
   1380                 Mms.DATE,
   1381                 cutOffTimestampInMillis / 1000L);
   1382         deleted += resolver.delete(Mms.CONTENT_URI, mmsSelection, null/*selectionArgs*/);
   1383         return deleted;
   1384     }
   1385 
   1386     /**
   1387      * Update the read status of SMS/MMS messages by thread and timestamp
   1388      *
   1389      * @param threadId The thread of sms/mms to change
   1390      * @param timestampInMillis Change the status before this timestamp
   1391      */
   1392     public static void updateSmsReadStatus(final long threadId, final long timestampInMillis) {
   1393         final ContentResolver resolver = Factory.get().getApplicationContext().getContentResolver();
   1394         final ContentValues values = new ContentValues();
   1395         values.put("read", 1);
   1396         values.put("seen", 1); /* If you read it you saw it */
   1397         final String smsSelection = String.format(
   1398                 Locale.US,
   1399                 "%s=%d AND %s<=%d AND %s=0",
   1400                 Sms.THREAD_ID,
   1401                 threadId,
   1402                 Sms.DATE,
   1403                 timestampInMillis,
   1404                 Sms.READ);
   1405         resolver.update(
   1406                 Sms.CONTENT_URI,
   1407                 values,
   1408                 smsSelection,
   1409                 null/*selectionArgs*/);
   1410         final String mmsSelection = String.format(
   1411                 Locale.US,
   1412                 "%s=%d AND %s<=%d AND %s=0",
   1413                 Mms.THREAD_ID,
   1414                 threadId,
   1415                 Mms.DATE,
   1416                 timestampInMillis / 1000L,
   1417                 Mms.READ);
   1418         resolver.update(
   1419                 Mms.CONTENT_URI,
   1420                 values,
   1421                 mmsSelection,
   1422                 null/*selectionArgs*/);
   1423     }
   1424 
   1425     /**
   1426      * Update the read status of a single MMS message by its URI
   1427      *
   1428      * @param mmsUri
   1429      * @param read
   1430      */
   1431     public static void updateReadStatusForMmsMessage(final Uri mmsUri, final boolean read) {
   1432         final ContentResolver resolver = Factory.get().getApplicationContext().getContentResolver();
   1433         final ContentValues values = new ContentValues();
   1434         values.put(Mms.READ, read ? 1 : 0);
   1435         resolver.update(mmsUri, values, null/*where*/, null/*selectionArgs*/);
   1436     }
   1437 
   1438     public static class AttachmentInfo {
   1439         public String mUrl;
   1440         public String mContentType;
   1441         public int mWidth;
   1442         public int mHeight;
   1443     }
   1444 
   1445     /**
   1446      * Convert byte array to Java String using a charset name
   1447      *
   1448      * @param bytes
   1449      * @param charsetName
   1450      * @return
   1451      */
   1452     public static String bytesToString(final byte[] bytes, final String charsetName) {
   1453         if (bytes == null) {
   1454             return null;
   1455         }
   1456         try {
   1457             return new String(bytes, charsetName);
   1458         } catch (final UnsupportedEncodingException e) {
   1459             LogUtil.e(TAG, "MmsUtils.bytesToString: " + e, e);
   1460             return new String(bytes);
   1461         }
   1462     }
   1463 
   1464     /**
   1465      * Convert a Java String to byte array using a charset name
   1466      *
   1467      * @param string
   1468      * @param charsetName
   1469      * @return
   1470      */
   1471     public static byte[] stringToBytes(final String string, final String charsetName) {
   1472         if (string == null) {
   1473             return null;
   1474         }
   1475         try {
   1476             return string.getBytes(charsetName);
   1477         } catch (final UnsupportedEncodingException e) {
   1478             LogUtil.e(TAG, "MmsUtils.stringToBytes: " + e, e);
   1479             return string.getBytes();
   1480         }
   1481     }
   1482 
   1483     private static final String[] TEST_DATE_SENT_PROJECTION = new String[] { Sms.DATE_SENT };
   1484     private static Boolean sHasSmsDateSentColumn = null;
   1485     /**
   1486      * Check if date_sent column exists on ICS and above devices. We need to do a test
   1487      * query to figure that out since on some ICS+ devices, somehow the date_sent column does
   1488      * not exist. http://b/17629135 tracks the associated compliance test.
   1489      *
   1490      * @return Whether "date_sent" column exists in sms table
   1491      */
   1492     public static boolean hasSmsDateSentColumn() {
   1493         if (sHasSmsDateSentColumn == null) {
   1494             Cursor cursor = null;
   1495             try {
   1496                 final Context context = Factory.get().getApplicationContext();
   1497                 final ContentResolver resolver = context.getContentResolver();
   1498                 cursor = SqliteWrapper.query(
   1499                         context,
   1500                         resolver,
   1501                         Sms.CONTENT_URI,
   1502                         TEST_DATE_SENT_PROJECTION,
   1503                         null/*selection*/,
   1504                         null/*selectionArgs*/,
   1505                         Sms.DATE_SENT + " ASC LIMIT 1");
   1506                 sHasSmsDateSentColumn = true;
   1507             } catch (final SQLiteException e) {
   1508                 LogUtil.w(TAG, "date_sent in sms table does not exist", e);
   1509                 sHasSmsDateSentColumn = false;
   1510             } finally {
   1511                 if (cursor != null) {
   1512                     cursor.close();
   1513                 }
   1514             }
   1515         }
   1516         return sHasSmsDateSentColumn;
   1517     }
   1518 
   1519     private static final String[] TEST_CARRIERS_PROJECTION =
   1520             new String[] { Telephony.Carriers.MMSC };
   1521     private static Boolean sUseSystemApn = null;
   1522     /**
   1523      * Check if we can access the APN data in the Telephony provider. Access was restricted in
   1524      * JB MR1 (and some JB MR2) devices. If we can't access the APN, we have to fall back and use
   1525      * a private table in our own app.
   1526      *
   1527      * @return Whether we can access the system APN table
   1528      */
   1529     public static boolean useSystemApnTable() {
   1530         if (sUseSystemApn == null) {
   1531             Cursor cursor = null;
   1532             try {
   1533                 final Context context = Factory.get().getApplicationContext();
   1534                 final ContentResolver resolver = context.getContentResolver();
   1535                 cursor = SqliteWrapper.query(
   1536                         context,
   1537                         resolver,
   1538                         Telephony.Carriers.CONTENT_URI,
   1539                         TEST_CARRIERS_PROJECTION,
   1540                         null/*selection*/,
   1541                         null/*selectionArgs*/,
   1542                         null);
   1543                 sUseSystemApn = true;
   1544             } catch (final SecurityException e) {
   1545                 LogUtil.w(TAG, "Can't access system APN, using internal table", e);
   1546                 sUseSystemApn = false;
   1547             } finally {
   1548                 if (cursor != null) {
   1549                     cursor.close();
   1550                 }
   1551             }
   1552         }
   1553         return sUseSystemApn;
   1554     }
   1555 
   1556     // For the internal debugger only
   1557     public static void setUseSystemApnTable(final boolean turnOn) {
   1558         if (!turnOn) {
   1559             // We're not turning on to the system table. Instead, we're using our internal table.
   1560             final int osVersion = OsUtil.getApiVersion();
   1561             if (osVersion != android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) {
   1562                 // We're turning on local APNs on a device where we wouldn't normally have the
   1563                 // local APN table. Build it here.
   1564 
   1565                 final SQLiteDatabase database = ApnDatabase.getApnDatabase().getWritableDatabase();
   1566 
   1567                 // Do we already have the table?
   1568                 Cursor cursor = null;
   1569                 try {
   1570                     cursor = database.query(ApnDatabase.APN_TABLE,
   1571                             ApnDatabase.APN_PROJECTION,
   1572                             null, null, null, null, null, null);
   1573                 } catch (final Exception e) {
   1574                     // Apparently there's no table, create it now.
   1575                     ApnDatabase.forceBuildAndLoadApnTables();
   1576                 } finally {
   1577                     if (cursor != null) {
   1578                         cursor.close();
   1579                     }
   1580                 }
   1581             }
   1582         }
   1583         sUseSystemApn = turnOn;
   1584     }
   1585 
   1586     /**
   1587      * Checks if we should dump sms, based on both the setting and the global debug
   1588      * flag
   1589      *
   1590      * @return if dump sms is enabled
   1591      */
   1592     public static boolean isDumpSmsEnabled() {
   1593         if (!DebugUtils.isDebugEnabled()) {
   1594             return false;
   1595         }
   1596         return getDumpSmsOrMmsPref(R.string.dump_sms_pref_key, R.bool.dump_sms_pref_default);
   1597     }
   1598 
   1599     /**
   1600      * Checks if we should dump mms, based on both the setting and the global debug
   1601      * flag
   1602      *
   1603      * @return if dump mms is enabled
   1604      */
   1605     public static boolean isDumpMmsEnabled() {
   1606         if (!DebugUtils.isDebugEnabled()) {
   1607             return false;
   1608         }
   1609         return getDumpSmsOrMmsPref(R.string.dump_mms_pref_key, R.bool.dump_mms_pref_default);
   1610     }
   1611 
   1612     /**
   1613      * Load the value of dump sms or mms setting preference
   1614      */
   1615     private static boolean getDumpSmsOrMmsPref(final int prefKeyRes, final int defaultKeyRes) {
   1616         final Context context = Factory.get().getApplicationContext();
   1617         final Resources resources = context.getResources();
   1618         final BuglePrefs prefs = BuglePrefs.getApplicationPrefs();
   1619         final String key = resources.getString(prefKeyRes);
   1620         final boolean defaultValue = resources.getBoolean(defaultKeyRes);
   1621         return prefs.getBoolean(key, defaultValue);
   1622     }
   1623 
   1624     public static final Uri MMS_PART_CONTENT_URI = Uri.parse("content://mms/part");
   1625 
   1626     /**
   1627      * Load MMS from telephony
   1628      *
   1629      * @param mmsUri The MMS pdu Uri
   1630      * @return A memory copy of the MMS pdu including parts (but not addresses)
   1631      */
   1632     public static DatabaseMessages.MmsMessage loadMms(final Uri mmsUri) {
   1633         final Context context = Factory.get().getApplicationContext();
   1634         final ContentResolver resolver = context.getContentResolver();
   1635         DatabaseMessages.MmsMessage mms = null;
   1636         Cursor cursor = null;
   1637         // Load pdu first
   1638         try {
   1639             cursor = SqliteWrapper.query(context, resolver,
   1640                     mmsUri,
   1641                     DatabaseMessages.MmsMessage.getProjection(),
   1642                     null/*selection*/, null/*selectionArgs*/, null/*sortOrder*/);
   1643             if (cursor != null && cursor.moveToFirst()) {
   1644                 mms = DatabaseMessages.MmsMessage.get(cursor);
   1645             }
   1646         } catch (final SQLiteException e) {
   1647             LogUtil.e(TAG, "MmsLoader: query pdu failure: " + e, e);
   1648         } finally {
   1649             if (cursor != null) {
   1650                 cursor.close();
   1651             }
   1652         }
   1653         if (mms == null) {
   1654             return null;
   1655         }
   1656         // Load parts except SMIL
   1657         // TODO: we may need to load SMIL part in the future.
   1658         final long rowId = MmsUtils.parseRowIdFromMessageUri(mmsUri);
   1659         final String selection = String.format(
   1660                 Locale.US,
   1661                 "%s != '%s' AND %s = ?",
   1662                 Mms.Part.CONTENT_TYPE,
   1663                 ContentType.APP_SMIL,
   1664                 Mms.Part.MSG_ID);
   1665         cursor = null;
   1666         try {
   1667             cursor = SqliteWrapper.query(context, resolver,
   1668                     MMS_PART_CONTENT_URI,
   1669                     DatabaseMessages.MmsPart.PROJECTION,
   1670                     selection,
   1671                     new String[] { Long.toString(rowId) },
   1672                     null/*sortOrder*/);
   1673             if (cursor != null) {
   1674                 while (cursor.moveToNext()) {
   1675                     mms.addPart(DatabaseMessages.MmsPart.get(cursor, true/*loadMedia*/));
   1676                 }
   1677             }
   1678         } catch (final SQLiteException e) {
   1679             LogUtil.e(TAG, "MmsLoader: query parts failure: " + e, e);
   1680         } finally {
   1681             if (cursor != null) {
   1682                 cursor.close();
   1683             }
   1684         }
   1685         return mms;
   1686     }
   1687 
   1688     /**
   1689      * Get the sender of an MMS message
   1690      *
   1691      * @param recipients The recipient list of the message
   1692      * @param mmsUri The pdu uri of the MMS
   1693      * @return The sender phone number of the MMS
   1694      */
   1695     public static String getMmsSender(final List<String> recipients, final String mmsUri) {
   1696         final Context context = Factory.get().getApplicationContext();
   1697         // We try to avoid the database query.
   1698         // If this is a 1v1 conv., then the other party is the sender
   1699         if (recipients != null && recipients.size() == 1) {
   1700             return recipients.get(0);
   1701         }
   1702         // Otherwise, we have to query the MMS addr table for sender address
   1703         // This should only be done for a received group mms message
   1704         final Cursor cursor = SqliteWrapper.query(
   1705                 context,
   1706                 context.getContentResolver(),
   1707                 Uri.withAppendedPath(Uri.parse(mmsUri), "addr"),
   1708                 new String[] { Mms.Addr.ADDRESS, Mms.Addr.CHARSET },
   1709                 Mms.Addr.TYPE + "=" + PduHeaders.FROM,
   1710                 null/*selectionArgs*/,
   1711                 null/*sortOrder*/);
   1712         if (cursor != null) {
   1713             try {
   1714                 if (cursor.moveToFirst()) {
   1715                     return DatabaseMessages.MmsAddr.get(cursor);
   1716                 }
   1717             } finally {
   1718                 cursor.close();
   1719             }
   1720         }
   1721         return null;
   1722     }
   1723 
   1724     public static int bugleStatusForMms(final boolean isOutgoing, final boolean isNotification,
   1725             final int messageBox) {
   1726         int bugleStatus = MessageData.BUGLE_STATUS_UNKNOWN;
   1727         // For a message we sync either
   1728         if (isOutgoing) {
   1729             if (messageBox == Mms.MESSAGE_BOX_OUTBOX || messageBox == Mms.MESSAGE_BOX_FAILED) {
   1730                 // Not sent counts as failed and available for manual resend
   1731                 bugleStatus = MessageData.BUGLE_STATUS_OUTGOING_FAILED;
   1732             } else {
   1733                 // Otherwise outgoing message is complete
   1734                 bugleStatus = MessageData.BUGLE_STATUS_OUTGOING_COMPLETE;
   1735             }
   1736         } else if (isNotification) {
   1737             // Incoming MMS notifications we sync count as failed and available for manual download
   1738             bugleStatus = MessageData.BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD;
   1739         } else {
   1740             // Other incoming MMS messages are complete
   1741             bugleStatus = MessageData.BUGLE_STATUS_INCOMING_COMPLETE;
   1742         }
   1743         return bugleStatus;
   1744     }
   1745 
   1746     public static MessageData createMmsMessage(final DatabaseMessages.MmsMessage mms,
   1747             final String conversationId, final String participantId, final String selfId,
   1748             final int bugleStatus) {
   1749         Assert.notNull(mms);
   1750         final boolean isNotification = (mms.mMmsMessageType ==
   1751                 PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND);
   1752         final int rawMmsStatus = (bugleStatus < MessageData.BUGLE_STATUS_FIRST_INCOMING
   1753                 ? mms.mRetrieveStatus : mms.mResponseStatus);
   1754 
   1755         final MessageData message = MessageData.createMmsMessage(mms.getUri(),
   1756                 participantId, selfId, conversationId, isNotification, bugleStatus,
   1757                 mms.mContentLocation, mms.mTransactionId, mms.mPriority, mms.mSubject,
   1758                 mms.mSeen, mms.mRead, mms.getSize(), rawMmsStatus,
   1759                 mms.mExpiryInMillis, mms.mSentTimestampInMillis, mms.mTimestampInMillis);
   1760 
   1761         for (final DatabaseMessages.MmsPart part : mms.mParts) {
   1762             final MessagePartData messagePart = MmsUtils.createMmsMessagePart(part);
   1763             // Import media and text parts (skip SMIL and others)
   1764             if (messagePart != null) {
   1765                 message.addPart(messagePart);
   1766             }
   1767         }
   1768 
   1769         if (!message.getParts().iterator().hasNext()) {
   1770             message.addPart(MessagePartData.createEmptyMessagePart());
   1771         }
   1772 
   1773         return message;
   1774     }
   1775 
   1776     public static MessagePartData createMmsMessagePart(final DatabaseMessages.MmsPart part) {
   1777         MessagePartData messagePart = null;
   1778         if (part.isText()) {
   1779             final int mmsTextLengthLimit =
   1780                     BugleGservices.get().getInt(BugleGservicesKeys.MMS_TEXT_LIMIT,
   1781                             BugleGservicesKeys.MMS_TEXT_LIMIT_DEFAULT);
   1782             String text = part.mText;
   1783             if (text != null && text.length() > mmsTextLengthLimit) {
   1784                 // Limit the text to a reasonable value. We ran into a situation where a vcard
   1785                 // with a photo was sent as plain text. The massive amount of text caused the
   1786                 // app to hang, ANR, and eventually crash in native text code.
   1787                 text = text.substring(0, mmsTextLengthLimit);
   1788             }
   1789             messagePart = MessagePartData.createTextMessagePart(text);
   1790         } else if (part.isMedia()) {
   1791             messagePart = MessagePartData.createMediaMessagePart(part.mContentType,
   1792                     part.getDataUri(), MessagePartData.UNSPECIFIED_SIZE,
   1793                     MessagePartData.UNSPECIFIED_SIZE);
   1794         }
   1795         return messagePart;
   1796     }
   1797 
   1798     public static class StatusPlusUri {
   1799         // The request status to be as the result of the operation
   1800         // e.g. MMS_REQUEST_MANUAL_RETRY
   1801         public final int status;
   1802         // The raw telephony status
   1803         public final int rawStatus;
   1804         // The raw telephony URI
   1805         public final Uri uri;
   1806         // The operation result code from system api invocation (sent by system)
   1807         // or mapped from internal exception (sent by app)
   1808         public final int resultCode;
   1809 
   1810         public StatusPlusUri(final int status, final int rawStatus, final Uri uri) {
   1811             this.status = status;
   1812             this.rawStatus = rawStatus;
   1813             this.uri = uri;
   1814             resultCode = MessageData.UNKNOWN_RESULT_CODE;
   1815         }
   1816 
   1817         public StatusPlusUri(final int status, final int rawStatus, final Uri uri,
   1818                 final int resultCode) {
   1819             this.status = status;
   1820             this.rawStatus = rawStatus;
   1821             this.uri = uri;
   1822             this.resultCode = resultCode;
   1823         }
   1824     }
   1825 
   1826     public static class SendReqResp {
   1827         public SendReq mSendReq;
   1828         public SendConf mSendConf;
   1829 
   1830         public SendReqResp(final SendReq sendReq, final SendConf sendConf) {
   1831             mSendReq = sendReq;
   1832             mSendConf = sendConf;
   1833         }
   1834     }
   1835 
   1836     /**
   1837      * Returned when sending/downloading MMS via platform APIs. In that case, we have to wait to
   1838      * receive the pending intent to determine status.
   1839      */
   1840     public static final StatusPlusUri STATUS_PENDING = new StatusPlusUri(-1, -1, null);
   1841 
   1842     public static StatusPlusUri downloadMmsMessage(final Context context, final Uri notificationUri,
   1843             final int subId, final String subPhoneNumber, final String transactionId,
   1844             final String contentLocation, final boolean autoDownload,
   1845             final long receivedTimestampInSeconds, Bundle extras) {
   1846         if (TextUtils.isEmpty(contentLocation)) {
   1847             LogUtil.e(TAG, "MmsUtils: Download from empty content location URL");
   1848             return new StatusPlusUri(
   1849                     MMS_REQUEST_NO_RETRY, MessageData.RAW_TELEPHONY_STATUS_UNDEFINED, null);
   1850         }
   1851         if (!isMmsDataAvailable(subId)) {
   1852             LogUtil.e(TAG,
   1853                     "MmsUtils: failed to download message, no data available");
   1854             return new StatusPlusUri(MMS_REQUEST_MANUAL_RETRY,
   1855                     MessageData.RAW_TELEPHONY_STATUS_UNDEFINED,
   1856                     null,
   1857                     SmsManager.MMS_ERROR_NO_DATA_NETWORK);
   1858         }
   1859         int status = MMS_REQUEST_MANUAL_RETRY;
   1860         try {
   1861             RetrieveConf retrieveConf = null;
   1862             if (DebugUtils.isDebugEnabled() &&
   1863                     MediaScratchFileProvider
   1864                             .isMediaScratchSpaceUri(Uri.parse(contentLocation))) {
   1865                 if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
   1866                     LogUtil.d(TAG, "MmsUtils: Reading MMS from dump file: " + contentLocation);
   1867                 }
   1868                 final String fileName = Uri.parse(contentLocation).getPathSegments().get(1);
   1869                 final byte[] data = DebugUtils.receiveFromDumpFile(fileName);
   1870                 retrieveConf = receiveFromDumpFile(data);
   1871             } else {
   1872                 if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
   1873                     LogUtil.d(TAG, "MmsUtils: Downloading MMS via MMS lib API; notification "
   1874                             + "message: " + notificationUri);
   1875                 }
   1876                 if (OsUtil.isAtLeastL_MR1()) {
   1877                     if (subId < 0) {
   1878                         LogUtil.e(TAG, "MmsUtils: Incoming MMS came from unknown SIM");
   1879                         throw new MmsFailureException(MMS_REQUEST_NO_RETRY,
   1880                                 "Message from unknown SIM");
   1881                     }
   1882                 } else {
   1883                     Assert.isTrue(subId == ParticipantData.DEFAULT_SELF_SUB_ID);
   1884                 }
   1885                 if (extras == null) {
   1886                     extras = new Bundle();
   1887                 }
   1888                 extras.putParcelable(DownloadMmsAction.EXTRA_NOTIFICATION_URI, notificationUri);
   1889                 extras.putInt(DownloadMmsAction.EXTRA_SUB_ID, subId);
   1890                 extras.putString(DownloadMmsAction.EXTRA_SUB_PHONE_NUMBER, subPhoneNumber);
   1891                 extras.putString(DownloadMmsAction.EXTRA_TRANSACTION_ID, transactionId);
   1892                 extras.putString(DownloadMmsAction.EXTRA_CONTENT_LOCATION, contentLocation);
   1893                 extras.putBoolean(DownloadMmsAction.EXTRA_AUTO_DOWNLOAD, autoDownload);
   1894                 extras.putLong(DownloadMmsAction.EXTRA_RECEIVED_TIMESTAMP,
   1895                         receivedTimestampInSeconds);
   1896 
   1897                 MmsSender.downloadMms(context, subId, contentLocation, extras);
   1898                 return STATUS_PENDING; // Download happens asynchronously; no status to return
   1899             }
   1900             return insertDownloadedMessageAndSendResponse(context, notificationUri, subId,
   1901                     subPhoneNumber, transactionId, contentLocation, autoDownload,
   1902                     receivedTimestampInSeconds, retrieveConf);
   1903 
   1904         } catch (final MmsFailureException e) {
   1905             LogUtil.e(TAG, "MmsUtils: failed to download message " + notificationUri, e);
   1906             status = e.retryHint;
   1907         } catch (final InvalidHeaderValueException e) {
   1908             LogUtil.e(TAG, "MmsUtils: failed to download message " + notificationUri, e);
   1909         }
   1910         return new StatusPlusUri(status, PDU_HEADER_VALUE_UNDEFINED, null);
   1911     }
   1912 
   1913     public static StatusPlusUri insertDownloadedMessageAndSendResponse(final Context context,
   1914             final Uri notificationUri, final int subId, final String subPhoneNumber,
   1915             final String transactionId, final String contentLocation,
   1916             final boolean autoDownload, final long receivedTimestampInSeconds,
   1917             final RetrieveConf retrieveConf) {
   1918         final byte[] transactionIdBytes = stringToBytes(transactionId, "UTF-8");
   1919         Uri messageUri = null;
   1920         int status = MMS_REQUEST_MANUAL_RETRY;
   1921         int retrieveStatus = PDU_HEADER_VALUE_UNDEFINED;
   1922 
   1923         retrieveStatus = retrieveConf.getRetrieveStatus();
   1924         if (retrieveStatus == PduHeaders.RETRIEVE_STATUS_OK) {
   1925             status = MMS_REQUEST_SUCCEEDED;
   1926         } else if (retrieveStatus >= PduHeaders.RETRIEVE_STATUS_ERROR_TRANSIENT_FAILURE &&
   1927                 retrieveStatus < PduHeaders.RETRIEVE_STATUS_ERROR_PERMANENT_FAILURE) {
   1928             status = MMS_REQUEST_AUTO_RETRY;
   1929         } else {
   1930             // else not meant to retry download
   1931             status = MMS_REQUEST_NO_RETRY;
   1932             LogUtil.e(TAG, "MmsUtils: failed to retrieve message; retrieveStatus: "
   1933                     + retrieveStatus);
   1934         }
   1935         final ContentValues values = new ContentValues(1);
   1936         values.put(Mms.RETRIEVE_STATUS, retrieveConf.getRetrieveStatus());
   1937         SqliteWrapper.update(context, context.getContentResolver(),
   1938                 notificationUri, values, null, null);
   1939 
   1940         if (status == MMS_REQUEST_SUCCEEDED) {
   1941             // Send response of the notification
   1942             if (autoDownload) {
   1943                 sendNotifyResponseForMmsDownload(context, subId, transactionIdBytes,
   1944                         contentLocation, PduHeaders.STATUS_RETRIEVED);
   1945             } else {
   1946                 sendAcknowledgeForMmsDownload(context, subId, transactionIdBytes, contentLocation);
   1947             }
   1948 
   1949             // Insert downloaded message into telephony
   1950             final Uri inboxUri = MmsUtils.insertReceivedMmsMessage(context, retrieveConf, subId,
   1951                     subPhoneNumber, receivedTimestampInSeconds, contentLocation);
   1952             messageUri = ContentUris.withAppendedId(Mms.CONTENT_URI, ContentUris.parseId(inboxUri));
   1953         } else if (status == MMS_REQUEST_AUTO_RETRY) {
   1954             // For a retry do nothing
   1955         } else if (status == MMS_REQUEST_MANUAL_RETRY && autoDownload) {
   1956             // Failure from autodownload - just treat like manual download
   1957             sendNotifyResponseForMmsDownload(context, subId, transactionIdBytes,
   1958                     contentLocation, PduHeaders.STATUS_DEFERRED);
   1959         }
   1960         return new StatusPlusUri(status, retrieveStatus, messageUri);
   1961     }
   1962 
   1963     /**
   1964      * Send response for MMS download - catches and ignores errors
   1965      */
   1966     public static void sendNotifyResponseForMmsDownload(final Context context, final int subId,
   1967             final byte[] transactionId, final String contentLocation, final int status) {
   1968         try {
   1969             if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
   1970                 LogUtil.d(TAG, "MmsUtils: Sending M-NotifyResp.ind for received MMS, status: "
   1971                         + String.format("0x%X", status));
   1972             }
   1973             if (contentLocation == null) {
   1974                 LogUtil.w(TAG, "MmsUtils: Can't send NotifyResp; contentLocation is null");
   1975                 return;
   1976             }
   1977             if (transactionId == null) {
   1978                 LogUtil.w(TAG, "MmsUtils: Can't send NotifyResp; transaction id is null");
   1979                 return;
   1980             }
   1981             if (!isMmsDataAvailable(subId)) {
   1982                 LogUtil.w(TAG, "MmsUtils: Can't send NotifyResp; no data available");
   1983                 return;
   1984             }
   1985             MmsSender.sendNotifyResponseForMmsDownload(
   1986                     context, subId, transactionId, contentLocation, status);
   1987         } catch (final MmsFailureException e) {
   1988             LogUtil.e(TAG, "sendNotifyResponseForMmsDownload: failed to retrieve message " + e, e);
   1989         } catch (final InvalidHeaderValueException e) {
   1990             LogUtil.e(TAG, "sendNotifyResponseForMmsDownload: failed to retrieve message " + e, e);
   1991         }
   1992     }
   1993 
   1994     /**
   1995      * Send acknowledge for mms download - catched and ignores errors
   1996      */
   1997     public static void sendAcknowledgeForMmsDownload(final Context context, final int subId,
   1998             final byte[] transactionId, final String contentLocation) {
   1999         try {
   2000             if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
   2001                 LogUtil.d(TAG, "MmsUtils: Sending M-Acknowledge.ind for received MMS");
   2002             }
   2003             if (contentLocation == null) {
   2004                 LogUtil.w(TAG, "MmsUtils: Can't send AckInd; contentLocation is null");
   2005                 return;
   2006             }
   2007             if (transactionId == null) {
   2008                 LogUtil.w(TAG, "MmsUtils: Can't send AckInd; transaction id is null");
   2009                 return;
   2010             }
   2011             if (!isMmsDataAvailable(subId)) {
   2012                 LogUtil.w(TAG, "MmsUtils: Can't send AckInd; no data available");
   2013                 return;
   2014             }
   2015             MmsSender.sendAcknowledgeForMmsDownload(context, subId, transactionId, contentLocation);
   2016         } catch (final MmsFailureException e) {
   2017             LogUtil.e(TAG, "sendAcknowledgeForMmsDownload: failed to retrieve message " + e, e);
   2018         } catch (final InvalidHeaderValueException e) {
   2019             LogUtil.e(TAG, "sendAcknowledgeForMmsDownload: failed to retrieve message " + e, e);
   2020         }
   2021     }
   2022 
   2023     /**
   2024      * Try parsing a PDU without knowing the carrier. This is useful for importing
   2025      * MMS or storing draft when carrier info is not available
   2026      *
   2027      * @param data The PDU data
   2028      * @return Parsed PDU, null if failed to parse
   2029      */
   2030     private static GenericPdu parsePduForAnyCarrier(final byte[] data) {
   2031         GenericPdu pdu = null;
   2032         try {
   2033             pdu = (new PduParser(data, true/*parseContentDisposition*/)).parse();
   2034         } catch (final RuntimeException e) {
   2035             LogUtil.d(TAG, "parsePduForAnyCarrier: Failed to parse PDU with content disposition",
   2036                     e);
   2037         }
   2038         if (pdu == null) {
   2039             try {
   2040                 pdu = (new PduParser(data, false/*parseContentDisposition*/)).parse();
   2041             } catch (final RuntimeException e) {
   2042                 LogUtil.d(TAG,
   2043                         "parsePduForAnyCarrier: Failed to parse PDU without content disposition",
   2044                         e);
   2045             }
   2046         }
   2047         return pdu;
   2048     }
   2049 
   2050     private static RetrieveConf receiveFromDumpFile(final byte[] data) throws MmsFailureException {
   2051         final GenericPdu pdu = parsePduForAnyCarrier(data);
   2052         if (pdu == null || !(pdu instanceof RetrieveConf)) {
   2053             LogUtil.e(TAG, "receiveFromDumpFile: Parsing retrieved PDU failure");
   2054             throw new MmsFailureException(MMS_REQUEST_MANUAL_RETRY, "Failed reading dump file");
   2055         }
   2056         return (RetrieveConf) pdu;
   2057     }
   2058 
   2059     private static boolean isMmsDataAvailable(final int subId) {
   2060         if (OsUtil.isAtLeastL_MR1()) {
   2061             // L_MR1 above may support sending mms via wifi
   2062             return true;
   2063         }
   2064         final PhoneUtils phoneUtils = PhoneUtils.get(subId);
   2065         return !phoneUtils.isAirplaneModeOn() && phoneUtils.isMobileDataEnabled();
   2066     }
   2067 
   2068     private static boolean isSmsDataAvailable(final int subId) {
   2069         if (OsUtil.isAtLeastL_MR1()) {
   2070             // L_MR1 above may support sending sms via wifi
   2071             return true;
   2072         }
   2073         final PhoneUtils phoneUtils = PhoneUtils.get(subId);
   2074         return !phoneUtils.isAirplaneModeOn();
   2075     }
   2076 
   2077     public static boolean isMobileDataEnabled(final int subId) {
   2078         final PhoneUtils phoneUtils = PhoneUtils.get(subId);
   2079         return phoneUtils.isMobileDataEnabled();
   2080     }
   2081 
   2082     public static boolean isAirplaneModeOn(final int subId) {
   2083         final PhoneUtils phoneUtils = PhoneUtils.get(subId);
   2084         return phoneUtils.isAirplaneModeOn();
   2085     }
   2086 
   2087     public static StatusPlusUri sendMmsMessage(final Context context, final int subId,
   2088             final Uri messageUri, final Bundle extras) {
   2089         int status = MMS_REQUEST_MANUAL_RETRY;
   2090         int rawStatus = MessageData.RAW_TELEPHONY_STATUS_UNDEFINED;
   2091         if (!isMmsDataAvailable(subId)) {
   2092             LogUtil.w(TAG, "MmsUtils: failed to send message, no data available");
   2093             return new StatusPlusUri(MMS_REQUEST_MANUAL_RETRY,
   2094                     MessageData.RAW_TELEPHONY_STATUS_UNDEFINED,
   2095                     messageUri,
   2096                     SmsManager.MMS_ERROR_NO_DATA_NETWORK);
   2097         }
   2098         final PduPersister persister = PduPersister.getPduPersister(context);
   2099         try {
   2100             final SendReq sendReq = (SendReq) persister.load(messageUri);
   2101             if (sendReq == null) {
   2102                 LogUtil.w(TAG, "MmsUtils: Sending MMS was deleted; uri = " + messageUri);
   2103                 return new StatusPlusUri(MMS_REQUEST_NO_RETRY,
   2104                         MessageData.RAW_TELEPHONY_STATUS_UNDEFINED, messageUri);
   2105             }
   2106             if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
   2107                 LogUtil.d(TAG, String.format("MmsUtils: Sending MMS, message uri: %s", messageUri));
   2108             }
   2109             extras.putInt(SendMessageAction.KEY_SUB_ID, subId);
   2110             MmsSender.sendMms(context, subId, messageUri, sendReq, extras);
   2111             return STATUS_PENDING;
   2112         } catch (final MmsFailureException e) {
   2113             status = e.retryHint;
   2114             rawStatus = e.rawStatus;
   2115             LogUtil.e(TAG, "MmsUtils: failed to send message " + e, e);
   2116         } catch (final InvalidHeaderValueException e) {
   2117             LogUtil.e(TAG, "MmsUtils: failed to send message " + e, e);
   2118         } catch (final IllegalArgumentException e) {
   2119             LogUtil.e(TAG, "MmsUtils: invalid message to send " + e, e);
   2120         } catch (final MmsException e) {
   2121             LogUtil.e(TAG, "MmsUtils: failed to send message " + e, e);
   2122         }
   2123         // If we get here, some exception occurred
   2124         return new StatusPlusUri(status, rawStatus, messageUri);
   2125     }
   2126 
   2127     public static StatusPlusUri updateSentMmsMessageStatus(final Context context,
   2128             final Uri messageUri, final SendConf sendConf) {
   2129         int status = MMS_REQUEST_MANUAL_RETRY;
   2130         final int respStatus = sendConf.getResponseStatus();
   2131 
   2132         final ContentValues values = new ContentValues(2);
   2133         values.put(Mms.RESPONSE_STATUS, respStatus);
   2134         final byte[] messageId = sendConf.getMessageId();
   2135         if (messageId != null && messageId.length > 0) {
   2136             values.put(Mms.MESSAGE_ID, PduPersister.toIsoString(messageId));
   2137         }
   2138         SqliteWrapper.update(context, context.getContentResolver(),
   2139                 messageUri, values, null, null);
   2140         if (respStatus == PduHeaders.RESPONSE_STATUS_OK) {
   2141             status = MMS_REQUEST_SUCCEEDED;
   2142         } else if (respStatus == PduHeaders.RESPONSE_STATUS_ERROR_TRANSIENT_FAILURE ||
   2143                 respStatus == PduHeaders.RESPONSE_STATUS_ERROR_TRANSIENT_NETWORK_PROBLEM ||
   2144                 respStatus == PduHeaders.RESPONSE_STATUS_ERROR_TRANSIENT_PARTIAL_SUCCESS) {
   2145             status = MMS_REQUEST_AUTO_RETRY;
   2146         } else {
   2147             // else permanent failure
   2148             LogUtil.e(TAG, "MmsUtils: failed to send message; respStatus = "
   2149                     + String.format("0x%X", respStatus));
   2150         }
   2151         return new StatusPlusUri(status, respStatus, messageUri);
   2152     }
   2153 
   2154     public static void clearMmsStatus(final Context context, final Uri uri) {
   2155         // Messaging application can leave invalid values in STATUS field of M-Notification.ind
   2156         // messages.  Take this opportunity to clear it.
   2157         // Downloading status just kept in local db and not reflected into telephony.
   2158         final ContentValues values = new ContentValues(1);
   2159         values.putNull(Mms.STATUS);
   2160         SqliteWrapper.update(context, context.getContentResolver(),
   2161                     uri, values, null, null);
   2162     }
   2163 
   2164     // Selection for new dedup algorithm:
   2165     // ((m_type<>130) OR (exp>NOW)) AND (date>NOW-7d) AND (date<NOW+7d) AND (ct_l=xxxxxx)
   2166     // i.e. If it is NotificationInd and not expired or not NotificationInd
   2167     //      AND message is received with +/- 7 days from now
   2168     //      AND content location is the input URL
   2169     private static final String DUP_NOTIFICATION_QUERY_SELECTION =
   2170             "((" + Mms.MESSAGE_TYPE + "<>?) OR (" + Mms.EXPIRY + ">?)) AND ("
   2171                     + Mms.DATE + ">?) AND (" + Mms.DATE + "<?) AND (" + Mms.CONTENT_LOCATION +
   2172                     "=?)";
   2173     // Selection for old behavior: only checks NotificationInd and its content location
   2174     private static final String DUP_NOTIFICATION_QUERY_SELECTION_OLD =
   2175             "(" + Mms.MESSAGE_TYPE + "=?) AND (" + Mms.CONTENT_LOCATION + "=?)";
   2176 
   2177     private static final int MAX_RETURN = 32;
   2178     private static String[] getDupNotifications(final Context context, final NotificationInd nInd) {
   2179         final byte[] rawLocation = nInd.getContentLocation();
   2180         if (rawLocation != null) {
   2181             final String location = new String(rawLocation);
   2182             // We can not be sure if the content location of an MMS is globally and historically
   2183             // unique. So we limit the dedup time within the last 7 days
   2184             // (or configured by gservices remotely). If the same content location shows up after
   2185             // that, we will download regardless. Duplicated message is better than no message.
   2186             String selection;
   2187             String[] selectionArgs;
   2188             final long timeLimit = BugleGservices.get().getLong(
   2189                     BugleGservicesKeys.MMS_WAP_PUSH_DEDUP_TIME_LIMIT_SECS,
   2190                     BugleGservicesKeys.MMS_WAP_PUSH_DEDUP_TIME_LIMIT_SECS_DEFAULT);
   2191             if (timeLimit > 0) {
   2192                 // New dedup algorithm
   2193                 selection = DUP_NOTIFICATION_QUERY_SELECTION;
   2194                 final long nowSecs = System.currentTimeMillis() / 1000;
   2195                 final long timeLowerBoundSecs = nowSecs - timeLimit;
   2196                 // Need upper bound to protect against clock change so that a message has a time
   2197                 // stamp in the future
   2198                 final long timeUpperBoundSecs = nowSecs + timeLimit;
   2199                 selectionArgs = new String[] {
   2200                         Integer.toString(PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND),
   2201                         Long.toString(nowSecs),
   2202                         Long.toString(timeLowerBoundSecs),
   2203                         Long.toString(timeUpperBoundSecs),
   2204                         location
   2205                 };
   2206             } else {
   2207                 // If time limit is 0, we revert back to old behavior in case the new
   2208                 // dedup algorithm behaves badly
   2209                 selection = DUP_NOTIFICATION_QUERY_SELECTION_OLD;
   2210                 selectionArgs = new String[] {
   2211                         Integer.toString(PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND),
   2212                         location
   2213                 };
   2214             }
   2215             Cursor cursor = null;
   2216             try {
   2217                 cursor = SqliteWrapper.query(
   2218                         context, context.getContentResolver(),
   2219                         Mms.CONTENT_URI, new String[] { Mms._ID },
   2220                         selection, selectionArgs, null);
   2221                 final int dupCount = cursor.getCount();
   2222                 if (dupCount > 0) {
   2223                     // We already received the same notification before.
   2224                     // Don't want to return too many dups. It is only for debugging.
   2225                     final int returnCount = dupCount < MAX_RETURN ? dupCount : MAX_RETURN;
   2226                     final String[] dups = new String[returnCount];
   2227                     for (int i = 0; cursor.moveToNext() && i < returnCount; i++) {
   2228                         dups[i] = cursor.getString(0);
   2229                     }
   2230                     return dups;
   2231                 }
   2232             } catch (final SQLiteException e) {
   2233                 LogUtil.e(TAG, "query failure: " + e, e);
   2234             } finally {
   2235                 cursor.close();
   2236             }
   2237         }
   2238         return null;
   2239     }
   2240 
   2241     /**
   2242      * Try parse the address using RFC822 format. If it fails to parse, then return the
   2243      * original address
   2244      *
   2245      * @param address The MMS ind sender address to parse
   2246      * @return The real address. If in RFC822 format, returns the correct email.
   2247      */
   2248     private static String parsePotentialRfc822EmailAddress(final String address) {
   2249         if (address == null || !address.contains("@") || !address.contains("<")) {
   2250             return address;
   2251         }
   2252         final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(address);
   2253         if (tokens != null && tokens.length > 0) {
   2254             for (final Rfc822Token token : tokens) {
   2255                 if (token != null && !TextUtils.isEmpty(token.getAddress())) {
   2256                     return token.getAddress();
   2257                 }
   2258             }
   2259         }
   2260         return address;
   2261     }
   2262 
   2263     public static DatabaseMessages.MmsMessage processReceivedPdu(final Context context,
   2264             final byte[] pushData, final int subId, final String subPhoneNumber) {
   2265         // Parse data
   2266 
   2267         // Insert placeholder row to telephony and local db
   2268         // Get raw PDU push-data from the message and parse it
   2269         final PduParser parser = new PduParser(pushData,
   2270                 MmsConfig.get(subId).getSupportMmsContentDisposition());
   2271         final GenericPdu pdu = parser.parse();
   2272 
   2273         if (null == pdu) {
   2274             LogUtil.e(TAG, "Invalid PUSH data");
   2275             return null;
   2276         }
   2277 
   2278         final PduPersister p = PduPersister.getPduPersister(context);
   2279         final int type = pdu.getMessageType();
   2280 
   2281         Uri messageUri = null;
   2282         switch (type) {
   2283             case PduHeaders.MESSAGE_TYPE_DELIVERY_IND:
   2284             case PduHeaders.MESSAGE_TYPE_READ_ORIG_IND: {
   2285                 // TODO: Should this be commented out?
   2286 //                threadId = findThreadId(context, pdu, type);
   2287 //                if (threadId == -1) {
   2288 //                    // The associated SendReq isn't found, therefore skip
   2289 //                    // processing this PDU.
   2290 //                    break;
   2291 //                }
   2292 
   2293 //                Uri uri = p.persist(pdu, Inbox.CONTENT_URI, true,
   2294 //                        MessagingPreferenceActivity.getIsGroupMmsEnabled(mContext), null);
   2295 //                // Update thread ID for ReadOrigInd & DeliveryInd.
   2296 //                ContentValues values = new ContentValues(1);
   2297 //                values.put(Mms.THREAD_ID, threadId);
   2298 //                SqliteWrapper.update(mContext, cr, uri, values, null, null);
   2299                 LogUtil.w(TAG, "Received unsupported WAP Push, type=" + type);
   2300                 break;
   2301             }
   2302             case PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND: {
   2303                 final NotificationInd nInd = (NotificationInd) pdu;
   2304 
   2305                 if (MmsConfig.get(subId).getTransIdEnabled()) {
   2306                     final byte [] contentLocationTemp = nInd.getContentLocation();
   2307                     if ('=' == contentLocationTemp[contentLocationTemp.length - 1]) {
   2308                         final byte [] transactionIdTemp = nInd.getTransactionId();
   2309                         final byte [] contentLocationWithId =
   2310                                 new byte [contentLocationTemp.length
   2311                                                                   + transactionIdTemp.length];
   2312                         System.arraycopy(contentLocationTemp, 0, contentLocationWithId,
   2313                                 0, contentLocationTemp.length);
   2314                         System.arraycopy(transactionIdTemp, 0, contentLocationWithId,
   2315                                 contentLocationTemp.length, transactionIdTemp.length);
   2316                         nInd.setContentLocation(contentLocationWithId);
   2317                     }
   2318                 }
   2319                 final String[] dups = getDupNotifications(context, nInd);
   2320                 if (dups == null) {
   2321                     // TODO: Do we handle Rfc822 Email Addresses?
   2322                     //final String contentLocation =
   2323                     //        MmsUtils.bytesToString(nInd.getContentLocation(), "UTF-8");
   2324                     //final byte[] transactionId = nInd.getTransactionId();
   2325                     //final long messageSize = nInd.getMessageSize();
   2326                     //final long expiry = nInd.getExpiry();
   2327                     //final String transactionIdString =
   2328                     //        MmsUtils.bytesToString(transactionId, "UTF-8");
   2329 
   2330                     //final EncodedStringValue fromEncoded = nInd.getFrom();
   2331                     // An mms ind received from email address will have from address shown as
   2332                     // "John Doe <johndoe (at) foobar.com>" but the actual received message will only
   2333                     // have the email address. So let's try to parse the RFC822 format to get the
   2334                     // real email. Otherwise we will create two conversations for the MMS
   2335                     // notification and the actual MMS message if auto retrieve is disabled.
   2336                     //final String from = parsePotentialRfc822EmailAddress(
   2337                     //        fromEncoded != null ? fromEncoded.getString() : null);
   2338 
   2339                     Uri inboxUri = null;
   2340                     try {
   2341                         inboxUri = p.persist(pdu, Mms.Inbox.CONTENT_URI, subId, subPhoneNumber,
   2342                                 null);
   2343                         messageUri = ContentUris.withAppendedId(Mms.CONTENT_URI,
   2344                                 ContentUris.parseId(inboxUri));
   2345                     } catch (final MmsException e) {
   2346                         LogUtil.e(TAG, "Failed to save the data from PUSH: type=" + type, e);
   2347                     }
   2348                 } else {
   2349                     LogUtil.w(TAG, "Received WAP Push is a dup: " + Joiner.on(',').join(dups));
   2350                     if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
   2351                         LogUtil.w(TAG, "Dup WAP Push url=" + new String(nInd.getContentLocation()));
   2352                     }
   2353                 }
   2354                 break;
   2355             }
   2356             default:
   2357                 LogUtil.e(TAG, "Received unrecognized WAP Push, type=" + type);
   2358         }
   2359 
   2360         DatabaseMessages.MmsMessage mms = null;
   2361         if (messageUri != null) {
   2362             mms = MmsUtils.loadMms(messageUri);
   2363         }
   2364         return mms;
   2365     }
   2366 
   2367     public static Uri insertSendingMmsMessage(final Context context, final List<String> recipients,
   2368             final MessageData content, final int subId, final String subPhoneNumber,
   2369             final long timestamp) {
   2370         final SendReq sendReq = createMmsSendReq(
   2371                 context, subId, recipients.toArray(new String[recipients.size()]), content,
   2372                 DEFAULT_DELIVERY_REPORT_MODE,
   2373                 DEFAULT_READ_REPORT_MODE,
   2374                 DEFAULT_EXPIRY_TIME_IN_SECONDS,
   2375                 DEFAULT_PRIORITY,
   2376                 timestamp);
   2377         Uri messageUri = null;
   2378         if (sendReq != null) {
   2379             final Uri outboxUri = MmsUtils.insertSendReq(context, sendReq, subId, subPhoneNumber);
   2380             if (outboxUri != null) {
   2381                 messageUri = ContentUris.withAppendedId(Telephony.Mms.CONTENT_URI,
   2382                         ContentUris.parseId(outboxUri));
   2383                 if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
   2384                     LogUtil.d(TAG, "Mmsutils: Inserted sending MMS message into telephony, uri: "
   2385                             + outboxUri);
   2386                 }
   2387             } else {
   2388                 LogUtil.e(TAG, "insertSendingMmsMessage: failed to persist message into telephony");
   2389             }
   2390         }
   2391         return messageUri;
   2392     }
   2393 
   2394     public static MessageData readSendingMmsMessage(final Uri messageUri,
   2395             final String conversationId, final String participantId, final String selfId) {
   2396         MessageData message = null;
   2397         if (messageUri != null) {
   2398             final DatabaseMessages.MmsMessage mms = MmsUtils.loadMms(messageUri);
   2399 
   2400             // Make sure that the message has not been deleted from the Telephony DB
   2401             if (mms != null) {
   2402                 // Transform the message
   2403                 message = MmsUtils.createMmsMessage(mms, conversationId, participantId, selfId,
   2404                         MessageData.BUGLE_STATUS_OUTGOING_RESENDING);
   2405             }
   2406         }
   2407         return message;
   2408     }
   2409 
   2410     /**
   2411      * Create an MMS message with subject, text and image
   2412      *
   2413      * @return Both the M-Send.req and the M-Send.conf for processing in the caller
   2414      * @throws MmsException
   2415      */
   2416     private static SendReq createMmsSendReq(final Context context, final int subId,
   2417             final String[] recipients, final MessageData message,
   2418             final boolean requireDeliveryReport, final boolean requireReadReport,
   2419             final long expiryTime, final int priority, final long timestampMillis) {
   2420         Assert.notNull(context);
   2421         if (recipients == null || recipients.length < 1) {
   2422             throw new IllegalArgumentException("MMS sendReq no recipient");
   2423         }
   2424 
   2425         // Make a copy so we don't propagate changes to recipients to outside of this method
   2426         final String[] recipientsCopy = new String[recipients.length];
   2427         // Don't send phone number as is since some received phone number is malformed
   2428         // for sending. We need to strip the separators.
   2429         for (int i = 0; i < recipients.length; i++) {
   2430             final String recipient = recipients[i];
   2431             if (EmailAddress.isValidEmail(recipients[i])) {
   2432                 // Don't do stripping for emails
   2433                 recipientsCopy[i] = recipient;
   2434             } else {
   2435                 recipientsCopy[i] = stripPhoneNumberSeparators(recipient);
   2436             }
   2437         }
   2438 
   2439         SendReq sendReq = null;
   2440         try {
   2441             sendReq = createSendReq(context, subId, recipientsCopy,
   2442                     message, requireDeliveryReport,
   2443                     requireReadReport, expiryTime, priority, timestampMillis);
   2444         } catch (final InvalidHeaderValueException e) {
   2445             LogUtil.e(TAG, "InvalidHeaderValue creating sendReq PDU");
   2446         } catch (final OutOfMemoryError e) {
   2447             LogUtil.e(TAG, "Out of memory error creating sendReq PDU");
   2448         }
   2449         return sendReq;
   2450     }
   2451 
   2452     /**
   2453      * Stripping out the invalid characters in a phone number before sending
   2454      * MMS. We only keep alphanumeric and '*', '#', '+'.
   2455      */
   2456     private static String stripPhoneNumberSeparators(final String phoneNumber) {
   2457         if (phoneNumber == null) {
   2458             return null;
   2459         }
   2460         final int len = phoneNumber.length();
   2461         final StringBuilder ret = new StringBuilder(len);
   2462         for (int i = 0; i < len; i++) {
   2463             final char c = phoneNumber.charAt(i);
   2464             if (Character.isLetterOrDigit(c) || c == '+' || c == '*' || c == '#') {
   2465                 ret.append(c);
   2466             }
   2467         }
   2468         return ret.toString();
   2469     }
   2470 
   2471     /**
   2472      * Create M-Send.req for the MMS message to be sent.
   2473      *
   2474      * @return the M-Send.req
   2475      * @throws InvalidHeaderValueException if there is any error in parsing the input
   2476      */
   2477     static SendReq createSendReq(final Context context, final int subId,
   2478             final String[] recipients, final MessageData message,
   2479             final boolean requireDeliveryReport,
   2480             final boolean requireReadReport, final long expiryTime, final int priority,
   2481             final long timestampMillis)
   2482             throws InvalidHeaderValueException {
   2483         final SendReq req = new SendReq();
   2484         // From, per spec
   2485         final String lineNumber = PhoneUtils.get(subId).getCanonicalForSelf(true/*allowOverride*/);
   2486         if (!TextUtils.isEmpty(lineNumber)) {
   2487             req.setFrom(new EncodedStringValue(lineNumber));
   2488         }
   2489         // To
   2490         final EncodedStringValue[] encodedNumbers = EncodedStringValue.encodeStrings(recipients);
   2491         if (encodedNumbers != null) {
   2492             req.setTo(encodedNumbers);
   2493         }
   2494         // Subject
   2495         if (!TextUtils.isEmpty(message.getMmsSubject())) {
   2496             req.setSubject(new EncodedStringValue(message.getMmsSubject()));
   2497         }
   2498         // Date
   2499         req.setDate(timestampMillis / 1000L);
   2500         // Body
   2501         final MmsInfo bodyInfo = MmsUtils.makePduBody(context, message, subId);
   2502         req.setBody(bodyInfo.mPduBody);
   2503         // Message size
   2504         req.setMessageSize(bodyInfo.mMessageSize);
   2505         // Message class
   2506         req.setMessageClass(PduHeaders.MESSAGE_CLASS_PERSONAL_STR.getBytes());
   2507         // Expiry
   2508         req.setExpiry(expiryTime);
   2509         // Priority
   2510         req.setPriority(priority);
   2511         // Delivery report
   2512         req.setDeliveryReport(requireDeliveryReport ? PduHeaders.VALUE_YES : PduHeaders.VALUE_NO);
   2513         // Read report
   2514         req.setReadReport(requireReadReport ? PduHeaders.VALUE_YES : PduHeaders.VALUE_NO);
   2515         return req;
   2516     }
   2517 
   2518     public static boolean isDeliveryReportRequired(final int subId) {
   2519         if (!MmsConfig.get(subId).getSMSDeliveryReportsEnabled()) {
   2520             return false;
   2521         }
   2522         final Context context = Factory.get().getApplicationContext();
   2523         final Resources res = context.getResources();
   2524         final BuglePrefs prefs = BuglePrefs.getSubscriptionPrefs(subId);
   2525         final String deliveryReportKey = res.getString(R.string.delivery_reports_pref_key);
   2526         final boolean defaultValue = res.getBoolean(R.bool.delivery_reports_pref_default);
   2527         return prefs.getBoolean(deliveryReportKey, defaultValue);
   2528     }
   2529 
   2530     public static int sendSmsMessage(final String recipient, final String messageText,
   2531             final Uri requestUri, final int subId,
   2532             final String smsServiceCenter, final boolean requireDeliveryReport) {
   2533         if (!isSmsDataAvailable(subId)) {
   2534             LogUtil.w(TAG, "MmsUtils: can't send SMS without radio");
   2535             return MMS_REQUEST_MANUAL_RETRY;
   2536         }
   2537         final Context context = Factory.get().getApplicationContext();
   2538         int status = MMS_REQUEST_MANUAL_RETRY;
   2539         try {
   2540             // Send a single message
   2541             final SendResult result = SmsSender.sendMessage(
   2542                     context,
   2543                     subId,
   2544                     recipient,
   2545                     messageText,
   2546                     smsServiceCenter,
   2547                     requireDeliveryReport,
   2548                     requestUri);
   2549             if (!result.hasPending()) {
   2550                 // not timed out, check failures
   2551                 final int failureLevel = result.getHighestFailureLevel();
   2552                 switch (failureLevel) {
   2553                     case SendResult.FAILURE_LEVEL_NONE:
   2554                         status = MMS_REQUEST_SUCCEEDED;
   2555                         break;
   2556                     case SendResult.FAILURE_LEVEL_TEMPORARY:
   2557                         status = MMS_REQUEST_AUTO_RETRY;
   2558                         LogUtil.e(TAG, "MmsUtils: SMS temporary failure");
   2559                         break;
   2560                     case SendResult.FAILURE_LEVEL_PERMANENT:
   2561                         LogUtil.e(TAG, "MmsUtils: SMS permanent failure");
   2562                         break;
   2563                 }
   2564             } else {
   2565                 // Timed out
   2566                 LogUtil.e(TAG, "MmsUtils: sending SMS timed out");
   2567             }
   2568         } catch (final Exception e) {
   2569             LogUtil.e(TAG, "MmsUtils: failed to send SMS " + e, e);
   2570         }
   2571         return status;
   2572     }
   2573 
   2574     /**
   2575      * Delete SMS and MMS messages in a particular thread
   2576      *
   2577      * @return the number of messages deleted
   2578      */
   2579     public static int deleteThread(final long threadId, final long cutOffTimestampInMillis) {
   2580         final ContentResolver resolver = Factory.get().getApplicationContext().getContentResolver();
   2581         final Uri threadUri = ContentUris.withAppendedId(Telephony.Threads.CONTENT_URI, threadId);
   2582         if (cutOffTimestampInMillis < Long.MAX_VALUE) {
   2583             return resolver.delete(threadUri, Sms.DATE + "<=?",
   2584                     new String[] { Long.toString(cutOffTimestampInMillis) });
   2585         } else {
   2586             return resolver.delete(threadUri, null /* smsSelection */, null /* selectionArgs */);
   2587         }
   2588     }
   2589 
   2590     /**
   2591      * Delete single SMS and MMS message
   2592      *
   2593      * @return number of rows deleted (should be 1 or 0)
   2594      */
   2595     public static int deleteMessage(final Uri messageUri) {
   2596         final ContentResolver resolver = Factory.get().getApplicationContext().getContentResolver();
   2597         return resolver.delete(messageUri, null /* selection */, null /* selectionArgs */);
   2598     }
   2599 
   2600     public static byte[] createDebugNotificationInd(final String fileName) {
   2601         byte[] pduData = null;
   2602         try {
   2603             final Context context = Factory.get().getApplicationContext();
   2604             // Load the message file
   2605             final byte[] data = DebugUtils.receiveFromDumpFile(fileName);
   2606             final RetrieveConf retrieveConf = receiveFromDumpFile(data);
   2607             // Create the notification
   2608             final NotificationInd notification = new NotificationInd();
   2609             final long expiry = System.currentTimeMillis() / 1000 + 600;
   2610             notification.setTransactionId(fileName.getBytes());
   2611             notification.setMmsVersion(retrieveConf.getMmsVersion());
   2612             notification.setFrom(retrieveConf.getFrom());
   2613             notification.setSubject(retrieveConf.getSubject());
   2614             notification.setExpiry(expiry);
   2615             notification.setMessageSize(data.length);
   2616             notification.setMessageClass(retrieveConf.getMessageClass());
   2617 
   2618             final Uri.Builder builder = MediaScratchFileProvider.getUriBuilder();
   2619             builder.appendPath(fileName);
   2620             final Uri contentLocation = builder.build();
   2621             notification.setContentLocation(contentLocation.toString().getBytes());
   2622 
   2623             // Serialize
   2624             pduData = new PduComposer(context, notification).make();
   2625             if (pduData == null || pduData.length < 1) {
   2626                 throw new IllegalArgumentException("Empty or zero length PDU data");
   2627             }
   2628         } catch (final MmsFailureException e) {
   2629             // Nothing to do
   2630         } catch (final InvalidHeaderValueException e) {
   2631             // Nothing to do
   2632         }
   2633         return pduData;
   2634     }
   2635 
   2636     public static int mapRawStatusToErrorResourceId(final int bugleStatus, final int rawStatus) {
   2637         int stringResId = R.string.message_status_send_failed;
   2638         switch (rawStatus) {
   2639             case PduHeaders.RESPONSE_STATUS_ERROR_SERVICE_DENIED:
   2640             case PduHeaders.RESPONSE_STATUS_ERROR_PERMANENT_SERVICE_DENIED:
   2641             //case PduHeaders.RESPONSE_STATUS_ERROR_PERMANENT_REPLY_CHARGING_LIMITATIONS_NOT_MET:
   2642             //case PduHeaders.RESPONSE_STATUS_ERROR_PERMANENT_REPLY_CHARGING_REQUEST_NOT_ACCEPTED:
   2643             //case PduHeaders.RESPONSE_STATUS_ERROR_PERMANENT_REPLY_CHARGING_FORWARDING_DENIED:
   2644             //case PduHeaders.RESPONSE_STATUS_ERROR_PERMANENT_REPLY_CHARGING_NOT_SUPPORTED:
   2645             //case PduHeaders.RESPONSE_STATUS_ERROR_PERMANENT_ADDRESS_HIDING_NOT_SUPPORTED:
   2646             //case PduHeaders.RESPONSE_STATUS_ERROR_PERMANENT_LACK_OF_PREPAID:
   2647                 stringResId = R.string.mms_failure_outgoing_service;
   2648                 break;
   2649             case PduHeaders.RESPONSE_STATUS_ERROR_SENDING_ADDRESS_UNRESOLVED:
   2650             case PduHeaders.RESPONSE_STATUS_ERROR_TRANSIENT_SENDNG_ADDRESS_UNRESOLVED:
   2651             case PduHeaders.RESPONSE_STATUS_ERROR_PERMANENT_SENDING_ADDRESS_UNRESOLVED:
   2652                 stringResId = R.string.mms_failure_outgoing_address;
   2653                 break;
   2654             case PduHeaders.RESPONSE_STATUS_ERROR_MESSAGE_FORMAT_CORRUPT:
   2655             case PduHeaders.RESPONSE_STATUS_ERROR_PERMANENT_MESSAGE_FORMAT_CORRUPT:
   2656                 stringResId = R.string.mms_failure_outgoing_corrupt;
   2657                 break;
   2658             case PduHeaders.RESPONSE_STATUS_ERROR_CONTENT_NOT_ACCEPTED:
   2659             case PduHeaders.RESPONSE_STATUS_ERROR_PERMANENT_CONTENT_NOT_ACCEPTED:
   2660                 stringResId = R.string.mms_failure_outgoing_content;
   2661                 break;
   2662             case PduHeaders.RESPONSE_STATUS_ERROR_UNSUPPORTED_MESSAGE:
   2663             //case PduHeaders.RESPONSE_STATUS_ERROR_MESSAGE_NOT_FOUND:
   2664             //case PduHeaders.RESPONSE_STATUS_ERROR_TRANSIENT_MESSAGE_NOT_FOUND:
   2665                 stringResId = R.string.mms_failure_outgoing_unsupported;
   2666                 break;
   2667             case MessageData.RAW_TELEPHONY_STATUS_MESSAGE_TOO_BIG:
   2668                 stringResId = R.string.mms_failure_outgoing_too_large;
   2669                 break;
   2670         }
   2671         return stringResId;
   2672     }
   2673 
   2674     /**
   2675      * The absence of a connection type.
   2676      */
   2677     public static final int TYPE_NONE = -1;
   2678 
   2679     public static int getConnectivityEventNetworkType(final Context context, final Intent intent) {
   2680         final ConnectivityManager connMgr = (ConnectivityManager)
   2681                 context.getSystemService(Context.CONNECTIVITY_SERVICE);
   2682         if (OsUtil.isAtLeastJB_MR1()) {
   2683             return intent.getIntExtra(ConnectivityManager.EXTRA_NETWORK_TYPE, TYPE_NONE);
   2684         } else {
   2685             final NetworkInfo info = (NetworkInfo) intent.getParcelableExtra(
   2686                     ConnectivityManager.EXTRA_NETWORK_INFO);
   2687             if (info != null) {
   2688                 return info.getType();
   2689             }
   2690         }
   2691         return TYPE_NONE;
   2692     }
   2693 
   2694     /**
   2695      * Dump the raw MMS data into a file
   2696      *
   2697      * @param rawPdu The raw pdu data
   2698      * @param pdu The parsed pdu, used to construct a dump file name
   2699      */
   2700     public static void dumpPdu(final byte[] rawPdu, final GenericPdu pdu) {
   2701         if (rawPdu == null || rawPdu.length < 1) {
   2702             return;
   2703         }
   2704         final String dumpFileName = MmsUtils.MMS_DUMP_PREFIX + getDumpFileId(pdu);
   2705         final File dumpFile = DebugUtils.getDebugFile(dumpFileName, true);
   2706         if (dumpFile != null) {
   2707             try {
   2708                 final FileOutputStream fos = new FileOutputStream(dumpFile);
   2709                 final BufferedOutputStream bos = new BufferedOutputStream(fos);
   2710                 try {
   2711                     bos.write(rawPdu);
   2712                     bos.flush();
   2713                 } finally {
   2714                     bos.close();
   2715                 }
   2716                 DebugUtils.ensureReadable(dumpFile);
   2717             } catch (final IOException e) {
   2718                 LogUtil.e(TAG, "dumpPdu: " + e, e);
   2719             }
   2720         }
   2721     }
   2722 
   2723     /**
   2724      * Get the dump file id based on the parsed PDU
   2725      * 1. Use message id if not empty
   2726      * 2. Use transaction id if message id is empty
   2727      * 3. If all above is empty, use random UUID
   2728      *
   2729      * @param pdu the parsed PDU
   2730      * @return the id of the dump file
   2731      */
   2732     private static String getDumpFileId(final GenericPdu pdu) {
   2733         String fileId = null;
   2734         if (pdu != null && pdu instanceof RetrieveConf) {
   2735             final RetrieveConf retrieveConf = (RetrieveConf) pdu;
   2736             if (retrieveConf.getMessageId() != null) {
   2737                 fileId = new String(retrieveConf.getMessageId());
   2738             } else if (retrieveConf.getTransactionId() != null) {
   2739                 fileId = new String(retrieveConf.getTransactionId());
   2740             }
   2741         }
   2742         if (TextUtils.isEmpty(fileId)) {
   2743             fileId = UUID.randomUUID().toString();
   2744         }
   2745         return fileId;
   2746     }
   2747 }
   2748