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