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