package net.everythingandroid.smspopup.util; import java.util.ArrayList; import net.everythingandroid.smspopup.BuildConfig; import net.everythingandroid.smspopup.R; import net.everythingandroid.smspopup.provider.SmsMmsMessage; import net.everythingandroid.smspopup.provider.SmsPopupContract.ContactNotifications; import net.everythingandroid.smspopup.receiver.ReminderReceiver; import net.everythingandroid.smspopup.service.SmsMonitorService; import net.everythingandroid.smspopup.util.ManagePreferences.Defaults; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.database.Cursor; import android.graphics.Color; import android.graphics.Typeface; import android.media.AudioManager; import android.media.MediaPlayer; import android.net.Uri; import android.preference.PreferenceManager; import android.provider.Settings; import android.telephony.TelephonyManager; import android.text.Spannable; import android.text.SpannableString; import android.text.style.StyleSpan; /* * This class handles the Notifications (sounds/vibrate/LED) */ public class ManageNotification { public static final int NOTIFICATION_ALERT = 1337; public static final int NOTIFICATION_TEST = 888; public static final int NOTIFICATION_SEND_FAILED = 100; public static final String defaultRingtone = Settings.System.DEFAULT_NOTIFICATION_URI.toString(); private static final Uri UNDELIVERED_URI = Uri.parse("content://mms-sms/undelivered"); private static MediaPlayer mPlayer = null; private static final int NOTIFY = 0; private static final int FAILED = 1; public static final int[][] NOTIF_ICON_RES = { { R.drawable.stat_notify_sms, R.drawable.stat_notify_sms_failed }, { R.drawable.stat_notify_sms_old, R.drawable.stat_notify_sms_failed }, { R.drawable.stat_notify_sms_htc, R.drawable.stat_notify_sms_failed_htc }, { R.drawable.stat_notify_sms_blur, R.drawable.stat_notify_sms_failed }, { R.drawable.stat_notify_sms_blue, R.drawable.stat_notify_sms_failed }, { R.drawable.stat_notify_sms_gray, R.drawable.stat_notify_sms_failed }, { R.drawable.stat_notify_sms_green, R.drawable.stat_notify_sms_failed }, { R.drawable.stat_notify_sms_lb, R.drawable.stat_notify_sms_failed }, { R.drawable.stat_notify_sms_orange, R.drawable.stat_notify_sms_failed }, { R.drawable.stat_notify_sms_pink, R.drawable.stat_notify_sms_failed }, { R.drawable.stat_notify_sms_purple, R.drawable.stat_notify_sms_failed }, { R.drawable.stat_notify_sms_red, R.drawable.stat_notify_sms_failed }, { R.drawable.stat_notify_sms_white, R.drawable.stat_notify_sms_failed }, { R.drawable.stat_notify_sms_yellow, R.drawable.stat_notify_sms_failed }, { R.drawable.stat_notify_sms_old_blue, R.drawable.stat_notify_sms_failed }, { R.drawable.stat_notify_sms_old_gray, R.drawable.stat_notify_sms_failed }, { R.drawable.stat_notify_sms_old_green, R.drawable.stat_notify_sms_failed }, { R.drawable.stat_notify_sms_old_lb, R.drawable.stat_notify_sms_failed }, { R.drawable.stat_notify_sms_old_orange, R.drawable.stat_notify_sms_failed }, { R.drawable.stat_notify_sms_old_pink, R.drawable.stat_notify_sms_failed }, { R.drawable.stat_notify_sms_old_purple, R.drawable.stat_notify_sms_failed }, { R.drawable.stat_notify_sms_old_red, R.drawable.stat_notify_sms_failed }, { R.drawable.stat_notify_sms_old_white, R.drawable.stat_notify_sms_failed }, { R.drawable.stat_notify_sms_old_yellow, R.drawable.stat_notify_sms_failed }, { R.drawable.stat_notify_sms_htc_blue, R.drawable.stat_notify_sms_failed }, { R.drawable.stat_notify_sms_htc_gray, R.drawable.stat_notify_sms_failed }, { R.drawable.stat_notify_sms_htc_green, R.drawable.stat_notify_sms_failed }, { R.drawable.stat_notify_sms_htc_lb, R.drawable.stat_notify_sms_failed }, { R.drawable.stat_notify_sms_htc_orange, R.drawable.stat_notify_sms_failed }, { R.drawable.stat_notify_sms_htc_pink, R.drawable.stat_notify_sms_failed }, { R.drawable.stat_notify_sms_htc_purple, R.drawable.stat_notify_sms_failed }, { R.drawable.stat_notify_sms_htc_red, R.drawable.stat_notify_sms_failed }, { R.drawable.stat_notify_sms_htc_white, R.drawable.stat_notify_sms_failed }, { R.drawable.stat_notify_sms_htc_yellow, R.drawable.stat_notify_sms_failed }, { R.drawable.stat_notify_sms_blur_blue, R.drawable.stat_notify_sms_failed }, { R.drawable.stat_notify_sms_blur_gray, R.drawable.stat_notify_sms_failed }, { R.drawable.stat_notify_sms_blur_green, R.drawable.stat_notify_sms_failed }, { R.drawable.stat_notify_sms_blur_lb, R.drawable.stat_notify_sms_failed }, { R.drawable.stat_notify_sms_blur_orange, R.drawable.stat_notify_sms_failed }, { R.drawable.stat_notify_sms_blur_pink, R.drawable.stat_notify_sms_failed }, { R.drawable.stat_notify_sms_blur_purple, R.drawable.stat_notify_sms_failed }, { R.drawable.stat_notify_sms_blur_red, R.drawable.stat_notify_sms_failed }, { R.drawable.stat_notify_sms_blur_white, R.drawable.stat_notify_sms_failed }, { R.drawable.stat_notify_sms_blur_yellow, R.drawable.stat_notify_sms_failed }, }; /* * Class to hold the popup notification elements */ static class PopupNotification { public Notification notification; public boolean privacyMode; public boolean privacySender; public boolean privacyAlways; public int notifIcon; public int notifFailedIcon; public boolean replyToThread; PopupNotification(Notification n) { this.notification = n; } public void notify(Context context, int notif) { NotificationManager myNM = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); // Seems this is needed for the number value to take effect on the Notification myNM.cancel(notif); if (BuildConfig.DEBUG) Log.v("*** Notify running ***"); myNM.notify(notif, notification); } } /* * Default to NOTIFICATION_ALERT if notif is left out */ public static void show(Context context, SmsMmsMessage message) { show(context, message, 1); } public static void show(Context context, SmsMmsMessage message, int messageCount) { show(context, message, messageCount, NOTIFICATION_ALERT); } /* * Show/play the notification given a SmsMmsMessage and a notification ID (really just * NOTIFICATION_ALERT for the main alert and NOTIFICATION_TEST for the test notification from * the preferences screen) */ public static void show(Context context, SmsMmsMessage message, int messageCount, int notif) { notify(context, message, messageCount, false, notif); } /* * Only update the notification given the SmsMmsMessage (ie. do not play the vibrate/sound, just * update the text). */ public static void update(Context context, SmsMmsMessage message, int messageCount) { // TODO: can I just use Notification.setLatestEventInfo() to update instead? if (message != null && messageCount > 0) { notify(context, message, messageCount, true, NOTIFICATION_ALERT); } else { // TODO: Should reply flag be set to true? ManageNotification.clearAll(context, true); } } /* * The main notify method */ private static void notify(Context context, SmsMmsMessage message, int messageCount, boolean onlyUpdate, int notif) { if (message == null || messageCount < 1) { return; } final String messageBody = message.getMessageBody(); final String contactName = message.getContactName(); final long timestamp = message.getTimestamp(); final PopupNotification n = buildNotification(context, message.getContactId(), message.getContactLookupKey(), onlyUpdate, notif); if (n == null) { return; } // The notification title, sub-text and text that will scroll String contentTitle = ""; String contentText = ""; SpannableString scrollText; /* * This service runs a content observer on the system sms db to help clear the * notification icon in the case the user reads the messages outside of sms popup. the * service will be stopped when unread messages = 0 */ SmsMonitorService.beginStartingService(context); // If we're in privacy mode and the keyguard is on then just display // the name of the person, otherwise scroll the name and message if (n.privacyMode && (ManageKeyguard.inKeyguardRestrictedInputMode() || n.privacyAlways)) { if (n.privacySender) { scrollText = new SpannableString(context.getString( R.string.notification_scroll_privacy_no_name)); contentTitle = scrollText.toString(); } else { scrollText = new SpannableString(context.getString( R.string.notification_scroll_privacy, contactName)); contentTitle = contactName; contentText = context.getString(R.string.notification_scroll_privacy_no_name); } } else { contentTitle = contactName; contentText = messageBody; scrollText = new SpannableString(context.getString(R.string.notification_scroll, contactName, messageBody)); // Set contact name as bold scrollText.setSpan(new StyleSpan(Typeface.BOLD), 0, contactName.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } // If updating don't set scroll text if (onlyUpdate) { scrollText = null; } // Flag that a notification has been sent for this message message.setNotify(false); // The default intent when the notification is clicked (Inbox) Intent smsIntent = SmsPopupUtils.getSmsInboxIntent(); // If more than one message waiting ... if (messageCount > 1) { contentTitle = context.getString(R.string.notification_multiple_title); contentText = context.getString(R.string.notification_multiple_text, messageCount); } else { // Else 1 message, set intent accordingly smsIntent = message.getReplyIntent(n.replyToThread); } /* * Ok, let's create our Notification object and set up all its parameters. */ // Set the icon, scrolling text and timestamp n.notification.icon = n.notifIcon; n.notification.tickerText = scrollText; n.notification.when = timestamp; // Notification notification = // new Notification(R.drawable.stat_notify_sms, scrollText, timestamp); // Set the PendingIntent if the status message is clicked PendingIntent notifIntent = PendingIntent.getActivity(context, 0, smsIntent, 0); // Set the messages that show when the status bar is pulled down n.notification.setLatestEventInfo(context, contentTitle, contentText, notifIntent); // Set number of events that this notification signifies (unread messages) if (messageCount > 1) { n.notification.number = messageCount; } n.notify(context, notif); } /* * Build the notification from user preferences */ private static PopupNotification buildNotification(Context context, String contactId, String contactLookupKey, boolean onlyUpdate, int notif) { ManagePreferences mPrefs = new ManagePreferences(context, contactId, contactLookupKey); AudioManager mAM = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); // Check if notifications are enabled - if not, we're done :) if (!mPrefs.getBoolean( R.string.pref_notif_enabled_key, Defaults.PREFS_NOTIF_ENABLED, ContactNotifications.ENABLED)) { return null; } // Get some preferences: vibrate and vibrate_pattern prefs boolean vibrate = mPrefs.getBoolean( R.string.pref_vibrate_key, Defaults.PREFS_VIBRATE_ENABLED, ContactNotifications.VIBRATE_ENABLED); String vibrate_pattern_raw = mPrefs.getString( R.string.pref_vibrate_pattern_key, Defaults.PREFS_VIBRATE_PATTERN, ContactNotifications.VIBRATE_PATTERN); String vibrate_pattern_custom_raw = mPrefs.getString( R.string.pref_vibrate_pattern_custom_key, Defaults.PREFS_VIBRATE_PATTERN, ContactNotifications.VIBRATE_PATTERN_CUSTOM); // Get LED preferences boolean flashLed = mPrefs.getBoolean( R.string.pref_flashled_key, Defaults.PREFS_LED_ENABLED, ContactNotifications.LED_ENABLED); String flashLedCol = mPrefs.getString( R.string.pref_flashled_color_key, Defaults.PREFS_LED_COLOR, ContactNotifications.LED_COLOR); String flashLedColCustom = mPrefs.getString( R.string.pref_flashled_color_custom_key, Defaults.PREFS_LED_COLOR, ContactNotifications.LED_COLOR_CUSTOM); String flashLedPattern = mPrefs.getString( R.string.pref_flashled_pattern_key, Defaults.PREFS_LED_PATTERN, ContactNotifications.LED_PATTERN); String flashLedPatternCustom = mPrefs.getString( R.string.pref_flashled_pattern_custom_key, Defaults.PREFS_LED_PATTERN, ContactNotifications.LED_PATTERN_CUSTOM); // Try and parse the user ringtone, use the default if it fails Uri notifSoundUri = Uri.parse(mPrefs.getString( R.string.pref_notif_sound_key, defaultRingtone, ContactNotifications.RINGTONE)); if (BuildConfig.DEBUG) Log.v("Sounds URI = " + notifSoundUri.toString()); // Fetch privacy settings boolean privacyMode = mPrefs.getBoolean(R.string.pref_privacy_key, Defaults.PREFS_PRIVACY); boolean privacySender = mPrefs.getBoolean(R.string.pref_privacy_sender_key, Defaults.PREFS_PRIVACY_SENDER); boolean privacyAlways = mPrefs.getBoolean(R.string.pref_privacy_always_key, Defaults.PREFS_PRIVACY_ALWAYS); // Fetch notification icon int notifIcon = Integer.valueOf(mPrefs.getString(R.string.pref_notif_icon_key, Defaults.PREFS_NOTIF_ICON)); if (notifIcon < 0 || notifIcon >= NOTIF_ICON_RES.length) { notifIcon = Integer.valueOf(Defaults.PREFS_NOTIF_ICON); mPrefs.putString(R.string.pref_notif_icon_key, String.valueOf(notifIcon)); } boolean replyToThread = mPrefs.getBoolean(R.string.pref_reply_to_thread_key, Defaults.PREFS_REPLY_TO_THREAD); boolean notifyOnCall = mPrefs.getBoolean(R.string.pref_notifyOnCall_key, Defaults.PREFS_NOTIFY_ON_CALL); // All done with prefs, close it up mPrefs.close(); /* * Ok, let's create our Notification object and set up all its parameters. */ Notification notification = new Notification(); // Set auto-cancel flag notification.flags = Notification.FLAG_AUTO_CANCEL; // Set audio stream to ring notification.audioStreamType = Notification.STREAM_DEFAULT; /* * If this is a new notification (not updating a notification) then set LED, vibrate and * ringtone to fire */ if (!onlyUpdate) { // Set up LED pattern and color if (flashLed) { notification.flags |= Notification.FLAG_SHOW_LIGHTS; /* * Set up LED blinking pattern */ int[] led_pattern = null; if (context.getString(R.string.pref_custom_val).equals(flashLedPattern)) { led_pattern = parseLEDPattern(flashLedPatternCustom); } else { led_pattern = parseLEDPattern(flashLedPattern); } // Set to default if there was a problem if (led_pattern == null) { led_pattern = parseLEDPattern(Defaults.PREFS_LED_PATTERN); } notification.ledOnMS = led_pattern[0]; notification.ledOffMS = led_pattern[1]; /* * Set up LED color */ // Check if a custom color is set if (context.getString(R.string.pref_custom_val).equals(flashLedCol)) { flashLedCol = flashLedColCustom; } // Default in case the parse fails int col = Color.parseColor(Defaults.PREFS_LED_COLOR); // Try and parse the color if (flashLedCol != null) { try { col = Color.parseColor(flashLedCol); } catch (IllegalArgumentException e) { // No need to do anything here } } notification.ledARGB = col; } // Get system telephony manager TelephonyManager mTM = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); // Setup vibrate and notification sound only if call state is idle (not on a call) if (mTM.getCallState() == TelephonyManager.CALL_STATE_IDLE) { /* * Set up vibrate pattern */ // If vibrate is ON, or if phone is set to vibrate if ((vibrate || AudioManager.RINGER_MODE_VIBRATE == mAM.getRingerMode())) { long[] vibrate_pattern = null; if (context.getString(R.string.pref_custom_val).equals(vibrate_pattern_raw)) { vibrate_pattern = parseVibratePattern(vibrate_pattern_custom_raw); } else { vibrate_pattern = parseVibratePattern(vibrate_pattern_raw); } if (vibrate_pattern != null) { notification.vibrate = vibrate_pattern; } else { notification.defaults = Notification.DEFAULT_VIBRATE; } } /* * Set up notification sound */ notification.sound = notifSoundUri; } else if (notifyOnCall) { // On a call or making a call try { // Use MediaPlayer to play so they can hear the notification over the ear piece if (mPlayer == null) { mPlayer = MediaPlayer.create(context, notifSoundUri); } // Check null again in case mediaplayer couldn't be created if (mPlayer != null) { mPlayer.start(); } } catch (IllegalStateException e) { if (BuildConfig.DEBUG) Log.v("MediaPlayer, IllegalStateException - " + e); } } } // Set intent to execute if the "clear all" notifications button is pressed - // basically stop any future reminders. Intent deleteIntent = new Intent(new Intent(context, ReminderReceiver.class)); deleteIntent.setAction(Intent.ACTION_DELETE); PendingIntent pendingDeleteIntent = PendingIntent.getBroadcast(context, 0, deleteIntent, 0); notification.deleteIntent = pendingDeleteIntent; PopupNotification popupNotification = new PopupNotification(notification); popupNotification.replyToThread = replyToThread; popupNotification.privacyMode = privacyMode; popupNotification.privacySender = privacySender; popupNotification.privacyAlways = privacyAlways; popupNotification.notifIcon = NOTIF_ICON_RES[notifIcon][NOTIFY]; popupNotification.notifFailedIcon = NOTIF_ICON_RES[notifIcon][FAILED]; return popupNotification; } // Clear the standard notification alert public static void clear(Context context) { clear(context, NOTIFICATION_ALERT); } // Clear a single notification public static void clear(Context context, int notif) { NotificationManager myNM = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); myNM.cancel(notif); } /* * Build the notification with no contactId */ private static PopupNotification buildNotification(Context context, boolean onlyUpdate, int notif) { return buildNotification(context, null, null, onlyUpdate, notif); } public static void clearAll(Context context, boolean reply) { SharedPreferences myPrefs = PreferenceManager.getDefaultSharedPreferences(context); if (reply || myPrefs.getBoolean(context.getString(R.string.pref_markread_key), Defaults.PREFS_MARK_READ)) { NotificationManager myNM = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); myNM.cancelAll(); } } public static void clearAll(Context context) { clearAll(context, true); } /** * Parse the user provided custom vibrate pattern into a long[] * */ // TODO: tidy this up public static long[] parseVibratePattern(String stringPattern) { ArrayList<Long> arrayListPattern = new ArrayList<Long>(); Long l; if (stringPattern == null) return null; String[] splitPattern = stringPattern.split(","); int VIBRATE_PATTERN_MAX_SECONDS = 60000; int VIBRATE_PATTERN_MAX_PATTERN = 100; for (int i = 0; i < splitPattern.length; i++) { try { l = Long.parseLong(splitPattern[i].trim()); } catch (NumberFormatException e) { return null; } if (l > VIBRATE_PATTERN_MAX_SECONDS) { return null; } arrayListPattern.add(l); } // TODO: can i just cast the whole ArrayList into long[]? int size = arrayListPattern.size(); if (size > 0 && size < VIBRATE_PATTERN_MAX_PATTERN) { long[] pattern = new long[size]; for (int i = 0; i < pattern.length; i++) { pattern[i] = arrayListPattern.get(i); } return pattern; } return null; } /** * Parse LED pattern string into int[] * * @param stringPattern * @return */ public static int[] parseLEDPattern(String stringPattern) { int on, off; if (stringPattern == null) return null; String[] splitPattern = stringPattern.split(","); if (splitPattern.length != 2) return null; final int LED_PATTERN_MIN_SECONDS = 0; final int LED_PATTERN_MAX_SECONDS = 60000; try { on = Integer.parseInt(splitPattern[0]); } catch (NumberFormatException e) { return null; } try { off = Integer.parseInt(splitPattern[1]); } catch (NumberFormatException e) { return null; } if (on >= LED_PATTERN_MIN_SECONDS && on <= LED_PATTERN_MAX_SECONDS && off >= LED_PATTERN_MIN_SECONDS && off <= LED_PATTERN_MAX_SECONDS) { return new int[] { on, off }; } return null; } public static void notifySendFailed(Context context) { PopupNotification n = buildNotification(context, false, NOTIFICATION_SEND_FAILED); if (n == null) return; // The notification title, sub-text and text that will scroll String contentTitle; String contentText; // SpannableString scrollText; long[] threadIdResult = { 0 }; int failedCount = getUndeliveredMessageCount(context, threadIdResult); // The default intent when the notification is clicked (Inbox) Intent smsIntent = SmsPopupUtils.getSmsInboxIntent(); // If more than one message failed if (failedCount > 1) { contentTitle = "Multiple errors"; contentText = "Multiple errors when sending messages"; // smsIntent = SMSPopupUtils.getSmsIntent(); } else { // Else 1 message failed contentTitle = "Error sending message"; contentText = "Error sending message"; long threadId = (threadIdResult[0] != 0 ? threadIdResult[0] : 0); smsIntent = SmsPopupUtils.getSmsToIntent(context, threadId); } // Set the icon, scrolling text and timestamp n.notification.icon = n.notifFailedIcon; n.notification.tickerText = contentTitle; n.notification.when = System.currentTimeMillis(); // Set the PendingIntent if the status message is clicked PendingIntent notifIntent = PendingIntent.getActivity(context, 0, smsIntent, 0); // Set the messages that show when the status bar is pulled down n.notification.setLatestEventInfo(context, contentTitle, contentText, notifIntent); n.notify(context, NOTIFICATION_SEND_FAILED); } // threadIdResult[0] contains the thread id of the first message. // threadIdResult[1] is nonzero if the thread ids of all the messages are the same. // You can pass in null for threadIdResult. // You can pass in a threadIdResult of size 1 to avoid the comparison of each thread id. private static int getUndeliveredMessageCount(Context context, long[] threadIdResult) { // TODO: switch projection to use common static variables Cursor undeliveredCursor = context.getContentResolver().query( UNDELIVERED_URI, new String[] { "thread_id" // Mms.THREAD_ID }, "read=0", null, null); if (undeliveredCursor == null) { return 0; } int count = undeliveredCursor.getCount(); try { if (threadIdResult != null && undeliveredCursor.moveToFirst()) { threadIdResult[0] = undeliveredCursor.getLong(0); if (threadIdResult.length >= 2) { // Test to see if all the undelivered messages belong to the same thread. long firstId = threadIdResult[0]; while (undeliveredCursor.moveToNext()) { if (undeliveredCursor.getLong(0) != firstId) { firstId = 0; break; } } threadIdResult[1] = firstId; // non-zero if all ids are the same } } } finally { undeliveredCursor.close(); } return count; } public static void updateSendFailedNotification(Context context) { if (getUndeliveredMessageCount(context, null) < 1) { clear(context, NOTIFICATION_SEND_FAILED); } else { notifySendFailed(context); // rebuild and adjust the message count if necessary. } } }