From 61960c9add7d16aea804f53322850fa0d2837af2 Mon Sep 17 00:00:00 2001 From: Steven Luo Date: Sun, 29 May 2011 17:49:07 -0700 Subject: [PATCH] Overhaul notifications system Features: * Now displays the number of mentions that the user has not seen in the notification. * When no mentions are outstanding, display which servers the user is connected to, not the last message. * When more than one mention is outstanding, display the names of the conversations with new mentions, not just the last message received. * Notifications of mentions are suppressed if you're in the conversation at the time of the mention. * Notifications of mentions automatically clear when you bring up the conversation. * Vibrate notifications now generate the user's chosen default vibrate pattern, not a hard-coded one. * Add ticker text to the notification that's displayed when the IRCService goes into the foreground, instead of displaying a blank ticker. To allow for all of this, the implementation moves most of the details of generating the notification text into the IRCService, which now exposes addNewMention() and notifyConnected()/notifyDisconnected() methods instead of the lower-level updateNotification(). --- application/res/values/strings.xml | 2 + .../yaaic/activity/ConversationActivity.java | 14 +- .../src/org/yaaic/irc/IRCConnection.java | 48 +++--- application/src/org/yaaic/irc/IRCService.java | 146 +++++++++++++++--- .../ConversationSelectedListener.java | 14 +- .../src/org/yaaic/model/Conversation.java | 25 +++ application/src/org/yaaic/model/Server.java | 17 ++ 7 files changed, 225 insertions(+), 41 deletions(-) diff --git a/application/res/values/strings.xml b/application/res/values/strings.xml index 366112c..56c10cb 100644 --- a/application/res/values/strings.xml +++ b/application/res/values/strings.xml @@ -113,8 +113,10 @@ Give a user voice status Get information about a user + Yaaic is running Connected to %1$s Disconnected from %1$s + New messages in: %1$s Connected to %1$s %1$s deops %2$s diff --git a/application/src/org/yaaic/activity/ConversationActivity.java b/application/src/org/yaaic/activity/ConversationActivity.java index be27f33..8f4d6a6 100644 --- a/application/src/org/yaaic/activity/ConversationActivity.java +++ b/application/src/org/yaaic/activity/ConversationActivity.java @@ -154,7 +154,7 @@ public class ConversationActivity extends Activity implements ServiceConnection, deckAdapter = new DeckAdapter(); deck = (Gallery) findViewById(R.id.deck); - deck.setOnItemSelectedListener(new ConversationSelectedListener(server, (TextView) findViewById(R.id.title), dots)); + deck.setOnItemSelectedListener(new ConversationSelectedListener(this, server, (TextView) findViewById(R.id.title), dots)); deck.setAdapter(deckAdapter); deck.setOnItemClickListener(new ConversationClickListener(deckAdapter, switcher)); deck.setBackgroundDrawable(new NonScalingBackgroundDrawable(this, deck, R.drawable.background)); @@ -244,6 +244,14 @@ public class ConversationActivity extends Activity implements ServiceConnection, mAdapter.addBulkMessages(conversation.getBuffer()); conversation.clearBuffer(); } + + // Clear new message notifications for the selected conversation + if (conversation.getStatus() == Conversation.STATUS_SELECTED && conversation.getNewMentions() > 0) { + Intent ackIntent = new Intent(this, IRCService.class); + ackIntent.setAction(IRCService.ACTION_ACK_NEW_MENTIONS); + ackIntent.putExtra(IRCService.EXTRA_ACK_CONVTITLE, conversation.getName()); + startService(ackIntent); + } } // Join channel that has been selected in JoinActivity (onActivityResult()) @@ -256,6 +264,8 @@ public class ConversationActivity extends Activity implements ServiceConnection, } }.start(); } + + server.setIsForeground(true); } /** @@ -266,6 +276,8 @@ public class ConversationActivity extends Activity implements ServiceConnection, { super.onPause(); + server.setIsForeground(false); + if (binder != null && binder.getService() != null) { binder.getService().checkServiceStatus(); } diff --git a/application/src/org/yaaic/irc/IRCConnection.java b/application/src/org/yaaic/irc/IRCConnection.java index fe57eaa..c1cdc13 100644 --- a/application/src/org/yaaic/irc/IRCConnection.java +++ b/application/src/org/yaaic/irc/IRCConnection.java @@ -141,7 +141,7 @@ public class IRCConnection extends PircBot Broadcast.createServerIntent(Broadcast.SERVER_UPDATE, server.getId()) ); - service.updateNotification(service.getString(R.string.notification_connected, server.getTitle())); + service.notifyConnected(server.getTitle()); Message message = new Message(service.getString(R.string.message_connected, server.getTitle())); message.setColor(Message.COLOR_GREEN); @@ -245,11 +245,14 @@ public class IRCConnection extends PircBot boolean mentioned = isMentioned(action); if (mentioned || target.equals(this.getNick())) { - service.updateNotification( - target + ": " + sender + " " + action, - service.getSettings().isVibrateHighlightEnabled(), - service.getSettings().isSoundHighlightEnabled() - ); + if (conversation.getStatus() != Conversation.STATUS_SELECTED || !server.getIsForeground()) { + service.addNewMention( + conversation, + conversation.getName() + ": " + sender + " " + action, + service.getSettings().isVibrateHighlightEnabled(), + service.getSettings().isSoundHighlightEnabled() + ); + } } if (mentioned) { @@ -410,20 +413,24 @@ public class IRCConnection extends PircBot protected void onMessage(String target, String sender, String login, String hostname, String text) { Message message = new Message(text, sender); + Conversation conversation = server.getConversation(target); if (isMentioned(text)) { // highlight message.setColor(Message.COLOR_RED); - service.updateNotification( - target + ": <" + sender + "> " + text, - service.getSettings().isVibrateHighlightEnabled(), - service.getSettings().isSoundHighlightEnabled() - ); + if (conversation.getStatus() != Conversation.STATUS_SELECTED || !server.getIsForeground()) { + service.addNewMention( + conversation, + target + ": <" + sender + "> " + text, + service.getSettings().isVibrateHighlightEnabled(), + service.getSettings().isSoundHighlightEnabled() + ); + } - server.getConversation(target).setStatus(Conversation.STATUS_HIGHLIGHT); + conversation.setStatus(Conversation.STATUS_HIGHLIGHT); } - server.getConversation(target).addMessage(message); + conversation.addMessage(message); Intent intent = Broadcast.createConversationIntent( Broadcast.CONVERSATION_MESSAGE, @@ -619,11 +626,14 @@ public class IRCConnection extends PircBot return; } - service.updateNotification( - "<" + sender + "> " + text, - service.getSettings().isVibrateHighlightEnabled(), - service.getSettings().isSoundHighlightEnabled() - ); + if (conversation.getStatus() != Conversation.STATUS_SELECTED || !server.getIsForeground()) { + service.addNewMention( + conversation, + "<" + sender + "> " + text, + service.getSettings().isVibrateHighlightEnabled(), + service.getSettings().isSoundHighlightEnabled() + ); + } if (isMentioned(text)) { message.setColor(Message.COLOR_RED); @@ -1099,7 +1109,7 @@ public class IRCConnection extends PircBot server.setStatus(Status.DISCONNECTED); } - service.updateNotification(service.getString(R.string.notification_disconnected, server.getTitle())); + service.notifyDisconnected(server.getTitle()); Intent sIntent = Broadcast.createServerIntent(Broadcast.SERVER_UPDATE, server.getId()); service.sendBroadcast(sIntent); diff --git a/application/src/org/yaaic/irc/IRCService.java b/application/src/org/yaaic/irc/IRCService.java index 499de71..4da5799 100644 --- a/application/src/org/yaaic/irc/IRCService.java +++ b/application/src/org/yaaic/irc/IRCService.java @@ -24,6 +24,7 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; +import java.util.LinkedHashMap; import org.jibble.pircbot.IrcException; import org.jibble.pircbot.NickAlreadyInUseException; @@ -32,6 +33,7 @@ import org.yaaic.Yaaic; import org.yaaic.activity.ServersActivity; import org.yaaic.db.Database; import org.yaaic.model.Broadcast; +import org.yaaic.model.Conversation; import org.yaaic.model.Message; import org.yaaic.model.Server; import org.yaaic.model.ServerInfo; @@ -54,6 +56,11 @@ public class IRCService extends Service private final IRCBinder binder; private final HashMap connections; private boolean foreground = false; + private ArrayList connectedServerTitles; + private LinkedHashMap mentions; + private int newMentions = 0; + + private static final int FOREGROUND_NOTIFICATION = 1; @SuppressWarnings("rawtypes") private static final Class[] mStartForegroundSignature = new Class[] { int.class, Notification.class }; @@ -62,6 +69,8 @@ public class IRCService extends Service public static final String ACTION_FOREGROUND = "org.yaaic.service.foreground"; public static final String ACTION_BACKGROUND = "org.yaaic.service.background"; + public static final String ACTION_ACK_NEW_MENTIONS = "org.yaaic.service.ack_new_mentions"; + public static final String EXTRA_ACK_CONVTITLE = "org.yaaic.service.ack_convtitle"; private NotificationManager notificationManager; private Method mStartForeground; @@ -80,6 +89,8 @@ public class IRCService extends Service this.connections = new HashMap(); this.binder = new IRCBinder(this); + this.connectedServerTitles = new ArrayList(); + this.mentions = new LinkedHashMap(); } /** @@ -166,7 +177,7 @@ public class IRCService extends Service foreground = true; // Set the icon, scrolling text and timestamp - notification = new Notification(R.drawable.icon, "", System.currentTimeMillis()); + notification = new Notification(R.drawable.icon, getText(R.string.notification_running), System.currentTimeMillis()); // The PendingIntent to launch our activity if the user selects this notification Intent notifyIntent = new Intent(this, ServersActivity.class); @@ -176,51 +187,146 @@ public class IRCService extends Service // Set the info for the views that show in the notification panel. notification.setLatestEventInfo(this, getText(R.string.app_name), "", contentIntent); - startForegroundCompat(R.string.app_name, notification); + startForegroundCompat(FOREGROUND_NOTIFICATION, notification); } else if (ACTION_BACKGROUND.equals(intent.getAction()) && !foreground) { - stopForegroundCompat(R.string.app_name); + stopForegroundCompat(FOREGROUND_NOTIFICATION); + } else if (ACTION_ACK_NEW_MENTIONS.equals(intent.getAction())) { + ackNewMentions(intent.getStringExtra(EXTRA_ACK_CONVTITLE)); } } - /** - * Update notification - * - * @param text The text to display - */ - public void updateNotification(String text) - { - updateNotification(text, false, false); - } - /** * Update notification and vibrate if needed * - * @param text The text to display + * @param text The ticker text to display + * @param contentText The text to display in the notification dropdown * @param vibrate True if the device should vibrate, false otherwise + * @param sound True if the device should make sound, false otherwise */ - public void updateNotification(String text, boolean vibrate, boolean sound) + private void updateNotification(String text, String contentText, boolean vibrate, boolean sound) { if (foreground) { - notificationManager.cancel(R.string.app_name); notification = new Notification(R.drawable.icon, text, System.currentTimeMillis()); Intent notifyIntent = new Intent(this, ServersActivity.class); notifyIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); PendingIntent contentIntent = PendingIntent.getActivity(this, 0, notifyIntent, 0); - notification.setLatestEventInfo(this, getText(R.string.app_name), text, contentIntent); + + if (contentText == null) { + if (!connectedServerTitles.isEmpty()) { + StringBuilder sb = new StringBuilder(); + for (String title : connectedServerTitles) { + sb.append(title + ", "); + } + contentText = getString(R.string.notification_connected, sb.substring(0, sb.length()-2)); + } else { + contentText = ""; + } + } + + notification.setLatestEventInfo(this, getText(R.string.app_name), contentText, contentIntent); if (vibrate) { - long[] pattern = {0,100,200,300}; - notification.vibrate = pattern; + notification.defaults |= Notification.DEFAULT_VIBRATE; } if (sound) { notification.defaults |= Notification.DEFAULT_SOUND; } - notificationManager.notify(R.string.app_name, notification); + notification.number = newMentions; + + notificationManager.notify(FOREGROUND_NOTIFICATION, notification); } } + /** + * Update the status bar notification for a new mention + */ + private void notifyMention(String msg, boolean vibrate, boolean sound) + { + String contentText = null; + + if (newMentions == 1 && msg != null) { + contentText = msg; + } else if (newMentions >= 1) { + StringBuilder sb = new StringBuilder(); + for (Conversation conv : mentions.values()) { + sb.append(conv.getName() + " (" + conv.getNewMentions() + "), "); + } + contentText = getString(R.string.notification_mentions, sb.substring(0, sb.length()-2)); + } + + updateNotification(msg, contentText, vibrate, sound); + } + + /** + * Notify the service of a new mention (updates the status bar notification) + * + * @param conversation The conversation where the new mention occurred + * @param msg The text of the new message + * @param vibrate Whether the notification should include vibration + * @param sound Whether the notification should include sound + */ + public void addNewMention(Conversation conversation, String msg, boolean vibrate, boolean sound) + { + if (conversation == null) + return; + + String convTitle = conversation.getName(); + + conversation.addNewMention(); + ++newMentions; + if (!mentions.containsKey(convTitle)) { + mentions.put(convTitle, conversation); + } + + notifyMention(msg, vibrate, sound); + } + + /** + * Notify the service that new mentions have been viewed (updates the status bar notification) + * + * @param convTitle The title of the conversation whose new mentions have been read + */ + public void ackNewMentions(String convTitle) + { + if (convTitle == null) + return; + + Conversation conversation = mentions.remove(convTitle); + if (conversation == null) + return; + newMentions -= conversation.getNewMentions(); + conversation.clearNewMentions(); + if (newMentions < 0) + newMentions = 0; + + notifyMention(null, false, false); + } + + /** + * Notify the service of connection to a server (updates the status bar notification) + * + * @param title The title of the newly connected server + */ + public void notifyConnected(String title) + { + connectedServerTitles.add(title); + updateNotification(getString(R.string.notification_connected, title), null, false, false); + } + + /** + * Notify the service of disconnection from a server (updates the status bar notification) + * + * @param title The title of the disconnected server + */ + public void notifyDisconnected(String title) + { + connectedServerTitles.remove(title); + updateNotification(getString(R.string.notification_disconnected, title), null, false, false); + } + + /** * This is a wrapper around the new startForeground method, using the older * APIs if it is not available. diff --git a/application/src/org/yaaic/listener/ConversationSelectedListener.java b/application/src/org/yaaic/listener/ConversationSelectedListener.java index 1363614..5a83a93 100644 --- a/application/src/org/yaaic/listener/ConversationSelectedListener.java +++ b/application/src/org/yaaic/listener/ConversationSelectedListener.java @@ -24,11 +24,14 @@ import org.yaaic.model.Channel; import org.yaaic.model.Conversation; import org.yaaic.model.Server; import org.yaaic.view.ConversationSwitcher; +import org.yaaic.irc.IRCService; import android.view.View; import android.widget.AdapterView; import android.widget.AdapterView.OnItemSelectedListener; import android.widget.TextView; +import android.content.Context; +import android.content.Intent; /** * Listener for conversation selections @@ -37,6 +40,7 @@ import android.widget.TextView; */ public class ConversationSelectedListener implements OnItemSelectedListener { + private final Context ctx; private final Server server; private final TextView titleView; private final ConversationSwitcher switcher; @@ -47,8 +51,9 @@ public class ConversationSelectedListener implements OnItemSelectedListener * @param server * @param titleView */ - public ConversationSelectedListener(Server server, TextView titleView, ConversationSwitcher switcher) + public ConversationSelectedListener(Context ctx, Server server, TextView titleView, ConversationSwitcher switcher) { + this.ctx = ctx; this.server = server; this.titleView = titleView; this.switcher = switcher; @@ -80,6 +85,13 @@ public class ConversationSelectedListener implements OnItemSelectedListener previousConversation.setStatus(Conversation.STATUS_DEFAULT); } + if (conversation.getNewMentions() > 0) { + Intent i = new Intent(ctx, IRCService.class); + i.setAction(IRCService.ACTION_ACK_NEW_MENTIONS); + i.putExtra(IRCService.EXTRA_ACK_CONVTITLE, conversation.getName()); + ctx.startService(i); + } + conversation.setStatus(Conversation.STATUS_SELECTED); server.setSelectedConversation(conversation.getName()); } diff --git a/application/src/org/yaaic/model/Conversation.java b/application/src/org/yaaic/model/Conversation.java index 43f3013..fc83922 100644 --- a/application/src/org/yaaic/model/Conversation.java +++ b/application/src/org/yaaic/model/Conversation.java @@ -47,6 +47,7 @@ public abstract class Conversation private final LinkedList history; private final String name; private int status = 1; + private int newMentions = 0; /** * Get the type of conversation (channel, query, ..) @@ -179,4 +180,28 @@ public abstract class Conversation { return status; } + + /** + * Increment the count of unread mentions in this conversation + */ + public void addNewMention() + { + ++newMentions; + } + + /** + * Mark all new mentions as unread + */ + public void clearNewMentions() + { + newMentions = 0; + } + + /** + * Get the number of unread mentions in this conversation + */ + public int getNewMentions() + { + return newMentions; + } } diff --git a/application/src/org/yaaic/model/Server.java b/application/src/org/yaaic/model/Server.java index e28e405..2053826 100644 --- a/application/src/org/yaaic/model/Server.java +++ b/application/src/org/yaaic/model/Server.java @@ -48,6 +48,7 @@ public class Server private int status = Status.DISCONNECTED; private String selected = ""; + private boolean isForeground = false; /** * Create a new server object @@ -404,4 +405,20 @@ public class Server return R.drawable.connecting; } + + /** + * Get whether a ConversationActivity for this server is currently in the foreground. + */ + public boolean getIsForeground() + { + return isForeground; + } + + /** + * Set whether a ConversationActivity for this server is currently in the foreground. + */ + public void setIsForeground(boolean isForeground) + { + this.isForeground = isForeground; + } }