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