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, MultimediaMessagePdu mmp)405 public static int getAttachmentType(SlideshowModel model, MultimediaMessagePdu mmp) { 406 if (model == null || mmp == 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 // Handle the multimedia message only has subject 437 String subject = mmp.getSubject() != null ? mmp.getSubject().getString() : null; 438 if (!TextUtils.isEmpty(subject)) { 439 return WorkingMessage.TEXT; 440 } 441 } 442 443 return MessageItem.ATTACHMENT_TYPE_NOT_LOADED; 444 } 445 formatTimeStampString(Context context, long when)446 public static String formatTimeStampString(Context context, long when) { 447 return formatTimeStampString(context, when, false); 448 } 449 formatTimeStampString(Context context, long when, boolean fullFormat)450 public static String formatTimeStampString(Context context, long when, boolean fullFormat) { 451 Time then = new Time(); 452 then.set(when); 453 Time now = new Time(); 454 now.setToNow(); 455 456 // Basic settings for formatDateTime() we want for all cases. 457 int format_flags = DateUtils.FORMAT_NO_NOON_MIDNIGHT | 458 DateUtils.FORMAT_ABBREV_ALL | 459 DateUtils.FORMAT_CAP_AMPM; 460 461 // If the message is from a different year, show the date and year. 462 if (then.year != now.year) { 463 format_flags |= DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_DATE; 464 } else if (then.yearDay != now.yearDay) { 465 // If it is from a different day than today, show only the date. 466 format_flags |= DateUtils.FORMAT_SHOW_DATE; 467 } else { 468 // Otherwise, if the message is from today, show the time. 469 format_flags |= DateUtils.FORMAT_SHOW_TIME; 470 } 471 472 // If the caller has asked for full details, make sure to show the date 473 // and time no matter what we've determined above (but still make showing 474 // the year only happen if it is a different year from today). 475 if (fullFormat) { 476 format_flags |= (DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME); 477 } 478 479 return DateUtils.formatDateTime(context, when, format_flags); 480 } 481 selectAudio(Activity activity, int requestCode)482 public static void selectAudio(Activity activity, int requestCode) { 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 activity.getString(R.string.select_audio)); 489 activity.startActivityForResult(intent, requestCode); 490 } 491 recordSound(Activity activity, int requestCode, long sizeLimit)492 public static void recordSound(Activity activity, int requestCode, long sizeLimit) { 493 Intent intent = new Intent(Intent.ACTION_GET_CONTENT); 494 intent.setType(ContentType.AUDIO_AMR); 495 intent.setClassName("com.android.soundrecorder", 496 "com.android.soundrecorder.SoundRecorder"); 497 intent.putExtra(android.provider.MediaStore.Audio.Media.EXTRA_MAX_BYTES, sizeLimit); 498 activity.startActivityForResult(intent, requestCode); 499 } 500 recordVideo(Activity activity, int requestCode, long sizeLimit)501 public static void recordVideo(Activity activity, int requestCode, long sizeLimit) { 502 // The video recorder can sometimes return a file that's larger than the max we 503 // say we can handle. Try to handle that overshoot by specifying an 85% limit. 504 sizeLimit *= .85F; 505 506 int durationLimit = getVideoCaptureDurationLimit(sizeLimit); 507 508 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 509 log("recordVideo: durationLimit: " + durationLimit + 510 " sizeLimit: " + sizeLimit); 511 } 512 513 Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); 514 intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0); 515 intent.putExtra("android.intent.extra.sizeLimit", sizeLimit); 516 intent.putExtra("android.intent.extra.durationLimit", durationLimit); 517 intent.putExtra(MediaStore.EXTRA_OUTPUT, TempFileProvider.SCRAP_CONTENT_URI); 518 activity.startActivityForResult(intent, requestCode); 519 } 520 capturePicture(Activity activity, int requestCode)521 public static void capturePicture(Activity activity, int requestCode) { 522 Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); 523 intent.putExtra(MediaStore.EXTRA_OUTPUT, TempFileProvider.SCRAP_CONTENT_URI); 524 activity.startActivityForResult(intent, requestCode); 525 } 526 527 // Public for until tests getVideoCaptureDurationLimit(long bytesAvailable)528 public static int getVideoCaptureDurationLimit(long bytesAvailable) { 529 CamcorderProfile camcorder = CamcorderProfile.get(CamcorderProfile.QUALITY_LOW); 530 if (camcorder == null) { 531 return 0; 532 } 533 bytesAvailable *= 8; // convert to bits 534 long seconds = bytesAvailable / (camcorder.audioBitRate + camcorder.videoBitRate); 535 536 // Find the best match for one of the fixed durations 537 for (int i = sVideoDuration.length - 1; i >= 0; i--) { 538 if (seconds >= sVideoDuration[i]) { 539 return sVideoDuration[i]; 540 } 541 } 542 return 0; 543 } 544 selectVideo(Context context, int requestCode)545 public static void selectVideo(Context context, int requestCode) { 546 selectMediaByType(context, requestCode, ContentType.VIDEO_UNSPECIFIED, true); 547 } 548 selectImage(Context context, int requestCode)549 public static void selectImage(Context context, int requestCode) { 550 selectMediaByType(context, requestCode, ContentType.IMAGE_UNSPECIFIED, false); 551 } 552 selectMediaByType( Context context, int requestCode, String contentType, boolean localFilesOnly)553 private static void selectMediaByType( 554 Context context, int requestCode, String contentType, boolean localFilesOnly) { 555 if (context instanceof Activity) { 556 557 Intent innerIntent = new Intent(Intent.ACTION_GET_CONTENT); 558 559 innerIntent.setType(contentType); 560 if (localFilesOnly) { 561 innerIntent.putExtra(Intent.EXTRA_LOCAL_ONLY, true); 562 } 563 564 Intent wrapperIntent = Intent.createChooser(innerIntent, null); 565 566 ((Activity) context).startActivityForResult(wrapperIntent, requestCode); 567 } 568 } 569 viewSimpleSlideshow(Context context, SlideshowModel slideshow)570 public static void viewSimpleSlideshow(Context context, SlideshowModel slideshow) { 571 if (!slideshow.isSimple()) { 572 throw new IllegalArgumentException( 573 "viewSimpleSlideshow() called on a non-simple slideshow"); 574 } 575 SlideModel slide = slideshow.get(0); 576 MediaModel mm = null; 577 if (slide.hasImage()) { 578 mm = slide.getImage(); 579 } else if (slide.hasVideo()) { 580 mm = slide.getVideo(); 581 } 582 583 if (mm == null) { 584 return; 585 } 586 587 Intent intent = new Intent(Intent.ACTION_VIEW); 588 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 589 intent.putExtra("SingleItemOnly", true); // So we don't see "surrounding" images in Gallery 590 591 String contentType; 592 contentType = mm.getContentType(); 593 intent.setDataAndType(mm.getUri(), contentType); 594 context.startActivity(intent); 595 } 596 showErrorDialog(Activity activity, String title, String message)597 public static void showErrorDialog(Activity activity, 598 String title, String message) { 599 if (activity.isFinishing()) { 600 return; 601 } 602 AlertDialog.Builder builder = new AlertDialog.Builder(activity); 603 604 builder.setIcon(R.drawable.ic_sms_mms_not_delivered); 605 builder.setTitle(title); 606 builder.setMessage(message); 607 builder.setPositiveButton(android.R.string.ok, new OnClickListener() { 608 @Override 609 public void onClick(DialogInterface dialog, int which) { 610 if (which == DialogInterface.BUTTON_POSITIVE) { 611 dialog.dismiss(); 612 } 613 } 614 }); 615 builder.show(); 616 } 617 618 /** 619 * The quality parameter which is used to compress JPEG images. 620 */ 621 public static final int IMAGE_COMPRESSION_QUALITY = 95; 622 /** 623 * The minimum quality parameter which is used to compress JPEG images. 624 */ 625 public static final int MINIMUM_IMAGE_COMPRESSION_QUALITY = 50; 626 627 /** 628 * Message overhead that reduces the maximum image byte size. 629 * 5000 is a realistic overhead number that allows for user to also include 630 * a small MIDI file or a couple pages of text along with the picture. 631 */ 632 public static final int MESSAGE_OVERHEAD = 5000; 633 resizeImageAsync(final Context context, final Uri imageUri, final Handler handler, final ResizeImageResultCallback cb, final boolean append)634 public static void resizeImageAsync(final Context context, 635 final Uri imageUri, final Handler handler, 636 final ResizeImageResultCallback cb, 637 final boolean append) { 638 639 // Show a progress toast if the resize hasn't finished 640 // within one second. 641 // Stash the runnable for showing it away so we can cancel 642 // it later if the resize completes ahead of the deadline. 643 final Runnable showProgress = new Runnable() { 644 @Override 645 public void run() { 646 Toast.makeText(context, R.string.compressing, Toast.LENGTH_SHORT).show(); 647 } 648 }; 649 // Schedule it for one second from now. 650 handler.postDelayed(showProgress, 1000); 651 652 new Thread(new Runnable() { 653 @Override 654 public void run() { 655 final PduPart part; 656 try { 657 UriImage image = new UriImage(context, imageUri); 658 int widthLimit = MmsConfig.getMaxImageWidth(); 659 int heightLimit = MmsConfig.getMaxImageHeight(); 660 // In mms_config.xml, the max width has always been declared larger than the max 661 // height. Swap the width and height limits if necessary so we scale the picture 662 // as little as possible. 663 if (image.getHeight() > image.getWidth()) { 664 int temp = widthLimit; 665 widthLimit = heightLimit; 666 heightLimit = temp; 667 } 668 669 part = image.getResizedImageAsPart( 670 widthLimit, 671 heightLimit, 672 MmsConfig.getMaxMessageSize() - MESSAGE_OVERHEAD); 673 } finally { 674 // Cancel pending show of the progress toast if necessary. 675 handler.removeCallbacks(showProgress); 676 } 677 678 handler.post(new Runnable() { 679 @Override 680 public void run() { 681 cb.onResizeResult(part, append); 682 } 683 }); 684 } 685 }, "MessageUtils.resizeImageAsync").start(); 686 } 687 showDiscardDraftConfirmDialog(Context context, OnClickListener listener)688 public static void showDiscardDraftConfirmDialog(Context context, 689 OnClickListener listener) { 690 new AlertDialog.Builder(context) 691 .setMessage(R.string.discard_message_reason) 692 .setPositiveButton(R.string.yes, listener) 693 .setNegativeButton(R.string.no, null) 694 .show(); 695 } 696 getLocalNumber()697 public static String getLocalNumber() { 698 if (null == sLocalNumber) { 699 sLocalNumber = MmsApp.getApplication().getTelephonyManager().getLine1Number(); 700 } 701 return sLocalNumber; 702 } 703 isLocalNumber(String number)704 public static boolean isLocalNumber(String number) { 705 if (number == null) { 706 return false; 707 } 708 709 // we don't use Mms.isEmailAddress() because it is too strict for comparing addresses like 710 // "foo+caf_=6505551212=tmomail.net@gmail.com", which is the 'from' address from a forwarded email 711 // message from Gmail. We don't want to treat "foo+caf_=6505551212=tmomail.net@gmail.com" and 712 // "6505551212" to be the same. 713 if (number.indexOf('@') >= 0) { 714 return false; 715 } 716 717 return PhoneNumberUtils.compare(number, getLocalNumber()); 718 } 719 handleReadReport(final Context context, final Collection<Long> threadIds, final int status, final Runnable callback)720 public static void handleReadReport(final Context context, 721 final Collection<Long> threadIds, 722 final int status, 723 final Runnable callback) { 724 StringBuilder selectionBuilder = new StringBuilder(Mms.MESSAGE_TYPE + " = " 725 + PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF 726 + " AND " + Mms.READ + " = 0" 727 + " AND " + Mms.READ_REPORT + " = " + PduHeaders.VALUE_YES); 728 729 String[] selectionArgs = null; 730 if (threadIds != null) { 731 String threadIdSelection = null; 732 StringBuilder buf = new StringBuilder(); 733 selectionArgs = new String[threadIds.size()]; 734 int i = 0; 735 736 for (long threadId : threadIds) { 737 if (i > 0) { 738 buf.append(" OR "); 739 } 740 buf.append(Mms.THREAD_ID).append("=?"); 741 selectionArgs[i++] = Long.toString(threadId); 742 } 743 threadIdSelection = buf.toString(); 744 745 selectionBuilder.append(" AND (" + threadIdSelection + ")"); 746 } 747 748 final Cursor c = SqliteWrapper.query(context, context.getContentResolver(), 749 Mms.Inbox.CONTENT_URI, new String[] {Mms._ID, Mms.MESSAGE_ID}, 750 selectionBuilder.toString(), selectionArgs, null); 751 752 if (c == null) { 753 return; 754 } 755 756 final Map<String, String> map = new HashMap<String, String>(); 757 try { 758 if (c.getCount() == 0) { 759 if (callback != null) { 760 callback.run(); 761 } 762 return; 763 } 764 765 while (c.moveToNext()) { 766 Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, c.getLong(0)); 767 map.put(c.getString(1), AddressUtils.getFrom(context, uri)); 768 } 769 } finally { 770 c.close(); 771 } 772 773 OnClickListener positiveListener = new OnClickListener() { 774 @Override 775 public void onClick(DialogInterface dialog, int which) { 776 for (final Map.Entry<String, String> entry : map.entrySet()) { 777 MmsMessageSender.sendReadRec(context, entry.getValue(), 778 entry.getKey(), status); 779 } 780 781 if (callback != null) { 782 callback.run(); 783 } 784 dialog.dismiss(); 785 } 786 }; 787 788 OnClickListener negativeListener = new OnClickListener() { 789 @Override 790 public void onClick(DialogInterface dialog, int which) { 791 if (callback != null) { 792 callback.run(); 793 } 794 dialog.dismiss(); 795 } 796 }; 797 798 OnCancelListener cancelListener = new OnCancelListener() { 799 @Override 800 public void onCancel(DialogInterface dialog) { 801 if (callback != null) { 802 callback.run(); 803 } 804 dialog.dismiss(); 805 } 806 }; 807 808 confirmReadReportDialog(context, positiveListener, 809 negativeListener, 810 cancelListener); 811 } 812 confirmReadReportDialog(Context context, OnClickListener positiveListener, OnClickListener negativeListener, OnCancelListener cancelListener)813 private static void confirmReadReportDialog(Context context, 814 OnClickListener positiveListener, OnClickListener negativeListener, 815 OnCancelListener cancelListener) { 816 AlertDialog.Builder builder = new AlertDialog.Builder(context); 817 builder.setCancelable(true); 818 builder.setTitle(R.string.confirm); 819 builder.setMessage(R.string.message_send_read_report); 820 builder.setPositiveButton(R.string.yes, positiveListener); 821 builder.setNegativeButton(R.string.no, negativeListener); 822 builder.setOnCancelListener(cancelListener); 823 builder.show(); 824 } 825 extractEncStrFromCursor(Cursor cursor, int columnRawBytes, int columnCharset)826 public static String extractEncStrFromCursor(Cursor cursor, 827 int columnRawBytes, int columnCharset) { 828 String rawBytes = cursor.getString(columnRawBytes); 829 int charset = cursor.getInt(columnCharset); 830 831 if (TextUtils.isEmpty(rawBytes)) { 832 return ""; 833 } else if (charset == CharacterSets.ANY_CHARSET) { 834 return rawBytes; 835 } else { 836 return new EncodedStringValue(charset, PduPersister.getBytes(rawBytes)).getString(); 837 } 838 } 839 extractEncStr(Context context, EncodedStringValue value)840 private static String extractEncStr(Context context, EncodedStringValue value) { 841 if (value != null) { 842 return value.getString(); 843 } else { 844 return ""; 845 } 846 } 847 extractUris(URLSpan[] spans)848 public static ArrayList<String> extractUris(URLSpan[] spans) { 849 int size = spans.length; 850 ArrayList<String> accumulator = new ArrayList<String>(); 851 852 for (int i = 0; i < size; i++) { 853 accumulator.add(spans[i].getURL()); 854 } 855 return accumulator; 856 } 857 858 /** 859 * Play/view the message attachments. 860 * TOOD: We need to save the draft before launching another activity to view the attachments. 861 * This is hacky though since we will do saveDraft twice and slow down the UI. 862 * We should pass the slideshow in intent extra to the view activity instead of 863 * asking it to read attachments from database. 864 * @param activity 865 * @param msgUri the MMS message URI in database 866 * @param slideshow the slideshow to save 867 * @param persister the PDU persister for updating the database 868 * @param sendReq the SendReq for updating the database 869 */ viewMmsMessageAttachment(Activity activity, Uri msgUri, SlideshowModel slideshow, AsyncDialog asyncDialog)870 public static void viewMmsMessageAttachment(Activity activity, Uri msgUri, 871 SlideshowModel slideshow, AsyncDialog asyncDialog) { 872 viewMmsMessageAttachment(activity, msgUri, slideshow, 0, asyncDialog); 873 } 874 viewMmsMessageAttachment(final Activity activity, final Uri msgUri, final SlideshowModel slideshow, final int requestCode, AsyncDialog asyncDialog)875 public static void viewMmsMessageAttachment(final Activity activity, final Uri msgUri, 876 final SlideshowModel slideshow, final int requestCode, AsyncDialog asyncDialog) { 877 boolean isSimple = (slideshow == null) ? false : slideshow.isSimple(); 878 if (isSimple) { 879 // In attachment-editor mode, we only ever have one slide. 880 MessageUtils.viewSimpleSlideshow(activity, slideshow); 881 } else { 882 // The user wants to view the slideshow. We have to persist the slideshow parts 883 // in a background task. If the task takes longer than a half second, a progress dialog 884 // is displayed. Once the PDU persisting is done, another runnable on the UI thread get 885 // executed to start the SlideshowActivity. 886 asyncDialog.runAsync(new Runnable() { 887 @Override 888 public void run() { 889 // If a slideshow was provided, save it to disk first. 890 if (slideshow != null) { 891 PduPersister persister = PduPersister.getPduPersister(activity); 892 try { 893 PduBody pb = slideshow.toPduBody(); 894 persister.updateParts(msgUri, pb, null); 895 slideshow.sync(pb); 896 } catch (MmsException e) { 897 Log.e(TAG, "Unable to save message for preview"); 898 return; 899 } 900 } 901 } 902 }, new Runnable() { 903 @Override 904 public void run() { 905 // Once the above background thread is complete, this runnable is run 906 // on the UI thread to launch the slideshow activity. 907 launchSlideshowActivity(activity, msgUri, requestCode); 908 } 909 }, R.string.building_slideshow_title); 910 } 911 } 912 launchSlideshowActivity(Context context, Uri msgUri, int requestCode)913 public static void launchSlideshowActivity(Context context, Uri msgUri, int requestCode) { 914 // Launch the slideshow activity to play/view. 915 Intent intent = new Intent(context, SlideshowActivity.class); 916 intent.setData(msgUri); 917 intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); 918 if (requestCode > 0 && context instanceof Activity) { 919 ((Activity)context).startActivityForResult(intent, requestCode); 920 } else { 921 context.startActivity(intent); 922 } 923 924 } 925 926 /** 927 * Debugging 928 */ writeHprofDataToFile()929 public static void writeHprofDataToFile(){ 930 String filename = Environment.getExternalStorageDirectory() + "/mms_oom_hprof_data"; 931 try { 932 android.os.Debug.dumpHprofData(filename); 933 Log.i(TAG, "##### written hprof data to " + filename); 934 } catch (IOException ex) { 935 Log.e(TAG, "writeHprofDataToFile: caught " + ex); 936 } 937 } 938 939 // An alias (or commonly called "nickname") is: 940 // Nickname must begin with a letter. 941 // Only letters a-z, numbers 0-9, or . are allowed in Nickname field. isAlias(String string)942 public static boolean isAlias(String string) { 943 if (!MmsConfig.isAliasEnabled()) { 944 return false; 945 } 946 947 int len = string == null ? 0 : string.length(); 948 949 if (len < MmsConfig.getAliasMinChars() || len > MmsConfig.getAliasMaxChars()) { 950 return false; 951 } 952 953 if (!Character.isLetter(string.charAt(0))) { // Nickname begins with a letter 954 return false; 955 } 956 for (int i = 1; i < len; i++) { 957 char c = string.charAt(i); 958 if (!(Character.isLetterOrDigit(c) || c == '.')) { 959 return false; 960 } 961 } 962 963 return true; 964 } 965 966 /** 967 * Given a phone number, return the string without syntactic sugar, meaning parens, 968 * spaces, slashes, dots, dashes, etc. If the input string contains non-numeric 969 * non-punctuation characters, return null. 970 */ parsePhoneNumberForMms(String address)971 private static String parsePhoneNumberForMms(String address) { 972 StringBuilder builder = new StringBuilder(); 973 int len = address.length(); 974 975 for (int i = 0; i < len; i++) { 976 char c = address.charAt(i); 977 978 // accept the first '+' in the address 979 if (c == '+' && builder.length() == 0) { 980 builder.append(c); 981 continue; 982 } 983 984 if (Character.isDigit(c)) { 985 builder.append(c); 986 continue; 987 } 988 989 if (numericSugarMap.get(c) == null) { 990 return null; 991 } 992 } 993 return builder.toString(); 994 } 995 996 /** 997 * Returns true if the address passed in is a valid MMS address. 998 */ isValidMmsAddress(String address)999 public static boolean isValidMmsAddress(String address) { 1000 String retVal = parseMmsAddress(address); 1001 return (retVal != null); 1002 } 1003 1004 /** 1005 * parse the input address to be a valid MMS address. 1006 * - if the address is an email address, leave it as is. 1007 * - if the address can be parsed into a valid MMS phone number, return the parsed number. 1008 * - if the address is a compliant alias address, leave it as is. 1009 */ parseMmsAddress(String address)1010 public static String parseMmsAddress(String address) { 1011 // if it's a valid Email address, use that. 1012 if (Mms.isEmailAddress(address)) { 1013 return address; 1014 } 1015 1016 // if we are able to parse the address to a MMS compliant phone number, take that. 1017 String retVal = parsePhoneNumberForMms(address); 1018 if (retVal != null && retVal.length() != 0) { 1019 return retVal; 1020 } 1021 1022 // if it's an alias compliant address, use that. 1023 if (isAlias(address)) { 1024 return address; 1025 } 1026 1027 // it's not a valid MMS address, return null 1028 return null; 1029 } 1030 log(String msg)1031 private static void log(String msg) { 1032 Log.d(TAG, "[MsgUtils] " + msg); 1033 } 1034 } 1035