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