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