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 { 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++) { 112 numericSugarMap.put(NUMERIC_CHARS_SUGAR[i], NUMERIC_CHARS_SUGAR[i]); 113 } 114 } 115 116 117 private MessageUtils() { 118 // Forbidden being instantiated. 119 } 120 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 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 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 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 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 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 387 public static String formatTimeStampString(Context context, long when) { 388 return formatTimeStampString(context, when, false); 389 } 390 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 */ 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 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 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 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 514 public static void selectVideo(Context context, int requestCode) { 515 selectMediaByType(context, requestCode, ContentType.VIDEO_UNSPECIFIED); 516 } 517 518 public static void selectImage(Context context, int requestCode) { 519 selectMediaByType(context, requestCode, ContentType.IMAGE_UNSPECIFIED); 520 } 521 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 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 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 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 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 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 670 public static String getLocalNumber() { 671 if (null == sLocalNumber) { 672 sLocalNumber = MmsApp.getApplication().getTelephonyManager().getLine1Number(); 673 } 674 return sLocalNumber; 675 } 676 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 (at) 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 (at) 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 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 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 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 794 private static String extractEncStr(Context context, EncodedStringValue value) { 795 if (value != null) { 796 return value.getString(); 797 } else { 798 return ""; 799 } 800 } 801 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 */ 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 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 */ 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 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 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 */ 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 */ 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 */ 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 994 private static void log(String msg) { 995 Log.d(TAG, "[MsgUtils] " + msg); 996 } 997 } 998