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