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