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