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) {
406        if (model == 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
437        return MessageItem.ATTACHMENT_TYPE_NOT_LOADED;
438    }
439
440    public static String formatTimeStampString(Context context, long when) {
441        return formatTimeStampString(context, when, false);
442    }
443
444    public static String formatTimeStampString(Context context, long when, boolean fullFormat) {
445        Time then = new Time();
446        then.set(when);
447        Time now = new Time();
448        now.setToNow();
449
450        // Basic settings for formatDateTime() we want for all cases.
451        int format_flags = DateUtils.FORMAT_NO_NOON_MIDNIGHT |
452                           DateUtils.FORMAT_ABBREV_ALL |
453                           DateUtils.FORMAT_CAP_AMPM;
454
455        // If the message is from a different year, show the date and year.
456        if (then.year != now.year) {
457            format_flags |= DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_DATE;
458        } else if (then.yearDay != now.yearDay) {
459            // If it is from a different day than today, show only the date.
460            format_flags |= DateUtils.FORMAT_SHOW_DATE;
461        } else {
462            // Otherwise, if the message is from today, show the time.
463            format_flags |= DateUtils.FORMAT_SHOW_TIME;
464        }
465
466        // If the caller has asked for full details, make sure to show the date
467        // and time no matter what we've determined above (but still make showing
468        // the year only happen if it is a different year from today).
469        if (fullFormat) {
470            format_flags |= (DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME);
471        }
472
473        return DateUtils.formatDateTime(context, when, format_flags);
474    }
475
476    public static void selectAudio(Activity activity, int requestCode) {
477        Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER);
478        intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, false);
479        intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, false);
480        intent.putExtra(RingtoneManager.EXTRA_RINGTONE_INCLUDE_DRM, false);
481        intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE,
482                activity.getString(R.string.select_audio));
483        activity.startActivityForResult(intent, requestCode);
484    }
485
486    public static void recordSound(Activity activity, int requestCode, long sizeLimit) {
487        Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
488        intent.setType(ContentType.AUDIO_AMR);
489        intent.setClassName("com.android.soundrecorder",
490                "com.android.soundrecorder.SoundRecorder");
491        intent.putExtra(android.provider.MediaStore.Audio.Media.EXTRA_MAX_BYTES, sizeLimit);
492        activity.startActivityForResult(intent, requestCode);
493    }
494
495    public static void recordVideo(Activity activity, int requestCode, long sizeLimit) {
496        // The video recorder can sometimes return a file that's larger than the max we
497        // say we can handle. Try to handle that overshoot by specifying an 85% limit.
498        sizeLimit *= .85F;
499
500        int durationLimit = getVideoCaptureDurationLimit(sizeLimit);
501
502        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
503            log("recordVideo: durationLimit: " + durationLimit +
504                    " sizeLimit: " + sizeLimit);
505        }
506
507        Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
508        intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0);
509        intent.putExtra("android.intent.extra.sizeLimit", sizeLimit);
510        intent.putExtra("android.intent.extra.durationLimit", durationLimit);
511        intent.putExtra(MediaStore.EXTRA_OUTPUT, TempFileProvider.SCRAP_CONTENT_URI);
512        activity.startActivityForResult(intent, requestCode);
513    }
514
515    public static void capturePicture(Activity activity, int requestCode) {
516        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
517        intent.putExtra(MediaStore.EXTRA_OUTPUT, TempFileProvider.SCRAP_CONTENT_URI);
518        activity.startActivityForResult(intent, requestCode);
519    }
520
521    // Public for until tests
522    public static int getVideoCaptureDurationLimit(long bytesAvailable) {
523        CamcorderProfile camcorder = CamcorderProfile.get(CamcorderProfile.QUALITY_LOW);
524        if (camcorder == null) {
525            return 0;
526        }
527        bytesAvailable *= 8;        // convert to bits
528        long seconds = bytesAvailable / (camcorder.audioBitRate + camcorder.videoBitRate);
529
530        // Find the best match for one of the fixed durations
531        for (int i = sVideoDuration.length - 1; i >= 0; i--) {
532            if (seconds >= sVideoDuration[i]) {
533                return sVideoDuration[i];
534            }
535        }
536        return 0;
537    }
538
539    public static void selectVideo(Context context, int requestCode) {
540        selectMediaByType(context, requestCode, ContentType.VIDEO_UNSPECIFIED, true);
541    }
542
543    public static void selectImage(Context context, int requestCode) {
544        selectMediaByType(context, requestCode, ContentType.IMAGE_UNSPECIFIED, false);
545    }
546
547    private static void selectMediaByType(
548            Context context, int requestCode, String contentType, boolean localFilesOnly) {
549         if (context instanceof Activity) {
550
551            Intent innerIntent = new Intent(Intent.ACTION_GET_CONTENT);
552
553            innerIntent.setType(contentType);
554            if (localFilesOnly) {
555                innerIntent.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
556            }
557
558            Intent wrapperIntent = Intent.createChooser(innerIntent, null);
559
560            ((Activity) context).startActivityForResult(wrapperIntent, requestCode);
561        }
562    }
563
564    public static void viewSimpleSlideshow(Context context, SlideshowModel slideshow) {
565        if (!slideshow.isSimple()) {
566            throw new IllegalArgumentException(
567                    "viewSimpleSlideshow() called on a non-simple slideshow");
568        }
569        SlideModel slide = slideshow.get(0);
570        MediaModel mm = null;
571        if (slide.hasImage()) {
572            mm = slide.getImage();
573        } else if (slide.hasVideo()) {
574            mm = slide.getVideo();
575        }
576
577        Intent intent = new Intent(Intent.ACTION_VIEW);
578        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
579        intent.putExtra("SingleItemOnly", true); // So we don't see "surrounding" images in Gallery
580
581        String contentType;
582        contentType = mm.getContentType();
583        intent.setDataAndType(mm.getUri(), contentType);
584        context.startActivity(intent);
585    }
586
587    public static void showErrorDialog(Activity activity,
588            String title, String message) {
589        if (activity.isFinishing()) {
590            return;
591        }
592        AlertDialog.Builder builder = new AlertDialog.Builder(activity);
593
594        builder.setIcon(R.drawable.ic_sms_mms_not_delivered);
595        builder.setTitle(title);
596        builder.setMessage(message);
597        builder.setPositiveButton(android.R.string.ok, new OnClickListener() {
598            @Override
599            public void onClick(DialogInterface dialog, int which) {
600                if (which == DialogInterface.BUTTON_POSITIVE) {
601                    dialog.dismiss();
602                }
603            }
604        });
605        builder.show();
606    }
607
608    /**
609     * The quality parameter which is used to compress JPEG images.
610     */
611    public static final int IMAGE_COMPRESSION_QUALITY = 95;
612    /**
613     * The minimum quality parameter which is used to compress JPEG images.
614     */
615    public static final int MINIMUM_IMAGE_COMPRESSION_QUALITY = 50;
616
617    /**
618     * Message overhead that reduces the maximum image byte size.
619     * 5000 is a realistic overhead number that allows for user to also include
620     * a small MIDI file or a couple pages of text along with the picture.
621     */
622    public static final int MESSAGE_OVERHEAD = 5000;
623
624    public static void resizeImageAsync(final Context context,
625            final Uri imageUri, final Handler handler,
626            final ResizeImageResultCallback cb,
627            final boolean append) {
628
629        // Show a progress toast if the resize hasn't finished
630        // within one second.
631        // Stash the runnable for showing it away so we can cancel
632        // it later if the resize completes ahead of the deadline.
633        final Runnable showProgress = new Runnable() {
634            @Override
635            public void run() {
636                Toast.makeText(context, R.string.compressing, Toast.LENGTH_SHORT).show();
637            }
638        };
639        // Schedule it for one second from now.
640        handler.postDelayed(showProgress, 1000);
641
642        new Thread(new Runnable() {
643            @Override
644            public void run() {
645                final PduPart part;
646                try {
647                    UriImage image = new UriImage(context, imageUri);
648                    int widthLimit = MmsConfig.getMaxImageWidth();
649                    int heightLimit = MmsConfig.getMaxImageHeight();
650                    // In mms_config.xml, the max width has always been declared larger than the max
651                    // height. Swap the width and height limits if necessary so we scale the picture
652                    // as little as possible.
653                    if (image.getHeight() > image.getWidth()) {
654                        int temp = widthLimit;
655                        widthLimit = heightLimit;
656                        heightLimit = temp;
657                    }
658
659                    part = image.getResizedImageAsPart(
660                        widthLimit,
661                        heightLimit,
662                        MmsConfig.getMaxMessageSize() - MESSAGE_OVERHEAD);
663                } finally {
664                    // Cancel pending show of the progress toast if necessary.
665                    handler.removeCallbacks(showProgress);
666                }
667
668                handler.post(new Runnable() {
669                    @Override
670                    public void run() {
671                        cb.onResizeResult(part, append);
672                    }
673                });
674            }
675        }, "MessageUtils.resizeImageAsync").start();
676    }
677
678    public static void showDiscardDraftConfirmDialog(Context context,
679            OnClickListener listener) {
680        new AlertDialog.Builder(context)
681                .setMessage(R.string.discard_message_reason)
682                .setPositiveButton(R.string.yes, listener)
683                .setNegativeButton(R.string.no, null)
684                .show();
685    }
686
687    public static String getLocalNumber() {
688        if (null == sLocalNumber) {
689            sLocalNumber = MmsApp.getApplication().getTelephonyManager().getLine1Number();
690        }
691        return sLocalNumber;
692    }
693
694    public static boolean isLocalNumber(String number) {
695        if (number == null) {
696            return false;
697        }
698
699        // we don't use Mms.isEmailAddress() because it is too strict for comparing addresses like
700        // "foo+caf_=6505551212=tmomail.net@gmail.com", which is the 'from' address from a forwarded email
701        // message from Gmail. We don't want to treat "foo+caf_=6505551212=tmomail.net@gmail.com" and
702        // "6505551212" to be the same.
703        if (number.indexOf('@') >= 0) {
704            return false;
705        }
706
707        return PhoneNumberUtils.compare(number, getLocalNumber());
708    }
709
710    public static void handleReadReport(final Context context,
711            final Collection<Long> threadIds,
712            final int status,
713            final Runnable callback) {
714        StringBuilder selectionBuilder = new StringBuilder(Mms.MESSAGE_TYPE + " = "
715                + PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF
716                + " AND " + Mms.READ + " = 0"
717                + " AND " + Mms.READ_REPORT + " = " + PduHeaders.VALUE_YES);
718
719        String[] selectionArgs = null;
720        if (threadIds != null) {
721            String threadIdSelection = null;
722            StringBuilder buf = new StringBuilder();
723            selectionArgs = new String[threadIds.size()];
724            int i = 0;
725
726            for (long threadId : threadIds) {
727                if (i > 0) {
728                    buf.append(" OR ");
729                }
730                buf.append(Mms.THREAD_ID).append("=?");
731                selectionArgs[i++] = Long.toString(threadId);
732            }
733            threadIdSelection = buf.toString();
734
735            selectionBuilder.append(" AND (" + threadIdSelection + ")");
736        }
737
738        final Cursor c = SqliteWrapper.query(context, context.getContentResolver(),
739                        Mms.Inbox.CONTENT_URI, new String[] {Mms._ID, Mms.MESSAGE_ID},
740                        selectionBuilder.toString(), selectionArgs, null);
741
742        if (c == null) {
743            return;
744        }
745
746        final Map<String, String> map = new HashMap<String, String>();
747        try {
748            if (c.getCount() == 0) {
749                if (callback != null) {
750                    callback.run();
751                }
752                return;
753            }
754
755            while (c.moveToNext()) {
756                Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, c.getLong(0));
757                map.put(c.getString(1), AddressUtils.getFrom(context, uri));
758            }
759        } finally {
760            c.close();
761        }
762
763        OnClickListener positiveListener = new OnClickListener() {
764            @Override
765            public void onClick(DialogInterface dialog, int which) {
766                for (final Map.Entry<String, String> entry : map.entrySet()) {
767                    MmsMessageSender.sendReadRec(context, entry.getValue(),
768                                                 entry.getKey(), status);
769                }
770
771                if (callback != null) {
772                    callback.run();
773                }
774                dialog.dismiss();
775            }
776        };
777
778        OnClickListener negativeListener = new OnClickListener() {
779            @Override
780            public void onClick(DialogInterface dialog, int which) {
781                if (callback != null) {
782                    callback.run();
783                }
784                dialog.dismiss();
785            }
786        };
787
788        OnCancelListener cancelListener = new OnCancelListener() {
789            @Override
790            public void onCancel(DialogInterface dialog) {
791                if (callback != null) {
792                    callback.run();
793                }
794                dialog.dismiss();
795            }
796        };
797
798        confirmReadReportDialog(context, positiveListener,
799                                         negativeListener,
800                                         cancelListener);
801    }
802
803    private static void confirmReadReportDialog(Context context,
804            OnClickListener positiveListener, OnClickListener negativeListener,
805            OnCancelListener cancelListener) {
806        AlertDialog.Builder builder = new AlertDialog.Builder(context);
807        builder.setCancelable(true);
808        builder.setTitle(R.string.confirm);
809        builder.setMessage(R.string.message_send_read_report);
810        builder.setPositiveButton(R.string.yes, positiveListener);
811        builder.setNegativeButton(R.string.no, negativeListener);
812        builder.setOnCancelListener(cancelListener);
813        builder.show();
814    }
815
816    public static String extractEncStrFromCursor(Cursor cursor,
817            int columnRawBytes, int columnCharset) {
818        String rawBytes = cursor.getString(columnRawBytes);
819        int charset = cursor.getInt(columnCharset);
820
821        if (TextUtils.isEmpty(rawBytes)) {
822            return "";
823        } else if (charset == CharacterSets.ANY_CHARSET) {
824            return rawBytes;
825        } else {
826            return new EncodedStringValue(charset, PduPersister.getBytes(rawBytes)).getString();
827        }
828    }
829
830    private static String extractEncStr(Context context, EncodedStringValue value) {
831        if (value != null) {
832            return value.getString();
833        } else {
834            return "";
835        }
836    }
837
838    public static ArrayList<String> extractUris(URLSpan[] spans) {
839        int size = spans.length;
840        ArrayList<String> accumulator = new ArrayList<String>();
841
842        for (int i = 0; i < size; i++) {
843            accumulator.add(spans[i].getURL());
844        }
845        return accumulator;
846    }
847
848    /**
849     * Play/view the message attachments.
850     * TOOD: We need to save the draft before launching another activity to view the attachments.
851     *       This is hacky though since we will do saveDraft twice and slow down the UI.
852     *       We should pass the slideshow in intent extra to the view activity instead of
853     *       asking it to read attachments from database.
854     * @param activity
855     * @param msgUri the MMS message URI in database
856     * @param slideshow the slideshow to save
857     * @param persister the PDU persister for updating the database
858     * @param sendReq the SendReq for updating the database
859     */
860    public static void viewMmsMessageAttachment(Activity activity, Uri msgUri,
861            SlideshowModel slideshow, AsyncDialog asyncDialog) {
862        viewMmsMessageAttachment(activity, msgUri, slideshow, 0, asyncDialog);
863    }
864
865    public static void viewMmsMessageAttachment(final Activity activity, final Uri msgUri,
866            final SlideshowModel slideshow, final int requestCode, AsyncDialog asyncDialog) {
867        boolean isSimple = (slideshow == null) ? false : slideshow.isSimple();
868        if (isSimple) {
869            // In attachment-editor mode, we only ever have one slide.
870            MessageUtils.viewSimpleSlideshow(activity, slideshow);
871        } else {
872            // The user wants to view the slideshow. We have to persist the slideshow parts
873            // in a background task. If the task takes longer than a half second, a progress dialog
874            // is displayed. Once the PDU persisting is done, another runnable on the UI thread get
875            // executed to start the SlideshowActivity.
876            asyncDialog.runAsync(new Runnable() {
877                @Override
878                public void run() {
879                    // If a slideshow was provided, save it to disk first.
880                    if (slideshow != null) {
881                        PduPersister persister = PduPersister.getPduPersister(activity);
882                        try {
883                            PduBody pb = slideshow.toPduBody();
884                            persister.updateParts(msgUri, pb, null);
885                            slideshow.sync(pb);
886                        } catch (MmsException e) {
887                            Log.e(TAG, "Unable to save message for preview");
888                            return;
889                        }
890                    }
891                }
892            }, new Runnable() {
893                @Override
894                public void run() {
895                    // Once the above background thread is complete, this runnable is run
896                    // on the UI thread to launch the slideshow activity.
897                    launchSlideshowActivity(activity, msgUri, requestCode);
898                }
899            }, R.string.building_slideshow_title);
900        }
901    }
902
903    public static void launchSlideshowActivity(Context context, Uri msgUri, int requestCode) {
904        // Launch the slideshow activity to play/view.
905        Intent intent = new Intent(context, SlideshowActivity.class);
906        intent.setData(msgUri);
907        if (requestCode > 0 && context instanceof Activity) {
908            ((Activity)context).startActivityForResult(intent, requestCode);
909        } else {
910            context.startActivity(intent);
911        }
912
913    }
914
915    /**
916     * Debugging
917     */
918    public static void writeHprofDataToFile(){
919        String filename = Environment.getExternalStorageDirectory() + "/mms_oom_hprof_data";
920        try {
921            android.os.Debug.dumpHprofData(filename);
922            Log.i(TAG, "##### written hprof data to " + filename);
923        } catch (IOException ex) {
924            Log.e(TAG, "writeHprofDataToFile: caught " + ex);
925        }
926    }
927
928    // An alias (or commonly called "nickname") is:
929    // Nickname must begin with a letter.
930    // Only letters a-z, numbers 0-9, or . are allowed in Nickname field.
931    public static boolean isAlias(String string) {
932        if (!MmsConfig.isAliasEnabled()) {
933            return false;
934        }
935
936        int len = string == null ? 0 : string.length();
937
938        if (len < MmsConfig.getAliasMinChars() || len > MmsConfig.getAliasMaxChars()) {
939            return false;
940        }
941
942        if (!Character.isLetter(string.charAt(0))) {    // Nickname begins with a letter
943            return false;
944        }
945        for (int i = 1; i < len; i++) {
946            char c = string.charAt(i);
947            if (!(Character.isLetterOrDigit(c) || c == '.')) {
948                return false;
949            }
950        }
951
952        return true;
953    }
954
955    /**
956     * Given a phone number, return the string without syntactic sugar, meaning parens,
957     * spaces, slashes, dots, dashes, etc. If the input string contains non-numeric
958     * non-punctuation characters, return null.
959     */
960    private static String parsePhoneNumberForMms(String address) {
961        StringBuilder builder = new StringBuilder();
962        int len = address.length();
963
964        for (int i = 0; i < len; i++) {
965            char c = address.charAt(i);
966
967            // accept the first '+' in the address
968            if (c == '+' && builder.length() == 0) {
969                builder.append(c);
970                continue;
971            }
972
973            if (Character.isDigit(c)) {
974                builder.append(c);
975                continue;
976            }
977
978            if (numericSugarMap.get(c) == null) {
979                return null;
980            }
981        }
982        return builder.toString();
983    }
984
985    /**
986     * Returns true if the address passed in is a valid MMS address.
987     */
988    public static boolean isValidMmsAddress(String address) {
989        String retVal = parseMmsAddress(address);
990        return (retVal != null);
991    }
992
993    /**
994     * parse the input address to be a valid MMS address.
995     * - if the address is an email address, leave it as is.
996     * - if the address can be parsed into a valid MMS phone number, return the parsed number.
997     * - if the address is a compliant alias address, leave it as is.
998     */
999    public static String parseMmsAddress(String address) {
1000        // if it's a valid Email address, use that.
1001        if (Mms.isEmailAddress(address)) {
1002            return address;
1003        }
1004
1005        // if we are able to parse the address to a MMS compliant phone number, take that.
1006        String retVal = parsePhoneNumberForMms(address);
1007        if (retVal != null) {
1008            return retVal;
1009        }
1010
1011        // if it's an alias compliant address, use that.
1012        if (isAlias(address)) {
1013            return address;
1014        }
1015
1016        // it's not a valid MMS address, return null
1017        return null;
1018    }
1019
1020    private static void log(String msg) {
1021        Log.d(TAG, "[MsgUtils] " + msg);
1022    }
1023}
1024