1 /* 2 * Copyright (C) 2008 Esmertec AG. 3 * Copyright (C) 2008 The Android Open Source Project 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18 package com.android.mms.ui; 19 20 import com.android.mms.MmsApp; 21 import com.android.mms.MmsConfig; 22 import com.android.mms.R; 23 import com.android.mms.LogTag; 24 import com.android.mms.TempFileProvider; 25 import com.android.mms.data.WorkingMessage; 26 import com.android.mms.model.MediaModel; 27 import com.android.mms.model.SlideModel; 28 import com.android.mms.model.SlideshowModel; 29 import com.android.mms.transaction.MmsMessageSender; 30 import com.android.mms.util.AddressUtils; 31 import com.google.android.mms.ContentType; 32 import com.google.android.mms.MmsException; 33 import com.google.android.mms.pdu.CharacterSets; 34 import com.google.android.mms.pdu.EncodedStringValue; 35 import com.google.android.mms.pdu.MultimediaMessagePdu; 36 import com.google.android.mms.pdu.NotificationInd; 37 import com.google.android.mms.pdu.PduBody; 38 import com.google.android.mms.pdu.PduHeaders; 39 import com.google.android.mms.pdu.PduPart; 40 import com.google.android.mms.pdu.PduPersister; 41 import com.google.android.mms.pdu.RetrieveConf; 42 import com.google.android.mms.pdu.SendReq; 43 import android.database.sqlite.SqliteWrapper; 44 45 import android.app.Activity; 46 import android.app.AlertDialog; 47 import android.content.ContentUris; 48 import android.content.Context; 49 import android.content.DialogInterface; 50 import android.content.Intent; 51 import android.content.DialogInterface.OnCancelListener; 52 import android.content.DialogInterface.OnClickListener; 53 import android.content.res.Resources; 54 import android.database.Cursor; 55 import android.media.CamcorderProfile; 56 import android.media.RingtoneManager; 57 import android.net.Uri; 58 import android.os.Environment; 59 import android.os.Handler; 60 import android.provider.MediaStore; 61 import android.provider.Telephony.Mms; 62 import android.provider.Telephony.Sms; 63 import android.telephony.PhoneNumberUtils; 64 import android.text.TextUtils; 65 import android.text.format.DateUtils; 66 import android.text.format.Time; 67 import android.text.style.URLSpan; 68 import android.util.Log; 69 import android.widget.Toast; 70 71 import java.io.IOException; 72 import java.util.ArrayList; 73 import java.util.Collection; 74 import java.util.HashMap; 75 import java.util.Map; 76 import java.util.concurrent.ConcurrentHashMap; 77 78 /** 79 * An utility class for managing messages. 80 */ 81 public class MessageUtils { 82 interface ResizeImageResultCallback { 83 void onResizeResult(PduPart part, boolean append); 84 } 85 86 private static final String TAG = LogTag.TAG; 87 private static String sLocalNumber; 88 89 // Cache of both groups of space-separated ids to their full 90 // comma-separated display names, as well as individual ids to 91 // display names. 92 // TODO: is it possible for canonical address ID keys to be 93 // re-used? SQLite does reuse IDs on NULL id_ insert, but does 94 // anything ever delete from the mmssms.db canonical_addresses 95 // table? Nothing that I could find. 96 private static final Map<String, String> sRecipientAddress = 97 new ConcurrentHashMap<String, String>(20 /* initial capacity */); 98 99 100 /** 101 * MMS address parsing data structures 102 */ 103 // allowable phone number separators 104 private static final char[] NUMERIC_CHARS_SUGAR = { 105 '-', '.', ',', '(', ')', ' ', '/', '\\', '*', '#', '+' 106 }; 107 108 private static HashMap numericSugarMap = new HashMap (NUMERIC_CHARS_SUGAR.length); 109 110 static { 111 for (int i = 0; i < NUMERIC_CHARS_SUGAR.length; i++) { 112 numericSugarMap.put(NUMERIC_CHARS_SUGAR[i], NUMERIC_CHARS_SUGAR[i]); 113 } 114 } 115 116 117 private MessageUtils() { 118 // Forbidden being instantiated. 119 } 120 121 public static String getMessageDetails(Context context, Cursor cursor, int size) { 122 if (cursor == null) { 123 return null; 124 } 125 126 if ("mms".equals(cursor.getString(MessageListAdapter.COLUMN_MSG_TYPE))) { 127 int type = cursor.getInt(MessageListAdapter.COLUMN_MMS_MESSAGE_TYPE); 128 switch (type) { 129 case PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND: 130 return getNotificationIndDetails(context, cursor); 131 case PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF: 132 case PduHeaders.MESSAGE_TYPE_SEND_REQ: 133 return getMultimediaMessageDetails(context, cursor, size); 134 default: 135 Log.w(TAG, "No details could be retrieved."); 136 return ""; 137 } 138 } else { 139 return getTextMessageDetails(context, cursor); 140 } 141 } 142 143 private static String getNotificationIndDetails(Context context, Cursor cursor) { 144 StringBuilder details = new StringBuilder(); 145 Resources res = context.getResources(); 146 147 long id = cursor.getLong(MessageListAdapter.COLUMN_ID); 148 Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, id); 149 NotificationInd nInd; 150 151 try { 152 nInd = (NotificationInd) PduPersister.getPduPersister( 153 context).load(uri); 154 } catch (MmsException e) { 155 Log.e(TAG, "Failed to load the message: " + uri, e); 156 return context.getResources().getString(R.string.cannot_get_details); 157 } 158 159 // Message Type: Mms Notification. 160 details.append(res.getString(R.string.message_type_label)); 161 details.append(res.getString(R.string.multimedia_notification)); 162 163 // From: *** 164 String from = extractEncStr(context, nInd.getFrom()); 165 details.append('\n'); 166 details.append(res.getString(R.string.from_label)); 167 details.append(!TextUtils.isEmpty(from)? from: 168 res.getString(R.string.hidden_sender_address)); 169 170 // Date: *** 171 details.append('\n'); 172 details.append(res.getString( 173 R.string.expire_on, 174 MessageUtils.formatTimeStampString( 175 context, nInd.getExpiry() * 1000L, true))); 176 177 // Subject: *** 178 details.append('\n'); 179 details.append(res.getString(R.string.subject_label)); 180 181 EncodedStringValue subject = nInd.getSubject(); 182 if (subject != null) { 183 details.append(subject.getString()); 184 } 185 186 // Message class: Personal/Advertisement/Infomational/Auto 187 details.append('\n'); 188 details.append(res.getString(R.string.message_class_label)); 189 details.append(new String(nInd.getMessageClass())); 190 191 // Message size: *** KB 192 details.append('\n'); 193 details.append(res.getString(R.string.message_size_label)); 194 details.append(String.valueOf((nInd.getMessageSize() + 1023) / 1024)); 195 details.append(context.getString(R.string.kilobyte)); 196 197 return details.toString(); 198 } 199 200 private static String getMultimediaMessageDetails( 201 Context context, Cursor cursor, int size) { 202 int type = cursor.getInt(MessageListAdapter.COLUMN_MMS_MESSAGE_TYPE); 203 if (type == PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND) { 204 return getNotificationIndDetails(context, cursor); 205 } 206 207 StringBuilder details = new StringBuilder(); 208 Resources res = context.getResources(); 209 210 long id = cursor.getLong(MessageListAdapter.COLUMN_ID); 211 Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, id); 212 MultimediaMessagePdu msg; 213 214 try { 215 msg = (MultimediaMessagePdu) PduPersister.getPduPersister( 216 context).load(uri); 217 } catch (MmsException e) { 218 Log.e(TAG, "Failed to load the message: " + uri, e); 219 return context.getResources().getString(R.string.cannot_get_details); 220 } 221 222 // Message Type: Text message. 223 details.append(res.getString(R.string.message_type_label)); 224 details.append(res.getString(R.string.multimedia_message)); 225 226 if (msg instanceof RetrieveConf) { 227 // From: *** 228 String from = extractEncStr(context, ((RetrieveConf) msg).getFrom()); 229 details.append('\n'); 230 details.append(res.getString(R.string.from_label)); 231 details.append(!TextUtils.isEmpty(from)? from: 232 res.getString(R.string.hidden_sender_address)); 233 } 234 235 // To: *** 236 details.append('\n'); 237 details.append(res.getString(R.string.to_address_label)); 238 EncodedStringValue[] to = msg.getTo(); 239 if (to != null) { 240 details.append(EncodedStringValue.concat(to)); 241 } 242 else { 243 Log.w(TAG, "recipient list is empty!"); 244 } 245 246 247 // Bcc: *** 248 if (msg instanceof SendReq) { 249 EncodedStringValue[] values = ((SendReq) msg).getBcc(); 250 if ((values != null) && (values.length > 0)) { 251 details.append('\n'); 252 details.append(res.getString(R.string.bcc_label)); 253 details.append(EncodedStringValue.concat(values)); 254 } 255 } 256 257 // Date: *** 258 details.append('\n'); 259 int msgBox = cursor.getInt(MessageListAdapter.COLUMN_MMS_MESSAGE_BOX); 260 if (msgBox == Mms.MESSAGE_BOX_DRAFTS) { 261 details.append(res.getString(R.string.saved_label)); 262 } else if (msgBox == Mms.MESSAGE_BOX_INBOX) { 263 details.append(res.getString(R.string.received_label)); 264 } else { 265 details.append(res.getString(R.string.sent_label)); 266 } 267 268 details.append(MessageUtils.formatTimeStampString( 269 context, msg.getDate() * 1000L, true)); 270 271 // Subject: *** 272 details.append('\n'); 273 details.append(res.getString(R.string.subject_label)); 274 275 EncodedStringValue subject = msg.getSubject(); 276 if (subject != null) { 277 String subStr = subject.getString(); 278 // Message size should include size of subject. 279 size += subStr.length(); 280 details.append(subStr); 281 } 282 283 // Priority: High/Normal/Low 284 details.append('\n'); 285 details.append(res.getString(R.string.priority_label)); 286 details.append(getPriorityDescription(context, msg.getPriority())); 287 288 // Message size: *** KB 289 details.append('\n'); 290 details.append(res.getString(R.string.message_size_label)); 291 details.append((size - 1)/1000 + 1); 292 details.append(" KB"); 293 294 return details.toString(); 295 } 296 297 private static String getTextMessageDetails(Context context, Cursor cursor) { 298 Log.d(TAG, "getTextMessageDetails"); 299 300 StringBuilder details = new StringBuilder(); 301 Resources res = context.getResources(); 302 303 // Message Type: Text message. 304 details.append(res.getString(R.string.message_type_label)); 305 details.append(res.getString(R.string.text_message)); 306 307 // Address: *** 308 details.append('\n'); 309 int smsType = cursor.getInt(MessageListAdapter.COLUMN_SMS_TYPE); 310 if (Sms.isOutgoingFolder(smsType)) { 311 details.append(res.getString(R.string.to_address_label)); 312 } else { 313 details.append(res.getString(R.string.from_label)); 314 } 315 details.append(cursor.getString(MessageListAdapter.COLUMN_SMS_ADDRESS)); 316 317 // Sent: *** 318 if (smsType == Sms.MESSAGE_TYPE_INBOX) { 319 long date_sent = cursor.getLong(MessageListAdapter.COLUMN_SMS_DATE_SENT); 320 if (date_sent > 0) { 321 details.append('\n'); 322 details.append(res.getString(R.string.sent_label)); 323 details.append(MessageUtils.formatTimeStampString(context, date_sent, true)); 324 } 325 } 326 327 // Received: *** 328 details.append('\n'); 329 if (smsType == Sms.MESSAGE_TYPE_DRAFT) { 330 details.append(res.getString(R.string.saved_label)); 331 } else if (smsType == Sms.MESSAGE_TYPE_INBOX) { 332 details.append(res.getString(R.string.received_label)); 333 } else { 334 details.append(res.getString(R.string.sent_label)); 335 } 336 337 long date = cursor.getLong(MessageListAdapter.COLUMN_SMS_DATE); 338 details.append(MessageUtils.formatTimeStampString(context, date, true)); 339 340 // Error code: *** 341 int errorCode = cursor.getInt(MessageListAdapter.COLUMN_SMS_ERROR_CODE); 342 if (errorCode != 0) { 343 details.append('\n') 344 .append(res.getString(R.string.error_code_label)) 345 .append(errorCode); 346 } 347 348 return details.toString(); 349 } 350 351 static private String getPriorityDescription(Context context, int PriorityValue) { 352 Resources res = context.getResources(); 353 switch(PriorityValue) { 354 case PduHeaders.PRIORITY_HIGH: 355 return res.getString(R.string.priority_high); 356 case PduHeaders.PRIORITY_LOW: 357 return res.getString(R.string.priority_low); 358 case PduHeaders.PRIORITY_NORMAL: 359 default: 360 return res.getString(R.string.priority_normal); 361 } 362 } 363 364 public static int getAttachmentType(SlideshowModel model) { 365 if (model == null) { 366 return WorkingMessage.TEXT; 367 } 368 369 int numberOfSlides = model.size(); 370 if (numberOfSlides > 1) { 371 return WorkingMessage.SLIDESHOW; 372 } else if (numberOfSlides == 1) { 373 // Only one slide in the slide-show. 374 SlideModel slide = model.get(0); 375 if (slide.hasVideo()) { 376 return WorkingMessage.VIDEO; 377 } 378 379 if (slide.hasAudio() && slide.hasImage()) { 380 return WorkingMessage.SLIDESHOW; 381 } 382 383 if (slide.hasAudio()) { 384 return WorkingMessage.AUDIO; 385 } 386 387 if (slide.hasImage()) { 388 return WorkingMessage.IMAGE; 389 } 390 391 if (slide.hasText()) { 392 return WorkingMessage.TEXT; 393 } 394 } 395 396 return WorkingMessage.TEXT; 397 } 398 399 public static String formatTimeStampString(Context context, long when) { 400 return formatTimeStampString(context, when, false); 401 } 402 403 public static String formatTimeStampString(Context context, long when, boolean fullFormat) { 404 Time then = new Time(); 405 then.set(when); 406 Time now = new Time(); 407 now.setToNow(); 408 409 // Basic settings for formatDateTime() we want for all cases. 410 int format_flags = DateUtils.FORMAT_NO_NOON_MIDNIGHT | 411 DateUtils.FORMAT_ABBREV_ALL | 412 DateUtils.FORMAT_CAP_AMPM; 413 414 // If the message is from a different year, show the date and year. 415 if (then.year != now.year) { 416 format_flags |= DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_DATE; 417 } else if (then.yearDay != now.yearDay) { 418 // If it is from a different day than today, show only the date. 419 format_flags |= DateUtils.FORMAT_SHOW_DATE; 420 } else { 421 // Otherwise, if the message is from today, show the time. 422 format_flags |= DateUtils.FORMAT_SHOW_TIME; 423 } 424 425 // If the caller has asked for full details, make sure to show the date 426 // and time no matter what we've determined above (but still make showing 427 // the year only happen if it is a different year from today). 428 if (fullFormat) { 429 format_flags |= (DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME); 430 } 431 432 return DateUtils.formatDateTime(context, when, format_flags); 433 } 434 435 public static void selectAudio(Context context, int requestCode) { 436 if (context instanceof Activity) { 437 Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER); 438 intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, false); 439 intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, false); 440 intent.putExtra(RingtoneManager.EXTRA_RINGTONE_INCLUDE_DRM, false); 441 intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, 442 context.getString(R.string.select_audio)); 443 ((Activity) context).startActivityForResult(intent, requestCode); 444 } 445 } 446 447 public static void recordSound(Context context, int requestCode, long sizeLimit) { 448 if (context instanceof Activity) { 449 Intent intent = new Intent(Intent.ACTION_GET_CONTENT); 450 intent.setType(ContentType.AUDIO_AMR); 451 intent.setClassName("com.android.soundrecorder", 452 "com.android.soundrecorder.SoundRecorder"); 453 intent.putExtra(android.provider.MediaStore.Audio.Media.EXTRA_MAX_BYTES, sizeLimit); 454 455 ((Activity) context).startActivityForResult(intent, requestCode); 456 } 457 } 458 459 public static void recordVideo(Context context, int requestCode, long sizeLimit) { 460 if (context instanceof Activity) { 461 int durationLimit = getVideoCaptureDurationLimit(); 462 Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); 463 intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0); 464 intent.putExtra("android.intent.extra.sizeLimit", sizeLimit); 465 intent.putExtra("android.intent.extra.durationLimit", durationLimit); 466 intent.putExtra(MediaStore.EXTRA_OUTPUT, TempFileProvider.SCRAP_CONTENT_URI); 467 468 ((Activity) context).startActivityForResult(intent, requestCode); 469 } 470 } 471 472 private static int getVideoCaptureDurationLimit() { 473 CamcorderProfile camcorder = CamcorderProfile.get(CamcorderProfile.QUALITY_LOW); 474 return camcorder == null ? 0 : camcorder.duration; 475 } 476 477 public static void selectVideo(Context context, int requestCode) { 478 selectMediaByType(context, requestCode, ContentType.VIDEO_UNSPECIFIED, true); 479 } 480 481 public static void selectImage(Context context, int requestCode) { 482 selectMediaByType(context, requestCode, ContentType.IMAGE_UNSPECIFIED, false); 483 } 484 485 private static void selectMediaByType( 486 Context context, int requestCode, String contentType, boolean localFilesOnly) { 487 if (context instanceof Activity) { 488 489 Intent innerIntent = new Intent(Intent.ACTION_GET_CONTENT); 490 491 innerIntent.setType(contentType); 492 if (localFilesOnly) { 493 innerIntent.putExtra(Intent.EXTRA_LOCAL_ONLY, true); 494 } 495 496 Intent wrapperIntent = Intent.createChooser(innerIntent, null); 497 498 ((Activity) context).startActivityForResult(wrapperIntent, requestCode); 499 } 500 } 501 502 public static void viewSimpleSlideshow(Context context, SlideshowModel slideshow) { 503 if (!slideshow.isSimple()) { 504 throw new IllegalArgumentException( 505 "viewSimpleSlideshow() called on a non-simple slideshow"); 506 } 507 SlideModel slide = slideshow.get(0); 508 MediaModel mm = null; 509 if (slide.hasImage()) { 510 mm = slide.getImage(); 511 } else if (slide.hasVideo()) { 512 mm = slide.getVideo(); 513 } 514 515 Intent intent = new Intent(Intent.ACTION_VIEW); 516 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 517 intent.putExtra("SingleItemOnly", true); // So we don't see "surrounding" images in Gallery 518 519 String contentType; 520 if (mm.isDrmProtected()) { 521 contentType = mm.getDrmObject().getContentType(); 522 } else { 523 contentType = mm.getContentType(); 524 } 525 intent.setDataAndType(mm.getUri(), contentType); 526 context.startActivity(intent); 527 } 528 529 public static void showErrorDialog(Context context, 530 String title, String message) { 531 AlertDialog.Builder builder = new AlertDialog.Builder(context); 532 533 builder.setIcon(R.drawable.ic_sms_mms_not_delivered); 534 builder.setTitle(title); 535 builder.setMessage(message); 536 builder.setPositiveButton(android.R.string.ok, new OnClickListener() { 537 @Override 538 public void onClick(DialogInterface dialog, int which) { 539 if (which == DialogInterface.BUTTON_POSITIVE) { 540 dialog.dismiss(); 541 } 542 } 543 }); 544 builder.show(); 545 } 546 547 /** 548 * The quality parameter which is used to compress JPEG images. 549 */ 550 public static final int IMAGE_COMPRESSION_QUALITY = 95; 551 /** 552 * The minimum quality parameter which is used to compress JPEG images. 553 */ 554 public static final int MINIMUM_IMAGE_COMPRESSION_QUALITY = 50; 555 556 /** 557 * Message overhead that reduces the maximum image byte size. 558 * 5000 is a realistic overhead number that allows for user to also include 559 * a small MIDI file or a couple pages of text along with the picture. 560 */ 561 public static final int MESSAGE_OVERHEAD = 5000; 562 563 public static void resizeImageAsync(final Context context, 564 final Uri imageUri, final Handler handler, 565 final ResizeImageResultCallback cb, 566 final boolean append) { 567 568 // Show a progress toast if the resize hasn't finished 569 // within one second. 570 // Stash the runnable for showing it away so we can cancel 571 // it later if the resize completes ahead of the deadline. 572 final Runnable showProgress = new Runnable() { 573 @Override 574 public void run() { 575 Toast.makeText(context, R.string.compressing, Toast.LENGTH_SHORT).show(); 576 } 577 }; 578 // Schedule it for one second from now. 579 handler.postDelayed(showProgress, 1000); 580 581 new Thread(new Runnable() { 582 @Override 583 public void run() { 584 final PduPart part; 585 try { 586 UriImage image = new UriImage(context, imageUri); 587 int widthLimit = MmsConfig.getMaxImageWidth(); 588 int heightLimit = MmsConfig.getMaxImageHeight(); 589 // In mms_config.xml, the max width has always been declared larger than the max 590 // height. Swap the width and height limits if necessary so we scale the picture 591 // as little as possible. 592 if (image.getHeight() > image.getWidth()) { 593 int temp = widthLimit; 594 widthLimit = heightLimit; 595 heightLimit = temp; 596 } 597 598 part = image.getResizedImageAsPart( 599 widthLimit, 600 heightLimit, 601 MmsConfig.getMaxMessageSize() - MESSAGE_OVERHEAD); 602 } finally { 603 // Cancel pending show of the progress toast if necessary. 604 handler.removeCallbacks(showProgress); 605 } 606 607 handler.post(new Runnable() { 608 @Override 609 public void run() { 610 cb.onResizeResult(part, append); 611 } 612 }); 613 } 614 }).start(); 615 } 616 617 public static void showDiscardDraftConfirmDialog(Context context, 618 OnClickListener listener) { 619 new AlertDialog.Builder(context) 620 .setIcon(android.R.drawable.ic_dialog_alert) 621 .setTitle(R.string.discard_message) 622 .setMessage(R.string.discard_message_reason) 623 .setPositiveButton(R.string.yes, listener) 624 .setNegativeButton(R.string.no, null) 625 .show(); 626 } 627 628 public static String getLocalNumber() { 629 if (null == sLocalNumber) { 630 sLocalNumber = MmsApp.getApplication().getTelephonyManager().getLine1Number(); 631 } 632 return sLocalNumber; 633 } 634 635 public static boolean isLocalNumber(String number) { 636 if (number == null) { 637 return false; 638 } 639 640 // we don't use Mms.isEmailAddress() because it is too strict for comparing addresses like 641 // "foo+caf_=6505551212=tmomail.net (at) gmail.com", which is the 'from' address from a forwarded email 642 // message from Gmail. We don't want to treat "foo+caf_=6505551212=tmomail.net (at) gmail.com" and 643 // "6505551212" to be the same. 644 if (number.indexOf('@') >= 0) { 645 return false; 646 } 647 648 return PhoneNumberUtils.compare(number, getLocalNumber()); 649 } 650 651 public static void handleReadReport(final Context context, 652 final Collection<Long> threadIds, 653 final int status, 654 final Runnable callback) { 655 StringBuilder selectionBuilder = new StringBuilder(Mms.MESSAGE_TYPE + " = " 656 + PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF 657 + " AND " + Mms.READ + " = 0" 658 + " AND " + Mms.READ_REPORT + " = " + PduHeaders.VALUE_YES); 659 660 String[] selectionArgs = null; 661 if (threadIds != null) { 662 String threadIdSelection = null; 663 StringBuilder buf = new StringBuilder(); 664 selectionArgs = new String[threadIds.size()]; 665 int i = 0; 666 667 for (long threadId : threadIds) { 668 if (i > 0) { 669 buf.append(" OR "); 670 } 671 buf.append(Mms.THREAD_ID).append("=?"); 672 selectionArgs[i++] = Long.toString(threadId); 673 } 674 threadIdSelection = buf.toString(); 675 676 selectionBuilder.append(" AND (" + threadIdSelection + ")"); 677 } 678 679 final Cursor c = SqliteWrapper.query(context, context.getContentResolver(), 680 Mms.Inbox.CONTENT_URI, new String[] {Mms._ID, Mms.MESSAGE_ID}, 681 selectionBuilder.toString(), selectionArgs, null); 682 683 if (c == null) { 684 return; 685 } 686 687 final Map<String, String> map = new HashMap<String, String>(); 688 try { 689 if (c.getCount() == 0) { 690 if (callback != null) { 691 callback.run(); 692 } 693 return; 694 } 695 696 while (c.moveToNext()) { 697 Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, c.getLong(0)); 698 map.put(c.getString(1), AddressUtils.getFrom(context, uri)); 699 } 700 } finally { 701 c.close(); 702 } 703 704 OnClickListener positiveListener = new OnClickListener() { 705 @Override 706 public void onClick(DialogInterface dialog, int which) { 707 for (final Map.Entry<String, String> entry : map.entrySet()) { 708 MmsMessageSender.sendReadRec(context, entry.getValue(), 709 entry.getKey(), status); 710 } 711 712 if (callback != null) { 713 callback.run(); 714 } 715 dialog.dismiss(); 716 } 717 }; 718 719 OnClickListener negativeListener = new OnClickListener() { 720 @Override 721 public void onClick(DialogInterface dialog, int which) { 722 if (callback != null) { 723 callback.run(); 724 } 725 dialog.dismiss(); 726 } 727 }; 728 729 OnCancelListener cancelListener = new OnCancelListener() { 730 @Override 731 public void onCancel(DialogInterface dialog) { 732 if (callback != null) { 733 callback.run(); 734 } 735 dialog.dismiss(); 736 } 737 }; 738 739 confirmReadReportDialog(context, positiveListener, 740 negativeListener, 741 cancelListener); 742 } 743 744 private static void confirmReadReportDialog(Context context, 745 OnClickListener positiveListener, OnClickListener negativeListener, 746 OnCancelListener cancelListener) { 747 AlertDialog.Builder builder = new AlertDialog.Builder(context); 748 builder.setCancelable(true); 749 builder.setTitle(R.string.confirm); 750 builder.setMessage(R.string.message_send_read_report); 751 builder.setPositiveButton(R.string.yes, positiveListener); 752 builder.setNegativeButton(R.string.no, negativeListener); 753 builder.setOnCancelListener(cancelListener); 754 builder.show(); 755 } 756 757 public static String extractEncStrFromCursor(Cursor cursor, 758 int columnRawBytes, int columnCharset) { 759 String rawBytes = cursor.getString(columnRawBytes); 760 int charset = cursor.getInt(columnCharset); 761 762 if (TextUtils.isEmpty(rawBytes)) { 763 return ""; 764 } else if (charset == CharacterSets.ANY_CHARSET) { 765 return rawBytes; 766 } else { 767 return new EncodedStringValue(charset, PduPersister.getBytes(rawBytes)).getString(); 768 } 769 } 770 771 private static String extractEncStr(Context context, EncodedStringValue value) { 772 if (value != null) { 773 return value.getString(); 774 } else { 775 return ""; 776 } 777 } 778 779 public static ArrayList<String> extractUris(URLSpan[] spans) { 780 int size = spans.length; 781 ArrayList<String> accumulator = new ArrayList<String>(); 782 783 for (int i = 0; i < size; i++) { 784 accumulator.add(spans[i].getURL()); 785 } 786 return accumulator; 787 } 788 789 /** 790 * Play/view the message attachments. 791 * TOOD: We need to save the draft before launching another activity to view the attachments. 792 * This is hacky though since we will do saveDraft twice and slow down the UI. 793 * We should pass the slideshow in intent extra to the view activity instead of 794 * asking it to read attachments from database. 795 * @param context 796 * @param msgUri the MMS message URI in database 797 * @param slideshow the slideshow to save 798 * @param persister the PDU persister for updating the database 799 * @param sendReq the SendReq for updating the database 800 */ 801 public static void viewMmsMessageAttachment(Context context, Uri msgUri, 802 SlideshowModel slideshow) { 803 viewMmsMessageAttachment(context, msgUri, slideshow, 0); 804 } 805 806 private static void viewMmsMessageAttachment(Context context, Uri msgUri, 807 SlideshowModel slideshow, int requestCode) { 808 boolean isSimple = (slideshow == null) ? false : slideshow.isSimple(); 809 if (isSimple) { 810 // In attachment-editor mode, we only ever have one slide. 811 MessageUtils.viewSimpleSlideshow(context, slideshow); 812 } else { 813 // If a slideshow was provided, save it to disk first. 814 if (slideshow != null) { 815 PduPersister persister = PduPersister.getPduPersister(context); 816 try { 817 PduBody pb = slideshow.toPduBody(); 818 persister.updateParts(msgUri, pb); 819 slideshow.sync(pb); 820 } catch (MmsException e) { 821 Log.e(TAG, "Unable to save message for preview"); 822 return; 823 } 824 } 825 // Launch the slideshow activity to play/view. 826 Intent intent = new Intent(context, SlideshowActivity.class); 827 intent.setData(msgUri); 828 if (requestCode > 0 && context instanceof Activity) { 829 ((Activity)context).startActivityForResult(intent, requestCode); 830 } else { 831 context.startActivity(intent); 832 } 833 } 834 } 835 836 public static void viewMmsMessageAttachment(Context context, WorkingMessage msg, 837 int requestCode) { 838 SlideshowModel slideshow = msg.getSlideshow(); 839 if (slideshow == null) { 840 throw new IllegalStateException("msg.getSlideshow() == null"); 841 } 842 if (slideshow.isSimple()) { 843 MessageUtils.viewSimpleSlideshow(context, slideshow); 844 } else { 845 Uri uri = msg.saveAsMms(false); 846 if (uri != null) { 847 // Pass null for the slideshow paramater, otherwise viewMmsMessageAttachment 848 // will persist the slideshow to disk again (we just did that above in saveAsMms) 849 viewMmsMessageAttachment(context, uri, null, requestCode); 850 } 851 } 852 } 853 854 /** 855 * Debugging 856 */ 857 public static void writeHprofDataToFile(){ 858 String filename = Environment.getExternalStorageDirectory() + "/mms_oom_hprof_data"; 859 try { 860 android.os.Debug.dumpHprofData(filename); 861 Log.i(TAG, "##### written hprof data to " + filename); 862 } catch (IOException ex) { 863 Log.e(TAG, "writeHprofDataToFile: caught " + ex); 864 } 865 } 866 867 // An alias (or commonly called "nickname") is: 868 // Nickname must begin with a letter. 869 // Only letters a-z, numbers 0-9, or . are allowed in Nickname field. 870 public static boolean isAlias(String string) { 871 if (!MmsConfig.isAliasEnabled()) { 872 return false; 873 } 874 875 int len = string == null ? 0 : string.length(); 876 877 if (len < MmsConfig.getAliasMinChars() || len > MmsConfig.getAliasMaxChars()) { 878 return false; 879 } 880 881 if (!Character.isLetter(string.charAt(0))) { // Nickname begins with a letter 882 return false; 883 } 884 for (int i = 1; i < len; i++) { 885 char c = string.charAt(i); 886 if (!(Character.isLetterOrDigit(c) || c == '.')) { 887 return false; 888 } 889 } 890 891 return true; 892 } 893 894 /** 895 * Given a phone number, return the string without syntactic sugar, meaning parens, 896 * spaces, slashes, dots, dashes, etc. If the input string contains non-numeric 897 * non-punctuation characters, return null. 898 */ 899 private static String parsePhoneNumberForMms(String address) { 900 StringBuilder builder = new StringBuilder(); 901 int len = address.length(); 902 903 for (int i = 0; i < len; i++) { 904 char c = address.charAt(i); 905 906 // accept the first '+' in the address 907 if (c == '+' && builder.length() == 0) { 908 builder.append(c); 909 continue; 910 } 911 912 if (Character.isDigit(c)) { 913 builder.append(c); 914 continue; 915 } 916 917 if (numericSugarMap.get(c) == null) { 918 return null; 919 } 920 } 921 return builder.toString(); 922 } 923 924 /** 925 * Returns true if the address passed in is a valid MMS address. 926 */ 927 public static boolean isValidMmsAddress(String address) { 928 String retVal = parseMmsAddress(address); 929 return (retVal != null); 930 } 931 932 /** 933 * parse the input address to be a valid MMS address. 934 * - if the address is an email address, leave it as is. 935 * - if the address can be parsed into a valid MMS phone number, return the parsed number. 936 * - if the address is a compliant alias address, leave it as is. 937 */ 938 public static String parseMmsAddress(String address) { 939 // if it's a valid Email address, use that. 940 if (Mms.isEmailAddress(address)) { 941 return address; 942 } 943 944 // if we are able to parse the address to a MMS compliant phone number, take that. 945 String retVal = parsePhoneNumberForMms(address); 946 if (retVal != null) { 947 return retVal; 948 } 949 950 // if it's an alias compliant address, use that. 951 if (isAlias(address)) { 952 return address; 953 } 954 955 // it's not a valid MMS address, return null 956 return null; 957 } 958 959 private static void log(String msg) { 960 Log.d(TAG, "[MsgUtils] " + msg); 961 } 962 } 963