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