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