1 /* 2 * Copyright (C) 2008 Esmertec AG. 3 * Copyright (C) 2008 The Android Open Source Project 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18 package com.android.mms.transaction; 19 20 import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND; 21 import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF; 22 23 import java.util.ArrayList; 24 import java.util.Comparator; 25 import java.util.HashSet; 26 import java.util.Iterator; 27 import java.util.Set; 28 import java.util.SortedSet; 29 import java.util.TreeSet; 30 31 import android.app.Notification; 32 import android.app.NotificationManager; 33 import android.app.PendingIntent; 34 import android.app.TaskStackBuilder; 35 import android.content.BroadcastReceiver; 36 import android.content.ContentResolver; 37 import android.content.Context; 38 import android.content.Intent; 39 import android.content.IntentFilter; 40 import android.content.SharedPreferences; 41 import android.content.res.Resources; 42 import android.database.Cursor; 43 import android.database.sqlite.SqliteWrapper; 44 import android.graphics.Bitmap; 45 import android.graphics.Typeface; 46 import android.graphics.drawable.BitmapDrawable; 47 import android.media.AudioManager; 48 import android.net.Uri; 49 import android.os.AsyncTask; 50 import android.os.Handler; 51 import android.preference.PreferenceManager; 52 import android.provider.Telephony.Mms; 53 import android.provider.Telephony.Sms; 54 import android.text.Spannable; 55 import android.text.SpannableString; 56 import android.text.SpannableStringBuilder; 57 import android.text.TextUtils; 58 import android.text.style.StyleSpan; 59 import android.text.style.TextAppearanceSpan; 60 import android.util.Log; 61 import android.widget.Toast; 62 63 import com.android.mms.LogTag; 64 import com.android.mms.MmsConfig; 65 import com.android.mms.R; 66 import com.android.mms.data.Contact; 67 import com.android.mms.data.Conversation; 68 import com.android.mms.data.WorkingMessage; 69 import com.android.mms.model.SlideModel; 70 import com.android.mms.model.SlideshowModel; 71 import com.android.mms.ui.ComposeMessageActivity; 72 import com.android.mms.ui.ConversationList; 73 import com.android.mms.ui.MessageUtils; 74 import com.android.mms.ui.MessagingPreferenceActivity; 75 import com.android.mms.util.AddressUtils; 76 import com.android.mms.util.DownloadManager; 77 import com.android.mms.widget.MmsWidgetProvider; 78 79 import com.google.android.mms.MmsException; 80 import com.google.android.mms.pdu.EncodedStringValue; 81 import com.google.android.mms.pdu.GenericPdu; 82 import com.google.android.mms.pdu.MultimediaMessagePdu; 83 import com.google.android.mms.pdu.PduHeaders; 84 import com.google.android.mms.pdu.PduPersister; 85 86 /** 87 * This class is used to update the notification indicator. It will check whether 88 * there are unread messages. If yes, it would show the notification indicator, 89 * otherwise, hide the indicator. 90 */ 91 public class MessagingNotification { 92 93 private static final String TAG = LogTag.APP; 94 private static final boolean DEBUG = false; 95 96 private static final int NOTIFICATION_ID = 123; 97 public static final int MESSAGE_FAILED_NOTIFICATION_ID = 789; 98 public static final int DOWNLOAD_FAILED_NOTIFICATION_ID = 531; 99 /** 100 * This is the volume at which to play the in-conversation notification sound, 101 * expressed as a fraction of the system notification volume. 102 */ 103 private static final float IN_CONVERSATION_NOTIFICATION_VOLUME = 0.25f; 104 105 // This must be consistent with the column constants below. 106 private static final String[] MMS_STATUS_PROJECTION = new String[] { 107 Mms.THREAD_ID, Mms.DATE, Mms._ID, Mms.SUBJECT, Mms.SUBJECT_CHARSET }; 108 109 // This must be consistent with the column constants below. 110 private static final String[] SMS_STATUS_PROJECTION = new String[] { 111 Sms.THREAD_ID, Sms.DATE, Sms.ADDRESS, Sms.SUBJECT, Sms.BODY }; 112 113 // These must be consistent with MMS_STATUS_PROJECTION and 114 // SMS_STATUS_PROJECTION. 115 private static final int COLUMN_THREAD_ID = 0; 116 private static final int COLUMN_DATE = 1; 117 private static final int COLUMN_MMS_ID = 2; 118 private static final int COLUMN_SMS_ADDRESS = 2; 119 private static final int COLUMN_SUBJECT = 3; 120 private static final int COLUMN_SUBJECT_CS = 4; 121 private static final int COLUMN_SMS_BODY = 4; 122 123 private static final String[] SMS_THREAD_ID_PROJECTION = new String[] { Sms.THREAD_ID }; 124 private static final String[] MMS_THREAD_ID_PROJECTION = new String[] { Mms.THREAD_ID }; 125 126 private static final String NEW_INCOMING_SM_CONSTRAINT = 127 "(" + Sms.TYPE + " = " + Sms.MESSAGE_TYPE_INBOX 128 + " AND " + Sms.SEEN + " = 0)"; 129 130 private static final String NEW_DELIVERY_SM_CONSTRAINT = 131 "(" + Sms.TYPE + " = " + Sms.MESSAGE_TYPE_SENT 132 + " AND " + Sms.STATUS + " = "+ Sms.STATUS_COMPLETE +")"; 133 134 private static final String NEW_INCOMING_MM_CONSTRAINT = 135 "(" + Mms.MESSAGE_BOX + "=" + Mms.MESSAGE_BOX_INBOX 136 + " AND " + Mms.SEEN + "=0" 137 + " AND (" + Mms.MESSAGE_TYPE + "=" + MESSAGE_TYPE_NOTIFICATION_IND 138 + " OR " + Mms.MESSAGE_TYPE + "=" + MESSAGE_TYPE_RETRIEVE_CONF + "))"; 139 140 private static final NotificationInfoComparator INFO_COMPARATOR = 141 new NotificationInfoComparator(); 142 143 private static final Uri UNDELIVERED_URI = Uri.parse("content://mms-sms/undelivered"); 144 145 146 private final static String NOTIFICATION_DELETED_ACTION = 147 "com.android.mms.NOTIFICATION_DELETED_ACTION"; 148 149 public static class OnDeletedReceiver extends BroadcastReceiver { 150 @Override onReceive(Context context, Intent intent)151 public void onReceive(Context context, Intent intent) { 152 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 153 Log.d(TAG, "[MessagingNotification] clear notification: mark all msgs seen"); 154 } 155 156 Conversation.markAllConversationsAsSeen(context); 157 } 158 } 159 160 public static final long THREAD_ALL = -1; 161 public static final long THREAD_NONE = -2; 162 /** 163 * Keeps track of the thread ID of the conversation that's currently displayed to the user 164 */ 165 private static long sCurrentlyDisplayedThreadId; 166 private static final Object sCurrentlyDisplayedThreadLock = new Object(); 167 168 private static OnDeletedReceiver sNotificationDeletedReceiver = new OnDeletedReceiver(); 169 private static Intent sNotificationOnDeleteIntent; 170 private static Handler sHandler = new Handler(); 171 private static PduPersister sPduPersister; 172 private static final int MAX_BITMAP_DIMEN_DP = 360; 173 private static float sScreenDensity; 174 175 private static final int MAX_MESSAGES_TO_SHOW = 8; // the maximum number of new messages to 176 // show in a single notification. 177 178 MessagingNotification()179 private MessagingNotification() { 180 } 181 init(Context context)182 public static void init(Context context) { 183 // set up the intent filter for notification deleted action 184 IntentFilter intentFilter = new IntentFilter(); 185 intentFilter.addAction(NOTIFICATION_DELETED_ACTION); 186 187 // TODO: should we unregister when the app gets killed? 188 context.registerReceiver(sNotificationDeletedReceiver, intentFilter); 189 sPduPersister = PduPersister.getPduPersister(context); 190 191 // initialize the notification deleted action 192 sNotificationOnDeleteIntent = new Intent(NOTIFICATION_DELETED_ACTION); 193 194 sScreenDensity = context.getResources().getDisplayMetrics().density; 195 } 196 197 /** 198 * Specifies which message thread is currently being viewed by the user. New messages in that 199 * thread will not generate a notification icon and will play the notification sound at a lower 200 * volume. Make sure you set this to THREAD_NONE when the UI component that shows the thread is 201 * no longer visible to the user (e.g. Activity.onPause(), etc.) 202 * @param threadId The ID of the thread that the user is currently viewing. Pass THREAD_NONE 203 * if the user is not viewing a thread, or THREAD_ALL if the user is viewing the conversation 204 * list (note: that latter one has no effect as of this implementation) 205 */ setCurrentlyDisplayedThreadId(long threadId)206 public static void setCurrentlyDisplayedThreadId(long threadId) { 207 synchronized (sCurrentlyDisplayedThreadLock) { 208 sCurrentlyDisplayedThreadId = threadId; 209 if (DEBUG) { 210 Log.d(TAG, "setCurrentlyDisplayedThreadId: " + sCurrentlyDisplayedThreadId); 211 } 212 } 213 } 214 215 /** 216 * Checks to see if there are any "unseen" messages or delivery 217 * reports. Shows the most recent notification if there is one. 218 * Does its work and query in a worker thread. 219 * 220 * @param context the context to use 221 */ nonBlockingUpdateNewMessageIndicator(final Context context, final long newMsgThreadId, final boolean isStatusMessage)222 public static void nonBlockingUpdateNewMessageIndicator(final Context context, 223 final long newMsgThreadId, 224 final boolean isStatusMessage) { 225 if (DEBUG) { 226 Log.d(TAG, "nonBlockingUpdateNewMessageIndicator: newMsgThreadId: " + 227 newMsgThreadId + 228 " sCurrentlyDisplayedThreadId: " + sCurrentlyDisplayedThreadId); 229 } 230 new Thread(new Runnable() { 231 @Override 232 public void run() { 233 blockingUpdateNewMessageIndicator(context, newMsgThreadId, isStatusMessage); 234 } 235 }, "MessagingNotification.nonBlockingUpdateNewMessageIndicator").start(); 236 } 237 238 /** 239 * Checks to see if there are any "unseen" messages or delivery 240 * reports and builds a sorted (by delivery date) list of unread notifications. 241 * 242 * @param context the context to use 243 * @param newMsgThreadId The thread ID of a new message that we're to notify about; if there's 244 * no new message, use THREAD_NONE. If we should notify about multiple or unknown thread IDs, 245 * use THREAD_ALL. 246 * @param isStatusMessage 247 */ blockingUpdateNewMessageIndicator(Context context, long newMsgThreadId, boolean isStatusMessage)248 public static void blockingUpdateNewMessageIndicator(Context context, long newMsgThreadId, 249 boolean isStatusMessage) { 250 if (DEBUG) { 251 Contact.logWithTrace(TAG, "blockingUpdateNewMessageIndicator: newMsgThreadId: " + 252 newMsgThreadId); 253 } 254 final boolean isDefaultSmsApp = MmsConfig.isSmsEnabled(context); 255 if (!isDefaultSmsApp) { 256 cancelNotification(context, NOTIFICATION_ID); 257 if (DEBUG || Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 258 Log.d(TAG, "blockingUpdateNewMessageIndicator: not the default sms app - skipping " 259 + "notification"); 260 } 261 return; 262 } 263 264 // notificationSet is kept sorted by the incoming message delivery time, with the 265 // most recent message first. 266 SortedSet<NotificationInfo> notificationSet = 267 new TreeSet<NotificationInfo>(INFO_COMPARATOR); 268 269 Set<Long> threads = new HashSet<Long>(4); 270 271 addMmsNotificationInfos(context, threads, notificationSet); 272 addSmsNotificationInfos(context, threads, notificationSet); 273 274 if (notificationSet.isEmpty()) { 275 if (DEBUG) { 276 Log.d(TAG, "blockingUpdateNewMessageIndicator: notificationSet is empty, " + 277 "canceling existing notifications"); 278 } 279 cancelNotification(context, NOTIFICATION_ID); 280 } else { 281 if (DEBUG || Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 282 Log.d(TAG, "blockingUpdateNewMessageIndicator: count=" + notificationSet.size() + 283 ", newMsgThreadId=" + newMsgThreadId); 284 } 285 synchronized (sCurrentlyDisplayedThreadLock) { 286 if (newMsgThreadId > 0 && newMsgThreadId == sCurrentlyDisplayedThreadId && 287 threads.contains(newMsgThreadId)) { 288 if (DEBUG) { 289 Log.d(TAG, "blockingUpdateNewMessageIndicator: newMsgThreadId == " + 290 "sCurrentlyDisplayedThreadId so NOT showing notification," + 291 " but playing soft sound. threadId: " + newMsgThreadId); 292 } 293 playInConversationNotificationSound(context); 294 return; 295 } 296 } 297 updateNotification(context, newMsgThreadId != THREAD_NONE, threads.size(), 298 notificationSet); 299 } 300 301 // And deals with delivery reports (which use Toasts). It's safe to call in a worker 302 // thread because the toast will eventually get posted to a handler. 303 MmsSmsDeliveryInfo delivery = getSmsNewDeliveryInfo(context); 304 if (delivery != null) { 305 delivery.deliver(context, isStatusMessage); 306 } 307 308 notificationSet.clear(); 309 threads.clear(); 310 } 311 312 /** 313 * Play the in-conversation notification sound (it's the regular notification sound, but 314 * played at half-volume 315 */ playInConversationNotificationSound(Context context)316 private static void playInConversationNotificationSound(Context context) { 317 SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); 318 String ringtoneStr = sp.getString(MessagingPreferenceActivity.NOTIFICATION_RINGTONE, 319 null); 320 if (TextUtils.isEmpty(ringtoneStr)) { 321 // Nothing to play 322 return; 323 } 324 Uri ringtoneUri = Uri.parse(ringtoneStr); 325 final NotificationPlayer player = new NotificationPlayer(LogTag.APP); 326 player.play(context, ringtoneUri, false, AudioManager.STREAM_NOTIFICATION, 327 IN_CONVERSATION_NOTIFICATION_VOLUME); 328 329 // Stop the sound after five seconds to handle continuous ringtones 330 sHandler.postDelayed(new Runnable() { 331 @Override 332 public void run() { 333 player.stop(); 334 } 335 }, 5000); 336 } 337 338 /** 339 * Updates all pending notifications, clearing or updating them as 340 * necessary. 341 */ blockingUpdateAllNotifications(final Context context, long threadId)342 public static void blockingUpdateAllNotifications(final Context context, long threadId) { 343 if (DEBUG) { 344 Contact.logWithTrace(TAG, "blockingUpdateAllNotifications: newMsgThreadId: " + 345 threadId); 346 } 347 nonBlockingUpdateNewMessageIndicator(context, threadId, false); 348 nonBlockingUpdateSendFailedNotification(context); 349 updateDownloadFailedNotification(context); 350 MmsWidgetProvider.notifyDatasetChanged(context); 351 } 352 353 private static final class MmsSmsDeliveryInfo { 354 public CharSequence mTicker; 355 public long mTimeMillis; 356 MmsSmsDeliveryInfo(CharSequence ticker, long timeMillis)357 public MmsSmsDeliveryInfo(CharSequence ticker, long timeMillis) { 358 mTicker = ticker; 359 mTimeMillis = timeMillis; 360 } 361 deliver(Context context, boolean isStatusMessage)362 public void deliver(Context context, boolean isStatusMessage) { 363 updateDeliveryNotification( 364 context, isStatusMessage, mTicker, mTimeMillis); 365 } 366 } 367 368 private static final class NotificationInfo { 369 public final Intent mClickIntent; 370 public final String mMessage; 371 public final CharSequence mTicker; 372 public final long mTimeMillis; 373 public final String mTitle; 374 public final Bitmap mAttachmentBitmap; 375 public final Contact mSender; 376 public final boolean mIsSms; 377 public final int mAttachmentType; 378 public final String mSubject; 379 public final long mThreadId; 380 381 /** 382 * @param isSms true if sms, false if mms 383 * @param clickIntent where to go when the user taps the notification 384 * @param message for a single message, this is the message text 385 * @param subject text of mms subject 386 * @param ticker text displayed ticker-style across the notification, typically formatted 387 * as sender: message 388 * @param timeMillis date the message was received 389 * @param title for a single message, this is the sender 390 * @param attachmentBitmap a bitmap of an attachment, such as a picture or video 391 * @param sender contact of the sender 392 * @param attachmentType of the mms attachment 393 * @param threadId thread this message belongs to 394 */ NotificationInfo(boolean isSms, Intent clickIntent, String message, String subject, CharSequence ticker, long timeMillis, String title, Bitmap attachmentBitmap, Contact sender, int attachmentType, long threadId)395 public NotificationInfo(boolean isSms, 396 Intent clickIntent, String message, String subject, 397 CharSequence ticker, long timeMillis, String title, 398 Bitmap attachmentBitmap, Contact sender, 399 int attachmentType, long threadId) { 400 mIsSms = isSms; 401 mClickIntent = clickIntent; 402 mMessage = message; 403 mSubject = subject; 404 mTicker = ticker; 405 mTimeMillis = timeMillis; 406 mTitle = title; 407 mAttachmentBitmap = attachmentBitmap; 408 mSender = sender; 409 mAttachmentType = attachmentType; 410 mThreadId = threadId; 411 } 412 getTime()413 public long getTime() { 414 return mTimeMillis; 415 } 416 417 // This is the message string used in bigText and bigPicture notifications. formatBigMessage(Context context)418 public CharSequence formatBigMessage(Context context) { 419 final TextAppearanceSpan notificationSubjectSpan = new TextAppearanceSpan( 420 context, R.style.NotificationPrimaryText); 421 422 // Change multiple newlines (with potential white space between), into a single new line 423 final String message = 424 !TextUtils.isEmpty(mMessage) ? mMessage.replaceAll("\\n\\s+", "\n") : ""; 425 426 SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(); 427 if (!TextUtils.isEmpty(mSubject)) { 428 spannableStringBuilder.append(mSubject); 429 spannableStringBuilder.setSpan(notificationSubjectSpan, 0, mSubject.length(), 0); 430 } 431 if (mAttachmentType > WorkingMessage.TEXT) { 432 if (spannableStringBuilder.length() > 0) { 433 spannableStringBuilder.append('\n'); 434 } 435 spannableStringBuilder.append(getAttachmentTypeString(context, mAttachmentType)); 436 } 437 if (mMessage != null) { 438 if (spannableStringBuilder.length() > 0) { 439 spannableStringBuilder.append('\n'); 440 } 441 spannableStringBuilder.append(mMessage); 442 } 443 return spannableStringBuilder; 444 } 445 446 // This is the message string used in each line of an inboxStyle notification. formatInboxMessage(Context context)447 public CharSequence formatInboxMessage(Context context) { 448 final TextAppearanceSpan notificationSenderSpan = new TextAppearanceSpan( 449 context, R.style.NotificationPrimaryText); 450 451 final TextAppearanceSpan notificationSubjectSpan = new TextAppearanceSpan( 452 context, R.style.NotificationSubjectText); 453 454 // Change multiple newlines (with potential white space between), into a single new line 455 final String message = 456 !TextUtils.isEmpty(mMessage) ? mMessage.replaceAll("\\n\\s+", "\n") : ""; 457 458 SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(); 459 final String sender = mSender.getName(); 460 if (!TextUtils.isEmpty(sender)) { 461 spannableStringBuilder.append(sender); 462 spannableStringBuilder.setSpan(notificationSenderSpan, 0, sender.length(), 0); 463 } 464 String separator = context.getString(R.string.notification_separator); 465 if (!mIsSms) { 466 if (!TextUtils.isEmpty(mSubject)) { 467 if (spannableStringBuilder.length() > 0) { 468 spannableStringBuilder.append(separator); 469 } 470 int start = spannableStringBuilder.length(); 471 spannableStringBuilder.append(mSubject); 472 spannableStringBuilder.setSpan(notificationSubjectSpan, start, 473 start + mSubject.length(), 0); 474 } 475 if (mAttachmentType > WorkingMessage.TEXT) { 476 if (spannableStringBuilder.length() > 0) { 477 spannableStringBuilder.append(separator); 478 } 479 spannableStringBuilder.append(getAttachmentTypeString(context, mAttachmentType)); 480 } 481 } 482 if (message.length() > 0) { 483 if (spannableStringBuilder.length() > 0) { 484 spannableStringBuilder.append(separator); 485 } 486 int start = spannableStringBuilder.length(); 487 spannableStringBuilder.append(message); 488 spannableStringBuilder.setSpan(notificationSubjectSpan, start, 489 start + message.length(), 0); 490 } 491 return spannableStringBuilder; 492 } 493 494 // This is the summary string used in bigPicture notifications. formatPictureMessage(Context context)495 public CharSequence formatPictureMessage(Context context) { 496 final TextAppearanceSpan notificationSubjectSpan = new TextAppearanceSpan( 497 context, R.style.NotificationPrimaryText); 498 499 // Change multiple newlines (with potential white space between), into a single new line 500 final String message = 501 !TextUtils.isEmpty(mMessage) ? mMessage.replaceAll("\\n\\s+", "\n") : ""; 502 503 // Show the subject or the message (if no subject) 504 SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(); 505 if (!TextUtils.isEmpty(mSubject)) { 506 spannableStringBuilder.append(mSubject); 507 spannableStringBuilder.setSpan(notificationSubjectSpan, 0, mSubject.length(), 0); 508 } 509 if (message.length() > 0 && spannableStringBuilder.length() == 0) { 510 spannableStringBuilder.append(message); 511 spannableStringBuilder.setSpan(notificationSubjectSpan, 0, message.length(), 0); 512 } 513 return spannableStringBuilder; 514 } 515 } 516 517 // Return a formatted string with all the sender names separated by commas. formatSenders(Context context, ArrayList<NotificationInfo> senders)518 private static CharSequence formatSenders(Context context, 519 ArrayList<NotificationInfo> senders) { 520 final TextAppearanceSpan notificationSenderSpan = new TextAppearanceSpan( 521 context, R.style.NotificationPrimaryText); 522 523 String separator = context.getString(R.string.enumeration_comma); // ", " 524 SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(); 525 int len = senders.size(); 526 for (int i = 0; i < len; i++) { 527 if (i > 0) { 528 spannableStringBuilder.append(separator); 529 } 530 spannableStringBuilder.append(senders.get(i).mSender.getName()); 531 } 532 spannableStringBuilder.setSpan(notificationSenderSpan, 0, 533 spannableStringBuilder.length(), 0); 534 return spannableStringBuilder; 535 } 536 537 // Return a formatted string with the attachmentType spelled out as a string. For 538 // no attachment (or just text), return null. getAttachmentTypeString(Context context, int attachmentType)539 private static CharSequence getAttachmentTypeString(Context context, int attachmentType) { 540 final TextAppearanceSpan notificationAttachmentSpan = new TextAppearanceSpan( 541 context, R.style.NotificationSecondaryText); 542 int id = 0; 543 switch (attachmentType) { 544 case WorkingMessage.AUDIO: id = R.string.attachment_audio; break; 545 case WorkingMessage.VIDEO: id = R.string.attachment_video; break; 546 case WorkingMessage.SLIDESHOW: id = R.string.attachment_slideshow; break; 547 case WorkingMessage.IMAGE: id = R.string.attachment_picture; break; 548 } 549 if (id > 0) { 550 final SpannableString spannableString = new SpannableString(context.getString(id)); 551 spannableString.setSpan(notificationAttachmentSpan, 552 0, spannableString.length(), 0); 553 return spannableString; 554 } 555 return null; 556 } 557 558 /** 559 * 560 * Sorts by the time a notification was received in descending order -- newer first. 561 * 562 */ 563 private static final class NotificationInfoComparator 564 implements Comparator<NotificationInfo> { 565 @Override compare( NotificationInfo info1, NotificationInfo info2)566 public int compare( 567 NotificationInfo info1, NotificationInfo info2) { 568 return Long.signum(info2.getTime() - info1.getTime()); 569 } 570 } 571 addMmsNotificationInfos( Context context, Set<Long> threads, SortedSet<NotificationInfo> notificationSet)572 private static final void addMmsNotificationInfos( 573 Context context, Set<Long> threads, SortedSet<NotificationInfo> notificationSet) { 574 ContentResolver resolver = context.getContentResolver(); 575 576 // This query looks like this when logged: 577 // I/Database( 147): elapsedTime4Sql|/data/data/com.android.providers.telephony/databases/ 578 // mmssms.db|0.362 ms|SELECT thread_id, date, _id, sub, sub_cs FROM pdu WHERE ((msg_box=1 579 // AND seen=0 AND (m_type=130 OR m_type=132))) ORDER BY date desc 580 581 Cursor cursor = SqliteWrapper.query(context, resolver, Mms.CONTENT_URI, 582 MMS_STATUS_PROJECTION, NEW_INCOMING_MM_CONSTRAINT, 583 null, Mms.DATE + " desc"); 584 585 if (cursor == null) { 586 return; 587 } 588 589 try { 590 while (cursor.moveToNext()) { 591 592 long msgId = cursor.getLong(COLUMN_MMS_ID); 593 Uri msgUri = Mms.CONTENT_URI.buildUpon().appendPath( 594 Long.toString(msgId)).build(); 595 String address = AddressUtils.getFrom(context, msgUri); 596 597 Contact contact = Contact.get(address, false); 598 if (contact.getSendToVoicemail()) { 599 // don't notify, skip this one 600 continue; 601 } 602 603 String subject = getMmsSubject( 604 cursor.getString(COLUMN_SUBJECT), cursor.getInt(COLUMN_SUBJECT_CS)); 605 subject = MessageUtils.cleanseMmsSubject(context, subject); 606 607 long threadId = cursor.getLong(COLUMN_THREAD_ID); 608 long timeMillis = cursor.getLong(COLUMN_DATE) * 1000; 609 610 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 611 Log.d(TAG, "addMmsNotificationInfos: count=" + cursor.getCount() + 612 ", addr = " + address + ", thread_id=" + threadId); 613 } 614 615 // Extract the message and/or an attached picture from the first slide 616 Bitmap attachedPicture = null; 617 String messageBody = null; 618 int attachmentType = WorkingMessage.TEXT; 619 try { 620 GenericPdu pdu = sPduPersister.load(msgUri); 621 if (pdu != null && pdu instanceof MultimediaMessagePdu) { 622 SlideshowModel slideshow = SlideshowModel.createFromPduBody(context, 623 ((MultimediaMessagePdu)pdu).getBody()); 624 attachmentType = getAttachmentType(slideshow); 625 SlideModel firstSlide = slideshow.get(0); 626 if (firstSlide != null) { 627 if (firstSlide.hasImage()) { 628 int maxDim = dp2Pixels(MAX_BITMAP_DIMEN_DP); 629 attachedPicture = firstSlide.getImage().getBitmap(maxDim, maxDim); 630 } 631 if (firstSlide.hasText()) { 632 messageBody = firstSlide.getText().getText(); 633 } 634 } 635 } 636 } catch (final MmsException e) { 637 Log.e(TAG, "MmsException loading uri: " + msgUri, e); 638 continue; // skip this bad boy -- don't generate an empty notification 639 } 640 641 NotificationInfo info = getNewMessageNotificationInfo(context, 642 false /* isSms */, 643 address, 644 messageBody, subject, 645 threadId, 646 timeMillis, 647 attachedPicture, 648 contact, 649 attachmentType); 650 651 notificationSet.add(info); 652 653 threads.add(threadId); 654 } 655 } finally { 656 cursor.close(); 657 } 658 } 659 660 // Look at the passed in slideshow and determine what type of attachment it is. getAttachmentType(SlideshowModel slideshow)661 private static int getAttachmentType(SlideshowModel slideshow) { 662 int slideCount = slideshow.size(); 663 664 if (slideCount == 0) { 665 return WorkingMessage.TEXT; 666 } else if (slideCount > 1) { 667 return WorkingMessage.SLIDESHOW; 668 } else { 669 SlideModel slide = slideshow.get(0); 670 if (slide.hasImage()) { 671 return WorkingMessage.IMAGE; 672 } else if (slide.hasVideo()) { 673 return WorkingMessage.VIDEO; 674 } else if (slide.hasAudio()) { 675 return WorkingMessage.AUDIO; 676 } 677 } 678 return WorkingMessage.TEXT; 679 } 680 dp2Pixels(int dip)681 private static final int dp2Pixels(int dip) { 682 return (int) (dip * sScreenDensity + 0.5f); 683 } 684 getSmsNewDeliveryInfo(Context context)685 private static final MmsSmsDeliveryInfo getSmsNewDeliveryInfo(Context context) { 686 ContentResolver resolver = context.getContentResolver(); 687 Cursor cursor = SqliteWrapper.query(context, resolver, Sms.CONTENT_URI, 688 SMS_STATUS_PROJECTION, NEW_DELIVERY_SM_CONSTRAINT, 689 null, Sms.DATE); 690 691 if (cursor == null) { 692 return null; 693 } 694 695 try { 696 if (!cursor.moveToLast()) { 697 return null; 698 } 699 700 String address = cursor.getString(COLUMN_SMS_ADDRESS); 701 long timeMillis = 3000; 702 703 Contact contact = Contact.get(address, false); 704 String name = contact.getNameAndNumber(); 705 706 return new MmsSmsDeliveryInfo(context.getString(R.string.delivery_toast_body, name), 707 timeMillis); 708 709 } finally { 710 cursor.close(); 711 } 712 } 713 addSmsNotificationInfos( Context context, Set<Long> threads, SortedSet<NotificationInfo> notificationSet)714 private static final void addSmsNotificationInfos( 715 Context context, Set<Long> threads, SortedSet<NotificationInfo> notificationSet) { 716 ContentResolver resolver = context.getContentResolver(); 717 Cursor cursor = SqliteWrapper.query(context, resolver, Sms.CONTENT_URI, 718 SMS_STATUS_PROJECTION, NEW_INCOMING_SM_CONSTRAINT, 719 null, Sms.DATE + " desc"); 720 721 if (cursor == null) { 722 return; 723 } 724 725 try { 726 while (cursor.moveToNext()) { 727 String address = cursor.getString(COLUMN_SMS_ADDRESS); 728 729 Contact contact = Contact.get(address, false); 730 if (contact.getSendToVoicemail()) { 731 // don't notify, skip this one 732 continue; 733 } 734 735 String message = cursor.getString(COLUMN_SMS_BODY); 736 long threadId = cursor.getLong(COLUMN_THREAD_ID); 737 long timeMillis = cursor.getLong(COLUMN_DATE); 738 739 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) 740 { 741 Log.d(TAG, "addSmsNotificationInfos: count=" + cursor.getCount() + 742 ", addr=" + address + ", thread_id=" + threadId); 743 } 744 745 746 NotificationInfo info = getNewMessageNotificationInfo(context, true /* isSms */, 747 address, message, null /* subject */, 748 threadId, timeMillis, null /* attachmentBitmap */, 749 contact, WorkingMessage.TEXT); 750 751 notificationSet.add(info); 752 753 threads.add(threadId); 754 threads.add(cursor.getLong(COLUMN_THREAD_ID)); 755 } 756 } finally { 757 cursor.close(); 758 } 759 } 760 getNewMessageNotificationInfo( Context context, boolean isSms, String address, String message, String subject, long threadId, long timeMillis, Bitmap attachmentBitmap, Contact contact, int attachmentType)761 private static final NotificationInfo getNewMessageNotificationInfo( 762 Context context, 763 boolean isSms, 764 String address, 765 String message, 766 String subject, 767 long threadId, 768 long timeMillis, 769 Bitmap attachmentBitmap, 770 Contact contact, 771 int attachmentType) { 772 Intent clickIntent = ComposeMessageActivity.createIntent(context, threadId); 773 clickIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK 774 | Intent.FLAG_ACTIVITY_SINGLE_TOP 775 | Intent.FLAG_ACTIVITY_CLEAR_TOP); 776 777 String senderInfo = buildTickerMessage( 778 context, address, null, null).toString(); 779 String senderInfoName = senderInfo.substring( 780 0, senderInfo.length() - 2); 781 CharSequence ticker = buildTickerMessage( 782 context, address, subject, message); 783 784 return new NotificationInfo(isSms, 785 clickIntent, message, subject, ticker, timeMillis, 786 senderInfoName, attachmentBitmap, contact, attachmentType, threadId); 787 } 788 cancelNotification(Context context, int notificationId)789 public static void cancelNotification(Context context, int notificationId) { 790 NotificationManager nm = (NotificationManager) context.getSystemService( 791 Context.NOTIFICATION_SERVICE); 792 793 Log.d(TAG, "cancelNotification"); 794 nm.cancel(notificationId); 795 } 796 updateDeliveryNotification(final Context context, boolean isStatusMessage, final CharSequence message, final long timeMillis)797 private static void updateDeliveryNotification(final Context context, 798 boolean isStatusMessage, 799 final CharSequence message, 800 final long timeMillis) { 801 if (!isStatusMessage) { 802 return; 803 } 804 805 806 if (!MessagingPreferenceActivity.getNotificationEnabled(context)) { 807 return; 808 } 809 810 sHandler.post(new Runnable() { 811 @Override 812 public void run() { 813 Toast.makeText(context, message, (int)timeMillis).show(); 814 } 815 }); 816 } 817 818 /** 819 * updateNotification is *the* main function for building the actual notification handed to 820 * the NotificationManager 821 * @param context 822 * @param isNew if we've got a new message, show the ticker 823 * @param uniqueThreadCount 824 * @param notificationSet the set of notifications to display 825 */ updateNotification( Context context, boolean isNew, int uniqueThreadCount, SortedSet<NotificationInfo> notificationSet)826 private static void updateNotification( 827 Context context, 828 boolean isNew, 829 int uniqueThreadCount, 830 SortedSet<NotificationInfo> notificationSet) { 831 // If the user has turned off notifications in settings, don't do any notifying. 832 if (!MessagingPreferenceActivity.getNotificationEnabled(context)) { 833 if (DEBUG) { 834 Log.d(TAG, "updateNotification: notifications turned off in prefs, bailing"); 835 } 836 return; 837 } 838 839 // Figure out what we've got -- whether all sms's, mms's, or a mixture of both. 840 final int messageCount = notificationSet.size(); 841 NotificationInfo mostRecentNotification = notificationSet.first(); 842 843 final Notification.Builder noti = new Notification.Builder(context) 844 .setWhen(mostRecentNotification.mTimeMillis); 845 846 if (isNew) { 847 noti.setTicker(mostRecentNotification.mTicker); 848 } 849 TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(context); 850 851 // If we have more than one unique thread, change the title (which would 852 // normally be the contact who sent the message) to a generic one that 853 // makes sense for multiple senders, and change the Intent to take the 854 // user to the conversation list instead of the specific thread. 855 856 // Cases: 857 // 1) single message from single thread - intent goes to ComposeMessageActivity 858 // 2) multiple messages from single thread - intent goes to ComposeMessageActivity 859 // 3) messages from multiple threads - intent goes to ConversationList 860 861 final Resources res = context.getResources(); 862 String title = null; 863 Bitmap avatar = null; 864 if (uniqueThreadCount > 1) { // messages from multiple threads 865 Intent mainActivityIntent = new Intent(Intent.ACTION_MAIN); 866 867 mainActivityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK 868 | Intent.FLAG_ACTIVITY_SINGLE_TOP 869 | Intent.FLAG_ACTIVITY_CLEAR_TOP); 870 871 mainActivityIntent.setType("vnd.android-dir/mms-sms"); 872 taskStackBuilder.addNextIntent(mainActivityIntent); 873 title = context.getString(R.string.message_count_notification, messageCount); 874 } else { // same thread, single or multiple messages 875 title = mostRecentNotification.mTitle; 876 BitmapDrawable contactDrawable = (BitmapDrawable)mostRecentNotification.mSender 877 .getAvatar(context, null); 878 if (contactDrawable != null) { 879 // Show the sender's avatar as the big icon. Contact bitmaps are 96x96 so we 880 // have to scale 'em up to 128x128 to fill the whole notification large icon. 881 avatar = contactDrawable.getBitmap(); 882 if (avatar != null) { 883 final int idealIconHeight = 884 res.getDimensionPixelSize(android.R.dimen.notification_large_icon_height); 885 final int idealIconWidth = 886 res.getDimensionPixelSize(android.R.dimen.notification_large_icon_width); 887 if (avatar.getHeight() < idealIconHeight) { 888 // Scale this image to fit the intended size 889 avatar = Bitmap.createScaledBitmap( 890 avatar, idealIconWidth, idealIconHeight, true); 891 } 892 if (avatar != null) { 893 noti.setLargeIcon(avatar); 894 } 895 } 896 } 897 898 taskStackBuilder.addParentStack(ComposeMessageActivity.class); 899 taskStackBuilder.addNextIntent(mostRecentNotification.mClickIntent); 900 } 901 // Always have to set the small icon or the notification is ignored 902 noti.setSmallIcon(R.drawable.stat_notify_sms); 903 904 NotificationManager nm = (NotificationManager) 905 context.getSystemService(Context.NOTIFICATION_SERVICE); 906 907 // Update the notification. 908 noti.setContentTitle(title) 909 .setContentIntent( 910 taskStackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)) 911 .setCategory(Notification.CATEGORY_MESSAGE) 912 .setPriority(Notification.PRIORITY_DEFAULT); // TODO: set based on contact coming 913 // from a favorite. 914 915 // Tag notification with all senders. 916 for (NotificationInfo info : notificationSet) { 917 Uri peopleReferenceUri = info.mSender.getPeopleReferenceUri(); 918 if (peopleReferenceUri != null) { 919 noti.addPerson(peopleReferenceUri.toString()); 920 } 921 } 922 923 int defaults = 0; 924 925 if (isNew) { 926 SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); 927 928 boolean vibrate = false; 929 if (sp.contains(MessagingPreferenceActivity.NOTIFICATION_VIBRATE)) { 930 // The most recent change to the vibrate preference is to store a boolean 931 // value in NOTIFICATION_VIBRATE. If prefs contain that preference, use that 932 // first. 933 vibrate = sp.getBoolean(MessagingPreferenceActivity.NOTIFICATION_VIBRATE, 934 false); 935 } else if (sp.contains(MessagingPreferenceActivity.NOTIFICATION_VIBRATE_WHEN)) { 936 // This is to support the pre-JellyBean MR1.1 version of vibrate preferences 937 // when vibrate was a tri-state setting. As soon as the user opens the Messaging 938 // app's settings, it will migrate this setting from NOTIFICATION_VIBRATE_WHEN 939 // to the boolean value stored in NOTIFICATION_VIBRATE. 940 String vibrateWhen = 941 sp.getString(MessagingPreferenceActivity.NOTIFICATION_VIBRATE_WHEN, null); 942 vibrate = "always".equals(vibrateWhen); 943 } 944 if (vibrate) { 945 defaults |= Notification.DEFAULT_VIBRATE; 946 } 947 948 String ringtoneStr = sp.getString(MessagingPreferenceActivity.NOTIFICATION_RINGTONE, 949 null); 950 noti.setSound(TextUtils.isEmpty(ringtoneStr) ? null : Uri.parse(ringtoneStr)); 951 Log.d(TAG, "updateNotification: new message, adding sound to the notification"); 952 } 953 954 defaults |= Notification.DEFAULT_LIGHTS; 955 956 noti.setDefaults(defaults); 957 958 // set up delete intent 959 noti.setDeleteIntent(PendingIntent.getBroadcast(context, 0, 960 sNotificationOnDeleteIntent, 0)); 961 962 final Notification notification; 963 964 if (messageCount == 1) { 965 // We've got a single message 966 967 // This sets the text for the collapsed form: 968 noti.setContentText(mostRecentNotification.formatBigMessage(context)); 969 970 if (mostRecentNotification.mAttachmentBitmap != null) { 971 // The message has a picture, show that 972 973 notification = new Notification.BigPictureStyle(noti) 974 .bigPicture(mostRecentNotification.mAttachmentBitmap) 975 // This sets the text for the expanded picture form: 976 .setSummaryText(mostRecentNotification.formatPictureMessage(context)) 977 .build(); 978 } else { 979 // Show a single notification -- big style with the text of the whole message 980 notification = new Notification.BigTextStyle(noti) 981 .bigText(mostRecentNotification.formatBigMessage(context)) 982 .build(); 983 } 984 if (DEBUG) { 985 Log.d(TAG, "updateNotification: single message notification"); 986 } 987 } else { 988 // We've got multiple messages 989 if (uniqueThreadCount == 1) { 990 // We've got multiple messages for the same thread. 991 // Starting with the oldest new message, display the full text of each message. 992 // Begin a line for each subsequent message. 993 SpannableStringBuilder buf = new SpannableStringBuilder(); 994 NotificationInfo infos[] = 995 notificationSet.toArray(new NotificationInfo[messageCount]); 996 int len = infos.length; 997 for (int i = len - 1; i >= 0; i--) { 998 NotificationInfo info = infos[i]; 999 1000 buf.append(info.formatBigMessage(context)); 1001 1002 if (i != 0) { 1003 buf.append('\n'); 1004 } 1005 } 1006 1007 noti.setContentText(context.getString(R.string.message_count_notification, 1008 messageCount)); 1009 1010 // Show a single notification -- big style with the text of all the messages 1011 notification = new Notification.BigTextStyle(noti) 1012 .bigText(buf) 1013 // Forcibly show the last line, with the app's smallIcon in it, if we 1014 // kicked the smallIcon out with an avatar bitmap 1015 .setSummaryText((avatar == null) ? null : " ") 1016 .build(); 1017 if (DEBUG) { 1018 Log.d(TAG, "updateNotification: multi messages for single thread"); 1019 } 1020 } else { 1021 // Build a set of the most recent notification per threadId. 1022 HashSet<Long> uniqueThreads = new HashSet<Long>(messageCount); 1023 ArrayList<NotificationInfo> mostRecentNotifPerThread = 1024 new ArrayList<NotificationInfo>(); 1025 Iterator<NotificationInfo> notifications = notificationSet.iterator(); 1026 while (notifications.hasNext()) { 1027 NotificationInfo notificationInfo = notifications.next(); 1028 if (!uniqueThreads.contains(notificationInfo.mThreadId)) { 1029 uniqueThreads.add(notificationInfo.mThreadId); 1030 mostRecentNotifPerThread.add(notificationInfo); 1031 } 1032 } 1033 // When collapsed, show all the senders like this: 1034 // Fred Flinstone, Barry Manilow, Pete... 1035 noti.setContentText(formatSenders(context, mostRecentNotifPerThread)); 1036 Notification.InboxStyle inboxStyle = new Notification.InboxStyle(noti); 1037 1038 // We have to set the summary text to non-empty so the content text doesn't show 1039 // up when expanded. 1040 inboxStyle.setSummaryText(" "); 1041 1042 // At this point we've got multiple messages in multiple threads. We only 1043 // want to show the most recent message per thread, which are in 1044 // mostRecentNotifPerThread. 1045 int uniqueThreadMessageCount = mostRecentNotifPerThread.size(); 1046 int maxMessages = Math.min(MAX_MESSAGES_TO_SHOW, uniqueThreadMessageCount); 1047 1048 for (int i = 0; i < maxMessages; i++) { 1049 NotificationInfo info = mostRecentNotifPerThread.get(i); 1050 inboxStyle.addLine(info.formatInboxMessage(context)); 1051 } 1052 notification = inboxStyle.build(); 1053 1054 uniqueThreads.clear(); 1055 mostRecentNotifPerThread.clear(); 1056 1057 if (DEBUG) { 1058 Log.d(TAG, "updateNotification: multi messages," + 1059 " showing inboxStyle notification"); 1060 } 1061 } 1062 } 1063 1064 nm.notify(NOTIFICATION_ID, notification); 1065 } 1066 buildTickerMessage( Context context, String address, String subject, String body)1067 protected static CharSequence buildTickerMessage( 1068 Context context, String address, String subject, String body) { 1069 String displayAddress = Contact.get(address, true).getName(); 1070 1071 StringBuilder buf = new StringBuilder( 1072 displayAddress == null 1073 ? "" 1074 : displayAddress.replace('\n', ' ').replace('\r', ' ')); 1075 buf.append(':').append(' '); 1076 1077 int offset = buf.length(); 1078 if (!TextUtils.isEmpty(subject)) { 1079 subject = subject.replace('\n', ' ').replace('\r', ' '); 1080 buf.append(subject); 1081 buf.append(' '); 1082 } 1083 1084 if (!TextUtils.isEmpty(body)) { 1085 body = body.replace('\n', ' ').replace('\r', ' '); 1086 buf.append(body); 1087 } 1088 1089 SpannableString spanText = new SpannableString(buf.toString()); 1090 spanText.setSpan(new StyleSpan(Typeface.BOLD), 0, offset, 1091 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 1092 1093 return spanText; 1094 } 1095 getMmsSubject(String sub, int charset)1096 private static String getMmsSubject(String sub, int charset) { 1097 return TextUtils.isEmpty(sub) ? "" 1098 : new EncodedStringValue(charset, PduPersister.getBytes(sub)).getString(); 1099 } 1100 notifyDownloadFailed(Context context, long threadId)1101 public static void notifyDownloadFailed(Context context, long threadId) { 1102 notifyFailed(context, true, threadId, false); 1103 } 1104 notifySendFailed(Context context)1105 public static void notifySendFailed(Context context) { 1106 notifyFailed(context, false, 0, false); 1107 } 1108 notifySendFailed(Context context, boolean noisy)1109 public static void notifySendFailed(Context context, boolean noisy) { 1110 notifyFailed(context, false, 0, noisy); 1111 } 1112 notifyFailed(Context context, boolean isDownload, long threadId, boolean noisy)1113 private static void notifyFailed(Context context, boolean isDownload, long threadId, 1114 boolean noisy) { 1115 // TODO factor out common code for creating notifications 1116 boolean enabled = MessagingPreferenceActivity.getNotificationEnabled(context); 1117 if (!enabled) { 1118 return; 1119 } 1120 1121 // Strategy: 1122 // a. If there is a single failure notification, tapping on the notification goes 1123 // to the compose view. 1124 // b. If there are two failure it stays in the thread view. Selecting one undelivered 1125 // thread will dismiss one undelivered notification but will still display the 1126 // notification.If you select the 2nd undelivered one it will dismiss the notification. 1127 1128 long[] msgThreadId = {0, 1}; // Dummy initial values, just to initialize the memory 1129 int totalFailedCount = getUndeliveredMessageCount(context, msgThreadId); 1130 if (totalFailedCount == 0 && !isDownload) { 1131 return; 1132 } 1133 // The getUndeliveredMessageCount method puts a non-zero value in msgThreadId[1] if all 1134 // failures are from the same thread. 1135 // If isDownload is true, we're dealing with 1 specific failure; therefore "all failed" are 1136 // indeed in the same thread since there's only 1. 1137 boolean allFailedInSameThread = (msgThreadId[1] != 0) || isDownload; 1138 1139 Intent failedIntent; 1140 Notification notification = new Notification(); 1141 String title; 1142 String description; 1143 if (totalFailedCount > 1) { 1144 description = context.getString(R.string.notification_failed_multiple, 1145 Integer.toString(totalFailedCount)); 1146 title = context.getString(R.string.notification_failed_multiple_title); 1147 } else { 1148 title = isDownload ? 1149 context.getString(R.string.message_download_failed_title) : 1150 context.getString(R.string.message_send_failed_title); 1151 1152 description = context.getString(R.string.message_failed_body); 1153 } 1154 1155 TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(context); 1156 if (allFailedInSameThread) { 1157 failedIntent = new Intent(context, ComposeMessageActivity.class); 1158 if (isDownload) { 1159 // When isDownload is true, the valid threadId is passed into this function. 1160 failedIntent.putExtra("failed_download_flag", true); 1161 } else { 1162 threadId = msgThreadId[0]; 1163 failedIntent.putExtra("undelivered_flag", true); 1164 } 1165 failedIntent.putExtra("thread_id", threadId); 1166 taskStackBuilder.addParentStack(ComposeMessageActivity.class); 1167 } else { 1168 failedIntent = new Intent(context, ConversationList.class); 1169 } 1170 taskStackBuilder.addNextIntent(failedIntent); 1171 1172 notification.icon = R.drawable.stat_notify_sms_failed; 1173 1174 notification.tickerText = title; 1175 1176 notification.setLatestEventInfo(context, title, description, 1177 taskStackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)); 1178 1179 if (noisy) { 1180 SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); 1181 boolean vibrate = sp.getBoolean(MessagingPreferenceActivity.NOTIFICATION_VIBRATE, 1182 false /* don't vibrate by default */); 1183 if (vibrate) { 1184 notification.defaults |= Notification.DEFAULT_VIBRATE; 1185 } 1186 1187 String ringtoneStr = sp.getString(MessagingPreferenceActivity.NOTIFICATION_RINGTONE, 1188 null); 1189 notification.sound = TextUtils.isEmpty(ringtoneStr) ? null : Uri.parse(ringtoneStr); 1190 } 1191 1192 NotificationManager notificationMgr = (NotificationManager) 1193 context.getSystemService(Context.NOTIFICATION_SERVICE); 1194 1195 if (isDownload) { 1196 notificationMgr.notify(DOWNLOAD_FAILED_NOTIFICATION_ID, notification); 1197 } else { 1198 notificationMgr.notify(MESSAGE_FAILED_NOTIFICATION_ID, notification); 1199 } 1200 } 1201 1202 /** 1203 * Query the DB and return the number of undelivered messages (total for both SMS and MMS) 1204 * @param context The context 1205 * @param threadIdResult A container to put the result in, according to the following rules: 1206 * threadIdResult[0] contains the thread id of the first message. 1207 * threadIdResult[1] is nonzero if the thread ids of all the messages are the same. 1208 * You can pass in null for threadIdResult. 1209 * You can pass in a threadIdResult of size 1 to avoid the comparison of each thread id. 1210 */ getUndeliveredMessageCount(Context context, long[] threadIdResult)1211 private static int getUndeliveredMessageCount(Context context, long[] threadIdResult) { 1212 Cursor undeliveredCursor = SqliteWrapper.query(context, context.getContentResolver(), 1213 UNDELIVERED_URI, MMS_THREAD_ID_PROJECTION, "read=0", null, null); 1214 if (undeliveredCursor == null) { 1215 return 0; 1216 } 1217 int count = undeliveredCursor.getCount(); 1218 try { 1219 if (threadIdResult != null && undeliveredCursor.moveToFirst()) { 1220 threadIdResult[0] = undeliveredCursor.getLong(0); 1221 1222 if (threadIdResult.length >= 2) { 1223 // Test to see if all the undelivered messages belong to the same thread. 1224 long firstId = threadIdResult[0]; 1225 while (undeliveredCursor.moveToNext()) { 1226 if (undeliveredCursor.getLong(0) != firstId) { 1227 firstId = 0; 1228 break; 1229 } 1230 } 1231 threadIdResult[1] = firstId; // non-zero if all ids are the same 1232 } 1233 } 1234 } finally { 1235 undeliveredCursor.close(); 1236 } 1237 return count; 1238 } 1239 nonBlockingUpdateSendFailedNotification(final Context context)1240 public static void nonBlockingUpdateSendFailedNotification(final Context context) { 1241 new AsyncTask<Void, Void, Integer>() { 1242 protected Integer doInBackground(Void... none) { 1243 return getUndeliveredMessageCount(context, null); 1244 } 1245 1246 protected void onPostExecute(Integer result) { 1247 if (result < 1) { 1248 cancelNotification(context, MESSAGE_FAILED_NOTIFICATION_ID); 1249 } else { 1250 // rebuild and adjust the message count if necessary. 1251 notifySendFailed(context); 1252 } 1253 } 1254 }.execute(); 1255 } 1256 1257 /** 1258 * If all the undelivered messages belong to "threadId", cancel the notification. 1259 */ updateSendFailedNotificationForThread(Context context, long threadId)1260 public static void updateSendFailedNotificationForThread(Context context, long threadId) { 1261 long[] msgThreadId = {0, 0}; 1262 if (getUndeliveredMessageCount(context, msgThreadId) > 0 1263 && msgThreadId[0] == threadId 1264 && msgThreadId[1] != 0) { 1265 cancelNotification(context, MESSAGE_FAILED_NOTIFICATION_ID); 1266 } 1267 } 1268 getDownloadFailedMessageCount(Context context)1269 private static int getDownloadFailedMessageCount(Context context) { 1270 // Look for any messages in the MMS Inbox that are of the type 1271 // NOTIFICATION_IND (i.e. not already downloaded) and in the 1272 // permanent failure state. If there are none, cancel any 1273 // failed download notification. 1274 Cursor c = SqliteWrapper.query(context, context.getContentResolver(), 1275 Mms.Inbox.CONTENT_URI, null, 1276 Mms.MESSAGE_TYPE + "=" + 1277 String.valueOf(PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND) + 1278 " AND " + Mms.STATUS + "=" + 1279 String.valueOf(DownloadManager.STATE_PERMANENT_FAILURE), 1280 null, null); 1281 if (c == null) { 1282 return 0; 1283 } 1284 int count = c.getCount(); 1285 c.close(); 1286 return count; 1287 } 1288 updateDownloadFailedNotification(Context context)1289 public static void updateDownloadFailedNotification(Context context) { 1290 if (getDownloadFailedMessageCount(context) < 1) { 1291 cancelNotification(context, DOWNLOAD_FAILED_NOTIFICATION_ID); 1292 } 1293 } 1294 isFailedToDeliver(Intent intent)1295 public static boolean isFailedToDeliver(Intent intent) { 1296 return (intent != null) && intent.getBooleanExtra("undelivered_flag", false); 1297 } 1298 isFailedToDownload(Intent intent)1299 public static boolean isFailedToDownload(Intent intent) { 1300 return (intent != null) && intent.getBooleanExtra("failed_download_flag", false); 1301 } 1302 1303 /** 1304 * Get the thread ID of the SMS message with the given URI 1305 * @param context The context 1306 * @param uri The URI of the SMS message 1307 * @return The thread ID, or THREAD_NONE if the URI contains no entries 1308 */ getSmsThreadId(Context context, Uri uri)1309 public static long getSmsThreadId(Context context, Uri uri) { 1310 Cursor cursor = SqliteWrapper.query( 1311 context, 1312 context.getContentResolver(), 1313 uri, 1314 SMS_THREAD_ID_PROJECTION, 1315 null, 1316 null, 1317 null); 1318 1319 if (cursor == null) { 1320 if (DEBUG) { 1321 Log.d(TAG, "getSmsThreadId uri: " + uri + " NULL cursor! returning THREAD_NONE"); 1322 } 1323 return THREAD_NONE; 1324 } 1325 1326 try { 1327 if (cursor.moveToFirst()) { 1328 int columnIndex = cursor.getColumnIndex(Sms.THREAD_ID); 1329 if (columnIndex < 0) { 1330 if (DEBUG) { 1331 Log.d(TAG, "getSmsThreadId uri: " + uri + 1332 " Couldn't read row 0, col -1! returning THREAD_NONE"); 1333 } 1334 return THREAD_NONE; 1335 } 1336 long threadId = cursor.getLong(columnIndex); 1337 if (DEBUG) { 1338 Log.d(TAG, "getSmsThreadId uri: " + uri + 1339 " returning threadId: " + threadId); 1340 } 1341 return threadId; 1342 } else { 1343 if (DEBUG) { 1344 Log.d(TAG, "getSmsThreadId uri: " + uri + 1345 " NULL cursor! returning THREAD_NONE"); 1346 } 1347 return THREAD_NONE; 1348 } 1349 } finally { 1350 cursor.close(); 1351 } 1352 } 1353 1354 /** 1355 * Get the thread ID of the MMS message with the given URI 1356 * @param context The context 1357 * @param uri The URI of the SMS message 1358 * @return The thread ID, or THREAD_NONE if the URI contains no entries 1359 */ getThreadId(Context context, Uri uri)1360 public static long getThreadId(Context context, Uri uri) { 1361 Cursor cursor = SqliteWrapper.query( 1362 context, 1363 context.getContentResolver(), 1364 uri, 1365 MMS_THREAD_ID_PROJECTION, 1366 null, 1367 null, 1368 null); 1369 1370 if (cursor == null) { 1371 if (DEBUG) { 1372 Log.d(TAG, "getThreadId uri: " + uri + " NULL cursor! returning THREAD_NONE"); 1373 } 1374 return THREAD_NONE; 1375 } 1376 1377 try { 1378 if (cursor.moveToFirst()) { 1379 int columnIndex = cursor.getColumnIndex(Mms.THREAD_ID); 1380 if (columnIndex < 0) { 1381 if (DEBUG) { 1382 Log.d(TAG, "getThreadId uri: " + uri + 1383 " Couldn't read row 0, col -1! returning THREAD_NONE"); 1384 } 1385 return THREAD_NONE; 1386 } 1387 long threadId = cursor.getLong(columnIndex); 1388 if (DEBUG) { 1389 Log.d(TAG, "getThreadId uri: " + uri + 1390 " returning threadId: " + threadId); 1391 } 1392 return threadId; 1393 } else { 1394 if (DEBUG) { 1395 Log.d(TAG, "getThreadId uri: " + uri + 1396 " NULL cursor! returning THREAD_NONE"); 1397 } 1398 return THREAD_NONE; 1399 } 1400 } finally { 1401 cursor.close(); 1402 } 1403 } 1404 } 1405