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