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