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().
---
v2:
* If there's a new message notification, keep showing the "New messages
  in" content text, even after a disconnect notification.
* Handle the case of a channel/query name duplicated between two or more
  connections more gracefully in new message notifications
* Fix a race in updating notifications

 application/res/values/strings.xml                 |    2 +
 .../org/yaaic/activity/ConversationActivity.java   |   15 ++-
 application/src/org/yaaic/irc/IRCConnection.java   |   51 +++++---
 application/src/org/yaaic/irc/IRCService.java      |  143 +++++++++++++++++---
 .../listener/ConversationSelectedListener.java     |   15 ++-
 application/src/org/yaaic/model/Conversation.java  |   25 ++++
 application/src/org/yaaic/model/Server.java        |   17 +++
 7 files changed, 227 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 @@
     <string name="command_desc_voice">Give a user voice status</string>
     <string name="command_desc_whois">Get information about a user</string>
 
+    <string name="notification_running">Yaaic is running</string>
     <string name="notification_connected">Connected to %1$s</string>
     <string name="notification_disconnected">Disconnected from %1$s</string>
+    <string name="notification_mentions">New messages in: %1$s</string>
 
     <string name="message_connected">Connected to %1$s</string>
     <string name="message_deop">%1$s deops %2$s</string>
diff --git a/application/src/org/yaaic/activity/ConversationActivity.java 
b/application/src/org/yaaic/activity/ConversationActivity.java
index 5892215..29bb562 100644
--- a/application/src/org/yaaic/activity/ConversationActivity.java
+++ b/application/src/org/yaaic/activity/ConversationActivity.java
@@ -153,7 +153,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));
@@ -237,6 +237,15 @@ 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_SERVERID, serverId);
+                ackIntent.putExtra(IRCService.EXTRA_ACK_CONVTITLE, 
conversation.getName());
+                startService(ackIntent);
+            }
         }
 
         // Join channel that has been selected in JoinActivity 
(onActivityResult())
@@ -249,6 +258,8 @@ public class ConversationActivity extends Activity 
implements ServiceConnection,
                 }
             }.start();
         }
+
+        server.setIsForeground(true);
     }
 
     /**
@@ -259,6 +270,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 9bb81a3..dc031c2 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,15 @@ 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(
+                    server.getId(),
+                    conversation,
+                    conversation.getName() + ": " + sender + " " + action,
+                    service.getSettings().isVibrateHighlightEnabled(),
+                    service.getSettings().isSoundHighlightEnabled()
+                );
+            }
         }
 
         if (mentioned) {
@@ -410,20 +414,25 @@ 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(
+                    server.getId(),
+                    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 +628,15 @@ 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(
+                server.getId(),
+                conversation,
+                "<" + sender + "> " + text,
+                service.getSettings().isVibrateHighlightEnabled(),
+                service.getSettings().isSoundHighlightEnabled()
+            );
+        }
 
         if (isMentioned(text)) {
             message.setColor(Message.COLOR_RED);
@@ -1091,7 +1104,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..29daabc 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<Integer, IRCConnection> connections;
     private boolean foreground = false;
+    private ArrayList<String> connectedServerTitles;
+    private LinkedHashMap<String, Conversation> 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,9 @@ 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_SERVERID = 
"org.yaaic.service.ack_serverid";
+    public static final String EXTRA_ACK_CONVTITLE = 
"org.yaaic.service.ack_convtitle";
 
     private NotificationManager notificationManager;
     private Method mStartForeground;
@@ -80,6 +90,8 @@ public class IRCService extends Service
 
         this.connections = new HashMap<Integer, IRCConnection>();
         this.binder = new IRCBinder(this);
+        this.connectedServerTitles = new ArrayList<String>();
+        this.mentions = new LinkedHashMap<String, Conversation>();
     }
 
     /**
@@ -166,7 +178,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,52 +188,143 @@ 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.getIntExtra(EXTRA_ACK_SERVERID, -1), 
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 (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));
+                } else 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);
+        }
+    }
+
+    /**
+     * Generates a string uniquely identifying a conversation.
+     */
+    public String getConversationId(int serverId, String title) {
+        return "" + serverId + ":" + title;
+    }
+
+    /**
+     * 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 synchronized void addNewMention(int serverId, Conversation 
conversation, String msg, boolean vibrate, boolean sound)
+    {
+        if (conversation == null)
+            return;
+
+        conversation.addNewMention();
+        ++newMentions;
+        String convId = getConversationId(serverId, conversation.getName());
+        if (!mentions.containsKey(convId)) {
+            mentions.put(convId, conversation);
+        }
+
+        if (newMentions == 1) {
+            updateNotification(msg, msg, vibrate, sound);
+        } else {
+            updateNotification(msg, null, 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 synchronized void ackNewMentions(int serverId, String convTitle)
+    {
+        if (convTitle == null)
+            return;
+
+        Conversation conversation = 
mentions.remove(getConversationId(serverId, convTitle));
+        if (conversation == null)
+            return;
+        newMentions -= conversation.getNewMentions();
+        conversation.clearNewMentions();
+        if (newMentions < 0)
+            newMentions = 0;
+
+        updateNotification(null, 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 synchronized 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 synchronized 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 01818b6..26c7d19 100644
--- a/application/src/org/yaaic/listener/ConversationSelectedListener.java
+++ b/application/src/org/yaaic/listener/ConversationSelectedListener.java
@@ -23,11 +23,14 @@ package org.yaaic.listener;
 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
@@ -36,6 +39,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;
@@ -46,8 +50,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;
@@ -75,6 +80,14 @@ 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_SERVERID, server.getId());
+                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<Message> 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;
+    }
 }
-- 
1.7.2.5

Reply via email to