Home | History | Annotate | Download | only in transaction
      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 
     18 package com.android.mms.transaction;
     19 
     20 import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND;
     21 import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF;
     22 
     23 import java.util.ArrayList;
     24 import java.util.Comparator;
     25 import java.util.HashSet;
     26 import java.util.Iterator;
     27 import java.util.Set;
     28 import java.util.SortedSet;
     29 import java.util.TreeSet;
     30 
     31 import android.app.Notification;
     32 import android.app.NotificationManager;
     33 import android.app.PendingIntent;
     34 import android.app.TaskStackBuilder;
     35 import android.content.BroadcastReceiver;
     36 import android.content.ContentResolver;
     37 import android.content.Context;
     38 import android.content.Intent;
     39 import android.content.IntentFilter;
     40 import android.content.SharedPreferences;
     41 import android.content.res.Resources;
     42 import android.database.Cursor;
     43 import android.database.sqlite.SqliteWrapper;
     44 import android.graphics.Bitmap;
     45 import android.graphics.Typeface;
     46 import android.graphics.drawable.BitmapDrawable;
     47 import android.media.AudioManager;
     48 import android.net.Uri;
     49 import android.os.AsyncTask;
     50 import android.os.Handler;
     51 import android.preference.PreferenceManager;
     52 import android.provider.Telephony.Mms;
     53 import android.provider.Telephony.Sms;
     54 import android.text.Spannable;
     55 import android.text.SpannableString;
     56 import android.text.SpannableStringBuilder;
     57 import android.text.TextUtils;
     58 import android.text.style.StyleSpan;
     59 import android.text.style.TextAppearanceSpan;
     60 import android.util.Log;
     61 import android.widget.Toast;
     62 
     63 import com.android.mms.LogTag;
     64 import com.android.mms.R;
     65 import com.android.mms.data.Contact;
     66 import com.android.mms.data.Conversation;
     67 import com.android.mms.data.WorkingMessage;
     68 import com.android.mms.model.SlideModel;
     69 import com.android.mms.model.SlideshowModel;
     70 import com.android.mms.ui.ComposeMessageActivity;
     71 import com.android.mms.ui.ConversationList;
     72 import com.android.mms.ui.MessageUtils;
     73 import com.android.mms.ui.MessagingPreferenceActivity;
     74 import com.android.mms.util.AddressUtils;
     75 import com.android.mms.util.DownloadManager;
     76 import com.android.mms.widget.MmsWidgetProvider;
     77 import com.google.android.mms.MmsException;
     78 import com.google.android.mms.pdu.EncodedStringValue;
     79 import com.google.android.mms.pdu.GenericPdu;
     80 import com.google.android.mms.pdu.MultimediaMessagePdu;
     81 import com.google.android.mms.pdu.PduHeaders;
     82 import 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  */
     89 public 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