• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
getRequestStatusDescription(final int status)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      */
getSmsTypeSelectionSql()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      */
getMmsTypeSelectionSql()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 
makePduBody(final Context context, final MessageData message, final int subId)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 
getMediaDurationMs(final Context context, final MessagePartData part, final int defaultDurationMs)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 
setPartContentLocationAndId(final PduPart part, final String srcName)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 
addTextPart(final Context context, final PduBody pb, final String text, final String srcName)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 
addPicturePart(final Context context, final PduBody pb, final int index, final MessagePartData messagePart, int widthLimit, int heightLimit, final int maxPartSize, final String srcName, final String contentType)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 
addPartForUri(final Context context, final PduBody pb, final String srcName, final Uri uri, final String contentType)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 
addVCardPart(final Context context, final PduBody pb, final MessagePartData messagePart, final String srcName)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      */
addVideoPart(final Context context, final PduBody pb, final MessagePartData messagePart, final String srcName)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 
addOtherPart(final Context context, final PduBody pb, final MessagePartData messagePart, final String srcName)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 
addSmilPart(final PduBody pb, final String smilTemplate, final String smilBody)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 
getSmilTemplate(final boolean hasVisualAttachments, final boolean hasNonVisualAttachments, final boolean hasText)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 
getDataLength(final Context context, final Uri uri)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      */
groupMmsEnabled(final int subId)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      */
getResizedImageAsPart(final int widthLimit, final int heightLimit, final int byteLimit, final int width, final int height, final int orientation, final Uri imageUri, final Context context, final String contentType)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      */
getMediaFileSize(final Uri uri)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 
getRecipientsByThread(final long threadId)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!
getRawRecipientIdsForThread(final long threadId)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 
getAddresses(final Context context, final String spaceSepIds)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
getOrCreateSmsThreadId(final Context context, final String dest)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
getOrCreateThreadId(final Context context, final List<String> dests)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      */
addMessageToUri(final ContentResolver resolver, final Uri uri, final int subId, final String address, final String body, final String subject, final Long date, final boolean read, final boolean seen, final int status, final int type, final long threadId)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
insertSmsMessage(final Context context, final Uri uri, final int subId, final String dest, final String text, final long timestamp, final int status, final int type, final long threadId)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.
updateSmsMessageSendingStatus(final Context context, final Uri uri, final int type, final long date)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
insertSendReq(final Context context, final GenericPdu pdu, final int subId, final String subPhoneNumber)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
insertReceivedMmsMessage(final Context context, final RetrieveConf retrieveConf, final int subId, final String subPhoneNumber, final long receivedTimestampInSeconds, final String contentLocation)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.
updateMmsMessageSendingStatus(final Context context, final Uri uri, final int box, final long timestampInMillis)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      */
parseReceivedSmsMessage( final Context context, final SmsMessage[] msgs, final int error)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.
replaceFormFeeds(final String s)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
buildMessageBodyFromPdus(final SmsMessage[] msgs)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
getMessageDate(final SmsMessage sms, long now)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      */
cleanseMmsSubject(final Resources resources, final String subject)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.
getSmsRecipients(final Uri 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      */
replaceUnicodeDigits(final String number)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      */
isDataRoamingEnabled()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      */
allowMmsAutoRetrieve(final int subId)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      */
parseRowIdFromMessageUri(final Uri messageUri)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 
getSmsMessageFromDeliveryReport(final Intent intent)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      */
updateSmsStatusAndDateSent(final Uri smsMessageUri, final int status, final long timeSentInMillis)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      */
getMediaTypeSelectionSql(final String contentTypeColumn)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      */
deleteMediaMessages()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      */
getSqlInOperand(final int count)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      */
getSqlInOperandArgs( final long[] ids, final int start, final int count)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      */
deleteMessagesOlderThan(final long cutOffTimestampInMillis)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      */
updateSmsReadStatus(final long threadId, final long timestampInMillis)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      */
updateReadStatusForMmsMessage(final Uri mmsUri, final boolean read)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      */
bytesToString(final byte[] bytes, final String charsetName)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      */
stringToBytes(final String string, final String charsetName)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      */
hasSmsDateSentColumn()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      */
useSystemApnTable()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
setUseSystemApnTable(final boolean turnOn)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      */
isDumpSmsEnabled()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      */
isDumpMmsEnabled()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      */
getDumpSmsOrMmsPref(final int prefKeyRes, final int defaultKeyRes)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      */
loadMms(final Uri mmsUri)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      */
getMmsSender(final List<String> recipients, final String mmsUri)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 
bugleStatusForMms(final boolean isOutgoing, final boolean isNotification, final int messageBox)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 
createMmsMessage(final DatabaseMessages.MmsMessage mms, final String conversationId, final String participantId, final String selfId, final int bugleStatus)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 
createMmsMessagePart(final DatabaseMessages.MmsPart part)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 
StatusPlusUri(final int status, final int rawStatus, final Uri uri)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 
StatusPlusUri(final int status, final int rawStatus, final Uri uri, final int resultCode)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 
SendReqResp(final SendReq sendReq, final SendConf sendConf)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 
downloadMmsMessage(final Context context, final Uri notificationUri, final int subId, final String subPhoneNumber, final String transactionId, final String contentLocation, final boolean autoDownload, final long receivedTimestampInSeconds, Bundle extras)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 
insertDownloadedMessageAndSendResponse(final Context context, final Uri notificationUri, final int subId, final String subPhoneNumber, final String transactionId, final String contentLocation, final boolean autoDownload, final long receivedTimestampInSeconds, final RetrieveConf retrieveConf)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      */
sendNotifyResponseForMmsDownload(final Context context, final int subId, final byte[] transactionId, final String contentLocation, final int status)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      */
sendAcknowledgeForMmsDownload(final Context context, final int subId, final byte[] transactionId, final String contentLocation)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      */
parsePduForAnyCarrier(final byte[] data)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 
receiveFromDumpFile(final byte[] data)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 
isMmsDataAvailable(final int subId)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 
isSmsDataAvailable(final int subId)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 
isMobileDataEnabled(final int subId)2077     public static boolean isMobileDataEnabled(final int subId) {
2078         final PhoneUtils phoneUtils = PhoneUtils.get(subId);
2079         return phoneUtils.isMobileDataEnabled();
2080     }
2081 
isAirplaneModeOn(final int subId)2082     public static boolean isAirplaneModeOn(final int subId) {
2083         final PhoneUtils phoneUtils = PhoneUtils.get(subId);
2084         return phoneUtils.isAirplaneModeOn();
2085     }
2086 
sendMmsMessage(final Context context, final int subId, final Uri messageUri, final Bundle extras)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 
updateSentMmsMessageStatus(final Context context, final Uri messageUri, final SendConf sendConf)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 
clearMmsStatus(final Context context, final Uri uri)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;
getDupNotifications(final Context context, final NotificationInd nInd)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@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