• 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);
334 
335         if (cursor == null)
336             return null;
337 
338         try {
339             if (!cursor.moveToLast())
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, 1};    // Dummy initial values, just to initialize the memory
608         int totalFailedCount = getUndeliveredMessageCount(context, msgThreadId);
609         if (totalFailedCount == 0 && !isDownload) {
610             return;
611         }
612         // The getUndeliveredMessageCount method puts a non-zero value in msgThreadId[1] if all
613         // failures are from the same thread.
614         // If isDownload is true, we're dealing with 1 specific failure; therefore "all failed" are
615         // indeed in the same thread since there's only 1.
616         boolean allFailedInSameThread = (msgThreadId[1] != 0) || isDownload;
617 
618         Intent failedIntent;
619         Notification notification = new Notification();
620         String title;
621         String description;
622         if (totalFailedCount > 1) {
623             description = context.getString(R.string.notification_failed_multiple,
624                     Integer.toString(totalFailedCount));
625             title = context.getString(R.string.notification_failed_multiple_title);
626         } else {
627             title = isDownload ?
628                         context.getString(R.string.message_download_failed_title) :
629                         context.getString(R.string.message_send_failed_title);
630 
631             description = context.getString(R.string.message_failed_body);
632         }
633 
634         if (allFailedInSameThread) {
635             failedIntent = new Intent(context, ComposeMessageActivity.class);
636             if (isDownload) {
637                 // When isDownload is true, the valid threadId is passed into this function.
638                 failedIntent.putExtra("failed_download_flag", true);
639             } else {
640                 threadId = msgThreadId[0];
641                 failedIntent.putExtra("undelivered_flag", true);
642             }
643             failedIntent.putExtra("thread_id", threadId);
644         } else {
645             failedIntent = new Intent(context, ConversationList.class);
646         }
647 
648         failedIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
649         PendingIntent pendingIntent = PendingIntent.getActivity(
650                 context, 0, failedIntent, PendingIntent.FLAG_UPDATE_CURRENT);
651 
652         notification.icon = R.drawable.stat_notify_sms_failed;
653 
654         notification.tickerText = title;
655 
656         notification.setLatestEventInfo(context, title, description, pendingIntent);
657 
658         if (noisy) {
659             boolean vibrate = sp.getBoolean(MessagingPreferenceActivity.NOTIFICATION_VIBRATE,
660                     false /* don't vibrate by default */);
661             if (vibrate) {
662                 notification.defaults |= Notification.DEFAULT_VIBRATE;
663             }
664 
665             String ringtoneStr = sp.getString(MessagingPreferenceActivity.NOTIFICATION_RINGTONE,
666                     null);
667             notification.sound = TextUtils.isEmpty(ringtoneStr) ? null : Uri.parse(ringtoneStr);
668         }
669 
670         if (isDownload) {
671             nm.notify(DOWNLOAD_FAILED_NOTIFICATION_ID, notification);
672         } else {
673             nm.notify(MESSAGE_FAILED_NOTIFICATION_ID, notification);
674         }
675     }
676 
677     /**
678      * Query the DB and return the number of undelivered messages (total for both SMS and MMS)
679      * @param context The context
680      * @param threadIdResult A container to put the result in, according to the following rules:
681      *  threadIdResult[0] contains the thread id of the first message.
682      *  threadIdResult[1] is nonzero if the thread ids of all the messages are the same.
683      *  You can pass in null for threadIdResult.
684      *  You can pass in a threadIdResult of size 1 to avoid the comparison of each thread id.
685      */
getUndeliveredMessageCount(Context context, long[] threadIdResult)686     private static int getUndeliveredMessageCount(Context context, long[] threadIdResult) {
687         Cursor undeliveredCursor = SqliteWrapper.query(context, context.getContentResolver(),
688                 UNDELIVERED_URI, new String[] { Mms.THREAD_ID }, "read=0", null, null);
689         if (undeliveredCursor == null) {
690             return 0;
691         }
692         int count = undeliveredCursor.getCount();
693         try {
694             if (threadIdResult != null && undeliveredCursor.moveToFirst()) {
695                 threadIdResult[0] = undeliveredCursor.getLong(0);
696 
697                 if (threadIdResult.length >= 2) {
698                     // Test to see if all the undelivered messages belong to the same thread.
699                     long firstId = threadIdResult[0];
700                     while (undeliveredCursor.moveToNext()) {
701                         if (undeliveredCursor.getLong(0) != firstId) {
702                             firstId = 0;
703                             break;
704                         }
705                     }
706                     threadIdResult[1] = firstId;    // non-zero if all ids are the same
707                 }
708             }
709         } finally {
710             undeliveredCursor.close();
711         }
712         return count;
713     }
714 
updateSendFailedNotification(Context context)715     public static void updateSendFailedNotification(Context context) {
716         if (getUndeliveredMessageCount(context, null) < 1) {
717             cancelNotification(context, MESSAGE_FAILED_NOTIFICATION_ID);
718         } else {
719             notifySendFailed(context);      // rebuild and adjust the message count if necessary.
720         }
721     }
722 
723     /**
724      *  If all the undelivered messages belong to "threadId", cancel the notification.
725      */
updateSendFailedNotificationForThread(Context context, long threadId)726     public static void updateSendFailedNotificationForThread(Context context, long threadId) {
727         long[] msgThreadId = {0, 0};
728         if (getUndeliveredMessageCount(context, msgThreadId) > 0
729                 && msgThreadId[0] == threadId
730                 && msgThreadId[1] != 0) {
731             cancelNotification(context, MESSAGE_FAILED_NOTIFICATION_ID);
732         }
733     }
734 
getDownloadFailedMessageCount(Context context)735     private static int getDownloadFailedMessageCount(Context context) {
736         // Look for any messages in the MMS Inbox that are of the type
737         // NOTIFICATION_IND (i.e. not already downloaded) and in the
738         // permanent failure state.  If there are none, cancel any
739         // failed download notification.
740         Cursor c = SqliteWrapper.query(context, context.getContentResolver(),
741                 Mms.Inbox.CONTENT_URI, null,
742                 Mms.MESSAGE_TYPE + "=" +
743                     String.valueOf(PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND) +
744                 " AND " + Mms.STATUS + "=" +
745                     String.valueOf(DownloadManager.STATE_PERMANENT_FAILURE),
746                 null, null);
747         if (c == null) {
748             return 0;
749         }
750         int count = c.getCount();
751         c.close();
752         return count;
753     }
754 
updateDownloadFailedNotification(Context context)755     public static void updateDownloadFailedNotification(Context context) {
756         if (getDownloadFailedMessageCount(context) < 1) {
757             cancelNotification(context, DOWNLOAD_FAILED_NOTIFICATION_ID);
758         }
759     }
760 
isFailedToDeliver(Intent intent)761     public static boolean isFailedToDeliver(Intent intent) {
762         return (intent != null) && intent.getBooleanExtra("undelivered_flag", false);
763     }
764 
isFailedToDownload(Intent intent)765     public static boolean isFailedToDownload(Intent intent) {
766         return (intent != null) && intent.getBooleanExtra("failed_download_flag", false);
767     }
768 
769 
770 }
771