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.
}
}
}