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