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