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