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