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