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, MultimediaMessagePdu mmp) { 406 if (model == null || mmp == 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 // Handle the multimedia message only has subject 437 String subject = mmp.getSubject() != null ? mmp.getSubject().getString() : null; 438 if (!TextUtils.isEmpty(subject)) { 439 return WorkingMessage.TEXT; 440 } 441 } 442 443 return MessageItem.ATTACHMENT_TYPE_NOT_LOADED; 444 } 445 446 public static String formatTimeStampString(Context context, long when) { 447 return formatTimeStampString(context, when, false); 448 } 449 450 public static String formatTimeStampString(Context context, long when, boolean fullFormat) { 451 Time then = new Time(); 452 then.set(when); 453 Time now = new Time(); 454 now.setToNow(); 455 456 // Basic settings for formatDateTime() we want for all cases. 457 int format_flags = DateUtils.FORMAT_NO_NOON_MIDNIGHT | 458 DateUtils.FORMAT_ABBREV_ALL | 459 DateUtils.FORMAT_CAP_AMPM; 460 461 // If the message is from a different year, show the date and year. 462 if (then.year != now.year) { 463 format_flags |= DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_DATE; 464 } else if (then.yearDay != now.yearDay) { 465 // If it is from a different day than today, show only the date. 466 format_flags |= DateUtils.FORMAT_SHOW_DATE; 467 } else { 468 // Otherwise, if the message is from today, show the time. 469 format_flags |= DateUtils.FORMAT_SHOW_TIME; 470 } 471 472 // If the caller has asked for full details, make sure to show the date 473 // and time no matter what we've determined above (but still make showing 474 // the year only happen if it is a different year from today). 475 if (fullFormat) { 476 format_flags |= (DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME); 477 } 478 479 return DateUtils.formatDateTime(context, when, format_flags); 480 } 481 482 public static void selectAudio(Activity activity, int requestCode) { 483 Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER); 484 intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, false); 485 intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, false); 486 intent.putExtra(RingtoneManager.EXTRA_RINGTONE_INCLUDE_DRM, false); 487 intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, 488 activity.getString(R.string.select_audio)); 489 activity.startActivityForResult(intent, requestCode); 490 } 491 492 public static void recordSound(Activity activity, int requestCode, long sizeLimit) { 493 Intent intent = new Intent(Intent.ACTION_GET_CONTENT); 494 intent.setType(ContentType.AUDIO_AMR); 495 intent.setClassName("com.android.soundrecorder", 496 "com.android.soundrecorder.SoundRecorder"); 497 intent.putExtra(android.provider.MediaStore.Audio.Media.EXTRA_MAX_BYTES, sizeLimit); 498 activity.startActivityForResult(intent, requestCode); 499 } 500 501 public static void recordVideo(Activity activity, int requestCode, long sizeLimit) { 502 // The video recorder can sometimes return a file that's larger than the max we 503 // say we can handle. Try to handle that overshoot by specifying an 85% limit. 504 sizeLimit *= .85F; 505 506 int durationLimit = getVideoCaptureDurationLimit(sizeLimit); 507 508 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 509 log("recordVideo: durationLimit: " + durationLimit + 510 " sizeLimit: " + sizeLimit); 511 } 512 513 Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); 514 intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0); 515 intent.putExtra("android.intent.extra.sizeLimit", sizeLimit); 516 intent.putExtra("android.intent.extra.durationLimit", durationLimit); 517 intent.putExtra(MediaStore.EXTRA_OUTPUT, TempFileProvider.SCRAP_CONTENT_URI); 518 activity.startActivityForResult(intent, requestCode); 519 } 520 521 public static void capturePicture(Activity activity, int requestCode) { 522 Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); 523 intent.putExtra(MediaStore.EXTRA_OUTPUT, TempFileProvider.SCRAP_CONTENT_URI); 524 activity.startActivityForResult(intent, requestCode); 525 } 526 527 // Public for until tests 528 public static int getVideoCaptureDurationLimit(long bytesAvailable) { 529 CamcorderProfile camcorder = CamcorderProfile.get(CamcorderProfile.QUALITY_LOW); 530 if (camcorder == null) { 531 return 0; 532 } 533 bytesAvailable *= 8; // convert to bits 534 long seconds = bytesAvailable / (camcorder.audioBitRate + camcorder.videoBitRate); 535 536 // Find the best match for one of the fixed durations 537 for (int i = sVideoDuration.length - 1; i >= 0; i--) { 538 if (seconds >= sVideoDuration[i]) { 539 return sVideoDuration[i]; 540 } 541 } 542 return 0; 543 } 544 545 public static void selectVideo(Context context, int requestCode) { 546 selectMediaByType(context, requestCode, ContentType.VIDEO_UNSPECIFIED, true); 547 } 548 549 public static void selectImage(Context context, int requestCode) { 550 selectMediaByType(context, requestCode, ContentType.IMAGE_UNSPECIFIED, false); 551 } 552 553 private static void selectMediaByType( 554 Context context, int requestCode, String contentType, boolean localFilesOnly) { 555 if (context instanceof Activity) { 556 557 Intent innerIntent = new Intent(Intent.ACTION_GET_CONTENT); 558 559 innerIntent.setType(contentType); 560 if (localFilesOnly) { 561 innerIntent.putExtra(Intent.EXTRA_LOCAL_ONLY, true); 562 } 563 564 Intent wrapperIntent = Intent.createChooser(innerIntent, null); 565 566 ((Activity) context).startActivityForResult(wrapperIntent, requestCode); 567 } 568 } 569 570 public static void viewSimpleSlideshow(Context context, SlideshowModel slideshow) { 571 if (!slideshow.isSimple()) { 572 throw new IllegalArgumentException( 573 "viewSimpleSlideshow() called on a non-simple slideshow"); 574 } 575 SlideModel slide = slideshow.get(0); 576 MediaModel mm = null; 577 if (slide.hasImage()) { 578 mm = slide.getImage(); 579 } else if (slide.hasVideo()) { 580 mm = slide.getVideo(); 581 } 582 583 if (mm == null) { 584 return; 585 } 586 587 Intent intent = new Intent(Intent.ACTION_VIEW); 588 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 589 intent.putExtra("SingleItemOnly", true); // So we don't see "surrounding" images in Gallery 590 591 String contentType; 592 contentType = mm.getContentType(); 593 intent.setDataAndType(mm.getUri(), contentType); 594 context.startActivity(intent); 595 } 596 597 public static void showErrorDialog(Activity activity, 598 String title, String message) { 599 if (activity.isFinishing()) { 600 return; 601 } 602 AlertDialog.Builder builder = new AlertDialog.Builder(activity); 603 604 builder.setIcon(R.drawable.ic_sms_mms_not_delivered); 605 builder.setTitle(title); 606 builder.setMessage(message); 607 builder.setPositiveButton(android.R.string.ok, new OnClickListener() { 608 @Override 609 public void onClick(DialogInterface dialog, int which) { 610 if (which == DialogInterface.BUTTON_POSITIVE) { 611 dialog.dismiss(); 612 } 613 } 614 }); 615 builder.show(); 616 } 617 618 /** 619 * The quality parameter which is used to compress JPEG images. 620 */ 621 public static final int IMAGE_COMPRESSION_QUALITY = 95; 622 /** 623 * The minimum quality parameter which is used to compress JPEG images. 624 */ 625 public static final int MINIMUM_IMAGE_COMPRESSION_QUALITY = 50; 626 627 /** 628 * Message overhead that reduces the maximum image byte size. 629 * 5000 is a realistic overhead number that allows for user to also include 630 * a small MIDI file or a couple pages of text along with the picture. 631 */ 632 public static final int MESSAGE_OVERHEAD = 5000; 633 634 public static void resizeImageAsync(final Context context, 635 final Uri imageUri, final Handler handler, 636 final ResizeImageResultCallback cb, 637 final boolean append) { 638 639 // Show a progress toast if the resize hasn't finished 640 // within one second. 641 // Stash the runnable for showing it away so we can cancel 642 // it later if the resize completes ahead of the deadline. 643 final Runnable showProgress = new Runnable() { 644 @Override 645 public void run() { 646 Toast.makeText(context, R.string.compressing, Toast.LENGTH_SHORT).show(); 647 } 648 }; 649 // Schedule it for one second from now. 650 handler.postDelayed(showProgress, 1000); 651 652 new Thread(new Runnable() { 653 @Override 654 public void run() { 655 final PduPart part; 656 try { 657 UriImage image = new UriImage(context, imageUri); 658 int widthLimit = MmsConfig.getMaxImageWidth(); 659 int heightLimit = MmsConfig.getMaxImageHeight(); 660 // In mms_config.xml, the max width has always been declared larger than the max 661 // height. Swap the width and height limits if necessary so we scale the picture 662 // as little as possible. 663 if (image.getHeight() > image.getWidth()) { 664 int temp = widthLimit; 665 widthLimit = heightLimit; 666 heightLimit = temp; 667 } 668 669 part = image.getResizedImageAsPart( 670 widthLimit, 671 heightLimit, 672 MmsConfig.getMaxMessageSize() - MESSAGE_OVERHEAD); 673 } finally { 674 // Cancel pending show of the progress toast if necessary. 675 handler.removeCallbacks(showProgress); 676 } 677 678 handler.post(new Runnable() { 679 @Override 680 public void run() { 681 cb.onResizeResult(part, append); 682 } 683 }); 684 } 685 }, "MessageUtils.resizeImageAsync").start(); 686 } 687 688 public static void showDiscardDraftConfirmDialog(Context context, 689 OnClickListener listener) { 690 new AlertDialog.Builder(context) 691 .setMessage(R.string.discard_message_reason) 692 .setPositiveButton(R.string.yes, listener) 693 .setNegativeButton(R.string.no, null) 694 .show(); 695 } 696 697 public static String getLocalNumber() { 698 if (null == sLocalNumber) { 699 sLocalNumber = MmsApp.getApplication().getTelephonyManager().getLine1Number(); 700 } 701 return sLocalNumber; 702 } 703 704 public static boolean isLocalNumber(String number) { 705 if (number == null) { 706 return false; 707 } 708 709 // we don't use Mms.isEmailAddress() because it is too strict for comparing addresses like 710 // "foo+caf_=6505551212=tmomail.net (at) gmail.com", which is the 'from' address from a forwarded email 711 // message from Gmail. We don't want to treat "foo+caf_=6505551212=tmomail.net (at) gmail.com" and 712 // "6505551212" to be the same. 713 if (number.indexOf('@') >= 0) { 714 return false; 715 } 716 717 return PhoneNumberUtils.compare(number, getLocalNumber()); 718 } 719 720 public static void handleReadReport(final Context context, 721 final Collection<Long> threadIds, 722 final int status, 723 final Runnable callback) { 724 StringBuilder selectionBuilder = new StringBuilder(Mms.MESSAGE_TYPE + " = " 725 + PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF 726 + " AND " + Mms.READ + " = 0" 727 + " AND " + Mms.READ_REPORT + " = " + PduHeaders.VALUE_YES); 728 729 String[] selectionArgs = null; 730 if (threadIds != null) { 731 String threadIdSelection = null; 732 StringBuilder buf = new StringBuilder(); 733 selectionArgs = new String[threadIds.size()]; 734 int i = 0; 735 736 for (long threadId : threadIds) { 737 if (i > 0) { 738 buf.append(" OR "); 739 } 740 buf.append(Mms.THREAD_ID).append("=?"); 741 selectionArgs[i++] = Long.toString(threadId); 742 } 743 threadIdSelection = buf.toString(); 744 745 selectionBuilder.append(" AND (" + threadIdSelection + ")"); 746 } 747 748 final Cursor c = SqliteWrapper.query(context, context.getContentResolver(), 749 Mms.Inbox.CONTENT_URI, new String[] {Mms._ID, Mms.MESSAGE_ID}, 750 selectionBuilder.toString(), selectionArgs, null); 751 752 if (c == null) { 753 return; 754 } 755 756 final Map<String, String> map = new HashMap<String, String>(); 757 try { 758 if (c.getCount() == 0) { 759 if (callback != null) { 760 callback.run(); 761 } 762 return; 763 } 764 765 while (c.moveToNext()) { 766 Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, c.getLong(0)); 767 map.put(c.getString(1), AddressUtils.getFrom(context, uri)); 768 } 769 } finally { 770 c.close(); 771 } 772 773 OnClickListener positiveListener = new OnClickListener() { 774 @Override 775 public void onClick(DialogInterface dialog, int which) { 776 for (final Map.Entry<String, String> entry : map.entrySet()) { 777 MmsMessageSender.sendReadRec(context, entry.getValue(), 778 entry.getKey(), status); 779 } 780 781 if (callback != null) { 782 callback.run(); 783 } 784 dialog.dismiss(); 785 } 786 }; 787 788 OnClickListener negativeListener = new OnClickListener() { 789 @Override 790 public void onClick(DialogInterface dialog, int which) { 791 if (callback != null) { 792 callback.run(); 793 } 794 dialog.dismiss(); 795 } 796 }; 797 798 OnCancelListener cancelListener = new OnCancelListener() { 799 @Override 800 public void onCancel(DialogInterface dialog) { 801 if (callback != null) { 802 callback.run(); 803 } 804 dialog.dismiss(); 805 } 806 }; 807 808 confirmReadReportDialog(context, positiveListener, 809 negativeListener, 810 cancelListener); 811 } 812 813 private static void confirmReadReportDialog(Context context, 814 OnClickListener positiveListener, OnClickListener negativeListener, 815 OnCancelListener cancelListener) { 816 AlertDialog.Builder builder = new AlertDialog.Builder(context); 817 builder.setCancelable(true); 818 builder.setTitle(R.string.confirm); 819 builder.setMessage(R.string.message_send_read_report); 820 builder.setPositiveButton(R.string.yes, positiveListener); 821 builder.setNegativeButton(R.string.no, negativeListener); 822 builder.setOnCancelListener(cancelListener); 823 builder.show(); 824 } 825 826 public static String extractEncStrFromCursor(Cursor cursor, 827 int columnRawBytes, int columnCharset) { 828 String rawBytes = cursor.getString(columnRawBytes); 829 int charset = cursor.getInt(columnCharset); 830 831 if (TextUtils.isEmpty(rawBytes)) { 832 return ""; 833 } else if (charset == CharacterSets.ANY_CHARSET) { 834 return rawBytes; 835 } else { 836 return new EncodedStringValue(charset, PduPersister.getBytes(rawBytes)).getString(); 837 } 838 } 839 840 private static String extractEncStr(Context context, EncodedStringValue value) { 841 if (value != null) { 842 return value.getString(); 843 } else { 844 return ""; 845 } 846 } 847 848 public static ArrayList<String> extractUris(URLSpan[] spans) { 849 int size = spans.length; 850 ArrayList<String> accumulator = new ArrayList<String>(); 851 852 for (int i = 0; i < size; i++) { 853 accumulator.add(spans[i].getURL()); 854 } 855 return accumulator; 856 } 857 858 /** 859 * Play/view the message attachments. 860 * TOOD: We need to save the draft before launching another activity to view the attachments. 861 * This is hacky though since we will do saveDraft twice and slow down the UI. 862 * We should pass the slideshow in intent extra to the view activity instead of 863 * asking it to read attachments from database. 864 * @param activity 865 * @param msgUri the MMS message URI in database 866 * @param slideshow the slideshow to save 867 * @param persister the PDU persister for updating the database 868 * @param sendReq the SendReq for updating the database 869 */ 870 public static void viewMmsMessageAttachment(Activity activity, Uri msgUri, 871 SlideshowModel slideshow, AsyncDialog asyncDialog) { 872 viewMmsMessageAttachment(activity, msgUri, slideshow, 0, asyncDialog); 873 } 874 875 public static void viewMmsMessageAttachment(final Activity activity, final Uri msgUri, 876 final SlideshowModel slideshow, final int requestCode, AsyncDialog asyncDialog) { 877 boolean isSimple = (slideshow == null) ? false : slideshow.isSimple(); 878 if (isSimple) { 879 // In attachment-editor mode, we only ever have one slide. 880 MessageUtils.viewSimpleSlideshow(activity, slideshow); 881 } else { 882 // The user wants to view the slideshow. We have to persist the slideshow parts 883 // in a background task. If the task takes longer than a half second, a progress dialog 884 // is displayed. Once the PDU persisting is done, another runnable on the UI thread get 885 // executed to start the SlideshowActivity. 886 asyncDialog.runAsync(new Runnable() { 887 @Override 888 public void run() { 889 // If a slideshow was provided, save it to disk first. 890 if (slideshow != null) { 891 PduPersister persister = PduPersister.getPduPersister(activity); 892 try { 893 PduBody pb = slideshow.toPduBody(); 894 persister.updateParts(msgUri, pb, null); 895 slideshow.sync(pb); 896 } catch (MmsException e) { 897 Log.e(TAG, "Unable to save message for preview"); 898 return; 899 } 900 } 901 } 902 }, new Runnable() { 903 @Override 904 public void run() { 905 // Once the above background thread is complete, this runnable is run 906 // on the UI thread to launch the slideshow activity. 907 launchSlideshowActivity(activity, msgUri, requestCode); 908 } 909 }, R.string.building_slideshow_title); 910 } 911 } 912 913 public static void launchSlideshowActivity(Context context, Uri msgUri, int requestCode) { 914 // Launch the slideshow activity to play/view. 915 Intent intent = new Intent(context, SlideshowActivity.class); 916 intent.setData(msgUri); 917 intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); 918 if (requestCode > 0 && context instanceof Activity) { 919 ((Activity)context).startActivityForResult(intent, requestCode); 920 } else { 921 context.startActivity(intent); 922 } 923 924 } 925 926 /** 927 * Debugging 928 */ 929 public static void writeHprofDataToFile(){ 930 String filename = Environment.getExternalStorageDirectory() + "/mms_oom_hprof_data"; 931 try { 932 android.os.Debug.dumpHprofData(filename); 933 Log.i(TAG, "##### written hprof data to " + filename); 934 } catch (IOException ex) { 935 Log.e(TAG, "writeHprofDataToFile: caught " + ex); 936 } 937 } 938 939 // An alias (or commonly called "nickname") is: 940 // Nickname must begin with a letter. 941 // Only letters a-z, numbers 0-9, or . are allowed in Nickname field. 942 public static boolean isAlias(String string) { 943 if (!MmsConfig.isAliasEnabled()) { 944 return false; 945 } 946 947 int len = string == null ? 0 : string.length(); 948 949 if (len < MmsConfig.getAliasMinChars() || len > MmsConfig.getAliasMaxChars()) { 950 return false; 951 } 952 953 if (!Character.isLetter(string.charAt(0))) { // Nickname begins with a letter 954 return false; 955 } 956 for (int i = 1; i < len; i++) { 957 char c = string.charAt(i); 958 if (!(Character.isLetterOrDigit(c) || c == '.')) { 959 return false; 960 } 961 } 962 963 return true; 964 } 965 966 /** 967 * Given a phone number, return the string without syntactic sugar, meaning parens, 968 * spaces, slashes, dots, dashes, etc. If the input string contains non-numeric 969 * non-punctuation characters, return null. 970 */ 971 private static String parsePhoneNumberForMms(String address) { 972 StringBuilder builder = new StringBuilder(); 973 int len = address.length(); 974 975 for (int i = 0; i < len; i++) { 976 char c = address.charAt(i); 977 978 // accept the first '+' in the address 979 if (c == '+' && builder.length() == 0) { 980 builder.append(c); 981 continue; 982 } 983 984 if (Character.isDigit(c)) { 985 builder.append(c); 986 continue; 987 } 988 989 if (numericSugarMap.get(c) == null) { 990 return null; 991 } 992 } 993 return builder.toString(); 994 } 995 996 /** 997 * Returns true if the address passed in is a valid MMS address. 998 */ 999 public static boolean isValidMmsAddress(String address) { 1000 String retVal = parseMmsAddress(address); 1001 return (retVal != null); 1002 } 1003 1004 /** 1005 * parse the input address to be a valid MMS address. 1006 * - if the address is an email address, leave it as is. 1007 * - if the address can be parsed into a valid MMS phone number, return the parsed number. 1008 * - if the address is a compliant alias address, leave it as is. 1009 */ 1010 public static String parseMmsAddress(String address) { 1011 // if it's a valid Email address, use that. 1012 if (Mms.isEmailAddress(address)) { 1013 return address; 1014 } 1015 1016 // if we are able to parse the address to a MMS compliant phone number, take that. 1017 String retVal = parsePhoneNumberForMms(address); 1018 if (retVal != null && retVal.length() != 0) { 1019 return retVal; 1020 } 1021 1022 // if it's an alias compliant address, use that. 1023 if (isAlias(address)) { 1024 return address; 1025 } 1026 1027 // it's not a valid MMS address, return null 1028 return null; 1029 } 1030 1031 private static void log(String msg) { 1032 Log.d(TAG, "[MsgUtils] " + msg); 1033 } 1034 } 1035