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