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