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