diff --git a/lib/src/main/java/io/ably/lib/realtime/Presence.java b/lib/src/main/java/io/ably/lib/realtime/Presence.java
index 9a7e89e7e..2c785b9e5 100644
--- a/lib/src/main/java/io/ably/lib/realtime/Presence.java
+++ b/lib/src/main/java/io/ably/lib/realtime/Presence.java
@@ -10,6 +10,7 @@
import io.ably.lib.types.Callback;
import io.ably.lib.types.ErrorInfo;
import io.ably.lib.types.MessageDecodeException;
+import io.ably.lib.types.MessageExtras;
import io.ably.lib.types.PaginatedResult;
import io.ably.lib.types.Param;
import io.ably.lib.types.PresenceMessage;
@@ -483,8 +484,27 @@ private void unsubscribeImpl(PresenceMessage.Action action, PresenceListener lis
* @throws AblyException
*/
public void enter(Object data, CompletionListener listener) throws AblyException {
+ enter(data, null, listener);
+ }
+
+ /**
+ * Enters the presence set for the channel, optionally passing a data payload and extras.
+ * A clientId is required to be present on a channel.
+ * An optional callback may be provided to notify of the success or failure of the operation.
+ *
+ *
+ * Spec: RTP8
+ *
+ * @param data The payload associated with the presence member.
+ * @param extras The extras associated with the presence member.
+ * @param listener A callback to notify of the success or failure of the operation.
+ *
+ * This listener is invoked on a background thread.
+ * @throws AblyException
+ */
+ public void enter(Object data, MessageExtras extras, CompletionListener listener) throws AblyException {
Log.v(TAG, "enter(); channel = " + channel.name);
- updatePresence(new PresenceMessage(PresenceMessage.Action.enter, null, data), listener);
+ updatePresence(new PresenceMessage(PresenceMessage.Action.enter, null, data, extras), listener);
}
/**
@@ -502,8 +522,27 @@ public void enter(Object data, CompletionListener listener) throws AblyException
* @throws AblyException
*/
public void update(Object data, CompletionListener listener) throws AblyException {
+ update(data, null, listener);
+ }
+
+ /**
+ * Updates the data payload for a presence member, optionally passing extras.
+ * If called before entering the presence set, this is treated as an {@link PresenceMessage.Action#enter} event.
+ * An optional callback may be provided to notify of the success or failure of the operation.
+ *
+ *
+ * Spec: RTP9
+ *
+ * @param data The payload associated with the presence member.
+ * @param extras The extras associated with the presence member.
+ * @param listener A callback to notify of the success or failure of the operation.
+ *
+ * This listener is invoked on a background thread.
+ * @throws AblyException
+ */
+ public void update(Object data, MessageExtras extras, CompletionListener listener) throws AblyException {
Log.v(TAG, "update(); channel = " + channel.name);
- updatePresence(new PresenceMessage(PresenceMessage.Action.update, null, data), listener);
+ updatePresence(new PresenceMessage(PresenceMessage.Action.update, null, data, extras), listener);
}
/**
@@ -520,8 +559,26 @@ public void update(Object data, CompletionListener listener) throws AblyExceptio
* @throws AblyException
*/
public void leave(Object data, CompletionListener listener) throws AblyException {
+ leave(data, null, listener);
+ }
+
+ /**
+ * Leaves the presence set for the channel, optionally passing extras.
+ * A client must have previously entered the presence set before they can leave it.
+ *
+ *
+ * Spec: RTP10
+ *
+ * @param data The payload associated with the presence member.
+ * @param extras The extras associated with the presence member.
+ * @param listener a listener to notify of the success or failure of the operation.
+ *
+ * This listener is invoked on a background thread.
+ * @throws AblyException
+ */
+ public void leave(Object data, MessageExtras extras, CompletionListener listener) throws AblyException {
Log.v(TAG, "leave(); channel = " + channel.name);
- updatePresence(new PresenceMessage(PresenceMessage.Action.leave, null, data), listener);
+ updatePresence(new PresenceMessage(PresenceMessage.Action.leave, null, data, extras), listener);
}
/**
@@ -584,6 +641,25 @@ public void enterClient(String clientId, Object data) throws AblyException {
* This listener is invoked on a background thread.
*/
public void enterClient(String clientId, Object data, CompletionListener listener) throws AblyException {
+ enterClient(clientId, data, null, listener);
+ }
+
+ /**
+ * Enters the presence set of the channel for a given clientId, optionally passing extras.
+ * Enables a single client to update presence on behalf of any number of clients using a single connection.
+ * The library must have been instantiated with an API key or a token bound to a wildcard clientId.
+ *
+ *
+ * Spec: RTP4, RTP14, RTP15
+ *
+ * @param clientId The ID of the client to enter into the presence set.
+ * @param data The payload associated with the presence member.
+ * @param extras The extras associated with the presence member.
+ * @param listener A callback to notify of the success or failure of the operation.
+ *
+ * This listener is invoked on a background thread.
+ */
+ public void enterClient(String clientId, Object data, MessageExtras extras, CompletionListener listener) throws AblyException {
if(clientId == null) {
String errorMessage = String.format(Locale.ROOT, "Channel %s: unable to enter presence channel (null clientId specified)", channel.name);
Log.v(TAG, errorMessage);
@@ -593,10 +669,10 @@ public void enterClient(String clientId, Object data, CompletionListener listene
}
}
Log.v(TAG, "enterClient(); channel = " + channel.name + "; clientId = " + clientId);
- updatePresence(new PresenceMessage(PresenceMessage.Action.enter, clientId, data), listener);
+ updatePresence(new PresenceMessage(PresenceMessage.Action.enter, clientId, data, extras), listener);
}
- private void enterClientWithId(String id, String clientId, Object data, CompletionListener listener) throws AblyException {
+ private void enterClientWithId(String id, String clientId, Object data, MessageExtras extras, CompletionListener listener) throws AblyException {
if(clientId == null) {
String errorMessage = String.format(Locale.ROOT, "Channel %s: unable to enter presence channel (null clientId specified)", channel.name);
Log.v(TAG, errorMessage);
@@ -605,7 +681,7 @@ private void enterClientWithId(String id, String clientId, Object data, Completi
return;
}
}
- PresenceMessage presenceMsg = new PresenceMessage(PresenceMessage.Action.enter, clientId, data);
+ PresenceMessage presenceMsg = new PresenceMessage(PresenceMessage.Action.enter, clientId, data, extras);
presenceMsg.id = id;
Log.v(TAG, "enterClient(); channel = " + channel.name + "; clientId = " + clientId);
updatePresence(presenceMsg, listener);
@@ -658,6 +734,26 @@ public void updateClient(String clientId, Object data) throws AblyException {
* This listener is invoked on a background thread.
*/
public void updateClient(String clientId, Object data, CompletionListener listener) throws AblyException {
+ updateClient(clientId, data, null, listener);
+ }
+
+ /**
+ * Updates the data payload for a presence member using a given clientId, optionally passing extras.
+ * Enables a single client to update presence on behalf of any number of clients using a single connection.
+ * The library must have been instantiated with an API key or a token bound to a wildcard clientId.
+ * An optional callback may be provided to notify of the success or failure of the operation.
+ *
+ *
+ * Spec: RTP15
+ *
+ * @param clientId The ID of the client to update in the presence set.
+ * @param data The payload to update for the presence member.
+ * @param extras The extras associated with the presence member.
+ * @param listener A callback to notify of the success or failure of the operation.
+ *
+ * This listener is invoked on a background thread.
+ */
+ public void updateClient(String clientId, Object data, MessageExtras extras, CompletionListener listener) throws AblyException {
if(clientId == null) {
String errorMessage = String.format(Locale.ROOT, "Channel %s: unable to update presence channel (null clientId specified)", channel.name);
Log.v(TAG, errorMessage);
@@ -667,7 +763,7 @@ public void updateClient(String clientId, Object data, CompletionListener listen
}
}
Log.v(TAG, "updateClient(); channel = " + channel.name + "; clientId = " + clientId);
- updatePresence(new PresenceMessage(PresenceMessage.Action.update, clientId, data), listener);
+ updatePresence(new PresenceMessage(PresenceMessage.Action.update, clientId, data, extras), listener);
}
/**
@@ -714,6 +810,25 @@ public void leaveClient(String clientId, Object data) throws AblyException {
* This listener is invoked on a background thread.
*/
public void leaveClient(String clientId, Object data, CompletionListener listener) throws AblyException {
+ leaveClient(clientId, data, null, listener);
+ }
+
+ /**
+ * Leaves the presence set of the channel for a given clientId, optionally passing extras.
+ * Enables a single client to update presence on behalf of any number of clients using a single connection.
+ * The library must have been instantiated with an API key or a token bound to a wildcard clientId.
+ *
+ *
+ * Spec: RTP15
+ *
+ * @param clientId The ID of the client to leave the presence set for.
+ * @param data The payload associated with the presence member.
+ * @param extras The extras associated with the presence member.
+ * @param listener A callback to notify of the success or failure of the operation.
+ *
+ * This listener is invoked on a background thread.
+ */
+ public void leaveClient(String clientId, Object data, MessageExtras extras, CompletionListener listener) throws AblyException {
if(clientId == null) {
String errorMessage = String.format(Locale.ROOT, "Channel %s: unable to leave presence channel (null clientId specified)", channel.name);
Log.v(TAG, errorMessage);
@@ -723,7 +838,7 @@ public void leaveClient(String clientId, Object data, CompletionListener listene
}
}
Log.v(TAG, "leaveClient(); channel = " + channel.name + "; clientId = " + clientId);
- updatePresence(new PresenceMessage(PresenceMessage.Action.leave, clientId, data), listener);
+ updatePresence(new PresenceMessage(PresenceMessage.Action.leave, clientId, data, extras), listener);
}
/**
@@ -934,7 +1049,7 @@ void onAttached(boolean hasPresence) {
void enterInternalMembers() {
for (final PresenceMessage item: internalPresence.members.values()) {
try {
- enterClientWithId(item.id, item.clientId, item.data, new CompletionListener() {
+ enterClientWithId(item.id, item.clientId, item.data, item.extras, new CompletionListener() {
@Override
public void onSuccess() {
}
diff --git a/lib/src/main/java/io/ably/lib/types/PresenceMessage.java b/lib/src/main/java/io/ably/lib/types/PresenceMessage.java
index d3073a879..2ba220ba0 100644
--- a/lib/src/main/java/io/ably/lib/types/PresenceMessage.java
+++ b/lib/src/main/java/io/ably/lib/types/PresenceMessage.java
@@ -75,6 +75,16 @@ public enum Action {
*/
public Action action;
+ /**
+ * A MessageExtras object of arbitrary key-value pairs that may contain metadata, and/or ancillary payloads.
+ * Valid payloads include {@link DeltaExtras}, {@link JsonObject}.
+ *
+ * Spec: TP3i
+ */
+ public MessageExtras extras;
+
+ private static final String EXTRAS = "extras";
+
/**
* Default constructor
*/
@@ -96,9 +106,21 @@ public PresenceMessage(Action action, String clientId) {
* @param data
*/
public PresenceMessage(Action action, String clientId, Object data) {
+ this(action, clientId, data, null);
+ }
+
+ /**
+ * Construct a PresenceMessage with extras
+ * @param action
+ * @param clientId
+ * @param data
+ * @param extras
+ */
+ public PresenceMessage(Action action, String clientId, Object data, MessageExtras extras) {
this.action = action;
this.clientId = clientId;
this.data = data;
+ this.extras = extras;
}
/**
@@ -123,16 +145,22 @@ public Object clone() {
result.encoding = encoding;
result.data = data;
result.action = action;
+ result.extras = extras;
return result;
}
void writeMsgpack(MessagePacker packer) throws IOException {
int fieldCount = super.countFields();
++fieldCount;
+ if(extras != null) ++fieldCount;
packer.packMapHeader(fieldCount);
super.writeFields(packer);
packer.packString("action");
packer.packInt(action.getValue());
+ if(extras != null) {
+ packer.packString(EXTRAS);
+ extras.write(packer);
+ }
}
PresenceMessage readMsgpack(MessageUnpacker unpacker) throws IOException {
@@ -145,6 +173,8 @@ PresenceMessage readMsgpack(MessageUnpacker unpacker) throws IOException {
if(super.readField(unpacker, fieldName, fieldFormat)) { continue; }
if(fieldName.equals("action")) {
action = Action.findByValue(unpacker.unpackInt());
+ } else if (fieldName.equals(EXTRAS)) {
+ extras = MessageExtras.read(unpacker);
} else {
Log.v(TAG, "Unexpected field: " + fieldName);
unpacker.skipValue();
@@ -260,6 +290,24 @@ public static PresenceMessage[] fromEncodedArray(String presenceMsgArray, Channe
}
}
+ @Override
+ protected void read(final JsonObject map) throws MessageDecodeException {
+ super.read(map);
+
+ final JsonElement extrasElement = map.get(EXTRAS);
+ if (extrasElement != null && !extrasElement.isJsonNull()) {
+ if (!extrasElement.isJsonObject()) {
+ throw MessageDecodeException.fromDescription("PresenceMessage extras is of type \"" + extrasElement.getClass() + "\" when expected a JSON object.");
+ }
+ extras = MessageExtras.read(extrasElement.getAsJsonObject());
+ }
+
+ Integer actionValue = readInt(map, "action");
+ if (actionValue != null) {
+ action = Action.findByValue(actionValue);
+ }
+ }
+
public static class ActionSerializer implements JsonDeserializer {
@Override
public Action deserialize(JsonElement json, Type t, JsonDeserializationContext ctx)
@@ -268,13 +316,32 @@ public Action deserialize(JsonElement json, Type t, JsonDeserializationContext c
}
}
- public static class Serializer implements JsonSerializer {
+ public static class Serializer implements JsonSerializer, JsonDeserializer {
@Override
public JsonElement serialize(PresenceMessage message, Type typeOfMessage, JsonSerializationContext ctx) {
final JsonObject json = BaseMessage.toJsonObject(message);
if(message.action != null) json.addProperty("action", message.action.getValue());
+ if(message.extras != null) {
+ json.add(EXTRAS, Serialisation.gson.toJsonTree(message.extras));
+ }
return json;
}
+
+ @Override
+ public PresenceMessage deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
+ if (!(json instanceof JsonObject)) {
+ throw new JsonParseException("Expected an object but got \"" + json.getClass() + "\".");
+ }
+
+ final PresenceMessage message = new PresenceMessage();
+ try {
+ message.read((JsonObject) json);
+ } catch (MessageDecodeException e) {
+ Log.e(TAG, e.getMessage(), e);
+ throw new JsonParseException("Failed to deserialize PresenceMessage from JSON.", e);
+ }
+ return message;
+ }
}
/**
diff --git a/lib/src/test/java/io/ably/lib/test/realtime/RealtimePresenceTest.java b/lib/src/test/java/io/ably/lib/test/realtime/RealtimePresenceTest.java
index 2534139bd..3d0c8dc81 100644
--- a/lib/src/test/java/io/ably/lib/test/realtime/RealtimePresenceTest.java
+++ b/lib/src/test/java/io/ably/lib/test/realtime/RealtimePresenceTest.java
@@ -3625,6 +3625,354 @@ public void messages_from_encoded_json_array() throws AblyException {
}
}
+ /**
+ * Enter presence with extras field and verify it comes back on the other side
+ * Test TP3i
+ */
+ @Test
+ public void presence_enter_with_extras() {
+ AblyRealtime clientAbly1 = null;
+ TestChannel testChannel = new TestChannel();
+ try {
+ /* subscribe for presence events in the anonymous connection */
+ PresenceWaiter presenceWaiter = new PresenceWaiter(testChannel.realtimeChannel);
+ /* set up a connection with specific clientId */
+ ClientOptions client1Opts = new ClientOptions() {{
+ tokenDetails = token1;
+ clientId = testClientId1;
+ }};
+ fillInOptions(client1Opts);
+ clientAbly1 = new AblyRealtime(client1Opts);
+
+ /* wait until connected */
+ (new ConnectionWaiter(clientAbly1.connection)).waitFor(ConnectionState.connected);
+ assertEquals("Verify connected state reached", clientAbly1.connection.state, ConnectionState.connected);
+
+ /* get channel and attach */
+ Channel client1Channel = clientAbly1.channels.get(testChannel.channelName);
+ client1Channel.attach();
+ (new ChannelWaiter(client1Channel)).waitFor(ChannelState.attached);
+ assertEquals("Verify attached state reached", client1Channel.state, ChannelState.attached);
+
+ /* create extras with headers.foo */
+ JsonObject extrasJson = new JsonObject();
+ JsonObject headers = new JsonObject();
+ headers.addProperty("foo", "bar");
+ extrasJson.add("headers", headers);
+ io.ably.lib.types.MessageExtras extras = new io.ably.lib.types.MessageExtras(extrasJson);
+
+ /* create presence message with extras */
+ String enterString = "Test data (presence_enter_with_extras)";
+ PresenceMessage presenceMsg = new PresenceMessage(PresenceMessage.Action.enter, null, enterString);
+ presenceMsg.extras = extras;
+
+ /* let client1 enter the channel with extras and wait for the entered event to be delivered */
+ CompletionWaiter enterComplete = new CompletionWaiter();
+ client1Channel.presence.updatePresence(presenceMsg, enterComplete);
+ presenceWaiter.waitFor(testClientId1, Action.enter);
+ PresenceMessage receivedMessage = presenceWaiter.contains(testClientId1, Action.enter);
+ assertNotNull("Verify presence message received", receivedMessage);
+ assertEquals("Verify data matches", enterString, receivedMessage.data);
+
+ /* verify extras field is present and correct */
+ assertNotNull("Verify extras is not null", receivedMessage.extras);
+ JsonObject receivedExtrasJson = receivedMessage.extras.asJsonObject();
+ assertNotNull("Verify extras JSON is not null", receivedExtrasJson);
+ assertTrue("Verify headers exists in extras", receivedExtrasJson.has("headers"));
+ JsonObject receivedHeaders = receivedExtrasJson.getAsJsonObject("headers");
+ assertNotNull("Verify headers object is not null", receivedHeaders);
+ assertTrue("Verify foo exists in headers", receivedHeaders.has("foo"));
+ assertEquals("Verify foo value matches", "bar", receivedHeaders.get("foo").getAsString());
+
+ /* verify enter callback called on completion */
+ enterComplete.waitFor();
+ assertTrue("Verify enter callback called on completion", enterComplete.success);
+ } catch(AblyException e) {
+ e.printStackTrace();
+ fail("Unexpected exception running test: " + e.getMessage());
+ } finally {
+ if(clientAbly1 != null)
+ clientAbly1.close();
+ if(testChannel != null)
+ testChannel.dispose();
+ }
+ }
+
+ /**
+ * Enter presence using the convenience enter(data, extras, listener) method
+ * and verify extras come back on the subscriber side.
+ */
+ @Test
+ public void presence_enter_with_extras_convenience() {
+ AblyRealtime clientAbly1 = null;
+ TestChannel testChannel = new TestChannel();
+ try {
+ /* subscribe for presence events in the anonymous connection */
+ PresenceWaiter presenceWaiter = new PresenceWaiter(testChannel.realtimeChannel);
+ /* set up a connection with specific clientId */
+ ClientOptions client1Opts = new ClientOptions() {{
+ tokenDetails = token1;
+ clientId = testClientId1;
+ }};
+ fillInOptions(client1Opts);
+ clientAbly1 = new AblyRealtime(client1Opts);
+
+ /* wait until connected */
+ (new ConnectionWaiter(clientAbly1.connection)).waitFor(ConnectionState.connected);
+ assertEquals("Verify connected state reached", clientAbly1.connection.state, ConnectionState.connected);
+
+ /* get channel and attach */
+ Channel client1Channel = clientAbly1.channels.get(testChannel.channelName);
+ client1Channel.attach();
+ (new ChannelWaiter(client1Channel)).waitFor(ChannelState.attached);
+ assertEquals("Verify attached state reached", client1Channel.state, ChannelState.attached);
+
+ /* create extras with headers */
+ JsonObject extrasJson = new JsonObject();
+ JsonObject headers = new JsonObject();
+ headers.addProperty("foo", "bar");
+ extrasJson.add("headers", headers);
+ io.ably.lib.types.MessageExtras extras = new io.ably.lib.types.MessageExtras(extrasJson);
+
+ /* enter using the convenience method with extras */
+ String enterString = "Test data (presence_enter_with_extras_convenience)";
+ CompletionWaiter enterComplete = new CompletionWaiter();
+ client1Channel.presence.enter(enterString, extras, enterComplete);
+ presenceWaiter.waitFor(testClientId1, Action.enter);
+ PresenceMessage receivedMessage = presenceWaiter.contains(testClientId1, Action.enter);
+ assertNotNull("Verify presence message received", receivedMessage);
+ assertEquals("Verify data matches", enterString, receivedMessage.data);
+
+ /* verify extras field is present and correct */
+ assertNotNull("Verify extras is not null", receivedMessage.extras);
+ JsonObject receivedExtrasJson = receivedMessage.extras.asJsonObject();
+ assertTrue("Verify headers exists in extras", receivedExtrasJson.has("headers"));
+ JsonObject receivedHeaders = receivedExtrasJson.getAsJsonObject("headers");
+ assertEquals("Verify foo value matches", "bar", receivedHeaders.get("foo").getAsString());
+
+ /* verify enter callback called on completion */
+ enterComplete.waitFor();
+ assertTrue("Verify enter callback called on completion", enterComplete.success);
+ } catch(AblyException e) {
+ e.printStackTrace();
+ fail("Unexpected exception running test: " + e.getMessage());
+ } finally {
+ if(clientAbly1 != null)
+ clientAbly1.close();
+ if(testChannel != null)
+ testChannel.dispose();
+ }
+ }
+
+ /**
+ * Update presence using the convenience update(data, extras, listener) method
+ * and verify extras come back on the subscriber side.
+ */
+ @Test
+ public void presence_update_with_extras() {
+ AblyRealtime clientAbly1 = null;
+ TestChannel testChannel = new TestChannel();
+ try {
+ /* subscribe for presence events in the anonymous connection */
+ PresenceWaiter presenceWaiter = new PresenceWaiter(testChannel.realtimeChannel);
+ /* set up a connection with specific clientId */
+ ClientOptions client1Opts = new ClientOptions() {{
+ tokenDetails = token1;
+ clientId = testClientId1;
+ }};
+ fillInOptions(client1Opts);
+ clientAbly1 = new AblyRealtime(client1Opts);
+
+ /* wait until connected */
+ (new ConnectionWaiter(clientAbly1.connection)).waitFor(ConnectionState.connected);
+ assertEquals("Verify connected state reached", clientAbly1.connection.state, ConnectionState.connected);
+
+ /* get channel and attach */
+ Channel client1Channel = clientAbly1.channels.get(testChannel.channelName);
+ client1Channel.attach();
+ (new ChannelWaiter(client1Channel)).waitFor(ChannelState.attached);
+ assertEquals("Verify attached state reached", client1Channel.state, ChannelState.attached);
+
+ /* enter first (no extras) */
+ CompletionWaiter enterComplete = new CompletionWaiter();
+ client1Channel.presence.enter("initial data", enterComplete);
+ enterComplete.waitFor();
+ presenceWaiter.waitFor(testClientId1, Action.enter);
+
+ /* create extras with headers */
+ JsonObject extrasJson = new JsonObject();
+ JsonObject headers = new JsonObject();
+ headers.addProperty("key", "value");
+ extrasJson.add("headers", headers);
+ io.ably.lib.types.MessageExtras extras = new io.ably.lib.types.MessageExtras(extrasJson);
+
+ /* update using the convenience method with extras */
+ String updateString = "Test data (presence_update_with_extras)";
+ CompletionWaiter updateComplete = new CompletionWaiter();
+ client1Channel.presence.update(updateString, extras, updateComplete);
+ presenceWaiter.waitFor(testClientId1, Action.update);
+ PresenceMessage receivedMessage = presenceWaiter.contains(testClientId1, Action.update);
+ assertNotNull("Verify presence message received", receivedMessage);
+ assertEquals("Verify data matches", updateString, receivedMessage.data);
+
+ /* verify extras field is present and correct */
+ assertNotNull("Verify extras is not null", receivedMessage.extras);
+ JsonObject receivedExtrasJson = receivedMessage.extras.asJsonObject();
+ assertTrue("Verify headers exists in extras", receivedExtrasJson.has("headers"));
+ JsonObject receivedHeaders = receivedExtrasJson.getAsJsonObject("headers");
+ assertEquals("Verify key value matches", "value", receivedHeaders.get("key").getAsString());
+
+ /* verify update callback called on completion */
+ updateComplete.waitFor();
+ assertTrue("Verify update callback called on completion", updateComplete.success);
+ } catch(AblyException e) {
+ e.printStackTrace();
+ fail("Unexpected exception running test: " + e.getMessage());
+ } finally {
+ if(clientAbly1 != null)
+ clientAbly1.close();
+ if(testChannel != null)
+ testChannel.dispose();
+ }
+ }
+
+ /**
+ * Leave presence using the convenience leave(data, extras, listener) method
+ * and verify extras come back on the subscriber side.
+ */
+ @Test
+ public void presence_leave_with_extras() {
+ AblyRealtime clientAbly1 = null;
+ TestChannel testChannel = new TestChannel();
+ try {
+ /* subscribe for presence events in the anonymous connection */
+ PresenceWaiter presenceWaiter = new PresenceWaiter(testChannel.realtimeChannel);
+ /* set up a connection with specific clientId */
+ ClientOptions client1Opts = new ClientOptions() {{
+ tokenDetails = token1;
+ clientId = testClientId1;
+ }};
+ fillInOptions(client1Opts);
+ clientAbly1 = new AblyRealtime(client1Opts);
+
+ /* wait until connected */
+ (new ConnectionWaiter(clientAbly1.connection)).waitFor(ConnectionState.connected);
+ assertEquals("Verify connected state reached", clientAbly1.connection.state, ConnectionState.connected);
+
+ /* get channel and attach */
+ Channel client1Channel = clientAbly1.channels.get(testChannel.channelName);
+ client1Channel.attach();
+ (new ChannelWaiter(client1Channel)).waitFor(ChannelState.attached);
+ assertEquals("Verify attached state reached", client1Channel.state, ChannelState.attached);
+
+ /* enter first (no extras) */
+ CompletionWaiter enterComplete = new CompletionWaiter();
+ client1Channel.presence.enter("initial data", enterComplete);
+ enterComplete.waitFor();
+ presenceWaiter.waitFor(testClientId1, Action.enter);
+
+ /* create extras with headers */
+ JsonObject extrasJson = new JsonObject();
+ JsonObject headers = new JsonObject();
+ headers.addProperty("reason", "goodbye");
+ extrasJson.add("headers", headers);
+ io.ably.lib.types.MessageExtras extras = new io.ably.lib.types.MessageExtras(extrasJson);
+
+ /* leave using the convenience method with extras */
+ String leaveString = "Test data (presence_leave_with_extras)";
+ CompletionWaiter leaveComplete = new CompletionWaiter();
+ client1Channel.presence.leave(leaveString, extras, leaveComplete);
+ presenceWaiter.waitFor(testClientId1, Action.leave);
+ PresenceMessage receivedMessage = presenceWaiter.contains(testClientId1, Action.leave);
+ assertNotNull("Verify presence message received", receivedMessage);
+ assertEquals("Verify data matches", leaveString, receivedMessage.data);
+
+ /* verify extras field is present and correct */
+ assertNotNull("Verify extras is not null", receivedMessage.extras);
+ JsonObject receivedExtrasJson = receivedMessage.extras.asJsonObject();
+ assertTrue("Verify headers exists in extras", receivedExtrasJson.has("headers"));
+ JsonObject receivedHeaders = receivedExtrasJson.getAsJsonObject("headers");
+ assertEquals("Verify reason value matches", "goodbye", receivedHeaders.get("reason").getAsString());
+
+ /* verify leave callback called on completion */
+ leaveComplete.waitFor();
+ assertTrue("Verify leave callback called on completion", leaveComplete.success);
+ } catch(AblyException e) {
+ e.printStackTrace();
+ fail("Unexpected exception running test: " + e.getMessage());
+ } finally {
+ if(clientAbly1 != null)
+ clientAbly1.close();
+ if(testChannel != null)
+ testChannel.dispose();
+ }
+ }
+
+ /**
+ * Enter presence for a specific clientId using enterClient(clientId, data, extras, listener)
+ * and verify extras come back on the subscriber side.
+ */
+ @Test
+ public void presence_enterClient_with_extras() {
+ AblyRealtime clientAbly1 = null;
+ TestChannel testChannel = new TestChannel();
+ try {
+ /* subscribe for presence events in the anonymous connection */
+ PresenceWaiter presenceWaiter = new PresenceWaiter(testChannel.realtimeChannel);
+ /* set up a connection with wildcard clientId capability */
+ ClientOptions client1Opts = new ClientOptions() {{
+ tokenDetails = wildcardToken;
+ }};
+ fillInOptions(client1Opts);
+ clientAbly1 = new AblyRealtime(client1Opts);
+
+ /* wait until connected */
+ (new ConnectionWaiter(clientAbly1.connection)).waitFor(ConnectionState.connected);
+ assertEquals("Verify connected state reached", clientAbly1.connection.state, ConnectionState.connected);
+
+ /* get channel and attach */
+ Channel client1Channel = clientAbly1.channels.get(testChannel.channelName);
+ client1Channel.attach();
+ (new ChannelWaiter(client1Channel)).waitFor(ChannelState.attached);
+ assertEquals("Verify attached state reached", client1Channel.state, ChannelState.attached);
+
+ /* create extras with headers */
+ JsonObject extrasJson = new JsonObject();
+ JsonObject headers = new JsonObject();
+ headers.addProperty("role", "admin");
+ extrasJson.add("headers", headers);
+ io.ably.lib.types.MessageExtras extras = new io.ably.lib.types.MessageExtras(extrasJson);
+
+ /* enter using enterClient with extras */
+ String enterString = "Test data (presence_enterClient_with_extras)";
+ CompletionWaiter enterComplete = new CompletionWaiter();
+ client1Channel.presence.enterClient(testClientId1, enterString, extras, enterComplete);
+ presenceWaiter.waitFor(testClientId1, Action.enter);
+ PresenceMessage receivedMessage = presenceWaiter.contains(testClientId1, Action.enter);
+ assertNotNull("Verify presence message received", receivedMessage);
+ assertEquals("Verify data matches", enterString, receivedMessage.data);
+
+ /* verify extras field is present and correct */
+ assertNotNull("Verify extras is not null", receivedMessage.extras);
+ JsonObject receivedExtrasJson = receivedMessage.extras.asJsonObject();
+ assertTrue("Verify headers exists in extras", receivedExtrasJson.has("headers"));
+ JsonObject receivedHeaders = receivedExtrasJson.getAsJsonObject("headers");
+ assertEquals("Verify role value matches", "admin", receivedHeaders.get("role").getAsString());
+
+ /* verify enter callback called on completion */
+ enterComplete.waitFor();
+ assertTrue("Verify enter callback called on completion", enterComplete.success);
+ } catch(AblyException e) {
+ e.printStackTrace();
+ fail("Unexpected exception running test: " + e.getMessage());
+ } finally {
+ if(clientAbly1 != null)
+ clientAbly1.close();
+ if(testChannel != null)
+ testChannel.dispose();
+ }
+ }
+
static class MessagesData {
public PresenceMessage[] messages;
}
diff --git a/pubsub-adapter/src/main/kotlin/com/ably/pubsub/RealtimePresence.kt b/pubsub-adapter/src/main/kotlin/com/ably/pubsub/RealtimePresence.kt
index cbd00805e..8f5873d16 100644
--- a/pubsub-adapter/src/main/kotlin/com/ably/pubsub/RealtimePresence.kt
+++ b/pubsub-adapter/src/main/kotlin/com/ably/pubsub/RealtimePresence.kt
@@ -4,6 +4,7 @@ import com.ably.Subscription
import io.ably.lib.realtime.ChannelState
import io.ably.lib.realtime.CompletionListener
import io.ably.lib.realtime.Presence.PresenceListener
+import io.ably.lib.types.MessageExtras
import io.ably.lib.types.PresenceMessage
import java.util.*
@@ -71,45 +72,48 @@ public interface RealtimePresence : Presence {
public fun subscribe(actions: EnumSet, listener: PresenceListener): Subscription
/**
- * Enters the presence set for the channel, optionally passing a data payload.
+ * Enters the presence set for the channel, optionally passing a data payload and extras.
* A clientId is required to be present on a channel.
* An optional callback may be provided to notify of the success or failure of the operation.
*
* Spec: RTP8
*
* @param data The payload associated with the presence member.
+ * @param extras The extras associated with the presence member.
* @param listener A callback to notify of the success or failure of the operation.
* This listener is invoked on a background thread.
*/
- public fun enter(data: Any? = null, listener: CompletionListener? = null)
+ public fun enter(data: Any? = null, extras: MessageExtras? = null, listener: CompletionListener? = null)
/**
- * Updates the data payload for a presence member.
+ * Updates the data payload for a presence member, optionally passing extras.
* If called before entering the presence set, this is treated as an [PresenceMessage.Action.enter] event.
* An optional callback may be provided to notify of the success or failure of the operation.
*
* Spec: RTP9
*
* @param data The payload associated with the presence member.
+ * @param extras The extras associated with the presence member.
* @param listener A callback to notify of the success or failure of the operation.
* This listener is invoked on a background thread.
*/
- public fun update(data: Any? = null, listener: CompletionListener? = null)
+ public fun update(data: Any? = null, extras: MessageExtras? = null, listener: CompletionListener? = null)
/**
- * Leaves the presence set for the channel.
+ * Leaves the presence set for the channel, optionally passing extras.
* A client must have previously entered the presence set before they can leave it.
*
* Spec: RTP10
*
* @param data The payload associated with the presence member.
+ * @param extras The extras associated with the presence member.
* @param listener a listener to notify of the success or failure of the operation.
* This listener is invoked on a background thread.
*/
- public fun leave(data: Any? = null, listener: CompletionListener? = null)
+ public fun leave(data: Any? = null, extras: MessageExtras? = null, listener: CompletionListener? = null)
/**
- * Enters the presence set of the channel for a given clientId.
+ * Enters the presence set of the channel for a given clientId, optionally passing extras.
* Enables a single client to update presence on behalf of any number of clients using a single connection.
* The library must have been instantiated with an API key or a token bound to a wildcard clientId.
*
@@ -117,13 +121,14 @@ public interface RealtimePresence : Presence {
*
* @param clientId The ID of the client to enter into the presence set.
* @param data The payload associated with the presence member.
+ * @param extras The extras associated with the presence member.
* @param listener A callback to notify of the success or failure of the operation.
* This listener is invoked on a background thread.
*/
- public fun enterClient(clientId: String, data: Any? = null, listener: CompletionListener? = null)
+ public fun enterClient(clientId: String, data: Any? = null, extras: MessageExtras? = null, listener: CompletionListener? = null)
/**
- * Updates the data payload for a presence member using a given clientId.
+ * Updates the data payload for a presence member using a given clientId, optionally passing extras.
* Enables a single client to update presence on behalf of any number of clients using a single connection.
* The library must have been instantiated with an API key or a token bound to a wildcard clientId.
* An optional callback may be provided to notify of the success or failure of the operation.
@@ -132,13 +137,14 @@ public interface RealtimePresence : Presence {
*
* @param clientId The ID of the client to update in the presence set.
* @param data The payload to update for the presence member.
+ * @param extras The extras associated with the presence member.
* @param listener A callback to notify of the success or failure of the operation.
* This listener is invoked on a background thread.
*/
- public fun updateClient(clientId: String, data: Any? = null, listener: CompletionListener? = null)
+ public fun updateClient(clientId: String, data: Any? = null, extras: MessageExtras? = null, listener: CompletionListener? = null)
/**
- * Leaves the presence set of the channel for a given clientId.
+ * Leaves the presence set of the channel for a given clientId, optionally passing extras.
* Enables a single client to update presence on behalf of any number of clients using a single connection.
* The library must have been instantiated with an API key or a token bound to a wildcard clientId.
*
@@ -146,8 +152,9 @@ public interface RealtimePresence : Presence {
*
* @param clientId The ID of the client to leave the presence set for.
* @param data The payload associated with the presence member.
+ * @param extras The extras associated with the presence member.
* @param listener A callback to notify of the success or failure of the operation.
* This listener is invoked on a background thread.
*/
- public fun leaveClient(clientId: String?, data: Any? = null, listener: CompletionListener? = null)
+ public fun leaveClient(clientId: String?, data: Any? = null, extras: MessageExtras? = null, listener: CompletionListener? = null)
}
diff --git a/pubsub-adapter/src/main/kotlin/io/ably/lib/realtime/RealtimePresenceAdapter.kt b/pubsub-adapter/src/main/kotlin/io/ably/lib/realtime/RealtimePresenceAdapter.kt
index 441c3dace..bf0e8ab5f 100644
--- a/pubsub-adapter/src/main/kotlin/io/ably/lib/realtime/RealtimePresenceAdapter.kt
+++ b/pubsub-adapter/src/main/kotlin/io/ably/lib/realtime/RealtimePresenceAdapter.kt
@@ -44,20 +44,23 @@ internal class RealtimePresenceAdapter(private val javaPresence: Presence) : Rea
}
}
- override fun enter(data: Any?, listener: CompletionListener?) = javaPresence.enter(data, listener)
+ override fun enter(data: Any?, extras: MessageExtras?, listener: CompletionListener?) =
+ javaPresence.enter(data, extras, listener)
- override fun update(data: Any?, listener: CompletionListener?) = javaPresence.update(data, listener)
+ override fun update(data: Any?, extras: MessageExtras?, listener: CompletionListener?) =
+ javaPresence.update(data, extras, listener)
- override fun leave(data: Any?, listener: CompletionListener?) = javaPresence.leave(data, listener)
+ override fun leave(data: Any?, extras: MessageExtras?, listener: CompletionListener?) =
+ javaPresence.leave(data, extras, listener)
- override fun enterClient(clientId: String, data: Any?, listener: CompletionListener?) =
- javaPresence.enterClient(clientId, data, listener)
+ override fun enterClient(clientId: String, data: Any?, extras: MessageExtras?, listener: CompletionListener?) =
+ javaPresence.enterClient(clientId, data, extras, listener)
- override fun updateClient(clientId: String, data: Any?, listener: CompletionListener?) =
- javaPresence.updateClient(clientId, data, listener)
+ override fun updateClient(clientId: String, data: Any?, extras: MessageExtras?, listener: CompletionListener?) =
+ javaPresence.updateClient(clientId, data, extras, listener)
- override fun leaveClient(clientId: String?, data: Any?, listener: CompletionListener?) =
- javaPresence.leaveClient(clientId, data, listener)
+ override fun leaveClient(clientId: String?, data: Any?, extras: MessageExtras?, listener: CompletionListener?) =
+ javaPresence.leaveClient(clientId, data, extras, listener)
override fun history(start: Long?, end: Long?, limit: Int, orderBy: OrderBy): PaginatedResult =
javaPresence.history(buildHistoryParams(start, end, limit, orderBy).toTypedArray())