MessageUtils.java revision 0ecc26df09777835cfa8dbfd3c48ca7b7fa7f011
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 18package com.android.mms.ui; 19 20import com.android.mms.MmsApp; 21import com.android.mms.MmsConfig; 22import com.android.mms.R; 23import com.android.mms.LogTag; 24import com.android.mms.TempFileProvider; 25import com.android.mms.data.WorkingMessage; 26import com.android.mms.model.MediaModel; 27import com.android.mms.model.SlideModel; 28import com.android.mms.model.SlideshowModel; 29import com.android.mms.transaction.MmsMessageSender; 30import com.android.mms.util.AddressUtils; 31import com.google.android.mms.ContentType; 32import com.google.android.mms.MmsException; 33import com.google.android.mms.pdu.CharacterSets; 34import com.google.android.mms.pdu.EncodedStringValue; 35import com.google.android.mms.pdu.MultimediaMessagePdu; 36import com.google.android.mms.pdu.NotificationInd; 37import com.google.android.mms.pdu.PduBody; 38import com.google.android.mms.pdu.PduHeaders; 39import com.google.android.mms.pdu.PduPart; 40import com.google.android.mms.pdu.PduPersister; 41import com.google.android.mms.pdu.RetrieveConf; 42import com.google.android.mms.pdu.SendReq; 43import android.database.sqlite.SqliteWrapper; 44 45import android.app.Activity; 46import android.app.AlertDialog; 47import android.content.ContentUris; 48import android.content.Context; 49import android.content.DialogInterface; 50import android.content.Intent; 51import android.content.DialogInterface.OnCancelListener; 52import android.content.DialogInterface.OnClickListener; 53import android.content.res.Resources; 54import android.database.Cursor; 55import android.media.CamcorderProfile; 56import android.media.RingtoneManager; 57import android.net.Uri; 58import android.os.Environment; 59import android.os.Handler; 60import android.provider.MediaStore; 61import android.provider.Telephony.Mms; 62import android.provider.Telephony.Sms; 63import android.telephony.PhoneNumberUtils; 64import android.text.TextUtils; 65import android.text.format.DateUtils; 66import android.text.format.Time; 67import android.text.style.URLSpan; 68import android.util.Log; 69import android.widget.Toast; 70 71import java.io.IOException; 72import java.util.ArrayList; 73import java.util.Collection; 74import java.util.HashMap; 75import java.util.Map; 76import java.util.concurrent.ConcurrentHashMap; 77 78/** 79 * An utility class for managing messages. 80 */ 81public 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 // Delivered: *** 341 if (smsType == Sms.MESSAGE_TYPE_SENT) { 342 // For sent messages with delivery reports, we stick the delivery time in the 343 // date_sent column (see MessageStatusReceiver). 344 long dateDelivered = cursor.getLong(MessageListAdapter.COLUMN_SMS_DATE_SENT); 345 if (dateDelivered > 0) { 346 details.append('\n'); 347 details.append(res.getString(R.string.delivered_label)); 348 details.append(MessageUtils.formatTimeStampString(context, dateDelivered, true)); 349 } 350 } 351 352 // Error code: *** 353 int errorCode = cursor.getInt(MessageListAdapter.COLUMN_SMS_ERROR_CODE); 354 if (errorCode != 0) { 355 details.append('\n') 356 .append(res.getString(R.string.error_code_label)) 357 .append(errorCode); 358 } 359 360 return details.toString(); 361 } 362 363 static private String getPriorityDescription(Context context, int PriorityValue) { 364 Resources res = context.getResources(); 365 switch(PriorityValue) { 366 case PduHeaders.PRIORITY_HIGH: 367 return res.getString(R.string.priority_high); 368 case PduHeaders.PRIORITY_LOW: 369 return res.getString(R.string.priority_low); 370 case PduHeaders.PRIORITY_NORMAL: 371 default: 372 return res.getString(R.string.priority_normal); 373 } 374 } 375 376 public static int getAttachmentType(SlideshowModel model) { 377 if (model == null) { 378 return WorkingMessage.TEXT; 379 } 380 381 int numberOfSlides = model.size(); 382 if (numberOfSlides > 1) { 383 return WorkingMessage.SLIDESHOW; 384 } else if (numberOfSlides == 1) { 385 // Only one slide in the slide-show. 386 SlideModel slide = model.get(0); 387 if (slide.hasVideo()) { 388 return WorkingMessage.VIDEO; 389 } 390 391 if (slide.hasAudio() && slide.hasImage()) { 392 return WorkingMessage.SLIDESHOW; 393 } 394 395 if (slide.hasAudio()) { 396 return WorkingMessage.AUDIO; 397 } 398 399 if (slide.hasImage()) { 400 return WorkingMessage.IMAGE; 401 } 402 403 if (slide.hasText()) { 404 return WorkingMessage.TEXT; 405 } 406 } 407 408 return WorkingMessage.TEXT; 409 } 410 411 public static String formatTimeStampString(Context context, long when) { 412 return formatTimeStampString(context, when, false); 413 } 414 415 public static String formatTimeStampString(Context context, long when, boolean fullFormat) { 416 Time then = new Time(); 417 then.set(when); 418 Time now = new Time(); 419 now.setToNow(); 420 421 // Basic settings for formatDateTime() we want for all cases. 422 int format_flags = DateUtils.FORMAT_NO_NOON_MIDNIGHT | 423 DateUtils.FORMAT_ABBREV_ALL | 424 DateUtils.FORMAT_CAP_AMPM; 425 426 // If the message is from a different year, show the date and year. 427 if (then.year != now.year) { 428 format_flags |= DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_DATE; 429 } else if (then.yearDay != now.yearDay) { 430 // If it is from a different day than today, show only the date. 431 format_flags |= DateUtils.FORMAT_SHOW_DATE; 432 } else { 433 // Otherwise, if the message is from today, show the time. 434 format_flags |= DateUtils.FORMAT_SHOW_TIME; 435 } 436 437 // If the caller has asked for full details, make sure to show the date 438 // and time no matter what we've determined above (but still make showing 439 // the year only happen if it is a different year from today). 440 if (fullFormat) { 441 format_flags |= (DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME); 442 } 443 444 return DateUtils.formatDateTime(context, when, format_flags); 445 } 446 447 public static void selectAudio(Context context, int requestCode) { 448 if (context instanceof Activity) { 449 Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER); 450 intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, false); 451 intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, false); 452 intent.putExtra(RingtoneManager.EXTRA_RINGTONE_INCLUDE_DRM, false); 453 intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, 454 context.getString(R.string.select_audio)); 455 ((Activity) context).startActivityForResult(intent, requestCode); 456 } 457 } 458 459 public static void recordSound(Context context, int requestCode, long sizeLimit) { 460 if (context instanceof Activity) { 461 Intent intent = new Intent(Intent.ACTION_GET_CONTENT); 462 intent.setType(ContentType.AUDIO_AMR); 463 intent.setClassName("com.android.soundrecorder", 464 "com.android.soundrecorder.SoundRecorder"); 465 intent.putExtra(android.provider.MediaStore.Audio.Media.EXTRA_MAX_BYTES, sizeLimit); 466 467 ((Activity) context).startActivityForResult(intent, requestCode); 468 } 469 } 470 471 public static void recordVideo(Context context, int requestCode, long sizeLimit) { 472 if (context instanceof Activity) { 473 int durationLimit = getVideoCaptureDurationLimit(); 474 Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); 475 intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0); 476 intent.putExtra("android.intent.extra.sizeLimit", sizeLimit); 477 intent.putExtra("android.intent.extra.durationLimit", durationLimit); 478 intent.putExtra(MediaStore.EXTRA_OUTPUT, TempFileProvider.SCRAP_CONTENT_URI); 479 480 ((Activity) context).startActivityForResult(intent, requestCode); 481 } 482 } 483 484 private static int getVideoCaptureDurationLimit() { 485 CamcorderProfile camcorder = CamcorderProfile.get(CamcorderProfile.QUALITY_LOW); 486 return camcorder == null ? 0 : camcorder.duration; 487 } 488 489 public static void selectVideo(Context context, int requestCode) { 490 selectMediaByType(context, requestCode, ContentType.VIDEO_UNSPECIFIED, true); 491 } 492 493 public static void selectImage(Context context, int requestCode) { 494 selectMediaByType(context, requestCode, ContentType.IMAGE_UNSPECIFIED, false); 495 } 496 497 private static void selectMediaByType( 498 Context context, int requestCode, String contentType, boolean localFilesOnly) { 499 if (context instanceof Activity) { 500 501 Intent innerIntent = new Intent(Intent.ACTION_GET_CONTENT); 502 503 innerIntent.setType(contentType); 504 if (localFilesOnly) { 505 innerIntent.putExtra(Intent.EXTRA_LOCAL_ONLY, true); 506 } 507 508 Intent wrapperIntent = Intent.createChooser(innerIntent, null); 509 510 ((Activity) context).startActivityForResult(wrapperIntent, requestCode); 511 } 512 } 513 514 public static void viewSimpleSlideshow(Context context, SlideshowModel slideshow) { 515 if (!slideshow.isSimple()) { 516 throw new IllegalArgumentException( 517 "viewSimpleSlideshow() called on a non-simple slideshow"); 518 } 519 SlideModel slide = slideshow.get(0); 520 MediaModel mm = null; 521 if (slide.hasImage()) { 522 mm = slide.getImage(); 523 } else if (slide.hasVideo()) { 524 mm = slide.getVideo(); 525 } 526 527 Intent intent = new Intent(Intent.ACTION_VIEW); 528 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 529 intent.putExtra("SingleItemOnly", true); // So we don't see "surrounding" images in Gallery 530 531 String contentType; 532 contentType = mm.getContentType(); 533 intent.setDataAndType(mm.getUri(), contentType); 534 context.startActivity(intent); 535 } 536 537 public static void showErrorDialog(Activity activity, 538 String title, String message) { 539 if (activity.isFinishing()) { 540 return; 541 } 542 AlertDialog.Builder builder = new AlertDialog.Builder(activity); 543 544 builder.setIcon(R.drawable.ic_sms_mms_not_delivered); 545 builder.setTitle(title); 546 builder.setMessage(message); 547 builder.setPositiveButton(android.R.string.ok, new OnClickListener() { 548 @Override 549 public void onClick(DialogInterface dialog, int which) { 550 if (which == DialogInterface.BUTTON_POSITIVE) { 551 dialog.dismiss(); 552 } 553 } 554 }); 555 builder.show(); 556 } 557 558 /** 559 * The quality parameter which is used to compress JPEG images. 560 */ 561 public static final int IMAGE_COMPRESSION_QUALITY = 95; 562 /** 563 * The minimum quality parameter which is used to compress JPEG images. 564 */ 565 public static final int MINIMUM_IMAGE_COMPRESSION_QUALITY = 50; 566 567 /** 568 * Message overhead that reduces the maximum image byte size. 569 * 5000 is a realistic overhead number that allows for user to also include 570 * a small MIDI file or a couple pages of text along with the picture. 571 */ 572 public static final int MESSAGE_OVERHEAD = 5000; 573 574 public static void resizeImageAsync(final Context context, 575 final Uri imageUri, final Handler handler, 576 final ResizeImageResultCallback cb, 577 final boolean append) { 578 579 // Show a progress toast if the resize hasn't finished 580 // within one second. 581 // Stash the runnable for showing it away so we can cancel 582 // it later if the resize completes ahead of the deadline. 583 final Runnable showProgress = new Runnable() { 584 @Override 585 public void run() { 586 Toast.makeText(context, R.string.compressing, Toast.LENGTH_SHORT).show(); 587 } 588 }; 589 // Schedule it for one second from now. 590 handler.postDelayed(showProgress, 1000); 591 592 new Thread(new Runnable() { 593 @Override 594 public void run() { 595 final PduPart part; 596 try { 597 UriImage image = new UriImage(context, imageUri); 598 int widthLimit = MmsConfig.getMaxImageWidth(); 599 int heightLimit = MmsConfig.getMaxImageHeight(); 600 // In mms_config.xml, the max width has always been declared larger than the max 601 // height. Swap the width and height limits if necessary so we scale the picture 602 // as little as possible. 603 if (image.getHeight() > image.getWidth()) { 604 int temp = widthLimit; 605 widthLimit = heightLimit; 606 heightLimit = temp; 607 } 608 609 part = image.getResizedImageAsPart( 610 widthLimit, 611 heightLimit, 612 MmsConfig.getMaxMessageSize() - MESSAGE_OVERHEAD); 613 } finally { 614 // Cancel pending show of the progress toast if necessary. 615 handler.removeCallbacks(showProgress); 616 } 617 618 handler.post(new Runnable() { 619 @Override 620 public void run() { 621 cb.onResizeResult(part, append); 622 } 623 }); 624 } 625 }, "MessageUtils.resizeImageAsync").start(); 626 } 627 628 public static void showDiscardDraftConfirmDialog(Context context, 629 OnClickListener listener) { 630 new AlertDialog.Builder(context) 631 .setMessage(R.string.discard_message_reason) 632 .setPositiveButton(R.string.yes, listener) 633 .setNegativeButton(R.string.no, null) 634 .show(); 635 } 636 637 public static String getLocalNumber() { 638 if (null == sLocalNumber) { 639 sLocalNumber = MmsApp.getApplication().getTelephonyManager().getLine1Number(); 640 } 641 return sLocalNumber; 642 } 643 644 public static boolean isLocalNumber(String number) { 645 if (number == null) { 646 return false; 647 } 648 649 // we don't use Mms.isEmailAddress() because it is too strict for comparing addresses like 650 // "foo+caf_=6505551212=tmomail.net@gmail.com", which is the 'from' address from a forwarded email 651 // message from Gmail. We don't want to treat "foo+caf_=6505551212=tmomail.net@gmail.com" and 652 // "6505551212" to be the same. 653 if (number.indexOf('@') >= 0) { 654 return false; 655 } 656 657 return PhoneNumberUtils.compare(number, getLocalNumber()); 658 } 659 660 public static void handleReadReport(final Context context, 661 final Collection<Long> threadIds, 662 final int status, 663 final Runnable callback) { 664 StringBuilder selectionBuilder = new StringBuilder(Mms.MESSAGE_TYPE + " = " 665 + PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF 666 + " AND " + Mms.READ + " = 0" 667 + " AND " + Mms.READ_REPORT + " = " + PduHeaders.VALUE_YES); 668 669 String[] selectionArgs = null; 670 if (threadIds != null) { 671 String threadIdSelection = null; 672 StringBuilder buf = new StringBuilder(); 673 selectionArgs = new String[threadIds.size()]; 674 int i = 0; 675 676 for (long threadId : threadIds) { 677 if (i > 0) { 678 buf.append(" OR "); 679 } 680 buf.append(Mms.THREAD_ID).append("=?"); 681 selectionArgs[i++] = Long.toString(threadId); 682 } 683 threadIdSelection = buf.toString(); 684 685 selectionBuilder.append(" AND (" + threadIdSelection + ")"); 686 } 687 688 final Cursor c = SqliteWrapper.query(context, context.getContentResolver(), 689 Mms.Inbox.CONTENT_URI, new String[] {Mms._ID, Mms.MESSAGE_ID}, 690 selectionBuilder.toString(), selectionArgs, null); 691 692 if (c == null) { 693 return; 694 } 695 696 final Map<String, String> map = new HashMap<String, String>(); 697 try { 698 if (c.getCount() == 0) { 699 if (callback != null) { 700 callback.run(); 701 } 702 return; 703 } 704 705 while (c.moveToNext()) { 706 Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, c.getLong(0)); 707 map.put(c.getString(1), AddressUtils.getFrom(context, uri)); 708 } 709 } finally { 710 c.close(); 711 } 712 713 OnClickListener positiveListener = new OnClickListener() { 714 @Override 715 public void onClick(DialogInterface dialog, int which) { 716 for (final Map.Entry<String, String> entry : map.entrySet()) { 717 MmsMessageSender.sendReadRec(context, entry.getValue(), 718 entry.getKey(), status); 719 } 720 721 if (callback != null) { 722 callback.run(); 723 } 724 dialog.dismiss(); 725 } 726 }; 727 728 OnClickListener negativeListener = new OnClickListener() { 729 @Override 730 public void onClick(DialogInterface dialog, int which) { 731 if (callback != null) { 732 callback.run(); 733 } 734 dialog.dismiss(); 735 } 736 }; 737 738 OnCancelListener cancelListener = new OnCancelListener() { 739 @Override 740 public void onCancel(DialogInterface dialog) { 741 if (callback != null) { 742 callback.run(); 743 } 744 dialog.dismiss(); 745 } 746 }; 747 748 confirmReadReportDialog(context, positiveListener, 749 negativeListener, 750 cancelListener); 751 } 752 753 private static void confirmReadReportDialog(Context context, 754 OnClickListener positiveListener, OnClickListener negativeListener, 755 OnCancelListener cancelListener) { 756 AlertDialog.Builder builder = new AlertDialog.Builder(context); 757 builder.setCancelable(true); 758 builder.setTitle(R.string.confirm); 759 builder.setMessage(R.string.message_send_read_report); 760 builder.setPositiveButton(R.string.yes, positiveListener); 761 builder.setNegativeButton(R.string.no, negativeListener); 762 builder.setOnCancelListener(cancelListener); 763 builder.show(); 764 } 765 766 public static String extractEncStrFromCursor(Cursor cursor, 767 int columnRawBytes, int columnCharset) { 768 String rawBytes = cursor.getString(columnRawBytes); 769 int charset = cursor.getInt(columnCharset); 770 771 if (TextUtils.isEmpty(rawBytes)) { 772 return ""; 773 } else if (charset == CharacterSets.ANY_CHARSET) { 774 return rawBytes; 775 } else { 776 return new EncodedStringValue(charset, PduPersister.getBytes(rawBytes)).getString(); 777 } 778 } 779 780 private static String extractEncStr(Context context, EncodedStringValue value) { 781 if (value != null) { 782 return value.getString(); 783 } else { 784 return ""; 785 } 786 } 787 788 public static ArrayList<String> extractUris(URLSpan[] spans) { 789 int size = spans.length; 790 ArrayList<String> accumulator = new ArrayList<String>(); 791 792 for (int i = 0; i < size; i++) { 793 accumulator.add(spans[i].getURL()); 794 } 795 return accumulator; 796 } 797 798 /** 799 * Play/view the message attachments. 800 * TOOD: We need to save the draft before launching another activity to view the attachments. 801 * This is hacky though since we will do saveDraft twice and slow down the UI. 802 * We should pass the slideshow in intent extra to the view activity instead of 803 * asking it to read attachments from database. 804 * @param context 805 * @param msgUri the MMS message URI in database 806 * @param slideshow the slideshow to save 807 * @param persister the PDU persister for updating the database 808 * @param sendReq the SendReq for updating the database 809 */ 810 public static void viewMmsMessageAttachment(Context context, Uri msgUri, 811 SlideshowModel slideshow) { 812 viewMmsMessageAttachment(context, msgUri, slideshow, 0); 813 } 814 815 private static void viewMmsMessageAttachment(Context context, Uri msgUri, 816 SlideshowModel slideshow, int requestCode) { 817 boolean isSimple = (slideshow == null) ? false : slideshow.isSimple(); 818 if (isSimple) { 819 // In attachment-editor mode, we only ever have one slide. 820 MessageUtils.viewSimpleSlideshow(context, slideshow); 821 } else { 822 // If a slideshow was provided, save it to disk first. 823 if (slideshow != null) { 824 PduPersister persister = PduPersister.getPduPersister(context); 825 try { 826 PduBody pb = slideshow.toPduBody(); 827 persister.updateParts(msgUri, pb); 828 slideshow.sync(pb); 829 } catch (MmsException e) { 830 Log.e(TAG, "Unable to save message for preview"); 831 return; 832 } 833 } 834 // Launch the slideshow activity to play/view. 835 Intent intent = new Intent(context, SlideshowActivity.class); 836 intent.setData(msgUri); 837 if (requestCode > 0 && context instanceof Activity) { 838 ((Activity)context).startActivityForResult(intent, requestCode); 839 } else { 840 context.startActivity(intent); 841 } 842 } 843 } 844 845 public static void viewMmsMessageAttachment(Context context, WorkingMessage msg, 846 int requestCode) { 847 SlideshowModel slideshow = msg.getSlideshow(); 848 if (slideshow == null) { 849 throw new IllegalStateException("msg.getSlideshow() == null"); 850 } 851 if (slideshow.isSimple()) { 852 MessageUtils.viewSimpleSlideshow(context, slideshow); 853 } else { 854 Uri uri = msg.saveAsMms(false); 855 if (uri != null) { 856 // Pass null for the slideshow paramater, otherwise viewMmsMessageAttachment 857 // will persist the slideshow to disk again (we just did that above in saveAsMms) 858 viewMmsMessageAttachment(context, uri, null, requestCode); 859 } 860 } 861 } 862 863 /** 864 * Debugging 865 */ 866 public static void writeHprofDataToFile(){ 867 String filename = Environment.getExternalStorageDirectory() + "/mms_oom_hprof_data"; 868 try { 869 android.os.Debug.dumpHprofData(filename); 870 Log.i(TAG, "##### written hprof data to " + filename); 871 } catch (IOException ex) { 872 Log.e(TAG, "writeHprofDataToFile: caught " + ex); 873 } 874 } 875 876 // An alias (or commonly called "nickname") is: 877 // Nickname must begin with a letter. 878 // Only letters a-z, numbers 0-9, or . are allowed in Nickname field. 879 public static boolean isAlias(String string) { 880 if (!MmsConfig.isAliasEnabled()) { 881 return false; 882 } 883 884 int len = string == null ? 0 : string.length(); 885 886 if (len < MmsConfig.getAliasMinChars() || len > MmsConfig.getAliasMaxChars()) { 887 return false; 888 } 889 890 if (!Character.isLetter(string.charAt(0))) { // Nickname begins with a letter 891 return false; 892 } 893 for (int i = 1; i < len; i++) { 894 char c = string.charAt(i); 895 if (!(Character.isLetterOrDigit(c) || c == '.')) { 896 return false; 897 } 898 } 899 900 return true; 901 } 902 903 /** 904 * Given a phone number, return the string without syntactic sugar, meaning parens, 905 * spaces, slashes, dots, dashes, etc. If the input string contains non-numeric 906 * non-punctuation characters, return null. 907 */ 908 private static String parsePhoneNumberForMms(String address) { 909 StringBuilder builder = new StringBuilder(); 910 int len = address.length(); 911 912 for (int i = 0; i < len; i++) { 913 char c = address.charAt(i); 914 915 // accept the first '+' in the address 916 if (c == '+' && builder.length() == 0) { 917 builder.append(c); 918 continue; 919 } 920 921 if (Character.isDigit(c)) { 922 builder.append(c); 923 continue; 924 } 925 926 if (numericSugarMap.get(c) == null) { 927 return null; 928 } 929 } 930 return builder.toString(); 931 } 932 933 /** 934 * Returns true if the address passed in is a valid MMS address. 935 */ 936 public static boolean isValidMmsAddress(String address) { 937 String retVal = parseMmsAddress(address); 938 return (retVal != null); 939 } 940 941 /** 942 * parse the input address to be a valid MMS address. 943 * - if the address is an email address, leave it as is. 944 * - if the address can be parsed into a valid MMS phone number, return the parsed number. 945 * - if the address is a compliant alias address, leave it as is. 946 */ 947 public static String parseMmsAddress(String address) { 948 // if it's a valid Email address, use that. 949 if (Mms.isEmailAddress(address)) { 950 return address; 951 } 952 953 // if we are able to parse the address to a MMS compliant phone number, take that. 954 String retVal = parsePhoneNumberForMms(address); 955 if (retVal != null) { 956 return retVal; 957 } 958 959 // if it's an alias compliant address, use that. 960 if (isAlias(address)) { 961 return address; 962 } 963 964 // it's not a valid MMS address, return null 965 return null; 966 } 967 968 private static void log(String msg) { 969 Log.d(TAG, "[MsgUtils] " + msg); 970 } 971} 972