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