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