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         Intent intent = new Intent(Intent.ACTION_VIEW);
    584         intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
    585         intent.putExtra("SingleItemOnly", true); // So we don't see "surrounding" images in Gallery
    586 
    587         String contentType;
    588         contentType = mm.getContentType();
    589         intent.setDataAndType(mm.getUri(), contentType);
    590         context.startActivity(intent);
    591     }
    592 
    593     public static void showErrorDialog(Activity activity,
    594             String title, String message) {
    595         if (activity.isFinishing()) {
    596             return;
    597         }
    598         AlertDialog.Builder builder = new AlertDialog.Builder(activity);
    599 
    600         builder.setIcon(R.drawable.ic_sms_mms_not_delivered);
    601         builder.setTitle(title);
    602         builder.setMessage(message);
    603         builder.setPositiveButton(android.R.string.ok, new OnClickListener() {
    604             @Override
    605             public void onClick(DialogInterface dialog, int which) {
    606                 if (which == DialogInterface.BUTTON_POSITIVE) {
    607                     dialog.dismiss();
    608                 }
    609             }
    610         });
    611         builder.show();
    612     }
    613 
    614     /**
    615      * The quality parameter which is used to compress JPEG images.
    616      */
    617     public static final int IMAGE_COMPRESSION_QUALITY = 95;
    618     /**
    619      * The minimum quality parameter which is used to compress JPEG images.
    620      */
    621     public static final int MINIMUM_IMAGE_COMPRESSION_QUALITY = 50;
    622 
    623     /**
    624      * Message overhead that reduces the maximum image byte size.
    625      * 5000 is a realistic overhead number that allows for user to also include
    626      * a small MIDI file or a couple pages of text along with the picture.
    627      */
    628     public static final int MESSAGE_OVERHEAD = 5000;
    629 
    630     public static void resizeImageAsync(final Context context,
    631             final Uri imageUri, final Handler handler,
    632             final ResizeImageResultCallback cb,
    633             final boolean append) {
    634 
    635         // Show a progress toast if the resize hasn't finished
    636         // within one second.
    637         // Stash the runnable for showing it away so we can cancel
    638         // it later if the resize completes ahead of the deadline.
    639         final Runnable showProgress = new Runnable() {
    640             @Override
    641             public void run() {
    642                 Toast.makeText(context, R.string.compressing, Toast.LENGTH_SHORT).show();
    643             }
    644         };
    645         // Schedule it for one second from now.
    646         handler.postDelayed(showProgress, 1000);
    647 
    648         new Thread(new Runnable() {
    649             @Override
    650             public void run() {
    651                 final PduPart part;
    652                 try {
    653                     UriImage image = new UriImage(context, imageUri);
    654                     int widthLimit = MmsConfig.getMaxImageWidth();
    655                     int heightLimit = MmsConfig.getMaxImageHeight();
    656                     // In mms_config.xml, the max width has always been declared larger than the max
    657                     // height. Swap the width and height limits if necessary so we scale the picture
    658                     // as little as possible.
    659                     if (image.getHeight() > image.getWidth()) {
    660                         int temp = widthLimit;
    661                         widthLimit = heightLimit;
    662                         heightLimit = temp;
    663                     }
    664 
    665                     part = image.getResizedImageAsPart(
    666                         widthLimit,
    667                         heightLimit,
    668                         MmsConfig.getMaxMessageSize() - MESSAGE_OVERHEAD);
    669                 } finally {
    670                     // Cancel pending show of the progress toast if necessary.
    671                     handler.removeCallbacks(showProgress);
    672                 }
    673 
    674                 handler.post(new Runnable() {
    675                     @Override
    676                     public void run() {
    677                         cb.onResizeResult(part, append);
    678                     }
    679                 });
    680             }
    681         }, "MessageUtils.resizeImageAsync").start();
    682     }
    683 
    684     public static void showDiscardDraftConfirmDialog(Context context,
    685             OnClickListener listener) {
    686         new AlertDialog.Builder(context)
    687                 .setMessage(R.string.discard_message_reason)
    688                 .setPositiveButton(R.string.yes, listener)
    689                 .setNegativeButton(R.string.no, null)
    690                 .show();
    691     }
    692 
    693     public static String getLocalNumber() {
    694         if (null == sLocalNumber) {
    695             sLocalNumber = MmsApp.getApplication().getTelephonyManager().getLine1Number();
    696         }
    697         return sLocalNumber;
    698     }
    699 
    700     public static boolean isLocalNumber(String number) {
    701         if (number == null) {
    702             return false;
    703         }
    704 
    705         // we don't use Mms.isEmailAddress() because it is too strict for comparing addresses like
    706         // "foo+caf_=6505551212=tmomail.net (at) gmail.com", which is the 'from' address from a forwarded email
    707         // message from Gmail. We don't want to treat "foo+caf_=6505551212=tmomail.net (at) gmail.com" and
    708         // "6505551212" to be the same.
    709         if (number.indexOf('@') >= 0) {
    710             return false;
    711         }
    712 
    713         return PhoneNumberUtils.compare(number, getLocalNumber());
    714     }
    715 
    716     public static void handleReadReport(final Context context,
    717             final Collection<Long> threadIds,
    718             final int status,
    719             final Runnable callback) {
    720         StringBuilder selectionBuilder = new StringBuilder(Mms.MESSAGE_TYPE + " = "
    721                 + PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF
    722                 + " AND " + Mms.READ + " = 0"
    723                 + " AND " + Mms.READ_REPORT + " = " + PduHeaders.VALUE_YES);
    724 
    725         String[] selectionArgs = null;
    726         if (threadIds != null) {
    727             String threadIdSelection = null;
    728             StringBuilder buf = new StringBuilder();
    729             selectionArgs = new String[threadIds.size()];
    730             int i = 0;
    731 
    732             for (long threadId : threadIds) {
    733                 if (i > 0) {
    734                     buf.append(" OR ");
    735                 }
    736                 buf.append(Mms.THREAD_ID).append("=?");
    737                 selectionArgs[i++] = Long.toString(threadId);
    738             }
    739             threadIdSelection = buf.toString();
    740 
    741             selectionBuilder.append(" AND (" + threadIdSelection + ")");
    742         }
    743 
    744         final Cursor c = SqliteWrapper.query(context, context.getContentResolver(),
    745                         Mms.Inbox.CONTENT_URI, new String[] {Mms._ID, Mms.MESSAGE_ID},
    746                         selectionBuilder.toString(), selectionArgs, null);
    747 
    748         if (c == null) {
    749             return;
    750         }
    751 
    752         final Map<String, String> map = new HashMap<String, String>();
    753         try {
    754             if (c.getCount() == 0) {
    755                 if (callback != null) {
    756                     callback.run();
    757                 }
    758                 return;
    759             }
    760 
    761             while (c.moveToNext()) {
    762                 Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, c.getLong(0));
    763                 map.put(c.getString(1), AddressUtils.getFrom(context, uri));
    764             }
    765         } finally {
    766             c.close();
    767         }
    768 
    769         OnClickListener positiveListener = new OnClickListener() {
    770             @Override
    771             public void onClick(DialogInterface dialog, int which) {
    772                 for (final Map.Entry<String, String> entry : map.entrySet()) {
    773                     MmsMessageSender.sendReadRec(context, entry.getValue(),
    774                                                  entry.getKey(), status);
    775                 }
    776 
    777                 if (callback != null) {
    778                     callback.run();
    779                 }
    780                 dialog.dismiss();
    781             }
    782         };
    783 
    784         OnClickListener negativeListener = new OnClickListener() {
    785             @Override
    786             public void onClick(DialogInterface dialog, int which) {
    787                 if (callback != null) {
    788                     callback.run();
    789                 }
    790                 dialog.dismiss();
    791             }
    792         };
    793 
    794         OnCancelListener cancelListener = new OnCancelListener() {
    795             @Override
    796             public void onCancel(DialogInterface dialog) {
    797                 if (callback != null) {
    798                     callback.run();
    799                 }
    800                 dialog.dismiss();
    801             }
    802         };
    803 
    804         confirmReadReportDialog(context, positiveListener,
    805                                          negativeListener,
    806                                          cancelListener);
    807     }
    808 
    809     private static void confirmReadReportDialog(Context context,
    810             OnClickListener positiveListener, OnClickListener negativeListener,
    811             OnCancelListener cancelListener) {
    812         AlertDialog.Builder builder = new AlertDialog.Builder(context);
    813         builder.setCancelable(true);
    814         builder.setTitle(R.string.confirm);
    815         builder.setMessage(R.string.message_send_read_report);
    816         builder.setPositiveButton(R.string.yes, positiveListener);
    817         builder.setNegativeButton(R.string.no, negativeListener);
    818         builder.setOnCancelListener(cancelListener);
    819         builder.show();
    820     }
    821 
    822     public static String extractEncStrFromCursor(Cursor cursor,
    823             int columnRawBytes, int columnCharset) {
    824         String rawBytes = cursor.getString(columnRawBytes);
    825         int charset = cursor.getInt(columnCharset);
    826 
    827         if (TextUtils.isEmpty(rawBytes)) {
    828             return "";
    829         } else if (charset == CharacterSets.ANY_CHARSET) {
    830             return rawBytes;
    831         } else {
    832             return new EncodedStringValue(charset, PduPersister.getBytes(rawBytes)).getString();
    833         }
    834     }
    835 
    836     private static String extractEncStr(Context context, EncodedStringValue value) {
    837         if (value != null) {
    838             return value.getString();
    839         } else {
    840             return "";
    841         }
    842     }
    843 
    844     public static ArrayList<String> extractUris(URLSpan[] spans) {
    845         int size = spans.length;
    846         ArrayList<String> accumulator = new ArrayList<String>();
    847 
    848         for (int i = 0; i < size; i++) {
    849             accumulator.add(spans[i].getURL());
    850         }
    851         return accumulator;
    852     }
    853 
    854     /**
    855      * Play/view the message attachments.
    856      * TOOD: We need to save the draft before launching another activity to view the attachments.
    857      *       This is hacky though since we will do saveDraft twice and slow down the UI.
    858      *       We should pass the slideshow in intent extra to the view activity instead of
    859      *       asking it to read attachments from database.
    860      * @param activity
    861      * @param msgUri the MMS message URI in database
    862      * @param slideshow the slideshow to save
    863      * @param persister the PDU persister for updating the database
    864      * @param sendReq the SendReq for updating the database
    865      */
    866     public static void viewMmsMessageAttachment(Activity activity, Uri msgUri,
    867             SlideshowModel slideshow, AsyncDialog asyncDialog) {
    868         viewMmsMessageAttachment(activity, msgUri, slideshow, 0, asyncDialog);
    869     }
    870 
    871     public static void viewMmsMessageAttachment(final Activity activity, final Uri msgUri,
    872             final SlideshowModel slideshow, final int requestCode, AsyncDialog asyncDialog) {
    873         boolean isSimple = (slideshow == null) ? false : slideshow.isSimple();
    874         if (isSimple) {
    875             // In attachment-editor mode, we only ever have one slide.
    876             MessageUtils.viewSimpleSlideshow(activity, slideshow);
    877         } else {
    878             // The user wants to view the slideshow. We have to persist the slideshow parts
    879             // in a background task. If the task takes longer than a half second, a progress dialog
    880             // is displayed. Once the PDU persisting is done, another runnable on the UI thread get
    881             // executed to start the SlideshowActivity.
    882             asyncDialog.runAsync(new Runnable() {
    883                 @Override
    884                 public void run() {
    885                     // If a slideshow was provided, save it to disk first.
    886                     if (slideshow != null) {
    887                         PduPersister persister = PduPersister.getPduPersister(activity);
    888                         try {
    889                             PduBody pb = slideshow.toPduBody();
    890                             persister.updateParts(msgUri, pb, null);
    891                             slideshow.sync(pb);
    892                         } catch (MmsException e) {
    893                             Log.e(TAG, "Unable to save message for preview");
    894                             return;
    895                         }
    896                     }
    897                 }
    898             }, new Runnable() {
    899                 @Override
    900                 public void run() {
    901                     // Once the above background thread is complete, this runnable is run
    902                     // on the UI thread to launch the slideshow activity.
    903                     launchSlideshowActivity(activity, msgUri, requestCode);
    904                 }
    905             }, R.string.building_slideshow_title);
    906         }
    907     }
    908 
    909     public static void launchSlideshowActivity(Context context, Uri msgUri, int requestCode) {
    910         // Launch the slideshow activity to play/view.
    911         Intent intent = new Intent(context, SlideshowActivity.class);
    912         intent.setData(msgUri);
    913         intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
    914         if (requestCode > 0 && context instanceof Activity) {
    915             ((Activity)context).startActivityForResult(intent, requestCode);
    916         } else {
    917             context.startActivity(intent);
    918         }
    919 
    920     }
    921 
    922     /**
    923      * Debugging
    924      */
    925     public static void writeHprofDataToFile(){
    926         String filename = Environment.getExternalStorageDirectory() + "/mms_oom_hprof_data";
    927         try {
    928             android.os.Debug.dumpHprofData(filename);
    929             Log.i(TAG, "##### written hprof data to " + filename);
    930         } catch (IOException ex) {
    931             Log.e(TAG, "writeHprofDataToFile: caught " + ex);
    932         }
    933     }
    934 
    935     // An alias (or commonly called "nickname") is:
    936     // Nickname must begin with a letter.
    937     // Only letters a-z, numbers 0-9, or . are allowed in Nickname field.
    938     public static boolean isAlias(String string) {
    939         if (!MmsConfig.isAliasEnabled()) {
    940             return false;
    941         }
    942 
    943         int len = string == null ? 0 : string.length();
    944 
    945         if (len < MmsConfig.getAliasMinChars() || len > MmsConfig.getAliasMaxChars()) {
    946             return false;
    947         }
    948 
    949         if (!Character.isLetter(string.charAt(0))) {    // Nickname begins with a letter
    950             return false;
    951         }
    952         for (int i = 1; i < len; i++) {
    953             char c = string.charAt(i);
    954             if (!(Character.isLetterOrDigit(c) || c == '.')) {
    955                 return false;
    956             }
    957         }
    958 
    959         return true;
    960     }
    961 
    962     /**
    963      * Given a phone number, return the string without syntactic sugar, meaning parens,
    964      * spaces, slashes, dots, dashes, etc. If the input string contains non-numeric
    965      * non-punctuation characters, return null.
    966      */
    967     private static String parsePhoneNumberForMms(String address) {
    968         StringBuilder builder = new StringBuilder();
    969         int len = address.length();
    970 
    971         for (int i = 0; i < len; i++) {
    972             char c = address.charAt(i);
    973 
    974             // accept the first '+' in the address
    975             if (c == '+' && builder.length() == 0) {
    976                 builder.append(c);
    977                 continue;
    978             }
    979 
    980             if (Character.isDigit(c)) {
    981                 builder.append(c);
    982                 continue;
    983             }
    984 
    985             if (numericSugarMap.get(c) == null) {
    986                 return null;
    987             }
    988         }
    989         return builder.toString();
    990     }
    991 
    992     /**
    993      * Returns true if the address passed in is a valid MMS address.
    994      */
    995     public static boolean isValidMmsAddress(String address) {
    996         String retVal = parseMmsAddress(address);
    997         return (retVal != null);
    998     }
    999 
   1000     /**
   1001      * parse the input address to be a valid MMS address.
   1002      * - if the address is an email address, leave it as is.
   1003      * - if the address can be parsed into a valid MMS phone number, return the parsed number.
   1004      * - if the address is a compliant alias address, leave it as is.
   1005      */
   1006     public static String parseMmsAddress(String address) {
   1007         // if it's a valid Email address, use that.
   1008         if (Mms.isEmailAddress(address)) {
   1009             return address;
   1010         }
   1011 
   1012         // if we are able to parse the address to a MMS compliant phone number, take that.
   1013         String retVal = parsePhoneNumberForMms(address);
   1014         if (retVal != null) {
   1015             return retVal;
   1016         }
   1017 
   1018         // if it's an alias compliant address, use that.
   1019         if (isAlias(address)) {
   1020             return address;
   1021         }
   1022 
   1023         // it's not a valid MMS address, return null
   1024         return null;
   1025     }
   1026 
   1027     private static void log(String msg) {
   1028         Log.d(TAG, "[MsgUtils] " + msg);
   1029     }
   1030 }
   1031