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