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