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