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.ui.ComposeMessageActivity;
     28 import com.android.mms.ui.ConversationList;
     29 import com.android.mms.ui.MessagingPreferenceActivity;
     30 import com.android.mms.util.AddressUtils;
     31 import com.android.mms.util.DownloadManager;
     32 
     33 import com.google.android.mms.pdu.EncodedStringValue;
     34 import com.google.android.mms.pdu.PduHeaders;
     35 import com.google.android.mms.pdu.PduPersister;
     36 import android.database.sqlite.SqliteWrapper;
     37 
     38 import android.app.Notification;
     39 import android.app.NotificationManager;
     40 import android.app.PendingIntent;
     41 import android.content.ContentResolver;
     42 import android.content.Context;
     43 import android.content.Intent;
     44 import android.content.SharedPreferences;
     45 import android.content.BroadcastReceiver;
     46 import android.content.IntentFilter;
     47 import android.database.Cursor;
     48 import android.graphics.Typeface;
     49 import android.media.AudioManager;
     50 import android.net.Uri;
     51 import android.os.Handler;
     52 import android.preference.PreferenceManager;
     53 import android.provider.Telephony.Mms;
     54 import android.provider.Telephony.Sms;
     55 import android.text.Spannable;
     56 import android.text.SpannableString;
     57 import android.text.TextUtils;
     58 import android.text.style.StyleSpan;
     59 import android.util.Log;
     60 import android.widget.Toast;
     61 
     62 import java.util.Comparator;
     63 import java.util.HashSet;
     64 import java.util.Set;
     65 import java.util.SortedSet;
     66 import java.util.TreeSet;
     67 
     68 /**
     69  * This class is used to update the notification indicator. It will check whether
     70  * there are unread messages. If yes, it would show the notification indicator,
     71  * otherwise, hide the indicator.
     72  */
     73 public class MessagingNotification {
     74     private static final String TAG = LogTag.APP;
     75 
     76     private static final int NOTIFICATION_ID = 123;
     77     public static final int MESSAGE_FAILED_NOTIFICATION_ID = 789;
     78     public static final int DOWNLOAD_FAILED_NOTIFICATION_ID = 531;
     79 
     80     // This must be consistent with the column constants below.
     81     private static final String[] MMS_STATUS_PROJECTION = new String[] {
     82         Mms.THREAD_ID, Mms.DATE, Mms._ID, Mms.SUBJECT, Mms.SUBJECT_CHARSET };
     83 
     84     // This must be consistent with the column constants below.
     85     private static final String[] SMS_STATUS_PROJECTION = new String[] {
     86         Sms.THREAD_ID, Sms.DATE, Sms.ADDRESS, Sms.SUBJECT, Sms.BODY };
     87 
     88     // These must be consistent with MMS_STATUS_PROJECTION and
     89     // SMS_STATUS_PROJECTION.
     90     private static final int COLUMN_THREAD_ID   = 0;
     91     private static final int COLUMN_DATE        = 1;
     92     private static final int COLUMN_MMS_ID      = 2;
     93     private static final int COLUMN_SMS_ADDRESS = 2;
     94     private static final int COLUMN_SUBJECT     = 3;
     95     private static final int COLUMN_SUBJECT_CS  = 4;
     96     private static final int COLUMN_SMS_BODY    = 4;
     97 
     98     private static final String NEW_INCOMING_SM_CONSTRAINT =
     99             "(" + Sms.TYPE + " = " + Sms.MESSAGE_TYPE_INBOX
    100             + " AND " + Sms.SEEN + " = 0)";
    101 
    102     private static final String NEW_DELIVERY_SM_CONSTRAINT =
    103         "(" + Sms.TYPE + " = " + Sms.MESSAGE_TYPE_SENT
    104         + " AND " + Sms.STATUS + " = "+ Sms.STATUS_COMPLETE +")";
    105 
    106     private static final String NEW_INCOMING_MM_CONSTRAINT =
    107             "(" + Mms.MESSAGE_BOX + "=" + Mms.MESSAGE_BOX_INBOX
    108             + " AND " + Mms.SEEN + "=0"
    109             + " AND (" + Mms.MESSAGE_TYPE + "=" + MESSAGE_TYPE_NOTIFICATION_IND
    110             + " OR " + Mms.MESSAGE_TYPE + "=" + MESSAGE_TYPE_RETRIEVE_CONF + "))";
    111 
    112     private static final MmsSmsNotificationInfoComparator INFO_COMPARATOR =
    113             new MmsSmsNotificationInfoComparator();
    114 
    115     private static final Uri UNDELIVERED_URI = Uri.parse("content://mms-sms/undelivered");
    116 
    117 
    118     private final static String NOTIFICATION_DELETED_ACTION =
    119             "com.android.mms.NOTIFICATION_DELETED_ACTION";
    120 
    121     public static class OnDeletedReceiver extends BroadcastReceiver {
    122         public void onReceive(Context context, Intent intent) {
    123             if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
    124                 Log.d(TAG, "[MessagingNotification] clear notification: mark all msgs seen");
    125             }
    126 
    127             Conversation.markAllConversationsAsSeen(context);
    128         }
    129     };
    130     private static OnDeletedReceiver sNotificationDeletedReceiver = new OnDeletedReceiver();
    131     private static Intent sNotificationOnDeleteIntent;
    132     private static Handler mToastHandler = new Handler();
    133 
    134     private MessagingNotification() {
    135     }
    136 
    137     public static void init(Context context) {
    138         // set up the intent filter for notification deleted action
    139         IntentFilter intentFilter = new IntentFilter();
    140         intentFilter.addAction(NOTIFICATION_DELETED_ACTION);
    141         context.registerReceiver(sNotificationDeletedReceiver, intentFilter);
    142 
    143         // initialize the notification deleted action
    144         sNotificationOnDeleteIntent = new Intent(NOTIFICATION_DELETED_ACTION);
    145     }
    146 
    147     /**
    148      * Checks to see if there are any "unseen" messages or delivery
    149      * reports.  Shows the most recent notification if there is one.
    150      * Does its work and query in a worker thread.
    151      *
    152      * @param context the context to use
    153      */
    154     public static void nonBlockingUpdateNewMessageIndicator(final Context context,
    155             final boolean isNew,
    156             final boolean isStatusMessage) {
    157         new Thread(new Runnable() {
    158             public void run() {
    159                 blockingUpdateNewMessageIndicator(context, isNew, isStatusMessage);
    160             }
    161         }).start();
    162     }
    163 
    164     /**
    165      * Checks to see if there are any "unseen" messages or delivery
    166      * reports.  Shows the most recent notification if there is one.
    167      *
    168      * @param context the context to use
    169      * @param isNew if notify a new message comes, it should be true, otherwise, false.
    170      */
    171     public static void blockingUpdateNewMessageIndicator(Context context, boolean isNew,
    172             boolean isStatusMessage) {
    173         SortedSet<MmsSmsNotificationInfo> accumulator =
    174                 new TreeSet<MmsSmsNotificationInfo>(INFO_COMPARATOR);
    175         MmsSmsDeliveryInfo delivery = null;
    176         Set<Long> threads = new HashSet<Long>(4);
    177 
    178         int count = 0;
    179         count += accumulateNotificationInfo(
    180                 accumulator, getMmsNewMessageNotificationInfo(context, threads));
    181         count += accumulateNotificationInfo(
    182                 accumulator, getSmsNewMessageNotificationInfo(context, threads));
    183 
    184         cancelNotification(context, NOTIFICATION_ID);
    185         if (!accumulator.isEmpty()) {
    186             if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
    187                 Log.d(TAG, "blockingUpdateNewMessageIndicator: count=" + count +
    188                         ", isNew=" + isNew);
    189             }
    190             accumulator.first().deliver(context, isNew, count, threads.size());
    191         }
    192 
    193         // And deals with delivery reports (which use Toasts). It's safe to call in a worker
    194         // thread because the toast will eventually get posted to a handler.
    195         delivery = getSmsNewDeliveryInfo(context);
    196         if (delivery != null) {
    197             delivery.deliver(context, isStatusMessage);
    198         }
    199     }
    200 
    201     /**
    202      * Updates all pending notifications, clearing or updating them as
    203      * necessary.
    204      */
    205     public static void blockingUpdateAllNotifications(final Context context) {
    206         nonBlockingUpdateNewMessageIndicator(context, false, false);
    207         updateSendFailedNotification(context);
    208         updateDownloadFailedNotification(context);
    209     }
    210 
    211     private static final int accumulateNotificationInfo(
    212             SortedSet set, MmsSmsNotificationInfo info) {
    213         if (info != null) {
    214             set.add(info);
    215 
    216             return info.mCount;
    217         }
    218 
    219         return 0;
    220     }
    221 
    222     private static final class MmsSmsDeliveryInfo {
    223         public CharSequence mTicker;
    224         public long mTimeMillis;
    225 
    226         public MmsSmsDeliveryInfo(CharSequence ticker, long timeMillis) {
    227             mTicker = ticker;
    228             mTimeMillis = timeMillis;
    229         }
    230 
    231         public void deliver(Context context, boolean isStatusMessage) {
    232             updateDeliveryNotification(
    233                     context, isStatusMessage, mTicker, mTimeMillis);
    234         }
    235     }
    236 
    237     private static final class MmsSmsNotificationInfo {
    238         public Intent mClickIntent;
    239         public String mDescription;
    240         public int mIconResourceId;
    241         public CharSequence mTicker;
    242         public long mTimeMillis;
    243         public String mTitle;
    244         public int mCount;
    245 
    246         public MmsSmsNotificationInfo(
    247                 Intent clickIntent, String description, int iconResourceId,
    248                 CharSequence ticker, long timeMillis, String title, int count) {
    249             mClickIntent = clickIntent;
    250             mDescription = description;
    251             mIconResourceId = iconResourceId;
    252             mTicker = ticker;
    253             mTimeMillis = timeMillis;
    254             mTitle = title;
    255             mCount = count;
    256         }
    257 
    258         public void deliver(Context context, boolean isNew, int count, int uniqueThreads) {
    259             updateNotification(
    260                     context, mClickIntent, mDescription, mIconResourceId, isNew,
    261                     (isNew? mTicker : null), // only display the ticker if the message is new
    262                     mTimeMillis, mTitle, count, uniqueThreads);
    263         }
    264 
    265         public long getTime() {
    266             return mTimeMillis;
    267         }
    268     }
    269 
    270     private static final class MmsSmsNotificationInfoComparator
    271             implements Comparator<MmsSmsNotificationInfo> {
    272         public int compare(
    273                 MmsSmsNotificationInfo info1, MmsSmsNotificationInfo info2) {
    274             return Long.signum(info2.getTime() - info1.getTime());
    275         }
    276     }
    277 
    278     private static final MmsSmsNotificationInfo getMmsNewMessageNotificationInfo(
    279             Context context, Set<Long> threads) {
    280         ContentResolver resolver = context.getContentResolver();
    281 
    282         // This query looks like this when logged:
    283         // I/Database(  147): elapsedTime4Sql|/data/data/com.android.providers.telephony/databases/
    284         // mmssms.db|0.362 ms|SELECT thread_id, date, _id, sub, sub_cs FROM pdu WHERE ((msg_box=1
    285         // AND seen=0 AND (m_type=130 OR m_type=132))) ORDER BY date desc
    286 
    287         Cursor cursor = SqliteWrapper.query(context, resolver, Mms.CONTENT_URI,
    288                             MMS_STATUS_PROJECTION, NEW_INCOMING_MM_CONSTRAINT,
    289                             null, Mms.DATE + " desc");
    290 
    291         if (cursor == null) {
    292             return null;
    293         }
    294 
    295         try {
    296             if (!cursor.moveToFirst()) {
    297                 return null;
    298             }
    299             long msgId = cursor.getLong(COLUMN_MMS_ID);
    300             Uri msgUri = Mms.CONTENT_URI.buildUpon().appendPath(
    301                     Long.toString(msgId)).build();
    302             String address = AddressUtils.getFrom(context, msgUri);
    303             String subject = getMmsSubject(
    304                     cursor.getString(COLUMN_SUBJECT), cursor.getInt(COLUMN_SUBJECT_CS));
    305             long threadId = cursor.getLong(COLUMN_THREAD_ID);
    306             long timeMillis = cursor.getLong(COLUMN_DATE) * 1000;
    307 
    308             if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
    309                 Log.d(TAG, "getMmsNewMessageNotificationInfo: count=" + cursor.getCount() +
    310                         ", first addr = " + address + ", thread_id=" + threadId);
    311             }
    312 
    313             MmsSmsNotificationInfo info = getNewMessageNotificationInfo(
    314                     address, subject, context,
    315                     R.drawable.stat_notify_mms, null, threadId,
    316                     timeMillis, cursor.getCount());
    317 
    318             threads.add(threadId);
    319             while (cursor.moveToNext()) {
    320                 threads.add(cursor.getLong(COLUMN_THREAD_ID));
    321             }
    322 
    323             return info;
    324         } finally {
    325             cursor.close();
    326         }
    327     }
    328 
    329     private static final MmsSmsDeliveryInfo getSmsNewDeliveryInfo(Context context) {
    330         ContentResolver resolver = context.getContentResolver();
    331         Cursor cursor = SqliteWrapper.query(context, resolver, Sms.CONTENT_URI,
    332                     SMS_STATUS_PROJECTION, NEW_DELIVERY_SM_CONSTRAINT,
    333                     null, Sms.DATE + " desc");
    334 
    335         if (cursor == null)
    336             return null;
    337 
    338         try {
    339             if (!cursor.moveToFirst())
    340             return null;
    341 
    342             String address = cursor.getString(COLUMN_SMS_ADDRESS);
    343             long timeMillis = 3000;
    344 
    345             return new MmsSmsDeliveryInfo(String.format(
    346                 context.getString(R.string.delivery_toast_body), address),
    347                 timeMillis);
    348 
    349         } finally {
    350             cursor.close();
    351         }
    352     }
    353 
    354     private static final MmsSmsNotificationInfo getSmsNewMessageNotificationInfo(
    355             Context context, Set<Long> threads) {
    356         ContentResolver resolver = context.getContentResolver();
    357         Cursor cursor = SqliteWrapper.query(context, resolver, Sms.CONTENT_URI,
    358                             SMS_STATUS_PROJECTION, NEW_INCOMING_SM_CONSTRAINT,
    359                             null, Sms.DATE + " desc");
    360 
    361         if (cursor == null) {
    362             return null;
    363         }
    364 
    365         try {
    366             if (!cursor.moveToFirst()) {
    367                 return null;
    368             }
    369 
    370             String address = cursor.getString(COLUMN_SMS_ADDRESS);
    371             String body = cursor.getString(COLUMN_SMS_BODY);
    372             long threadId = cursor.getLong(COLUMN_THREAD_ID);
    373             long timeMillis = cursor.getLong(COLUMN_DATE);
    374 
    375             //if (Log.isLoggable(LogTag.APP, Log.VERBOSE))
    376             {
    377                 Log.d(TAG, "getSmsNewMessageNotificationInfo: count=" + cursor.getCount() +
    378                         ", first addr=" + address + ", thread_id=" + threadId);
    379             }
    380 
    381             MmsSmsNotificationInfo info = getNewMessageNotificationInfo(
    382                     address, body, context, R.drawable.stat_notify_sms,
    383                     null, threadId, timeMillis, cursor.getCount());
    384 
    385             threads.add(threadId);
    386             while (cursor.moveToNext()) {
    387                 threads.add(cursor.getLong(COLUMN_THREAD_ID));
    388             }
    389 
    390             return info;
    391         } finally {
    392             cursor.close();
    393         }
    394     }
    395 
    396     private static final MmsSmsNotificationInfo getNewMessageNotificationInfo(
    397             String address,
    398             String body,
    399             Context context,
    400             int iconResourceId,
    401             String subject,
    402             long threadId,
    403             long timeMillis,
    404             int count) {
    405         Intent clickIntent = ComposeMessageActivity.createIntent(context, threadId);
    406         clickIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
    407                 | Intent.FLAG_ACTIVITY_SINGLE_TOP
    408                 | Intent.FLAG_ACTIVITY_CLEAR_TOP);
    409 
    410         String senderInfo = buildTickerMessage(
    411                 context, address, null, null).toString();
    412         String senderInfoName = senderInfo.substring(
    413                 0, senderInfo.length() - 2);
    414         CharSequence ticker = buildTickerMessage(
    415                 context, address, subject, body);
    416 
    417         return new MmsSmsNotificationInfo(
    418                 clickIntent, body, iconResourceId, ticker, timeMillis,
    419                 senderInfoName, count);
    420     }
    421 
    422     public static void cancelNotification(Context context, int notificationId) {
    423         NotificationManager nm = (NotificationManager) context.getSystemService(
    424                 Context.NOTIFICATION_SERVICE);
    425 
    426         nm.cancel(notificationId);
    427     }
    428 
    429     private static void updateDeliveryNotification(final Context context,
    430                                                    boolean isStatusMessage,
    431                                                    final CharSequence message,
    432                                                    final long timeMillis) {
    433         if (!isStatusMessage) {
    434             return;
    435         }
    436 
    437         SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
    438 
    439         if (!sp.getBoolean(MessagingPreferenceActivity.NOTIFICATION_ENABLED, true)) {
    440             return;
    441         }
    442 
    443         mToastHandler.post(new Runnable() {
    444             public void run() {
    445                 Toast.makeText(context, message, (int)timeMillis).show();
    446             }
    447         });
    448     }
    449 
    450     private static void updateNotification(
    451             Context context,
    452             Intent clickIntent,
    453             String description,
    454             int iconRes,
    455             boolean isNew,
    456             CharSequence ticker,
    457             long timeMillis,
    458             String title,
    459             int messageCount,
    460             int uniqueThreadCount) {
    461         SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
    462 
    463         if (!sp.getBoolean(MessagingPreferenceActivity.NOTIFICATION_ENABLED, true)) {
    464             return;
    465         }
    466 
    467         Notification notification = new Notification(iconRes, ticker, timeMillis);
    468 
    469         // If we have more than one unique thread, change the title (which would
    470         // normally be the contact who sent the message) to a generic one that
    471         // makes sense for multiple senders, and change the Intent to take the
    472         // user to the conversation list instead of the specific thread.
    473         if (uniqueThreadCount > 1) {
    474             title = context.getString(R.string.notification_multiple_title);
    475             clickIntent = new Intent(Intent.ACTION_MAIN);
    476 
    477             clickIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
    478                     | Intent.FLAG_ACTIVITY_SINGLE_TOP
    479                     | Intent.FLAG_ACTIVITY_CLEAR_TOP);
    480 
    481             clickIntent.setType("vnd.android-dir/mms-sms");
    482         }
    483 
    484         // If there is more than one message, change the description (which
    485         // would normally be a snippet of the individual message text) to
    486         // a string indicating how many "unseen" messages there are.
    487         if (messageCount > 1) {
    488             description = context.getString(R.string.notification_multiple,
    489                     Integer.toString(messageCount));
    490         }
    491 
    492         // Make a startActivity() PendingIntent for the notification.
    493         PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, clickIntent,
    494                 PendingIntent.FLAG_UPDATE_CURRENT);
    495 
    496         // Update the notification.
    497         notification.setLatestEventInfo(context, title, description, pendingIntent);
    498 
    499         if (isNew) {
    500             String vibrateWhen;
    501             if (sp.contains(MessagingPreferenceActivity.NOTIFICATION_VIBRATE_WHEN)) {
    502                 vibrateWhen =
    503                     sp.getString(MessagingPreferenceActivity.NOTIFICATION_VIBRATE_WHEN, null);
    504             } else if (sp.contains(MessagingPreferenceActivity.NOTIFICATION_VIBRATE)) {
    505                 vibrateWhen = sp.getBoolean(MessagingPreferenceActivity.NOTIFICATION_VIBRATE, false) ?
    506                     context.getString(R.string.prefDefault_vibrate_true) :
    507                     context.getString(R.string.prefDefault_vibrate_false);
    508             } else {
    509                 vibrateWhen = context.getString(R.string.prefDefault_vibrateWhen);
    510             }
    511 
    512             boolean vibrateAlways = vibrateWhen.equals("always");
    513             boolean vibrateSilent = vibrateWhen.equals("silent");
    514             AudioManager audioManager =
    515                 (AudioManager)context.getSystemService(Context.AUDIO_SERVICE);
    516             boolean nowSilent =
    517                 audioManager.getRingerMode() == AudioManager.RINGER_MODE_VIBRATE;
    518 
    519             if (vibrateAlways || vibrateSilent && nowSilent) {
    520                 notification.defaults |= Notification.DEFAULT_VIBRATE;
    521             }
    522 
    523             String ringtoneStr = sp.getString(MessagingPreferenceActivity.NOTIFICATION_RINGTONE,
    524                     null);
    525             notification.sound = TextUtils.isEmpty(ringtoneStr) ? null : Uri.parse(ringtoneStr);
    526         }
    527 
    528         notification.flags |= Notification.FLAG_SHOW_LIGHTS;
    529         notification.defaults |= Notification.DEFAULT_LIGHTS;
    530 
    531         // set up delete intent
    532         notification.deleteIntent = PendingIntent.getBroadcast(context, 0,
    533                 sNotificationOnDeleteIntent, 0);
    534 
    535         NotificationManager nm = (NotificationManager)
    536             context.getSystemService(Context.NOTIFICATION_SERVICE);
    537 
    538         nm.notify(NOTIFICATION_ID, notification);
    539     }
    540 
    541     protected static CharSequence buildTickerMessage(
    542             Context context, String address, String subject, String body) {
    543         String displayAddress = Contact.get(address, true).getName();
    544 
    545         StringBuilder buf = new StringBuilder(
    546                 displayAddress == null
    547                 ? ""
    548                 : displayAddress.replace('\n', ' ').replace('\r', ' '));
    549         buf.append(':').append(' ');
    550 
    551         int offset = buf.length();
    552         if (!TextUtils.isEmpty(subject)) {
    553             subject = subject.replace('\n', ' ').replace('\r', ' ');
    554             buf.append(subject);
    555             buf.append(' ');
    556         }
    557 
    558         if (!TextUtils.isEmpty(body)) {
    559             body = body.replace('\n', ' ').replace('\r', ' ');
    560             buf.append(body);
    561         }
    562 
    563         SpannableString spanText = new SpannableString(buf.toString());
    564         spanText.setSpan(new StyleSpan(Typeface.BOLD), 0, offset,
    565                 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    566 
    567         return spanText;
    568     }
    569 
    570     private static String getMmsSubject(String sub, int charset) {
    571         return TextUtils.isEmpty(sub) ? ""
    572                 : new EncodedStringValue(charset, PduPersister.getBytes(sub)).getString();
    573     }
    574 
    575     public static void notifyDownloadFailed(Context context, long threadId) {
    576         notifyFailed(context, true, threadId, false);
    577     }
    578 
    579     public static void notifySendFailed(Context context) {
    580         notifyFailed(context, false, 0, false);
    581     }
    582 
    583     public static void notifySendFailed(Context context, boolean noisy) {
    584         notifyFailed(context, false, 0, noisy);
    585     }
    586 
    587     private static void notifyFailed(Context context, boolean isDownload, long threadId,
    588                                      boolean noisy) {
    589         // TODO factor out common code for creating notifications
    590         SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
    591 
    592         boolean enabled = sp.getBoolean(MessagingPreferenceActivity.NOTIFICATION_ENABLED, true);
    593         if (!enabled) {
    594             return;
    595         }
    596 
    597         NotificationManager nm = (NotificationManager)
    598                 context.getSystemService(Context.NOTIFICATION_SERVICE);
    599 
    600         // Strategy:
    601         // a. If there is a single failure notification, tapping on the notification goes
    602         //    to the compose view.
    603         // b. If there are two failure it stays in the thread view. Selecting one undelivered
    604         //    thread will dismiss one undelivered notification but will still display the
    605         //    notification.If you select the 2nd undelivered one it will dismiss the notification.
    606 
    607         long[] msgThreadId = {0};
    608         int totalFailedCount = getUndeliveredMessageCount(context, msgThreadId);
    609 
    610         Intent failedIntent;
    611         Notification notification = new Notification();
    612         String title;
    613         String description;
    614         if (totalFailedCount > 1) {
    615             description = context.getString(R.string.notification_failed_multiple,
    616                     Integer.toString(totalFailedCount));
    617             title = context.getString(R.string.notification_failed_multiple_title);
    618 
    619             failedIntent = new Intent(context, ConversationList.class);
    620         } else {
    621             title = isDownload ?
    622                         context.getString(R.string.message_download_failed_title) :
    623                         context.getString(R.string.message_send_failed_title);
    624 
    625             description = context.getString(R.string.message_failed_body);
    626             failedIntent = new Intent(context, ComposeMessageActivity.class);
    627             if (isDownload) {
    628                 // When isDownload is true, the valid threadId is passed into this function.
    629                 failedIntent.putExtra("failed_download_flag", true);
    630             } else {
    631                 threadId = (msgThreadId[0] != 0 ? msgThreadId[0] : 0);
    632                 failedIntent.putExtra("undelivered_flag", true);
    633             }
    634             failedIntent.putExtra("thread_id", threadId);
    635         }
    636 
    637         failedIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
    638         PendingIntent pendingIntent = PendingIntent.getActivity(
    639                 context, 0, failedIntent, PendingIntent.FLAG_UPDATE_CURRENT);
    640 
    641         notification.icon = R.drawable.stat_notify_sms_failed;
    642 
    643         notification.tickerText = title;
    644 
    645         notification.setLatestEventInfo(context, title, description, pendingIntent);
    646 
    647         if (noisy) {
    648             boolean vibrate = sp.getBoolean(MessagingPreferenceActivity.NOTIFICATION_VIBRATE,
    649                     false /* don't vibrate by default */);
    650             if (vibrate) {
    651                 notification.defaults |= Notification.DEFAULT_VIBRATE;
    652             }
    653 
    654             String ringtoneStr = sp.getString(MessagingPreferenceActivity.NOTIFICATION_RINGTONE,
    655                     null);
    656             notification.sound = TextUtils.isEmpty(ringtoneStr) ? null : Uri.parse(ringtoneStr);
    657         }
    658 
    659         if (isDownload) {
    660             nm.notify(DOWNLOAD_FAILED_NOTIFICATION_ID, notification);
    661         } else {
    662             nm.notify(MESSAGE_FAILED_NOTIFICATION_ID, notification);
    663         }
    664     }
    665 
    666     // threadIdResult[0] contains the thread id of the first message.
    667     // threadIdResult[1] is nonzero if the thread ids of all the messages are the same.
    668     // You can pass in null for threadIdResult.
    669     // You can pass in a threadIdResult of size 1 to avoid the comparison of each thread id.
    670     private static int getUndeliveredMessageCount(Context context, long[] threadIdResult) {
    671         Cursor undeliveredCursor = SqliteWrapper.query(context, context.getContentResolver(),
    672                 UNDELIVERED_URI, new String[] { Mms.THREAD_ID }, "read=0", null, null);
    673         if (undeliveredCursor == null) {
    674             return 0;
    675         }
    676         int count = undeliveredCursor.getCount();
    677         try {
    678             if (threadIdResult != null && undeliveredCursor.moveToFirst()) {
    679                 threadIdResult[0] = undeliveredCursor.getLong(0);
    680 
    681                 if (threadIdResult.length >= 2) {
    682                     // Test to see if all the undelivered messages belong to the same thread.
    683                     long firstId = threadIdResult[0];
    684                     while (undeliveredCursor.moveToNext()) {
    685                         if (undeliveredCursor.getLong(0) != firstId) {
    686                             firstId = 0;
    687                             break;
    688                         }
    689                     }
    690                     threadIdResult[1] = firstId;    // non-zero if all ids are the same
    691                 }
    692             }
    693         } finally {
    694             undeliveredCursor.close();
    695         }
    696         return count;
    697     }
    698 
    699     public static void updateSendFailedNotification(Context context) {
    700         if (getUndeliveredMessageCount(context, null) < 1) {
    701             cancelNotification(context, MESSAGE_FAILED_NOTIFICATION_ID);
    702         } else {
    703             notifySendFailed(context);      // rebuild and adjust the message count if necessary.
    704         }
    705     }
    706 
    707     /**
    708      *  If all the undelivered messages belong to "threadId", cancel the notification.
    709      */
    710     public static void updateSendFailedNotificationForThread(Context context, long threadId) {
    711         long[] msgThreadId = {0, 0};
    712         if (getUndeliveredMessageCount(context, msgThreadId) > 0
    713                 && msgThreadId[0] == threadId
    714                 && msgThreadId[1] != 0) {
    715             cancelNotification(context, MESSAGE_FAILED_NOTIFICATION_ID);
    716         }
    717     }
    718 
    719     private static int getDownloadFailedMessageCount(Context context) {
    720         // Look for any messages in the MMS Inbox that are of the type
    721         // NOTIFICATION_IND (i.e. not already downloaded) and in the
    722         // permanent failure state.  If there are none, cancel any
    723         // failed download notification.
    724         Cursor c = SqliteWrapper.query(context, context.getContentResolver(),
    725                 Mms.Inbox.CONTENT_URI, null,
    726                 Mms.MESSAGE_TYPE + "=" +
    727                     String.valueOf(PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND) +
    728                 " AND " + Mms.STATUS + "=" +
    729                     String.valueOf(DownloadManager.STATE_PERMANENT_FAILURE),
    730                 null, null);
    731         if (c == null) {
    732             return 0;
    733         }
    734         int count = c.getCount();
    735         c.close();
    736         return count;
    737     }
    738 
    739     public static void updateDownloadFailedNotification(Context context) {
    740         if (getDownloadFailedMessageCount(context) < 1) {
    741             cancelNotification(context, DOWNLOAD_FAILED_NOTIFICATION_ID);
    742         }
    743     }
    744 
    745     public static boolean isFailedToDeliver(Intent intent) {
    746         return (intent != null) && intent.getBooleanExtra("undelivered_flag", false);
    747     }
    748 
    749     public static boolean isFailedToDownload(Intent intent) {
    750         return (intent != null) && intent.getBooleanExtra("failed_download_flag", false);
    751     }
    752 
    753 
    754 }
    755