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 com.android.mms.R; 24 import com.android.mms.LogTag; 25 import com.android.mms.data.Contact; 26 import com.android.mms.data.Conversation; 27 import com.android.mms.ui.ComposeMessageActivity; 28 import com.android.mms.ui.ConversationList; 29 import com.android.mms.ui.MessagingPreferenceActivity; 30 import com.android.mms.util.AddressUtils; 31 import com.android.mms.util.DownloadManager; 32 33 import com.google.android.mms.pdu.EncodedStringValue; 34 import com.google.android.mms.pdu.PduHeaders; 35 import com.google.android.mms.pdu.PduPersister; 36 import android.database.sqlite.SqliteWrapper; 37 38 import android.app.Notification; 39 import android.app.NotificationManager; 40 import android.app.PendingIntent; 41 import android.content.ContentResolver; 42 import android.content.Context; 43 import android.content.Intent; 44 import android.content.SharedPreferences; 45 import android.content.BroadcastReceiver; 46 import android.content.IntentFilter; 47 import android.database.Cursor; 48 import android.graphics.Typeface; 49 import android.media.AudioManager; 50 import android.net.Uri; 51 import android.os.Handler; 52 import android.preference.PreferenceManager; 53 import android.provider.Telephony.Mms; 54 import android.provider.Telephony.Sms; 55 import android.text.Spannable; 56 import android.text.SpannableString; 57 import android.text.TextUtils; 58 import android.text.style.StyleSpan; 59 import android.util.Log; 60 import android.widget.Toast; 61 62 import java.util.Comparator; 63 import java.util.HashSet; 64 import java.util.Set; 65 import java.util.SortedSet; 66 import java.util.TreeSet; 67 68 /** 69 * This class is used to update the notification indicator. It will check whether 70 * there are unread messages. If yes, it would show the notification indicator, 71 * otherwise, hide the indicator. 72 */ 73 public class MessagingNotification { 74 private static final String TAG = LogTag.APP; 75 76 private static final int NOTIFICATION_ID = 123; 77 public static final int MESSAGE_FAILED_NOTIFICATION_ID = 789; 78 public static final int DOWNLOAD_FAILED_NOTIFICATION_ID = 531; 79 80 // This must be consistent with the column constants below. 81 private static final String[] MMS_STATUS_PROJECTION = new String[] { 82 Mms.THREAD_ID, Mms.DATE, Mms._ID, Mms.SUBJECT, Mms.SUBJECT_CHARSET }; 83 84 // This must be consistent with the column constants below. 85 private static final String[] SMS_STATUS_PROJECTION = new String[] { 86 Sms.THREAD_ID, Sms.DATE, Sms.ADDRESS, Sms.SUBJECT, Sms.BODY }; 87 88 // These must be consistent with MMS_STATUS_PROJECTION and 89 // SMS_STATUS_PROJECTION. 90 private static final int COLUMN_THREAD_ID = 0; 91 private static final int COLUMN_DATE = 1; 92 private static final int COLUMN_MMS_ID = 2; 93 private static final int COLUMN_SMS_ADDRESS = 2; 94 private static final int COLUMN_SUBJECT = 3; 95 private static final int COLUMN_SUBJECT_CS = 4; 96 private static final int COLUMN_SMS_BODY = 4; 97 98 private static final String NEW_INCOMING_SM_CONSTRAINT = 99 "(" + Sms.TYPE + " = " + Sms.MESSAGE_TYPE_INBOX 100 + " AND " + Sms.SEEN + " = 0)"; 101 102 private static final String NEW_DELIVERY_SM_CONSTRAINT = 103 "(" + Sms.TYPE + " = " + Sms.MESSAGE_TYPE_SENT 104 + " AND " + Sms.STATUS + " = "+ Sms.STATUS_COMPLETE +")"; 105 106 private static final String NEW_INCOMING_MM_CONSTRAINT = 107 "(" + Mms.MESSAGE_BOX + "=" + Mms.MESSAGE_BOX_INBOX 108 + " AND " + Mms.SEEN + "=0" 109 + " AND (" + Mms.MESSAGE_TYPE + "=" + MESSAGE_TYPE_NOTIFICATION_IND 110 + " OR " + Mms.MESSAGE_TYPE + "=" + MESSAGE_TYPE_RETRIEVE_CONF + "))"; 111 112 private static final MmsSmsNotificationInfoComparator INFO_COMPARATOR = 113 new MmsSmsNotificationInfoComparator(); 114 115 private static final Uri UNDELIVERED_URI = Uri.parse("content://mms-sms/undelivered"); 116 117 118 private final static String NOTIFICATION_DELETED_ACTION = 119 "com.android.mms.NOTIFICATION_DELETED_ACTION"; 120 121 public static class OnDeletedReceiver extends BroadcastReceiver { 122 public void onReceive(Context context, Intent intent) { 123 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 124 Log.d(TAG, "[MessagingNotification] clear notification: mark all msgs seen"); 125 } 126 127 Conversation.markAllConversationsAsSeen(context); 128 } 129 }; 130 private static OnDeletedReceiver sNotificationDeletedReceiver = new OnDeletedReceiver(); 131 private static Intent sNotificationOnDeleteIntent; 132 private static Handler mToastHandler = new Handler(); 133 134 private MessagingNotification() { 135 } 136 137 public static void init(Context context) { 138 // set up the intent filter for notification deleted action 139 IntentFilter intentFilter = new IntentFilter(); 140 intentFilter.addAction(NOTIFICATION_DELETED_ACTION); 141 context.registerReceiver(sNotificationDeletedReceiver, intentFilter); 142 143 // initialize the notification deleted action 144 sNotificationOnDeleteIntent = new Intent(NOTIFICATION_DELETED_ACTION); 145 } 146 147 /** 148 * Checks to see if there are any "unseen" messages or delivery 149 * reports. Shows the most recent notification if there is one. 150 * Does its work and query in a worker thread. 151 * 152 * @param context the context to use 153 */ 154 public static void nonBlockingUpdateNewMessageIndicator(final Context context, 155 final boolean isNew, 156 final boolean isStatusMessage) { 157 new Thread(new Runnable() { 158 public void run() { 159 blockingUpdateNewMessageIndicator(context, isNew, isStatusMessage); 160 } 161 }).start(); 162 } 163 164 /** 165 * Checks to see if there are any "unseen" messages or delivery 166 * reports. Shows the most recent notification if there is one. 167 * 168 * @param context the context to use 169 * @param isNew if notify a new message comes, it should be true, otherwise, false. 170 */ 171 public static void blockingUpdateNewMessageIndicator(Context context, boolean isNew, 172 boolean isStatusMessage) { 173 SortedSet<MmsSmsNotificationInfo> accumulator = 174 new TreeSet<MmsSmsNotificationInfo>(INFO_COMPARATOR); 175 MmsSmsDeliveryInfo delivery = null; 176 Set<Long> threads = new HashSet<Long>(4); 177 178 int count = 0; 179 count += accumulateNotificationInfo( 180 accumulator, getMmsNewMessageNotificationInfo(context, threads)); 181 count += accumulateNotificationInfo( 182 accumulator, getSmsNewMessageNotificationInfo(context, threads)); 183 184 cancelNotification(context, NOTIFICATION_ID); 185 if (!accumulator.isEmpty()) { 186 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 187 Log.d(TAG, "blockingUpdateNewMessageIndicator: count=" + count + 188 ", isNew=" + isNew); 189 } 190 accumulator.first().deliver(context, isNew, count, threads.size()); 191 } 192 193 // And deals with delivery reports (which use Toasts). It's safe to call in a worker 194 // thread because the toast will eventually get posted to a handler. 195 delivery = getSmsNewDeliveryInfo(context); 196 if (delivery != null) { 197 delivery.deliver(context, isStatusMessage); 198 } 199 } 200 201 /** 202 * Updates all pending notifications, clearing or updating them as 203 * necessary. 204 */ 205 public static void blockingUpdateAllNotifications(final Context context) { 206 nonBlockingUpdateNewMessageIndicator(context, false, false); 207 updateSendFailedNotification(context); 208 updateDownloadFailedNotification(context); 209 } 210 211 private static final int accumulateNotificationInfo( 212 SortedSet set, MmsSmsNotificationInfo info) { 213 if (info != null) { 214 set.add(info); 215 216 return info.mCount; 217 } 218 219 return 0; 220 } 221 222 private static final class MmsSmsDeliveryInfo { 223 public CharSequence mTicker; 224 public long mTimeMillis; 225 226 public MmsSmsDeliveryInfo(CharSequence ticker, long timeMillis) { 227 mTicker = ticker; 228 mTimeMillis = timeMillis; 229 } 230 231 public void deliver(Context context, boolean isStatusMessage) { 232 updateDeliveryNotification( 233 context, isStatusMessage, mTicker, mTimeMillis); 234 } 235 } 236 237 private static final class MmsSmsNotificationInfo { 238 public Intent mClickIntent; 239 public String mDescription; 240 public int mIconResourceId; 241 public CharSequence mTicker; 242 public long mTimeMillis; 243 public String mTitle; 244 public int mCount; 245 246 public MmsSmsNotificationInfo( 247 Intent clickIntent, String description, int iconResourceId, 248 CharSequence ticker, long timeMillis, String title, int count) { 249 mClickIntent = clickIntent; 250 mDescription = description; 251 mIconResourceId = iconResourceId; 252 mTicker = ticker; 253 mTimeMillis = timeMillis; 254 mTitle = title; 255 mCount = count; 256 } 257 258 public void deliver(Context context, boolean isNew, int count, int uniqueThreads) { 259 updateNotification( 260 context, mClickIntent, mDescription, mIconResourceId, isNew, 261 (isNew? mTicker : null), // only display the ticker if the message is new 262 mTimeMillis, mTitle, count, uniqueThreads); 263 } 264 265 public long getTime() { 266 return mTimeMillis; 267 } 268 } 269 270 private static final class MmsSmsNotificationInfoComparator 271 implements Comparator<MmsSmsNotificationInfo> { 272 public int compare( 273 MmsSmsNotificationInfo info1, MmsSmsNotificationInfo info2) { 274 return Long.signum(info2.getTime() - info1.getTime()); 275 } 276 } 277 278 private static final MmsSmsNotificationInfo getMmsNewMessageNotificationInfo( 279 Context context, Set<Long> threads) { 280 ContentResolver resolver = context.getContentResolver(); 281 282 // This query looks like this when logged: 283 // I/Database( 147): elapsedTime4Sql|/data/data/com.android.providers.telephony/databases/ 284 // mmssms.db|0.362 ms|SELECT thread_id, date, _id, sub, sub_cs FROM pdu WHERE ((msg_box=1 285 // AND seen=0 AND (m_type=130 OR m_type=132))) ORDER BY date desc 286 287 Cursor cursor = SqliteWrapper.query(context, resolver, Mms.CONTENT_URI, 288 MMS_STATUS_PROJECTION, NEW_INCOMING_MM_CONSTRAINT, 289 null, Mms.DATE + " desc"); 290 291 if (cursor == null) { 292 return null; 293 } 294 295 try { 296 if (!cursor.moveToFirst()) { 297 return null; 298 } 299 long msgId = cursor.getLong(COLUMN_MMS_ID); 300 Uri msgUri = Mms.CONTENT_URI.buildUpon().appendPath( 301 Long.toString(msgId)).build(); 302 String address = AddressUtils.getFrom(context, msgUri); 303 String subject = getMmsSubject( 304 cursor.getString(COLUMN_SUBJECT), cursor.getInt(COLUMN_SUBJECT_CS)); 305 long threadId = cursor.getLong(COLUMN_THREAD_ID); 306 long timeMillis = cursor.getLong(COLUMN_DATE) * 1000; 307 308 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 309 Log.d(TAG, "getMmsNewMessageNotificationInfo: count=" + cursor.getCount() + 310 ", first addr = " + address + ", thread_id=" + threadId); 311 } 312 313 MmsSmsNotificationInfo info = getNewMessageNotificationInfo( 314 address, subject, context, 315 R.drawable.stat_notify_mms, null, threadId, 316 timeMillis, cursor.getCount()); 317 318 threads.add(threadId); 319 while (cursor.moveToNext()) { 320 threads.add(cursor.getLong(COLUMN_THREAD_ID)); 321 } 322 323 return info; 324 } finally { 325 cursor.close(); 326 } 327 } 328 329 private static final MmsSmsDeliveryInfo getSmsNewDeliveryInfo(Context context) { 330 ContentResolver resolver = context.getContentResolver(); 331 Cursor cursor = SqliteWrapper.query(context, resolver, Sms.CONTENT_URI, 332 SMS_STATUS_PROJECTION, NEW_DELIVERY_SM_CONSTRAINT, 333 null, Sms.DATE + " desc"); 334 335 if (cursor == null) 336 return null; 337 338 try { 339 if (!cursor.moveToFirst()) 340 return null; 341 342 String address = cursor.getString(COLUMN_SMS_ADDRESS); 343 long timeMillis = 3000; 344 345 return new MmsSmsDeliveryInfo(String.format( 346 context.getString(R.string.delivery_toast_body), address), 347 timeMillis); 348 349 } finally { 350 cursor.close(); 351 } 352 } 353 354 private static final MmsSmsNotificationInfo getSmsNewMessageNotificationInfo( 355 Context context, Set<Long> threads) { 356 ContentResolver resolver = context.getContentResolver(); 357 Cursor cursor = SqliteWrapper.query(context, resolver, Sms.CONTENT_URI, 358 SMS_STATUS_PROJECTION, NEW_INCOMING_SM_CONSTRAINT, 359 null, Sms.DATE + " desc"); 360 361 if (cursor == null) { 362 return null; 363 } 364 365 try { 366 if (!cursor.moveToFirst()) { 367 return null; 368 } 369 370 String address = cursor.getString(COLUMN_SMS_ADDRESS); 371 String body = cursor.getString(COLUMN_SMS_BODY); 372 long threadId = cursor.getLong(COLUMN_THREAD_ID); 373 long timeMillis = cursor.getLong(COLUMN_DATE); 374 375 //if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) 376 { 377 Log.d(TAG, "getSmsNewMessageNotificationInfo: count=" + cursor.getCount() + 378 ", first addr=" + address + ", thread_id=" + threadId); 379 } 380 381 MmsSmsNotificationInfo info = getNewMessageNotificationInfo( 382 address, body, context, R.drawable.stat_notify_sms, 383 null, threadId, timeMillis, cursor.getCount()); 384 385 threads.add(threadId); 386 while (cursor.moveToNext()) { 387 threads.add(cursor.getLong(COLUMN_THREAD_ID)); 388 } 389 390 return info; 391 } finally { 392 cursor.close(); 393 } 394 } 395 396 private static final MmsSmsNotificationInfo getNewMessageNotificationInfo( 397 String address, 398 String body, 399 Context context, 400 int iconResourceId, 401 String subject, 402 long threadId, 403 long timeMillis, 404 int count) { 405 Intent clickIntent = ComposeMessageActivity.createIntent(context, threadId); 406 clickIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK 407 | Intent.FLAG_ACTIVITY_SINGLE_TOP 408 | Intent.FLAG_ACTIVITY_CLEAR_TOP); 409 410 String senderInfo = buildTickerMessage( 411 context, address, null, null).toString(); 412 String senderInfoName = senderInfo.substring( 413 0, senderInfo.length() - 2); 414 CharSequence ticker = buildTickerMessage( 415 context, address, subject, body); 416 417 return new MmsSmsNotificationInfo( 418 clickIntent, body, iconResourceId, ticker, timeMillis, 419 senderInfoName, count); 420 } 421 422 public static void cancelNotification(Context context, int notificationId) { 423 NotificationManager nm = (NotificationManager) context.getSystemService( 424 Context.NOTIFICATION_SERVICE); 425 426 nm.cancel(notificationId); 427 } 428 429 private static void updateDeliveryNotification(final Context context, 430 boolean isStatusMessage, 431 final CharSequence message, 432 final long timeMillis) { 433 if (!isStatusMessage) { 434 return; 435 } 436 437 SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); 438 439 if (!sp.getBoolean(MessagingPreferenceActivity.NOTIFICATION_ENABLED, true)) { 440 return; 441 } 442 443 mToastHandler.post(new Runnable() { 444 public void run() { 445 Toast.makeText(context, message, (int)timeMillis).show(); 446 } 447 }); 448 } 449 450 private static void updateNotification( 451 Context context, 452 Intent clickIntent, 453 String description, 454 int iconRes, 455 boolean isNew, 456 CharSequence ticker, 457 long timeMillis, 458 String title, 459 int messageCount, 460 int uniqueThreadCount) { 461 SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); 462 463 if (!sp.getBoolean(MessagingPreferenceActivity.NOTIFICATION_ENABLED, true)) { 464 return; 465 } 466 467 Notification notification = new Notification(iconRes, ticker, timeMillis); 468 469 // If we have more than one unique thread, change the title (which would 470 // normally be the contact who sent the message) to a generic one that 471 // makes sense for multiple senders, and change the Intent to take the 472 // user to the conversation list instead of the specific thread. 473 if (uniqueThreadCount > 1) { 474 title = context.getString(R.string.notification_multiple_title); 475 clickIntent = new Intent(Intent.ACTION_MAIN); 476 477 clickIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK 478 | Intent.FLAG_ACTIVITY_SINGLE_TOP 479 | Intent.FLAG_ACTIVITY_CLEAR_TOP); 480 481 clickIntent.setType("vnd.android-dir/mms-sms"); 482 } 483 484 // If there is more than one message, change the description (which 485 // would normally be a snippet of the individual message text) to 486 // a string indicating how many "unseen" messages there are. 487 if (messageCount > 1) { 488 description = context.getString(R.string.notification_multiple, 489 Integer.toString(messageCount)); 490 } 491 492 // Make a startActivity() PendingIntent for the notification. 493 PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, clickIntent, 494 PendingIntent.FLAG_UPDATE_CURRENT); 495 496 // Update the notification. 497 notification.setLatestEventInfo(context, title, description, pendingIntent); 498 499 if (isNew) { 500 String vibrateWhen; 501 if (sp.contains(MessagingPreferenceActivity.NOTIFICATION_VIBRATE_WHEN)) { 502 vibrateWhen = 503 sp.getString(MessagingPreferenceActivity.NOTIFICATION_VIBRATE_WHEN, null); 504 } else if (sp.contains(MessagingPreferenceActivity.NOTIFICATION_VIBRATE)) { 505 vibrateWhen = sp.getBoolean(MessagingPreferenceActivity.NOTIFICATION_VIBRATE, false) ? 506 context.getString(R.string.prefDefault_vibrate_true) : 507 context.getString(R.string.prefDefault_vibrate_false); 508 } else { 509 vibrateWhen = context.getString(R.string.prefDefault_vibrateWhen); 510 } 511 512 boolean vibrateAlways = vibrateWhen.equals("always"); 513 boolean vibrateSilent = vibrateWhen.equals("silent"); 514 AudioManager audioManager = 515 (AudioManager)context.getSystemService(Context.AUDIO_SERVICE); 516 boolean nowSilent = 517 audioManager.getRingerMode() == AudioManager.RINGER_MODE_VIBRATE; 518 519 if (vibrateAlways || vibrateSilent && nowSilent) { 520 notification.defaults |= Notification.DEFAULT_VIBRATE; 521 } 522 523 String ringtoneStr = sp.getString(MessagingPreferenceActivity.NOTIFICATION_RINGTONE, 524 null); 525 notification.sound = TextUtils.isEmpty(ringtoneStr) ? null : Uri.parse(ringtoneStr); 526 } 527 528 notification.flags |= Notification.FLAG_SHOW_LIGHTS; 529 notification.defaults |= Notification.DEFAULT_LIGHTS; 530 531 // set up delete intent 532 notification.deleteIntent = PendingIntent.getBroadcast(context, 0, 533 sNotificationOnDeleteIntent, 0); 534 535 NotificationManager nm = (NotificationManager) 536 context.getSystemService(Context.NOTIFICATION_SERVICE); 537 538 nm.notify(NOTIFICATION_ID, notification); 539 } 540 541 protected static CharSequence buildTickerMessage( 542 Context context, String address, String subject, String body) { 543 String displayAddress = Contact.get(address, true).getName(); 544 545 StringBuilder buf = new StringBuilder( 546 displayAddress == null 547 ? "" 548 : displayAddress.replace('\n', ' ').replace('\r', ' ')); 549 buf.append(':').append(' '); 550 551 int offset = buf.length(); 552 if (!TextUtils.isEmpty(subject)) { 553 subject = subject.replace('\n', ' ').replace('\r', ' '); 554 buf.append(subject); 555 buf.append(' '); 556 } 557 558 if (!TextUtils.isEmpty(body)) { 559 body = body.replace('\n', ' ').replace('\r', ' '); 560 buf.append(body); 561 } 562 563 SpannableString spanText = new SpannableString(buf.toString()); 564 spanText.setSpan(new StyleSpan(Typeface.BOLD), 0, offset, 565 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 566 567 return spanText; 568 } 569 570 private static String getMmsSubject(String sub, int charset) { 571 return TextUtils.isEmpty(sub) ? "" 572 : new EncodedStringValue(charset, PduPersister.getBytes(sub)).getString(); 573 } 574 575 public static void notifyDownloadFailed(Context context, long threadId) { 576 notifyFailed(context, true, threadId, false); 577 } 578 579 public static void notifySendFailed(Context context) { 580 notifyFailed(context, false, 0, false); 581 } 582 583 public static void notifySendFailed(Context context, boolean noisy) { 584 notifyFailed(context, false, 0, noisy); 585 } 586 587 private static void notifyFailed(Context context, boolean isDownload, long threadId, 588 boolean noisy) { 589 // TODO factor out common code for creating notifications 590 SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); 591 592 boolean enabled = sp.getBoolean(MessagingPreferenceActivity.NOTIFICATION_ENABLED, true); 593 if (!enabled) { 594 return; 595 } 596 597 NotificationManager nm = (NotificationManager) 598 context.getSystemService(Context.NOTIFICATION_SERVICE); 599 600 // Strategy: 601 // a. If there is a single failure notification, tapping on the notification goes 602 // to the compose view. 603 // b. If there are two failure it stays in the thread view. Selecting one undelivered 604 // thread will dismiss one undelivered notification but will still display the 605 // notification.If you select the 2nd undelivered one it will dismiss the notification. 606 607 long[] msgThreadId = {0}; 608 int totalFailedCount = getUndeliveredMessageCount(context, msgThreadId); 609 610 Intent failedIntent; 611 Notification notification = new Notification(); 612 String title; 613 String description; 614 if (totalFailedCount > 1) { 615 description = context.getString(R.string.notification_failed_multiple, 616 Integer.toString(totalFailedCount)); 617 title = context.getString(R.string.notification_failed_multiple_title); 618 619 failedIntent = new Intent(context, ConversationList.class); 620 } else { 621 title = isDownload ? 622 context.getString(R.string.message_download_failed_title) : 623 context.getString(R.string.message_send_failed_title); 624 625 description = context.getString(R.string.message_failed_body); 626 failedIntent = new Intent(context, ComposeMessageActivity.class); 627 if (isDownload) { 628 // When isDownload is true, the valid threadId is passed into this function. 629 failedIntent.putExtra("failed_download_flag", true); 630 } else { 631 threadId = (msgThreadId[0] != 0 ? msgThreadId[0] : 0); 632 failedIntent.putExtra("undelivered_flag", true); 633 } 634 failedIntent.putExtra("thread_id", threadId); 635 } 636 637 failedIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); 638 PendingIntent pendingIntent = PendingIntent.getActivity( 639 context, 0, failedIntent, PendingIntent.FLAG_UPDATE_CURRENT); 640 641 notification.icon = R.drawable.stat_notify_sms_failed; 642 643 notification.tickerText = title; 644 645 notification.setLatestEventInfo(context, title, description, pendingIntent); 646 647 if (noisy) { 648 boolean vibrate = sp.getBoolean(MessagingPreferenceActivity.NOTIFICATION_VIBRATE, 649 false /* don't vibrate by default */); 650 if (vibrate) { 651 notification.defaults |= Notification.DEFAULT_VIBRATE; 652 } 653 654 String ringtoneStr = sp.getString(MessagingPreferenceActivity.NOTIFICATION_RINGTONE, 655 null); 656 notification.sound = TextUtils.isEmpty(ringtoneStr) ? null : Uri.parse(ringtoneStr); 657 } 658 659 if (isDownload) { 660 nm.notify(DOWNLOAD_FAILED_NOTIFICATION_ID, notification); 661 } else { 662 nm.notify(MESSAGE_FAILED_NOTIFICATION_ID, notification); 663 } 664 } 665 666 // threadIdResult[0] contains the thread id of the first message. 667 // threadIdResult[1] is nonzero if the thread ids of all the messages are the same. 668 // You can pass in null for threadIdResult. 669 // You can pass in a threadIdResult of size 1 to avoid the comparison of each thread id. 670 private static int getUndeliveredMessageCount(Context context, long[] threadIdResult) { 671 Cursor undeliveredCursor = SqliteWrapper.query(context, context.getContentResolver(), 672 UNDELIVERED_URI, new String[] { Mms.THREAD_ID }, "read=0", null, null); 673 if (undeliveredCursor == null) { 674 return 0; 675 } 676 int count = undeliveredCursor.getCount(); 677 try { 678 if (threadIdResult != null && undeliveredCursor.moveToFirst()) { 679 threadIdResult[0] = undeliveredCursor.getLong(0); 680 681 if (threadIdResult.length >= 2) { 682 // Test to see if all the undelivered messages belong to the same thread. 683 long firstId = threadIdResult[0]; 684 while (undeliveredCursor.moveToNext()) { 685 if (undeliveredCursor.getLong(0) != firstId) { 686 firstId = 0; 687 break; 688 } 689 } 690 threadIdResult[1] = firstId; // non-zero if all ids are the same 691 } 692 } 693 } finally { 694 undeliveredCursor.close(); 695 } 696 return count; 697 } 698 699 public static void updateSendFailedNotification(Context context) { 700 if (getUndeliveredMessageCount(context, null) < 1) { 701 cancelNotification(context, MESSAGE_FAILED_NOTIFICATION_ID); 702 } else { 703 notifySendFailed(context); // rebuild and adjust the message count if necessary. 704 } 705 } 706 707 /** 708 * If all the undelivered messages belong to "threadId", cancel the notification. 709 */ 710 public static void updateSendFailedNotificationForThread(Context context, long threadId) { 711 long[] msgThreadId = {0, 0}; 712 if (getUndeliveredMessageCount(context, msgThreadId) > 0 713 && msgThreadId[0] == threadId 714 && msgThreadId[1] != 0) { 715 cancelNotification(context, MESSAGE_FAILED_NOTIFICATION_ID); 716 } 717 } 718 719 private static int getDownloadFailedMessageCount(Context context) { 720 // Look for any messages in the MMS Inbox that are of the type 721 // NOTIFICATION_IND (i.e. not already downloaded) and in the 722 // permanent failure state. If there are none, cancel any 723 // failed download notification. 724 Cursor c = SqliteWrapper.query(context, context.getContentResolver(), 725 Mms.Inbox.CONTENT_URI, null, 726 Mms.MESSAGE_TYPE + "=" + 727 String.valueOf(PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND) + 728 " AND " + Mms.STATUS + "=" + 729 String.valueOf(DownloadManager.STATE_PERMANENT_FAILURE), 730 null, null); 731 if (c == null) { 732 return 0; 733 } 734 int count = c.getCount(); 735 c.close(); 736 return count; 737 } 738 739 public static void updateDownloadFailedNotification(Context context) { 740 if (getDownloadFailedMessageCount(context) < 1) { 741 cancelNotification(context, DOWNLOAD_FAILED_NOTIFICATION_ID); 742 } 743 } 744 745 public static boolean isFailedToDeliver(Intent intent) { 746 return (intent != null) && intent.getBooleanExtra("undelivered_flag", false); 747 } 748 749 public static boolean isFailedToDownload(Intent intent) { 750 return (intent != null) && intent.getBooleanExtra("failed_download_flag", false); 751 } 752 753 754 } 755