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 Intent intent = new Intent(Intent.ACTION_VIEW); 584 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 585 intent.putExtra("SingleItemOnly", true); // So we don't see "surrounding" images in Gallery 586 587 String contentType; 588 contentType = mm.getContentType(); 589 intent.setDataAndType(mm.getUri(), contentType); 590 context.startActivity(intent); 591 } 592 593 public static void showErrorDialog(Activity activity, 594 String title, String message) { 595 if (activity.isFinishing()) { 596 return; 597 } 598 AlertDialog.Builder builder = new AlertDialog.Builder(activity); 599 600 builder.setIcon(R.drawable.ic_sms_mms_not_delivered); 601 builder.setTitle(title); 602 builder.setMessage(message); 603 builder.setPositiveButton(android.R.string.ok, new OnClickListener() { 604 @Override 605 public void onClick(DialogInterface dialog, int which) { 606 if (which == DialogInterface.BUTTON_POSITIVE) { 607 dialog.dismiss(); 608 } 609 } 610 }); 611 builder.show(); 612 } 613 614 /** 615 * The quality parameter which is used to compress JPEG images. 616 */ 617 public static final int IMAGE_COMPRESSION_QUALITY = 95; 618 /** 619 * The minimum quality parameter which is used to compress JPEG images. 620 */ 621 public static final int MINIMUM_IMAGE_COMPRESSION_QUALITY = 50; 622 623 /** 624 * Message overhead that reduces the maximum image byte size. 625 * 5000 is a realistic overhead number that allows for user to also include 626 * a small MIDI file or a couple pages of text along with the picture. 627 */ 628 public static final int MESSAGE_OVERHEAD = 5000; 629 630 public static void resizeImageAsync(final Context context, 631 final Uri imageUri, final Handler handler, 632 final ResizeImageResultCallback cb, 633 final boolean append) { 634 635 // Show a progress toast if the resize hasn't finished 636 // within one second. 637 // Stash the runnable for showing it away so we can cancel 638 // it later if the resize completes ahead of the deadline. 639 final Runnable showProgress = new Runnable() { 640 @Override 641 public void run() { 642 Toast.makeText(context, R.string.compressing, Toast.LENGTH_SHORT).show(); 643 } 644 }; 645 // Schedule it for one second from now. 646 handler.postDelayed(showProgress, 1000); 647 648 new Thread(new Runnable() { 649 @Override 650 public void run() { 651 final PduPart part; 652 try { 653 UriImage image = new UriImage(context, imageUri); 654 int widthLimit = MmsConfig.getMaxImageWidth(); 655 int heightLimit = MmsConfig.getMaxImageHeight(); 656 // In mms_config.xml, the max width has always been declared larger than the max 657 // height. Swap the width and height limits if necessary so we scale the picture 658 // as little as possible. 659 if (image.getHeight() > image.getWidth()) { 660 int temp = widthLimit; 661 widthLimit = heightLimit; 662 heightLimit = temp; 663 } 664 665 part = image.getResizedImageAsPart( 666 widthLimit, 667 heightLimit, 668 MmsConfig.getMaxMessageSize() - MESSAGE_OVERHEAD); 669 } finally { 670 // Cancel pending show of the progress toast if necessary. 671 handler.removeCallbacks(showProgress); 672 } 673 674 handler.post(new Runnable() { 675 @Override 676 public void run() { 677 cb.onResizeResult(part, append); 678 } 679 }); 680 } 681 }, "MessageUtils.resizeImageAsync").start(); 682 } 683 684 public static void showDiscardDraftConfirmDialog(Context context, 685 OnClickListener listener) { 686 new AlertDialog.Builder(context) 687 .setMessage(R.string.discard_message_reason) 688 .setPositiveButton(R.string.yes, listener) 689 .setNegativeButton(R.string.no, null) 690 .show(); 691 } 692 693 public static String getLocalNumber() { 694 if (null == sLocalNumber) { 695 sLocalNumber = MmsApp.getApplication().getTelephonyManager().getLine1Number(); 696 } 697 return sLocalNumber; 698 } 699 700 public static boolean isLocalNumber(String number) { 701 if (number == null) { 702 return false; 703 } 704 705 // we don't use Mms.isEmailAddress() because it is too strict for comparing addresses like 706 // "foo+caf_=6505551212=tmomail.net (at) gmail.com", which is the 'from' address from a forwarded email 707 // message from Gmail. We don't want to treat "foo+caf_=6505551212=tmomail.net (at) gmail.com" and 708 // "6505551212" to be the same. 709 if (number.indexOf('@') >= 0) { 710 return false; 711 } 712 713 return PhoneNumberUtils.compare(number, getLocalNumber()); 714 } 715 716 public static void handleReadReport(final Context context, 717 final Collection<Long> threadIds, 718 final int status, 719 final Runnable callback) { 720 StringBuilder selectionBuilder = new StringBuilder(Mms.MESSAGE_TYPE + " = " 721 + PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF 722 + " AND " + Mms.READ + " = 0" 723 + " AND " + Mms.READ_REPORT + " = " + PduHeaders.VALUE_YES); 724 725 String[] selectionArgs = null; 726 if (threadIds != null) { 727 String threadIdSelection = null; 728 StringBuilder buf = new StringBuilder(); 729 selectionArgs = new String[threadIds.size()]; 730 int i = 0; 731 732 for (long threadId : threadIds) { 733 if (i > 0) { 734 buf.append(" OR "); 735 } 736 buf.append(Mms.THREAD_ID).append("=?"); 737 selectionArgs[i++] = Long.toString(threadId); 738 } 739 threadIdSelection = buf.toString(); 740 741 selectionBuilder.append(" AND (" + threadIdSelection + ")"); 742 } 743 744 final Cursor c = SqliteWrapper.query(context, context.getContentResolver(), 745 Mms.Inbox.CONTENT_URI, new String[] {Mms._ID, Mms.MESSAGE_ID}, 746 selectionBuilder.toString(), selectionArgs, null); 747 748 if (c == null) { 749 return; 750 } 751 752 final Map<String, String> map = new HashMap<String, String>(); 753 try { 754 if (c.getCount() == 0) { 755 if (callback != null) { 756 callback.run(); 757 } 758 return; 759 } 760 761 while (c.moveToNext()) { 762 Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, c.getLong(0)); 763 map.put(c.getString(1), AddressUtils.getFrom(context, uri)); 764 } 765 } finally { 766 c.close(); 767 } 768 769 OnClickListener positiveListener = new OnClickListener() { 770 @Override 771 public void onClick(DialogInterface dialog, int which) { 772 for (final Map.Entry<String, String> entry : map.entrySet()) { 773 MmsMessageSender.sendReadRec(context, entry.getValue(), 774 entry.getKey(), status); 775 } 776 777 if (callback != null) { 778 callback.run(); 779 } 780 dialog.dismiss(); 781 } 782 }; 783 784 OnClickListener negativeListener = new OnClickListener() { 785 @Override 786 public void onClick(DialogInterface dialog, int which) { 787 if (callback != null) { 788 callback.run(); 789 } 790 dialog.dismiss(); 791 } 792 }; 793 794 OnCancelListener cancelListener = new OnCancelListener() { 795 @Override 796 public void onCancel(DialogInterface dialog) { 797 if (callback != null) { 798 callback.run(); 799 } 800 dialog.dismiss(); 801 } 802 }; 803 804 confirmReadReportDialog(context, positiveListener, 805 negativeListener, 806 cancelListener); 807 } 808 809 private static void confirmReadReportDialog(Context context, 810 OnClickListener positiveListener, OnClickListener negativeListener, 811 OnCancelListener cancelListener) { 812 AlertDialog.Builder builder = new AlertDialog.Builder(context); 813 builder.setCancelable(true); 814 builder.setTitle(R.string.confirm); 815 builder.setMessage(R.string.message_send_read_report); 816 builder.setPositiveButton(R.string.yes, positiveListener); 817 builder.setNegativeButton(R.string.no, negativeListener); 818 builder.setOnCancelListener(cancelListener); 819 builder.show(); 820 } 821 822 public static String extractEncStrFromCursor(Cursor cursor, 823 int columnRawBytes, int columnCharset) { 824 String rawBytes = cursor.getString(columnRawBytes); 825 int charset = cursor.getInt(columnCharset); 826 827 if (TextUtils.isEmpty(rawBytes)) { 828 return ""; 829 } else if (charset == CharacterSets.ANY_CHARSET) { 830 return rawBytes; 831 } else { 832 return new EncodedStringValue(charset, PduPersister.getBytes(rawBytes)).getString(); 833 } 834 } 835 836 private static String extractEncStr(Context context, EncodedStringValue value) { 837 if (value != null) { 838 return value.getString(); 839 } else { 840 return ""; 841 } 842 } 843 844 public static ArrayList<String> extractUris(URLSpan[] spans) { 845 int size = spans.length; 846 ArrayList<String> accumulator = new ArrayList<String>(); 847 848 for (int i = 0; i < size; i++) { 849 accumulator.add(spans[i].getURL()); 850 } 851 return accumulator; 852 } 853 854 /** 855 * Play/view the message attachments. 856 * TOOD: We need to save the draft before launching another activity to view the attachments. 857 * This is hacky though since we will do saveDraft twice and slow down the UI. 858 * We should pass the slideshow in intent extra to the view activity instead of 859 * asking it to read attachments from database. 860 * @param activity 861 * @param msgUri the MMS message URI in database 862 * @param slideshow the slideshow to save 863 * @param persister the PDU persister for updating the database 864 * @param sendReq the SendReq for updating the database 865 */ 866 public static void viewMmsMessageAttachment(Activity activity, Uri msgUri, 867 SlideshowModel slideshow, AsyncDialog asyncDialog) { 868 viewMmsMessageAttachment(activity, msgUri, slideshow, 0, asyncDialog); 869 } 870 871 public static void viewMmsMessageAttachment(final Activity activity, final Uri msgUri, 872 final SlideshowModel slideshow, final int requestCode, AsyncDialog asyncDialog) { 873 boolean isSimple = (slideshow == null) ? false : slideshow.isSimple(); 874 if (isSimple) { 875 // In attachment-editor mode, we only ever have one slide. 876 MessageUtils.viewSimpleSlideshow(activity, slideshow); 877 } else { 878 // The user wants to view the slideshow. We have to persist the slideshow parts 879 // in a background task. If the task takes longer than a half second, a progress dialog 880 // is displayed. Once the PDU persisting is done, another runnable on the UI thread get 881 // executed to start the SlideshowActivity. 882 asyncDialog.runAsync(new Runnable() { 883 @Override 884 public void run() { 885 // If a slideshow was provided, save it to disk first. 886 if (slideshow != null) { 887 PduPersister persister = PduPersister.getPduPersister(activity); 888 try { 889 PduBody pb = slideshow.toPduBody(); 890 persister.updateParts(msgUri, pb, null); 891 slideshow.sync(pb); 892 } catch (MmsException e) { 893 Log.e(TAG, "Unable to save message for preview"); 894 return; 895 } 896 } 897 } 898 }, new Runnable() { 899 @Override 900 public void run() { 901 // Once the above background thread is complete, this runnable is run 902 // on the UI thread to launch the slideshow activity. 903 launchSlideshowActivity(activity, msgUri, requestCode); 904 } 905 }, R.string.building_slideshow_title); 906 } 907 } 908 909 public static void launchSlideshowActivity(Context context, Uri msgUri, int requestCode) { 910 // Launch the slideshow activity to play/view. 911 Intent intent = new Intent(context, SlideshowActivity.class); 912 intent.setData(msgUri); 913 intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); 914 if (requestCode > 0 && context instanceof Activity) { 915 ((Activity)context).startActivityForResult(intent, requestCode); 916 } else { 917 context.startActivity(intent); 918 } 919 920 } 921 922 /** 923 * Debugging 924 */ 925 public static void writeHprofDataToFile(){ 926 String filename = Environment.getExternalStorageDirectory() + "/mms_oom_hprof_data"; 927 try { 928 android.os.Debug.dumpHprofData(filename); 929 Log.i(TAG, "##### written hprof data to " + filename); 930 } catch (IOException ex) { 931 Log.e(TAG, "writeHprofDataToFile: caught " + ex); 932 } 933 } 934 935 // An alias (or commonly called "nickname") is: 936 // Nickname must begin with a letter. 937 // Only letters a-z, numbers 0-9, or . are allowed in Nickname field. 938 public static boolean isAlias(String string) { 939 if (!MmsConfig.isAliasEnabled()) { 940 return false; 941 } 942 943 int len = string == null ? 0 : string.length(); 944 945 if (len < MmsConfig.getAliasMinChars() || len > MmsConfig.getAliasMaxChars()) { 946 return false; 947 } 948 949 if (!Character.isLetter(string.charAt(0))) { // Nickname begins with a letter 950 return false; 951 } 952 for (int i = 1; i < len; i++) { 953 char c = string.charAt(i); 954 if (!(Character.isLetterOrDigit(c) || c == '.')) { 955 return false; 956 } 957 } 958 959 return true; 960 } 961 962 /** 963 * Given a phone number, return the string without syntactic sugar, meaning parens, 964 * spaces, slashes, dots, dashes, etc. If the input string contains non-numeric 965 * non-punctuation characters, return null. 966 */ 967 private static String parsePhoneNumberForMms(String address) { 968 StringBuilder builder = new StringBuilder(); 969 int len = address.length(); 970 971 for (int i = 0; i < len; i++) { 972 char c = address.charAt(i); 973 974 // accept the first '+' in the address 975 if (c == '+' && builder.length() == 0) { 976 builder.append(c); 977 continue; 978 } 979 980 if (Character.isDigit(c)) { 981 builder.append(c); 982 continue; 983 } 984 985 if (numericSugarMap.get(c) == null) { 986 return null; 987 } 988 } 989 return builder.toString(); 990 } 991 992 /** 993 * Returns true if the address passed in is a valid MMS address. 994 */ 995 public static boolean isValidMmsAddress(String address) { 996 String retVal = parseMmsAddress(address); 997 return (retVal != null); 998 } 999 1000 /** 1001 * parse the input address to be a valid MMS address. 1002 * - if the address is an email address, leave it as is. 1003 * - if the address can be parsed into a valid MMS phone number, return the parsed number. 1004 * - if the address is a compliant alias address, leave it as is. 1005 */ 1006 public static String parseMmsAddress(String address) { 1007 // if it's a valid Email address, use that. 1008 if (Mms.isEmailAddress(address)) { 1009 return address; 1010 } 1011 1012 // if we are able to parse the address to a MMS compliant phone number, take that. 1013 String retVal = parsePhoneNumberForMms(address); 1014 if (retVal != null) { 1015 return retVal; 1016 } 1017 1018 // if it's an alias compliant address, use that. 1019 if (isAlias(address)) { 1020 return address; 1021 } 1022 1023 // it's not a valid MMS address, return null 1024 return null; 1025 } 1026 1027 private static void log(String msg) { 1028 Log.d(TAG, "[MsgUtils] " + msg); 1029 } 1030 } 1031