1 /* 2 * Copyright (C) 2008 Esmertec AG. 3 * Copyright (C) 2008 The Android Open Source Project 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18 package com.android.mms.transaction; 19 20 import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND; 21 import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF; 22 23 import java.util.ArrayList; 24 import java.util.Comparator; 25 import java.util.HashSet; 26 import java.util.Iterator; 27 import java.util.Set; 28 import java.util.SortedSet; 29 import java.util.TreeSet; 30 31 import android.app.Notification; 32 import android.app.NotificationManager; 33 import android.app.PendingIntent; 34 import android.app.TaskStackBuilder; 35 import android.content.BroadcastReceiver; 36 import android.content.ContentResolver; 37 import android.content.Context; 38 import android.content.Intent; 39 import android.content.IntentFilter; 40 import android.content.SharedPreferences; 41 import android.content.res.Resources; 42 import android.database.Cursor; 43 import android.database.sqlite.SqliteWrapper; 44 import android.graphics.Bitmap; 45 import android.graphics.Typeface; 46 import android.graphics.drawable.BitmapDrawable; 47 import android.media.AudioManager; 48 import android.net.Uri; 49 import android.os.AsyncTask; 50 import android.os.Handler; 51 import android.preference.PreferenceManager; 52 import android.provider.Telephony.Mms; 53 import android.provider.Telephony.Sms; 54 import android.text.Spannable; 55 import android.text.SpannableString; 56 import android.text.SpannableStringBuilder; 57 import android.text.TextUtils; 58 import android.text.style.StyleSpan; 59 import android.text.style.TextAppearanceSpan; 60 import android.util.Log; 61 import android.widget.Toast; 62 63 import com.android.mms.LogTag; 64 import com.android.mms.R; 65 import com.android.mms.data.Contact; 66 import com.android.mms.data.Conversation; 67 import com.android.mms.data.WorkingMessage; 68 import com.android.mms.model.SlideModel; 69 import com.android.mms.model.SlideshowModel; 70 import com.android.mms.ui.ComposeMessageActivity; 71 import com.android.mms.ui.ConversationList; 72 import com.android.mms.ui.MessageUtils; 73 import com.android.mms.ui.MessagingPreferenceActivity; 74 import com.android.mms.util.AddressUtils; 75 import com.android.mms.util.DownloadManager; 76 import com.android.mms.widget.MmsWidgetProvider; 77 import com.google.android.mms.MmsException; 78 import com.google.android.mms.pdu.EncodedStringValue; 79 import com.google.android.mms.pdu.GenericPdu; 80 import com.google.android.mms.pdu.MultimediaMessagePdu; 81 import com.google.android.mms.pdu.PduHeaders; 82 import com.google.android.mms.pdu.PduPersister; 83 84 /** 85 * This class is used to update the notification indicator. It will check whether 86 * there are unread messages. If yes, it would show the notification indicator, 87 * otherwise, hide the indicator. 88 */ 89 public class MessagingNotification { 90 91 private static final String TAG = LogTag.APP; 92 private static final boolean DEBUG = false; 93 94 private static final int NOTIFICATION_ID = 123; 95 public static final int MESSAGE_FAILED_NOTIFICATION_ID = 789; 96 public static final int DOWNLOAD_FAILED_NOTIFICATION_ID = 531; 97 /** 98 * This is the volume at which to play the in-conversation notification sound, 99 * expressed as a fraction of the system notification volume. 100 */ 101 private static final float IN_CONVERSATION_NOTIFICATION_VOLUME = 0.25f; 102 103 // This must be consistent with the column constants below. 104 private static final String[] MMS_STATUS_PROJECTION = new String[] { 105 Mms.THREAD_ID, Mms.DATE, Mms._ID, Mms.SUBJECT, Mms.SUBJECT_CHARSET }; 106 107 // This must be consistent with the column constants below. 108 private static final String[] SMS_STATUS_PROJECTION = new String[] { 109 Sms.THREAD_ID, Sms.DATE, Sms.ADDRESS, Sms.SUBJECT, Sms.BODY }; 110 111 // These must be consistent with MMS_STATUS_PROJECTION and 112 // SMS_STATUS_PROJECTION. 113 private static final int COLUMN_THREAD_ID = 0; 114 private static final int COLUMN_DATE = 1; 115 private static final int COLUMN_MMS_ID = 2; 116 private static final int COLUMN_SMS_ADDRESS = 2; 117 private static final int COLUMN_SUBJECT = 3; 118 private static final int COLUMN_SUBJECT_CS = 4; 119 private static final int COLUMN_SMS_BODY = 4; 120 121 private static final String[] SMS_THREAD_ID_PROJECTION = new String[] { Sms.THREAD_ID }; 122 private static final String[] MMS_THREAD_ID_PROJECTION = new String[] { Mms.THREAD_ID }; 123 124 private static final String NEW_INCOMING_SM_CONSTRAINT = 125 "(" + Sms.TYPE + " = " + Sms.MESSAGE_TYPE_INBOX 126 + " AND " + Sms.SEEN + " = 0)"; 127 128 private static final String NEW_DELIVERY_SM_CONSTRAINT = 129 "(" + Sms.TYPE + " = " + Sms.MESSAGE_TYPE_SENT 130 + " AND " + Sms.STATUS + " = "+ Sms.STATUS_COMPLETE +")"; 131 132 private static final String NEW_INCOMING_MM_CONSTRAINT = 133 "(" + Mms.MESSAGE_BOX + "=" + Mms.MESSAGE_BOX_INBOX 134 + " AND " + Mms.SEEN + "=0" 135 + " AND (" + Mms.MESSAGE_TYPE + "=" + MESSAGE_TYPE_NOTIFICATION_IND 136 + " OR " + Mms.MESSAGE_TYPE + "=" + MESSAGE_TYPE_RETRIEVE_CONF + "))"; 137 138 private static final NotificationInfoComparator INFO_COMPARATOR = 139 new NotificationInfoComparator(); 140 141 private static final Uri UNDELIVERED_URI = Uri.parse("content://mms-sms/undelivered"); 142 143 144 private final static String NOTIFICATION_DELETED_ACTION = 145 "com.android.mms.NOTIFICATION_DELETED_ACTION"; 146 147 public static class OnDeletedReceiver extends BroadcastReceiver { 148 @Override onReceive(Context context, Intent intent)149 public void onReceive(Context context, Intent intent) { 150 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 151 Log.d(TAG, "[MessagingNotification] clear notification: mark all msgs seen"); 152 } 153 154 Conversation.markAllConversationsAsSeen(context); 155 } 156 } 157 158 public static final long THREAD_ALL = -1; 159 public static final long THREAD_NONE = -2; 160 /** 161 * Keeps track of the thread ID of the conversation that's currently displayed to the user 162 */ 163 private static long sCurrentlyDisplayedThreadId; 164 private static final Object sCurrentlyDisplayedThreadLock = new Object(); 165 166 private static OnDeletedReceiver sNotificationDeletedReceiver = new OnDeletedReceiver(); 167 private static Intent sNotificationOnDeleteIntent; 168 private static Handler sToastHandler = new Handler(); 169 private static PduPersister sPduPersister; 170 private static final int MAX_BITMAP_DIMEN_DP = 360; 171 private static float sScreenDensity; 172 173 private static final int MAX_MESSAGES_TO_SHOW = 8; // the maximum number of new messages to 174 // show in a single notification. 175 176 MessagingNotification()177 private MessagingNotification() { 178 } 179 init(Context context)180 public static void init(Context context) { 181 // set up the intent filter for notification deleted action 182 IntentFilter intentFilter = new IntentFilter(); 183 intentFilter.addAction(NOTIFICATION_DELETED_ACTION); 184 185 // TODO: should we unregister when the app gets killed? 186 context.registerReceiver(sNotificationDeletedReceiver, intentFilter); 187 sPduPersister = PduPersister.getPduPersister(context); 188 189 // initialize the notification deleted action 190 sNotificationOnDeleteIntent = new Intent(NOTIFICATION_DELETED_ACTION); 191 192 sScreenDensity = context.getResources().getDisplayMetrics().density; 193 } 194 195 /** 196 * Specifies which message thread is currently being viewed by the user. New messages in that 197 * thread will not generate a notification icon and will play the notification sound at a lower 198 * volume. Make sure you set this to THREAD_NONE when the UI component that shows the thread is 199 * no longer visible to the user (e.g. Activity.onPause(), etc.) 200 * @param threadId The ID of the thread that the user is currently viewing. Pass THREAD_NONE 201 * if the user is not viewing a thread, or THREAD_ALL if the user is viewing the conversation 202 * list (note: that latter one has no effect as of this implementation) 203 */ setCurrentlyDisplayedThreadId(long threadId)204 public static void setCurrentlyDisplayedThreadId(long threadId) { 205 synchronized (sCurrentlyDisplayedThreadLock) { 206 sCurrentlyDisplayedThreadId = threadId; 207 if (DEBUG) { 208 Log.d(TAG, "setCurrentlyDisplayedThreadId: " + sCurrentlyDisplayedThreadId); 209 } 210 } 211 } 212 213 /** 214 * Checks to see if there are any "unseen" messages or delivery 215 * reports. Shows the most recent notification if there is one. 216 * Does its work and query in a worker thread. 217 * 218 * @param context the context to use 219 */ nonBlockingUpdateNewMessageIndicator(final Context context, final long newMsgThreadId, final boolean isStatusMessage)220 public static void nonBlockingUpdateNewMessageIndicator(final Context context, 221 final long newMsgThreadId, 222 final boolean isStatusMessage) { 223 if (DEBUG) { 224 Log.d(TAG, "nonBlockingUpdateNewMessageIndicator: newMsgThreadId: " + 225 newMsgThreadId + 226 " sCurrentlyDisplayedThreadId: " + sCurrentlyDisplayedThreadId); 227 } 228 new Thread(new Runnable() { 229 @Override 230 public void run() { 231 blockingUpdateNewMessageIndicator(context, newMsgThreadId, isStatusMessage); 232 } 233 }, "MessagingNotification.nonBlockingUpdateNewMessageIndicator").start(); 234 } 235 236 /** 237 * Checks to see if there are any "unseen" messages or delivery 238 * reports and builds a sorted (by delivery date) list of unread notifications. 239 * 240 * @param context the context to use 241 * @param newMsgThreadId The thread ID of a new message that we're to notify about; if there's 242 * no new message, use THREAD_NONE. If we should notify about multiple or unknown thread IDs, 243 * use THREAD_ALL. 244 * @param isStatusMessage 245 */ blockingUpdateNewMessageIndicator(Context context, long newMsgThreadId, boolean isStatusMessage)246 public static void blockingUpdateNewMessageIndicator(Context context, long newMsgThreadId, 247 boolean isStatusMessage) { 248 if (DEBUG) { 249 Contact.logWithTrace(TAG, "blockingUpdateNewMessageIndicator: newMsgThreadId: " + 250 newMsgThreadId); 251 } 252 // notificationSet is kept sorted by the incoming message delivery time, with the 253 // most recent message first. 254 SortedSet<NotificationInfo> notificationSet = 255 new TreeSet<NotificationInfo>(INFO_COMPARATOR); 256 257 Set<Long> threads = new HashSet<Long>(4); 258 259 addMmsNotificationInfos(context, threads, notificationSet); 260 addSmsNotificationInfos(context, threads, notificationSet); 261 262 if (notificationSet.isEmpty()) { 263 if (DEBUG) { 264 Log.d(TAG, "blockingUpdateNewMessageIndicator: notificationSet is empty, " + 265 "canceling existing notifications"); 266 } 267 cancelNotification(context, NOTIFICATION_ID); 268 } else { 269 if (DEBUG || Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 270 Log.d(TAG, "blockingUpdateNewMessageIndicator: count=" + notificationSet.size() + 271 ", newMsgThreadId=" + newMsgThreadId); 272 } 273 synchronized (sCurrentlyDisplayedThreadLock) { 274 if (newMsgThreadId > 0 && newMsgThreadId == sCurrentlyDisplayedThreadId && 275 threads.contains(newMsgThreadId)) { 276 if (DEBUG) { 277 Log.d(TAG, "blockingUpdateNewMessageIndicator: newMsgThreadId == " + 278 "sCurrentlyDisplayedThreadId so NOT showing notification," + 279 " but playing soft sound. threadId: " + newMsgThreadId); 280 } 281 playInConversationNotificationSound(context); 282 return; 283 } 284 } 285 updateNotification(context, newMsgThreadId != THREAD_NONE, threads.size(), 286 notificationSet); 287 } 288 289 // And deals with delivery reports (which use Toasts). It's safe to call in a worker 290 // thread because the toast will eventually get posted to a handler. 291 MmsSmsDeliveryInfo delivery = getSmsNewDeliveryInfo(context); 292 if (delivery != null) { 293 delivery.deliver(context, isStatusMessage); 294 } 295 } 296 297 /** 298 * Play the in-conversation notification sound (it's the regular notification sound, but 299 * played at half-volume 300 */ playInConversationNotificationSound(Context context)301 private static void playInConversationNotificationSound(Context context) { 302 SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); 303 String ringtoneStr = sp.getString(MessagingPreferenceActivity.NOTIFICATION_RINGTONE, 304 null); 305 if (TextUtils.isEmpty(ringtoneStr)) { 306 // Nothing to play 307 return; 308 } 309 Uri ringtoneUri = Uri.parse(ringtoneStr); 310 NotificationPlayer player = new NotificationPlayer(LogTag.APP); 311 player.play(context, ringtoneUri, false, AudioManager.STREAM_NOTIFICATION, 312 IN_CONVERSATION_NOTIFICATION_VOLUME); 313 } 314 315 /** 316 * Updates all pending notifications, clearing or updating them as 317 * necessary. 318 */ blockingUpdateAllNotifications(final Context context, long threadId)319 public static void blockingUpdateAllNotifications(final Context context, long threadId) { 320 if (DEBUG) { 321 Contact.logWithTrace(TAG, "blockingUpdateAllNotifications: newMsgThreadId: " + 322 threadId); 323 } 324 nonBlockingUpdateNewMessageIndicator(context, threadId, false); 325 nonBlockingUpdateSendFailedNotification(context); 326 updateDownloadFailedNotification(context); 327 MmsWidgetProvider.notifyDatasetChanged(context); 328 } 329 330 private static final class MmsSmsDeliveryInfo { 331 public CharSequence mTicker; 332 public long mTimeMillis; 333 MmsSmsDeliveryInfo(CharSequence ticker, long timeMillis)334 public MmsSmsDeliveryInfo(CharSequence ticker, long timeMillis) { 335 mTicker = ticker; 336 mTimeMillis = timeMillis; 337 } 338 deliver(Context context, boolean isStatusMessage)339 public void deliver(Context context, boolean isStatusMessage) { 340 updateDeliveryNotification( 341 context, isStatusMessage, mTicker, mTimeMillis); 342 } 343 } 344 345 private static final class NotificationInfo { 346 public final Intent mClickIntent; 347 public final String mMessage; 348 public final CharSequence mTicker; 349 public final long mTimeMillis; 350 public final String mTitle; 351 public final Bitmap mAttachmentBitmap; 352 public final Contact mSender; 353 public final boolean mIsSms; 354 public final int mAttachmentType; 355 public final String mSubject; 356 public final long mThreadId; 357 358 /** 359 * @param isSms true if sms, false if mms 360 * @param clickIntent where to go when the user taps the notification 361 * @param message for a single message, this is the message text 362 * @param subject text of mms subject 363 * @param ticker text displayed ticker-style across the notification, typically formatted 364 * as sender: message 365 * @param timeMillis date the message was received 366 * @param title for a single message, this is the sender 367 * @param attachmentBitmap a bitmap of an attachment, such as a picture or video 368 * @param sender contact of the sender 369 * @param attachmentType of the mms attachment 370 * @param threadId thread this message belongs to 371 */ NotificationInfo(boolean isSms, Intent clickIntent, String message, String subject, CharSequence ticker, long timeMillis, String title, Bitmap attachmentBitmap, Contact sender, int attachmentType, long threadId)372 public NotificationInfo(boolean isSms, 373 Intent clickIntent, String message, String subject, 374 CharSequence ticker, long timeMillis, String title, 375 Bitmap attachmentBitmap, Contact sender, 376 int attachmentType, long threadId) { 377 mIsSms = isSms; 378 mClickIntent = clickIntent; 379 mMessage = message; 380 mSubject = subject; 381 mTicker = ticker; 382 mTimeMillis = timeMillis; 383 mTitle = title; 384 mAttachmentBitmap = attachmentBitmap; 385 mSender = sender; 386 mAttachmentType = attachmentType; 387 mThreadId = threadId; 388 } 389 getTime()390 public long getTime() { 391 return mTimeMillis; 392 } 393 394 // This is the message string used in bigText and bigPicture notifications. formatBigMessage(Context context)395 public CharSequence formatBigMessage(Context context) { 396 final TextAppearanceSpan notificationSubjectSpan = new TextAppearanceSpan( 397 context, R.style.NotificationPrimaryText); 398 399 // Change multiple newlines (with potential white space between), into a single new line 400 final String message = 401 !TextUtils.isEmpty(mMessage) ? mMessage.replaceAll("\\n\\s+", "\n") : ""; 402 403 SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(); 404 if (!TextUtils.isEmpty(mSubject)) { 405 spannableStringBuilder.append(mSubject); 406 spannableStringBuilder.setSpan(notificationSubjectSpan, 0, mSubject.length(), 0); 407 } 408 if (mAttachmentType > WorkingMessage.TEXT) { 409 if (spannableStringBuilder.length() > 0) { 410 spannableStringBuilder.append('\n'); 411 } 412 spannableStringBuilder.append(getAttachmentTypeString(context, mAttachmentType)); 413 } 414 if (mMessage != null) { 415 if (spannableStringBuilder.length() > 0) { 416 spannableStringBuilder.append('\n'); 417 } 418 spannableStringBuilder.append(mMessage); 419 } 420 return spannableStringBuilder; 421 } 422 423 // This is the message string used in each line of an inboxStyle notification. formatInboxMessage(Context context)424 public CharSequence formatInboxMessage(Context context) { 425 final TextAppearanceSpan notificationSenderSpan = new TextAppearanceSpan( 426 context, R.style.NotificationPrimaryText); 427 428 final TextAppearanceSpan notificationSubjectSpan = new TextAppearanceSpan( 429 context, R.style.NotificationSubjectText); 430 431 // Change multiple newlines (with potential white space between), into a single new line 432 final String message = 433 !TextUtils.isEmpty(mMessage) ? mMessage.replaceAll("\\n\\s+", "\n") : ""; 434 435 SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(); 436 final String sender = mSender.getName(); 437 if (!TextUtils.isEmpty(sender)) { 438 spannableStringBuilder.append(sender); 439 spannableStringBuilder.setSpan(notificationSenderSpan, 0, sender.length(), 0); 440 } 441 String separator = context.getString(R.string.notification_separator); 442 if (!mIsSms) { 443 if (!TextUtils.isEmpty(mSubject)) { 444 if (spannableStringBuilder.length() > 0) { 445 spannableStringBuilder.append(separator); 446 } 447 int start = spannableStringBuilder.length(); 448 spannableStringBuilder.append(mSubject); 449 spannableStringBuilder.setSpan(notificationSubjectSpan, start, 450 start + mSubject.length(), 0); 451 } 452 if (mAttachmentType > WorkingMessage.TEXT) { 453 if (spannableStringBuilder.length() > 0) { 454 spannableStringBuilder.append(separator); 455 } 456 spannableStringBuilder.append(getAttachmentTypeString(context, mAttachmentType)); 457 } 458 } 459 if (message.length() > 0) { 460 if (spannableStringBuilder.length() > 0) { 461 spannableStringBuilder.append(separator); 462 } 463 int start = spannableStringBuilder.length(); 464 spannableStringBuilder.append(message); 465 spannableStringBuilder.setSpan(notificationSubjectSpan, start, 466 start + message.length(), 0); 467 } 468 return spannableStringBuilder; 469 } 470 471 // This is the summary string used in bigPicture notifications. formatPictureMessage(Context context)472 public CharSequence formatPictureMessage(Context context) { 473 final TextAppearanceSpan notificationSubjectSpan = new TextAppearanceSpan( 474 context, R.style.NotificationPrimaryText); 475 476 // Change multiple newlines (with potential white space between), into a single new line 477 final String message = 478 !TextUtils.isEmpty(mMessage) ? mMessage.replaceAll("\\n\\s+", "\n") : ""; 479 480 // Show the subject or the message (if no subject) 481 SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(); 482 if (!TextUtils.isEmpty(mSubject)) { 483 spannableStringBuilder.append(mSubject); 484 spannableStringBuilder.setSpan(notificationSubjectSpan, 0, mSubject.length(), 0); 485 } 486 if (message.length() > 0 && spannableStringBuilder.length() == 0) { 487 spannableStringBuilder.append(message); 488 spannableStringBuilder.setSpan(notificationSubjectSpan, 0, message.length(), 0); 489 } 490 return spannableStringBuilder; 491 } 492 } 493 494 // Return a formatted string with all the sender names separated by commas. formatSenders(Context context, ArrayList<NotificationInfo> senders)495 private static CharSequence formatSenders(Context context, 496 ArrayList<NotificationInfo> senders) { 497 final TextAppearanceSpan notificationSenderSpan = new TextAppearanceSpan( 498 context, R.style.NotificationPrimaryText); 499 500 String separator = context.getString(R.string.enumeration_comma); // ", " 501 SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(); 502 int len = senders.size(); 503 for (int i = 0; i < len; i++) { 504 if (i > 0) { 505 spannableStringBuilder.append(separator); 506 } 507 spannableStringBuilder.append(senders.get(i).mSender.getName()); 508 } 509 spannableStringBuilder.setSpan(notificationSenderSpan, 0, 510 spannableStringBuilder.length(), 0); 511 return spannableStringBuilder; 512 } 513 514 // Return a formatted string with the attachmentType spelled out as a string. For 515 // no attachment (or just text), return null. getAttachmentTypeString(Context context, int attachmentType)516 private static CharSequence getAttachmentTypeString(Context context, int attachmentType) { 517 final TextAppearanceSpan notificationAttachmentSpan = new TextAppearanceSpan( 518 context, R.style.NotificationSecondaryText); 519 int id = 0; 520 switch (attachmentType) { 521 case WorkingMessage.AUDIO: id = R.string.attachment_audio; break; 522 case WorkingMessage.VIDEO: id = R.string.attachment_video; break; 523 case WorkingMessage.SLIDESHOW: id = R.string.attachment_slideshow; break; 524 case WorkingMessage.IMAGE: id = R.string.attachment_picture; break; 525 } 526 if (id > 0) { 527 final SpannableString spannableString = new SpannableString(context.getString(id)); 528 spannableString.setSpan(notificationAttachmentSpan, 529 0, spannableString.length(), 0); 530 return spannableString; 531 } 532 return null; 533 } 534 535 /** 536 * 537 * Sorts by the time a notification was received in descending order -- newer first. 538 * 539 */ 540 private static final class NotificationInfoComparator 541 implements Comparator<NotificationInfo> { 542 @Override compare( NotificationInfo info1, NotificationInfo info2)543 public int compare( 544 NotificationInfo info1, NotificationInfo info2) { 545 return Long.signum(info2.getTime() - info1.getTime()); 546 } 547 } 548 addMmsNotificationInfos( Context context, Set<Long> threads, SortedSet<NotificationInfo> notificationSet)549 private static final void addMmsNotificationInfos( 550 Context context, Set<Long> threads, SortedSet<NotificationInfo> notificationSet) { 551 ContentResolver resolver = context.getContentResolver(); 552 553 // This query looks like this when logged: 554 // I/Database( 147): elapsedTime4Sql|/data/data/com.android.providers.telephony/databases/ 555 // mmssms.db|0.362 ms|SELECT thread_id, date, _id, sub, sub_cs FROM pdu WHERE ((msg_box=1 556 // AND seen=0 AND (m_type=130 OR m_type=132))) ORDER BY date desc 557 558 Cursor cursor = SqliteWrapper.query(context, resolver, Mms.CONTENT_URI, 559 MMS_STATUS_PROJECTION, NEW_INCOMING_MM_CONSTRAINT, 560 null, Mms.DATE + " desc"); 561 562 if (cursor == null) { 563 return; 564 } 565 566 try { 567 while (cursor.moveToNext()) { 568 569 long msgId = cursor.getLong(COLUMN_MMS_ID); 570 Uri msgUri = Mms.CONTENT_URI.buildUpon().appendPath( 571 Long.toString(msgId)).build(); 572 String address = AddressUtils.getFrom(context, msgUri); 573 574 Contact contact = Contact.get(address, false); 575 if (contact.getSendToVoicemail()) { 576 // don't notify, skip this one 577 continue; 578 } 579 580 String subject = getMmsSubject( 581 cursor.getString(COLUMN_SUBJECT), cursor.getInt(COLUMN_SUBJECT_CS)); 582 subject = MessageUtils.cleanseMmsSubject(context, subject); 583 584 long threadId = cursor.getLong(COLUMN_THREAD_ID); 585 long timeMillis = cursor.getLong(COLUMN_DATE) * 1000; 586 587 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 588 Log.d(TAG, "addMmsNotificationInfos: count=" + cursor.getCount() + 589 ", addr = " + address + ", thread_id=" + threadId); 590 } 591 592 // Extract the message and/or an attached picture from the first slide 593 Bitmap attachedPicture = null; 594 String messageBody = null; 595 int attachmentType = WorkingMessage.TEXT; 596 try { 597 GenericPdu pdu = sPduPersister.load(msgUri); 598 if (pdu != null && pdu instanceof MultimediaMessagePdu) { 599 SlideshowModel slideshow = SlideshowModel.createFromPduBody(context, 600 ((MultimediaMessagePdu)pdu).getBody()); 601 attachmentType = getAttachmentType(slideshow); 602 SlideModel firstSlide = slideshow.get(0); 603 if (firstSlide != null) { 604 if (firstSlide.hasImage()) { 605 int maxDim = dp2Pixels(MAX_BITMAP_DIMEN_DP); 606 attachedPicture = firstSlide.getImage().getBitmap(maxDim, maxDim); 607 } 608 if (firstSlide.hasText()) { 609 messageBody = firstSlide.getText().getText(); 610 } 611 } 612 } 613 } catch (final MmsException e) { 614 Log.e(TAG, "MmsException loading uri: " + msgUri, e); 615 continue; // skip this bad boy -- don't generate an empty notification 616 } 617 618 NotificationInfo info = getNewMessageNotificationInfo(context, 619 false /* isSms */, 620 address, 621 messageBody, subject, 622 threadId, 623 timeMillis, 624 attachedPicture, 625 contact, 626 attachmentType); 627 628 notificationSet.add(info); 629 630 threads.add(threadId); 631 } 632 } finally { 633 cursor.close(); 634 } 635 } 636 637 // Look at the passed in slideshow and determine what type of attachment it is. getAttachmentType(SlideshowModel slideshow)638 private static int getAttachmentType(SlideshowModel slideshow) { 639 int slideCount = slideshow.size(); 640 641 if (slideCount == 0) { 642 return WorkingMessage.TEXT; 643 } else if (slideCount > 1) { 644 return WorkingMessage.SLIDESHOW; 645 } else { 646 SlideModel slide = slideshow.get(0); 647 if (slide.hasImage()) { 648 return WorkingMessage.IMAGE; 649 } else if (slide.hasVideo()) { 650 return WorkingMessage.VIDEO; 651 } else if (slide.hasAudio()) { 652 return WorkingMessage.AUDIO; 653 } 654 } 655 return WorkingMessage.TEXT; 656 } 657 dp2Pixels(int dip)658 private static final int dp2Pixels(int dip) { 659 return (int) (dip * sScreenDensity + 0.5f); 660 } 661 getSmsNewDeliveryInfo(Context context)662 private static final MmsSmsDeliveryInfo getSmsNewDeliveryInfo(Context context) { 663 ContentResolver resolver = context.getContentResolver(); 664 Cursor cursor = SqliteWrapper.query(context, resolver, Sms.CONTENT_URI, 665 SMS_STATUS_PROJECTION, NEW_DELIVERY_SM_CONSTRAINT, 666 null, Sms.DATE); 667 668 if (cursor == null) { 669 return null; 670 } 671 672 try { 673 if (!cursor.moveToLast()) { 674 return null; 675 } 676 677 String address = cursor.getString(COLUMN_SMS_ADDRESS); 678 long timeMillis = 3000; 679 680 Contact contact = Contact.get(address, false); 681 String name = contact.getNameAndNumber(); 682 683 return new MmsSmsDeliveryInfo(context.getString(R.string.delivery_toast_body, name), 684 timeMillis); 685 686 } finally { 687 cursor.close(); 688 } 689 } 690 addSmsNotificationInfos( Context context, Set<Long> threads, SortedSet<NotificationInfo> notificationSet)691 private static final void addSmsNotificationInfos( 692 Context context, Set<Long> threads, SortedSet<NotificationInfo> notificationSet) { 693 ContentResolver resolver = context.getContentResolver(); 694 Cursor cursor = SqliteWrapper.query(context, resolver, Sms.CONTENT_URI, 695 SMS_STATUS_PROJECTION, NEW_INCOMING_SM_CONSTRAINT, 696 null, Sms.DATE + " desc"); 697 698 if (cursor == null) { 699 return; 700 } 701 702 try { 703 while (cursor.moveToNext()) { 704 String address = cursor.getString(COLUMN_SMS_ADDRESS); 705 706 Contact contact = Contact.get(address, false); 707 if (contact.getSendToVoicemail()) { 708 // don't notify, skip this one 709 continue; 710 } 711 712 String message = cursor.getString(COLUMN_SMS_BODY); 713 long threadId = cursor.getLong(COLUMN_THREAD_ID); 714 long timeMillis = cursor.getLong(COLUMN_DATE); 715 716 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) 717 { 718 Log.d(TAG, "addSmsNotificationInfos: count=" + cursor.getCount() + 719 ", addr=" + address + ", thread_id=" + threadId); 720 } 721 722 723 NotificationInfo info = getNewMessageNotificationInfo(context, true /* isSms */, 724 address, message, null /* subject */, 725 threadId, timeMillis, null /* attachmentBitmap */, 726 contact, WorkingMessage.TEXT); 727 728 notificationSet.add(info); 729 730 threads.add(threadId); 731 threads.add(cursor.getLong(COLUMN_THREAD_ID)); 732 } 733 } finally { 734 cursor.close(); 735 } 736 } 737 getNewMessageNotificationInfo( Context context, boolean isSms, String address, String message, String subject, long threadId, long timeMillis, Bitmap attachmentBitmap, Contact contact, int attachmentType)738 private static final NotificationInfo getNewMessageNotificationInfo( 739 Context context, 740 boolean isSms, 741 String address, 742 String message, 743 String subject, 744 long threadId, 745 long timeMillis, 746 Bitmap attachmentBitmap, 747 Contact contact, 748 int attachmentType) { 749 Intent clickIntent = ComposeMessageActivity.createIntent(context, threadId); 750 clickIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK 751 | Intent.FLAG_ACTIVITY_SINGLE_TOP 752 | Intent.FLAG_ACTIVITY_CLEAR_TOP); 753 754 String senderInfo = buildTickerMessage( 755 context, address, null, null).toString(); 756 String senderInfoName = senderInfo.substring( 757 0, senderInfo.length() - 2); 758 CharSequence ticker = buildTickerMessage( 759 context, address, subject, message); 760 761 return new NotificationInfo(isSms, 762 clickIntent, message, subject, ticker, timeMillis, 763 senderInfoName, attachmentBitmap, contact, attachmentType, threadId); 764 } 765 cancelNotification(Context context, int notificationId)766 public static void cancelNotification(Context context, int notificationId) { 767 NotificationManager nm = (NotificationManager) context.getSystemService( 768 Context.NOTIFICATION_SERVICE); 769 770 Log.d(TAG, "cancelNotification"); 771 nm.cancel(notificationId); 772 } 773 updateDeliveryNotification(final Context context, boolean isStatusMessage, final CharSequence message, final long timeMillis)774 private static void updateDeliveryNotification(final Context context, 775 boolean isStatusMessage, 776 final CharSequence message, 777 final long timeMillis) { 778 if (!isStatusMessage) { 779 return; 780 } 781 782 783 if (!MessagingPreferenceActivity.getNotificationEnabled(context)) { 784 return; 785 } 786 787 sToastHandler.post(new Runnable() { 788 @Override 789 public void run() { 790 Toast.makeText(context, message, (int)timeMillis).show(); 791 } 792 }); 793 } 794 795 /** 796 * updateNotification is *the* main function for building the actual notification handed to 797 * the NotificationManager 798 * @param context 799 * @param isNew if we've got a new message, show the ticker 800 * @param uniqueThreadCount 801 * @param notificationSet the set of notifications to display 802 */ updateNotification( Context context, boolean isNew, int uniqueThreadCount, SortedSet<NotificationInfo> notificationSet)803 private static void updateNotification( 804 Context context, 805 boolean isNew, 806 int uniqueThreadCount, 807 SortedSet<NotificationInfo> notificationSet) { 808 // If the user has turned off notifications in settings, don't do any notifying. 809 if (!MessagingPreferenceActivity.getNotificationEnabled(context)) { 810 if (DEBUG) { 811 Log.d(TAG, "updateNotification: notifications turned off in prefs, bailing"); 812 } 813 return; 814 } 815 816 // Figure out what we've got -- whether all sms's, mms's, or a mixture of both. 817 final int messageCount = notificationSet.size(); 818 NotificationInfo mostRecentNotification = notificationSet.first(); 819 820 final Notification.Builder noti = new Notification.Builder(context) 821 .setWhen(mostRecentNotification.mTimeMillis); 822 823 if (isNew) { 824 noti.setTicker(mostRecentNotification.mTicker); 825 } 826 TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(context); 827 828 // If we have more than one unique thread, change the title (which would 829 // normally be the contact who sent the message) to a generic one that 830 // makes sense for multiple senders, and change the Intent to take the 831 // user to the conversation list instead of the specific thread. 832 833 // Cases: 834 // 1) single message from single thread - intent goes to ComposeMessageActivity 835 // 2) multiple messages from single thread - intent goes to ComposeMessageActivity 836 // 3) messages from multiple threads - intent goes to ConversationList 837 838 final Resources res = context.getResources(); 839 String title = null; 840 Bitmap avatar = null; 841 if (uniqueThreadCount > 1) { // messages from multiple threads 842 Intent mainActivityIntent = new Intent(Intent.ACTION_MAIN); 843 844 mainActivityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK 845 | Intent.FLAG_ACTIVITY_SINGLE_TOP 846 | Intent.FLAG_ACTIVITY_CLEAR_TOP); 847 848 mainActivityIntent.setType("vnd.android-dir/mms-sms"); 849 taskStackBuilder.addNextIntent(mainActivityIntent); 850 title = context.getString(R.string.message_count_notification, messageCount); 851 } else { // same thread, single or multiple messages 852 title = mostRecentNotification.mTitle; 853 BitmapDrawable contactDrawable = (BitmapDrawable)mostRecentNotification.mSender 854 .getAvatar(context, null); 855 if (contactDrawable != null) { 856 // Show the sender's avatar as the big icon. Contact bitmaps are 96x96 so we 857 // have to scale 'em up to 128x128 to fill the whole notification large icon. 858 avatar = contactDrawable.getBitmap(); 859 if (avatar != null) { 860 final int idealIconHeight = 861 res.getDimensionPixelSize(android.R.dimen.notification_large_icon_height); 862 final int idealIconWidth = 863 res.getDimensionPixelSize(android.R.dimen.notification_large_icon_width); 864 if (avatar.getHeight() < idealIconHeight) { 865 // Scale this image to fit the intended size 866 avatar = Bitmap.createScaledBitmap( 867 avatar, idealIconWidth, idealIconHeight, true); 868 } 869 if (avatar != null) { 870 noti.setLargeIcon(avatar); 871 } 872 } 873 } 874 875 taskStackBuilder.addParentStack(ComposeMessageActivity.class); 876 taskStackBuilder.addNextIntent(mostRecentNotification.mClickIntent); 877 } 878 // Always have to set the small icon or the notification is ignored 879 noti.setSmallIcon(R.drawable.stat_notify_sms); 880 881 NotificationManager nm = (NotificationManager) 882 context.getSystemService(Context.NOTIFICATION_SERVICE); 883 884 // Update the notification. 885 noti.setContentTitle(title) 886 .setContentIntent( 887 taskStackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)) 888 .addKind(Notification.KIND_MESSAGE) 889 .setPriority(Notification.PRIORITY_DEFAULT); // TODO: set based on contact coming 890 // from a favorite. 891 892 int defaults = 0; 893 894 if (isNew) { 895 SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); 896 897 boolean vibrate = false; 898 if (sp.contains(MessagingPreferenceActivity.NOTIFICATION_VIBRATE)) { 899 // The most recent change to the vibrate preference is to store a boolean 900 // value in NOTIFICATION_VIBRATE. If prefs contain that preference, use that 901 // first. 902 vibrate = sp.getBoolean(MessagingPreferenceActivity.NOTIFICATION_VIBRATE, 903 false); 904 } else if (sp.contains(MessagingPreferenceActivity.NOTIFICATION_VIBRATE_WHEN)) { 905 // This is to support the pre-JellyBean MR1.1 version of vibrate preferences 906 // when vibrate was a tri-state setting. As soon as the user opens the Messaging 907 // app's settings, it will migrate this setting from NOTIFICATION_VIBRATE_WHEN 908 // to the boolean value stored in NOTIFICATION_VIBRATE. 909 String vibrateWhen = 910 sp.getString(MessagingPreferenceActivity.NOTIFICATION_VIBRATE_WHEN, null); 911 vibrate = "always".equals(vibrateWhen); 912 } 913 if (vibrate) { 914 defaults |= Notification.DEFAULT_VIBRATE; 915 } 916 917 String ringtoneStr = sp.getString(MessagingPreferenceActivity.NOTIFICATION_RINGTONE, 918 null); 919 noti.setSound(TextUtils.isEmpty(ringtoneStr) ? null : Uri.parse(ringtoneStr)); 920 Log.d(TAG, "updateNotification: new message, adding sound to the notification"); 921 } 922 923 defaults |= Notification.DEFAULT_LIGHTS; 924 925 noti.setDefaults(defaults); 926 927 // set up delete intent 928 noti.setDeleteIntent(PendingIntent.getBroadcast(context, 0, 929 sNotificationOnDeleteIntent, 0)); 930 931 final Notification notification; 932 933 if (messageCount == 1) { 934 // We've got a single message 935 936 // This sets the text for the collapsed form: 937 noti.setContentText(mostRecentNotification.formatBigMessage(context)); 938 939 if (mostRecentNotification.mAttachmentBitmap != null) { 940 // The message has a picture, show that 941 942 notification = new Notification.BigPictureStyle(noti) 943 .bigPicture(mostRecentNotification.mAttachmentBitmap) 944 // This sets the text for the expanded picture form: 945 .setSummaryText(mostRecentNotification.formatPictureMessage(context)) 946 .build(); 947 } else { 948 // Show a single notification -- big style with the text of the whole message 949 notification = new Notification.BigTextStyle(noti) 950 .bigText(mostRecentNotification.formatBigMessage(context)) 951 .build(); 952 } 953 if (DEBUG) { 954 Log.d(TAG, "updateNotification: single message notification"); 955 } 956 } else { 957 // We've got multiple messages 958 if (uniqueThreadCount == 1) { 959 // We've got multiple messages for the same thread. 960 // Starting with the oldest new message, display the full text of each message. 961 // Begin a line for each subsequent message. 962 SpannableStringBuilder buf = new SpannableStringBuilder(); 963 NotificationInfo infos[] = 964 notificationSet.toArray(new NotificationInfo[messageCount]); 965 int len = infos.length; 966 for (int i = len - 1; i >= 0; i--) { 967 NotificationInfo info = infos[i]; 968 969 buf.append(info.formatBigMessage(context)); 970 971 if (i != 0) { 972 buf.append('\n'); 973 } 974 } 975 976 noti.setContentText(context.getString(R.string.message_count_notification, 977 messageCount)); 978 979 // Show a single notification -- big style with the text of all the messages 980 notification = new Notification.BigTextStyle(noti) 981 .bigText(buf) 982 // Forcibly show the last line, with the app's smallIcon in it, if we 983 // kicked the smallIcon out with an avatar bitmap 984 .setSummaryText((avatar == null) ? null : " ") 985 .build(); 986 if (DEBUG) { 987 Log.d(TAG, "updateNotification: multi messages for single thread"); 988 } 989 } else { 990 // Build a set of the most recent notification per threadId. 991 HashSet<Long> uniqueThreads = new HashSet<Long>(messageCount); 992 ArrayList<NotificationInfo> mostRecentNotifPerThread = 993 new ArrayList<NotificationInfo>(); 994 Iterator<NotificationInfo> notifications = notificationSet.iterator(); 995 while (notifications.hasNext()) { 996 NotificationInfo notificationInfo = notifications.next(); 997 if (!uniqueThreads.contains(notificationInfo.mThreadId)) { 998 uniqueThreads.add(notificationInfo.mThreadId); 999 mostRecentNotifPerThread.add(notificationInfo); 1000 } 1001 } 1002 // When collapsed, show all the senders like this: 1003 // Fred Flinstone, Barry Manilow, Pete... 1004 noti.setContentText(formatSenders(context, mostRecentNotifPerThread)); 1005 Notification.InboxStyle inboxStyle = new Notification.InboxStyle(noti); 1006 1007 // We have to set the summary text to non-empty so the content text doesn't show 1008 // up when expanded. 1009 inboxStyle.setSummaryText(" "); 1010 1011 // At this point we've got multiple messages in multiple threads. We only 1012 // want to show the most recent message per thread, which are in 1013 // mostRecentNotifPerThread. 1014 int uniqueThreadMessageCount = mostRecentNotifPerThread.size(); 1015 int maxMessages = Math.min(MAX_MESSAGES_TO_SHOW, uniqueThreadMessageCount); 1016 1017 for (int i = 0; i < maxMessages; i++) { 1018 NotificationInfo info = mostRecentNotifPerThread.get(i); 1019 inboxStyle.addLine(info.formatInboxMessage(context)); 1020 } 1021 notification = inboxStyle.build(); 1022 if (DEBUG) { 1023 Log.d(TAG, "updateNotification: multi messages," + 1024 " showing inboxStyle notification"); 1025 } 1026 } 1027 } 1028 1029 nm.notify(NOTIFICATION_ID, notification); 1030 } 1031 buildTickerMessage( Context context, String address, String subject, String body)1032 protected static CharSequence buildTickerMessage( 1033 Context context, String address, String subject, String body) { 1034 String displayAddress = Contact.get(address, true).getName(); 1035 1036 StringBuilder buf = new StringBuilder( 1037 displayAddress == null 1038 ? "" 1039 : displayAddress.replace('\n', ' ').replace('\r', ' ')); 1040 buf.append(':').append(' '); 1041 1042 int offset = buf.length(); 1043 if (!TextUtils.isEmpty(subject)) { 1044 subject = subject.replace('\n', ' ').replace('\r', ' '); 1045 buf.append(subject); 1046 buf.append(' '); 1047 } 1048 1049 if (!TextUtils.isEmpty(body)) { 1050 body = body.replace('\n', ' ').replace('\r', ' '); 1051 buf.append(body); 1052 } 1053 1054 SpannableString spanText = new SpannableString(buf.toString()); 1055 spanText.setSpan(new StyleSpan(Typeface.BOLD), 0, offset, 1056 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 1057 1058 return spanText; 1059 } 1060 getMmsSubject(String sub, int charset)1061 private static String getMmsSubject(String sub, int charset) { 1062 return TextUtils.isEmpty(sub) ? "" 1063 : new EncodedStringValue(charset, PduPersister.getBytes(sub)).getString(); 1064 } 1065 notifyDownloadFailed(Context context, long threadId)1066 public static void notifyDownloadFailed(Context context, long threadId) { 1067 notifyFailed(context, true, threadId, false); 1068 } 1069 notifySendFailed(Context context)1070 public static void notifySendFailed(Context context) { 1071 notifyFailed(context, false, 0, false); 1072 } 1073 notifySendFailed(Context context, boolean noisy)1074 public static void notifySendFailed(Context context, boolean noisy) { 1075 notifyFailed(context, false, 0, noisy); 1076 } 1077 notifyFailed(Context context, boolean isDownload, long threadId, boolean noisy)1078 private static void notifyFailed(Context context, boolean isDownload, long threadId, 1079 boolean noisy) { 1080 // TODO factor out common code for creating notifications 1081 boolean enabled = MessagingPreferenceActivity.getNotificationEnabled(context); 1082 if (!enabled) { 1083 return; 1084 } 1085 1086 // Strategy: 1087 // a. If there is a single failure notification, tapping on the notification goes 1088 // to the compose view. 1089 // b. If there are two failure it stays in the thread view. Selecting one undelivered 1090 // thread will dismiss one undelivered notification but will still display the 1091 // notification.If you select the 2nd undelivered one it will dismiss the notification. 1092 1093 long[] msgThreadId = {0, 1}; // Dummy initial values, just to initialize the memory 1094 int totalFailedCount = getUndeliveredMessageCount(context, msgThreadId); 1095 if (totalFailedCount == 0 && !isDownload) { 1096 return; 1097 } 1098 // The getUndeliveredMessageCount method puts a non-zero value in msgThreadId[1] if all 1099 // failures are from the same thread. 1100 // If isDownload is true, we're dealing with 1 specific failure; therefore "all failed" are 1101 // indeed in the same thread since there's only 1. 1102 boolean allFailedInSameThread = (msgThreadId[1] != 0) || isDownload; 1103 1104 Intent failedIntent; 1105 Notification notification = new Notification(); 1106 String title; 1107 String description; 1108 if (totalFailedCount > 1) { 1109 description = context.getString(R.string.notification_failed_multiple, 1110 Integer.toString(totalFailedCount)); 1111 title = context.getString(R.string.notification_failed_multiple_title); 1112 } else { 1113 title = isDownload ? 1114 context.getString(R.string.message_download_failed_title) : 1115 context.getString(R.string.message_send_failed_title); 1116 1117 description = context.getString(R.string.message_failed_body); 1118 } 1119 1120 TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(context); 1121 if (allFailedInSameThread) { 1122 failedIntent = new Intent(context, ComposeMessageActivity.class); 1123 if (isDownload) { 1124 // When isDownload is true, the valid threadId is passed into this function. 1125 failedIntent.putExtra("failed_download_flag", true); 1126 } else { 1127 threadId = msgThreadId[0]; 1128 failedIntent.putExtra("undelivered_flag", true); 1129 } 1130 failedIntent.putExtra("thread_id", threadId); 1131 taskStackBuilder.addParentStack(ComposeMessageActivity.class); 1132 } else { 1133 failedIntent = new Intent(context, ConversationList.class); 1134 } 1135 taskStackBuilder.addNextIntent(failedIntent); 1136 1137 notification.icon = R.drawable.stat_notify_sms_failed; 1138 1139 notification.tickerText = title; 1140 1141 notification.setLatestEventInfo(context, title, description, 1142 taskStackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)); 1143 1144 if (noisy) { 1145 SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); 1146 boolean vibrate = sp.getBoolean(MessagingPreferenceActivity.NOTIFICATION_VIBRATE, 1147 false /* don't vibrate by default */); 1148 if (vibrate) { 1149 notification.defaults |= Notification.DEFAULT_VIBRATE; 1150 } 1151 1152 String ringtoneStr = sp.getString(MessagingPreferenceActivity.NOTIFICATION_RINGTONE, 1153 null); 1154 notification.sound = TextUtils.isEmpty(ringtoneStr) ? null : Uri.parse(ringtoneStr); 1155 } 1156 1157 NotificationManager notificationMgr = (NotificationManager) 1158 context.getSystemService(Context.NOTIFICATION_SERVICE); 1159 1160 if (isDownload) { 1161 notificationMgr.notify(DOWNLOAD_FAILED_NOTIFICATION_ID, notification); 1162 } else { 1163 notificationMgr.notify(MESSAGE_FAILED_NOTIFICATION_ID, notification); 1164 } 1165 } 1166 1167 /** 1168 * Query the DB and return the number of undelivered messages (total for both SMS and MMS) 1169 * @param context The context 1170 * @param threadIdResult A container to put the result in, according to the following rules: 1171 * threadIdResult[0] contains the thread id of the first message. 1172 * threadIdResult[1] is nonzero if the thread ids of all the messages are the same. 1173 * You can pass in null for threadIdResult. 1174 * You can pass in a threadIdResult of size 1 to avoid the comparison of each thread id. 1175 */ getUndeliveredMessageCount(Context context, long[] threadIdResult)1176 private static int getUndeliveredMessageCount(Context context, long[] threadIdResult) { 1177 Cursor undeliveredCursor = SqliteWrapper.query(context, context.getContentResolver(), 1178 UNDELIVERED_URI, MMS_THREAD_ID_PROJECTION, "read=0", null, null); 1179 if (undeliveredCursor == null) { 1180 return 0; 1181 } 1182 int count = undeliveredCursor.getCount(); 1183 try { 1184 if (threadIdResult != null && undeliveredCursor.moveToFirst()) { 1185 threadIdResult[0] = undeliveredCursor.getLong(0); 1186 1187 if (threadIdResult.length >= 2) { 1188 // Test to see if all the undelivered messages belong to the same thread. 1189 long firstId = threadIdResult[0]; 1190 while (undeliveredCursor.moveToNext()) { 1191 if (undeliveredCursor.getLong(0) != firstId) { 1192 firstId = 0; 1193 break; 1194 } 1195 } 1196 threadIdResult[1] = firstId; // non-zero if all ids are the same 1197 } 1198 } 1199 } finally { 1200 undeliveredCursor.close(); 1201 } 1202 return count; 1203 } 1204 nonBlockingUpdateSendFailedNotification(final Context context)1205 public static void nonBlockingUpdateSendFailedNotification(final Context context) { 1206 new AsyncTask<Void, Void, Integer>() { 1207 protected Integer doInBackground(Void... none) { 1208 return getUndeliveredMessageCount(context, null); 1209 } 1210 1211 protected void onPostExecute(Integer result) { 1212 if (result < 1) { 1213 cancelNotification(context, MESSAGE_FAILED_NOTIFICATION_ID); 1214 } else { 1215 // rebuild and adjust the message count if necessary. 1216 notifySendFailed(context); 1217 } 1218 } 1219 }.execute(); 1220 } 1221 1222 /** 1223 * If all the undelivered messages belong to "threadId", cancel the notification. 1224 */ updateSendFailedNotificationForThread(Context context, long threadId)1225 public static void updateSendFailedNotificationForThread(Context context, long threadId) { 1226 long[] msgThreadId = {0, 0}; 1227 if (getUndeliveredMessageCount(context, msgThreadId) > 0 1228 && msgThreadId[0] == threadId 1229 && msgThreadId[1] != 0) { 1230 cancelNotification(context, MESSAGE_FAILED_NOTIFICATION_ID); 1231 } 1232 } 1233 getDownloadFailedMessageCount(Context context)1234 private static int getDownloadFailedMessageCount(Context context) { 1235 // Look for any messages in the MMS Inbox that are of the type 1236 // NOTIFICATION_IND (i.e. not already downloaded) and in the 1237 // permanent failure state. If there are none, cancel any 1238 // failed download notification. 1239 Cursor c = SqliteWrapper.query(context, context.getContentResolver(), 1240 Mms.Inbox.CONTENT_URI, null, 1241 Mms.MESSAGE_TYPE + "=" + 1242 String.valueOf(PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND) + 1243 " AND " + Mms.STATUS + "=" + 1244 String.valueOf(DownloadManager.STATE_PERMANENT_FAILURE), 1245 null, null); 1246 if (c == null) { 1247 return 0; 1248 } 1249 int count = c.getCount(); 1250 c.close(); 1251 return count; 1252 } 1253 updateDownloadFailedNotification(Context context)1254 public static void updateDownloadFailedNotification(Context context) { 1255 if (getDownloadFailedMessageCount(context) < 1) { 1256 cancelNotification(context, DOWNLOAD_FAILED_NOTIFICATION_ID); 1257 } 1258 } 1259 isFailedToDeliver(Intent intent)1260 public static boolean isFailedToDeliver(Intent intent) { 1261 return (intent != null) && intent.getBooleanExtra("undelivered_flag", false); 1262 } 1263 isFailedToDownload(Intent intent)1264 public static boolean isFailedToDownload(Intent intent) { 1265 return (intent != null) && intent.getBooleanExtra("failed_download_flag", false); 1266 } 1267 1268 /** 1269 * Get the thread ID of the SMS message with the given URI 1270 * @param context The context 1271 * @param uri The URI of the SMS message 1272 * @return The thread ID, or THREAD_NONE if the URI contains no entries 1273 */ getSmsThreadId(Context context, Uri uri)1274 public static long getSmsThreadId(Context context, Uri uri) { 1275 Cursor cursor = SqliteWrapper.query( 1276 context, 1277 context.getContentResolver(), 1278 uri, 1279 SMS_THREAD_ID_PROJECTION, 1280 null, 1281 null, 1282 null); 1283 1284 if (cursor == null) { 1285 if (DEBUG) { 1286 Log.d(TAG, "getSmsThreadId uri: " + uri + " NULL cursor! returning THREAD_NONE"); 1287 } 1288 return THREAD_NONE; 1289 } 1290 1291 try { 1292 if (cursor.moveToFirst()) { 1293 long threadId = cursor.getLong(cursor.getColumnIndex(Sms.THREAD_ID)); 1294 if (DEBUG) { 1295 Log.d(TAG, "getSmsThreadId uri: " + uri + 1296 " returning threadId: " + threadId); 1297 } 1298 return threadId; 1299 } else { 1300 if (DEBUG) { 1301 Log.d(TAG, "getSmsThreadId uri: " + uri + 1302 " NULL cursor! returning THREAD_NONE"); 1303 } 1304 return THREAD_NONE; 1305 } 1306 } finally { 1307 cursor.close(); 1308 } 1309 } 1310 1311 /** 1312 * Get the thread ID of the MMS message with the given URI 1313 * @param context The context 1314 * @param uri The URI of the SMS message 1315 * @return The thread ID, or THREAD_NONE if the URI contains no entries 1316 */ getThreadId(Context context, Uri uri)1317 public static long getThreadId(Context context, Uri uri) { 1318 Cursor cursor = SqliteWrapper.query( 1319 context, 1320 context.getContentResolver(), 1321 uri, 1322 MMS_THREAD_ID_PROJECTION, 1323 null, 1324 null, 1325 null); 1326 1327 if (cursor == null) { 1328 if (DEBUG) { 1329 Log.d(TAG, "getThreadId uri: " + uri + " NULL cursor! returning THREAD_NONE"); 1330 } 1331 return THREAD_NONE; 1332 } 1333 1334 try { 1335 if (cursor.moveToFirst()) { 1336 long threadId = cursor.getLong(cursor.getColumnIndex(Mms.THREAD_ID)); 1337 if (DEBUG) { 1338 Log.d(TAG, "getThreadId uri: " + uri + 1339 " returning threadId: " + threadId); 1340 } 1341 return threadId; 1342 } else { 1343 if (DEBUG) { 1344 Log.d(TAG, "getThreadId uri: " + uri + 1345 " NULL cursor! returning THREAD_NONE"); 1346 } 1347 return THREAD_NONE; 1348 } 1349 } finally { 1350 cursor.close(); 1351 } 1352 } 1353 } 1354