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        // 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
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
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
399    public static String formatTimeStampString(Context context, long when) {
400        return formatTimeStampString(context, when, false);
401    }
402
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
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
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
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
472    private static int getVideoCaptureDurationLimit() {
473        CamcorderProfile camcorder = CamcorderProfile.get(CamcorderProfile.QUALITY_LOW);
474        return camcorder == null ? 0 : camcorder.duration;
475    }
476
477    public static void selectVideo(Context context, int requestCode) {
478        selectMediaByType(context, requestCode, ContentType.VIDEO_UNSPECIFIED, true);
479    }
480
481    public static void selectImage(Context context, int requestCode) {
482        selectMediaByType(context, requestCode, ContentType.IMAGE_UNSPECIFIED, false);
483    }
484
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
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
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
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
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
628    public static String getLocalNumber() {
629        if (null == sLocalNumber) {
630            sLocalNumber = MmsApp.getApplication().getTelephonyManager().getLine1Number();
631        }
632        return sLocalNumber;
633    }
634
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
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
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
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
771    private static String extractEncStr(Context context, EncodedStringValue value) {
772        if (value != null) {
773            return value.getString();
774        } else {
775            return "";
776        }
777    }
778
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     */
801    public static void viewMmsMessageAttachment(Context context, Uri msgUri,
802            SlideshowModel slideshow) {
803        viewMmsMessageAttachment(context, msgUri, slideshow, 0);
804    }
805
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
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     */
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.
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     */
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     */
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     */
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
959    private static void log(String msg) {
960        Log.d(TAG, "[MsgUtils] " + msg);
961    }
962}
963