Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 124 additions & 9 deletions lib/src/main/java/io/ably/lib/realtime/Presence.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
*
* <p>
* 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.
* <p>
* 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);
}

/**
Expand All @@ -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.
*
* <p>
* 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.
* <p>
* 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);
}

/**
Expand All @@ -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.
*
* <p>
* 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.
* <p>
* 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);
}

/**
Expand Down Expand Up @@ -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.
*
* <p>
* 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.
* <p>
* 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);
Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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.
*
* <p>
* 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.
* <p>
* 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);
Expand All @@ -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);
}

/**
Expand Down Expand Up @@ -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.
*
* <p>
* 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.
* <p>
* 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);
Expand All @@ -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);
}

/**
Expand Down Expand Up @@ -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() {
}
Expand Down
69 changes: 68 additions & 1 deletion lib/src/main/java/io/ably/lib/types/PresenceMessage.java
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
* <p>
* Spec: TP3i
*/
public MessageExtras extras;

private static final String EXTRAS = "extras";

/**
* Default constructor
*/
Expand All @@ -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;
}

/**
Expand All @@ -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 {
Expand All @@ -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();
Expand Down Expand Up @@ -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<Action> {
@Override
public Action deserialize(JsonElement json, Type t, JsonDeserializationContext ctx)
Expand All @@ -268,13 +316,32 @@ public Action deserialize(JsonElement json, Type t, JsonDeserializationContext c
}
}

public static class Serializer implements JsonSerializer<PresenceMessage> {
public static class Serializer implements JsonSerializer<PresenceMessage>, JsonDeserializer<PresenceMessage> {
@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;
}
}

/**
Expand Down
Loading
Loading