| /* |
| * 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.Set; |
| import java.util.SortedSet; |
| import java.util.TreeSet; |
| |
| 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.graphics.drawable.BitmapDrawable; |
| import android.media.AudioManager; |
| import android.net.Uri; |
| import android.os.AsyncTask; |
| import android.os.Handler; |
| import android.preference.PreferenceManager; |
| import android.provider.Telephony.Mms; |
| import android.provider.Telephony.Sms; |
| 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.mms.LogTag; |
| 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.model.SlideModel; |
| import com.android.mms.model.SlideshowModel; |
| import com.android.mms.ui.ComposeMessageActivity; |
| import com.android.mms.ui.ConversationList; |
| import com.android.mms.ui.MessageUtils; |
| import com.android.mms.ui.MessagingPreferenceActivity; |
| 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; |
| |
| private static final int NOTIFICATION_ID = 123; |
| public static final int MESSAGE_FAILED_NOTIFICATION_ID = 789; |
| public static final int DOWNLOAD_FAILED_NOTIFICATION_ID = 531; |
| /** |
| * 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 }; |
| |
| // 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.SUBJECT, Sms.BODY }; |
| |
| // These must be consistent with MMS_STATUS_PROJECTION and |
| // SMS_STATUS_PROJECTION. |
| private static final int COLUMN_THREAD_ID = 0; |
| private static final int COLUMN_DATE = 1; |
| private static final int COLUMN_MMS_ID = 2; |
| private static final int COLUMN_SMS_ADDRESS = 2; |
| private static final int COLUMN_SUBJECT = 3; |
| private static final int COLUMN_SUBJECT_CS = 4; |
| private static final int COLUMN_SMS_BODY = 4; |
| |
| 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 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 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 OnDeletedReceiver sNotificationDeletedReceiver = new OnDeletedReceiver(); |
| private static Intent sNotificationOnDeleteIntent; |
| private static Handler sToastHandler = 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); |
| } |
| } |
| } |
| |
| /** |
| * 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); |
| } |
| 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); |
| } |
| // 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); |
| } |
| synchronized (sCurrentlyDisplayedThreadLock) { |
| if (newMsgThreadId > 0 && newMsgThreadId == sCurrentlyDisplayedThreadId && |
| threads.contains(newMsgThreadId)) { |
| if (DEBUG) { |
| Log.d(TAG, "blockingUpdateNewMessageIndicator: newMsgThreadId == " + |
| "sCurrentlyDisplayedThreadId so NOT showing notification," + |
| " but playing soft sound. threadId: " + newMsgThreadId); |
| } |
| playInConversationNotificationSound(context); |
| return; |
| } |
| } |
| updateNotification(context, newMsgThreadId != THREAD_NONE, 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); |
| } |
| } |
| |
| /** |
| * Play the in-conversation notification sound (it's the regular notification sound, but |
| * played at half-volume |
| */ |
| private static void playInConversationNotificationSound(Context context) { |
| SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); |
| String ringtoneStr = sp.getString(MessagingPreferenceActivity.NOTIFICATION_RINGTONE, |
| null); |
| if (TextUtils.isEmpty(ringtoneStr)) { |
| // Nothing to play |
| return; |
| } |
| Uri ringtoneUri = Uri.parse(ringtoneStr); |
| NotificationPlayer player = new NotificationPlayer(LogTag.APP); |
| player.play(context, ringtoneUri, false, AudioManager.STREAM_NOTIFICATION, |
| IN_CONVERSATION_NOTIFICATION_VOLUME); |
| } |
| |
| /** |
| * 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); |
| } |
| } |
| |
| private static final class NotificationInfo { |
| public final Intent mClickIntent; |
| public final String mMessage; |
| 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 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 ticker, long timeMillis, String title, |
| Bitmap attachmentBitmap, Contact sender, |
| int attachmentType, long threadId) { |
| mIsSms = isSms; |
| mClickIntent = clickIntent; |
| mMessage = message; |
| mSubject = subject; |
| 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; |
| } |
| } |
| |
| // 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_SUBJECT), cursor.getInt(COLUMN_SUBJECT_CS)); |
| subject = MessageUtils.cleanseMmsSubject(context, subject); |
| |
| long threadId = cursor.getLong(COLUMN_THREAD_ID); |
| long timeMillis = cursor.getLong(COLUMN_DATE) * 1000; |
| |
| 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, |
| timeMillis, |
| attachedPicture, |
| contact, |
| attachmentType); |
| |
| 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); |
| |
| 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_THREAD_ID); |
| long timeMillis = cursor.getLong(COLUMN_DATE); |
| |
| 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, timeMillis, null /* attachmentBitmap */, |
| contact, WorkingMessage.TEXT); |
| |
| notificationSet.add(info); |
| |
| threads.add(threadId); |
| threads.add(cursor.getLong(COLUMN_THREAD_ID)); |
| } |
| } finally { |
| cursor.close(); |
| } |
| } |
| |
| private static final NotificationInfo getNewMessageNotificationInfo( |
| Context context, |
| boolean isSms, |
| String address, |
| String message, |
| String subject, |
| long threadId, |
| long timeMillis, |
| Bitmap attachmentBitmap, |
| Contact contact, |
| int attachmentType) { |
| Intent clickIntent = ComposeMessageActivity.createIntent(context, threadId); |
| clickIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
| | Intent.FLAG_ACTIVITY_SINGLE_TOP |
| | Intent.FLAG_ACTIVITY_CLEAR_TOP); |
| |
| String senderInfo = buildTickerMessage( |
| context, address, null, null).toString(); |
| String senderInfoName = senderInfo.substring( |
| 0, senderInfo.length() - 2); |
| CharSequence ticker = buildTickerMessage( |
| context, address, subject, message); |
| |
| return new NotificationInfo(isSms, |
| clickIntent, message, subject, ticker, timeMillis, |
| senderInfoName, attachmentBitmap, contact, attachmentType, threadId); |
| } |
| |
| 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; |
| } |
| |
| sToastHandler.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 isNew if we've got a new message, show the ticker |
| * @param uniqueThreadCount |
| * @param notificationSet the set of notifications to display |
| */ |
| private static void updateNotification( |
| Context context, |
| boolean isNew, |
| int uniqueThreadCount, |
| SortedSet<NotificationInfo> notificationSet) { |
| // If the user has turned off notifications in settings, don't do any notifying. |
| if (!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 Notification.Builder noti = new Notification.Builder(context) |
| .setWhen(mostRecentNotification.mTimeMillis); |
| |
| if (isNew) { |
| noti.setTicker(mostRecentNotification.mTicker); |
| } |
| TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(context); |
| |
| // 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; |
| if (uniqueThreadCount > 1) { // messages from multiple threads |
| Intent mainActivityIntent = new Intent(Intent.ACTION_MAIN); |
| |
| mainActivityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
| | Intent.FLAG_ACTIVITY_SINGLE_TOP |
| | Intent.FLAG_ACTIVITY_CLEAR_TOP); |
| |
| mainActivityIntent.setType("vnd.android-dir/mms-sms"); |
| taskStackBuilder.addNextIntent(mainActivityIntent); |
| title = context.getString(R.string.message_count_notification, messageCount); |
| } else { // same thread, single or multiple messages |
| title = mostRecentNotification.mTitle; |
| BitmapDrawable contactDrawable = (BitmapDrawable)mostRecentNotification.mSender |
| .getAvatar(context, null); |
| if (contactDrawable != 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. |
| avatar = contactDrawable.getBitmap(); |
| if (avatar != null) { |
| final int idealIconHeight = |
| res.getDimensionPixelSize(android.R.dimen.notification_large_icon_height); |
| final int idealIconWidth = |
| res.getDimensionPixelSize(android.R.dimen.notification_large_icon_width); |
| if (avatar.getHeight() < idealIconHeight) { |
| // Scale this image to fit the intended size |
| avatar = Bitmap.createScaledBitmap( |
| avatar, idealIconWidth, idealIconHeight, true); |
| } |
| if (avatar != null) { |
| noti.setLargeIcon(avatar); |
| } |
| } |
| } |
| |
| taskStackBuilder.addParentStack(ComposeMessageActivity.class); |
| taskStackBuilder.addNextIntent(mostRecentNotification.mClickIntent); |
| } |
| // Always have to set the small icon or the notification is ignored |
| noti.setSmallIcon(R.drawable.stat_notify_sms); |
| |
| NotificationManager nm = (NotificationManager) |
| context.getSystemService(Context.NOTIFICATION_SERVICE); |
| |
| // Update the notification. |
| noti.setContentTitle(title) |
| .setContentIntent( |
| taskStackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)) |
| .addKind(Notification.KIND_MESSAGE) |
| .setPriority(Notification.PRIORITY_DEFAULT); // TODO: set based on contact coming |
| // from a favorite. |
| |
| int defaults = 0; |
| |
| if (isNew) { |
| 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, "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)); |
| |
| final Notification notification; |
| |
| 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 |
| |
| notification = new Notification.BigPictureStyle(noti) |
| .bigPicture(mostRecentNotification.mAttachmentBitmap) |
| // This sets the text for the expanded picture form: |
| .setSummaryText(mostRecentNotification.formatPictureMessage(context)) |
| .build(); |
| } else { |
| // Show a single notification -- big style with the text of the whole message |
| notification = new Notification.BigTextStyle(noti) |
| .bigText(mostRecentNotification.formatBigMessage(context)) |
| .build(); |
| } |
| if (DEBUG) { |
| Log.d(TAG, "updateNotification: single message notification"); |
| } |
| } else { |
| // We've got multiple messages |
| if (uniqueThreadCount == 1) { |
| // 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 |
| notification = new Notification.BigTextStyle(noti) |
| .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 : " ") |
| .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)); |
| Notification.InboxStyle inboxStyle = new Notification.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(); |
| if (DEBUG) { |
| Log.d(TAG, "updateNotification: multi messages," + |
| " showing inboxStyle notification"); |
| } |
| } |
| } |
| |
| nm.notify(NOTIFICATION_ID, notification); |
| } |
| |
| 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', ' ')); |
| 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. |
| |
| long[] msgThreadId = {0, 1}; // Dummy initial values, just to initialize the memory |
| int totalFailedCount = getUndeliveredMessageCount(context, msgThreadId); |
| if (totalFailedCount == 0 && !isDownload) { |
| return; |
| } |
| // The getUndeliveredMessageCount method puts a non-zero value in msgThreadId[1] if all |
| // failures are from the same thread. |
| // If isDownload is true, we're dealing with 1 specific failure; therefore "all failed" are |
| // indeed in the same thread since there's only 1. |
| boolean allFailedInSameThread = (msgThreadId[1] != 0) || isDownload; |
| |
| 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); |
| if (allFailedInSameThread) { |
| 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); |
| } else { |
| threadId = msgThreadId[0]; |
| failedIntent.putExtra("undelivered_flag", true); |
| } |
| failedIntent.putExtra("thread_id", threadId); |
| taskStackBuilder.addParentStack(ComposeMessageActivity.class); |
| } else { |
| failedIntent = new Intent(context, ConversationList.class); |
| } |
| 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); |
| } |
| } |
| |
| /** |
| * Query the DB and return the number of undelivered messages (total for both SMS and MMS) |
| * @param context The context |
| * @param threadIdResult A container to put the result in, according to the following rules: |
| * threadIdResult[0] contains the thread id of the first message. |
| * threadIdResult[1] is nonzero if the thread ids of all the messages are the same. |
| * You can pass in null for threadIdResult. |
| * You can pass in a threadIdResult of size 1 to avoid the comparison of each thread id. |
| */ |
| private static int getUndeliveredMessageCount(Context context, long[] threadIdResult) { |
| 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(); |
| try { |
| if (threadIdResult != null && undeliveredCursor.moveToFirst()) { |
| threadIdResult[0] = undeliveredCursor.getLong(0); |
| |
| if (threadIdResult.length >= 2) { |
| // Test to see if all the undelivered messages belong to the same thread. |
| long firstId = threadIdResult[0]; |
| while (undeliveredCursor.moveToNext()) { |
| if (undeliveredCursor.getLong(0) != firstId) { |
| firstId = 0; |
| break; |
| } |
| } |
| threadIdResult[1] = firstId; // non-zero if all ids are the same |
| } |
| } |
| } finally { |
| undeliveredCursor.close(); |
| } |
| return count; |
| } |
| |
| public static void nonBlockingUpdateSendFailedNotification(final Context context) { |
| new AsyncTask<Void, Void, Integer>() { |
| protected Integer doInBackground(Void... none) { |
| return getUndeliveredMessageCount(context, null); |
| } |
| |
| 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) { |
| long[] msgThreadId = {0, 0}; |
| if (getUndeliveredMessageCount(context, msgThreadId) > 0 |
| && msgThreadId[0] == threadId |
| && msgThreadId[1] != 0) { |
| cancelNotification(context, MESSAGE_FAILED_NOTIFICATION_ID); |
| } |
| } |
| |
| 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()) { |
| long threadId = cursor.getLong(cursor.getColumnIndex(Sms.THREAD_ID)); |
| 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()) { |
| long threadId = cursor.getLong(cursor.getColumnIndex(Mms.THREAD_ID)); |
| 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(); |
| } |
| } |
| } |