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