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