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