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