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
onReceive(Context context, Intent intent)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 
MessagingNotification()179     private MessagingNotification() {
180     }
181 
init(Context context)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      */
setCurrentlyDisplayedThreadId(long threadId)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      */
nonBlockingUpdateNewMessageIndicator(final Context context, final long newMsgThreadId, final boolean isStatusMessage)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      */
blockingUpdateNewMessageIndicator(Context context, long newMsgThreadId, boolean isStatusMessage)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      */
playInConversationNotificationSound(Context context)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      */
blockingUpdateAllNotifications(final Context context, long threadId)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 
MmsSmsDeliveryInfo(CharSequence ticker, long timeMillis)357         public MmsSmsDeliveryInfo(CharSequence ticker, long timeMillis) {
358             mTicker = ticker;
359             mTimeMillis = timeMillis;
360         }
361 
deliver(Context context, boolean isStatusMessage)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          */
NotificationInfo(boolean isSms, Intent clickIntent, String message, String subject, CharSequence ticker, long timeMillis, String title, Bitmap attachmentBitmap, Contact sender, int attachmentType, long threadId)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 
getTime()413         public long getTime() {
414             return mTimeMillis;
415         }
416 
417         // This is the message string used in bigText and bigPicture notifications.
formatBigMessage(Context context)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.
formatInboxMessage(Context context)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.
formatPictureMessage(Context context)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.
formatSenders(Context context, ArrayList<NotificationInfo> senders)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.
getAttachmentTypeString(Context context, int attachmentType)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
compare( NotificationInfo info1, NotificationInfo info2)566         public int compare(
567                 NotificationInfo info1, NotificationInfo info2) {
568             return Long.signum(info2.getTime() - info1.getTime());
569         }
570     }
571 
addMmsNotificationInfos( Context context, Set<Long> threads, SortedSet<NotificationInfo> notificationSet)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.
getAttachmentType(SlideshowModel slideshow)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 
dp2Pixels(int dip)681     private static final int dp2Pixels(int dip) {
682         return (int) (dip * sScreenDensity + 0.5f);
683     }
684 
getSmsNewDeliveryInfo(Context context)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 
addSmsNotificationInfos( Context context, Set<Long> threads, SortedSet<NotificationInfo> notificationSet)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 
getNewMessageNotificationInfo( Context context, boolean isSms, String address, String message, String subject, long threadId, long timeMillis, Bitmap attachmentBitmap, Contact contact, int attachmentType)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 
cancelNotification(Context context, int notificationId)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 
updateDeliveryNotification(final Context context, boolean isStatusMessage, final CharSequence message, final long timeMillis)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      */
updateNotification( Context context, boolean isNew, int uniqueThreadCount, SortedSet<NotificationInfo> notificationSet)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 
buildTickerMessage( Context context, String address, String subject, String body)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 
getMmsSubject(String sub, int charset)1096     private static String getMmsSubject(String sub, int charset) {
1097         return TextUtils.isEmpty(sub) ? ""
1098                 : new EncodedStringValue(charset, PduPersister.getBytes(sub)).getString();
1099     }
1100 
notifyDownloadFailed(Context context, long threadId)1101     public static void notifyDownloadFailed(Context context, long threadId) {
1102         notifyFailed(context, true, threadId, false);
1103     }
1104 
notifySendFailed(Context context)1105     public static void notifySendFailed(Context context) {
1106         notifyFailed(context, false, 0, false);
1107     }
1108 
notifySendFailed(Context context, boolean noisy)1109     public static void notifySendFailed(Context context, boolean noisy) {
1110         notifyFailed(context, false, 0, noisy);
1111     }
1112 
notifyFailed(Context context, boolean isDownload, long threadId, boolean noisy)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      */
getUndeliveredMessageCount(Context context, long[] threadIdResult)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 
nonBlockingUpdateSendFailedNotification(final Context context)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      */
updateSendFailedNotificationForThread(Context context, long threadId)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 
getDownloadFailedMessageCount(Context context)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 
updateDownloadFailedNotification(Context context)1289     public static void updateDownloadFailedNotification(Context context) {
1290         if (getDownloadFailedMessageCount(context) < 1) {
1291             cancelNotification(context, DOWNLOAD_FAILED_NOTIFICATION_ID);
1292         }
1293     }
1294 
isFailedToDeliver(Intent intent)1295     public static boolean isFailedToDeliver(Intent intent) {
1296         return (intent != null) && intent.getBooleanExtra("undelivered_flag", false);
1297     }
1298 
isFailedToDownload(Intent intent)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      */
getSmsThreadId(Context context, Uri uri)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      */
getThreadId(Context context, Uri uri)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