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