MessagingNotification.java revision 810fae756f4d2db00b35d7c9dfead252737abdac
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 18package com.android.mms.transaction; 19 20import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND; 21import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF; 22 23import com.android.mms.R; 24import com.android.mms.ui.ComposeMessageActivity; 25import com.android.mms.ui.ConversationList; 26import com.android.mms.ui.MessagingPreferenceActivity; 27import com.android.mms.util.AddressUtils; 28import com.android.mms.util.ContactInfoCache; 29import com.android.mms.util.DownloadManager; 30 31import com.google.android.mms.pdu.EncodedStringValue; 32import com.google.android.mms.pdu.PduHeaders; 33import com.google.android.mms.pdu.PduPersister; 34import com.google.android.mms.util.SqliteWrapper; 35 36import android.app.Notification; 37import android.app.NotificationManager; 38import android.app.PendingIntent; 39import android.content.ContentResolver; 40import android.content.Context; 41import android.content.Intent; 42import android.content.SharedPreferences; 43import android.database.Cursor; 44import android.graphics.Typeface; 45import android.net.Uri; 46import android.preference.PreferenceManager; 47import android.provider.Telephony.Mms; 48import android.provider.Telephony.Sms; 49import android.provider.Telephony.Threads; 50import android.text.Spannable; 51import android.text.SpannableString; 52import android.text.TextUtils; 53import android.text.style.StyleSpan; 54import android.util.Log; 55 56import java.util.Comparator; 57import java.util.HashSet; 58import java.util.Set; 59import java.util.SortedSet; 60import java.util.TreeSet; 61 62/** 63 * This class is used to update the notification indicator. It will check whether 64 * there are unread messages. If yes, it would show the notification indicator, 65 * otherwise, hide the indicator. 66 */ 67public class MessagingNotification { 68 public static final String NOTIFICATION_CLICK_RECEIVER = 69 "com.android.mms.transaction.NotificationClickReceiver"; 70 private static final String TAG = "MessagingNotification"; 71 72 private static final int NOTIFICATION_ID = 123; 73 public static final int MESSAGE_FAILED_NOTIFICATION_ID = 789; 74 public static final int DOWNLOAD_FAILED_NOTIFICATION_ID = 531; 75 76 // This must be consistent with the column constants below. 77 private static final String[] MMS_STATUS_PROJECTION = new String[] { 78 Mms.THREAD_ID, Mms.DATE, Mms._ID, Mms.SUBJECT, Mms.SUBJECT_CHARSET }; 79 80 // This must be consistent with the column constants below. 81 private static final String[] SMS_STATUS_PROJECTION = new String[] { 82 Sms.THREAD_ID, Sms.DATE, Sms.ADDRESS, Sms.SUBJECT, Sms.BODY }; 83 84 // These must be consistent with MMS_STATUS_PROJECTION and 85 // SMS_STATUS_PROJECTION. 86 private static final int COLUMN_THREAD_ID = 0; 87 private static final int COLUMN_DATE = 1; 88 private static final int COLUMN_MMS_ID = 2; 89 private static final int COLUMN_SMS_ADDRESS = 2; 90 private static final int COLUMN_SUBJECT = 3; 91 private static final int COLUMN_SUBJECT_CS = 4; 92 private static final int COLUMN_SMS_BODY = 4; 93 94 private static final String NEW_INCOMING_SM_CONSTRAINT = 95 "(" + Sms.TYPE + " = " + Sms.MESSAGE_TYPE_INBOX 96 + " AND " + Sms.READ + " = 0)"; 97 98 private static final String NEW_INCOMING_MM_CONSTRAINT = 99 "(" + Mms.MESSAGE_BOX + "=" + Mms.MESSAGE_BOX_INBOX 100 + " AND " + Mms.READ + "=0" 101 + " AND (" + Mms.MESSAGE_TYPE + "=" + MESSAGE_TYPE_NOTIFICATION_IND 102 + " OR " + Mms.MESSAGE_TYPE + "=" + MESSAGE_TYPE_RETRIEVE_CONF + "))"; 103 104 private static final MmsSmsNotificationInfoComparator INFO_COMPARATOR = 105 new MmsSmsNotificationInfoComparator(); 106 107 private static final Uri UNDELIVERED_URI = Uri.parse("content://mms-sms/undelivered"); 108 109 private MessagingNotification() { 110 } 111 112 /** 113 * Checks to see if there are any unread messages or delivery 114 * reports. Shows the most recent notification if there is one. 115 * 116 * @param context the context to use 117 */ 118 public static void updateNewMessageIndicator(Context context) { 119 updateNewMessageIndicator(context, false); 120 } 121 122 /** 123 * Checks to see if there are any unread messages or delivery 124 * reports. Shows the most recent notification if there is one. 125 * 126 * @param context the context to use 127 * @param isNew if notify a new message comes, it should be true, otherwise, false. 128 */ 129 public static void updateNewMessageIndicator(Context context, boolean isNew) { 130 SortedSet<MmsSmsNotificationInfo> accumulator = 131 new TreeSet<MmsSmsNotificationInfo>(INFO_COMPARATOR); 132 Set<Long> threads = new HashSet<Long>(4); 133 134 int count = 0; 135 count += accumulateNotificationInfo( 136 accumulator, getMmsNewMessageNotificationInfo(context, threads)); 137 count += accumulateNotificationInfo( 138 accumulator, getSmsNewMessageNotificationInfo(context, threads)); 139 140 cancelNotification(context, NOTIFICATION_ID); 141 if (!accumulator.isEmpty()) { 142 accumulator.first().deliver(context, isNew, count, threads.size()); 143 } 144 } 145 146 /** 147 * Updates all pending notifications, clearing or updating them as 148 * necessary. This task is completed in the background on a worker 149 * thread. 150 */ 151 public static void updateAllNotifications(final Context context) { 152 new Thread(new Runnable() { 153 public void run() { 154 updateNewMessageIndicator(context); 155 updateSendFailedNotification(context); 156 updateDownloadFailedNotification(context); 157 } 158 }).start(); 159 } 160 161 /** 162 * Deletes any delivery report notifications for the specified 163 * thread, then checks to see if there are any unread messages or 164 * delivery reports. Shows the most recent notification if there 165 * is one. 166 * 167 * @param context the context to use 168 * @param threadId the thread for which to clear delivery notifications 169 */ 170 public static void updateNewMessageIndicator( 171 Context context, long threadId) { 172 updateNewMessageIndicator(context); 173 } 174 175 private static final int accumulateNotificationInfo( 176 SortedSet set, MmsSmsNotificationInfo info) { 177 if (info != null) { 178 set.add(info); 179 180 return info.mCount; 181 } 182 183 return 0; 184 } 185 186 private static final class MmsSmsNotificationInfo { 187 public Intent mClickIntent; 188 public String mDescription; 189 public int mIconResourceId; 190 public CharSequence mTicker; 191 public long mTimeMillis; 192 public String mTitle; 193 public int mCount; 194 195 public MmsSmsNotificationInfo( 196 Intent clickIntent, String description, int iconResourceId, 197 CharSequence ticker, long timeMillis, String title, int count) { 198 mClickIntent = clickIntent; 199 mDescription = description; 200 mIconResourceId = iconResourceId; 201 mTicker = ticker; 202 mTimeMillis = timeMillis; 203 mTitle = title; 204 mCount = count; 205 } 206 207 public void deliver(Context context, boolean isNew, int count, int uniqueThreads) { 208 updateNotification( 209 context, mClickIntent, mDescription, mIconResourceId, 210 isNew, mTicker, mTimeMillis, mTitle, count, uniqueThreads); 211 } 212 213 public long getTime() { 214 return mTimeMillis; 215 } 216 } 217 218 private static final class MmsSmsNotificationInfoComparator 219 implements Comparator<MmsSmsNotificationInfo> { 220 public int compare( 221 MmsSmsNotificationInfo info1, MmsSmsNotificationInfo info2) { 222 return Long.signum(info2.getTime() - info1.getTime()); 223 } 224 } 225 226 public static final MmsSmsNotificationInfo getMmsNewMessageNotificationInfo( 227 Context context, Set<Long> threads) { 228 ContentResolver resolver = context.getContentResolver(); 229 Cursor cursor = SqliteWrapper.query(context, resolver, Mms.CONTENT_URI, 230 MMS_STATUS_PROJECTION, NEW_INCOMING_MM_CONSTRAINT, 231 null, Mms.DATE + " desc"); 232 233 if (cursor == null) { 234 return null; 235 } 236 237 try { 238 if (!cursor.moveToFirst()) { 239 return null; 240 } 241 long msgId = cursor.getLong(COLUMN_MMS_ID); 242 Uri msgUri = Mms.CONTENT_URI.buildUpon().appendPath( 243 Long.toString(msgId)).build(); 244 String address = AddressUtils.getFrom(context, msgUri); 245 String subject = getMmsSubject( 246 cursor.getString(COLUMN_SUBJECT), cursor.getInt(COLUMN_SUBJECT_CS)); 247 long threadId = cursor.getLong(COLUMN_THREAD_ID); 248 long timeMillis = cursor.getLong(COLUMN_DATE) * 1000; 249 250 MmsSmsNotificationInfo info = getNewMessageNotificationInfo( 251 address, subject, context, 252 R.drawable.stat_notify_mms, null, threadId, 253 timeMillis, cursor.getCount()); 254 255 threads.add(threadId); 256 while (cursor.moveToNext()) { 257 threads.add(cursor.getLong(COLUMN_THREAD_ID)); 258 } 259 260 return info; 261 } finally { 262 cursor.close(); 263 } 264 } 265 266 public static final MmsSmsNotificationInfo getSmsNewMessageNotificationInfo( 267 Context context, Set<Long> threads) { 268 ContentResolver resolver = context.getContentResolver(); 269 Cursor cursor = SqliteWrapper.query(context, resolver, Sms.CONTENT_URI, 270 SMS_STATUS_PROJECTION, NEW_INCOMING_SM_CONSTRAINT, 271 null, Sms.DATE + " desc"); 272 273 if (cursor == null) { 274 return null; 275 } 276 277 try { 278 if (!cursor.moveToFirst()) { 279 return null; 280 } 281 282 String address = cursor.getString(COLUMN_SMS_ADDRESS); 283 String body = cursor.getString(COLUMN_SMS_BODY); 284 long threadId = cursor.getLong(COLUMN_THREAD_ID); 285 long timeMillis = cursor.getLong(COLUMN_DATE); 286 287 MmsSmsNotificationInfo info = getNewMessageNotificationInfo( 288 address, body, context, R.drawable.stat_notify_sms, 289 null, threadId, timeMillis, cursor.getCount()); 290 291 threads.add(threadId); 292 while (cursor.moveToNext()) { 293 threads.add(cursor.getLong(COLUMN_THREAD_ID)); 294 } 295 296 return info; 297 } finally { 298 cursor.close(); 299 } 300 } 301 302 private static final MmsSmsNotificationInfo getNewMessageNotificationInfo( 303 String address, 304 String body, 305 Context context, 306 int iconResourceId, 307 String subject, 308 long threadId, 309 long timeMillis, 310 int count) { 311 Intent clickIntent = getAppIntent(); 312 clickIntent.setData( 313 Uri.withAppendedPath( 314 clickIntent.getData(), Long.toString(threadId))); 315 clickIntent.setAction(Intent.ACTION_VIEW); 316 317 String senderInfo = buildTickerMessage( 318 context, address, null, null).toString(); 319 String senderInfoName = senderInfo.substring( 320 0, senderInfo.length() - 2); 321 CharSequence ticker = buildTickerMessage( 322 context, address, subject, body); 323 324 return new MmsSmsNotificationInfo( 325 clickIntent, body, iconResourceId, ticker, timeMillis, 326 senderInfoName, count); 327 } 328 329 public static void cancelNotification(Context context, int notificationId) { 330 NotificationManager nm = (NotificationManager) context.getSystemService( 331 Context.NOTIFICATION_SERVICE); 332 333 nm.cancel(notificationId); 334 } 335 336 private static Intent getAppIntent() { 337 Intent appIntent = new Intent(Intent.ACTION_MAIN, Threads.CONTENT_URI); 338 339 appIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); 340 return appIntent; 341 } 342 343 private static void updateNotification( 344 Context context, 345 Intent clickIntent, 346 String description, 347 int iconRes, 348 boolean isNew, 349 CharSequence ticker, 350 long timeMillis, 351 String title, 352 int messageCount, 353 int uniqueThreadCount) { 354 SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); 355 356 if (!sp.getBoolean( 357 MessagingPreferenceActivity.NOTIFICATION_ENABLED, true)) { 358 return; 359 } 360 361 Notification notification = new Notification(iconRes, ticker, timeMillis); 362 363 // If we have more than one unique thread, change the title (which would 364 // normally be the contact who sent the message) to a generic one that 365 // makes sense for multiple senders, and change the Intent to take the 366 // user to the conversation list instead of the specific thread. 367 if (uniqueThreadCount > 1) { 368 title = context.getString(R.string.notification_multiple_title); 369 clickIntent = getAppIntent(); 370 clickIntent.setAction(Intent.ACTION_MAIN); 371 clickIntent.setType("vnd.android-dir/mms-sms"); 372 } 373 374 // If there is more than one message, change the description (which 375 // would normally be a snippet of the individual message text) to 376 // a string indicating how many unread messages there are. 377 if (messageCount > 1) { 378 description = context.getString(R.string.notification_multiple, 379 Integer.toString(messageCount)); 380 } 381 382 // Make a startActivity() PendingIntent for the notification. 383 PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, clickIntent, 384 PendingIntent.FLAG_UPDATE_CURRENT); 385 386 // Update the notification. 387 notification.setLatestEventInfo(context, title, description, pendingIntent); 388 389 if (isNew) { 390 boolean vibrate = sp.getBoolean(MessagingPreferenceActivity.NOTIFICATION_VIBRATE, true); 391 if (vibrate) { 392 notification.defaults |= Notification.DEFAULT_VIBRATE; 393 } 394 395 String ringtoneStr = sp 396 .getString(MessagingPreferenceActivity.NOTIFICATION_RINGTONE, null); 397 notification.sound = TextUtils.isEmpty(ringtoneStr) ? null : Uri.parse(ringtoneStr); 398 } 399 400 notification.flags |= Notification.FLAG_SHOW_LIGHTS; 401 notification.ledARGB = 0xff00ff00; 402 notification.ledOnMS = 500; 403 notification.ledOffMS = 2000; 404 405 NotificationManager nm = (NotificationManager) 406 context.getSystemService(Context.NOTIFICATION_SERVICE); 407 408 nm.notify(NOTIFICATION_ID, notification); 409 } 410 411 protected static CharSequence buildTickerMessage( 412 Context context, String address, String subject, String body) { 413 String displayAddress = ContactInfoCache.getInstance() 414 .getContactName(context, address); 415 416 StringBuilder buf = new StringBuilder( 417 displayAddress == null 418 ? "" 419 : displayAddress.replace('\n', ' ').replace('\r', ' ')); 420 buf.append(':').append(' '); 421 422 int offset = buf.length(); 423 if (!TextUtils.isEmpty(subject)) { 424 subject = subject.replace('\n', ' ').replace('\r', ' '); 425 buf.append(subject); 426 buf.append(' '); 427 } 428 429 if (!TextUtils.isEmpty(body)) { 430 body = body.replace('\n', ' ').replace('\r', ' '); 431 buf.append(body); 432 } 433 434 SpannableString spanText = new SpannableString(buf.toString()); 435 spanText.setSpan(new StyleSpan(Typeface.BOLD), 0, offset, 436 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 437 438 return spanText; 439 } 440 441 private static String getMmsSubject(String sub, int charset) { 442 return TextUtils.isEmpty(sub) ? "" 443 : new EncodedStringValue(charset, PduPersister.getBytes(sub)).getString(); 444 } 445 446 public static void notifyDownloadFailed(Context context, long threadId) { 447 notifyFailed(context, true, threadId, false); 448 } 449 450 public static void notifySendFailed(Context context) { 451 notifyFailed(context, false, 0, false); 452 } 453 454 public static void notifySendFailed(Context context, boolean noisy) { 455 notifyFailed(context, false, 0, noisy); 456 } 457 458 private static void notifyFailed(Context context, boolean isDownload, long threadId, 459 boolean noisy) { 460 // TODO factor out common code for creating notifications 461 SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); 462 463 boolean enabled = sp.getBoolean(MessagingPreferenceActivity.NOTIFICATION_ENABLED, true); 464 if (!enabled) { 465 return; 466 } 467 468 NotificationManager nm = (NotificationManager) 469 context.getSystemService(Context.NOTIFICATION_SERVICE); 470 471 // Strategy: 472 // a. If there is a single failure notification, tapping on the notification goes 473 // to the compose view. 474 // b. If there are two failure it stays in the thread view. Selecting one undelivered 475 // thread will dismiss one undelivered notification but will still display the 476 // notification.If you select the 2nd undelivered one it will dismiss the notification. 477 478 long[] msgThreadId = {0}; 479 int totalFailedCount = getUndeliveredMessageCount(context, msgThreadId); 480 481 Intent failedIntent; 482 Notification notification = new Notification(); 483 String title; 484 String description; 485 if (totalFailedCount > 1) { 486 description = context.getString(R.string.notification_failed_multiple, 487 Integer.toString(totalFailedCount)); 488 title = context.getString(R.string.notification_failed_multiple_title); 489 490 failedIntent = new Intent(context, ConversationList.class); 491 } else { 492 title = isDownload ? 493 context.getString(R.string.message_download_failed_title) : 494 context.getString(R.string.message_send_failed_title); 495 496 description = context.getString(R.string.message_failed_body); 497 threadId = (msgThreadId[0] != 0 ? msgThreadId[0] : 0); 498 499 failedIntent = new Intent(context, ComposeMessageActivity.class); 500 failedIntent.putExtra("thread_id", threadId); 501 failedIntent.putExtra("undelivered_flag", true); 502 } 503 504 failedIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); 505 PendingIntent pendingIntent = PendingIntent.getActivity( 506 context, 0, failedIntent, PendingIntent.FLAG_UPDATE_CURRENT); 507 508 notification.icon = R.drawable.stat_notify_sms_failed; 509 510 notification.tickerText = title; 511 512 notification.setLatestEventInfo(context, title, description, pendingIntent); 513 514 if (noisy) { 515 boolean vibrate = sp.getBoolean(MessagingPreferenceActivity.NOTIFICATION_VIBRATE, true); 516 if (vibrate) { 517 notification.defaults |= Notification.DEFAULT_VIBRATE; 518 } 519 520 String ringtoneStr = sp.getString(MessagingPreferenceActivity.NOTIFICATION_RINGTONE, null); 521 notification.sound = TextUtils.isEmpty(ringtoneStr) ? null : Uri.parse(ringtoneStr); 522 } 523 524 if (isDownload) { 525 nm.notify(DOWNLOAD_FAILED_NOTIFICATION_ID, notification); 526 } else { 527 nm.notify(MESSAGE_FAILED_NOTIFICATION_ID, notification); 528 } 529 } 530 531 // threadIdResult[0] contains the thread id of the first message. 532 // threadIdResult[1] is nonzero if the thread ids of all the messages are the same. 533 // You can pass in null for threadIdResult. 534 // You can pass in a threadIdResult of size 1 to avoid the comparison of each thread id. 535 private static int getUndeliveredMessageCount(Context context, long[] threadIdResult) { 536 Cursor undeliveredCursor = SqliteWrapper.query(context, context.getContentResolver(), 537 UNDELIVERED_URI, new String[] { Mms.THREAD_ID }, "read=0", null, null); 538 if (undeliveredCursor == null) { 539 return 0; 540 } 541 int count = undeliveredCursor.getCount(); 542 try { 543 if (threadIdResult != null && undeliveredCursor.moveToFirst()) { 544 threadIdResult[0] = undeliveredCursor.getLong(0); 545 546 if (threadIdResult.length >= 2) { 547 // Test to see if all the undelivered messages belong to the same thread. 548 long firstId = threadIdResult[0]; 549 while (undeliveredCursor.moveToNext()) { 550 if (undeliveredCursor.getLong(0) != firstId) { 551 firstId = 0; 552 break; 553 } 554 } 555 threadIdResult[1] = firstId; // non-zero if all ids are the same 556 } 557 } 558 } finally { 559 undeliveredCursor.close(); 560 } 561 return count; 562 } 563 564 public static void updateSendFailedNotification(Context context) { 565 if (getUndeliveredMessageCount(context, null) < 1) { 566 cancelNotification(context, MESSAGE_FAILED_NOTIFICATION_ID); 567 } else { 568 notifySendFailed(context); // rebuild and adjust the message count if necessary. 569 } 570 } 571 572 /** 573 * If all the undelivered messages belong to "threadId", cancel the notification. 574 */ 575 public static void updateSendFailedNotificationForThread(Context context, long threadId) { 576 long[] msgThreadId = {0, 0}; 577 if (getUndeliveredMessageCount(context, msgThreadId) > 0 578 && msgThreadId[0] == threadId 579 && msgThreadId[1] != 0) { 580 cancelNotification(context, MESSAGE_FAILED_NOTIFICATION_ID); 581 } 582 } 583 584 private static int getDownloadFailedMessageCount(Context context) { 585 // Look for any messages in the MMS Inbox that are of the type 586 // NOTIFICATION_IND (i.e. not already downloaded) and in the 587 // permanent failure state. If there are none, cancel any 588 // failed download notification. 589 Cursor c = SqliteWrapper.query(context, context.getContentResolver(), 590 Mms.Inbox.CONTENT_URI, null, 591 Mms.MESSAGE_TYPE + "=" + 592 String.valueOf(PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND) + 593 " AND " + Mms.STATUS + "=" + 594 String.valueOf(DownloadManager.STATE_PERMANENT_FAILURE), 595 null, null); 596 if (c == null) { 597 return 0; 598 } 599 int count = c.getCount(); 600 c.close(); 601 return count; 602 } 603 604 public static void updateDownloadFailedNotification(Context context) { 605 if (getDownloadFailedMessageCount(context) < 1) { 606 cancelNotification(context, DOWNLOAD_FAILED_NOTIFICATION_ID); 607 } 608 } 609} 610