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