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