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