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