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