com.android.mms.transaction.MessagingNotification.java Source code

Java tutorial

Introduction

Here is the source code for com.android.mms.transaction.MessagingNotification.java

Source

/*
 * Copyright (C) 2010-2014, The Linux Foundation. All rights reserved.
 * Not a Contribution.
 * Copyright (C) 2008 Esmertec AG.
 * Copyright (C) 2008 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.mms.transaction;

import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND;
import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.List;

import android.app.ActivityManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.TaskStackBuilder;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.database.Cursor;
import android.database.sqlite.SqliteWrapper;
import android.graphics.Bitmap;
import android.graphics.Typeface;
import android.media.AudioManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.Parcel;
import android.os.Parcelable;
import android.preference.PreferenceManager;
import android.provider.BaseColumns;
import android.provider.Telephony.Mms;
import android.provider.Telephony.MmsSms;
import android.provider.Telephony.Sms;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationCompat.WearableExtender;
import android.support.v4.app.NotificationManagerCompat;
import android.support.v4.app.RemoteInput;
import android.telephony.TelephonyManager;
import android.telephony.SubscriptionManager;
import android.telephony.SubscriptionInfo;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.StyleSpan;
import android.text.style.TextAppearanceSpan;
import android.util.Log;
import android.widget.Toast;

import com.android.contacts.common.util.BitmapUtil;
import com.android.internal.telephony.PhoneConstants;
import com.android.mms.LogTag;
import com.android.mms.MmsConfig;
import com.android.mms.R;
import com.android.mms.data.Contact;
import com.android.mms.data.Conversation;
import com.android.mms.data.WorkingMessage;
import com.android.mms.data.cm.CMConversationSettings;
import com.android.mms.model.SlideModel;
import com.android.mms.model.SlideshowModel;
import com.android.mms.quickmessage.QmMarkRead;
import com.android.mms.quickmessage.QuickMessagePopup;
import com.android.mms.quickmessage.QuickMessageWear;
import com.android.mms.ui.ComposeMessageActivity;
import com.android.mms.ui.ConversationList;
import com.android.mms.ui.MailBoxMessageContent;
import com.android.mms.ui.MailBoxMessageList;
import com.android.mms.ui.ManageSimMessages;
import com.android.mms.ui.MessageUtils;
import com.android.mms.ui.MessagingPreferenceActivity;
import com.android.mms.ui.MobilePaperShowActivity;
import com.android.mms.util.AddressUtils;
import com.android.mms.util.DownloadManager;
import com.android.mms.widget.MmsWidgetProvider;

import com.google.android.mms.MmsException;
import com.google.android.mms.pdu.EncodedStringValue;
import com.google.android.mms.pdu.GenericPdu;
import com.google.android.mms.pdu.MultimediaMessagePdu;
import com.google.android.mms.pdu.PduHeaders;
import com.google.android.mms.pdu.PduPersister;

/**
 * This class is used to update the notification indicator. It will check whether
 * there are unread messages. If yes, it would show the notification indicator,
 * otherwise, hide the indicator.
 */
public class MessagingNotification {

    private static final String TAG = LogTag.APP;
    private static final boolean DEBUG = false;

    public static final int NOTIFICATION_ID = 123;
    public static final int FULL_NOTIFICATION_ID = 125;
    public static final int MESSAGE_FAILED_NOTIFICATION_ID = 789;
    public static final int DOWNLOAD_FAILED_NOTIFICATION_ID = 531;
    private static final int ICC_NOTIFICATION_ID_BASE = 1000;
    /**
     * This is the volume at which to play the in-conversation notification sound,
     * expressed as a fraction of the system notification volume.
     */
    private static final float IN_CONVERSATION_NOTIFICATION_VOLUME = 0.25f;

    // This must be consistent with the column constants below.
    private static final String[] MMS_STATUS_PROJECTION = new String[] { Mms.THREAD_ID, Mms.DATE, Mms._ID,
            Mms.SUBJECT, Mms.SUBJECT_CHARSET, Mms.SUBSCRIPTION_ID };

    // This must be consistent with the column constants below.
    private static final String[] SMS_STATUS_PROJECTION = new String[] { Sms.THREAD_ID, Sms.DATE, Sms.ADDRESS,
            Sms.BODY, Sms.SUBSCRIPTION_ID, Sms._ID };

    private static final String[] MAILBOX_PROJECTION = new String[] { MmsSms.TYPE_DISCRIMINATOR_COLUMN,
            BaseColumns._ID, Sms.TYPE, Mms.MESSAGE_BOX };

    private static final String[] SMS_THREAD_ID_PROJECTION = new String[] { Sms.THREAD_ID };
    private static final String[] MMS_THREAD_ID_PROJECTION = new String[] { Mms.THREAD_ID };

    private static final int COLUMN_MMS_THREAD_ID = 0;
    private static final int COLUMN_MMS_DATE = 1;
    private static final int COLUMN_MMS_ID = 2;
    private static final int COLUMN_MMS_SUBJECT = 3;
    private static final int COLUMN_MMS_SUBJECT_CS = 4;
    private static final int COLUMN_MMS_SUB_ID = 5;

    private static final int COLUMN_SMS_THREAD_ID = 0;
    private static final int COLUMN_SMS_DATE = 1;
    private static final int COLUMN_SMS_ADDRESS = 2;
    private static final int COLUMN_SMS_BODY = 3;
    private static final int COLUMN_SMS_SUB_ID = 4;
    private static final int COLUMN_SMS_ID = 5;

    private static final int MAILBOX_MSG_TYPE = 0;
    private static final int MAILBOX_ID = 1;
    private static final int MAILBOX_SMS_TYPE = 2;
    private static final int MAILBOX_MMS_BOX = 3;

    private static final String NEW_INCOMING_SM_CONSTRAINT = "(" + Sms.TYPE + " = " + Sms.MESSAGE_TYPE_INBOX
            + " AND " + Sms.SEEN + " = 0)";

    private static final String NEW_DELIVERY_SM_CONSTRAINT = "(" + Sms.TYPE + " = " + Sms.MESSAGE_TYPE_SENT
            + " AND " + Sms.STATUS + " = " + Sms.STATUS_COMPLETE + ")";

    private static final String NEW_INCOMING_MM_CONSTRAINT = "(" + Mms.MESSAGE_BOX + "=" + Mms.MESSAGE_BOX_INBOX
            + " AND " + Mms.SEEN + "=0" + " AND (" + Mms.MESSAGE_TYPE + "=" + MESSAGE_TYPE_NOTIFICATION_IND + " OR "
            + Mms.MESSAGE_TYPE + "=" + MESSAGE_TYPE_RETRIEVE_CONF + "))";

    private static final NotificationInfoComparator INFO_COMPARATOR = new NotificationInfoComparator();

    private static final Uri UNDELIVERED_URI = Uri.parse("content://mms-sms/undelivered");
    private final static String UNDELIVERED_FLAG = "undelivered_flag";
    private final static String FAILED_DOWNLOAD_FLAG = "failed_download_flag";

    private final static String NOTIFICATION_DELETED_ACTION = "com.android.mms.NOTIFICATION_DELETED_ACTION";

    public static class OnDeletedReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
                Log.d(TAG, "[MessagingNotification] clear notification: mark all msgs seen");
            }

            Conversation.markAllConversationsAsSeen(context);
        }
    }

    public static final long THREAD_ALL = -1;
    public static final long THREAD_NONE = -2;
    /**
     * Keeps track of the thread ID of the conversation that's currently displayed to the user
     */
    private static long sCurrentlyDisplayedThreadId;
    private static final Object sCurrentlyDisplayedThreadLock = new Object();

    private static long sCurrentlyDisplayedMsgType;
    private static final Object sCurrentlyDisplayedMsgTypeLock = new Object();

    private static OnDeletedReceiver sNotificationDeletedReceiver = new OnDeletedReceiver();
    private static Intent sNotificationOnDeleteIntent;
    private static Handler sHandler = new Handler();
    private static PduPersister sPduPersister;
    private static final int MAX_BITMAP_DIMEN_DP = 360;
    private static float sScreenDensity;

    private static final int MAX_MESSAGES_TO_SHOW = 8; // the maximum number of new messages to
                                                       // show in a single notification.

    private MessagingNotification() {
    }

    public static void init(Context context) {
        // set up the intent filter for notification deleted action
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction(NOTIFICATION_DELETED_ACTION);

        // TODO: should we unregister when the app gets killed?
        context.registerReceiver(sNotificationDeletedReceiver, intentFilter);
        sPduPersister = PduPersister.getPduPersister(context);

        // initialize the notification deleted action
        sNotificationOnDeleteIntent = new Intent(NOTIFICATION_DELETED_ACTION);

        sScreenDensity = context.getResources().getDisplayMetrics().density;
    }

    /**
     * Specifies which message thread is currently being viewed by the user. New messages in that
     * thread will not generate a notification icon and will play the notification sound at a lower
     * volume. Make sure you set this to THREAD_NONE when the UI component that shows the thread is
     * no longer visible to the user (e.g. Activity.onPause(), etc.)
     * @param threadId The ID of the thread that the user is currently viewing. Pass THREAD_NONE
     *  if the user is not viewing a thread, or THREAD_ALL if the user is viewing the conversation
     *  list (note: that latter one has no effect as of this implementation)
     */
    public static void setCurrentlyDisplayedThreadId(long threadId) {
        synchronized (sCurrentlyDisplayedThreadLock) {
            sCurrentlyDisplayedThreadId = threadId;
            if (DEBUG) {
                Log.d(TAG, "setCurrentlyDisplayedThreadId: " + sCurrentlyDisplayedThreadId);
            }
        }
    }

    public static void setCurrentlyDisplayedMsgType(long msgType) {
        synchronized (sCurrentlyDisplayedMsgTypeLock) {
            sCurrentlyDisplayedMsgType = msgType;
            if (DEBUG) {
                Log.d(TAG, "sCurrentlyDisplayedMsgType: " + sCurrentlyDisplayedMsgType);
            }
        }
    }

    /**
     * Checks to see if there are any "unseen" messages or delivery
     * reports.  Shows the most recent notification if there is one.
     * Does its work and query in a worker thread.
     *
     * @param context the context to use
     */
    public static void nonBlockingUpdateNewMessageIndicator(final Context context, final long newMsgThreadId,
            final boolean isStatusMessage) {
        if (DEBUG) {
            Log.d(TAG,
                    "nonBlockingUpdateNewMessageIndicator: newMsgThreadId: " + newMsgThreadId
                            + " sCurrentlyDisplayedThreadId: " + sCurrentlyDisplayedThreadId
                            + " sCurrentlyDisplayedMsgType: " + sCurrentlyDisplayedMsgType);
        }
        new Thread(new Runnable() {
            @Override
            public void run() {
                blockingUpdateNewMessageIndicator(context, newMsgThreadId, isStatusMessage);
            }
        }, "MessagingNotification.nonBlockingUpdateNewMessageIndicator").start();
    }

    /**
     * Checks to see if there are any "unseen" messages or delivery
     * reports and builds a sorted (by delivery date) list of unread notifications.
     *
     * @param context the context to use
     * @param newMsgThreadId The thread ID of a new message that we're to notify about; if there's
     *  no new message, use THREAD_NONE. If we should notify about multiple or unknown thread IDs,
     *  use THREAD_ALL.
     * @param isStatusMessage
     */
    public static void blockingUpdateNewMessageIndicator(Context context, long newMsgThreadId,
            boolean isStatusMessage) {
        if (DEBUG) {
            Contact.logWithTrace(TAG, "blockingUpdateNewMessageIndicator: newMsgThreadId: " + newMsgThreadId);
        }
        final boolean isDefaultSmsApp = MmsConfig.isSmsEnabled(context);
        if (!isDefaultSmsApp) {
            cancelNotification(context, NOTIFICATION_ID);
            if (DEBUG || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
                Log.d(TAG,
                        "blockingUpdateNewMessageIndicator: not the default sms app - skipping " + "notification");
            }
            return;
        }

        // notificationSet is kept sorted by the incoming message delivery time, with the
        // most recent message first.
        SortedSet<NotificationInfo> notificationSet = new TreeSet<NotificationInfo>(INFO_COMPARATOR);

        Set<Long> threads = new HashSet<Long>(4);

        addMmsNotificationInfos(context, threads, notificationSet);
        addSmsNotificationInfos(context, threads, notificationSet);

        if (notificationSet.isEmpty()) {
            if (DEBUG) {
                Log.d(TAG, "blockingUpdateNewMessageIndicator: notificationSet is empty, "
                        + "canceling existing notifications");
            }
            cancelNotification(context, NOTIFICATION_ID);
        } else {
            if (DEBUG || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
                Log.d(TAG, "blockingUpdateNewMessageIndicator: count=" + notificationSet.size()
                        + ", newMsgThreadId=" + newMsgThreadId);
            }

            if (isInCurrentConversation(newMsgThreadId, threads)) {
                if (DEBUG) {
                    Log.d(TAG,
                            "blockingUpdateNewMessageIndicator: newMsgThreadId == "
                                    + "sCurrentlyDisplayedThreadId so NOT showing notification,"
                                    + " but playing soft sound. threadId: " + newMsgThreadId);
                }
                playInConversationNotificationSound(context, newMsgThreadId);
                return;
            }
            updateNotification(context, newMsgThreadId, threads.size(), notificationSet);
        }

        // And deals with delivery reports (which use Toasts). It's safe to call in a worker
        // thread because the toast will eventually get posted to a handler.
        MmsSmsDeliveryInfo delivery = getSmsNewDeliveryInfo(context);
        if (delivery != null) {
            delivery.deliver(context, isStatusMessage);
        }

        notificationSet.clear();
        threads.clear();
    }

    private static boolean isInCurrentConversation(long newMsgThreadId, Set<Long> threads) {
        if (MessageUtils.isMailboxMode()) {
            // For mail box mode, only message with the same tpye will not show the notification.
            long newMsgType = (newMsgThreadId == THREAD_NONE) ? MailBoxMessageList.TYPE_INVALID
                    : MailBoxMessageList.TYPE_INBOX;
            synchronized (sCurrentlyDisplayedMsgTypeLock) {
                return newMsgType == sCurrentlyDisplayedMsgType;
            }
        } else {
            // For conversation mode, only incomming unread message with the same valid thread id
            // will not show the notification.
            synchronized (sCurrentlyDisplayedThreadLock) {
                return (newMsgThreadId > 0 && newMsgThreadId == sCurrentlyDisplayedThreadId
                        && threads.contains(newMsgThreadId));
            }
        }
    }

    public static void blockingUpdateNewIccMessageIndicator(Context context, String address, String message,
            int subId, long timeMillis) {
        final Notification.Builder noti = new Notification.Builder(context).setWhen(timeMillis);
        Contact contact = Contact.get(address, false);
        NotificationInfo info = getNewIccMessageNotificationInfo(context, true /* isSms */, address, message,
                null /* subject */, subId, timeMillis, null /* attachmentBitmap */, contact, WorkingMessage.TEXT);
        noti.setSmallIcon(R.drawable.stat_notify_sms);
        NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);

        //        TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(context);
        // Update the notification.
        PendingIntent pendingIntent;
        if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
            pendingIntent = PendingIntent.getActivity(context, 0, info.mClickIntent,
                    PendingIntent.FLAG_UPDATE_CURRENT);
        } else {
            // Use requestCode to avoid updating all intents of previous notifications
            pendingIntent = PendingIntent.getActivity(context, ICC_NOTIFICATION_ID_BASE + subId, info.mClickIntent,
                    PendingIntent.FLAG_UPDATE_CURRENT);
        }
        String title = info.mTitle;
        noti.setContentTitle(title).setContentIntent(pendingIntent)
                //taskStackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT))
                .setCategory(Notification.CATEGORY_MESSAGE).setPriority(Notification.PRIORITY_DEFAULT);

        int defaults = 0;
        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
        boolean vibrate = false;
        if (sp.contains(MessagingPreferenceActivity.NOTIFICATION_VIBRATE)) {
            // The most recent change to the vibrate preference is to store a boolean
            // value in NOTIFICATION_VIBRATE. If prefs contain that preference, use that
            // first.
            vibrate = sp.getBoolean(MessagingPreferenceActivity.NOTIFICATION_VIBRATE, false);
        } else if (sp.contains(MessagingPreferenceActivity.NOTIFICATION_VIBRATE_WHEN)) {
            // This is to support the pre-JellyBean MR1.1 version of vibrate preferences
            // when vibrate was a tri-state setting. As soon as the user opens the Messaging
            // app's settings, it will migrate this setting from NOTIFICATION_VIBRATE_WHEN
            // to the boolean value stored in NOTIFICATION_VIBRATE.
            String vibrateWhen = sp.getString(MessagingPreferenceActivity.NOTIFICATION_VIBRATE_WHEN, null);
            vibrate = "always".equals(vibrateWhen);
        }
        if (vibrate) {
            defaults |= Notification.DEFAULT_VIBRATE;
        }
        String ringtoneStr = sp.getString(MessagingPreferenceActivity.NOTIFICATION_RINGTONE, null);
        noti.setSound(TextUtils.isEmpty(ringtoneStr) ? null : Uri.parse(ringtoneStr));
        Log.d(TAG, "blockingUpdateNewIccMessageIndicator: adding sound to the notification");

        defaults |= Notification.DEFAULT_LIGHTS;

        noti.setDefaults(defaults);

        // set up delete intent
        noti.setDeleteIntent(PendingIntent.getBroadcast(context, 0, sNotificationOnDeleteIntent, 0));

        final Notification notification;
        // This sets the text for the collapsed form:
        noti.setContentText(info.formatBigMessage(context));

        if (info.mAttachmentBitmap != null) {
            // The message has a picture, show that

            notification = new Notification.BigPictureStyle(noti).bigPicture(info.mAttachmentBitmap)
                    // This sets the text for the expanded picture form:
                    .setSummaryText(info.formatPictureMessage(context)).build();
        } else {
            // Show a single notification -- big style with the text of the whole message
            notification = new Notification.BigTextStyle(noti).bigText(info.formatBigMessage(context)).build();
        }

        notifyUserIfFullScreen(context, title);
        nm.notify(ICC_NOTIFICATION_ID_BASE + subId, notification);
    }

    /**
     * Play the in-conversation notification sound (it's the regular notification sound, but
     * played at half-volume
     */
    private static void playInConversationNotificationSound(Context context, long newThreadId) {
        CMConversationSettings conversationSettings = CMConversationSettings.getOrNew(context, newThreadId);
        String ringtoneStr = conversationSettings.getNotificationTone();
        if (TextUtils.isEmpty(ringtoneStr)) {
            // Nothing to play
            return;
        }
        Uri ringtoneUri = Uri.parse(ringtoneStr);
        final NotificationPlayer player = new NotificationPlayer(LogTag.APP);
        player.play(context, ringtoneUri, false, AudioManager.STREAM_NOTIFICATION,
                IN_CONVERSATION_NOTIFICATION_VOLUME);

        // Stop the sound after five seconds to handle continuous ringtones
        sHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                player.stop();
            }
        }, 5000);
    }

    /**
     * Updates all pending notifications, clearing or updating them as
     * necessary.
     */
    public static void blockingUpdateAllNotifications(final Context context, long threadId) {
        if (DEBUG) {
            Contact.logWithTrace(TAG, "blockingUpdateAllNotifications: newMsgThreadId: " + threadId);
        }
        nonBlockingUpdateNewMessageIndicator(context, threadId, false);
        nonBlockingUpdateSendFailedNotification(context);
        updateDownloadFailedNotification(context);
        MmsWidgetProvider.notifyDatasetChanged(context);
    }

    private static final class MmsSmsDeliveryInfo {
        public CharSequence mTicker;
        public long mTimeMillis;

        public MmsSmsDeliveryInfo(CharSequence ticker, long timeMillis) {
            mTicker = ticker;
            mTimeMillis = timeMillis;
        }

        public void deliver(Context context, boolean isStatusMessage) {
            updateDeliveryNotification(context, isStatusMessage, mTicker, mTimeMillis);
        }
    }

    public static final class NotificationInfo implements Parcelable {
        public final Intent mClickIntent;
        public final String mMessage;
        public final CharSequence mSimName;
        public final CharSequence mTicker;
        public final long mTimeMillis;
        public final String mTitle;
        public final Bitmap mAttachmentBitmap;
        public final Contact mSender;
        public final boolean mIsSms;
        public final int mAttachmentType;
        public final String mSubject;
        public final long mThreadId;

        /**
         * @param isSms true if sms, false if mms
         * @param clickIntent where to go when the user taps the notification
         * @param message for a single message, this is the message text
         * @param subject text of mms subject
         * @param ticker text displayed ticker-style across the notification, typically formatted
         * as sender: message
         * @param simName name of receiving SIM or null if single-SIM
         * @param timeMillis date the message was received
         * @param title for a single message, this is the sender
         * @param attachmentBitmap a bitmap of an attachment, such as a picture or video
         * @param sender contact of the sender
         * @param attachmentType of the mms attachment
         * @param threadId thread this message belongs to
         */
        public NotificationInfo(boolean isSms, Intent clickIntent, String message, String subject,
                CharSequence simName, CharSequence ticker, long timeMillis, String title, Bitmap attachmentBitmap,
                Contact sender, int attachmentType, long threadId) {
            mIsSms = isSms;
            mClickIntent = clickIntent;
            mMessage = message;
            mSubject = subject;
            mSimName = simName;
            mTicker = ticker;
            mTimeMillis = timeMillis;
            mTitle = title;
            mAttachmentBitmap = attachmentBitmap;
            mSender = sender;
            mAttachmentType = attachmentType;
            mThreadId = threadId;
        }

        public long getTime() {
            return mTimeMillis;
        }

        // This is the message string used in bigText and bigPicture notifications.
        public CharSequence formatBigMessage(Context context) {
            final TextAppearanceSpan notificationSubjectSpan = new TextAppearanceSpan(context,
                    R.style.NotificationPrimaryText);

            // Change multiple newlines (with potential white space between), into a single new line
            final String message = !TextUtils.isEmpty(mMessage) ? mMessage.replaceAll("\\n\\s+", "\n") : "";

            SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder();
            if (!TextUtils.isEmpty(mSubject)) {
                spannableStringBuilder.append(mSubject);
                spannableStringBuilder.setSpan(notificationSubjectSpan, 0, mSubject.length(), 0);
            }
            if (mAttachmentType > WorkingMessage.TEXT) {
                if (spannableStringBuilder.length() > 0) {
                    spannableStringBuilder.append('\n');
                }
                spannableStringBuilder.append(getAttachmentTypeString(context, mAttachmentType));
            }
            if (mMessage != null) {
                if (spannableStringBuilder.length() > 0) {
                    spannableStringBuilder.append('\n');
                }
                spannableStringBuilder.append(mMessage);
            }
            return spannableStringBuilder;
        }

        // This is the message string used in each line of an inboxStyle notification.
        public CharSequence formatInboxMessage(Context context) {
            final TextAppearanceSpan notificationSenderSpan = new TextAppearanceSpan(context,
                    R.style.NotificationPrimaryText);

            final TextAppearanceSpan notificationSubjectSpan = new TextAppearanceSpan(context,
                    R.style.NotificationSubjectText);

            // Change multiple newlines (with potential white space between), into a single new line
            final String message = !TextUtils.isEmpty(mMessage) ? mMessage.replaceAll("\\n\\s+", "\n") : "";

            SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder();
            final String sender = mSender.getName();
            if (!TextUtils.isEmpty(sender)) {
                spannableStringBuilder.append(sender);
                spannableStringBuilder.setSpan(notificationSenderSpan, 0, sender.length(), 0);
            }
            String separator = context.getString(R.string.notification_separator);
            if (!mIsSms) {
                if (!TextUtils.isEmpty(mSubject)) {
                    if (spannableStringBuilder.length() > 0) {
                        spannableStringBuilder.append(separator);
                    }
                    int start = spannableStringBuilder.length();
                    spannableStringBuilder.append(mSubject);
                    spannableStringBuilder.setSpan(notificationSubjectSpan, start, start + mSubject.length(), 0);
                }
                if (mAttachmentType > WorkingMessage.TEXT) {
                    if (spannableStringBuilder.length() > 0) {
                        spannableStringBuilder.append(separator);
                    }
                    spannableStringBuilder.append(getAttachmentTypeString(context, mAttachmentType));
                }
            }
            if (message.length() > 0) {
                if (spannableStringBuilder.length() > 0) {
                    spannableStringBuilder.append(separator);
                }
                int start = spannableStringBuilder.length();
                spannableStringBuilder.append(message);
                spannableStringBuilder.setSpan(notificationSubjectSpan, start, start + message.length(), 0);
            }
            return spannableStringBuilder;
        }

        // This is the summary string used in bigPicture notifications.
        public CharSequence formatPictureMessage(Context context) {
            final TextAppearanceSpan notificationSubjectSpan = new TextAppearanceSpan(context,
                    R.style.NotificationPrimaryText);

            // Change multiple newlines (with potential white space between), into a single new line
            final String message = !TextUtils.isEmpty(mMessage) ? mMessage.replaceAll("\\n\\s+", "\n") : "";

            // Show the subject or the message (if no subject)
            SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder();
            if (!TextUtils.isEmpty(mSubject)) {
                spannableStringBuilder.append(mSubject);
                spannableStringBuilder.setSpan(notificationSubjectSpan, 0, mSubject.length(), 0);
            }
            if (message.length() > 0 && spannableStringBuilder.length() == 0) {
                spannableStringBuilder.append(message);
                spannableStringBuilder.setSpan(notificationSubjectSpan, 0, message.length(), 0);
            }
            return spannableStringBuilder;
        }

        @Override
        public int describeContents() {
            return 0;
        }

        @Override
        public void writeToParcel(Parcel arg0, int arg1) {
            arg0.writeByte((byte) (mIsSms ? 1 : 0));
            arg0.writeParcelable(mClickIntent, 0);
            arg0.writeString(mMessage);
            arg0.writeString(mSubject);
            arg0.writeCharSequence(mSimName);
            arg0.writeCharSequence(mTicker);
            arg0.writeLong(mTimeMillis);
            arg0.writeString(mTitle);
            arg0.writeParcelable(mAttachmentBitmap, 0);
            arg0.writeInt(mAttachmentType);
            arg0.writeLong(mThreadId);
        }

        public NotificationInfo(Parcel in) {
            mIsSms = in.readByte() == 1;
            mClickIntent = in.readParcelable(Intent.class.getClassLoader());
            mMessage = in.readString();
            mSubject = in.readString();
            mSimName = in.readCharSequence();
            mTicker = in.readCharSequence();
            mTimeMillis = in.readLong();
            mTitle = in.readString();
            mAttachmentBitmap = in.readParcelable(Bitmap.class.getClassLoader());
            mSender = null;
            mAttachmentType = in.readInt();
            mThreadId = in.readLong();
        }

        public static final Parcelable.Creator<NotificationInfo> CREATOR = new Parcelable.Creator<NotificationInfo>() {
            public NotificationInfo createFromParcel(Parcel in) {
                return new NotificationInfo(in);
            }

            public NotificationInfo[] newArray(int size) {
                return new NotificationInfo[size];
            }
        };

    }

    // Return a formatted string with all the sender names separated by commas.
    private static CharSequence formatSenders(Context context, ArrayList<NotificationInfo> senders) {
        final TextAppearanceSpan notificationSenderSpan = new TextAppearanceSpan(context,
                R.style.NotificationPrimaryText);

        String separator = context.getString(R.string.enumeration_comma); // ", "
        SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder();
        int len = senders.size();
        for (int i = 0; i < len; i++) {
            if (i > 0) {
                spannableStringBuilder.append(separator);
            }
            spannableStringBuilder.append(senders.get(i).mSender.getName());
        }
        spannableStringBuilder.setSpan(notificationSenderSpan, 0, spannableStringBuilder.length(), 0);
        return spannableStringBuilder;
    }

    // Return a formatted string with the attachmentType spelled out as a string. For
    // no attachment (or just text), return null.
    private static CharSequence getAttachmentTypeString(Context context, int attachmentType) {
        final TextAppearanceSpan notificationAttachmentSpan = new TextAppearanceSpan(context,
                R.style.NotificationSecondaryText);
        int id = 0;
        switch (attachmentType) {
        case WorkingMessage.AUDIO:
            id = R.string.attachment_audio;
            break;
        case WorkingMessage.VIDEO:
            id = R.string.attachment_video;
            break;
        case WorkingMessage.SLIDESHOW:
            id = R.string.attachment_slideshow;
            break;
        case WorkingMessage.IMAGE:
            id = R.string.attachment_picture;
            break;
        }
        if (id > 0) {
            final SpannableString spannableString = new SpannableString(context.getString(id));
            spannableString.setSpan(notificationAttachmentSpan, 0, spannableString.length(), 0);
            return spannableString;
        }
        return null;
    }

    /**
     *
     * Sorts by the time a notification was received in descending order -- newer first.
     *
     */
    private static final class NotificationInfoComparator implements Comparator<NotificationInfo> {
        @Override
        public int compare(NotificationInfo info1, NotificationInfo info2) {
            return Long.signum(info2.getTime() - info1.getTime());
        }
    }

    private static final void addMmsNotificationInfos(Context context, Set<Long> threads,
            SortedSet<NotificationInfo> notificationSet) {
        ContentResolver resolver = context.getContentResolver();

        // This query looks like this when logged:
        // I/Database(  147): elapsedTime4Sql|/data/data/com.android.providers.telephony/databases/
        // mmssms.db|0.362 ms|SELECT thread_id, date, _id, sub, sub_cs FROM pdu WHERE ((msg_box=1
        // AND seen=0 AND (m_type=130 OR m_type=132))) ORDER BY date desc

        Cursor cursor = SqliteWrapper.query(context, resolver, Mms.CONTENT_URI, MMS_STATUS_PROJECTION,
                NEW_INCOMING_MM_CONSTRAINT, null, Mms.DATE + " desc");

        if (cursor == null) {
            return;
        }

        try {
            while (cursor.moveToNext()) {

                long msgId = cursor.getLong(COLUMN_MMS_ID);
                Uri msgUri = Mms.CONTENT_URI.buildUpon().appendPath(Long.toString(msgId)).build();
                String address = AddressUtils.getFrom(context, msgUri);

                Contact contact = Contact.get(address, false);
                if (contact.getSendToVoicemail()) {
                    // don't notify, skip this one
                    continue;
                }

                String subject = getMmsSubject(cursor.getString(COLUMN_MMS_SUBJECT),
                        cursor.getInt(COLUMN_MMS_SUBJECT_CS));
                subject = MessageUtils.cleanseMmsSubject(context, subject);

                long threadId = cursor.getLong(COLUMN_MMS_THREAD_ID);
                long timeMillis = cursor.getLong(COLUMN_MMS_DATE) * 1000;
                int subId = cursor.getInt(COLUMN_MMS_SUB_ID);

                if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
                    Log.d(TAG, "addMmsNotificationInfos: count=" + cursor.getCount() + ", addr = " + address
                            + ", thread_id=" + threadId);
                }

                // Extract the message and/or an attached picture from the first slide
                Bitmap attachedPicture = null;
                String messageBody = null;
                int attachmentType = WorkingMessage.TEXT;
                try {
                    GenericPdu pdu = sPduPersister.load(msgUri);
                    if (pdu != null && pdu instanceof MultimediaMessagePdu) {
                        SlideshowModel slideshow = SlideshowModel.createFromPduBody(context,
                                ((MultimediaMessagePdu) pdu).getBody());
                        attachmentType = getAttachmentType(slideshow);
                        SlideModel firstSlide = slideshow.get(0);
                        if (firstSlide != null) {
                            if (firstSlide.hasImage()) {
                                int maxDim = dp2Pixels(MAX_BITMAP_DIMEN_DP);
                                attachedPicture = firstSlide.getImage().getBitmap(maxDim, maxDim);
                            }
                            if (firstSlide.hasText()) {
                                messageBody = firstSlide.getText().getText();
                            }
                        }
                    }
                } catch (final MmsException e) {
                    Log.e(TAG, "MmsException loading uri: " + msgUri, e);
                    continue; // skip this bad boy -- don't generate an empty notification
                }

                NotificationInfo info = getNewMessageNotificationInfo(context, false /* isSms */, address,
                        messageBody, subject, threadId, subId, timeMillis, attachedPicture, contact,
                        attachmentType);
                if (MessageUtils.isMailboxMode()) {
                    info.mClickIntent.setData(msgUri);
                }
                notificationSet.add(info);

                threads.add(threadId);
            }
        } finally {
            cursor.close();
        }
    }

    // Look at the passed in slideshow and determine what type of attachment it is.
    private static int getAttachmentType(SlideshowModel slideshow) {
        int slideCount = slideshow.size();

        if (slideCount == 0) {
            return WorkingMessage.TEXT;
        } else if (slideCount > 1) {
            return WorkingMessage.SLIDESHOW;
        } else {
            SlideModel slide = slideshow.get(0);
            if (slide.hasImage()) {
                return WorkingMessage.IMAGE;
            } else if (slide.hasVideo()) {
                return WorkingMessage.VIDEO;
            } else if (slide.hasAudio()) {
                return WorkingMessage.AUDIO;
            }
        }
        return WorkingMessage.TEXT;
    }

    private static final int dp2Pixels(int dip) {
        return (int) (dip * sScreenDensity + 0.5f);
    }

    private static final MmsSmsDeliveryInfo getSmsNewDeliveryInfo(Context context) {
        ContentResolver resolver = context.getContentResolver();
        Cursor cursor = SqliteWrapper.query(context, resolver, Sms.CONTENT_URI, SMS_STATUS_PROJECTION,
                NEW_DELIVERY_SM_CONSTRAINT, null, Sms.DATE);

        if (cursor == null) {
            return null;
        }

        try {
            if (!cursor.moveToLast()) {
                return null;
            }

            String address = cursor.getString(COLUMN_SMS_ADDRESS);
            long timeMillis = 3000;

            Contact contact = Contact.get(address, false);
            String name = contact.getNameAndNumber();

            return new MmsSmsDeliveryInfo(context.getString(R.string.delivery_toast_body, name), timeMillis);

        } finally {
            cursor.close();
        }
    }

    private static final void addSmsNotificationInfos(Context context, Set<Long> threads,
            SortedSet<NotificationInfo> notificationSet) {
        ContentResolver resolver = context.getContentResolver();
        Cursor cursor = SqliteWrapper.query(context, resolver, Sms.CONTENT_URI, SMS_STATUS_PROJECTION,
                NEW_INCOMING_SM_CONSTRAINT, null, Sms.DATE + " desc");

        if (cursor == null) {
            return;
        }

        try {
            while (cursor.moveToNext()) {
                String address = cursor.getString(COLUMN_SMS_ADDRESS);
                if (MessageUtils.isWapPushNumber(address)) {
                    String[] mAddresses = address.split(":");
                    address = mAddresses[context.getResources().getInteger(R.integer.wap_push_address_index)];
                }

                Contact contact = Contact.get(address, false);
                if (contact.getSendToVoicemail()) {
                    // don't notify, skip this one
                    continue;
                }

                String message = cursor.getString(COLUMN_SMS_BODY);
                long threadId = cursor.getLong(COLUMN_SMS_THREAD_ID);
                long timeMillis = cursor.getLong(COLUMN_SMS_DATE);
                int subId = cursor.getInt(COLUMN_SMS_SUB_ID);
                String msgId = cursor.getString(COLUMN_SMS_ID);

                if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
                    Log.d(TAG, "addSmsNotificationInfos: count=" + cursor.getCount() + ", addr=" + address
                            + ", thread_id=" + threadId);
                }

                NotificationInfo info = getNewMessageNotificationInfo(context, true /* isSms */, address, message,
                        null /* subject */, threadId, subId, timeMillis, null /* attachmentBitmap */, contact,
                        WorkingMessage.TEXT);
                if (MessageUtils.isMailboxMode()) {
                    info.mClickIntent.setData(Uri.withAppendedPath(Sms.CONTENT_URI, msgId));
                }
                notificationSet.add(info);

                threads.add(threadId);
                threads.add(cursor.getLong(COLUMN_SMS_THREAD_ID));
            }
        } finally {
            cursor.close();
        }
    }

    private static final NotificationInfo getNewMessageNotificationInfo(Context context, boolean isSms,
            String address, String message, String subject, long threadId, int subId, long timeMillis,
            Bitmap attachmentBitmap, Contact contact, int attachmentType) {
        Intent clickIntent = getClickIntent(context, isSms, threadId);

        String senderInfo = buildTickerMessage(context, address, null, null).toString();
        String senderInfoName = senderInfo.substring(0, senderInfo.length());
        CharSequence simName = MessageUtils.getSimName(context, subId);
        CharSequence ticker = buildTickerMessage(context, address, subject, message);

        return new NotificationInfo(isSms, clickIntent, message, subject, simName, ticker, timeMillis,
                senderInfoName, attachmentBitmap, contact, attachmentType, threadId);
    }

    private static final Intent getClickIntent(Context context, boolean isSms, long threadId) {
        Intent intent;
        if (!MessageUtils.isMailboxMode()) {
            intent = ComposeMessageActivity.createIntent(context, threadId);
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP
                    | Intent.FLAG_ACTIVITY_CLEAR_TOP);
            intent.putExtra(MessageUtils.EXTRA_KEY_NEW_MESSAGE_NEED_RELOAD, true);
        } else if (isSms) {
            intent = new Intent(context, MailBoxMessageContent.class);
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        } else if (DownloadManager.getInstance().isAuto()) {
            intent = new Intent(context, MobilePaperShowActivity.class);
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        } else {
            // Else case: for MMS not downloaded.
            intent = new Intent(context, MailBoxMessageList.class);
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP
                    | Intent.FLAG_ACTIVITY_CLEAR_TOP);
            intent.putExtra(MessageUtils.MAIL_BOX_ID, MailBoxMessageList.TYPE_INBOX);
        }
        return intent;
    }

    private static final NotificationInfo getNewIccMessageNotificationInfo(Context context, boolean isSms,
            String address, String message, String subject, int subId, long timeMillis, Bitmap attachmentBitmap,
            Contact contact, int attachmentType) {
        Intent clickIntent = new Intent(context, ManageSimMessages.class);
        clickIntent.setFlags(
                Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);

        clickIntent.putExtra(PhoneConstants.PHONE_KEY, SubscriptionManager.getPhoneId(subId));
        String senderInfo = buildTickerMessage(context, address, null, null).toString();
        String senderInfoName = senderInfo.substring(0, senderInfo.length());
        CharSequence simName = MessageUtils.getSimName(context, subId);
        CharSequence ticker = buildTickerMessage(context, address, subject, message);

        return new NotificationInfo(isSms, clickIntent, message, subject, simName, ticker, timeMillis,
                senderInfoName, attachmentBitmap, contact, attachmentType, 0);
    }

    public static void cancelNotification(Context context, int notificationId) {
        NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);

        Log.d(TAG, "cancelNotification");
        nm.cancel(notificationId);
    }

    private static void updateDeliveryNotification(final Context context, boolean isStatusMessage,
            final CharSequence message, final long timeMillis) {
        if (!isStatusMessage) {
            return;
        }

        if (!MessagingPreferenceActivity.getNotificationEnabled(context)) {
            return;
        }

        sHandler.post(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(context, message, (int) timeMillis).show();
            }
        });
    }

    /**
     * updateNotification is *the* main function for building the actual notification handed to
     * the NotificationManager
     * @param context
     * @param newThreadId the new thread id
     * @param uniqueThreadCount
     * @param notificationSet the set of notifications to display
     */
    private static void updateNotification(Context context, long newThreadId, int uniqueThreadCount,
            SortedSet<NotificationInfo> notificationSet) {
        boolean isNew = newThreadId != THREAD_NONE;
        CMConversationSettings conversationSettings = CMConversationSettings.getOrNew(context, newThreadId);

        // If the user has turned off notifications in settings, don't do any notifying.
        if ((isNew && !conversationSettings.getNotificationEnabled())
                || !MessagingPreferenceActivity.getNotificationEnabled(context)) {
            if (DEBUG) {
                Log.d(TAG, "updateNotification: notifications turned off in prefs, bailing");
            }
            return;
        }

        // Figure out what we've got -- whether all sms's, mms's, or a mixture of both.
        final int messageCount = notificationSet.size();
        NotificationInfo mostRecentNotification = notificationSet.first();

        final NotificationCompat.Builder noti = new NotificationCompat.Builder(context)
                .setWhen(mostRecentNotification.mTimeMillis);

        if (isNew) {
            noti.setTicker(mostRecentNotification.mTicker);
        }

        // If we have more than one unique thread, change the title (which would
        // normally be the contact who sent the message) to a generic one that
        // makes sense for multiple senders, and change the Intent to take the
        // user to the conversation list instead of the specific thread.

        // Cases:
        //   1) single message from single thread - intent goes to ComposeMessageActivity
        //   2) multiple messages from single thread - intent goes to ComposeMessageActivity
        //   3) messages from multiple threads - intent goes to ConversationList

        final Resources res = context.getResources();
        String title = null;
        Bitmap avatar = null;
        PendingIntent pendingIntent = null;
        boolean isMultiNewMessages = MessageUtils.isMailboxMode() ? messageCount > 1 : uniqueThreadCount > 1;
        if (isMultiNewMessages) { // messages from multiple threads
            Intent mainActivityIntent = getMultiThreadsViewIntent(context);
            pendingIntent = PendingIntent.getActivity(context, 0, mainActivityIntent,
                    PendingIntent.FLAG_UPDATE_CURRENT);
            title = context.getString(R.string.message_count_notification, messageCount);
        } else { // same thread, single or multiple messages
            title = mostRecentNotification.mTitle;
            avatar = mostRecentNotification.mSender.getAvatar(context);
            noti.setSubText(mostRecentNotification.mSimName); // no-op in single SIM case
            if (avatar != null) {
                // Show the sender's avatar as the big icon. Contact bitmaps are 96x96 so we
                // have to scale 'em up to 128x128 to fill the whole notification large icon.
                final int idealIconHeight = res
                        .getDimensionPixelSize(android.R.dimen.notification_large_icon_height);
                final int idealIconWidth = res.getDimensionPixelSize(android.R.dimen.notification_large_icon_width);
                noti.setLargeIcon(BitmapUtil.getRoundedBitmap(avatar, idealIconWidth, idealIconHeight));
            }

            pendingIntent = PendingIntent.getActivity(context, 0, mostRecentNotification.mClickIntent,
                    PendingIntent.FLAG_UPDATE_CURRENT);
        }
        // Always have to set the small icon or the notification is ignored
        noti.setSmallIcon(R.drawable.stat_notify_sms);

        NotificationManagerCompat nm = NotificationManagerCompat.from(context);

        // Update the notification.
        noti.setContentTitle(title).setContentIntent(pendingIntent)
                .setColor(context.getResources().getColor(R.color.mms_theme_color))
                .setCategory(Notification.CATEGORY_MESSAGE).setPriority(Notification.PRIORITY_DEFAULT); // TODO: set based on contact coming
                                                                                                                                                                                                                                      // from a favorite.

        // Tag notification with all senders.
        for (NotificationInfo info : notificationSet) {
            Uri peopleReferenceUri = info.mSender.getPeopleReferenceUri();
            if (peopleReferenceUri != null) {
                noti.addPerson(peopleReferenceUri.toString());
            }
        }

        int defaults = 0;

        if (isNew) {
            SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);

            if (conversationSettings.getVibrateEnabled()) {
                String pattern = conversationSettings.getVibratePattern();

                if (!TextUtils.isEmpty(pattern)) {
                    noti.setVibrate(parseVibratePattern(pattern));
                } else {
                    defaults |= Notification.DEFAULT_VIBRATE;
                }
            }

            String ringtoneStr = conversationSettings.getNotificationTone();
            noti.setSound(TextUtils.isEmpty(ringtoneStr) ? null : Uri.parse(ringtoneStr));
            Log.d(TAG, "updateNotification: new message, adding sound to the notification");
        }

        defaults |= Notification.DEFAULT_LIGHTS;

        noti.setDefaults(defaults);

        // set up delete intent
        noti.setDeleteIntent(PendingIntent.getBroadcast(context, 0, sNotificationOnDeleteIntent, 0));

        // See if QuickMessage pop-up support is enabled in preferences
        boolean qmPopupEnabled = MessagingPreferenceActivity.getQuickMessageEnabled(context);

        // Set up the QuickMessage intent
        Intent qmIntent = null;
        if (mostRecentNotification.mIsSms) {
            // QuickMessage support is only for SMS
            qmIntent = new Intent();
            qmIntent.setClass(context, QuickMessagePopup.class);
            qmIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP
                    | Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
            qmIntent.putExtra(QuickMessagePopup.SMS_FROM_NAME_EXTRA, mostRecentNotification.mSender.getName());
            qmIntent.putExtra(QuickMessagePopup.SMS_FROM_NUMBER_EXTRA, mostRecentNotification.mSender.getNumber());
            qmIntent.putExtra(QuickMessagePopup.SMS_NOTIFICATION_OBJECT_EXTRA, mostRecentNotification);
        }

        // Start getting the notification ready
        final Notification notification;

        //Create a WearableExtender to add actions too
        WearableExtender wearableExtender = new WearableExtender();

        if (messageCount == 1 || uniqueThreadCount == 1) {
            // Add the Quick Reply action only if the pop-up won't be shown already
            if (!qmPopupEnabled && qmIntent != null) {

                // This is a QR, we should show the keyboard when the user taps to reply
                qmIntent.putExtra(QuickMessagePopup.QR_SHOW_KEYBOARD_EXTRA, true);

                // Create the pending intent and add it to the notification
                CharSequence qmText = context.getText(R.string.menu_reply);
                PendingIntent qmPendingIntent = PendingIntent.getActivity(context, 0, qmIntent,
                        PendingIntent.FLAG_UPDATE_CURRENT);
                noti.addAction(R.drawable.ic_reply, qmText, qmPendingIntent);

                //Wearable
                noti.extend(wearableExtender.addAction(
                        new NotificationCompat.Action.Builder(R.drawable.ic_reply, qmText, qmPendingIntent)
                                .build()));
            }

            // Add the 'Mark as read' action
            CharSequence markReadText = context.getText(R.string.qm_mark_read);
            Intent mrIntent = new Intent();
            mrIntent.setClass(context, QmMarkRead.class);
            mrIntent.putExtra(QmMarkRead.SMS_THREAD_ID, mostRecentNotification.mThreadId);
            PendingIntent mrPendingIntent = PendingIntent.getBroadcast(context, 0, mrIntent,
                    PendingIntent.FLAG_UPDATE_CURRENT);
            noti.addAction(R.drawable.ic_mark_read, markReadText, mrPendingIntent);

            // Add the Call action
            CharSequence callText = context.getText(R.string.menu_call);
            Intent callIntent = new Intent(Intent.ACTION_CALL);
            callIntent.setData(Uri.parse("tel:" + mostRecentNotification.mSender.getNumber()));
            PendingIntent callPendingIntent = PendingIntent.getActivity(context, 0, callIntent,
                    PendingIntent.FLAG_UPDATE_CURRENT);
            noti.addAction(R.drawable.ic_menu_call, callText, callPendingIntent);

            //Wearable
            noti.extend(wearableExtender.addAction(
                    new NotificationCompat.Action.Builder(R.drawable.ic_menu_call, callText, callPendingIntent)
                            .build()));

            //Set up remote input
            String replyLabel = context.getString(R.string.qm_wear_voice_reply);
            RemoteInput remoteInput = new RemoteInput.Builder(QuickMessageWear.EXTRA_VOICE_REPLY)
                    .setLabel(replyLabel).build();
            //Set up pending intent for voice reply
            Intent voiceReplyIntent = new Intent(context, QuickMessageWear.class);
            voiceReplyIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP
                    | Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
            voiceReplyIntent.putExtra(QuickMessageWear.SMS_CONATCT, mostRecentNotification.mSender.getName());
            voiceReplyIntent.putExtra(QuickMessageWear.SMS_SENDER, mostRecentNotification.mSender.getNumber());
            voiceReplyIntent.putExtra(QuickMessageWear.SMS_THEAD_ID, mostRecentNotification.mThreadId);
            PendingIntent voiceReplyPendingIntent = PendingIntent.getActivity(context, 0, voiceReplyIntent,
                    PendingIntent.FLAG_UPDATE_CURRENT);

            //Wearable voice reply action
            NotificationCompat.Action action = new NotificationCompat.Action.Builder(R.drawable.ic_reply,
                    context.getString(R.string.qm_wear_reply_by_voice), voiceReplyPendingIntent)
                            .addRemoteInput(remoteInput).build();
            noti.extend(wearableExtender.addAction(action));
        }

        if (messageCount == 1) {
            // We've got a single message

            // This sets the text for the collapsed form:
            noti.setContentText(mostRecentNotification.formatBigMessage(context));

            if (mostRecentNotification.mAttachmentBitmap != null) {
                // The message has a picture, show that

                NotificationCompat.BigPictureStyle bigPictureStyle = new NotificationCompat.BigPictureStyle(noti)
                        .bigPicture(mostRecentNotification.mAttachmentBitmap)
                        .setSummaryText(mostRecentNotification.formatPictureMessage(context));

                notification = noti.setStyle(bigPictureStyle).build();
            } else {
                // Show a single notification -- big style with the text of the whole message
                NotificationCompat.BigTextStyle bigTextStyle1 = new NotificationCompat.BigTextStyle(noti)
                        .bigText(mostRecentNotification.formatBigMessage(context));

                notification = noti.setStyle(bigTextStyle1).build();
            }
            if (DEBUG) {
                Log.d(TAG, "updateNotification: single message notification");
            }
        } else {
            // We've got multiple messages
            if (!isMultiNewMessages) {
                // We've got multiple messages for the same thread.
                // Starting with the oldest new message, display the full text of each message.
                // Begin a line for each subsequent message.
                SpannableStringBuilder buf = new SpannableStringBuilder();
                NotificationInfo infos[] = notificationSet.toArray(new NotificationInfo[messageCount]);
                int len = infos.length;
                for (int i = len - 1; i >= 0; i--) {
                    NotificationInfo info = infos[i];

                    buf.append(info.formatBigMessage(context));

                    if (i != 0) {
                        buf.append('\n');
                    }
                }

                noti.setContentText(context.getString(R.string.message_count_notification, messageCount));

                // Show a single notification -- big style with the text of all the messages
                NotificationCompat.BigTextStyle bigTextStyle = new NotificationCompat.BigTextStyle();
                bigTextStyle.bigText(buf)
                        // Forcibly show the last line, with the app's smallIcon in it, if we
                        // kicked the smallIcon out with an avatar bitmap
                        .setSummaryText((avatar == null) ? null : " ");
                notification = noti.setStyle(bigTextStyle).build();
                if (DEBUG) {
                    Log.d(TAG, "updateNotification: multi messages for single thread");
                }
            } else {
                // Build a set of the most recent notification per threadId.
                HashSet<Long> uniqueThreads = new HashSet<Long>(messageCount);
                ArrayList<NotificationInfo> mostRecentNotifPerThread = new ArrayList<NotificationInfo>();
                Iterator<NotificationInfo> notifications = notificationSet.iterator();
                while (notifications.hasNext()) {
                    NotificationInfo notificationInfo = notifications.next();
                    if (!uniqueThreads.contains(notificationInfo.mThreadId)) {
                        uniqueThreads.add(notificationInfo.mThreadId);
                        mostRecentNotifPerThread.add(notificationInfo);
                    }
                }
                // When collapsed, show all the senders like this:
                //     Fred Flinstone, Barry Manilow, Pete...
                noti.setContentText(formatSenders(context, mostRecentNotifPerThread));
                NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle(noti);

                // We have to set the summary text to non-empty so the content text doesn't show
                // up when expanded.
                inboxStyle.setSummaryText(" ");

                // At this point we've got multiple messages in multiple threads. We only
                // want to show the most recent message per thread, which are in
                // mostRecentNotifPerThread.
                int uniqueThreadMessageCount = mostRecentNotifPerThread.size();
                int maxMessages = Math.min(MAX_MESSAGES_TO_SHOW, uniqueThreadMessageCount);

                for (int i = 0; i < maxMessages; i++) {
                    NotificationInfo info = mostRecentNotifPerThread.get(i);
                    inboxStyle.addLine(info.formatInboxMessage(context));
                }
                notification = inboxStyle.build();

                uniqueThreads.clear();
                mostRecentNotifPerThread.clear();

                if (DEBUG) {
                    Log.d(TAG, "updateNotification: multi messages," + " showing inboxStyle notification");
                }
            }
        }

        notifyUserIfFullScreen(context, title);
        nm.notify(NOTIFICATION_ID, notification);

        // Trigger the QuickMessage pop-up activity if enabled
        // But don't show the QuickMessage if the user is in a call or the phone is ringing
        if (qmPopupEnabled && qmIntent != null) {
            TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
            if (tm.getCallState() == TelephonyManager.CALL_STATE_IDLE && !ConversationList.mIsRunning
                    && !ComposeMessageActivity.mIsRunning) {
                context.startActivity(qmIntent);
            }
        }
    }

    // Parse the user provided custom vibrate pattern into a long[]
    private static long[] parseVibratePattern(String pattern) {
        String[] splitPattern = pattern.split(",");
        long[] result = new long[splitPattern.length];

        for (int i = 0; i < splitPattern.length; i++) {
            try {
                result[i] = Long.parseLong(splitPattern[i]);
            } catch (NumberFormatException e) {
                return null;
            }
        }

        return result;
    }

    protected static CharSequence buildTickerMessage(Context context, String address, String subject, String body) {
        String displayAddress = Contact.get(address, true).getName();

        StringBuilder buf = new StringBuilder(
                displayAddress == null ? "" : displayAddress.replace('\n', ' ').replace('\r', ' '));
        if (!TextUtils.isEmpty(subject) && !TextUtils.isEmpty(body)) {
            buf.append(':').append(' ');
        }

        int offset = buf.length();
        if (!TextUtils.isEmpty(subject)) {
            subject = subject.replace('\n', ' ').replace('\r', ' ');
            buf.append(subject);
            buf.append(' ');
        }

        if (!TextUtils.isEmpty(body)) {
            body = body.replace('\n', ' ').replace('\r', ' ');
            buf.append(body);
        }

        SpannableString spanText = new SpannableString(buf.toString());
        spanText.setSpan(new StyleSpan(Typeface.BOLD), 0, offset, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

        return spanText;
    }

    private static String getMmsSubject(String sub, int charset) {
        return TextUtils.isEmpty(sub) ? ""
                : new EncodedStringValue(charset, PduPersister.getBytes(sub)).getString();
    }

    public static void notifyDownloadFailed(Context context, long threadId) {
        notifyFailed(context, true, threadId, false);
    }

    public static void notifySendFailed(Context context) {
        notifyFailed(context, false, 0, false);
    }

    public static void notifySendFailed(Context context, boolean noisy) {
        notifyFailed(context, false, 0, noisy);
    }

    private static void notifyFailed(Context context, boolean isDownload, long threadId, boolean noisy) {
        // TODO factor out common code for creating notifications
        boolean enabled = MessagingPreferenceActivity.getNotificationEnabled(context);
        if (!enabled) {
            return;
        }

        // Strategy:
        // a. If there is a single failure notification, tapping on the notification goes
        //    to the compose view.
        // b. If there are two failure it stays in the thread view. Selecting one undelivered
        //    thread will dismiss one undelivered notification but will still display the
        //    notification.If you select the 2nd undelivered one it will dismiss the notification.
        int totalFailedCount = getUndeliveredMessageCount(context);

        Intent failedIntent;
        Notification notification = new Notification();
        String title;
        String description;
        if (totalFailedCount > 1) {
            description = context.getString(R.string.notification_failed_multiple,
                    Integer.toString(totalFailedCount));
            title = context.getString(R.string.notification_failed_multiple_title);
        } else {
            title = isDownload ? context.getString(R.string.message_download_failed_title)
                    : context.getString(R.string.message_send_failed_title);

            description = context.getString(R.string.message_failed_body);
        }

        TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(context);
        // Get failed intent by folder mode or conversation mode.
        if (MessageUtils.isMailboxMode()) {
            failedIntent = getFailedIntentFromFolderMode(context, totalFailedCount, isDownload);
            if (failedIntent == null) {
                return;
            } else if (isDownload) {
                // When isDownload is true, the valid threadId is passed into this function.
                failedIntent.putExtra(FAILED_DOWNLOAD_FLAG, true);
            } else {
                failedIntent.putExtra(UNDELIVERED_FLAG, true);
            }
        } else {
            failedIntent = getFailedIntentFromConversationMode(context, isDownload, threadId);
        }

        taskStackBuilder.addNextIntent(failedIntent);

        notification.icon = R.drawable.stat_notify_sms_failed;

        notification.tickerText = title;

        notification.setLatestEventInfo(context, title, description,
                taskStackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT));

        if (noisy) {
            SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
            boolean vibrate = sp.getBoolean(MessagingPreferenceActivity.NOTIFICATION_VIBRATE,
                    false /* don't vibrate by default */);
            if (vibrate) {
                notification.defaults |= Notification.DEFAULT_VIBRATE;
            }

            String ringtoneStr = sp.getString(MessagingPreferenceActivity.NOTIFICATION_RINGTONE, null);
            notification.sound = TextUtils.isEmpty(ringtoneStr) ? null : Uri.parse(ringtoneStr);
        }

        NotificationManager notificationMgr = (NotificationManager) context
                .getSystemService(Context.NOTIFICATION_SERVICE);

        if (isDownload) {
            notificationMgr.notify(DOWNLOAD_FAILED_NOTIFICATION_ID, notification);
        } else {
            notificationMgr.notify(MESSAGE_FAILED_NOTIFICATION_ID, notification);
        }
    }

    /**
     * Return the pending intent for failed messages in conversation mode.
     * @param context The context
     * @param isDownload Whether the message is failed to download
     * @param threadId The thread if of the  message failed to download
     */
    private static Intent getFailedIntentFromConversationMode(Context context, boolean isDownload, long threadId) {
        Cursor cursor = SqliteWrapper.query(context, context.getContentResolver(), UNDELIVERED_URI,
                MMS_THREAD_ID_PROJECTION, "read=0", null, null);
        if (cursor == null) {
            return null;
        }
        try {
            Intent failedIntent;
            if (isFailedMessagesInSameThread(cursor)) {
                failedIntent = new Intent(context, ComposeMessageActivity.class);
                if (isDownload) {
                    // When isDownload is true, the valid threadId is passed into this function.
                    failedIntent.putExtra(FAILED_DOWNLOAD_FLAG, true);
                    failedIntent.putExtra(ComposeMessageActivity.THREAD_ID, threadId);
                } else {
                    // For send failed case, get the thread id from the cursor.
                    failedIntent.putExtra(UNDELIVERED_FLAG, true);
                    failedIntent.putExtra(ComposeMessageActivity.THREAD_ID, getUndeliveredMessageThreadId(cursor));
                }
            } else {
                failedIntent = new Intent(context, ConversationList.class);
            }
            return failedIntent;
        } finally {
            cursor.close();
        }
    }

    /**
     * Return the pending intent for failed messages in folder mode.
     * @param context The context
     * @param failedCount The failed messages' count
     * @param isDownload Whether the messages is for received
     */
    private static Intent getFailedIntentFromFolderMode(Context context, int failedCount, boolean isDownload) {
        // Query the DB and return the cursor of the  undelivered messages
        Cursor cursor = SqliteWrapper.query(context, context.getContentResolver(), UNDELIVERED_URI,
                MAILBOX_PROJECTION, "read=0", null, null);
        if (cursor == null) {
            return null;
        }

        try {
            int mailboxId = MailBoxMessageList.TYPE_INVALID;
            Intent failedIntent;

            if (failedCount > 1) {
                if (isFailedMessagesInSameBox(cursor)) {
                    mailboxId = getUndeliveredMessageBoxId(cursor);
                } else {
                    mailboxId = MailBoxMessageList.TYPE_INBOX;
                }

                failedIntent = new Intent(context, MailBoxMessageList.class);
                failedIntent.putExtra(MessageUtils.MAIL_BOX_ID, mailboxId);
                failedIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                return failedIntent;
            }

            // The rest cases: the "failedCount" is 1.
            Uri msgUri;
            String type = getUndeliveredMessageType(cursor);
            Long msgId = getUndeliveredMessageId(cursor);
            if (TextUtils.isEmpty(type)) {
                return null;
            }
            if (type.equals("sms")) {
                failedIntent = new Intent(context, MailBoxMessageContent.class);
                msgUri = Uri.withAppendedPath(Sms.CONTENT_URI, String.valueOf(msgId));
                failedIntent.setData(msgUri);
            } else {
                // MMS type.
                if (isDownload) {
                    //  Download fail will jump to MailBoxMessageList INBOX.
                    failedIntent = new Intent(context, MailBoxMessageList.class);
                    mailboxId = MailBoxMessageList.TYPE_INBOX;
                    failedIntent.putExtra(MessageUtils.MAIL_BOX_ID, mailboxId);
                } else {
                    failedIntent = new Intent(context, MobilePaperShowActivity.class);
                    msgUri = Uri.withAppendedPath(Mms.CONTENT_URI, String.valueOf(msgId));
                    failedIntent.setData(msgUri);
                }
            }
            failedIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

            return failedIntent;
        } finally {
            cursor.close();
        }
    }

    // Query the DB and return the number of undelivered messages (total for both SMS and MMS)
    private static int getUndeliveredMessageCount(Context context) {
        Cursor undeliveredCursor = SqliteWrapper.query(context, context.getContentResolver(), UNDELIVERED_URI,
                MMS_THREAD_ID_PROJECTION, "read=0", null, null);
        if (undeliveredCursor == null) {
            return 0;
        }
        int count = undeliveredCursor.getCount();
        undeliveredCursor.close();
        return count;
    }

    // Get the box id of the first undelivered message
    private static long getUndeliveredMessageThreadId(Cursor cursor) {
        if (cursor.moveToFirst()) {
            return cursor.getLong(0);
        } else {
            return 0;
        }
    }

    // Whether all the undelivered messages belong to the same thread.
    private static boolean isFailedMessagesInSameThread(Cursor cursor) {
        long firstThreadId = getUndeliveredMessageThreadId(cursor);
        boolean isSame = true;
        while (cursor.moveToNext()) {
            if (cursor.getLong(0) != firstThreadId) {
                isSame = false;
                break;
            }
        }
        return isSame;
    }

    // Get the type of the first undelivered message (SMS or MMS)
    private static String getUndeliveredMessageType(Cursor cursor) {
        if (cursor.moveToFirst()) {
            return cursor.getString(MAILBOX_MSG_TYPE);
        } else {
            return null;
        }
    }

    // Get the box id of the first undelivered message
    private static long getUndeliveredMessageId(Cursor cursor) {
        if (cursor.moveToFirst()) {
            return cursor.getLong(MAILBOX_ID);
        } else {
            return 0;
        }
    }

    // Get the box id of the first undelivered message
    private static int getUndeliveredMessageBoxId(Cursor cursor) {
        if (cursor.moveToFirst()) {
            if (cursor.getString(MAILBOX_MSG_TYPE).equals("sms")) {
                return cursor.getInt(MAILBOX_SMS_TYPE);
            } else {
                return cursor.getInt(MAILBOX_MMS_BOX);
            }
        }
        return MailBoxMessageList.TYPE_INBOX;
    }

    // Whether all the undelivered messages belong to the same box.
    private static boolean isFailedMessagesInSameBox(Cursor cursor) {
        int firstBoxId = getUndeliveredMessageBoxId(cursor);
        boolean isSame = true;
        while (cursor.moveToNext()) {
            if (cursor.getString(MAILBOX_MSG_TYPE).equals("sms")) {
                if (cursor.getInt(MAILBOX_SMS_TYPE) != firstBoxId) {
                    isSame = false;
                    break;
                }
            } else {
                if (cursor.getInt(MAILBOX_MMS_BOX) != firstBoxId) {
                    isSame = false;
                    break;
                }
            }
        }
        return isSame;
    }

    public static void nonBlockingUpdateSendFailedNotification(final Context context) {
        new AsyncTask<Void, Void, Integer>() {
            protected Integer doInBackground(Void... none) {
                return getUndeliveredMessageCount(context);
            }

            protected void onPostExecute(Integer result) {
                if (result < 1) {
                    cancelNotification(context, MESSAGE_FAILED_NOTIFICATION_ID);
                } else {
                    // rebuild and adjust the message count if necessary.
                    notifySendFailed(context);
                }
            }
        }.execute();
    }

    /**
     *  If all the undelivered messages belong to "threadId", cancel the notification.
     */
    public static void updateSendFailedNotificationForThread(Context context, long threadId) {
        Cursor cursor = SqliteWrapper.query(context, context.getContentResolver(), UNDELIVERED_URI,
                MMS_THREAD_ID_PROJECTION, "read=0", null, null);
        if (cursor == null) {
            return;
        }
        try {
            if (cursor.getCount() > 0 && getUndeliveredMessageThreadId(cursor) == threadId
                    && isFailedMessagesInSameThread(cursor)) {
                cancelNotification(context, MESSAGE_FAILED_NOTIFICATION_ID);
            }
        } finally {
            cursor.close();
        }
    }

    private static int getDownloadFailedMessageCount(Context context) {
        // Look for any messages in the MMS Inbox that are of the type
        // NOTIFICATION_IND (i.e. not already downloaded) and in the
        // permanent failure state.  If there are none, cancel any
        // failed download notification.
        Cursor c = SqliteWrapper
                .query(context, context.getContentResolver(), Mms.Inbox.CONTENT_URI, null,
                        Mms.MESSAGE_TYPE + "=" + String.valueOf(PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND) + " AND "
                                + Mms.STATUS + "=" + String.valueOf(DownloadManager.STATE_PERMANENT_FAILURE),
                        null, null);
        if (c == null) {
            return 0;
        }
        int count = c.getCount();
        c.close();
        return count;
    }

    public static void updateDownloadFailedNotification(Context context) {
        if (getDownloadFailedMessageCount(context) < 1) {
            cancelNotification(context, DOWNLOAD_FAILED_NOTIFICATION_ID);
        }
    }

    public static boolean isFailedToDeliver(Intent intent) {
        return (intent != null) && intent.getBooleanExtra(UNDELIVERED_FLAG, false);
    }

    public static boolean isFailedToDownload(Intent intent) {
        return (intent != null) && intent.getBooleanExtra(FAILED_DOWNLOAD_FLAG, false);
    }

    /**
     * Get the thread ID of the SMS message with the given URI
     * @param context The context
     * @param uri The URI of the SMS message
     * @return The thread ID, or THREAD_NONE if the URI contains no entries
     */
    public static long getSmsThreadId(Context context, Uri uri) {
        Cursor cursor = SqliteWrapper.query(context, context.getContentResolver(), uri, SMS_THREAD_ID_PROJECTION,
                null, null, null);

        if (cursor == null) {
            if (DEBUG) {
                Log.d(TAG, "getSmsThreadId uri: " + uri + " NULL cursor! returning THREAD_NONE");
            }
            return THREAD_NONE;
        }

        try {
            if (cursor.moveToFirst()) {
                int columnIndex = cursor.getColumnIndex(Sms.THREAD_ID);
                if (columnIndex < 0) {
                    if (DEBUG) {
                        Log.d(TAG, "getSmsThreadId uri: " + uri
                                + " Couldn't read row 0, col -1! returning THREAD_NONE");
                    }
                    return THREAD_NONE;
                }
                long threadId = cursor.getLong(columnIndex);
                if (DEBUG) {
                    Log.d(TAG, "getSmsThreadId uri: " + uri + " returning threadId: " + threadId);
                }
                return threadId;
            } else {
                if (DEBUG) {
                    Log.d(TAG, "getSmsThreadId uri: " + uri + " NULL cursor! returning THREAD_NONE");
                }
                return THREAD_NONE;
            }
        } finally {
            cursor.close();
        }
    }

    /**
     * Get the thread ID of the MMS message with the given URI
     * @param context The context
     * @param uri The URI of the SMS message
     * @return The thread ID, or THREAD_NONE if the URI contains no entries
     */
    public static long getThreadId(Context context, Uri uri) {
        Cursor cursor = SqliteWrapper.query(context, context.getContentResolver(), uri, MMS_THREAD_ID_PROJECTION,
                null, null, null);

        if (cursor == null) {
            if (DEBUG) {
                Log.d(TAG, "getThreadId uri: " + uri + " NULL cursor! returning THREAD_NONE");
            }
            return THREAD_NONE;
        }

        try {
            if (cursor.moveToFirst()) {
                int columnIndex = cursor.getColumnIndex(Mms.THREAD_ID);
                if (columnIndex < 0) {
                    if (DEBUG) {
                        Log.d(TAG,
                                "getThreadId uri: " + uri + " Couldn't read row 0, col -1! returning THREAD_NONE");
                    }
                    return THREAD_NONE;
                }
                long threadId = cursor.getLong(columnIndex);
                if (DEBUG) {
                    Log.d(TAG, "getThreadId uri: " + uri + " returning threadId: " + threadId);
                }
                return threadId;
            } else {
                if (DEBUG) {
                    Log.d(TAG, "getThreadId uri: " + uri + " NULL cursor! returning THREAD_NONE");
                }
                return THREAD_NONE;
            }
        } finally {
            cursor.close();
        }
    }

    private static void notifyUserIfFullScreen(Context context, String from) {
        ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
        List<ActivityManager.RunningTaskInfo> runningTasks = am.getRunningTasks(1);

        if (runningTasks.size() > 0) {
            String topActivity = runningTasks.get(0).topActivity.getClassName();
            Log.d(TAG, "checkIsFullScreenMode: the top activity is: " + topActivity);
            if ((topActivity != null) && (topActivity.equals("com.android.browser.BrowserActivity"))) {
                Intent intent = new Intent("com.android.mms.transaction.MESSAGE_RECEIVED");
                intent.putExtra("from", from);
                context.sendBroadcast(intent);
            }
        }
    }

    public static void blockingRemoveIccNotifications(Context context, int subId) {
        cancelNotification(context, ICC_NOTIFICATION_ID_BASE + subId);
    }

    /**
     * Checks to see if the message memory is full.
     *
     * @param context the context to use
     * @param isFull if notify a full icon, it should be true, otherwise, false.
     */
    public static void updateSmsMessageFullIndicator(Context context, boolean isFull) {
        if (isFull) {
            sendFullNotification(context);
        } else {
            cancelNotification(context, FULL_NOTIFICATION_ID);
        }
    }

    /**
     * This method sends a notification to NotificationManager to display
     * an dialog indicating the message memory is full.
     */
    private static void sendFullNotification(Context context) {
        NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);

        String title = context.getString(R.string.sms_full_title);
        String description = context.getString(R.string.sms_full_body);
        PendingIntent intent = PendingIntent.getActivity(context, 0, new Intent(), 0);
        Notification notification = new Notification();
        notification.icon = R.drawable.stat_notify_sms_failed;
        notification.tickerText = title;
        notification.setLatestEventInfo(context, title, description, intent);
        nm.notify(FULL_NOTIFICATION_ID, notification);
    }

    /**
     * Return the intent of multi-unread messges notification.
     */
    public static Intent getMultiThreadsViewIntent(Context context) {
        Intent intent;
        if (MessageUtils.isMailboxMode()) {
            intent = new Intent(context, MailBoxMessageList.class);
            intent.putExtra(MessageUtils.MAIL_BOX_ID, MailBoxMessageList.TYPE_INBOX);
        } else {
            intent = new Intent(context, ConversationList.class);
        }
        intent.setFlags(
                Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);

        return intent;
    }

}