Build

Build Real-time Android Chat: User List With Presence

Michael Carroll on Jul 29, 2019
Build Real-time Android Chat: User List With Presence

This is Part Three of our four part series on building a mobile chat app for Android with PubNub. In our previous part, we covered how to build basic Android messaging and storing message presence.

In this part, we'll dive deeper into Presence, including how we made the “Presence” (buddy list/who’s online) tab in our sample app along with the associated patterns, data structures and resources in Android.

This will allow you to create a “buddy list” tab that can:

  • Display a dynamic list of users/devices with current presence state information.
  • Detect offline/disconnected users using the heartbeat feature.
  • Use the hereNow() feature to populate the presence list on first connection.

To do this, we dive into the core concepts of presence callbacks, more advanced Presence use cases, as well as the Android machinery that makes it all possible.

Android Data Stream Presence Tab

What is Presence?

PubNub Presence is our API for automatic detection of device and user state to identify who and what is connected. Built upon the publish/subscribe foundation, Presence makes it easy to deliver who’s online, and what are they doing, with no repeated requests.

The state provides data like name, age, online status (available, busy, typing, geolocation, etc.) enabling you to create rich user experience applications that leverage the actual participants of your application.

Presence Add-On

Presence has a few nuances we should talk about before diving in further. The first is that Presence is an add-on, so you’ll need to make sure it’s enabled in your application configuration.

There are 3 types of presence events:

  • Join: a user/device has subscribed to the channel.
  • Leave: a user/device has disconnected or unsubscribed from a channel.
  • Timeout: the user/device has not responded to PubNub within the specified heartbeat configuration.

In our case, we simply pass through the string value of the event for display purposes. In your application, you might choose to use terms like online, offline, idle, disconnected, unknown, etc.

Advanced PubNub users might say “Hey, you forgot about the state-change event!” That’s a great catch! We’re not forgetting it, we’re just saving it as an advanced feature at the end so that we can focus on core presence features. Adding a few lines of code to handle that one additional event type in the future should be a piece of cake.

Initializing PubNub

If you want to use PubNub, you’ll need to make friends with the PubNub class. You can create several instances of the App Context if you need multiple connections with different keys, callbacks or other configuration. In the sample app, we create two App Contexts because we want to separate the Pub/Sub and Presence listeners from the Multiplexing feature. That’s a rare case – you’ll probably only need one instance.

In our case, we initialize the PubNub object(s) in the MainActivity class right after the user logs in via the LoginActivity. This is because the username is required as the UUID field of the configuration object below. Note: if your application allows rapid switching between users, you may want to think carefully about how to create and destroy the App Context accordingly.

PNConfiguration config = new PNConfiguration();
config.setPublishKey(Constants.PUBNUB_PUBLISH_KEY);
config.setSubscribeKey(Constants.PUBNUB_SUBSCRIBE_KEY);
config.setUuid(this.mUsername);
config.setSecure(true);
this.mPubnub_DataStream = new PubNub(config);

Just to recap the code above.

  • Step 1: create a PNConfiguration object.
  • Step 2: pass in the Publish and Subscribe keys.
  • Step 3: pass in the user/device UUID (whatever you like, as long as it’s unique – this UUID will also used by the presence API).
  • Step 4: set the secure option to use TLS.
  • Step 5: create a new PubNub instance using the specified PNConfiguration.
  • One thing we should also note – if you really want to dive into the fluent API, you can also do something like this:
this.mPubnub_DataStream =
    new PubNub(new PNConfiguration()
        .setPublishKey(Constants.PUBNUB_PUBLISH_KEY)
        .setSubscribeKey(Constants.PUBNUB_SUBSCRIBE_KEY)
        .setUuid(this.mUsername).setSecure(true));

Nice, now we’re having some fun with it!

Subscribing to a Channel

OK, now that we have a connection to PubNub, the next thing we’d like to do is subscribe to a channel so we can receive presence events and display them in the UI ListView. That takes three steps:

  • Step 1 (optional): Create an Adapter to bridge between the callback and the application UI View.
  • Step 2: Create a subscription callback (that calls methods in the adapter, if applicable).
  • Step 3: Perform the channel subscription.

Once the app completes the subscription, the callback will be invoked for each new presence event that comes from PubNub.

Here’s how to create a subscription callback:

public class PresencePnCallback extends SubscribeCallback {
    private static final String TAG = PresencePnCallback.class.getName();
    private final PresenceListAdapter presenceListAdapter;
    public PresencePnCallback(PresenceListAdapter presenceListAdapter) {
        this.presenceListAdapter = presenceListAdapter;
    }
    @Override
    public void status(PubNub pubnub, PNStatus status) {
        // no status handling for simplicity
    }
    @Override
    public void message(PubNub pubnub, PNMessageResult message) {
        // no message handling for simplicity
    }
    @Override
    public void presence(PubNub pubnub, PNPresenceEventResult presence) {
        try {
            Log.v(TAG, "presenceP(" + JsonUtil.asJson(presence) + ")");
        } catch (Exception e) {
            e.printStackTrace();
        }
        String sender = presence.getUuid();
        String presenceString = presence.getEvent().toString();
        String timestamp = DateTimeUtil.getTimeStampUtc();
        PresencePojo pm = new PresencePojo(sender, presenceString, timestamp);
        presenceListAdapter.add(pm);
    }
}

In the code above, the status() method is where we handle events such as connection errors and reconnect events, publish or subscribe errors.

The message() method would be used if we also wanted this callback to handle message events on the channel. In this case, we omit them for code separation and instructional purposes.

In this case, the presence() method is where the magic happens – that’s where we put our logic for handling presence events. We get the username via the presence.getUuid() method, the event type using the presence.getEvent() method, and an ISO timestamp for display purposes. Once we have that Pojo object, we pass it along to the Adapter for processing and sending to the UI.

Here’s a recap of how to do the channel subscription: make sure to register the Presence callback as a listener on the PubNub object, and that the subscribe() call chain specifies the withPresence() option.

private final void initChannels() {
   ...
   this.mPubnub_DataStream.addListener(this.mPresencePnCallback);
   ...
   this.mPubnub_DataStream.subscribe().channels(PUBSUB_CHANNEL).withPresence().execute();
   ...

Let’s review that code a bit:

  • The first step is to register the callbacks with the PubNub object – you only need to do this once for each callback instance.
  • The next step is to subscribe to the desired channel, in this case, we want to make sure to specify withPresence()!
  • You only need to subscribe once (unless you subsequently call unsubscribe() on the channel and want to resubscribe).
  • That’s it! Now, as presence events come in from the Pub/Sub channel, the corresponding callback(s) will be invoked. Depending on your application, this might be all you need! Chances are though, you’ll need to propagate the data into your UI – that’s what we alluded to when we talked about the Adapter above. Let’s assume we’re using something like a ListView to display data, so we’ll want to check out how the PresenceListAdapter in the sample application works.

Understanding the Adapter

When would we need an adapter? Like we said before, the Adapter class is a way to bridge between our dynamic data collection and the Android UI. Here are the main aspects of the Adapter:

  • Part 1 (optional): a concrete Java collection containing the “real” values.
  • Part 2: mutation methods (in our case, just add()) to propagate new values from the PubNub callback using notifyDataSetChanged().
  • Part 3: a getView() method to instantiate each row of the View.

The reason why Part 1 is optional is that in some cases, the Java collection might be extremely large. It may not be possible nor desirable to keep all of those values in memory. Along those lines, it shouldn’t be hard to imagine a scenario where we omit the values List below, and use dynamic requests to a SQLite DB, file, or other data store in the getView() call accordingly.

public class PresenceListAdapter extends ArrayAdapter<PresencePojo> {
    private final Context context;
    private final LayoutInflater inflater;
    private final List presenceList = new ArrayList();
    private final Map<String, PresencePojo> latestPresence = new LinkedHashMap<String, PresencePojo>();
    public PresenceListAdapter(Context context) {
        super(context, R.layout.list_row_presence);
        this.context = context;
        this.inflater = LayoutInflater.from(context);
    }
    @Override
    public void add(PresencePojo message) {
        if (latestPresence.containsKey(message.getSender())) {
            this.presenceList.remove(message.getSender());
        }
        this.presenceList.add(0, message.getSender());
        latestPresence.put(message.getSender(), message);
        ((Activity) this.context).runOnUiThread(new Runnable() {
            @Override
            public void run() {
                notifyDataSetChanged();
            }
        });
    }
    @Override
    public View getView(final int position, View convertView, ViewGroup parent) {
        String sender = this.presenceList.get(position);
        PresencePojo presenceMsg = this.latestPresence.get(sender);
        PresenceMessageListRowUi msgView;
        if (convertView == null) {
            msgView = new PresenceMessageListRowUi();
            convertView = inflater.inflate(R.layout.list_row_presence, parent, false);
            msgView.sender = (TextView) convertView.findViewById(R.id.sender);
            msgView.presence = (TextView) convertView.findViewById(R.id.value);
            msgView.timestamp = (TextView) convertView.findViewById(R.id.timestamp);
            convertView.setTag(msgView);
        } else {
            msgView = (PresenceMessageListRowUi) convertView.getTag();
        }
        msgView.sender.setText(presenceMsg.getSender());
        msgView.presence.setText(presenceMsg.getPresence());
        msgView.timestamp.setText(presenceMsg.getTimestamp());
        return convertView;
    }
    ...
}

There are a couple things we should mention as features & enhancements for the Buddy list:

  • We should only list users once.
  • We should only show the latest presence status.
  • We should move the latest updated users to the top.

To implement this, we keep a List in the Adapter to represent the users in order, and keep a LinkedHashMap<String, PresencePojo> to make it easy to update the data value corresponding to a given user.

We should note a couple other things about the add() method above. First, we’re prepending new elements to the values list using List.add(index, value) where index is 0. In other cases, you may just want to use addition at the end of the list using List.add(value). Secondly, we’re using a RowUi object to hold all the UI elements that we need to update for a given row. That is what the getTag() and setTag() calls on the convertView instance are all about.

Before we forget, here’s the RowUi object for the Presence feature – it’s very small.

public class PresenceMessageListRowUi {
 public TextView sender;
 public TextView presence;
 public TextView timestamp;
 }

One last thing to note on this topic is that updates to the UI need to happen on the UI thread; that’s why we keep a reference to the context in the Adapter, and make sure to run the notifyDataSetChanged() call on the UI thread. If you don’t do that, you’ll see a ton of warnings in the logs and/or crash the app.

Advanced Presence Features

Displaying a list of users/devices is awesome, but how do we initialize it when we first start the app? The PubNub API makes this easy by exposing a hereNow() function that lets you get the full list of members in a channel. We call that method right after we subscribe to the channel in the initChannels() method in MainActivity. Here’s how it looks:

private final void initChannels() {
    ...
    this.mPubnub_DataStream.hereNow().channels(PUBSUB_CHANNEL).async(new PNCallback() {
        @Override
        public void onResponse(PNHereNowResult result, PNStatus status) {
            if (status.isError()) {
                return;
            }
            try {
                Log.v(TAG, JsonUtil.asJson(result));
                for (Map.Entry<String, PNHereNowChannelData> entry : result.getChannels().entrySet()) {
                    for (PNHereNowOccupantData occupant : entry.getValue().getOccupants()) {
                        MainActivity.this.mPresence.add(new PresencePojo(occupant.getUuid(), "join", DateTimeUtil.getTimeStampUtc()));
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    });
    ...
}

The hereNow() method accepts a list of channels to check, as well as a callback to be invoked when the results return. Right now, we return immediately if the status.isError(); in your case, you might want to display an error to the user or send it off to an exception logging service. If the call is successful, we iterate over the PNHereNowChannelData objects in the PNHereNowResult object to get the corresponding user UUIDs, create new Pojo objects for them with status “join”, and forward those on to the PresenceAdapter for subsequent display in the Presence Tab ListView. Not too shabby!

Also note that if the channel() parameter is not specified, the method will return a list of all subscribers on every channel. Remember, with great power comes great responsibility!

Channel Subscriptions with whereNow()

Besides getting the list of a given channel’s subscribers, there’s another similar use case for getting the list of what channels a user/device (actually, UUID) are subscribed to. Although this isn’t demonstrated in the full tutorial app, here’s the sample code for how to do it:

this.mPubnub_DataStream.whereNow().uuid("fred").async(new PNCallback() {
 @Override
 public void onResponse(PNWhereNowResult result, PNStatus status) {
 // return if error, otherwise process the list of channels
 }
 });

One aspect of this feature worth mentioning is that if the uuid() parameter is not specified, the method will return a list of everyone’s channels everywhere. That could be a lot of data!

Extra Data Attributes with state()

There are a ton of cases where data streams make sense. In the case of chat messages, it is intuitive to send messages over the channel for each chat message sent by a user. But what about other data, such as the typing… indicator or metadata such as location or extended presence information (such as Available vs. DoNotDisturb)? PubNub’s state() methods make it easy to attach attributes to users (actually UUIDs) and update them via the presence channel, that is, a side channel outside the “normal” channel itself.

Here’s how we set these state attributes (for our own UUID):

Map<String, String> newState = ImmutableList.of("hunger", "VERY_HUNGRY", "location", "SF, CA");
this.mPubnub_DataStream.setPresenceState().state(newState).async(new PNCallback() {
 @Override
 public void onResponse(PNSetStateResult result, PNStatus status) {
 // reject if error, otherwise process the response
 }
 });

When setState() calls occur, the new values are propagated using a state-change event type coming through the Presence event handler (just like join, leave or timeout events would be).

Alternatively, here’s how we get the state attributes (for any UUID) on-demand:

this.mPubnub_DataStream.getPresenceState().uuid("fred").async(new PNCallback() {
 @Override
 public void onResponse(PNGetStateResult result, PNStatus status) {
 // reject if error, otherwise process the response
 }
 });

The current state may also be returned as part of the hereNow() call by using the .includeState(true) as follows:

this.mPubnub_DataStream.hereNow().channels(PUBSUB_CHANNEL).includeState(true).async(new PNCallback() {
 @Override
 public void onResponse(PNHereNowResult result, PNStatus status) {
 // reject if error, otherwise process the result
 }
 });

Pretty nifty stuff!

Heartbeat Configuration & Timeouts

We alluded to this earlier – as devices encounter intermittent network connectivity, we may want to update their online status accordingly. To this end, PubNub offers two parameters to affect server-side detection of client outages: heartbeat and heartbeatInterval. The heartbeat timeout value reflects the amount of time that must pass without a heartbeat before a client is marked “timeout”. The heartbeatInterval value reflects the amount of time between client heartbeat pings to the server (by default, this is about half the heartbeat timeout value).

You can set these values in the PNConfiguration object as follows:

config.setPresenceTimeout(60); // client is marked disconnected after 60 seconds

The default heartbeat interval for 60 seconds will be ((60/2) – 1) seconds, or 29 seconds.

If you’d like to use a custom heartbeat interval, you can do this instead:

config.setPresenceTimeoutWithCustomInterval(60, 10); // client is marked disconnected after 60 seconds

In this case, the heartbeat timeout will be 60 seconds, and the client will send a heartbeat every 10s.

Next Steps

Let’s take a second to recap what we built:

  • A “buddy list” tab with dynamic list of users on a specified PubNub channel
  • The ability to detect leave and timeout events
  • Advanced state management for custom user/device attributes using PubNub state() methods.

In our final part of the series, we'll talk scaling, showing how to use Channel Groups and Multiplexing to build a more efficient and scalable Android messaging app.