org.kontalk.ui.MessagingNotification.java Source code

Java tutorial

Introduction

Here is the source code for org.kontalk.ui.MessagingNotification.java

Source

/*
 * Kontalk Android client
 * Copyright (C) 2017 Kontalk Devteam <devteam@kontalk.org>
    
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
    
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
    
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.kontalk.ui;

import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

import org.jxmpp.util.XmppStringUtils;

import android.accounts.Account;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.media.AudioManager;
import android.net.Uri;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationCompat.BigTextStyle;
import android.support.v4.app.NotificationCompat.InboxStyle;
import android.support.v4.app.NotificationCompat.Style;
import android.support.v4.app.NotificationManagerCompat;
import android.support.v4.app.TaskStackBuilder;
import android.support.v4.content.ContextCompat;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan;

import org.kontalk.R;
import org.kontalk.authenticator.Authenticator;
import org.kontalk.data.Contact;
import org.kontalk.message.CompositeMessage;
import org.kontalk.message.GroupCommandComponent;
import org.kontalk.provider.MessagesProviderUtils.GroupThreadContent;
import org.kontalk.provider.MyMessages.CommonColumns;
import org.kontalk.provider.MyMessages.Groups;
import org.kontalk.provider.MyMessages.Messages;
import org.kontalk.provider.MyMessages.Threads;
import org.kontalk.service.NotificationActionReceiver;
import org.kontalk.util.MessageUtils;
import org.kontalk.util.Preferences;
import org.kontalk.util.SystemUtils;

/**
 * Various utility methods for managing system notifications.
 * @author Daniele Ricci
 */
public class MessagingNotification {
    public static final int NOTIFICATION_ID_MESSAGES = 101;
    public static final int NOTIFICATION_ID_UPLOADING = 102;
    public static final int NOTIFICATION_ID_UPLOAD_ERROR = 103;
    public static final int NOTIFICATION_ID_DOWNLOADING = 104;
    public static final int NOTIFICATION_ID_DOWNLOAD_OK = 105;
    public static final int NOTIFICATION_ID_DOWNLOAD_ERROR = 106;
    public static final int NOTIFICATION_ID_QUICK_REPLY = 107;
    public static final int NOTIFICATION_ID_KEYPAIR_GEN = 108;
    public static final int NOTIFICATION_ID_INVITATION = 109;
    public static final int NOTIFICATION_ID_AUTH_ERROR = 110;

    private static final String[] MESSAGES_UNREAD_PROJECTION = { Messages.THREAD_ID, CommonColumns.PEER,
            Messages.BODY_MIME, Messages.BODY_CONTENT, Messages.ATTACHMENT_MIME, CommonColumns.ENCRYPTED,
            Groups.GROUP_JID, Groups.SUBJECT, };

    private static final String[] THREADS_UNREAD_PROJECTION = { CommonColumns._ID, CommonColumns.PEER, Threads.MIME,
            Threads.CONTENT, CommonColumns.ENCRYPTED, CommonColumns.UNREAD, Groups.GROUP_JID, Groups.SUBJECT, };

    private static final String MESSAGES_UNREAD_SELECTION = CommonColumns.NEW + " <> 0 AND "
            + CommonColumns.DIRECTION + " = " + Messages.DIRECTION_IN;

    /** Pending delayed notification update flag. */
    @SuppressWarnings("WeakerAccess")
    static volatile boolean sPending;

    /** Temporary disable all notifications flag */
    private static volatile boolean sDisabled;

    /** Peer to NOT be notified for new messages. */
    private static volatile String sPaused;

    /** Peer of last notified chat invitation. */
    private static volatile String sLastInvitation;

    /** Notification action intents stuff. */
    public static final String ACTION_NOTIFICATION_DELETED = "org.kontalk.ACTION_NOTIFICATION_DELETED";
    public static final String ACTION_NOTIFICATION_MARK_READ = "org.kontalk.ACTION_NOTIFICATION_MARK_READ";

    /** This class is not instanciable. */
    private MessagingNotification() {
    }

    public static void init(Context context) {
    }

    public static void setPaused(String jid) {
        sPaused = jid;
    }

    public static boolean isPaused(String jid) {
        return sPaused != null && sPaused.equalsIgnoreCase(XmppStringUtils.parseBareJid(jid));
    }

    /** Enables all notifications. */
    public static void enable() {
        sDisabled = false;
    }

    /** Temporarly disable all notifications. */
    public static void disable() {
        sDisabled = true;
    }

    private static boolean supportsBigNotifications() {
        return android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN;
    }

    /** Starts messages notification updates in another thread. */
    public static void delayedUpdateMessagesNotification(final Context context, final boolean isNew) {
        if (!sPending) {
            sPending = true;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    updateMessagesNotification(context, isNew);
                    sPending = false;
                }
            }).start();
        }
    }

    /**
     * Updates system notification for unread messages.
     * @param context
     * @param isNew if true a new message has come (starts notification alerts)
     */
    public static void updateMessagesNotification(Context context, boolean isNew) {
        // no default account. WTF?!?
        Account account = Authenticator.getDefaultAccount(context);
        if (account == null)
            return;

        // if notifying new messages, wait a little bit
        // to let all incoming messages come through
        /*
        FIXME this is useless because message are slow to arrive anyway
        (time to receive packs, store messages in db, etc. wastes too much time
        if (isNew) {
        try {
            Thread.sleep(500);
        }
        catch (InterruptedException e) {
            // ignored
        }
        }
        */

        ContentResolver res = context.getContentResolver();
        NotificationManagerCompat nm = NotificationManagerCompat.from(context);

        String query = MESSAGES_UNREAD_SELECTION;
        String[] args = null;
        String[] proj;
        String order;
        Uri uri;
        if (supportsBigNotifications()) {
            uri = Messages.CONTENT_URI;
            proj = MESSAGES_UNREAD_PROJECTION;
            order = Messages.DEFAULT_SORT_ORDER;
        } else {
            uri = Threads.CONTENT_URI;
            proj = THREADS_UNREAD_PROJECTION;
            order = Threads.INVERTED_SORT_ORDER;
        }

        // is there a peer to not notify for?
        final String paused = sPaused;
        if (paused != null) {
            query += " AND " + CommonColumns.PEER + " <> ? AND " + "(" + Groups.GROUP_JID + " IS NULL OR "
                    + Groups.GROUP_JID + " <> ?)";
            args = new String[] { paused, paused };
        }

        Cursor c = res.query(uri, proj, query, args, order);

        // this shouldn't happen, but who knows...
        if (c == null) {
            nm.cancel(NOTIFICATION_ID_MESSAGES);
            return;
        }

        // no unread messages - delete notification
        int unread = c.getCount();
        if (unread == 0) {
            c.close();
            nm.cancel(NOTIFICATION_ID_MESSAGES);
            return;
        }

        // notifications are disabled
        if (!Preferences.getNotificationsEnabled(context) || sDisabled)
            return;

        NotificationCompat.Builder builder = new NotificationCompat.Builder(context.getApplicationContext());
        Set<Uri> conversationIds = new HashSet<>(unread);

        if (supportsBigNotifications()) {
            NotificationGenerator ngen = new NotificationGenerator(context, builder);

            long id = 0;
            while (c.moveToNext()) {
                // thread_id for PendingIntent
                id = c.getLong(0);
                String peer = c.getString(1);
                String mime = c.getString(2);
                byte[] content = c.getBlob(3);
                String attMime = c.getString(4);
                boolean encrypted = c.getInt(5) != 0;
                String groupJid = c.getString(6);
                String groupSubject = c.getString(7);

                // store conversation id for intents
                conversationIds.add(ContentUris.withAppendedId(Threads.CONTENT_URI, id));

                ngen.addMessage(peer, mime, content, attMime, encrypted, groupJid, groupSubject);
            }
            c.close();

            int convCount = ngen.build(account, unread, conversationIds.iterator().next());

            builder.setSmallIcon(R.drawable.ic_stat_notify);
            builder.setVisibility(NotificationCompat.VISIBILITY_PRIVATE);

            Intent ni;
            // more than one unread conversation - open conversations list
            if (convCount > 1) {
                ni = new Intent(context, ConversationsActivity.class);
                ni.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP
                        | Intent.FLAG_ACTIVITY_CLEAR_TOP);
            }
            // one unread conversation - open compose message on that thread
            else {
                ni = ComposeMessage.fromConversation(context, id);
            }
            PendingIntent pi = createPendingIntent(context, ni);

            builder.setContentIntent(pi);
        }

        else {
            // loop all threads and accumulate them
            MessageAccumulator accumulator = new MessageAccumulator(context);
            while (c.moveToNext()) {
                long threadId = c.getLong(0);
                String peer = c.getString(1);
                String mime = c.getString(2);
                String content = c.getString(3);
                boolean encrypted = c.getInt(4) != 0;

                if (encrypted) {
                    content = context.getString(R.string.text_encrypted);
                } else if (content == null) {
                    content = CompositeMessage.getSampleTextContent(mime);
                } else if (GroupCommandComponent.supportsMimeType(mime)) {
                    // content is in a special format
                    GroupThreadContent parsed = GroupThreadContent.parseIncoming(content);
                    try {
                        peer = parsed.sender;
                        content = GroupCommandComponent.getTextContent(context, parsed.command, true);
                    } catch (UnsupportedOperationException e) {
                        // TODO using another string
                        content = context.getString(R.string.peer_unknown);
                    }
                }

                accumulator.accumulate(threadId, peer, content, c.getInt(5),
                        // group data
                        c.getString(6), c.getString(7));
                conversationIds.add(ContentUris.withAppendedId(Threads.CONTENT_URI, threadId));
            }
            c.close();

            builder.setTicker(accumulator.getTicker());
            Contact contact = accumulator.getContact();
            if (contact != null) {
                Bitmap avatar = contact.getAvatarBitmap(context);
                if (avatar != null)
                    builder.setLargeIcon(avatar);
            }
            builder.setNumber(accumulator.unreadCount);
            builder.setSmallIcon(R.drawable.ic_stat_notify);
            builder.setContentTitle(accumulator.getTitle());
            builder.setContentText(accumulator.getText());
            builder.setContentIntent(accumulator.getPendingIntent());
        }

        // build on delete intent for conversations
        Intent notificationDeleteIntent = new Intent(context, NotificationActionReceiver.class);
        notificationDeleteIntent.setAction(ACTION_NOTIFICATION_DELETED);
        notificationDeleteIntent.putExtra("org.kontalk.datalist",
                conversationIds.toArray(new Uri[conversationIds.size()]));
        builder.setDeleteIntent(PendingIntent.getBroadcast(context, 0, notificationDeleteIntent,
                PendingIntent.FLAG_UPDATE_CURRENT));

        if (isNew) {
            setDefaults(context, builder);
        }

        // features (priority, category)
        setFeatures(context, builder);

        nm.notify(NOTIFICATION_ID_MESSAGES, builder.build());

        /* TODO take this from configuration
        boolean quickReply = false;
        if (isNew && quickReply) {
        Intent i = new Intent(context.getApplicationContext(), QuickReplyActivity.class);
        i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
        i.putExtra("org.kontalk.quickreply.FROM", accumulator.getLastMessagePeer());
        i.putExtra("org.kontalk.quickreply.MESSAGE", accumulator.getLastMessageText());
        i.putExtra("org.kontalk.quickreply.OPEN_INTENT", accumulator.getLastMessagePendingIntent());
        context.startActivity(i);
        }
        */
    }

    private static void setDefaults(Context context, NotificationCompat.Builder builder) {
        int defaults = 0;

        if (Preferences.getNotificationLED(context)) {
            int ledColor = Preferences.getNotificationLEDColor(context);
            builder.setLights(ledColor, 1000, 1000);
        } else {
            // this will disable the LED completely
            builder.setLights(0, 0, 0);
        }

        String ringtone = Preferences.getNotificationRingtone(context);
        if (ringtone != null && ringtone.length() > 0)
            builder.setSound(Uri.parse(ringtone));

        String vibrate = Preferences.getNotificationVibrate(context);
        if ("always".equals(vibrate) || ("silent_only".equals(vibrate)
                && ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE))
                        .getRingerMode() != AudioManager.RINGER_MODE_NORMAL))
            defaults |= Notification.DEFAULT_VIBRATE;

        builder.setDefaults(defaults);
    }

    private static void setFeatures(Context context, NotificationCompat.Builder builder) {
        builder.setPriority(NotificationCompat.PRIORITY_HIGH);
        builder.setCategory(NotificationCompat.CATEGORY_MESSAGE);
        builder.setColor(ContextCompat.getColor(context, R.color.app_accent));
    }

    private static final class NotificationConversation {
        final String peer;
        final CharSequence allContent;
        final String groupJid;
        final String groupSubject;

        CharSequence lastContent;

        NotificationConversation(String peer, CharSequence allContent, CharSequence lastContent, String groupJid,
                String groupSubject) {
            this.peer = peer;
            this.allContent = allContent;
            this.lastContent = lastContent;
            this.groupJid = groupJid;
            this.groupSubject = groupSubject;
        }
    }

    /** Triggers a notification for a chat invitation. */
    public static void chatInvitation(Context context, String jid) {
        // open conversation, do not send notification
        if (jid.equalsIgnoreCase(sPaused))
            return;

        // find the contact for the userId
        Contact contact = Contact.findByUserId(context, jid);

        String title = contact.getDisplayName();

        // notification will open the conversation
        Intent ni = ComposeMessage.fromUserId(context, jid);
        ni.setFlags(
                Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
        PendingIntent pi = PendingIntent.getActivity(context, NOTIFICATION_ID_INVITATION, ni, 0);

        // build the notification
        NotificationCompat.Builder builder = new NotificationCompat.Builder(context.getApplicationContext())
                .setAutoCancel(true).setSmallIcon(R.drawable.ic_stat_notify)
                .setTicker(context.getString(R.string.title_invitation)).setContentTitle(title)
                .setContentText(context.getString(R.string.invite_notification)).setContentIntent(pi);

        // include an avatar if any
        if (contact != null) {
            Drawable avatar = contact.getAvatar(context);
            if (avatar != null)
                builder.setLargeIcon(MessageUtils.drawableToBitmap(avatar));
        }

        // defaults (sound, vibration, lights)
        setDefaults(context, builder);
        // features (priority, category)
        setFeatures(context, builder);

        // fire it up!
        NotificationManagerCompat nm = NotificationManagerCompat.from(context);
        nm.notify(NOTIFICATION_ID_INVITATION, builder.build());

        // this is for clearChatInvitation()
        sLastInvitation = jid;
    }

    /** Cancel a chat invitation notification. */
    public static void clearChatInvitation(Context context, String userId) {
        if (userId.equalsIgnoreCase(sLastInvitation)) {
            NotificationManagerCompat nm = NotificationManagerCompat.from(context);
            nm.cancel(NOTIFICATION_ID_INVITATION);
        }
    }

    /** Fires an authentication error notification. */
    public static void authenticationError(Context context) {
        // notification will open the conversation
        Intent ni = ConversationsActivity.authenticationErrorWarning(context);
        PendingIntent pi = PendingIntent.getActivity(context, NOTIFICATION_ID_AUTH_ERROR, ni,
                PendingIntent.FLAG_UPDATE_CURRENT);

        // build the notification
        NotificationCompat.Builder builder = new NotificationCompat.Builder(context.getApplicationContext())
                .setAutoCancel(true).setSmallIcon(R.drawable.ic_stat_notify)
                .setTicker(context.getString(R.string.title_auth_error))
                .setContentTitle(context.getString(R.string.title_auth_error))
                .setContentText(context.getString(R.string.notification_text_more)).setContentIntent(pi);

        // defaults (sound, vibration, lights)
        setDefaults(context, builder);

        // fire it up!
        NotificationManagerCompat nm = NotificationManagerCompat.from(context);
        nm.notify(NOTIFICATION_ID_AUTH_ERROR, builder.build());
    }

    public static void clearAuthenticationError(Context context) {
        NotificationManagerCompat nm = NotificationManagerCompat.from(context);
        nm.cancel(NOTIFICATION_ID_AUTH_ERROR);
    }

    /**
     * Takes messages to be notified and fills a notification builder.
     * Used only for big notifications (JB+).
     */
    private static final class NotificationGenerator {
        private final Context mContext;
        private final NotificationCompat.Builder mBuilder;
        private final Map<String, NotificationConversation> mConversations;

        NotificationGenerator(Context context, NotificationCompat.Builder builder) {
            mContext = context;
            mBuilder = builder;
            mConversations = new LinkedHashMap<>();
        }

        void addMessage(String peer, String mime, byte[] content, String attMime, boolean encrypted,
                String groupJid, String groupSubject) {
            String key = conversationKey(peer, groupJid);
            NotificationConversation conv = mConversations.get(key);
            if (conv == null) {
                conv = new NotificationConversation(peer, new StringBuilder(), null, groupJid, groupSubject);
                mConversations.put(key, conv);
            } else {
                ((StringBuilder) conv.allContent).append('\n');
            }

            String textContent;

            if (encrypted) {
                textContent = mContext.getString(R.string.text_encrypted);
            } else if (content == null && attMime != null) {
                textContent = CompositeMessage.getSampleTextContent(attMime);
            } else {
                textContent = content != null ? new String(content) : "";
                if (GroupCommandComponent.supportsMimeType(mime)) {
                    try {
                        textContent = GroupCommandComponent.getTextContent(mContext, textContent, true);
                    } catch (UnsupportedOperationException e) {
                        // TODO using another string
                        textContent = mContext.getString(R.string.peer_unknown);
                    }
                }
            }

            ((StringBuilder) conv.allContent).append(textContent);
            conv.lastContent = textContent;
        }

        private String conversationKey(String peer, String groupJid) {
            return groupJid != null ? (peer + ":" + groupJid) : peer;
        }

        private int size() {
            if (mConversations.size() == 1)
                return 1;

            // manual count is needed because we need to group chat groups (ehm)
            Set<String> keys = new HashSet<>();
            for (String key : mConversations.keySet()) {
                String[] parsed = key.split(":", 2);
                keys.add(parsed.length > 1 ? parsed[1] : parsed[0]);
            }
            return keys.size();
        }

        /**
         * Called when everything is set. This will fill the builder.
         * @return the number of conversations (i.e. threads) involved
         */
        int build(Account account, int unread, Uri firstThreadUri) {
            int convCount = size();
            Style style;
            CharSequence title, text, ticker;

            // more than one conversation - use InboxStyle
            if (mConversations.size() > 1) {
                // we are handling a notification for a single group
                boolean singleGroup = convCount == 1;

                style = new InboxStyle();

                if (!singleGroup) {
                    // ticker: "X unread messages"
                    ticker = mContext.getResources().getQuantityString(R.plurals.unread_messages, unread, unread);
                } else {
                    // ticker: "X messages @ group"
                    NotificationConversation conv = mConversations.values().iterator().next();
                    String groupSubject = conv.groupSubject;
                    ticker = mContext.getResources().getQuantityString(R.plurals.unread_messages_group, unread,
                            unread, groupSubject);
                }

                // title
                title = ticker;

                // text: comma separated names (TODO RTL?)
                StringBuilder btext = new StringBuilder();
                int count = 0;
                for (String convId : mConversations.keySet()) {
                    NotificationConversation conv = mConversations.get(convId);
                    count++;

                    Contact contact = Contact.findByUserId(mContext, conv.peer);
                    String name = contact.getDisplayName();

                    if (conv.groupJid != null) {
                        if (!singleGroup) {
                            name = mContext.getResources().getString(R.string.notification_group_title, name,
                                    (TextUtils.isEmpty(conv.groupSubject)
                                            ? mContext.getString(R.string.group_untitled)
                                            : conv.groupSubject));
                        }
                    }

                    // add person to notification
                    Uri personUri = contact.getUri();
                    if (personUri == null && contact.getNumber() != null) {
                        // no contact uri available, try phone number lookup
                        try {
                            personUri = Uri.parse("tel:" + contact.getNumber());
                        } catch (Exception ignored) {
                        }
                    }
                    if (personUri != null)
                        mBuilder.addPerson(personUri.toString());

                    if (btext.length() > 0)
                        btext.append(", ");
                    btext.append(name);

                    // inbox line
                    if (count < 5) {
                        SpannableStringBuilder buf = new SpannableStringBuilder();
                        buf.append(name).append(' ');
                        buf.setSpan(
                                new ForegroundColorSpan(
                                        ContextCompat.getColor(mContext, R.color.notification_name_color)),
                                0, buf.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                        // take just the last message
                        buf.append(conv.lastContent);

                        ((InboxStyle) style).addLine(buf);
                    }
                }

                if (btext.length() > 0)
                    text = btext.toString();
                else
                    // TODO i18n
                    text = "(unknown users)";

                String summary;
                int moreCount = mConversations.size() - count;
                if (moreCount > 0) {
                    summary = mContext.getResources().getQuantityString(R.plurals.notification_more, moreCount,
                            moreCount);
                } else {
                    summary = account.name;
                }

                ((InboxStyle) style).setSummaryText(summary);
            }
            // one conversation, use BigTextStyle
            else {
                NotificationConversation conv = mConversations.values().iterator().next();
                String content = conv.allContent.toString();
                CharSequence last = conv.lastContent;

                // big text content
                style = new BigTextStyle();
                ((BigTextStyle) style).bigText(content);
                ((BigTextStyle) style).setSummaryText(account.name);

                // ticker
                Contact contact = Contact.findByUserId(mContext, conv.peer);
                String name = contact.getDisplayName();

                if (conv.groupJid != null) {
                    name = mContext.getResources().getString(R.string.notification_group_title, name,
                            (TextUtils.isEmpty(conv.groupSubject) ? mContext.getString(R.string.group_untitled)
                                    : conv.groupSubject));
                }

                SpannableStringBuilder buf = new SpannableStringBuilder();
                buf.append(name).append(':').append(' ');
                buf.setSpan(new StyleSpan(Typeface.BOLD), 0, buf.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                buf.append(last);

                ticker = buf;

                // title
                title = name;

                // text
                text = (unread > 1)
                        ? mContext.getResources().getQuantityString(R.plurals.unread_messages, unread, unread)
                        : content;

                PendingIntent callPendingIntent = null;

                if (contact != null) {
                    Uri personUri = contact.getUri();
                    if (personUri == null && contact.getNumber() != null) {
                        // no contact uri available, try phone number lookup
                        try {
                            personUri = Uri.parse("tel:" + contact.getNumber());
                        } catch (Exception ignored) {
                        }
                    }
                    if (personUri != null)
                        mBuilder.addPerson(personUri.toString());

                    // avatar
                    Drawable avatar = contact.getAvatar(mContext);
                    if (avatar != null)
                        mBuilder.setLargeIcon(MessageUtils.drawableToBitmap(avatar));

                    // phone number for call intent
                    String phoneNumber = contact.getNumber();
                    if (phoneNumber != null) {
                        Intent callIntent = SystemUtils.externalIntent(Intent.ACTION_CALL,
                                Uri.parse("tel:" + phoneNumber));
                        callPendingIntent = PendingIntent.getActivity(mContext, 0, callIntent, 0);
                    }
                }

                // mark as read pending intent
                // TODO this should also be used for messages from a single group
                Intent markReadIntent = new Intent(ACTION_NOTIFICATION_MARK_READ, firstThreadUri, mContext,
                        NotificationActionReceiver.class);
                PendingIntent readPendingIntent = PendingIntent.getBroadcast(mContext, 0, markReadIntent,
                        PendingIntent.FLAG_UPDATE_CURRENT);

                mBuilder.addAction(R.drawable.ic_menu_check, mContext.getString(R.string.mark_read),
                        readPendingIntent);
                mBuilder.addAction(R.drawable.ic_menu_call, mContext.getString(R.string.call), callPendingIntent);
            }

            mBuilder.setTicker(ticker);
            mBuilder.setContentTitle(title);
            mBuilder.setContentText(text);
            mBuilder.setStyle(style);
            mBuilder.setNumber(unread);

            return convCount;
        }
    }

    static PendingIntent createPendingIntent(Context context, Intent intent) {
        return TaskStackBuilder.create(context)
                // add all of DetailsActivity's parents to the stack,
                // followed by DetailsActivity itself
                .addNextIntentWithParentStack(intent)
                .getPendingIntent(NOTIFICATION_ID_MESSAGES, PendingIntent.FLAG_UPDATE_CURRENT);
    }

    /**
     * This class accumulates all incoming unread threads and returns
     * well-formed data to be used in a {@link Notification}.
     * Used only for legacy notifications (pre-JB).
     */
    private static final class MessageAccumulator {
        final class ConversationStub {
            public long id;
            public String peer;
            public String content;
            public String groupJid;
            public String groupSubject;
        }

        private ConversationStub conversation;
        private int convCount;
        int unreadCount;
        private Context mContext;
        private Contact mContact;

        public MessageAccumulator(Context context) {
            mContext = context;
        }

        /** Adds a conversation thread to the accumulator. */
        public void accumulate(long id, String peer, String content, int unread, String groupJid,
                String groupSubject) {
            // check old accumulated conversation
            if (conversation != null) {
                if (!conversation.peer.equalsIgnoreCase(peer))
                    convCount++;
            }
            // no previous conversation - start counting
            else {
                convCount = 1;
                conversation = new ConversationStub();
            }

            conversation.id = id;
            conversation.peer = peer;
            conversation.content = content;
            conversation.groupJid = groupJid;
            conversation.groupSubject = groupSubject;

            unreadCount += unread;
        }

        private void cacheContact() {
            mContact = Contact.findByUserId(mContext, conversation.peer);
        }

        public Contact getContact() {
            return mContact;
        }

        /** Returns the text that should be used as a ticker in the notification. */
        public CharSequence getTicker() {
            cacheContact();
            String peer = (mContact != null) ? mContact.getDisplayName()
                    : mContext.getString(R.string.peer_unknown);
            // debug mode -- conversation.peer;

            // append group subject to contact name if any
            if (conversation.groupJid != null) {
                peer = mContext.getResources().getString(R.string.notification_group_title, peer,
                        (TextUtils.isEmpty(conversation.groupSubject) ? mContext.getString(R.string.group_untitled)
                                : conversation.groupSubject));
            }

            SpannableStringBuilder buf = new SpannableStringBuilder();
            buf.append(peer).append(':').append(' ');
            buf.setSpan(new StyleSpan(Typeface.BOLD), 0, buf.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            buf.append(conversation.content);

            return buf;
        }

        /** Returns the text that should be used as the notification title. */
        public String getTitle() {
            if (convCount > 1) {
                return mContext.getString(R.string.new_messages);
            } else {
                cacheContact();
                String peer = (mContact != null) ? mContact.getDisplayName()
                        : mContext.getString(R.string.peer_unknown);
                // debug mode -- conversation.peer;

                // append group subject to contact name if any
                if (conversation.groupJid != null) {
                    peer = mContext.getResources().getString(R.string.notification_group_title, peer,
                            (TextUtils.isEmpty(conversation.groupSubject)
                                    ? mContext.getString(R.string.group_untitled)
                                    : conversation.groupSubject));
                }

                return peer;
            }
        }

        /** Returns the text that should be used as the notification text. */
        public String getText() {
            return (unreadCount > 1)
                    ? mContext.getResources().getQuantityString(R.plurals.unread_messages, unreadCount, unreadCount)
                    : conversation.content;
        }

        /** Builds a {@link PendingIntent} to be used in the notification. */
        public PendingIntent getPendingIntent() {
            Intent ni;
            // more than one unread conversation - open ConversationList
            if (convCount > 1) {
                ni = new Intent(mContext, ConversationsActivity.class);
                ni.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP
                        | Intent.FLAG_ACTIVITY_CLEAR_TOP);
            }
            // one unread conversation - open ComposeMessage on that peer
            else {
                ni = ComposeMessage.fromConversation(mContext, conversation.id);
            }
            return createPendingIntent(mContext, ni);
        }

        public String getLastMessageText() {
            return conversation.content;
        }

        public String getLastMessagePeer() {
            return conversation.peer;
        }

        public PendingIntent getLastMessagePendingIntent() {
            // one unread conversation - open ComposeMessage on that peer
            Intent ni = ComposeMessage.fromConversation(mContext, conversation.id);
            return PendingIntent.getActivity(mContext, NOTIFICATION_ID_QUICK_REPLY, ni, 0);
        }
    }
}