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.transaction;
19
20import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND;
21import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF;
22
23import java.util.ArrayList;
24import java.util.Comparator;
25import java.util.HashSet;
26import java.util.Iterator;
27import java.util.Set;
28import java.util.SortedSet;
29import java.util.TreeSet;
30
31import android.app.Notification;
32import android.app.NotificationManager;
33import android.app.PendingIntent;
34import android.app.TaskStackBuilder;
35import android.content.BroadcastReceiver;
36import android.content.ContentResolver;
37import android.content.Context;
38import android.content.Intent;
39import android.content.IntentFilter;
40import android.content.SharedPreferences;
41import android.content.res.Resources;
42import android.database.Cursor;
43import android.database.sqlite.SqliteWrapper;
44import android.graphics.Bitmap;
45import android.graphics.Typeface;
46import android.graphics.drawable.BitmapDrawable;
47import android.media.AudioManager;
48import android.net.Uri;
49import android.os.AsyncTask;
50import android.os.Handler;
51import android.preference.PreferenceManager;
52import android.provider.Telephony.Mms;
53import android.provider.Telephony.Sms;
54import android.text.Spannable;
55import android.text.SpannableString;
56import android.text.SpannableStringBuilder;
57import android.text.TextUtils;
58import android.text.style.StyleSpan;
59import android.text.style.TextAppearanceSpan;
60import android.util.Log;
61import android.widget.Toast;
62
63import com.android.mms.LogTag;
64import com.android.mms.R;
65import com.android.mms.data.Contact;
66import com.android.mms.data.Conversation;
67import com.android.mms.data.WorkingMessage;
68import com.android.mms.model.SlideModel;
69import com.android.mms.model.SlideshowModel;
70import com.android.mms.ui.ComposeMessageActivity;
71import com.android.mms.ui.ConversationList;
72import com.android.mms.ui.MessageUtils;
73import com.android.mms.ui.MessagingPreferenceActivity;
74import com.android.mms.util.AddressUtils;
75import com.android.mms.util.DownloadManager;
76import com.android.mms.widget.MmsWidgetProvider;
77import com.google.android.mms.MmsException;
78import com.google.android.mms.pdu.EncodedStringValue;
79import com.google.android.mms.pdu.GenericPdu;
80import com.google.android.mms.pdu.MultimediaMessagePdu;
81import com.google.android.mms.pdu.PduHeaders;
82import com.google.android.mms.pdu.PduPersister;
83
84/**
85 * This class is used to update the notification indicator. It will check whether
86 * there are unread messages. If yes, it would show the notification indicator,
87 * otherwise, hide the indicator.
88 */
89public class MessagingNotification {
90
91    private static final String TAG = LogTag.APP;
92    private static final boolean DEBUG = false;
93
94    private static final int NOTIFICATION_ID = 123;
95    public static final int MESSAGE_FAILED_NOTIFICATION_ID = 789;
96    public static final int DOWNLOAD_FAILED_NOTIFICATION_ID = 531;
97    /**
98     * This is the volume at which to play the in-conversation notification sound,
99     * expressed as a fraction of the system notification volume.
100     */
101    private static final float IN_CONVERSATION_NOTIFICATION_VOLUME = 0.25f;
102
103    // This must be consistent with the column constants below.
104    private static final String[] MMS_STATUS_PROJECTION = new String[] {
105        Mms.THREAD_ID, Mms.DATE, Mms._ID, Mms.SUBJECT, Mms.SUBJECT_CHARSET };
106
107    // This must be consistent with the column constants below.
108    private static final String[] SMS_STATUS_PROJECTION = new String[] {
109        Sms.THREAD_ID, Sms.DATE, Sms.ADDRESS, Sms.SUBJECT, Sms.BODY };
110
111    // These must be consistent with MMS_STATUS_PROJECTION and
112    // SMS_STATUS_PROJECTION.
113    private static final int COLUMN_THREAD_ID   = 0;
114    private static final int COLUMN_DATE        = 1;
115    private static final int COLUMN_MMS_ID      = 2;
116    private static final int COLUMN_SMS_ADDRESS = 2;
117    private static final int COLUMN_SUBJECT     = 3;
118    private static final int COLUMN_SUBJECT_CS  = 4;
119    private static final int COLUMN_SMS_BODY    = 4;
120
121    private static final String[] SMS_THREAD_ID_PROJECTION = new String[] { Sms.THREAD_ID };
122    private static final String[] MMS_THREAD_ID_PROJECTION = new String[] { Mms.THREAD_ID };
123
124    private static final String NEW_INCOMING_SM_CONSTRAINT =
125            "(" + Sms.TYPE + " = " + Sms.MESSAGE_TYPE_INBOX
126            + " AND " + Sms.SEEN + " = 0)";
127
128    private static final String NEW_DELIVERY_SM_CONSTRAINT =
129        "(" + Sms.TYPE + " = " + Sms.MESSAGE_TYPE_SENT
130        + " AND " + Sms.STATUS + " = "+ Sms.STATUS_COMPLETE +")";
131
132    private static final String NEW_INCOMING_MM_CONSTRAINT =
133            "(" + Mms.MESSAGE_BOX + "=" + Mms.MESSAGE_BOX_INBOX
134            + " AND " + Mms.SEEN + "=0"
135            + " AND (" + Mms.MESSAGE_TYPE + "=" + MESSAGE_TYPE_NOTIFICATION_IND
136            + " OR " + Mms.MESSAGE_TYPE + "=" + MESSAGE_TYPE_RETRIEVE_CONF + "))";
137
138    private static final NotificationInfoComparator INFO_COMPARATOR =
139            new NotificationInfoComparator();
140
141    private static final Uri UNDELIVERED_URI = Uri.parse("content://mms-sms/undelivered");
142
143
144    private final static String NOTIFICATION_DELETED_ACTION =
145            "com.android.mms.NOTIFICATION_DELETED_ACTION";
146
147    public static class OnDeletedReceiver extends BroadcastReceiver {
148        @Override
149        public void onReceive(Context context, Intent intent) {
150            if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
151                Log.d(TAG, "[MessagingNotification] clear notification: mark all msgs seen");
152            }
153
154            Conversation.markAllConversationsAsSeen(context);
155        }
156    }
157
158    public static final long THREAD_ALL = -1;
159    public static final long THREAD_NONE = -2;
160    /**
161     * Keeps track of the thread ID of the conversation that's currently displayed to the user
162     */
163    private static long sCurrentlyDisplayedThreadId;
164    private static final Object sCurrentlyDisplayedThreadLock = new Object();
165
166    private static OnDeletedReceiver sNotificationDeletedReceiver = new OnDeletedReceiver();
167    private static Intent sNotificationOnDeleteIntent;
168    private static Handler sHandler = new Handler();
169    private static PduPersister sPduPersister;
170    private static final int MAX_BITMAP_DIMEN_DP = 360;
171    private static float sScreenDensity;
172
173    private static final int MAX_MESSAGES_TO_SHOW = 8;  // the maximum number of new messages to
174                                                        // show in a single notification.
175
176
177    private MessagingNotification() {
178    }
179
180    public static void init(Context context) {
181        // set up the intent filter for notification deleted action
182        IntentFilter intentFilter = new IntentFilter();
183        intentFilter.addAction(NOTIFICATION_DELETED_ACTION);
184
185        // TODO: should we unregister when the app gets killed?
186        context.registerReceiver(sNotificationDeletedReceiver, intentFilter);
187        sPduPersister = PduPersister.getPduPersister(context);
188
189        // initialize the notification deleted action
190        sNotificationOnDeleteIntent = new Intent(NOTIFICATION_DELETED_ACTION);
191
192        sScreenDensity = context.getResources().getDisplayMetrics().density;
193    }
194
195    /**
196     * Specifies which message thread is currently being viewed by the user. New messages in that
197     * thread will not generate a notification icon and will play the notification sound at a lower
198     * volume. Make sure you set this to THREAD_NONE when the UI component that shows the thread is
199     * no longer visible to the user (e.g. Activity.onPause(), etc.)
200     * @param threadId The ID of the thread that the user is currently viewing. Pass THREAD_NONE
201     *  if the user is not viewing a thread, or THREAD_ALL if the user is viewing the conversation
202     *  list (note: that latter one has no effect as of this implementation)
203     */
204    public static void setCurrentlyDisplayedThreadId(long threadId) {
205        synchronized (sCurrentlyDisplayedThreadLock) {
206            sCurrentlyDisplayedThreadId = threadId;
207            if (DEBUG) {
208                Log.d(TAG, "setCurrentlyDisplayedThreadId: " + sCurrentlyDisplayedThreadId);
209            }
210        }
211    }
212
213    /**
214     * Checks to see if there are any "unseen" messages or delivery
215     * reports.  Shows the most recent notification if there is one.
216     * Does its work and query in a worker thread.
217     *
218     * @param context the context to use
219     */
220    public static void nonBlockingUpdateNewMessageIndicator(final Context context,
221            final long newMsgThreadId,
222            final boolean isStatusMessage) {
223        if (DEBUG) {
224            Log.d(TAG, "nonBlockingUpdateNewMessageIndicator: newMsgThreadId: " +
225                    newMsgThreadId +
226                    " sCurrentlyDisplayedThreadId: " + sCurrentlyDisplayedThreadId);
227        }
228        new Thread(new Runnable() {
229            @Override
230            public void run() {
231                blockingUpdateNewMessageIndicator(context, newMsgThreadId, isStatusMessage);
232            }
233        }, "MessagingNotification.nonBlockingUpdateNewMessageIndicator").start();
234    }
235
236    /**
237     * Checks to see if there are any "unseen" messages or delivery
238     * reports and builds a sorted (by delivery date) list of unread notifications.
239     *
240     * @param context the context to use
241     * @param newMsgThreadId The thread ID of a new message that we're to notify about; if there's
242     *  no new message, use THREAD_NONE. If we should notify about multiple or unknown thread IDs,
243     *  use THREAD_ALL.
244     * @param isStatusMessage
245     */
246    public static void blockingUpdateNewMessageIndicator(Context context, long newMsgThreadId,
247            boolean isStatusMessage) {
248        if (DEBUG) {
249            Contact.logWithTrace(TAG, "blockingUpdateNewMessageIndicator: newMsgThreadId: " +
250                    newMsgThreadId);
251        }
252        // notificationSet is kept sorted by the incoming message delivery time, with the
253        // most recent message first.
254        SortedSet<NotificationInfo> notificationSet =
255                new TreeSet<NotificationInfo>(INFO_COMPARATOR);
256
257        Set<Long> threads = new HashSet<Long>(4);
258
259        addMmsNotificationInfos(context, threads, notificationSet);
260        addSmsNotificationInfos(context, threads, notificationSet);
261
262        if (notificationSet.isEmpty()) {
263            if (DEBUG) {
264                Log.d(TAG, "blockingUpdateNewMessageIndicator: notificationSet is empty, " +
265                        "canceling existing notifications");
266            }
267            cancelNotification(context, NOTIFICATION_ID);
268        } else {
269            if (DEBUG || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
270                Log.d(TAG, "blockingUpdateNewMessageIndicator: count=" + notificationSet.size() +
271                        ", newMsgThreadId=" + newMsgThreadId);
272            }
273            synchronized (sCurrentlyDisplayedThreadLock) {
274                if (newMsgThreadId > 0 && newMsgThreadId == sCurrentlyDisplayedThreadId &&
275                        threads.contains(newMsgThreadId)) {
276                    if (DEBUG) {
277                        Log.d(TAG, "blockingUpdateNewMessageIndicator: newMsgThreadId == " +
278                                "sCurrentlyDisplayedThreadId so NOT showing notification," +
279                                " but playing soft sound. threadId: " + newMsgThreadId);
280                    }
281                    playInConversationNotificationSound(context);
282                    return;
283                }
284            }
285            updateNotification(context, newMsgThreadId != THREAD_NONE, threads.size(),
286                    notificationSet);
287        }
288
289        // And deals with delivery reports (which use Toasts). It's safe to call in a worker
290        // thread because the toast will eventually get posted to a handler.
291        MmsSmsDeliveryInfo delivery = getSmsNewDeliveryInfo(context);
292        if (delivery != null) {
293            delivery.deliver(context, isStatusMessage);
294        }
295    }
296
297    /**
298     * Play the in-conversation notification sound (it's the regular notification sound, but
299     * played at half-volume
300     */
301    private static void playInConversationNotificationSound(Context context) {
302        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
303        String ringtoneStr = sp.getString(MessagingPreferenceActivity.NOTIFICATION_RINGTONE,
304                null);
305        if (TextUtils.isEmpty(ringtoneStr)) {
306            // Nothing to play
307            return;
308        }
309        Uri ringtoneUri = Uri.parse(ringtoneStr);
310        final NotificationPlayer player = new NotificationPlayer(LogTag.APP);
311        player.play(context, ringtoneUri, false, AudioManager.STREAM_NOTIFICATION,
312                IN_CONVERSATION_NOTIFICATION_VOLUME);
313
314        // Stop the sound after five seconds to handle continuous ringtones
315        sHandler.postDelayed(new Runnable() {
316            @Override
317            public void run() {
318                player.stop();
319            }
320        }, 5000);
321    }
322
323    /**
324     * Updates all pending notifications, clearing or updating them as
325     * necessary.
326     */
327    public static void blockingUpdateAllNotifications(final Context context, long threadId) {
328        if (DEBUG) {
329            Contact.logWithTrace(TAG, "blockingUpdateAllNotifications: newMsgThreadId: " +
330                    threadId);
331        }
332        nonBlockingUpdateNewMessageIndicator(context, threadId, false);
333        nonBlockingUpdateSendFailedNotification(context);
334        updateDownloadFailedNotification(context);
335        MmsWidgetProvider.notifyDatasetChanged(context);
336    }
337
338    private static final class MmsSmsDeliveryInfo {
339        public CharSequence mTicker;
340        public long mTimeMillis;
341
342        public MmsSmsDeliveryInfo(CharSequence ticker, long timeMillis) {
343            mTicker = ticker;
344            mTimeMillis = timeMillis;
345        }
346
347        public void deliver(Context context, boolean isStatusMessage) {
348            updateDeliveryNotification(
349                    context, isStatusMessage, mTicker, mTimeMillis);
350        }
351    }
352
353    private static final class NotificationInfo {
354        public final Intent mClickIntent;
355        public final String mMessage;
356        public final CharSequence mTicker;
357        public final long mTimeMillis;
358        public final String mTitle;
359        public final Bitmap mAttachmentBitmap;
360        public final Contact mSender;
361        public final boolean mIsSms;
362        public final int mAttachmentType;
363        public final String mSubject;
364        public final long mThreadId;
365
366        /**
367         * @param isSms true if sms, false if mms
368         * @param clickIntent where to go when the user taps the notification
369         * @param message for a single message, this is the message text
370         * @param subject text of mms subject
371         * @param ticker text displayed ticker-style across the notification, typically formatted
372         * as sender: message
373         * @param timeMillis date the message was received
374         * @param title for a single message, this is the sender
375         * @param attachmentBitmap a bitmap of an attachment, such as a picture or video
376         * @param sender contact of the sender
377         * @param attachmentType of the mms attachment
378         * @param threadId thread this message belongs to
379         */
380        public NotificationInfo(boolean isSms,
381                Intent clickIntent, String message, String subject,
382                CharSequence ticker, long timeMillis, String title,
383                Bitmap attachmentBitmap, Contact sender,
384                int attachmentType, long threadId) {
385            mIsSms = isSms;
386            mClickIntent = clickIntent;
387            mMessage = message;
388            mSubject = subject;
389            mTicker = ticker;
390            mTimeMillis = timeMillis;
391            mTitle = title;
392            mAttachmentBitmap = attachmentBitmap;
393            mSender = sender;
394            mAttachmentType = attachmentType;
395            mThreadId = threadId;
396        }
397
398        public long getTime() {
399            return mTimeMillis;
400        }
401
402        // This is the message string used in bigText and bigPicture notifications.
403        public CharSequence formatBigMessage(Context context) {
404            final TextAppearanceSpan notificationSubjectSpan = new TextAppearanceSpan(
405                    context, R.style.NotificationPrimaryText);
406
407            // Change multiple newlines (with potential white space between), into a single new line
408            final String message =
409                    !TextUtils.isEmpty(mMessage) ? mMessage.replaceAll("\\n\\s+", "\n") : "";
410
411            SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder();
412            if (!TextUtils.isEmpty(mSubject)) {
413                spannableStringBuilder.append(mSubject);
414                spannableStringBuilder.setSpan(notificationSubjectSpan, 0, mSubject.length(), 0);
415            }
416            if (mAttachmentType > WorkingMessage.TEXT) {
417                if (spannableStringBuilder.length() > 0) {
418                    spannableStringBuilder.append('\n');
419                }
420                spannableStringBuilder.append(getAttachmentTypeString(context, mAttachmentType));
421            }
422            if (mMessage != null) {
423                if (spannableStringBuilder.length() > 0) {
424                    spannableStringBuilder.append('\n');
425                }
426                spannableStringBuilder.append(mMessage);
427            }
428            return spannableStringBuilder;
429        }
430
431        // This is the message string used in each line of an inboxStyle notification.
432        public CharSequence formatInboxMessage(Context context) {
433          final TextAppearanceSpan notificationSenderSpan = new TextAppearanceSpan(
434                  context, R.style.NotificationPrimaryText);
435
436          final TextAppearanceSpan notificationSubjectSpan = new TextAppearanceSpan(
437                  context, R.style.NotificationSubjectText);
438
439          // Change multiple newlines (with potential white space between), into a single new line
440          final String message =
441                  !TextUtils.isEmpty(mMessage) ? mMessage.replaceAll("\\n\\s+", "\n") : "";
442
443          SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder();
444          final String sender = mSender.getName();
445          if (!TextUtils.isEmpty(sender)) {
446              spannableStringBuilder.append(sender);
447              spannableStringBuilder.setSpan(notificationSenderSpan, 0, sender.length(), 0);
448          }
449          String separator = context.getString(R.string.notification_separator);
450          if (!mIsSms) {
451              if (!TextUtils.isEmpty(mSubject)) {
452                  if (spannableStringBuilder.length() > 0) {
453                      spannableStringBuilder.append(separator);
454                  }
455                  int start = spannableStringBuilder.length();
456                  spannableStringBuilder.append(mSubject);
457                  spannableStringBuilder.setSpan(notificationSubjectSpan, start,
458                          start + mSubject.length(), 0);
459              }
460              if (mAttachmentType > WorkingMessage.TEXT) {
461                  if (spannableStringBuilder.length() > 0) {
462                      spannableStringBuilder.append(separator);
463                  }
464                  spannableStringBuilder.append(getAttachmentTypeString(context, mAttachmentType));
465              }
466          }
467          if (message.length() > 0) {
468              if (spannableStringBuilder.length() > 0) {
469                  spannableStringBuilder.append(separator);
470              }
471              int start = spannableStringBuilder.length();
472              spannableStringBuilder.append(message);
473              spannableStringBuilder.setSpan(notificationSubjectSpan, start,
474                      start + message.length(), 0);
475          }
476          return spannableStringBuilder;
477        }
478
479        // This is the summary string used in bigPicture notifications.
480        public CharSequence formatPictureMessage(Context context) {
481            final TextAppearanceSpan notificationSubjectSpan = new TextAppearanceSpan(
482                    context, R.style.NotificationPrimaryText);
483
484            // Change multiple newlines (with potential white space between), into a single new line
485            final String message =
486                    !TextUtils.isEmpty(mMessage) ? mMessage.replaceAll("\\n\\s+", "\n") : "";
487
488            // Show the subject or the message (if no subject)
489            SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder();
490            if (!TextUtils.isEmpty(mSubject)) {
491                spannableStringBuilder.append(mSubject);
492                spannableStringBuilder.setSpan(notificationSubjectSpan, 0, mSubject.length(), 0);
493            }
494            if (message.length() > 0 && spannableStringBuilder.length() == 0) {
495                spannableStringBuilder.append(message);
496                spannableStringBuilder.setSpan(notificationSubjectSpan, 0, message.length(), 0);
497            }
498            return spannableStringBuilder;
499        }
500    }
501
502    // Return a formatted string with all the sender names separated by commas.
503    private static CharSequence formatSenders(Context context,
504            ArrayList<NotificationInfo> senders) {
505        final TextAppearanceSpan notificationSenderSpan = new TextAppearanceSpan(
506                context, R.style.NotificationPrimaryText);
507
508        String separator = context.getString(R.string.enumeration_comma);   // ", "
509        SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder();
510        int len = senders.size();
511        for (int i = 0; i < len; i++) {
512            if (i > 0) {
513                spannableStringBuilder.append(separator);
514            }
515            spannableStringBuilder.append(senders.get(i).mSender.getName());
516        }
517        spannableStringBuilder.setSpan(notificationSenderSpan, 0,
518                spannableStringBuilder.length(), 0);
519        return spannableStringBuilder;
520    }
521
522    // Return a formatted string with the attachmentType spelled out as a string. For
523    // no attachment (or just text), return null.
524    private static CharSequence getAttachmentTypeString(Context context, int attachmentType) {
525        final TextAppearanceSpan notificationAttachmentSpan = new TextAppearanceSpan(
526                context, R.style.NotificationSecondaryText);
527        int id = 0;
528        switch (attachmentType) {
529            case WorkingMessage.AUDIO: id = R.string.attachment_audio; break;
530            case WorkingMessage.VIDEO: id = R.string.attachment_video; break;
531            case WorkingMessage.SLIDESHOW: id = R.string.attachment_slideshow; break;
532            case WorkingMessage.IMAGE: id = R.string.attachment_picture; break;
533        }
534        if (id > 0) {
535            final SpannableString spannableString = new SpannableString(context.getString(id));
536            spannableString.setSpan(notificationAttachmentSpan,
537                    0, spannableString.length(), 0);
538            return spannableString;
539        }
540        return null;
541     }
542
543    /**
544     *
545     * Sorts by the time a notification was received in descending order -- newer first.
546     *
547     */
548    private static final class NotificationInfoComparator
549            implements Comparator<NotificationInfo> {
550        @Override
551        public int compare(
552                NotificationInfo info1, NotificationInfo info2) {
553            return Long.signum(info2.getTime() - info1.getTime());
554        }
555    }
556
557    private static final void addMmsNotificationInfos(
558            Context context, Set<Long> threads, SortedSet<NotificationInfo> notificationSet) {
559        ContentResolver resolver = context.getContentResolver();
560
561        // This query looks like this when logged:
562        // I/Database(  147): elapsedTime4Sql|/data/data/com.android.providers.telephony/databases/
563        // mmssms.db|0.362 ms|SELECT thread_id, date, _id, sub, sub_cs FROM pdu WHERE ((msg_box=1
564        // AND seen=0 AND (m_type=130 OR m_type=132))) ORDER BY date desc
565
566        Cursor cursor = SqliteWrapper.query(context, resolver, Mms.CONTENT_URI,
567                            MMS_STATUS_PROJECTION, NEW_INCOMING_MM_CONSTRAINT,
568                            null, Mms.DATE + " desc");
569
570        if (cursor == null) {
571            return;
572        }
573
574        try {
575            while (cursor.moveToNext()) {
576
577                long msgId = cursor.getLong(COLUMN_MMS_ID);
578                Uri msgUri = Mms.CONTENT_URI.buildUpon().appendPath(
579                        Long.toString(msgId)).build();
580                String address = AddressUtils.getFrom(context, msgUri);
581
582                Contact contact = Contact.get(address, false);
583                if (contact.getSendToVoicemail()) {
584                    // don't notify, skip this one
585                    continue;
586                }
587
588                String subject = getMmsSubject(
589                        cursor.getString(COLUMN_SUBJECT), cursor.getInt(COLUMN_SUBJECT_CS));
590                subject = MessageUtils.cleanseMmsSubject(context, subject);
591
592                long threadId = cursor.getLong(COLUMN_THREAD_ID);
593                long timeMillis = cursor.getLong(COLUMN_DATE) * 1000;
594
595                if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
596                    Log.d(TAG, "addMmsNotificationInfos: count=" + cursor.getCount() +
597                            ", addr = " + address + ", thread_id=" + threadId);
598                }
599
600                // Extract the message and/or an attached picture from the first slide
601                Bitmap attachedPicture = null;
602                String messageBody = null;
603                int attachmentType = WorkingMessage.TEXT;
604                try {
605                    GenericPdu pdu = sPduPersister.load(msgUri);
606                    if (pdu != null && pdu instanceof MultimediaMessagePdu) {
607                        SlideshowModel slideshow = SlideshowModel.createFromPduBody(context,
608                                ((MultimediaMessagePdu)pdu).getBody());
609                        attachmentType = getAttachmentType(slideshow);
610                        SlideModel firstSlide = slideshow.get(0);
611                        if (firstSlide != null) {
612                            if (firstSlide.hasImage()) {
613                                int maxDim = dp2Pixels(MAX_BITMAP_DIMEN_DP);
614                                attachedPicture = firstSlide.getImage().getBitmap(maxDim, maxDim);
615                            }
616                            if (firstSlide.hasText()) {
617                                messageBody = firstSlide.getText().getText();
618                            }
619                        }
620                    }
621                } catch (final MmsException e) {
622                    Log.e(TAG, "MmsException loading uri: " + msgUri, e);
623                    continue;   // skip this bad boy -- don't generate an empty notification
624                }
625
626                NotificationInfo info = getNewMessageNotificationInfo(context,
627                        false /* isSms */,
628                        address,
629                        messageBody, subject,
630                        threadId,
631                        timeMillis,
632                        attachedPicture,
633                        contact,
634                        attachmentType);
635
636                notificationSet.add(info);
637
638                threads.add(threadId);
639            }
640        } finally {
641            cursor.close();
642        }
643    }
644
645    // Look at the passed in slideshow and determine what type of attachment it is.
646    private static int getAttachmentType(SlideshowModel slideshow) {
647        int slideCount = slideshow.size();
648
649        if (slideCount == 0) {
650            return WorkingMessage.TEXT;
651        } else if (slideCount > 1) {
652            return WorkingMessage.SLIDESHOW;
653        } else {
654            SlideModel slide = slideshow.get(0);
655            if (slide.hasImage()) {
656                return WorkingMessage.IMAGE;
657            } else if (slide.hasVideo()) {
658                return WorkingMessage.VIDEO;
659            } else if (slide.hasAudio()) {
660                return WorkingMessage.AUDIO;
661            }
662        }
663        return WorkingMessage.TEXT;
664    }
665
666    private static final int dp2Pixels(int dip) {
667        return (int) (dip * sScreenDensity + 0.5f);
668    }
669
670    private static final MmsSmsDeliveryInfo getSmsNewDeliveryInfo(Context context) {
671        ContentResolver resolver = context.getContentResolver();
672        Cursor cursor = SqliteWrapper.query(context, resolver, Sms.CONTENT_URI,
673                    SMS_STATUS_PROJECTION, NEW_DELIVERY_SM_CONSTRAINT,
674                    null, Sms.DATE);
675
676        if (cursor == null) {
677            return null;
678        }
679
680        try {
681            if (!cursor.moveToLast()) {
682                return null;
683            }
684
685            String address = cursor.getString(COLUMN_SMS_ADDRESS);
686            long timeMillis = 3000;
687
688            Contact contact = Contact.get(address, false);
689            String name = contact.getNameAndNumber();
690
691            return new MmsSmsDeliveryInfo(context.getString(R.string.delivery_toast_body, name),
692                timeMillis);
693
694        } finally {
695            cursor.close();
696        }
697    }
698
699    private static final void addSmsNotificationInfos(
700            Context context, Set<Long> threads, SortedSet<NotificationInfo> notificationSet) {
701        ContentResolver resolver = context.getContentResolver();
702        Cursor cursor = SqliteWrapper.query(context, resolver, Sms.CONTENT_URI,
703                            SMS_STATUS_PROJECTION, NEW_INCOMING_SM_CONSTRAINT,
704                            null, Sms.DATE + " desc");
705
706        if (cursor == null) {
707            return;
708        }
709
710        try {
711            while (cursor.moveToNext()) {
712                String address = cursor.getString(COLUMN_SMS_ADDRESS);
713
714                Contact contact = Contact.get(address, false);
715                if (contact.getSendToVoicemail()) {
716                    // don't notify, skip this one
717                    continue;
718                }
719
720                String message = cursor.getString(COLUMN_SMS_BODY);
721                long threadId = cursor.getLong(COLUMN_THREAD_ID);
722                long timeMillis = cursor.getLong(COLUMN_DATE);
723
724                if (Log.isLoggable(LogTag.APP, Log.VERBOSE))
725                {
726                    Log.d(TAG, "addSmsNotificationInfos: count=" + cursor.getCount() +
727                            ", addr=" + address + ", thread_id=" + threadId);
728                }
729
730
731                NotificationInfo info = getNewMessageNotificationInfo(context, true /* isSms */,
732                        address, message, null /* subject */,
733                        threadId, timeMillis, null /* attachmentBitmap */,
734                        contact, WorkingMessage.TEXT);
735
736                notificationSet.add(info);
737
738                threads.add(threadId);
739                threads.add(cursor.getLong(COLUMN_THREAD_ID));
740            }
741        } finally {
742            cursor.close();
743        }
744    }
745
746    private static final NotificationInfo getNewMessageNotificationInfo(
747            Context context,
748            boolean isSms,
749            String address,
750            String message,
751            String subject,
752            long threadId,
753            long timeMillis,
754            Bitmap attachmentBitmap,
755            Contact contact,
756            int attachmentType) {
757        Intent clickIntent = ComposeMessageActivity.createIntent(context, threadId);
758        clickIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
759                | Intent.FLAG_ACTIVITY_SINGLE_TOP
760                | Intent.FLAG_ACTIVITY_CLEAR_TOP);
761
762        String senderInfo = buildTickerMessage(
763                context, address, null, null).toString();
764        String senderInfoName = senderInfo.substring(
765                0, senderInfo.length() - 2);
766        CharSequence ticker = buildTickerMessage(
767                context, address, subject, message);
768
769        return new NotificationInfo(isSms,
770                clickIntent, message, subject, ticker, timeMillis,
771                senderInfoName, attachmentBitmap, contact, attachmentType, threadId);
772    }
773
774    public static void cancelNotification(Context context, int notificationId) {
775        NotificationManager nm = (NotificationManager) context.getSystemService(
776                Context.NOTIFICATION_SERVICE);
777
778        Log.d(TAG, "cancelNotification");
779        nm.cancel(notificationId);
780    }
781
782    private static void updateDeliveryNotification(final Context context,
783                                                   boolean isStatusMessage,
784                                                   final CharSequence message,
785                                                   final long timeMillis) {
786        if (!isStatusMessage) {
787            return;
788        }
789
790
791        if (!MessagingPreferenceActivity.getNotificationEnabled(context)) {
792            return;
793        }
794
795        sHandler.post(new Runnable() {
796            @Override
797            public void run() {
798                Toast.makeText(context, message, (int)timeMillis).show();
799            }
800        });
801    }
802
803    /**
804     * updateNotification is *the* main function for building the actual notification handed to
805     * the NotificationManager
806     * @param context
807     * @param isNew if we've got a new message, show the ticker
808     * @param uniqueThreadCount
809     * @param notificationSet the set of notifications to display
810     */
811    private static void updateNotification(
812            Context context,
813            boolean isNew,
814            int uniqueThreadCount,
815            SortedSet<NotificationInfo> notificationSet) {
816        // If the user has turned off notifications in settings, don't do any notifying.
817        if (!MessagingPreferenceActivity.getNotificationEnabled(context)) {
818            if (DEBUG) {
819                Log.d(TAG, "updateNotification: notifications turned off in prefs, bailing");
820            }
821            return;
822        }
823
824        // Figure out what we've got -- whether all sms's, mms's, or a mixture of both.
825        final int messageCount = notificationSet.size();
826        NotificationInfo mostRecentNotification = notificationSet.first();
827
828        final Notification.Builder noti = new Notification.Builder(context)
829                .setWhen(mostRecentNotification.mTimeMillis);
830
831        if (isNew) {
832            noti.setTicker(mostRecentNotification.mTicker);
833        }
834        TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(context);
835
836        // If we have more than one unique thread, change the title (which would
837        // normally be the contact who sent the message) to a generic one that
838        // makes sense for multiple senders, and change the Intent to take the
839        // user to the conversation list instead of the specific thread.
840
841        // Cases:
842        //   1) single message from single thread - intent goes to ComposeMessageActivity
843        //   2) multiple messages from single thread - intent goes to ComposeMessageActivity
844        //   3) messages from multiple threads - intent goes to ConversationList
845
846        final Resources res = context.getResources();
847        String title = null;
848        Bitmap avatar = null;
849        if (uniqueThreadCount > 1) {    // messages from multiple threads
850            Intent mainActivityIntent = new Intent(Intent.ACTION_MAIN);
851
852            mainActivityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
853                    | Intent.FLAG_ACTIVITY_SINGLE_TOP
854                    | Intent.FLAG_ACTIVITY_CLEAR_TOP);
855
856            mainActivityIntent.setType("vnd.android-dir/mms-sms");
857            taskStackBuilder.addNextIntent(mainActivityIntent);
858            title = context.getString(R.string.message_count_notification, messageCount);
859        } else {    // same thread, single or multiple messages
860            title = mostRecentNotification.mTitle;
861            BitmapDrawable contactDrawable = (BitmapDrawable)mostRecentNotification.mSender
862                    .getAvatar(context, null);
863            if (contactDrawable != null) {
864                // Show the sender's avatar as the big icon. Contact bitmaps are 96x96 so we
865                // have to scale 'em up to 128x128 to fill the whole notification large icon.
866                avatar = contactDrawable.getBitmap();
867                if (avatar != null) {
868                    final int idealIconHeight =
869                        res.getDimensionPixelSize(android.R.dimen.notification_large_icon_height);
870                    final int idealIconWidth =
871                         res.getDimensionPixelSize(android.R.dimen.notification_large_icon_width);
872                    if (avatar.getHeight() < idealIconHeight) {
873                        // Scale this image to fit the intended size
874                        avatar = Bitmap.createScaledBitmap(
875                                avatar, idealIconWidth, idealIconHeight, true);
876                    }
877                    if (avatar != null) {
878                        noti.setLargeIcon(avatar);
879                    }
880                }
881            }
882
883            taskStackBuilder.addParentStack(ComposeMessageActivity.class);
884            taskStackBuilder.addNextIntent(mostRecentNotification.mClickIntent);
885        }
886        // Always have to set the small icon or the notification is ignored
887        noti.setSmallIcon(R.drawable.stat_notify_sms);
888
889        NotificationManager nm = (NotificationManager)
890                context.getSystemService(Context.NOTIFICATION_SERVICE);
891
892        // Update the notification.
893        noti.setContentTitle(title)
894            .setContentIntent(
895                    taskStackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT))
896            .addKind(Notification.KIND_MESSAGE)
897            .setPriority(Notification.PRIORITY_DEFAULT);     // TODO: set based on contact coming
898                                                             // from a favorite.
899
900        int defaults = 0;
901
902        if (isNew) {
903            SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
904
905            boolean vibrate = false;
906            if (sp.contains(MessagingPreferenceActivity.NOTIFICATION_VIBRATE)) {
907                // The most recent change to the vibrate preference is to store a boolean
908                // value in NOTIFICATION_VIBRATE. If prefs contain that preference, use that
909                // first.
910                vibrate = sp.getBoolean(MessagingPreferenceActivity.NOTIFICATION_VIBRATE,
911                        false);
912            } else if (sp.contains(MessagingPreferenceActivity.NOTIFICATION_VIBRATE_WHEN)) {
913                // This is to support the pre-JellyBean MR1.1 version of vibrate preferences
914                // when vibrate was a tri-state setting. As soon as the user opens the Messaging
915                // app's settings, it will migrate this setting from NOTIFICATION_VIBRATE_WHEN
916                // to the boolean value stored in NOTIFICATION_VIBRATE.
917                String vibrateWhen =
918                        sp.getString(MessagingPreferenceActivity.NOTIFICATION_VIBRATE_WHEN, null);
919                vibrate = "always".equals(vibrateWhen);
920            }
921            if (vibrate) {
922                defaults |= Notification.DEFAULT_VIBRATE;
923            }
924
925            String ringtoneStr = sp.getString(MessagingPreferenceActivity.NOTIFICATION_RINGTONE,
926                    null);
927            noti.setSound(TextUtils.isEmpty(ringtoneStr) ? null : Uri.parse(ringtoneStr));
928            Log.d(TAG, "updateNotification: new message, adding sound to the notification");
929        }
930
931        defaults |= Notification.DEFAULT_LIGHTS;
932
933        noti.setDefaults(defaults);
934
935        // set up delete intent
936        noti.setDeleteIntent(PendingIntent.getBroadcast(context, 0,
937                sNotificationOnDeleteIntent, 0));
938
939        final Notification notification;
940
941        if (messageCount == 1) {
942            // We've got a single message
943
944            // This sets the text for the collapsed form:
945            noti.setContentText(mostRecentNotification.formatBigMessage(context));
946
947            if (mostRecentNotification.mAttachmentBitmap != null) {
948                // The message has a picture, show that
949
950                notification = new Notification.BigPictureStyle(noti)
951                    .bigPicture(mostRecentNotification.mAttachmentBitmap)
952                    // This sets the text for the expanded picture form:
953                    .setSummaryText(mostRecentNotification.formatPictureMessage(context))
954                    .build();
955            } else {
956                // Show a single notification -- big style with the text of the whole message
957                notification = new Notification.BigTextStyle(noti)
958                    .bigText(mostRecentNotification.formatBigMessage(context))
959                    .build();
960            }
961            if (DEBUG) {
962                Log.d(TAG, "updateNotification: single message notification");
963            }
964        } else {
965            // We've got multiple messages
966            if (uniqueThreadCount == 1) {
967                // We've got multiple messages for the same thread.
968                // Starting with the oldest new message, display the full text of each message.
969                // Begin a line for each subsequent message.
970                SpannableStringBuilder buf = new SpannableStringBuilder();
971                NotificationInfo infos[] =
972                        notificationSet.toArray(new NotificationInfo[messageCount]);
973                int len = infos.length;
974                for (int i = len - 1; i >= 0; i--) {
975                    NotificationInfo info = infos[i];
976
977                    buf.append(info.formatBigMessage(context));
978
979                    if (i != 0) {
980                        buf.append('\n');
981                    }
982                }
983
984                noti.setContentText(context.getString(R.string.message_count_notification,
985                        messageCount));
986
987                // Show a single notification -- big style with the text of all the messages
988                notification = new Notification.BigTextStyle(noti)
989                    .bigText(buf)
990                    // Forcibly show the last line, with the app's smallIcon in it, if we
991                    // kicked the smallIcon out with an avatar bitmap
992                    .setSummaryText((avatar == null) ? null : " ")
993                    .build();
994                if (DEBUG) {
995                    Log.d(TAG, "updateNotification: multi messages for single thread");
996                }
997            } else {
998                // Build a set of the most recent notification per threadId.
999                HashSet<Long> uniqueThreads = new HashSet<Long>(messageCount);
1000                ArrayList<NotificationInfo> mostRecentNotifPerThread =
1001                        new ArrayList<NotificationInfo>();
1002                Iterator<NotificationInfo> notifications = notificationSet.iterator();
1003                while (notifications.hasNext()) {
1004                    NotificationInfo notificationInfo = notifications.next();
1005                    if (!uniqueThreads.contains(notificationInfo.mThreadId)) {
1006                        uniqueThreads.add(notificationInfo.mThreadId);
1007                        mostRecentNotifPerThread.add(notificationInfo);
1008                    }
1009                }
1010                // When collapsed, show all the senders like this:
1011                //     Fred Flinstone, Barry Manilow, Pete...
1012                noti.setContentText(formatSenders(context, mostRecentNotifPerThread));
1013                Notification.InboxStyle inboxStyle = new Notification.InboxStyle(noti);
1014
1015                // We have to set the summary text to non-empty so the content text doesn't show
1016                // up when expanded.
1017                inboxStyle.setSummaryText(" ");
1018
1019                // At this point we've got multiple messages in multiple threads. We only
1020                // want to show the most recent message per thread, which are in
1021                // mostRecentNotifPerThread.
1022                int uniqueThreadMessageCount = mostRecentNotifPerThread.size();
1023                int maxMessages = Math.min(MAX_MESSAGES_TO_SHOW, uniqueThreadMessageCount);
1024
1025                for (int i = 0; i < maxMessages; i++) {
1026                    NotificationInfo info = mostRecentNotifPerThread.get(i);
1027                    inboxStyle.addLine(info.formatInboxMessage(context));
1028                }
1029                notification = inboxStyle.build();
1030                if (DEBUG) {
1031                    Log.d(TAG, "updateNotification: multi messages," +
1032                            " showing inboxStyle notification");
1033                }
1034            }
1035        }
1036
1037        nm.notify(NOTIFICATION_ID, notification);
1038    }
1039
1040    protected static CharSequence buildTickerMessage(
1041            Context context, String address, String subject, String body) {
1042        String displayAddress = Contact.get(address, true).getName();
1043
1044        StringBuilder buf = new StringBuilder(
1045                displayAddress == null
1046                ? ""
1047                : displayAddress.replace('\n', ' ').replace('\r', ' '));
1048        buf.append(':').append(' ');
1049
1050        int offset = buf.length();
1051        if (!TextUtils.isEmpty(subject)) {
1052            subject = subject.replace('\n', ' ').replace('\r', ' ');
1053            buf.append(subject);
1054            buf.append(' ');
1055        }
1056
1057        if (!TextUtils.isEmpty(body)) {
1058            body = body.replace('\n', ' ').replace('\r', ' ');
1059            buf.append(body);
1060        }
1061
1062        SpannableString spanText = new SpannableString(buf.toString());
1063        spanText.setSpan(new StyleSpan(Typeface.BOLD), 0, offset,
1064                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
1065
1066        return spanText;
1067    }
1068
1069    private static String getMmsSubject(String sub, int charset) {
1070        return TextUtils.isEmpty(sub) ? ""
1071                : new EncodedStringValue(charset, PduPersister.getBytes(sub)).getString();
1072    }
1073
1074    public static void notifyDownloadFailed(Context context, long threadId) {
1075        notifyFailed(context, true, threadId, false);
1076    }
1077
1078    public static void notifySendFailed(Context context) {
1079        notifyFailed(context, false, 0, false);
1080    }
1081
1082    public static void notifySendFailed(Context context, boolean noisy) {
1083        notifyFailed(context, false, 0, noisy);
1084    }
1085
1086    private static void notifyFailed(Context context, boolean isDownload, long threadId,
1087                                     boolean noisy) {
1088        // TODO factor out common code for creating notifications
1089        boolean enabled = MessagingPreferenceActivity.getNotificationEnabled(context);
1090        if (!enabled) {
1091            return;
1092        }
1093
1094        // Strategy:
1095        // a. If there is a single failure notification, tapping on the notification goes
1096        //    to the compose view.
1097        // b. If there are two failure it stays in the thread view. Selecting one undelivered
1098        //    thread will dismiss one undelivered notification but will still display the
1099        //    notification.If you select the 2nd undelivered one it will dismiss the notification.
1100
1101        long[] msgThreadId = {0, 1};    // Dummy initial values, just to initialize the memory
1102        int totalFailedCount = getUndeliveredMessageCount(context, msgThreadId);
1103        if (totalFailedCount == 0 && !isDownload) {
1104            return;
1105        }
1106        // The getUndeliveredMessageCount method puts a non-zero value in msgThreadId[1] if all
1107        // failures are from the same thread.
1108        // If isDownload is true, we're dealing with 1 specific failure; therefore "all failed" are
1109        // indeed in the same thread since there's only 1.
1110        boolean allFailedInSameThread = (msgThreadId[1] != 0) || isDownload;
1111
1112        Intent failedIntent;
1113        Notification notification = new Notification();
1114        String title;
1115        String description;
1116        if (totalFailedCount > 1) {
1117            description = context.getString(R.string.notification_failed_multiple,
1118                    Integer.toString(totalFailedCount));
1119            title = context.getString(R.string.notification_failed_multiple_title);
1120        } else {
1121            title = isDownload ?
1122                        context.getString(R.string.message_download_failed_title) :
1123                        context.getString(R.string.message_send_failed_title);
1124
1125            description = context.getString(R.string.message_failed_body);
1126        }
1127
1128        TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(context);
1129        if (allFailedInSameThread) {
1130            failedIntent = new Intent(context, ComposeMessageActivity.class);
1131            if (isDownload) {
1132                // When isDownload is true, the valid threadId is passed into this function.
1133                failedIntent.putExtra("failed_download_flag", true);
1134            } else {
1135                threadId = msgThreadId[0];
1136                failedIntent.putExtra("undelivered_flag", true);
1137            }
1138            failedIntent.putExtra("thread_id", threadId);
1139            taskStackBuilder.addParentStack(ComposeMessageActivity.class);
1140        } else {
1141            failedIntent = new Intent(context, ConversationList.class);
1142        }
1143        taskStackBuilder.addNextIntent(failedIntent);
1144
1145        notification.icon = R.drawable.stat_notify_sms_failed;
1146
1147        notification.tickerText = title;
1148
1149        notification.setLatestEventInfo(context, title, description,
1150                taskStackBuilder.getPendingIntent(0,  PendingIntent.FLAG_UPDATE_CURRENT));
1151
1152        if (noisy) {
1153            SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
1154            boolean vibrate = sp.getBoolean(MessagingPreferenceActivity.NOTIFICATION_VIBRATE,
1155                    false /* don't vibrate by default */);
1156            if (vibrate) {
1157                notification.defaults |= Notification.DEFAULT_VIBRATE;
1158            }
1159
1160            String ringtoneStr = sp.getString(MessagingPreferenceActivity.NOTIFICATION_RINGTONE,
1161                    null);
1162            notification.sound = TextUtils.isEmpty(ringtoneStr) ? null : Uri.parse(ringtoneStr);
1163        }
1164
1165        NotificationManager notificationMgr = (NotificationManager)
1166                context.getSystemService(Context.NOTIFICATION_SERVICE);
1167
1168        if (isDownload) {
1169            notificationMgr.notify(DOWNLOAD_FAILED_NOTIFICATION_ID, notification);
1170        } else {
1171            notificationMgr.notify(MESSAGE_FAILED_NOTIFICATION_ID, notification);
1172        }
1173    }
1174
1175    /**
1176     * Query the DB and return the number of undelivered messages (total for both SMS and MMS)
1177     * @param context The context
1178     * @param threadIdResult A container to put the result in, according to the following rules:
1179     *  threadIdResult[0] contains the thread id of the first message.
1180     *  threadIdResult[1] is nonzero if the thread ids of all the messages are the same.
1181     *  You can pass in null for threadIdResult.
1182     *  You can pass in a threadIdResult of size 1 to avoid the comparison of each thread id.
1183     */
1184    private static int getUndeliveredMessageCount(Context context, long[] threadIdResult) {
1185        Cursor undeliveredCursor = SqliteWrapper.query(context, context.getContentResolver(),
1186                UNDELIVERED_URI, MMS_THREAD_ID_PROJECTION, "read=0", null, null);
1187        if (undeliveredCursor == null) {
1188            return 0;
1189        }
1190        int count = undeliveredCursor.getCount();
1191        try {
1192            if (threadIdResult != null && undeliveredCursor.moveToFirst()) {
1193                threadIdResult[0] = undeliveredCursor.getLong(0);
1194
1195                if (threadIdResult.length >= 2) {
1196                    // Test to see if all the undelivered messages belong to the same thread.
1197                    long firstId = threadIdResult[0];
1198                    while (undeliveredCursor.moveToNext()) {
1199                        if (undeliveredCursor.getLong(0) != firstId) {
1200                            firstId = 0;
1201                            break;
1202                        }
1203                    }
1204                    threadIdResult[1] = firstId;    // non-zero if all ids are the same
1205                }
1206            }
1207        } finally {
1208            undeliveredCursor.close();
1209        }
1210        return count;
1211    }
1212
1213    public static void nonBlockingUpdateSendFailedNotification(final Context context) {
1214        new AsyncTask<Void, Void, Integer>() {
1215            protected Integer doInBackground(Void... none) {
1216                return getUndeliveredMessageCount(context, null);
1217            }
1218
1219            protected void onPostExecute(Integer result) {
1220                if (result < 1) {
1221                    cancelNotification(context, MESSAGE_FAILED_NOTIFICATION_ID);
1222                } else {
1223                    // rebuild and adjust the message count if necessary.
1224                    notifySendFailed(context);
1225                }
1226            }
1227        }.execute();
1228    }
1229
1230    /**
1231     *  If all the undelivered messages belong to "threadId", cancel the notification.
1232     */
1233    public static void updateSendFailedNotificationForThread(Context context, long threadId) {
1234        long[] msgThreadId = {0, 0};
1235        if (getUndeliveredMessageCount(context, msgThreadId) > 0
1236                && msgThreadId[0] == threadId
1237                && msgThreadId[1] != 0) {
1238            cancelNotification(context, MESSAGE_FAILED_NOTIFICATION_ID);
1239        }
1240    }
1241
1242    private static int getDownloadFailedMessageCount(Context context) {
1243        // Look for any messages in the MMS Inbox that are of the type
1244        // NOTIFICATION_IND (i.e. not already downloaded) and in the
1245        // permanent failure state.  If there are none, cancel any
1246        // failed download notification.
1247        Cursor c = SqliteWrapper.query(context, context.getContentResolver(),
1248                Mms.Inbox.CONTENT_URI, null,
1249                Mms.MESSAGE_TYPE + "=" +
1250                    String.valueOf(PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND) +
1251                " AND " + Mms.STATUS + "=" +
1252                    String.valueOf(DownloadManager.STATE_PERMANENT_FAILURE),
1253                null, null);
1254        if (c == null) {
1255            return 0;
1256        }
1257        int count = c.getCount();
1258        c.close();
1259        return count;
1260    }
1261
1262    public static void updateDownloadFailedNotification(Context context) {
1263        if (getDownloadFailedMessageCount(context) < 1) {
1264            cancelNotification(context, DOWNLOAD_FAILED_NOTIFICATION_ID);
1265        }
1266    }
1267
1268    public static boolean isFailedToDeliver(Intent intent) {
1269        return (intent != null) && intent.getBooleanExtra("undelivered_flag", false);
1270    }
1271
1272    public static boolean isFailedToDownload(Intent intent) {
1273        return (intent != null) && intent.getBooleanExtra("failed_download_flag", false);
1274    }
1275
1276    /**
1277     * Get the thread ID of the SMS message with the given URI
1278     * @param context The context
1279     * @param uri The URI of the SMS message
1280     * @return The thread ID, or THREAD_NONE if the URI contains no entries
1281     */
1282    public static long getSmsThreadId(Context context, Uri uri) {
1283        Cursor cursor = SqliteWrapper.query(
1284            context,
1285            context.getContentResolver(),
1286            uri,
1287            SMS_THREAD_ID_PROJECTION,
1288            null,
1289            null,
1290            null);
1291
1292        if (cursor == null) {
1293            if (DEBUG) {
1294                Log.d(TAG, "getSmsThreadId uri: " + uri + " NULL cursor! returning THREAD_NONE");
1295            }
1296            return THREAD_NONE;
1297        }
1298
1299        try {
1300            if (cursor.moveToFirst()) {
1301                long threadId = cursor.getLong(cursor.getColumnIndex(Sms.THREAD_ID));
1302                if (DEBUG) {
1303                    Log.d(TAG, "getSmsThreadId uri: " + uri +
1304                            " returning threadId: " + threadId);
1305                }
1306                return threadId;
1307            } else {
1308                if (DEBUG) {
1309                    Log.d(TAG, "getSmsThreadId uri: " + uri +
1310                            " NULL cursor! returning THREAD_NONE");
1311                }
1312                return THREAD_NONE;
1313            }
1314        } finally {
1315            cursor.close();
1316        }
1317    }
1318
1319    /**
1320     * Get the thread ID of the MMS message with the given URI
1321     * @param context The context
1322     * @param uri The URI of the SMS message
1323     * @return The thread ID, or THREAD_NONE if the URI contains no entries
1324     */
1325    public static long getThreadId(Context context, Uri uri) {
1326        Cursor cursor = SqliteWrapper.query(
1327                context,
1328                context.getContentResolver(),
1329                uri,
1330                MMS_THREAD_ID_PROJECTION,
1331                null,
1332                null,
1333                null);
1334
1335        if (cursor == null) {
1336            if (DEBUG) {
1337                Log.d(TAG, "getThreadId uri: " + uri + " NULL cursor! returning THREAD_NONE");
1338            }
1339            return THREAD_NONE;
1340        }
1341
1342        try {
1343            if (cursor.moveToFirst()) {
1344                long threadId = cursor.getLong(cursor.getColumnIndex(Mms.THREAD_ID));
1345                if (DEBUG) {
1346                    Log.d(TAG, "getThreadId uri: " + uri +
1347                            " returning threadId: " + threadId);
1348                }
1349                return threadId;
1350            } else {
1351                if (DEBUG) {
1352                    Log.d(TAG, "getThreadId uri: " + uri +
1353                            " NULL cursor! returning THREAD_NONE");
1354                }
1355                return THREAD_NONE;
1356            }
1357        } finally {
1358            cursor.close();
1359        }
1360    }
1361}
1362