MessagingNotification.java revision 7ecebc423d3fb8e9d8b5703780582f5a486aa742
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 sToastHandler = 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        NotificationPlayer player = new NotificationPlayer(LogTag.APP);
311        player.play(context, ringtoneUri, false, AudioManager.STREAM_NOTIFICATION,
312                IN_CONVERSATION_NOTIFICATION_VOLUME);
313    }
314
315    /**
316     * Updates all pending notifications, clearing or updating them as
317     * necessary.
318     */
319    public static void blockingUpdateAllNotifications(final Context context, long threadId) {
320        if (DEBUG) {
321            Contact.logWithTrace(TAG, "blockingUpdateAllNotifications: newMsgThreadId: " +
322                    threadId);
323        }
324        nonBlockingUpdateNewMessageIndicator(context, threadId, false);
325        nonBlockingUpdateSendFailedNotification(context);
326        updateDownloadFailedNotification(context);
327        MmsWidgetProvider.notifyDatasetChanged(context);
328    }
329
330    private static final class MmsSmsDeliveryInfo {
331        public CharSequence mTicker;
332        public long mTimeMillis;
333
334        public MmsSmsDeliveryInfo(CharSequence ticker, long timeMillis) {
335            mTicker = ticker;
336            mTimeMillis = timeMillis;
337        }
338
339        public void deliver(Context context, boolean isStatusMessage) {
340            updateDeliveryNotification(
341                    context, isStatusMessage, mTicker, mTimeMillis);
342        }
343    }
344
345    private static final class NotificationInfo {
346        public final Intent mClickIntent;
347        public final String mMessage;
348        public final CharSequence mTicker;
349        public final long mTimeMillis;
350        public final String mTitle;
351        public final Bitmap mAttachmentBitmap;
352        public final Contact mSender;
353        public final boolean mIsSms;
354        public final int mAttachmentType;
355        public final String mSubject;
356        public final long mThreadId;
357
358        /**
359         * @param isSms true if sms, false if mms
360         * @param clickIntent where to go when the user taps the notification
361         * @param message for a single message, this is the message text
362         * @param subject text of mms subject
363         * @param ticker text displayed ticker-style across the notification, typically formatted
364         * as sender: message
365         * @param timeMillis date the message was received
366         * @param title for a single message, this is the sender
367         * @param attachmentBitmap a bitmap of an attachment, such as a picture or video
368         * @param sender contact of the sender
369         * @param attachmentType of the mms attachment
370         * @param threadId thread this message belongs to
371         */
372        public NotificationInfo(boolean isSms,
373                Intent clickIntent, String message, String subject,
374                CharSequence ticker, long timeMillis, String title,
375                Bitmap attachmentBitmap, Contact sender,
376                int attachmentType, long threadId) {
377            mIsSms = isSms;
378            mClickIntent = clickIntent;
379            mMessage = message;
380            mSubject = subject;
381            mTicker = ticker;
382            mTimeMillis = timeMillis;
383            mTitle = title;
384            mAttachmentBitmap = attachmentBitmap;
385            mSender = sender;
386            mAttachmentType = attachmentType;
387            mThreadId = threadId;
388        }
389
390        public long getTime() {
391            return mTimeMillis;
392        }
393
394        // This is the message string used in bigText and bigPicture notifications.
395        public CharSequence formatBigMessage(Context context) {
396            final TextAppearanceSpan notificationSubjectSpan = new TextAppearanceSpan(
397                    context, R.style.NotificationPrimaryText);
398
399            // Change multiple newlines (with potential white space between), into a single new line
400            final String message =
401                    !TextUtils.isEmpty(mMessage) ? mMessage.replaceAll("\\n\\s+", "\n") : "";
402
403            SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder();
404            if (!TextUtils.isEmpty(mSubject)) {
405                spannableStringBuilder.append(mSubject);
406                spannableStringBuilder.setSpan(notificationSubjectSpan, 0, mSubject.length(), 0);
407            }
408            if (mAttachmentType > WorkingMessage.TEXT) {
409                if (spannableStringBuilder.length() > 0) {
410                    spannableStringBuilder.append('\n');
411                }
412                spannableStringBuilder.append(getAttachmentTypeString(context, mAttachmentType));
413            }
414            if (mMessage != null) {
415                if (spannableStringBuilder.length() > 0) {
416                    spannableStringBuilder.append('\n');
417                }
418                spannableStringBuilder.append(mMessage);
419            }
420            return spannableStringBuilder;
421        }
422
423        // This is the message string used in each line of an inboxStyle notification.
424        public CharSequence formatInboxMessage(Context context) {
425          final TextAppearanceSpan notificationSenderSpan = new TextAppearanceSpan(
426                  context, R.style.NotificationPrimaryText);
427
428          final TextAppearanceSpan notificationSubjectSpan = new TextAppearanceSpan(
429                  context, R.style.NotificationSubjectText);
430
431          // Change multiple newlines (with potential white space between), into a single new line
432          final String message =
433                  !TextUtils.isEmpty(mMessage) ? mMessage.replaceAll("\\n\\s+", "\n") : "";
434
435          SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder();
436          final String sender = mSender.getName();
437          if (!TextUtils.isEmpty(sender)) {
438              spannableStringBuilder.append(sender);
439              spannableStringBuilder.setSpan(notificationSenderSpan, 0, sender.length(), 0);
440          }
441          String separator = context.getString(R.string.notification_separator);
442          if (!mIsSms) {
443              if (!TextUtils.isEmpty(mSubject)) {
444                  if (spannableStringBuilder.length() > 0) {
445                      spannableStringBuilder.append(separator);
446                  }
447                  int start = spannableStringBuilder.length();
448                  spannableStringBuilder.append(mSubject);
449                  spannableStringBuilder.setSpan(notificationSubjectSpan, start,
450                          start + mSubject.length(), 0);
451              }
452              if (mAttachmentType > WorkingMessage.TEXT) {
453                  if (spannableStringBuilder.length() > 0) {
454                      spannableStringBuilder.append(separator);
455                  }
456                  spannableStringBuilder.append(getAttachmentTypeString(context, mAttachmentType));
457              }
458          }
459          if (message.length() > 0) {
460              if (spannableStringBuilder.length() > 0) {
461                  spannableStringBuilder.append(separator);
462              }
463              int start = spannableStringBuilder.length();
464              spannableStringBuilder.append(message);
465              spannableStringBuilder.setSpan(notificationSubjectSpan, start,
466                      start + message.length(), 0);
467          }
468          return spannableStringBuilder;
469        }
470
471        // This is the summary string used in bigPicture notifications.
472        public CharSequence formatPictureMessage(Context context) {
473            final TextAppearanceSpan notificationSubjectSpan = new TextAppearanceSpan(
474                    context, R.style.NotificationPrimaryText);
475
476            // Change multiple newlines (with potential white space between), into a single new line
477            final String message =
478                    !TextUtils.isEmpty(mMessage) ? mMessage.replaceAll("\\n\\s+", "\n") : "";
479
480            // Show the subject or the message (if no subject)
481            SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder();
482            if (!TextUtils.isEmpty(mSubject)) {
483                spannableStringBuilder.append(mSubject);
484                spannableStringBuilder.setSpan(notificationSubjectSpan, 0, mSubject.length(), 0);
485            }
486            if (message.length() > 0 && spannableStringBuilder.length() == 0) {
487                spannableStringBuilder.append(message);
488                spannableStringBuilder.setSpan(notificationSubjectSpan, 0, message.length(), 0);
489            }
490            return spannableStringBuilder;
491        }
492    }
493
494    // Return a formatted string with all the sender names separated by commas.
495    private static CharSequence formatSenders(Context context,
496            ArrayList<NotificationInfo> senders) {
497        final TextAppearanceSpan notificationSenderSpan = new TextAppearanceSpan(
498                context, R.style.NotificationPrimaryText);
499
500        String separator = context.getString(R.string.enumeration_comma);   // ", "
501        SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder();
502        int len = senders.size();
503        for (int i = 0; i < len; i++) {
504            if (i > 0) {
505                spannableStringBuilder.append(separator);
506            }
507            spannableStringBuilder.append(senders.get(i).mSender.getName());
508        }
509        spannableStringBuilder.setSpan(notificationSenderSpan, 0,
510                spannableStringBuilder.length(), 0);
511        return spannableStringBuilder;
512    }
513
514    // Return a formatted string with the attachmentType spelled out as a string. For
515    // no attachment (or just text), return null.
516    private static CharSequence getAttachmentTypeString(Context context, int attachmentType) {
517        final TextAppearanceSpan notificationAttachmentSpan = new TextAppearanceSpan(
518                context, R.style.NotificationSecondaryText);
519        int id = 0;
520        switch (attachmentType) {
521            case WorkingMessage.AUDIO: id = R.string.attachment_audio; break;
522            case WorkingMessage.VIDEO: id = R.string.attachment_video; break;
523            case WorkingMessage.SLIDESHOW: id = R.string.attachment_slideshow; break;
524            case WorkingMessage.IMAGE: id = R.string.attachment_picture; break;
525        }
526        if (id > 0) {
527            final SpannableString spannableString = new SpannableString(context.getString(id));
528            spannableString.setSpan(notificationAttachmentSpan,
529                    0, spannableString.length(), 0);
530            return spannableString;
531        }
532        return null;
533     }
534
535    /**
536     *
537     * Sorts by the time a notification was received in descending order -- newer first.
538     *
539     */
540    private static final class NotificationInfoComparator
541            implements Comparator<NotificationInfo> {
542        @Override
543        public int compare(
544                NotificationInfo info1, NotificationInfo info2) {
545            return Long.signum(info2.getTime() - info1.getTime());
546        }
547    }
548
549    private static final void addMmsNotificationInfos(
550            Context context, Set<Long> threads, SortedSet<NotificationInfo> notificationSet) {
551        ContentResolver resolver = context.getContentResolver();
552
553        // This query looks like this when logged:
554        // I/Database(  147): elapsedTime4Sql|/data/data/com.android.providers.telephony/databases/
555        // mmssms.db|0.362 ms|SELECT thread_id, date, _id, sub, sub_cs FROM pdu WHERE ((msg_box=1
556        // AND seen=0 AND (m_type=130 OR m_type=132))) ORDER BY date desc
557
558        Cursor cursor = SqliteWrapper.query(context, resolver, Mms.CONTENT_URI,
559                            MMS_STATUS_PROJECTION, NEW_INCOMING_MM_CONSTRAINT,
560                            null, Mms.DATE + " desc");
561
562        if (cursor == null) {
563            return;
564        }
565
566        try {
567            while (cursor.moveToNext()) {
568
569                long msgId = cursor.getLong(COLUMN_MMS_ID);
570                Uri msgUri = Mms.CONTENT_URI.buildUpon().appendPath(
571                        Long.toString(msgId)).build();
572                String address = AddressUtils.getFrom(context, msgUri);
573
574                Contact contact = Contact.get(address, false);
575                if (contact.getSendToVoicemail()) {
576                    // don't notify, skip this one
577                    continue;
578                }
579
580                String subject = getMmsSubject(
581                        cursor.getString(COLUMN_SUBJECT), cursor.getInt(COLUMN_SUBJECT_CS));
582                subject = MessageUtils.cleanseMmsSubject(context, subject);
583
584                long threadId = cursor.getLong(COLUMN_THREAD_ID);
585                long timeMillis = cursor.getLong(COLUMN_DATE) * 1000;
586
587                if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
588                    Log.d(TAG, "addMmsNotificationInfos: count=" + cursor.getCount() +
589                            ", addr = " + address + ", thread_id=" + threadId);
590                }
591
592                // Extract the message and/or an attached picture from the first slide
593                Bitmap attachedPicture = null;
594                String messageBody = null;
595                int attachmentType = WorkingMessage.TEXT;
596                try {
597                    GenericPdu pdu = sPduPersister.load(msgUri);
598                    if (pdu != null && pdu instanceof MultimediaMessagePdu) {
599                        SlideshowModel slideshow = SlideshowModel.createFromPduBody(context,
600                                ((MultimediaMessagePdu)pdu).getBody());
601                        attachmentType = getAttachmentType(slideshow);
602                        SlideModel firstSlide = slideshow.get(0);
603                        if (firstSlide != null) {
604                            if (firstSlide.hasImage()) {
605                                int maxDim = dp2Pixels(MAX_BITMAP_DIMEN_DP);
606                                attachedPicture = firstSlide.getImage().getBitmap(maxDim, maxDim);
607                            }
608                            if (firstSlide.hasText()) {
609                                messageBody = firstSlide.getText().getText();
610                            }
611                        }
612                    }
613                } catch (final MmsException e) {
614                    Log.e(TAG, "MmsException loading uri: " + msgUri, e);
615                    continue;   // skip this bad boy -- don't generate an empty notification
616                }
617
618                NotificationInfo info = getNewMessageNotificationInfo(context,
619                        false /* isSms */,
620                        address,
621                        messageBody, subject,
622                        threadId,
623                        timeMillis,
624                        attachedPicture,
625                        contact,
626                        attachmentType);
627
628                notificationSet.add(info);
629
630                threads.add(threadId);
631            }
632        } finally {
633            cursor.close();
634        }
635    }
636
637    // Look at the passed in slideshow and determine what type of attachment it is.
638    private static int getAttachmentType(SlideshowModel slideshow) {
639        int slideCount = slideshow.size();
640
641        if (slideCount == 0) {
642            return WorkingMessage.TEXT;
643        } else if (slideCount > 1) {
644            return WorkingMessage.SLIDESHOW;
645        } else {
646            SlideModel slide = slideshow.get(0);
647            if (slide.hasImage()) {
648                return WorkingMessage.IMAGE;
649            } else if (slide.hasVideo()) {
650                return WorkingMessage.VIDEO;
651            } else if (slide.hasAudio()) {
652                return WorkingMessage.AUDIO;
653            }
654        }
655        return WorkingMessage.TEXT;
656    }
657
658    private static final int dp2Pixels(int dip) {
659        return (int) (dip * sScreenDensity + 0.5f);
660    }
661
662    private static final MmsSmsDeliveryInfo getSmsNewDeliveryInfo(Context context) {
663        ContentResolver resolver = context.getContentResolver();
664        Cursor cursor = SqliteWrapper.query(context, resolver, Sms.CONTENT_URI,
665                    SMS_STATUS_PROJECTION, NEW_DELIVERY_SM_CONSTRAINT,
666                    null, Sms.DATE);
667
668        if (cursor == null) {
669            return null;
670        }
671
672        try {
673            if (!cursor.moveToLast()) {
674                return null;
675            }
676
677            String address = cursor.getString(COLUMN_SMS_ADDRESS);
678            long timeMillis = 3000;
679
680            Contact contact = Contact.get(address, false);
681            String name = contact.getNameAndNumber();
682
683            return new MmsSmsDeliveryInfo(context.getString(R.string.delivery_toast_body, name),
684                timeMillis);
685
686        } finally {
687            cursor.close();
688        }
689    }
690
691    private static final void addSmsNotificationInfos(
692            Context context, Set<Long> threads, SortedSet<NotificationInfo> notificationSet) {
693        ContentResolver resolver = context.getContentResolver();
694        Cursor cursor = SqliteWrapper.query(context, resolver, Sms.CONTENT_URI,
695                            SMS_STATUS_PROJECTION, NEW_INCOMING_SM_CONSTRAINT,
696                            null, Sms.DATE + " desc");
697
698        if (cursor == null) {
699            return;
700        }
701
702        try {
703            while (cursor.moveToNext()) {
704                String address = cursor.getString(COLUMN_SMS_ADDRESS);
705
706                Contact contact = Contact.get(address, false);
707                if (contact.getSendToVoicemail()) {
708                    // don't notify, skip this one
709                    continue;
710                }
711
712                String message = cursor.getString(COLUMN_SMS_BODY);
713                long threadId = cursor.getLong(COLUMN_THREAD_ID);
714                long timeMillis = cursor.getLong(COLUMN_DATE);
715
716                if (Log.isLoggable(LogTag.APP, Log.VERBOSE))
717                {
718                    Log.d(TAG, "addSmsNotificationInfos: count=" + cursor.getCount() +
719                            ", addr=" + address + ", thread_id=" + threadId);
720                }
721
722
723                NotificationInfo info = getNewMessageNotificationInfo(context, true /* isSms */,
724                        address, message, null /* subject */,
725                        threadId, timeMillis, null /* attachmentBitmap */,
726                        contact, WorkingMessage.TEXT);
727
728                notificationSet.add(info);
729
730                threads.add(threadId);
731                threads.add(cursor.getLong(COLUMN_THREAD_ID));
732            }
733        } finally {
734            cursor.close();
735        }
736    }
737
738    private static final NotificationInfo getNewMessageNotificationInfo(
739            Context context,
740            boolean isSms,
741            String address,
742            String message,
743            String subject,
744            long threadId,
745            long timeMillis,
746            Bitmap attachmentBitmap,
747            Contact contact,
748            int attachmentType) {
749        Intent clickIntent = ComposeMessageActivity.createIntent(context, threadId);
750        clickIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
751                | Intent.FLAG_ACTIVITY_SINGLE_TOP
752                | Intent.FLAG_ACTIVITY_CLEAR_TOP);
753
754        String senderInfo = buildTickerMessage(
755                context, address, null, null).toString();
756        String senderInfoName = senderInfo.substring(
757                0, senderInfo.length() - 2);
758        CharSequence ticker = buildTickerMessage(
759                context, address, subject, message);
760
761        return new NotificationInfo(isSms,
762                clickIntent, message, subject, ticker, timeMillis,
763                senderInfoName, attachmentBitmap, contact, attachmentType, threadId);
764    }
765
766    public static void cancelNotification(Context context, int notificationId) {
767        NotificationManager nm = (NotificationManager) context.getSystemService(
768                Context.NOTIFICATION_SERVICE);
769
770        Log.d(TAG, "cancelNotification");
771        nm.cancel(notificationId);
772    }
773
774    private static void updateDeliveryNotification(final Context context,
775                                                   boolean isStatusMessage,
776                                                   final CharSequence message,
777                                                   final long timeMillis) {
778        if (!isStatusMessage) {
779            return;
780        }
781
782
783        if (!MessagingPreferenceActivity.getNotificationEnabled(context)) {
784            return;
785        }
786
787        sToastHandler.post(new Runnable() {
788            @Override
789            public void run() {
790                Toast.makeText(context, message, (int)timeMillis).show();
791            }
792        });
793    }
794
795    /**
796     * updateNotification is *the* main function for building the actual notification handed to
797     * the NotificationManager
798     * @param context
799     * @param isNew if we've got a new message, show the ticker
800     * @param uniqueThreadCount
801     * @param notificationSet the set of notifications to display
802     */
803    private static void updateNotification(
804            Context context,
805            boolean isNew,
806            int uniqueThreadCount,
807            SortedSet<NotificationInfo> notificationSet) {
808        // If the user has turned off notifications in settings, don't do any notifying.
809        if (!MessagingPreferenceActivity.getNotificationEnabled(context)) {
810            if (DEBUG) {
811                Log.d(TAG, "updateNotification: notifications turned off in prefs, bailing");
812            }
813            return;
814        }
815
816        // Figure out what we've got -- whether all sms's, mms's, or a mixture of both.
817        final int messageCount = notificationSet.size();
818        NotificationInfo mostRecentNotification = notificationSet.first();
819
820        final Notification.Builder noti = new Notification.Builder(context)
821                .setWhen(mostRecentNotification.mTimeMillis);
822
823        if (isNew) {
824            noti.setTicker(mostRecentNotification.mTicker);
825        }
826        TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(context);
827
828        // If we have more than one unique thread, change the title (which would
829        // normally be the contact who sent the message) to a generic one that
830        // makes sense for multiple senders, and change the Intent to take the
831        // user to the conversation list instead of the specific thread.
832
833        // Cases:
834        //   1) single message from single thread - intent goes to ComposeMessageActivity
835        //   2) multiple messages from single thread - intent goes to ComposeMessageActivity
836        //   3) messages from multiple threads - intent goes to ConversationList
837
838        final Resources res = context.getResources();
839        String title = null;
840        Bitmap avatar = null;
841        if (uniqueThreadCount > 1) {    // messages from multiple threads
842            Intent mainActivityIntent = new Intent(Intent.ACTION_MAIN);
843
844            mainActivityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
845                    | Intent.FLAG_ACTIVITY_SINGLE_TOP
846                    | Intent.FLAG_ACTIVITY_CLEAR_TOP);
847
848            mainActivityIntent.setType("vnd.android-dir/mms-sms");
849            taskStackBuilder.addNextIntent(mainActivityIntent);
850            title = context.getString(R.string.message_count_notification, messageCount);
851        } else {    // same thread, single or multiple messages
852            title = mostRecentNotification.mTitle;
853            BitmapDrawable contactDrawable = (BitmapDrawable)mostRecentNotification.mSender
854                    .getAvatar(context, null);
855            if (contactDrawable != null) {
856                // Show the sender's avatar as the big icon. Contact bitmaps are 96x96 so we
857                // have to scale 'em up to 128x128 to fill the whole notification large icon.
858                avatar = contactDrawable.getBitmap();
859                if (avatar != null) {
860                    final int idealIconHeight =
861                        res.getDimensionPixelSize(android.R.dimen.notification_large_icon_height);
862                    final int idealIconWidth =
863                         res.getDimensionPixelSize(android.R.dimen.notification_large_icon_width);
864                    if (avatar.getHeight() < idealIconHeight) {
865                        // Scale this image to fit the intended size
866                        avatar = Bitmap.createScaledBitmap(
867                                avatar, idealIconWidth, idealIconHeight, true);
868                    }
869                    if (avatar != null) {
870                        noti.setLargeIcon(avatar);
871                    }
872                }
873            }
874
875            taskStackBuilder.addParentStack(ComposeMessageActivity.class);
876            taskStackBuilder.addNextIntent(mostRecentNotification.mClickIntent);
877        }
878        // Always have to set the small icon or the notification is ignored
879        noti.setSmallIcon(R.drawable.stat_notify_sms);
880
881        NotificationManager nm = (NotificationManager)
882                context.getSystemService(Context.NOTIFICATION_SERVICE);
883
884        // Update the notification.
885        noti.setContentTitle(title)
886            .setContentIntent(
887                    taskStackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT))
888            .addKind(Notification.KIND_MESSAGE)
889            .setPriority(Notification.PRIORITY_DEFAULT);     // TODO: set based on contact coming
890                                                             // from a favorite.
891
892        int defaults = 0;
893
894        if (isNew) {
895            SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
896
897            boolean vibrate = false;
898            if (sp.contains(MessagingPreferenceActivity.NOTIFICATION_VIBRATE)) {
899                // The most recent change to the vibrate preference is to store a boolean
900                // value in NOTIFICATION_VIBRATE. If prefs contain that preference, use that
901                // first.
902                vibrate = sp.getBoolean(MessagingPreferenceActivity.NOTIFICATION_VIBRATE,
903                        false);
904            } else if (sp.contains(MessagingPreferenceActivity.NOTIFICATION_VIBRATE_WHEN)) {
905                // This is to support the pre-JellyBean MR1.1 version of vibrate preferences
906                // when vibrate was a tri-state setting. As soon as the user opens the Messaging
907                // app's settings, it will migrate this setting from NOTIFICATION_VIBRATE_WHEN
908                // to the boolean value stored in NOTIFICATION_VIBRATE.
909                String vibrateWhen =
910                        sp.getString(MessagingPreferenceActivity.NOTIFICATION_VIBRATE_WHEN, null);
911                vibrate = "always".equals(vibrateWhen);
912            }
913            if (vibrate) {
914                defaults |= Notification.DEFAULT_VIBRATE;
915            }
916
917            String ringtoneStr = sp.getString(MessagingPreferenceActivity.NOTIFICATION_RINGTONE,
918                    null);
919            noti.setSound(TextUtils.isEmpty(ringtoneStr) ? null : Uri.parse(ringtoneStr));
920            Log.d(TAG, "updateNotification: new message, adding sound to the notification");
921        }
922
923        defaults |= Notification.DEFAULT_LIGHTS;
924
925        noti.setDefaults(defaults);
926
927        // set up delete intent
928        noti.setDeleteIntent(PendingIntent.getBroadcast(context, 0,
929                sNotificationOnDeleteIntent, 0));
930
931        final Notification notification;
932
933        if (messageCount == 1) {
934            // We've got a single message
935
936            // This sets the text for the collapsed form:
937            noti.setContentText(mostRecentNotification.formatBigMessage(context));
938
939            if (mostRecentNotification.mAttachmentBitmap != null) {
940                // The message has a picture, show that
941
942                notification = new Notification.BigPictureStyle(noti)
943                    .bigPicture(mostRecentNotification.mAttachmentBitmap)
944                    // This sets the text for the expanded picture form:
945                    .setSummaryText(mostRecentNotification.formatPictureMessage(context))
946                    .build();
947            } else {
948                // Show a single notification -- big style with the text of the whole message
949                notification = new Notification.BigTextStyle(noti)
950                    .bigText(mostRecentNotification.formatBigMessage(context))
951                    .build();
952            }
953            if (DEBUG) {
954                Log.d(TAG, "updateNotification: single message notification");
955            }
956        } else {
957            // We've got multiple messages
958            if (uniqueThreadCount == 1) {
959                // We've got multiple messages for the same thread.
960                // Starting with the oldest new message, display the full text of each message.
961                // Begin a line for each subsequent message.
962                SpannableStringBuilder buf = new SpannableStringBuilder();
963                NotificationInfo infos[] =
964                        notificationSet.toArray(new NotificationInfo[messageCount]);
965                int len = infos.length;
966                for (int i = len - 1; i >= 0; i--) {
967                    NotificationInfo info = infos[i];
968
969                    buf.append(info.formatBigMessage(context));
970
971                    if (i != 0) {
972                        buf.append('\n');
973                    }
974                }
975
976                noti.setContentText(context.getString(R.string.message_count_notification,
977                        messageCount));
978
979                // Show a single notification -- big style with the text of all the messages
980                notification = new Notification.BigTextStyle(noti)
981                    .bigText(buf)
982                    // Forcibly show the last line, with the app's smallIcon in it, if we
983                    // kicked the smallIcon out with an avatar bitmap
984                    .setSummaryText((avatar == null) ? null : " ")
985                    .build();
986                if (DEBUG) {
987                    Log.d(TAG, "updateNotification: multi messages for single thread");
988                }
989            } else {
990                // Build a set of the most recent notification per threadId.
991                HashSet<Long> uniqueThreads = new HashSet<Long>(messageCount);
992                ArrayList<NotificationInfo> mostRecentNotifPerThread =
993                        new ArrayList<NotificationInfo>();
994                Iterator<NotificationInfo> notifications = notificationSet.iterator();
995                while (notifications.hasNext()) {
996                    NotificationInfo notificationInfo = notifications.next();
997                    if (!uniqueThreads.contains(notificationInfo.mThreadId)) {
998                        uniqueThreads.add(notificationInfo.mThreadId);
999                        mostRecentNotifPerThread.add(notificationInfo);
1000                    }
1001                }
1002                // When collapsed, show all the senders like this:
1003                //     Fred Flinstone, Barry Manilow, Pete...
1004                noti.setContentText(formatSenders(context, mostRecentNotifPerThread));
1005                Notification.InboxStyle inboxStyle = new Notification.InboxStyle(noti);
1006
1007                // We have to set the summary text to non-empty so the content text doesn't show
1008                // up when expanded.
1009                inboxStyle.setSummaryText(" ");
1010
1011                // At this point we've got multiple messages in multiple threads. We only
1012                // want to show the most recent message per thread, which are in
1013                // mostRecentNotifPerThread.
1014                int uniqueThreadMessageCount = mostRecentNotifPerThread.size();
1015                int maxMessages = Math.min(MAX_MESSAGES_TO_SHOW, uniqueThreadMessageCount);
1016
1017                for (int i = 0; i < maxMessages; i++) {
1018                    NotificationInfo info = mostRecentNotifPerThread.get(i);
1019                    inboxStyle.addLine(info.formatInboxMessage(context));
1020                }
1021                notification = inboxStyle.build();
1022                if (DEBUG) {
1023                    Log.d(TAG, "updateNotification: multi messages," +
1024                            " showing inboxStyle notification");
1025                }
1026            }
1027        }
1028
1029        nm.notify(NOTIFICATION_ID, notification);
1030    }
1031
1032    protected static CharSequence buildTickerMessage(
1033            Context context, String address, String subject, String body) {
1034        String displayAddress = Contact.get(address, true).getName();
1035
1036        StringBuilder buf = new StringBuilder(
1037                displayAddress == null
1038                ? ""
1039                : displayAddress.replace('\n', ' ').replace('\r', ' '));
1040        buf.append(':').append(' ');
1041
1042        int offset = buf.length();
1043        if (!TextUtils.isEmpty(subject)) {
1044            subject = subject.replace('\n', ' ').replace('\r', ' ');
1045            buf.append(subject);
1046            buf.append(' ');
1047        }
1048
1049        if (!TextUtils.isEmpty(body)) {
1050            body = body.replace('\n', ' ').replace('\r', ' ');
1051            buf.append(body);
1052        }
1053
1054        SpannableString spanText = new SpannableString(buf.toString());
1055        spanText.setSpan(new StyleSpan(Typeface.BOLD), 0, offset,
1056                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
1057
1058        return spanText;
1059    }
1060
1061    private static String getMmsSubject(String sub, int charset) {
1062        return TextUtils.isEmpty(sub) ? ""
1063                : new EncodedStringValue(charset, PduPersister.getBytes(sub)).getString();
1064    }
1065
1066    public static void notifyDownloadFailed(Context context, long threadId) {
1067        notifyFailed(context, true, threadId, false);
1068    }
1069
1070    public static void notifySendFailed(Context context) {
1071        notifyFailed(context, false, 0, false);
1072    }
1073
1074    public static void notifySendFailed(Context context, boolean noisy) {
1075        notifyFailed(context, false, 0, noisy);
1076    }
1077
1078    private static void notifyFailed(Context context, boolean isDownload, long threadId,
1079                                     boolean noisy) {
1080        // TODO factor out common code for creating notifications
1081        boolean enabled = MessagingPreferenceActivity.getNotificationEnabled(context);
1082        if (!enabled) {
1083            return;
1084        }
1085
1086        // Strategy:
1087        // a. If there is a single failure notification, tapping on the notification goes
1088        //    to the compose view.
1089        // b. If there are two failure it stays in the thread view. Selecting one undelivered
1090        //    thread will dismiss one undelivered notification but will still display the
1091        //    notification.If you select the 2nd undelivered one it will dismiss the notification.
1092
1093        long[] msgThreadId = {0, 1};    // Dummy initial values, just to initialize the memory
1094        int totalFailedCount = getUndeliveredMessageCount(context, msgThreadId);
1095        if (totalFailedCount == 0 && !isDownload) {
1096            return;
1097        }
1098        // The getUndeliveredMessageCount method puts a non-zero value in msgThreadId[1] if all
1099        // failures are from the same thread.
1100        // If isDownload is true, we're dealing with 1 specific failure; therefore "all failed" are
1101        // indeed in the same thread since there's only 1.
1102        boolean allFailedInSameThread = (msgThreadId[1] != 0) || isDownload;
1103
1104        Intent failedIntent;
1105        Notification notification = new Notification();
1106        String title;
1107        String description;
1108        if (totalFailedCount > 1) {
1109            description = context.getString(R.string.notification_failed_multiple,
1110                    Integer.toString(totalFailedCount));
1111            title = context.getString(R.string.notification_failed_multiple_title);
1112        } else {
1113            title = isDownload ?
1114                        context.getString(R.string.message_download_failed_title) :
1115                        context.getString(R.string.message_send_failed_title);
1116
1117            description = context.getString(R.string.message_failed_body);
1118        }
1119
1120        TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(context);
1121        if (allFailedInSameThread) {
1122            failedIntent = new Intent(context, ComposeMessageActivity.class);
1123            if (isDownload) {
1124                // When isDownload is true, the valid threadId is passed into this function.
1125                failedIntent.putExtra("failed_download_flag", true);
1126            } else {
1127                threadId = msgThreadId[0];
1128                failedIntent.putExtra("undelivered_flag", true);
1129            }
1130            failedIntent.putExtra("thread_id", threadId);
1131            taskStackBuilder.addParentStack(ComposeMessageActivity.class);
1132        } else {
1133            failedIntent = new Intent(context, ConversationList.class);
1134        }
1135        taskStackBuilder.addNextIntent(failedIntent);
1136
1137        notification.icon = R.drawable.stat_notify_sms_failed;
1138
1139        notification.tickerText = title;
1140
1141        notification.setLatestEventInfo(context, title, description,
1142                taskStackBuilder.getPendingIntent(0,  PendingIntent.FLAG_UPDATE_CURRENT));
1143
1144        if (noisy) {
1145            SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
1146            boolean vibrate = sp.getBoolean(MessagingPreferenceActivity.NOTIFICATION_VIBRATE,
1147                    false /* don't vibrate by default */);
1148            if (vibrate) {
1149                notification.defaults |= Notification.DEFAULT_VIBRATE;
1150            }
1151
1152            String ringtoneStr = sp.getString(MessagingPreferenceActivity.NOTIFICATION_RINGTONE,
1153                    null);
1154            notification.sound = TextUtils.isEmpty(ringtoneStr) ? null : Uri.parse(ringtoneStr);
1155        }
1156
1157        NotificationManager notificationMgr = (NotificationManager)
1158                context.getSystemService(Context.NOTIFICATION_SERVICE);
1159
1160        if (isDownload) {
1161            notificationMgr.notify(DOWNLOAD_FAILED_NOTIFICATION_ID, notification);
1162        } else {
1163            notificationMgr.notify(MESSAGE_FAILED_NOTIFICATION_ID, notification);
1164        }
1165    }
1166
1167    /**
1168     * Query the DB and return the number of undelivered messages (total for both SMS and MMS)
1169     * @param context The context
1170     * @param threadIdResult A container to put the result in, according to the following rules:
1171     *  threadIdResult[0] contains the thread id of the first message.
1172     *  threadIdResult[1] is nonzero if the thread ids of all the messages are the same.
1173     *  You can pass in null for threadIdResult.
1174     *  You can pass in a threadIdResult of size 1 to avoid the comparison of each thread id.
1175     */
1176    private static int getUndeliveredMessageCount(Context context, long[] threadIdResult) {
1177        Cursor undeliveredCursor = SqliteWrapper.query(context, context.getContentResolver(),
1178                UNDELIVERED_URI, MMS_THREAD_ID_PROJECTION, "read=0", null, null);
1179        if (undeliveredCursor == null) {
1180            return 0;
1181        }
1182        int count = undeliveredCursor.getCount();
1183        try {
1184            if (threadIdResult != null && undeliveredCursor.moveToFirst()) {
1185                threadIdResult[0] = undeliveredCursor.getLong(0);
1186
1187                if (threadIdResult.length >= 2) {
1188                    // Test to see if all the undelivered messages belong to the same thread.
1189                    long firstId = threadIdResult[0];
1190                    while (undeliveredCursor.moveToNext()) {
1191                        if (undeliveredCursor.getLong(0) != firstId) {
1192                            firstId = 0;
1193                            break;
1194                        }
1195                    }
1196                    threadIdResult[1] = firstId;    // non-zero if all ids are the same
1197                }
1198            }
1199        } finally {
1200            undeliveredCursor.close();
1201        }
1202        return count;
1203    }
1204
1205    public static void nonBlockingUpdateSendFailedNotification(final Context context) {
1206        new AsyncTask<Void, Void, Integer>() {
1207            protected Integer doInBackground(Void... none) {
1208                return getUndeliveredMessageCount(context, null);
1209            }
1210
1211            protected void onPostExecute(Integer result) {
1212                if (result < 1) {
1213                    cancelNotification(context, MESSAGE_FAILED_NOTIFICATION_ID);
1214                } else {
1215                    // rebuild and adjust the message count if necessary.
1216                    notifySendFailed(context);
1217                }
1218            }
1219        }.execute();
1220    }
1221
1222    /**
1223     *  If all the undelivered messages belong to "threadId", cancel the notification.
1224     */
1225    public static void updateSendFailedNotificationForThread(Context context, long threadId) {
1226        long[] msgThreadId = {0, 0};
1227        if (getUndeliveredMessageCount(context, msgThreadId) > 0
1228                && msgThreadId[0] == threadId
1229                && msgThreadId[1] != 0) {
1230            cancelNotification(context, MESSAGE_FAILED_NOTIFICATION_ID);
1231        }
1232    }
1233
1234    private static int getDownloadFailedMessageCount(Context context) {
1235        // Look for any messages in the MMS Inbox that are of the type
1236        // NOTIFICATION_IND (i.e. not already downloaded) and in the
1237        // permanent failure state.  If there are none, cancel any
1238        // failed download notification.
1239        Cursor c = SqliteWrapper.query(context, context.getContentResolver(),
1240                Mms.Inbox.CONTENT_URI, null,
1241                Mms.MESSAGE_TYPE + "=" +
1242                    String.valueOf(PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND) +
1243                " AND " + Mms.STATUS + "=" +
1244                    String.valueOf(DownloadManager.STATE_PERMANENT_FAILURE),
1245                null, null);
1246        if (c == null) {
1247            return 0;
1248        }
1249        int count = c.getCount();
1250        c.close();
1251        return count;
1252    }
1253
1254    public static void updateDownloadFailedNotification(Context context) {
1255        if (getDownloadFailedMessageCount(context) < 1) {
1256            cancelNotification(context, DOWNLOAD_FAILED_NOTIFICATION_ID);
1257        }
1258    }
1259
1260    public static boolean isFailedToDeliver(Intent intent) {
1261        return (intent != null) && intent.getBooleanExtra("undelivered_flag", false);
1262    }
1263
1264    public static boolean isFailedToDownload(Intent intent) {
1265        return (intent != null) && intent.getBooleanExtra("failed_download_flag", false);
1266    }
1267
1268    /**
1269     * Get the thread ID of the SMS message with the given URI
1270     * @param context The context
1271     * @param uri The URI of the SMS message
1272     * @return The thread ID, or THREAD_NONE if the URI contains no entries
1273     */
1274    public static long getSmsThreadId(Context context, Uri uri) {
1275        Cursor cursor = SqliteWrapper.query(
1276            context,
1277            context.getContentResolver(),
1278            uri,
1279            SMS_THREAD_ID_PROJECTION,
1280            null,
1281            null,
1282            null);
1283
1284        if (cursor == null) {
1285            if (DEBUG) {
1286                Log.d(TAG, "getSmsThreadId uri: " + uri + " NULL cursor! returning THREAD_NONE");
1287            }
1288            return THREAD_NONE;
1289        }
1290
1291        try {
1292            if (cursor.moveToFirst()) {
1293                int columnIndex = cursor.getColumnIndex(Sms.THREAD_ID);
1294                if (columnIndex < 0) {
1295                    if (DEBUG) {
1296                        Log.d(TAG, "getSmsThreadId uri: " + uri +
1297                                " Couldn't read row 0, col -1! returning THREAD_NONE");
1298                    }
1299                    return THREAD_NONE;
1300                }
1301                long threadId = cursor.getLong(columnIndex);
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                int columnIndex = cursor.getColumnIndex(Mms.THREAD_ID);
1345                if (columnIndex < 0) {
1346                    if (DEBUG) {
1347                        Log.d(TAG, "getThreadId uri: " + uri +
1348                                " Couldn't read row 0, col -1! returning THREAD_NONE");
1349                    }
1350                    return THREAD_NONE;
1351                }
1352                long threadId = cursor.getLong(columnIndex);
1353                if (DEBUG) {
1354                    Log.d(TAG, "getThreadId uri: " + uri +
1355                            " returning threadId: " + threadId);
1356                }
1357                return threadId;
1358            } else {
1359                if (DEBUG) {
1360                    Log.d(TAG, "getThreadId uri: " + uri +
1361                            " NULL cursor! returning THREAD_NONE");
1362                }
1363                return THREAD_NONE;
1364            }
1365        } finally {
1366            cursor.close();
1367        }
1368    }
1369}
1370