Home | History | Annotate | Download | only in ui
      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 {
     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++) {
    116             numericSugarMap.put(NUMERIC_CHARS_SUGAR[i], NUMERIC_CHARS_SUGAR[i]);
    117         }
    118     }
    119 
    120 
    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      */
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    446     public static String formatTimeStampString(Context context, long when) {
    447         return formatTimeStampString(context, when, false);
    448     }
    449 
    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 
    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 
    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 
    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 
    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
    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 
    545     public static void selectVideo(Context context, int requestCode) {
    546         selectMediaByType(context, requestCode, ContentType.VIDEO_UNSPECIFIED, true);
    547     }
    548 
    549     public static void selectImage(Context context, int requestCode) {
    550         selectMediaByType(context, requestCode, ContentType.IMAGE_UNSPECIFIED, false);
    551     }
    552 
    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 
    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 
    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 
    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 
    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 
    697     public static String getLocalNumber() {
    698         if (null == sLocalNumber) {
    699             sLocalNumber = MmsApp.getApplication().getTelephonyManager().getLine1Number();
    700         }
    701         return sLocalNumber;
    702     }
    703 
    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 (at) 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 (at) 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 
    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 
    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 
    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 
    840     private static String extractEncStr(Context context, EncodedStringValue value) {
    841         if (value != null) {
    842             return value.getString();
    843         } else {
    844             return "";
    845         }
    846     }
    847 
    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      */
    870     public static void viewMmsMessageAttachment(Activity activity, Uri msgUri,
    871             SlideshowModel slideshow, AsyncDialog asyncDialog) {
    872         viewMmsMessageAttachment(activity, msgUri, slideshow, 0, asyncDialog);
    873     }
    874 
    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 
    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      */
    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.
    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      */
    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      */
    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      */
   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 
   1031     private static void log(String msg) {
   1032         Log.d(TAG, "[MsgUtils] " + msg);
   1033     }
   1034 }
   1035