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.TempFileProvider;
     25 import com.android.mms.data.WorkingMessage;
     26 import com.android.mms.model.MediaModel;
     27 import com.android.mms.model.SlideModel;
     28 import com.android.mms.model.SlideshowModel;
     29 import com.android.mms.transaction.MmsMessageSender;
     30 import com.android.mms.util.AddressUtils;
     31 import com.google.android.mms.ContentType;
     32 import com.google.android.mms.MmsException;
     33 import com.google.android.mms.pdu.CharacterSets;
     34 import com.google.android.mms.pdu.EncodedStringValue;
     35 import com.google.android.mms.pdu.MultimediaMessagePdu;
     36 import com.google.android.mms.pdu.NotificationInd;
     37 import com.google.android.mms.pdu.PduBody;
     38 import com.google.android.mms.pdu.PduHeaders;
     39 import com.google.android.mms.pdu.PduPart;
     40 import com.google.android.mms.pdu.PduPersister;
     41 import com.google.android.mms.pdu.RetrieveConf;
     42 import com.google.android.mms.pdu.SendReq;
     43 import android.database.sqlite.SqliteWrapper;
     44 
     45 import android.app.Activity;
     46 import android.app.AlertDialog;
     47 import android.content.ContentUris;
     48 import android.content.Context;
     49 import android.content.DialogInterface;
     50 import android.content.Intent;
     51 import android.content.DialogInterface.OnCancelListener;
     52 import android.content.DialogInterface.OnClickListener;
     53 import android.content.res.Resources;
     54 import android.database.Cursor;
     55 import android.media.CamcorderProfile;
     56 import android.media.RingtoneManager;
     57 import android.net.Uri;
     58 import android.os.Environment;
     59 import android.os.Handler;
     60 import android.provider.MediaStore;
     61 import android.provider.Telephony.Mms;
     62 import android.provider.Telephony.Sms;
     63 import android.telephony.PhoneNumberUtils;
     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.IOException;
     72 import java.util.ArrayList;
     73 import java.util.Collection;
     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         Log.d(TAG, "getTextMessageDetails");
    299 
    300         StringBuilder details = new StringBuilder();
    301         Resources res = context.getResources();
    302 
    303         // Message Type: Text message.
    304         details.append(res.getString(R.string.message_type_label));
    305         details.append(res.getString(R.string.text_message));
    306 
    307         // Address: ***
    308         details.append('\n');
    309         int smsType = cursor.getInt(MessageListAdapter.COLUMN_SMS_TYPE);
    310         if (Sms.isOutgoingFolder(smsType)) {
    311             details.append(res.getString(R.string.to_address_label));
    312         } else {
    313             details.append(res.getString(R.string.from_label));
    314         }
    315         details.append(cursor.getString(MessageListAdapter.COLUMN_SMS_ADDRESS));
    316 
    317         // Sent: ***
    318         if (smsType == Sms.MESSAGE_TYPE_INBOX) {
    319             long date_sent = cursor.getLong(MessageListAdapter.COLUMN_SMS_DATE_SENT);
    320             if (date_sent > 0) {
    321                 details.append('\n');
    322                 details.append(res.getString(R.string.sent_label));
    323                 details.append(MessageUtils.formatTimeStampString(context, date_sent, true));
    324             }
    325         }
    326 
    327         // Received: ***
    328         details.append('\n');
    329         if (smsType == Sms.MESSAGE_TYPE_DRAFT) {
    330             details.append(res.getString(R.string.saved_label));
    331         } else if (smsType == Sms.MESSAGE_TYPE_INBOX) {
    332             details.append(res.getString(R.string.received_label));
    333         } else {
    334             details.append(res.getString(R.string.sent_label));
    335         }
    336 
    337         long date = cursor.getLong(MessageListAdapter.COLUMN_SMS_DATE);
    338         details.append(MessageUtils.formatTimeStampString(context, date, true));
    339 
    340         // Error code: ***
    341         int errorCode = cursor.getInt(MessageListAdapter.COLUMN_SMS_ERROR_CODE);
    342         if (errorCode != 0) {
    343             details.append('\n')
    344                 .append(res.getString(R.string.error_code_label))
    345                 .append(errorCode);
    346         }
    347 
    348         return details.toString();
    349     }
    350 
    351     static private String getPriorityDescription(Context context, int PriorityValue) {
    352         Resources res = context.getResources();
    353         switch(PriorityValue) {
    354             case PduHeaders.PRIORITY_HIGH:
    355                 return res.getString(R.string.priority_high);
    356             case PduHeaders.PRIORITY_LOW:
    357                 return res.getString(R.string.priority_low);
    358             case PduHeaders.PRIORITY_NORMAL:
    359             default:
    360                 return res.getString(R.string.priority_normal);
    361         }
    362     }
    363 
    364     public static int getAttachmentType(SlideshowModel model) {
    365         if (model == null) {
    366             return WorkingMessage.TEXT;
    367         }
    368 
    369         int numberOfSlides = model.size();
    370         if (numberOfSlides > 1) {
    371             return WorkingMessage.SLIDESHOW;
    372         } else if (numberOfSlides == 1) {
    373             // Only one slide in the slide-show.
    374             SlideModel slide = model.get(0);
    375             if (slide.hasVideo()) {
    376                 return WorkingMessage.VIDEO;
    377             }
    378 
    379             if (slide.hasAudio() && slide.hasImage()) {
    380                 return WorkingMessage.SLIDESHOW;
    381             }
    382 
    383             if (slide.hasAudio()) {
    384                 return WorkingMessage.AUDIO;
    385             }
    386 
    387             if (slide.hasImage()) {
    388                 return WorkingMessage.IMAGE;
    389             }
    390 
    391             if (slide.hasText()) {
    392                 return WorkingMessage.TEXT;
    393             }
    394         }
    395 
    396         return WorkingMessage.TEXT;
    397     }
    398 
    399     public static String formatTimeStampString(Context context, long when) {
    400         return formatTimeStampString(context, when, false);
    401     }
    402 
    403     public static String formatTimeStampString(Context context, long when, boolean fullFormat) {
    404         Time then = new Time();
    405         then.set(when);
    406         Time now = new Time();
    407         now.setToNow();
    408 
    409         // Basic settings for formatDateTime() we want for all cases.
    410         int format_flags = DateUtils.FORMAT_NO_NOON_MIDNIGHT |
    411                            DateUtils.FORMAT_ABBREV_ALL |
    412                            DateUtils.FORMAT_CAP_AMPM;
    413 
    414         // If the message is from a different year, show the date and year.
    415         if (then.year != now.year) {
    416             format_flags |= DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_DATE;
    417         } else if (then.yearDay != now.yearDay) {
    418             // If it is from a different day than today, show only the date.
    419             format_flags |= DateUtils.FORMAT_SHOW_DATE;
    420         } else {
    421             // Otherwise, if the message is from today, show the time.
    422             format_flags |= DateUtils.FORMAT_SHOW_TIME;
    423         }
    424 
    425         // If the caller has asked for full details, make sure to show the date
    426         // and time no matter what we've determined above (but still make showing
    427         // the year only happen if it is a different year from today).
    428         if (fullFormat) {
    429             format_flags |= (DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME);
    430         }
    431 
    432         return DateUtils.formatDateTime(context, when, format_flags);
    433     }
    434 
    435     public static void selectAudio(Context context, int requestCode) {
    436         if (context instanceof Activity) {
    437             Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER);
    438             intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, false);
    439             intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, false);
    440             intent.putExtra(RingtoneManager.EXTRA_RINGTONE_INCLUDE_DRM, false);
    441             intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE,
    442                     context.getString(R.string.select_audio));
    443             ((Activity) context).startActivityForResult(intent, requestCode);
    444         }
    445     }
    446 
    447     public static void recordSound(Context context, int requestCode, long sizeLimit) {
    448         if (context instanceof Activity) {
    449             Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
    450             intent.setType(ContentType.AUDIO_AMR);
    451             intent.setClassName("com.android.soundrecorder",
    452                     "com.android.soundrecorder.SoundRecorder");
    453             intent.putExtra(android.provider.MediaStore.Audio.Media.EXTRA_MAX_BYTES, sizeLimit);
    454 
    455             ((Activity) context).startActivityForResult(intent, requestCode);
    456         }
    457     }
    458 
    459     public static void recordVideo(Context context, int requestCode, long sizeLimit) {
    460         if (context instanceof Activity) {
    461             int durationLimit = getVideoCaptureDurationLimit();
    462             Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
    463             intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0);
    464             intent.putExtra("android.intent.extra.sizeLimit", sizeLimit);
    465             intent.putExtra("android.intent.extra.durationLimit", durationLimit);
    466             intent.putExtra(MediaStore.EXTRA_OUTPUT, TempFileProvider.SCRAP_CONTENT_URI);
    467 
    468             ((Activity) context).startActivityForResult(intent, requestCode);
    469         }
    470     }
    471 
    472     private static int getVideoCaptureDurationLimit() {
    473         CamcorderProfile camcorder = CamcorderProfile.get(CamcorderProfile.QUALITY_LOW);
    474         return camcorder == null ? 0 : camcorder.duration;
    475     }
    476 
    477     public static void selectVideo(Context context, int requestCode) {
    478         selectMediaByType(context, requestCode, ContentType.VIDEO_UNSPECIFIED, true);
    479     }
    480 
    481     public static void selectImage(Context context, int requestCode) {
    482         selectMediaByType(context, requestCode, ContentType.IMAGE_UNSPECIFIED, false);
    483     }
    484 
    485     private static void selectMediaByType(
    486             Context context, int requestCode, String contentType, boolean localFilesOnly) {
    487          if (context instanceof Activity) {
    488 
    489             Intent innerIntent = new Intent(Intent.ACTION_GET_CONTENT);
    490 
    491             innerIntent.setType(contentType);
    492             if (localFilesOnly) {
    493                 innerIntent.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
    494             }
    495 
    496             Intent wrapperIntent = Intent.createChooser(innerIntent, null);
    497 
    498             ((Activity) context).startActivityForResult(wrapperIntent, requestCode);
    499         }
    500     }
    501 
    502     public static void viewSimpleSlideshow(Context context, SlideshowModel slideshow) {
    503         if (!slideshow.isSimple()) {
    504             throw new IllegalArgumentException(
    505                     "viewSimpleSlideshow() called on a non-simple slideshow");
    506         }
    507         SlideModel slide = slideshow.get(0);
    508         MediaModel mm = null;
    509         if (slide.hasImage()) {
    510             mm = slide.getImage();
    511         } else if (slide.hasVideo()) {
    512             mm = slide.getVideo();
    513         }
    514 
    515         Intent intent = new Intent(Intent.ACTION_VIEW);
    516         intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
    517         intent.putExtra("SingleItemOnly", true); // So we don't see "surrounding" images in Gallery
    518 
    519         String contentType;
    520         if (mm.isDrmProtected()) {
    521             contentType = mm.getDrmObject().getContentType();
    522         } else {
    523             contentType = mm.getContentType();
    524         }
    525         intent.setDataAndType(mm.getUri(), contentType);
    526         context.startActivity(intent);
    527     }
    528 
    529     public static void showErrorDialog(Context context,
    530             String title, String message) {
    531         AlertDialog.Builder builder = new AlertDialog.Builder(context);
    532 
    533         builder.setIcon(R.drawable.ic_sms_mms_not_delivered);
    534         builder.setTitle(title);
    535         builder.setMessage(message);
    536         builder.setPositiveButton(android.R.string.ok, new OnClickListener() {
    537             @Override
    538             public void onClick(DialogInterface dialog, int which) {
    539                 if (which == DialogInterface.BUTTON_POSITIVE) {
    540                     dialog.dismiss();
    541                 }
    542             }
    543         });
    544         builder.show();
    545     }
    546 
    547     /**
    548      * The quality parameter which is used to compress JPEG images.
    549      */
    550     public static final int IMAGE_COMPRESSION_QUALITY = 95;
    551     /**
    552      * The minimum quality parameter which is used to compress JPEG images.
    553      */
    554     public static final int MINIMUM_IMAGE_COMPRESSION_QUALITY = 50;
    555 
    556     /**
    557      * Message overhead that reduces the maximum image byte size.
    558      * 5000 is a realistic overhead number that allows for user to also include
    559      * a small MIDI file or a couple pages of text along with the picture.
    560      */
    561     public static final int MESSAGE_OVERHEAD = 5000;
    562 
    563     public static void resizeImageAsync(final Context context,
    564             final Uri imageUri, final Handler handler,
    565             final ResizeImageResultCallback cb,
    566             final boolean append) {
    567 
    568         // Show a progress toast if the resize hasn't finished
    569         // within one second.
    570         // Stash the runnable for showing it away so we can cancel
    571         // it later if the resize completes ahead of the deadline.
    572         final Runnable showProgress = new Runnable() {
    573             @Override
    574             public void run() {
    575                 Toast.makeText(context, R.string.compressing, Toast.LENGTH_SHORT).show();
    576             }
    577         };
    578         // Schedule it for one second from now.
    579         handler.postDelayed(showProgress, 1000);
    580 
    581         new Thread(new Runnable() {
    582             @Override
    583             public void run() {
    584                 final PduPart part;
    585                 try {
    586                     UriImage image = new UriImage(context, imageUri);
    587                     int widthLimit = MmsConfig.getMaxImageWidth();
    588                     int heightLimit = MmsConfig.getMaxImageHeight();
    589                     // In mms_config.xml, the max width has always been declared larger than the max
    590                     // height. Swap the width and height limits if necessary so we scale the picture
    591                     // as little as possible.
    592                     if (image.getHeight() > image.getWidth()) {
    593                         int temp = widthLimit;
    594                         widthLimit = heightLimit;
    595                         heightLimit = temp;
    596                     }
    597 
    598                     part = image.getResizedImageAsPart(
    599                         widthLimit,
    600                         heightLimit,
    601                         MmsConfig.getMaxMessageSize() - MESSAGE_OVERHEAD);
    602                 } finally {
    603                     // Cancel pending show of the progress toast if necessary.
    604                     handler.removeCallbacks(showProgress);
    605                 }
    606 
    607                 handler.post(new Runnable() {
    608                     @Override
    609                     public void run() {
    610                         cb.onResizeResult(part, append);
    611                     }
    612                 });
    613             }
    614         }).start();
    615     }
    616 
    617     public static void showDiscardDraftConfirmDialog(Context context,
    618             OnClickListener listener) {
    619         new AlertDialog.Builder(context)
    620                 .setIcon(android.R.drawable.ic_dialog_alert)
    621                 .setTitle(R.string.discard_message)
    622                 .setMessage(R.string.discard_message_reason)
    623                 .setPositiveButton(R.string.yes, listener)
    624                 .setNegativeButton(R.string.no, null)
    625                 .show();
    626     }
    627 
    628     public static String getLocalNumber() {
    629         if (null == sLocalNumber) {
    630             sLocalNumber = MmsApp.getApplication().getTelephonyManager().getLine1Number();
    631         }
    632         return sLocalNumber;
    633     }
    634 
    635     public static boolean isLocalNumber(String number) {
    636         if (number == null) {
    637             return false;
    638         }
    639 
    640         // we don't use Mms.isEmailAddress() because it is too strict for comparing addresses like
    641         // "foo+caf_=6505551212=tmomail.net (at) gmail.com", which is the 'from' address from a forwarded email
    642         // message from Gmail. We don't want to treat "foo+caf_=6505551212=tmomail.net (at) gmail.com" and
    643         // "6505551212" to be the same.
    644         if (number.indexOf('@') >= 0) {
    645             return false;
    646         }
    647 
    648         return PhoneNumberUtils.compare(number, getLocalNumber());
    649     }
    650 
    651     public static void handleReadReport(final Context context,
    652             final Collection<Long> threadIds,
    653             final int status,
    654             final Runnable callback) {
    655         StringBuilder selectionBuilder = new StringBuilder(Mms.MESSAGE_TYPE + " = "
    656                 + PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF
    657                 + " AND " + Mms.READ + " = 0"
    658                 + " AND " + Mms.READ_REPORT + " = " + PduHeaders.VALUE_YES);
    659 
    660         String[] selectionArgs = null;
    661         if (threadIds != null) {
    662             String threadIdSelection = null;
    663             StringBuilder buf = new StringBuilder();
    664             selectionArgs = new String[threadIds.size()];
    665             int i = 0;
    666 
    667             for (long threadId : threadIds) {
    668                 if (i > 0) {
    669                     buf.append(" OR ");
    670                 }
    671                 buf.append(Mms.THREAD_ID).append("=?");
    672                 selectionArgs[i++] = Long.toString(threadId);
    673             }
    674             threadIdSelection = buf.toString();
    675 
    676             selectionBuilder.append(" AND (" + threadIdSelection + ")");
    677         }
    678 
    679         final Cursor c = SqliteWrapper.query(context, context.getContentResolver(),
    680                         Mms.Inbox.CONTENT_URI, new String[] {Mms._ID, Mms.MESSAGE_ID},
    681                         selectionBuilder.toString(), selectionArgs, null);
    682 
    683         if (c == null) {
    684             return;
    685         }
    686 
    687         final Map<String, String> map = new HashMap<String, String>();
    688         try {
    689             if (c.getCount() == 0) {
    690                 if (callback != null) {
    691                     callback.run();
    692                 }
    693                 return;
    694             }
    695 
    696             while (c.moveToNext()) {
    697                 Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, c.getLong(0));
    698                 map.put(c.getString(1), AddressUtils.getFrom(context, uri));
    699             }
    700         } finally {
    701             c.close();
    702         }
    703 
    704         OnClickListener positiveListener = new OnClickListener() {
    705             @Override
    706             public void onClick(DialogInterface dialog, int which) {
    707                 for (final Map.Entry<String, String> entry : map.entrySet()) {
    708                     MmsMessageSender.sendReadRec(context, entry.getValue(),
    709                                                  entry.getKey(), status);
    710                 }
    711 
    712                 if (callback != null) {
    713                     callback.run();
    714                 }
    715                 dialog.dismiss();
    716             }
    717         };
    718 
    719         OnClickListener negativeListener = new OnClickListener() {
    720             @Override
    721             public void onClick(DialogInterface dialog, int which) {
    722                 if (callback != null) {
    723                     callback.run();
    724                 }
    725                 dialog.dismiss();
    726             }
    727         };
    728 
    729         OnCancelListener cancelListener = new OnCancelListener() {
    730             @Override
    731             public void onCancel(DialogInterface dialog) {
    732                 if (callback != null) {
    733                     callback.run();
    734                 }
    735                 dialog.dismiss();
    736             }
    737         };
    738 
    739         confirmReadReportDialog(context, positiveListener,
    740                                          negativeListener,
    741                                          cancelListener);
    742     }
    743 
    744     private static void confirmReadReportDialog(Context context,
    745             OnClickListener positiveListener, OnClickListener negativeListener,
    746             OnCancelListener cancelListener) {
    747         AlertDialog.Builder builder = new AlertDialog.Builder(context);
    748         builder.setCancelable(true);
    749         builder.setTitle(R.string.confirm);
    750         builder.setMessage(R.string.message_send_read_report);
    751         builder.setPositiveButton(R.string.yes, positiveListener);
    752         builder.setNegativeButton(R.string.no, negativeListener);
    753         builder.setOnCancelListener(cancelListener);
    754         builder.show();
    755     }
    756 
    757     public static String extractEncStrFromCursor(Cursor cursor,
    758             int columnRawBytes, int columnCharset) {
    759         String rawBytes = cursor.getString(columnRawBytes);
    760         int charset = cursor.getInt(columnCharset);
    761 
    762         if (TextUtils.isEmpty(rawBytes)) {
    763             return "";
    764         } else if (charset == CharacterSets.ANY_CHARSET) {
    765             return rawBytes;
    766         } else {
    767             return new EncodedStringValue(charset, PduPersister.getBytes(rawBytes)).getString();
    768         }
    769     }
    770 
    771     private static String extractEncStr(Context context, EncodedStringValue value) {
    772         if (value != null) {
    773             return value.getString();
    774         } else {
    775             return "";
    776         }
    777     }
    778 
    779     public static ArrayList<String> extractUris(URLSpan[] spans) {
    780         int size = spans.length;
    781         ArrayList<String> accumulator = new ArrayList<String>();
    782 
    783         for (int i = 0; i < size; i++) {
    784             accumulator.add(spans[i].getURL());
    785         }
    786         return accumulator;
    787     }
    788 
    789     /**
    790      * Play/view the message attachments.
    791      * TOOD: We need to save the draft before launching another activity to view the attachments.
    792      *       This is hacky though since we will do saveDraft twice and slow down the UI.
    793      *       We should pass the slideshow in intent extra to the view activity instead of
    794      *       asking it to read attachments from database.
    795      * @param context
    796      * @param msgUri the MMS message URI in database
    797      * @param slideshow the slideshow to save
    798      * @param persister the PDU persister for updating the database
    799      * @param sendReq the SendReq for updating the database
    800      */
    801     public static void viewMmsMessageAttachment(Context context, Uri msgUri,
    802             SlideshowModel slideshow) {
    803         viewMmsMessageAttachment(context, msgUri, slideshow, 0);
    804     }
    805 
    806     private static void viewMmsMessageAttachment(Context context, Uri msgUri,
    807             SlideshowModel slideshow, int requestCode) {
    808         boolean isSimple = (slideshow == null) ? false : slideshow.isSimple();
    809         if (isSimple) {
    810             // In attachment-editor mode, we only ever have one slide.
    811             MessageUtils.viewSimpleSlideshow(context, slideshow);
    812         } else {
    813             // If a slideshow was provided, save it to disk first.
    814             if (slideshow != null) {
    815                 PduPersister persister = PduPersister.getPduPersister(context);
    816                 try {
    817                     PduBody pb = slideshow.toPduBody();
    818                     persister.updateParts(msgUri, pb);
    819                     slideshow.sync(pb);
    820                 } catch (MmsException e) {
    821                     Log.e(TAG, "Unable to save message for preview");
    822                     return;
    823                 }
    824             }
    825             // Launch the slideshow activity to play/view.
    826             Intent intent = new Intent(context, SlideshowActivity.class);
    827             intent.setData(msgUri);
    828             if (requestCode > 0 && context instanceof Activity) {
    829                 ((Activity)context).startActivityForResult(intent, requestCode);
    830             } else {
    831                 context.startActivity(intent);
    832             }
    833         }
    834     }
    835 
    836     public static void viewMmsMessageAttachment(Context context, WorkingMessage msg,
    837             int requestCode) {
    838         SlideshowModel slideshow = msg.getSlideshow();
    839         if (slideshow == null) {
    840             throw new IllegalStateException("msg.getSlideshow() == null");
    841         }
    842         if (slideshow.isSimple()) {
    843             MessageUtils.viewSimpleSlideshow(context, slideshow);
    844         } else {
    845             Uri uri = msg.saveAsMms(false);
    846             if (uri != null) {
    847                 // Pass null for the slideshow paramater, otherwise viewMmsMessageAttachment
    848                 // will persist the slideshow to disk again (we just did that above in saveAsMms)
    849                 viewMmsMessageAttachment(context, uri, null, requestCode);
    850             }
    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     // An alias (or commonly called "nickname") is:
    868     // Nickname must begin with a letter.
    869     // Only letters a-z, numbers 0-9, or . are allowed in Nickname field.
    870     public static boolean isAlias(String string) {
    871         if (!MmsConfig.isAliasEnabled()) {
    872             return false;
    873         }
    874 
    875         int len = string == null ? 0 : string.length();
    876 
    877         if (len < MmsConfig.getAliasMinChars() || len > MmsConfig.getAliasMaxChars()) {
    878             return false;
    879         }
    880 
    881         if (!Character.isLetter(string.charAt(0))) {    // Nickname begins with a letter
    882             return false;
    883         }
    884         for (int i = 1; i < len; i++) {
    885             char c = string.charAt(i);
    886             if (!(Character.isLetterOrDigit(c) || c == '.')) {
    887                 return false;
    888             }
    889         }
    890 
    891         return true;
    892     }
    893 
    894     /**
    895      * Given a phone number, return the string without syntactic sugar, meaning parens,
    896      * spaces, slashes, dots, dashes, etc. If the input string contains non-numeric
    897      * non-punctuation characters, return null.
    898      */
    899     private static String parsePhoneNumberForMms(String address) {
    900         StringBuilder builder = new StringBuilder();
    901         int len = address.length();
    902 
    903         for (int i = 0; i < len; i++) {
    904             char c = address.charAt(i);
    905 
    906             // accept the first '+' in the address
    907             if (c == '+' && builder.length() == 0) {
    908                 builder.append(c);
    909                 continue;
    910             }
    911 
    912             if (Character.isDigit(c)) {
    913                 builder.append(c);
    914                 continue;
    915             }
    916 
    917             if (numericSugarMap.get(c) == null) {
    918                 return null;
    919             }
    920         }
    921         return builder.toString();
    922     }
    923 
    924     /**
    925      * Returns true if the address passed in is a valid MMS address.
    926      */
    927     public static boolean isValidMmsAddress(String address) {
    928         String retVal = parseMmsAddress(address);
    929         return (retVal != null);
    930     }
    931 
    932     /**
    933      * parse the input address to be a valid MMS address.
    934      * - if the address is an email address, leave it as is.
    935      * - if the address can be parsed into a valid MMS phone number, return the parsed number.
    936      * - if the address is a compliant alias address, leave it as is.
    937      */
    938     public static String parseMmsAddress(String address) {
    939         // if it's a valid Email address, use that.
    940         if (Mms.isEmailAddress(address)) {
    941             return address;
    942         }
    943 
    944         // if we are able to parse the address to a MMS compliant phone number, take that.
    945         String retVal = parsePhoneNumberForMms(address);
    946         if (retVal != null) {
    947             return retVal;
    948         }
    949 
    950         // if it's an alias compliant address, use that.
    951         if (isAlias(address)) {
    952             return address;
    953         }
    954 
    955         // it's not a valid MMS address, return null
    956         return null;
    957     }
    958 
    959     private static void log(String msg) {
    960         Log.d(TAG, "[MsgUtils] " + msg);
    961     }
    962 }
    963