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