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