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