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