MessagingNotification.java revision 810fae756f4d2db00b35d7c9dfead252737abdac
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
18package com.android.mms.transaction;
19
20import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND;
21import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF;
22
23import com.android.mms.R;
24import com.android.mms.ui.ComposeMessageActivity;
25import com.android.mms.ui.ConversationList;
26import com.android.mms.ui.MessagingPreferenceActivity;
27import com.android.mms.util.AddressUtils;
28import com.android.mms.util.ContactInfoCache;
29import com.android.mms.util.DownloadManager;
30
31import com.google.android.mms.pdu.EncodedStringValue;
32import com.google.android.mms.pdu.PduHeaders;
33import com.google.android.mms.pdu.PduPersister;
34import com.google.android.mms.util.SqliteWrapper;
35
36import android.app.Notification;
37import android.app.NotificationManager;
38import android.app.PendingIntent;
39import android.content.ContentResolver;
40import android.content.Context;
41import android.content.Intent;
42import android.content.SharedPreferences;
43import android.database.Cursor;
44import android.graphics.Typeface;
45import android.net.Uri;
46import android.preference.PreferenceManager;
47import android.provider.Telephony.Mms;
48import android.provider.Telephony.Sms;
49import android.provider.Telephony.Threads;
50import android.text.Spannable;
51import android.text.SpannableString;
52import android.text.TextUtils;
53import android.text.style.StyleSpan;
54import android.util.Log;
55
56import java.util.Comparator;
57import java.util.HashSet;
58import java.util.Set;
59import java.util.SortedSet;
60import java.util.TreeSet;
61
62/**
63 * This class is used to update the notification indicator. It will check whether
64 * there are unread messages. If yes, it would show the notification indicator,
65 * otherwise, hide the indicator.
66 */
67public class MessagingNotification {
68    public static final String NOTIFICATION_CLICK_RECEIVER =
69            "com.android.mms.transaction.NotificationClickReceiver";
70    private static final String TAG = "MessagingNotification";
71
72    private static final int NOTIFICATION_ID = 123;
73    public static final int MESSAGE_FAILED_NOTIFICATION_ID = 789;
74    public static final int DOWNLOAD_FAILED_NOTIFICATION_ID = 531;
75
76    // This must be consistent with the column constants below.
77    private static final String[] MMS_STATUS_PROJECTION = new String[] {
78        Mms.THREAD_ID, Mms.DATE, Mms._ID, Mms.SUBJECT, Mms.SUBJECT_CHARSET };
79
80    // This must be consistent with the column constants below.
81    private static final String[] SMS_STATUS_PROJECTION = new String[] {
82        Sms.THREAD_ID, Sms.DATE, Sms.ADDRESS, Sms.SUBJECT, Sms.BODY };
83
84    // These must be consistent with MMS_STATUS_PROJECTION and
85    // SMS_STATUS_PROJECTION.
86    private static final int COLUMN_THREAD_ID   = 0;
87    private static final int COLUMN_DATE        = 1;
88    private static final int COLUMN_MMS_ID      = 2;
89    private static final int COLUMN_SMS_ADDRESS = 2;
90    private static final int COLUMN_SUBJECT     = 3;
91    private static final int COLUMN_SUBJECT_CS  = 4;
92    private static final int COLUMN_SMS_BODY    = 4;
93
94    private static final String NEW_INCOMING_SM_CONSTRAINT =
95            "(" + Sms.TYPE + " = " + Sms.MESSAGE_TYPE_INBOX
96            + " AND " + Sms.READ + " = 0)";
97
98    private static final String NEW_INCOMING_MM_CONSTRAINT =
99            "(" + Mms.MESSAGE_BOX + "=" + Mms.MESSAGE_BOX_INBOX
100            + " AND " + Mms.READ + "=0"
101            + " AND (" + Mms.MESSAGE_TYPE + "=" + MESSAGE_TYPE_NOTIFICATION_IND
102            + " OR " + Mms.MESSAGE_TYPE + "=" + MESSAGE_TYPE_RETRIEVE_CONF + "))";
103
104    private static final MmsSmsNotificationInfoComparator INFO_COMPARATOR =
105            new MmsSmsNotificationInfoComparator();
106
107    private static final Uri UNDELIVERED_URI = Uri.parse("content://mms-sms/undelivered");
108
109    private MessagingNotification() {
110    }
111
112    /**
113     * Checks to see if there are any unread messages or delivery
114     * reports.  Shows the most recent notification if there is one.
115     *
116     * @param context the context to use
117     */
118    public static void updateNewMessageIndicator(Context context) {
119        updateNewMessageIndicator(context, false);
120    }
121
122    /**
123     * Checks to see if there are any unread messages or delivery
124     * reports.  Shows the most recent notification if there is one.
125     *
126     * @param context the context to use
127     * @param isNew if notify a new message comes, it should be true, otherwise, false.
128     */
129    public static void updateNewMessageIndicator(Context context, boolean isNew) {
130        SortedSet<MmsSmsNotificationInfo> accumulator =
131                new TreeSet<MmsSmsNotificationInfo>(INFO_COMPARATOR);
132        Set<Long> threads = new HashSet<Long>(4);
133
134        int count = 0;
135        count += accumulateNotificationInfo(
136                accumulator, getMmsNewMessageNotificationInfo(context, threads));
137        count += accumulateNotificationInfo(
138                accumulator, getSmsNewMessageNotificationInfo(context, threads));
139
140        cancelNotification(context, NOTIFICATION_ID);
141        if (!accumulator.isEmpty()) {
142            accumulator.first().deliver(context, isNew, count, threads.size());
143        }
144    }
145
146    /**
147     * Updates all pending notifications, clearing or updating them as
148     * necessary.  This task is completed in the background on a worker
149     * thread.
150     */
151    public static void updateAllNotifications(final Context context) {
152        new Thread(new Runnable() {
153            public void run() {
154                updateNewMessageIndicator(context);
155                updateSendFailedNotification(context);
156                updateDownloadFailedNotification(context);
157            }
158        }).start();
159    }
160
161    /**
162     * Deletes any delivery report notifications for the specified
163     * thread, then checks to see if there are any unread messages or
164     * delivery reports.  Shows the most recent notification if there
165     * is one.
166     *
167     * @param context the context to use
168     * @param threadId the thread for which to clear delivery notifications
169     */
170    public static void updateNewMessageIndicator(
171            Context context, long threadId) {
172        updateNewMessageIndicator(context);
173    }
174
175    private static final int accumulateNotificationInfo(
176            SortedSet set, MmsSmsNotificationInfo info) {
177        if (info != null) {
178            set.add(info);
179
180            return info.mCount;
181        }
182
183        return 0;
184    }
185
186    private static final class MmsSmsNotificationInfo {
187        public Intent mClickIntent;
188        public String mDescription;
189        public int mIconResourceId;
190        public CharSequence mTicker;
191        public long mTimeMillis;
192        public String mTitle;
193        public int mCount;
194
195        public MmsSmsNotificationInfo(
196                Intent clickIntent, String description, int iconResourceId,
197                CharSequence ticker, long timeMillis, String title, int count) {
198            mClickIntent = clickIntent;
199            mDescription = description;
200            mIconResourceId = iconResourceId;
201            mTicker = ticker;
202            mTimeMillis = timeMillis;
203            mTitle = title;
204            mCount = count;
205        }
206
207        public void deliver(Context context, boolean isNew, int count, int uniqueThreads) {
208            updateNotification(
209                    context, mClickIntent, mDescription, mIconResourceId,
210                    isNew, mTicker, mTimeMillis, mTitle, count, uniqueThreads);
211        }
212
213        public long getTime() {
214            return mTimeMillis;
215        }
216    }
217
218    private static final class MmsSmsNotificationInfoComparator
219            implements Comparator<MmsSmsNotificationInfo> {
220        public int compare(
221                MmsSmsNotificationInfo info1, MmsSmsNotificationInfo info2) {
222            return Long.signum(info2.getTime() - info1.getTime());
223        }
224    }
225
226    public static final MmsSmsNotificationInfo getMmsNewMessageNotificationInfo(
227            Context context, Set<Long> threads) {
228        ContentResolver resolver = context.getContentResolver();
229        Cursor cursor = SqliteWrapper.query(context, resolver, Mms.CONTENT_URI,
230                            MMS_STATUS_PROJECTION, NEW_INCOMING_MM_CONSTRAINT,
231                            null, Mms.DATE + " desc");
232
233        if (cursor == null) {
234            return null;
235        }
236
237        try {
238            if (!cursor.moveToFirst()) {
239                return null;
240            }
241            long msgId = cursor.getLong(COLUMN_MMS_ID);
242            Uri msgUri = Mms.CONTENT_URI.buildUpon().appendPath(
243                    Long.toString(msgId)).build();
244            String address = AddressUtils.getFrom(context, msgUri);
245            String subject = getMmsSubject(
246                    cursor.getString(COLUMN_SUBJECT), cursor.getInt(COLUMN_SUBJECT_CS));
247            long threadId = cursor.getLong(COLUMN_THREAD_ID);
248            long timeMillis = cursor.getLong(COLUMN_DATE) * 1000;
249
250            MmsSmsNotificationInfo info = getNewMessageNotificationInfo(
251                    address, subject, context,
252                    R.drawable.stat_notify_mms, null, threadId,
253                    timeMillis, cursor.getCount());
254
255            threads.add(threadId);
256            while (cursor.moveToNext()) {
257                threads.add(cursor.getLong(COLUMN_THREAD_ID));
258            }
259
260            return info;
261        } finally {
262            cursor.close();
263        }
264    }
265
266    public static final MmsSmsNotificationInfo getSmsNewMessageNotificationInfo(
267            Context context, Set<Long> threads) {
268        ContentResolver resolver = context.getContentResolver();
269        Cursor cursor = SqliteWrapper.query(context, resolver, Sms.CONTENT_URI,
270                            SMS_STATUS_PROJECTION, NEW_INCOMING_SM_CONSTRAINT,
271                            null, Sms.DATE + " desc");
272
273        if (cursor == null) {
274            return null;
275        }
276
277        try {
278            if (!cursor.moveToFirst()) {
279                return null;
280            }
281
282            String address = cursor.getString(COLUMN_SMS_ADDRESS);
283            String body = cursor.getString(COLUMN_SMS_BODY);
284            long threadId = cursor.getLong(COLUMN_THREAD_ID);
285            long timeMillis = cursor.getLong(COLUMN_DATE);
286
287            MmsSmsNotificationInfo info = getNewMessageNotificationInfo(
288                    address, body, context, R.drawable.stat_notify_sms,
289                    null, threadId, timeMillis, cursor.getCount());
290
291            threads.add(threadId);
292            while (cursor.moveToNext()) {
293                threads.add(cursor.getLong(COLUMN_THREAD_ID));
294            }
295
296            return info;
297        } finally {
298            cursor.close();
299        }
300    }
301
302    private static final MmsSmsNotificationInfo getNewMessageNotificationInfo(
303            String address,
304            String body,
305            Context context,
306            int iconResourceId,
307            String subject,
308            long threadId,
309            long timeMillis,
310            int count) {
311        Intent clickIntent = getAppIntent();
312        clickIntent.setData(
313                Uri.withAppendedPath(
314                        clickIntent.getData(), Long.toString(threadId)));
315        clickIntent.setAction(Intent.ACTION_VIEW);
316
317        String senderInfo = buildTickerMessage(
318                context, address, null, null).toString();
319        String senderInfoName = senderInfo.substring(
320                0, senderInfo.length() - 2);
321        CharSequence ticker = buildTickerMessage(
322                context, address, subject, body);
323
324        return new MmsSmsNotificationInfo(
325                clickIntent, body, iconResourceId, ticker, timeMillis,
326                senderInfoName, count);
327    }
328
329    public static void cancelNotification(Context context, int notificationId) {
330        NotificationManager nm = (NotificationManager) context.getSystemService(
331                Context.NOTIFICATION_SERVICE);
332
333        nm.cancel(notificationId);
334    }
335
336    private static Intent getAppIntent() {
337        Intent appIntent = new Intent(Intent.ACTION_MAIN, Threads.CONTENT_URI);
338
339        appIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
340        return appIntent;
341   }
342
343    private static void updateNotification(
344            Context context,
345            Intent clickIntent,
346            String description,
347            int iconRes,
348            boolean isNew,
349            CharSequence ticker,
350            long timeMillis,
351            String title,
352            int messageCount,
353            int uniqueThreadCount) {
354        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
355
356        if (!sp.getBoolean(
357                    MessagingPreferenceActivity.NOTIFICATION_ENABLED, true)) {
358            return;
359        }
360
361        Notification notification = new Notification(iconRes, ticker, timeMillis);
362
363        // If we have more than one unique thread, change the title (which would
364        // normally be the contact who sent the message) to a generic one that
365        // makes sense for multiple senders, and change the Intent to take the
366        // user to the conversation list instead of the specific thread.
367        if (uniqueThreadCount > 1) {
368            title = context.getString(R.string.notification_multiple_title);
369            clickIntent = getAppIntent();
370            clickIntent.setAction(Intent.ACTION_MAIN);
371            clickIntent.setType("vnd.android-dir/mms-sms");
372        }
373
374        // If there is more than one message, change the description (which
375        // would normally be a snippet of the individual message text) to
376        // a string indicating how many unread messages there are.
377        if (messageCount > 1) {
378            description = context.getString(R.string.notification_multiple,
379                    Integer.toString(messageCount));
380        }
381
382        // Make a startActivity() PendingIntent for the notification.
383        PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, clickIntent,
384                PendingIntent.FLAG_UPDATE_CURRENT);
385
386        // Update the notification.
387        notification.setLatestEventInfo(context, title, description, pendingIntent);
388
389        if (isNew) {
390            boolean vibrate = sp.getBoolean(MessagingPreferenceActivity.NOTIFICATION_VIBRATE, true);
391            if (vibrate) {
392                notification.defaults |= Notification.DEFAULT_VIBRATE;
393            }
394
395            String ringtoneStr = sp
396                    .getString(MessagingPreferenceActivity.NOTIFICATION_RINGTONE, null);
397            notification.sound = TextUtils.isEmpty(ringtoneStr) ? null : Uri.parse(ringtoneStr);
398        }
399
400        notification.flags |= Notification.FLAG_SHOW_LIGHTS;
401        notification.ledARGB = 0xff00ff00;
402        notification.ledOnMS = 500;
403        notification.ledOffMS = 2000;
404
405        NotificationManager nm = (NotificationManager)
406            context.getSystemService(Context.NOTIFICATION_SERVICE);
407
408        nm.notify(NOTIFICATION_ID, notification);
409    }
410
411    protected static CharSequence buildTickerMessage(
412            Context context, String address, String subject, String body) {
413        String displayAddress = ContactInfoCache.getInstance()
414                .getContactName(context, address);
415
416        StringBuilder buf = new StringBuilder(
417                displayAddress == null
418                ? ""
419                : displayAddress.replace('\n', ' ').replace('\r', ' '));
420        buf.append(':').append(' ');
421
422        int offset = buf.length();
423        if (!TextUtils.isEmpty(subject)) {
424            subject = subject.replace('\n', ' ').replace('\r', ' ');
425            buf.append(subject);
426            buf.append(' ');
427        }
428
429        if (!TextUtils.isEmpty(body)) {
430            body = body.replace('\n', ' ').replace('\r', ' ');
431            buf.append(body);
432        }
433
434        SpannableString spanText = new SpannableString(buf.toString());
435        spanText.setSpan(new StyleSpan(Typeface.BOLD), 0, offset,
436                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
437
438        return spanText;
439    }
440
441    private static String getMmsSubject(String sub, int charset) {
442        return TextUtils.isEmpty(sub) ? ""
443                : new EncodedStringValue(charset, PduPersister.getBytes(sub)).getString();
444    }
445
446    public static void notifyDownloadFailed(Context context, long threadId) {
447        notifyFailed(context, true, threadId, false);
448    }
449
450    public static void notifySendFailed(Context context) {
451        notifyFailed(context, false, 0, false);
452    }
453
454    public static void notifySendFailed(Context context, boolean noisy) {
455        notifyFailed(context, false, 0, noisy);
456    }
457
458    private static void notifyFailed(Context context, boolean isDownload, long threadId,
459                                     boolean noisy) {
460        // TODO factor out common code for creating notifications
461        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
462
463        boolean enabled = sp.getBoolean(MessagingPreferenceActivity.NOTIFICATION_ENABLED, true);
464        if (!enabled) {
465            return;
466        }
467
468        NotificationManager nm = (NotificationManager)
469                context.getSystemService(Context.NOTIFICATION_SERVICE);
470
471        // Strategy:
472        // a. If there is a single failure notification, tapping on the notification goes
473        //    to the compose view.
474        // b. If there are two failure it stays in the thread view. Selecting one undelivered
475        //    thread will dismiss one undelivered notification but will still display the
476        //    notification.If you select the 2nd undelivered one it will dismiss the notification.
477
478        long[] msgThreadId = {0};
479        int totalFailedCount = getUndeliveredMessageCount(context, msgThreadId);
480
481        Intent failedIntent;
482        Notification notification = new Notification();
483        String title;
484        String description;
485        if (totalFailedCount > 1) {
486            description = context.getString(R.string.notification_failed_multiple,
487                    Integer.toString(totalFailedCount));
488            title = context.getString(R.string.notification_failed_multiple_title);
489
490            failedIntent = new Intent(context, ConversationList.class);
491        } else {
492            title = isDownload ?
493                        context.getString(R.string.message_download_failed_title) :
494                        context.getString(R.string.message_send_failed_title);
495
496            description = context.getString(R.string.message_failed_body);
497            threadId = (msgThreadId[0] != 0 ? msgThreadId[0] : 0);
498
499            failedIntent = new Intent(context, ComposeMessageActivity.class);
500            failedIntent.putExtra("thread_id", threadId);
501            failedIntent.putExtra("undelivered_flag", true);
502        }
503
504        failedIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
505        PendingIntent pendingIntent = PendingIntent.getActivity(
506                context, 0, failedIntent, PendingIntent.FLAG_UPDATE_CURRENT);
507
508        notification.icon = R.drawable.stat_notify_sms_failed;
509
510        notification.tickerText = title;
511
512        notification.setLatestEventInfo(context, title, description, pendingIntent);
513
514        if (noisy) {
515            boolean vibrate = sp.getBoolean(MessagingPreferenceActivity.NOTIFICATION_VIBRATE, true);
516            if (vibrate) {
517                notification.defaults |= Notification.DEFAULT_VIBRATE;
518            }
519
520            String ringtoneStr = sp.getString(MessagingPreferenceActivity.NOTIFICATION_RINGTONE, null);
521            notification.sound = TextUtils.isEmpty(ringtoneStr) ? null : Uri.parse(ringtoneStr);
522        }
523
524        if (isDownload) {
525            nm.notify(DOWNLOAD_FAILED_NOTIFICATION_ID, notification);
526        } else {
527            nm.notify(MESSAGE_FAILED_NOTIFICATION_ID, notification);
528        }
529    }
530
531    // threadIdResult[0] contains the thread id of the first message.
532    // threadIdResult[1] is nonzero if the thread ids of all the messages are the same.
533    // You can pass in null for threadIdResult.
534    // You can pass in a threadIdResult of size 1 to avoid the comparison of each thread id.
535    private static int getUndeliveredMessageCount(Context context, long[] threadIdResult) {
536        Cursor undeliveredCursor = SqliteWrapper.query(context, context.getContentResolver(),
537                UNDELIVERED_URI, new String[] { Mms.THREAD_ID }, "read=0", null, null);
538        if (undeliveredCursor == null) {
539            return 0;
540        }
541        int count = undeliveredCursor.getCount();
542        try {
543            if (threadIdResult != null && undeliveredCursor.moveToFirst()) {
544                threadIdResult[0] = undeliveredCursor.getLong(0);
545
546                if (threadIdResult.length >= 2) {
547                    // Test to see if all the undelivered messages belong to the same thread.
548                    long firstId = threadIdResult[0];
549                    while (undeliveredCursor.moveToNext()) {
550                        if (undeliveredCursor.getLong(0) != firstId) {
551                            firstId = 0;
552                            break;
553                        }
554                    }
555                    threadIdResult[1] = firstId;    // non-zero if all ids are the same
556                }
557            }
558        } finally {
559            undeliveredCursor.close();
560        }
561        return count;
562    }
563
564    public static void updateSendFailedNotification(Context context) {
565        if (getUndeliveredMessageCount(context, null) < 1) {
566            cancelNotification(context, MESSAGE_FAILED_NOTIFICATION_ID);
567        } else {
568            notifySendFailed(context);      // rebuild and adjust the message count if necessary.
569        }
570    }
571
572    /**
573     *  If all the undelivered messages belong to "threadId", cancel the notification.
574     */
575    public static void updateSendFailedNotificationForThread(Context context, long threadId) {
576        long[] msgThreadId = {0, 0};
577        if (getUndeliveredMessageCount(context, msgThreadId) > 0
578                && msgThreadId[0] == threadId
579                && msgThreadId[1] != 0) {
580            cancelNotification(context, MESSAGE_FAILED_NOTIFICATION_ID);
581        }
582    }
583
584    private static int getDownloadFailedMessageCount(Context context) {
585        // Look for any messages in the MMS Inbox that are of the type
586        // NOTIFICATION_IND (i.e. not already downloaded) and in the
587        // permanent failure state.  If there are none, cancel any
588        // failed download notification.
589        Cursor c = SqliteWrapper.query(context, context.getContentResolver(),
590                Mms.Inbox.CONTENT_URI, null,
591                Mms.MESSAGE_TYPE + "=" +
592                    String.valueOf(PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND) +
593                " AND " + Mms.STATUS + "=" +
594                    String.valueOf(DownloadManager.STATE_PERMANENT_FAILURE),
595                null, null);
596        if (c == null) {
597            return 0;
598        }
599        int count = c.getCount();
600        c.close();
601        return count;
602    }
603
604    public static void updateDownloadFailedNotification(Context context) {
605        if (getDownloadFailedMessageCount(context) < 1) {
606            cancelNotification(context, DOWNLOAD_FAILED_NOTIFICATION_ID);
607        }
608    }
609}
610