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