• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {
onResizeResult(PduPart part, boolean append)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++) {
numericSugarMap.put(NUMERIC_CHARS_SUGAR[i], NUMERIC_CHARS_SUGAR[i])112             numericSugarMap.put(NUMERIC_CHARS_SUGAR[i], NUMERIC_CHARS_SUGAR[i]);
113         }
114     }
115 
116 
MessageUtils()117     private MessageUtils() {
118         // Forbidden being instantiated.
119     }
120 
getMessageDetails(Context context, Cursor cursor, int size)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 
getNotificationIndDetails(Context context, Cursor cursor)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 
getMultimediaMessageDetails( Context context, Cursor cursor, int size)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 
getTextMessageDetails(Context context, Cursor cursor)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 
getPriorityDescription(Context context, int PriorityValue)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 
getAttachmentType(SlideshowModel model)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 
formatTimeStampString(Context context, long when)387     public static String formatTimeStampString(Context context, long when) {
388         return formatTimeStampString(context, when, false);
389     }
390 
formatTimeStampString(Context context, long when, boolean fullFormat)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      */
getRecipientsByIds(Context context, String recipientIds, boolean allowQuery)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 
extractIdsToAddresses(Context context, String recipients, boolean allowQuery)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 
selectAudio(Context context, int requestCode)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 
recordSound(Context context, int requestCode)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 
selectVideo(Context context, int requestCode)514     public static void selectVideo(Context context, int requestCode) {
515         selectMediaByType(context, requestCode, ContentType.VIDEO_UNSPECIFIED);
516     }
517 
selectImage(Context context, int requestCode)518     public static void selectImage(Context context, int requestCode) {
519         selectMediaByType(context, requestCode, ContentType.IMAGE_UNSPECIFIED);
520     }
521 
selectMediaByType( Context context, int requestCode, String contentType)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 
viewSimpleSlideshow(Context context, SlideshowModel slideshow)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 
showErrorDialog(Context context, String title, String message)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 
saveBitmapAsPart(Context context, Uri messageUri, Bitmap bitmap)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 
resizeImageAsync(final Context context, final Uri imageUri, final Handler handler, final ResizeImageResultCallback cb, final boolean append)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 
showDiscardDraftConfirmDialog(Context context, OnClickListener listener)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 
getLocalNumber()670     public static String getLocalNumber() {
671         if (null == sLocalNumber) {
672             sLocalNumber = MmsApp.getApplication().getTelephonyManager().getLine1Number();
673         }
674         return sLocalNumber;
675     }
676 
isLocalNumber(String number)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@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@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 
handleReadReport(final Context context, final long threadId, final int status, final Runnable callback)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 
confirmReadReportDialog(Context context, OnClickListener positiveListener, OnClickListener negativeListener, OnCancelListener cancelListener)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 
extractEncStrFromCursor(Cursor cursor, int columnRawBytes, int columnCharset)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 
extractEncStr(Context context, EncodedStringValue value)794     private static String extractEncStr(Context context, EncodedStringValue value) {
795         if (value != null) {
796             return value.getString();
797         } else {
798             return "";
799         }
800     }
801 
extractUris(URLSpan[] spans)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      */
viewMmsMessageAttachment(Context context, Uri msgUri, SlideshowModel slideshow)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 
viewMmsMessageAttachment(Context context, WorkingMessage msg)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      */
writeHprofDataToFile()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 
isAlias(String string)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 
isAlphaNumeric(String s)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      */
parsePhoneNumberForMms(String address)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      */
isValidMmsAddress(String address)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      */
parseMmsAddress(String address)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 
log(String msg)994     private static void log(String msg) {
995         Log.d(TAG, "[MsgUtils] " + msg);
996     }
997 }
998