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