From 27b2b064e4aac1fb4f2344759f200f80ac5911f5 Mon Sep 17 00:00:00 2001
From: sacOO7
Date: Tue, 29 Sep 2020 23:54:28 +0530
Subject: [PATCH 001/899] Fixed wait for channel attach state and members
presence sync
---
lib/src/main/java/io/ably/lib/realtime/Presence.java | 12 ++++++++----
1 file changed, 8 insertions(+), 4 deletions(-)
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 be6d07625..f5a93d7bd 100644
--- a/lib/src/main/java/io/ably/lib/realtime/Presence.java
+++ b/lib/src/main/java/io/ably/lib/realtime/Presence.java
@@ -786,10 +786,14 @@ private class PresenceMap {
*/
synchronized void waitForSync() throws AblyException, InterruptedException {
boolean syncIsComplete = false; /* temporary variable to avoid potential race conditions */
- while((channel.state == ChannelState.attached || channel.state == ChannelState.attaching) &&
- /* = (and not ==) is intentional */
- !(syncIsComplete = (!syncInProgress && syncComplete)))
+ while (channel.state == ChannelState.attaching) {
wait();
+ }
+ if (channel.state == ChannelState.attached) {
+ while (!(syncIsComplete = (!syncInProgress && syncComplete))) {
+ wait();
+ }
+ }
/* invalid channel state */
int errorCode;
@@ -992,7 +996,7 @@ synchronized List endSync() {
members.remove(itemKey);
}
residualMembers = null;
-
+
/* finish, notifying any waiters */
syncInProgress = false;
}
From e0e7f793da7ec0a186736b247b62136990891f63 Mon Sep 17 00:00:00 2001
From: sacOO7
Date: Wed, 30 Sep 2020 00:01:47 +0530
Subject: [PATCH 002/899] Added test for consistent presence during
intermittent detach cycles
---
.../test/realtime/RealtimePresenceTest.java | 69 +++++++++++++++++--
1 file changed, 65 insertions(+), 4 deletions(-)
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 a19f720df..a23c463e5 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
@@ -37,9 +37,7 @@
import io.ably.lib.test.common.ParameterizedTest;
import io.ably.lib.test.util.MockWebsocketFactory;
import io.ably.lib.transport.ConnectionManager;
-import io.ably.lib.transport.Defaults;
import io.ably.lib.types.PresenceMessage.Action;
-import io.ably.lib.util.Log;
public class RealtimePresenceTest extends ParameterizedTest {
@@ -415,7 +413,7 @@ public void enter_update_simple() {
/* let client1 update the channel and wait for the update event to be delivered */
CompletionWaiter updateComplete = new CompletionWaiter();
String reenterString = "Test data (enter_update_simple), updating";
- client1Channel.presence.enter(reenterString, updateComplete);
+ client1Channel.presence.update(reenterString, updateComplete);
presenceWaiter.waitFor(testClientId1, Action.update);
assertNotNull(presenceWaiter.contains(testClientId1, Action.update));
assertEquals(presenceWaiter.receivedMessages.get(0).data, reenterString);
@@ -485,7 +483,7 @@ public void enter_update_null() {
/* let client1 update the channel and wait for the update event to be delivered */
CompletionWaiter updateComplete = new CompletionWaiter();
String updateString = null;
- client1Channel.presence.enter(updateString, updateComplete);
+ client1Channel.presence.update(updateString, updateComplete);
presenceWaiter.waitFor(testClientId1, Action.update);
assertNotNull(presenceWaiter.contains(testClientId1, Action.update));
assertEquals(presenceWaiter.receivedMessages.get(0).data, updateString);
@@ -3244,6 +3242,69 @@ public void presence_get() throws AblyException, InterruptedException {
}
}
+
+ /**
+ * Test Presence.get()
+ * check if parent channel is able to detect presence
+ * during intermittent detach cycles
+ */
+
+ public void checkMembersWithChannelPresence(Channel testChannel) throws AblyException {
+ PresenceMessage[] presenceMessages = testChannel.presence.get(true);
+ testChannel.detach();
+ assertEquals("Members count with channel presence should be " + presenceMessages.length, presenceMessages.length, 1);
+ }
+
+ @Test
+ public void test_consistent_presence_for_members() {
+ 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);
+
+ (new ConnectionWaiter(clientAbly1.connection)).waitFor(ConnectionState.connected);
+ assertEquals("Verify connected state reached", clientAbly1.connection.state, ConnectionState.connected);
+
+ Channel client1Channel = clientAbly1.channels.get(testChannel.channelName);
+ client1Channel.attach();
+ (new ChannelWaiter(client1Channel)).waitFor(ChannelState.attached);
+ assertEquals("Verify attached state reached", client1Channel.state, ChannelState.attached);
+
+ String enterString = "Entering presence from child channel";
+
+ CompletionWaiter enterComplete = new CompletionWaiter();
+ client1Channel.presence.enter(enterString, enterComplete);
+ enterComplete.waitFor();
+
+ presenceWaiter.waitFor(testClientId1, Action.enter);
+ assertNotNull(presenceWaiter.contains(testClientId1, Action.enter));
+ assertEquals(presenceWaiter.receivedMessages.get(0).data, enterString);
+
+ int parent_detach_cycle = 6;
+ for (int cycle = 0; cycle < parent_detach_cycle ; cycle++) {
+ Thread.sleep(1000);
+ checkMembersWithChannelPresence(testChannel.realtimeChannel);
+ }
+
+ } catch(AblyException | InterruptedException e) {
+ e.printStackTrace();
+ fail("Unexpected exception running test: " + e.getMessage());
+ } finally {
+ if(clientAbly1 != null)
+ clientAbly1.close();
+ if(testChannel != null)
+ testChannel.dispose();
+ }
+ }
+
/**
* Authenticate using wildcard token, initialize AblyRealtime so clientId is not known a priori,
* call enter() without attaching first, start connection
From 11bc5c0ea3bea86a75429d4fe49f3fbb357cf7b2 Mon Sep 17 00:00:00 2001
From: sacOO7
Date: Sat, 3 Oct 2020 22:45:00 +0530
Subject: [PATCH 003/899] reformatted code in accordance with checkstyle
---
.../java/io/ably/lib/realtime/Presence.java | 2053 ++---
.../test/realtime/RealtimePresenceTest.java | 6831 +++++++++--------
2 files changed, 4446 insertions(+), 4438 deletions(-)
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 f5a93d7bd..9478a0a41 100644
--- a/lib/src/main/java/io/ably/lib/realtime/Presence.java
+++ b/lib/src/main/java/io/ably/lib/realtime/Presence.java
@@ -3,7 +3,6 @@
import io.ably.lib.http.BasePaginatedQuery;
import io.ably.lib.http.HttpCore;
import io.ably.lib.http.HttpUtils;
-import io.ably.lib.http.PaginatedQuery;
import io.ably.lib.transport.ConnectionManager;
import io.ably.lib.types.AblyException;
import io.ably.lib.types.AsyncPaginatedResult;
@@ -15,8 +14,16 @@
import io.ably.lib.types.PresenceSerializer;
import io.ably.lib.types.ProtocolMessage;
import io.ably.lib.util.Log;
-
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.EnumMap;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
/**
* A class that provides access to presence operations and state for the
@@ -24,1024 +31,1024 @@
*/
public class Presence {
- /************************************
- * subscriptions and PresenceListener
- ************************************/
-
- /**
- * String parameter names for get() call with Param... as an argument
- */
- public final static String GET_WAITFORSYNC = "waitForSync";
- public final static String GET_CLIENTID = "clientId";
- public final static String GET_CONNECTIONID = "connectionId";
-
- /**
- * Get the presence state for this channel. Take Param[] array as an argument.
- * Implicitly attaches the channel. However, if the channel is in or moves to the FAILED
- * state before the operation succeeds, it will result in an error
- * @param params
- * @return
- * @throws AblyException
- * @throws InterruptedException
- */
- public synchronized PresenceMessage[] get(Param... params) throws AblyException {
- if (channel.state == ChannelState.failed) {
- throw AblyException.fromErrorInfo(new ErrorInfo("channel operation failed (invalid channel state)", 90001));
- }
-
- channel.attach();
- try {
- Collection values = presence.get(params);
- return values.toArray(new PresenceMessage[values.size()]);
- } catch (InterruptedException e) {
- Log.v(TAG, String.format("Channel %s: get() operation interrupted", channel.name));
- throw AblyException.fromThrowable(e);
- }
- }
-
- /**
- * Get the presence state for this Channel, optionally waiting for sync to complete.
- * Implicitly attaches the Channel. However, if the channel is in or moves to the FAILED
- * state before the operation succeeds, it will result in an error
- * @return: the current present members.
- * @throws AblyException
- */
- public synchronized PresenceMessage[] get(boolean wait) throws AblyException {
- return get(new Param(GET_WAITFORSYNC, String.valueOf(wait)));
- }
-
- /**
- * Get the presence state for a given clientId. Implicitly attaches the
- * Channel. However, if the channel is in or moves to the FAILED
- * state before the operation succeeds, it will result in an error
- * @param wait
- * @return
- * @throws InterruptedException
- * @throws AblyException
- */
- public synchronized PresenceMessage[] get(String clientId, boolean wait) throws AblyException {
- return get(new Param(GET_WAITFORSYNC, String.valueOf(wait)), new Param(GET_CLIENTID, clientId));
- }
-
- /**
- * An interface allowing a listener to be notified of arrival of a presence message.
- */
- public interface PresenceListener {
- void onPresenceMessage(PresenceMessage message);
- }
-
- /**
- * Subscribe to presence events on the associated Channel. This implicitly
- * attaches the Channel if it is not already attached.
- * @param listener: the listener to me notified on arrival of presence messages.
- * @param completionListener listener to be called on success/failure
- * @throws AblyException
- */
- public void subscribe(PresenceListener listener, CompletionListener completionListener) throws AblyException {
- implicitAttachOnSubscribe(completionListener);
- listeners.add(listener);
- }
-
- /**
- * Same as above without completion listener
- */
- public void subscribe(PresenceListener listener) throws AblyException {
- subscribe(listener, null);
- }
-
- /**
- * Unsubscribe a previously subscribed presence listener for this channel.
- * @param listener: the previously subscribed listener.
- */
- public void unsubscribe(PresenceListener listener) {
- listeners.remove(listener);
- for (Multicaster multicaster: eventListeners.values()) {
- multicaster.remove(listener);
- }
- }
-
- /**
- * Subscribe to presence events with a specific action on the associated Channel.
- * This implicitly attaches the Channel if it is not already attached.
- *
- * @param action to be observed
- * @param listener
- * @param completionListener listener to be called on success/failure
- * @throws AblyException
- */
- public void subscribe(PresenceMessage.Action action, PresenceListener listener, CompletionListener completionListener) throws AblyException {
- implicitAttachOnSubscribe(completionListener);
- subscribeImpl(action, listener);
- }
-
- /**
- * Same as above without completion listener
- */
- public void subscribe(PresenceMessage.Action action, PresenceListener listener) throws AblyException {
- subscribe(action, listener, null);
- }
-
- /**
- * Unsubscribe a previously subscribed presence listener for this channel from specific action.
- *
- * @param action
- * @param listener
- */
- public void unsubscribe(PresenceMessage.Action action, PresenceListener listener) {
- unsubscribeImpl(action, listener);
- }
-
- /**
- * Subscribe to presence events with specific actions on the associated Channel.
- * This implicitly attaches the Channel if it is not already attached.
- *
- * @param actions to be observed
- * @param listener
- * @param completionListener listener to be called on success/failure
- * @throws AblyException
- */
- public void subscribe(EnumSet actions, PresenceListener listener, CompletionListener completionListener) throws AblyException {
- implicitAttachOnSubscribe(completionListener);
- for (PresenceMessage.Action action : actions) {
- subscribeImpl(action, listener);
- }
- }
-
- /**
- * Same as above without completion listener
- */
- public void subscribe(EnumSet actions, PresenceListener listener) throws AblyException {
- subscribe(actions, listener, null);
- }
-
- /**
- * Unsubscribe a previously subscribed presence listener for this channel from specific actions.
- *
- * @param actions
- * @param listener
- */
- public void unsubscribe(EnumSet actions, PresenceListener listener) {
- for (PresenceMessage.Action action : actions) {
- unsubscribeImpl(action, listener);
- }
- }
-
- /**
- * Unsubscribe all subscribed presence lisceners for this channel.
- */
- public void unsubscribe() {
- listeners.clear();
- eventListeners.clear();
- }
-
-
- /***
- * internal
- *
- */
-
- /**
- * Implicitly attach channel on subscribe. Throw exception if channel is in failed state
- * @param completionListener
- * @throws AblyException
- */
- private void implicitAttachOnSubscribe(CompletionListener completionListener) throws AblyException {
- if (channel.state == ChannelState.failed) {
- String errorString = String.format("Channel %s: subscribe in FAILED channel state", channel.name);
- Log.v(TAG, errorString);
- ErrorInfo errorInfo = new ErrorInfo(errorString, 90001);
- throw AblyException.fromErrorInfo(errorInfo);
- }
- channel.attach(completionListener);
- }
-
- /* End sync and emit leave messages for residual members */
- private void endSyncAndEmitLeaves() {
- currentSyncChannelSerial = null;
- List residualMembers = presence.endSync();
- for (PresenceMessage member: residualMembers) {
- /*
- * RTP19: ... The PresenceMessage published should contain the original attributes of the presence
- * member with the action set to LEAVE, PresenceMessage#id set to null, and the timestamp set
- * to the current time ...
- */
- member.action = PresenceMessage.Action.leave;
- member.id = null;
- member.timestamp = System.currentTimeMillis();
- }
- broadcastPresence(residualMembers.toArray(new PresenceMessage[residualMembers.size()]));
-
- /**
- * (RTP5c2) If a SYNC is initiated as part of the attach, then once the SYNC is complete,
- * all members not present in the PresenceMap but present in the internal PresenceMap must
- * be re-entered automatically by the client using the clientId and data attributes from
- * each. The members re-entered automatically must be removed from the internal PresenceMap
- * ensuring that members present on the channel are constructed from presence events sent
- * from Ably since the channel became ATTACHED
- */
- if (syncAsResultOfAttach) {
- syncAsResultOfAttach = false;
- for (PresenceMessage item: internalPresence.values()) {
- if (presence.put(item)) {
- /* Message is new to presence map, send it */
- final String clientId = item.clientId;
- try {
- PresenceMessage itemToSend = (PresenceMessage)item.clone();
- itemToSend.action = PresenceMessage.Action.enter;
- updatePresence(itemToSend, new CompletionListener() {
- @Override
- public void onSuccess() {
- }
-
- @Override
- public void onError(ErrorInfo reason) {
- /*
- * (RTP5c3) If any of the automatic ENTER presence messages published
- * in RTP5c2 fail, then an UPDATE event should be emitted on the channel
- * with resumed set to true and reason set to an ErrorInfo object with error
- * code value 91004 and the error message string containing the message
- * received from Ably (if applicable), the code received from Ably
- * (if applicable) and the explicit or implicit client_id of the PresenceMessage
- */
- String errorString = String.format("Cannot automatically re-enter %s on channel %s (%s)",
- clientId, channel.name, reason.message);
- Log.e(TAG, errorString);
- channel.emitUpdate(new ErrorInfo(errorString, 91004), true);
- }
- });
- } catch(AblyException e) {
- String errorString = String.format("Cannot automatically re-enter %s on channel %s (%s)",
- clientId, channel.name, e.errorInfo.message);
- Log.e(TAG, errorString);
- channel.emitUpdate(new ErrorInfo(errorString, 91004), true);
- }
- }
- }
- internalPresence.clear();
- }
- }
-
- void setPresence(PresenceMessage[] messages, boolean broadcast, String syncChannelSerial) {
- Log.v(TAG, "setPresence(); channel = " + channel.name + "; broadcast = " + broadcast + "; syncChannelSerial = " + syncChannelSerial);
- String syncCursor = null;
- if(syncChannelSerial != null) {
- int colonPos = syncChannelSerial.indexOf(':');
- String serial = colonPos >= 0 ? syncChannelSerial.substring(0, colonPos) : syncChannelSerial;
- /* Discard incomplete sync if serial has changed */
- if (presence.syncInProgress && currentSyncChannelSerial != null && !currentSyncChannelSerial.equals(serial))
- endSyncAndEmitLeaves();
- syncCursor = syncChannelSerial.substring(colonPos);
- if(syncCursor.length() > 1) {
- presence.startSync();
- currentSyncChannelSerial = serial;
- }
- }
- for(PresenceMessage update : messages) {
- boolean updateInternalPresence = update.connectionId.equals(channel.ably.connection.id);
- boolean broadcastThisUpdate = broadcast;
- PresenceMessage originalUpdate = update;
-
- switch(update.action) {
- case enter:
- case update:
- update = (PresenceMessage)update.clone();
- update.action = PresenceMessage.Action.present;
- case present:
- broadcastThisUpdate &= presence.put(update);
- if(updateInternalPresence)
- internalPresence.put(update);
- break;
- case leave:
- broadcastThisUpdate &= presence.remove(update);
- if(updateInternalPresence)
- internalPresence.remove(update);
- break;
- case absent:
- }
-
- /*
- * RTP2g: Any incoming presence message that passes the newness check should be emitted on the
- * Presence object, with an event name set to its original action.
- */
- if (broadcastThisUpdate)
- broadcastPresence(new PresenceMessage[]{originalUpdate});
- }
-
- /* if this is the last message in a sequence of sync updates, end the sync */
- if(syncChannelSerial == null || syncCursor.length() <= 1) {
- endSyncAndEmitLeaves();
- }
- }
-
- private void broadcastPresence(PresenceMessage[] messages) {
- for(PresenceMessage message : messages) {
- listeners.onPresenceMessage(message);
-
- Multicaster eventListener = eventListeners.get(message.action);
- if(eventListener != null)
- eventListener.onPresenceMessage(message);
- }
- }
-
- private final Multicaster listeners = new Multicaster();
- private final EnumMap eventListeners = new EnumMap<>(PresenceMessage.Action.class);
-
- private static class Multicaster extends io.ably.lib.util.Multicaster implements PresenceListener {
- @Override
- public void onPresenceMessage(PresenceMessage message) {
- for(PresenceListener member : members)
- try {
- member.onPresenceMessage(message);
- } catch(Throwable t) {}
- }
- }
-
- private void subscribeImpl(PresenceMessage.Action action, PresenceListener listener) {
- Multicaster listeners = eventListeners.get(action);
- if(listeners == null) {
- listeners = new Multicaster();
- eventListeners.put(action, listeners);
- }
- listeners.add(listener);
- }
-
- private void unsubscribeImpl(PresenceMessage.Action action, PresenceListener listener) {
- Multicaster listeners = eventListeners.get(action);
- if(listeners != null) {
- listeners.remove(listener);
- if(listeners.isEmpty()) {
- eventListeners.remove(action);
- }
- }
- }
-
-
- /************************************
- * enter/leave and pending messages
- ************************************/
-
- /**
- * Enter this client into this channel. This client will be added to the presence set
- * and presence subscribers will see an enter message for this client.
- * @param data: optional data (eg a status message) for this member.
- * See {@link io.ably.types.Data} for the supported data types.
- * @param listener: a listener to be notified on completion of the operation.
- * @throws AblyException
- */
- public void enter(Object data, CompletionListener listener) throws AblyException {
- Log.v(TAG, "enter(); channel = " + channel.name);
- updatePresence(new PresenceMessage(PresenceMessage.Action.enter, null, data), listener);
- }
-
- /**
- * Update the presence data for this client. If the client is not already a member of
- * the presence set it will be added, and presence subscribers will see an enter or
- * update message for this client.
- * @param data: optional data (eg a status message) for this member.
- * See {@link io.ably.types.Data} for the supported data types.
- * @param listener: a listener to be notified on completion of the operation.
- * @throws AblyException
- */
- public void update(Object data, CompletionListener listener) throws AblyException {
- Log.v(TAG, "update(); channel = " + channel.name);
- updatePresence(new PresenceMessage(PresenceMessage.Action.update, null, data), listener);
- }
-
- /**
- * Leave this client from this channel. This client will be removed from the presence
- * set and presence subscribers will see a leave message for this client.
- * @param data: optional data (eg a status message) for this member.
- * See {@link io.ably.types.Data} for the supported data types.
- * @param listener: a listener to be notified on completion of the operation.
- * @throws AblyException
- */
- public void leave(Object data, CompletionListener listener) throws AblyException {
- Log.v(TAG, "leave(); channel = " + channel.name);
- updatePresence(new PresenceMessage(PresenceMessage.Action.leave, null, data), listener);
- }
-
- /**
- * Leave this client from this channel. This client will be removed from the presence
- * set and presence subscribers will see a leave message for this client.
- * @param listener: a listener to be notified on completion of the operation.
- * @throws AblyException
- */
- public void leave(CompletionListener listener) throws AblyException {
- leave(null, listener);
- }
-
- /**
- * Enter a specified client into this channel. The given clientId will be added to
- * the presence set and presence subscribers will see a corresponding presence message
- * with an empty data payload.
- * This method is provided to support connections (eg connections from application
- * server instances) that act on behalf of multiple clientIds. In order to be able to
- * enter the channel with this method, the client library must have been instanced
- * either with a key, or with a token bound to the wildcard clientId.
- * @param clientId: the id of the client.
- */
- public void enterClient(String clientId) throws AblyException {
- enterClient(clientId, null);
- }
-
- /**
- * Enter a specified client into this channel. The given client will be added to the
- * presence set and presence subscribers will see a corresponding presence message.
- * This method is provided to support connections (eg connections from application
- * server instances) that act on behalf of multiple clientIds. In order to be able to
- * enter the channel with this method, the client library must have been instanced
- * either with a key, or with a token bound to the wildcard clientId.
- * @param clientId: the id of the client.
- * @param data: optional data (eg a status message) for this member.
- * @throws AblyException
- */
- public void enterClient(String clientId, Object data) throws AblyException {
- enterClient(clientId, data, null);
- }
-
- /**
- * Enter a specified client into this channel. The given client will be added to the
- * presence set and presence subscribers will see a corresponding presence message.
- * This method is provided to support connections (eg connections from application
- * server instances) that act on behalf of multiple clientIds. In order to be able to
- * enter the channel with this method, the client library must have been instanced
- * either with a key, or with a token bound to the wildcard clientId.
- * @param clientId: the id of the client.
- * @param data: optional data (eg a status message) for this member.
- * @param listener: a listener to be notified on completion of the operation.
- * @throws AblyException
- */
- public void enterClient(String clientId, Object data, CompletionListener listener) throws AblyException {
- if(clientId == null) {
- String errorMessage = String.format("Channel %s: unable to enter presence channel (null clientId specified)", channel.name);
- Log.v(TAG, errorMessage);
- if(listener != null) {
- listener.onError(new ErrorInfo(errorMessage, 40000));
- return;
- }
- }
- Log.v(TAG, "enterClient(); channel = " + channel.name + "; clientId = " + clientId);
- updatePresence(new PresenceMessage(PresenceMessage.Action.enter, clientId, data), listener);
- }
-
- /**
- * Update the presence data for a specified client into this channel.
- * If the client is not already a member of the presence set it will be added,
- * and presence subscribers will see a corresponding presence message
- * with an empty data payload. As for #enterClient above, the connection
- * must be authenticated in a way that enables it to represent an arbitrary clientId.
- * @param clientId: the id of the client.
- * @throws AblyException
- */
- public void updateClient(String clientId) throws AblyException {
- updateClient(clientId, null);
- }
-
- /**
- * Update the presence data for a specified client into this channel.
- * If the client is not already a member of the presence set it will be added, and
- * presence subscribers will see an enter or update message for this client.
- * As for #enterClient above, the connection must be authenticated in a way that
- * enables it to represent an arbitrary clientId.
- * @param clientId: the id of the client.
- * @param data: optional data (eg a status message) for this member.
- * @throws AblyException
- */
- public void updateClient(String clientId, Object data) throws AblyException {
- updateClient(clientId, data, null);
- }
-
- /**
- * Update the presence data for a specified client into this channel.
- * If the client is not already a member of the presence set it will be added, and
- * presence subscribers will see an enter or update message for this client.
- * As for #enterClient above, the connection must be authenticated in a way that
- * enables it to represent an arbitrary clientId.
- * @param clientId: the id of the client.
- * @param data: optional data (eg a status message) for this member.
- * @param listener: a listener to be notified on completion of the operation.
- * @throws AblyException
- */
- public void updateClient(String clientId, Object data, CompletionListener listener) throws AblyException {
- if(clientId == null) {
- String errorMessage = String.format("Channel %s: unable to update presence channel (null clientId specified)", channel.name);
- Log.v(TAG, errorMessage);
- if(listener != null) {
- listener.onError(new ErrorInfo(errorMessage, 40000));
- return;
- }
- }
- Log.v(TAG, "updateClient(); channel = " + channel.name + "; clientId = " + clientId);
- updatePresence(new PresenceMessage(PresenceMessage.Action.update, clientId, data), listener);
- }
-
- /**
- * Leave a given client from this channel. This client will be removed from the
- * presence set and presence subscribers will see a corresponding presence message
- * with an empty data payload.
- * @param clientId: the id of the client.
- * @throws AblyException
- */
- public void leaveClient(String clientId) throws AblyException {
- leaveClient(clientId, null);
- }
-
- /**
- * Leave a given client from this channel. This client will be removed from the
- * presence set and presence subscribers will see a leave message for this client.
- * @param clientId: the id of the client.
- * @param data: optional data (eg a status message) for this member.
- * @throws AblyException
- */
- public void leaveClient(String clientId, Object data) throws AblyException {
- leaveClient(clientId, data, null);
- }
-
- /**
- * Leave a given client from this channel. This client will be removed from the
- * presence set and presence subscribers will see a leave message for this client.
- * @param clientId: the id of the client.
- * @param data: optional data (eg a status message) for this member.
- * @param listener: a listener to be notified on completion of the operation.
- * @throws AblyException
- */
- public void leaveClient(String clientId, Object data, CompletionListener listener) throws AblyException {
- if(clientId == null) {
- String errorMessage = String.format("Channel %s: unable to leave presence channel (null clientId specified)", channel.name);
- Log.v(TAG, errorMessage);
- if(listener != null) {
- listener.onError(new ErrorInfo(errorMessage, 40000));
- return;
- }
- }
- Log.v(TAG, "leaveClient(); channel = " + channel.name + "; clientId = " + clientId);
- updatePresence(new PresenceMessage(PresenceMessage.Action.leave, clientId, data), listener);
- }
-
- /**
- * Update the presence for this channel with a given PresenceMessage update.
- * The connection must be authenticated in a way that enables it to represent
- * the clientId in the message.
- * @param msg: the presence message
- * @param listener: a listener to be notified on completion of the operation.
- * @throws AblyException
- */
- public void updatePresence(PresenceMessage msg, CompletionListener listener) throws AblyException {
- Log.v(TAG, "update(); channel = " + channel.name);
-
- AblyRealtime ably = channel.ably;
- boolean connected = (ably.connection.state == ConnectionState.connected);
- String clientId;
- try {
- clientId = ably.auth.checkClientId(msg, false, connected);
- } catch(AblyException e) {
- if(listener != null) {
- listener.onError(e.errorInfo);
- }
- return;
- }
-
- msg.encode(null);
- synchronized(channel) {
- switch(channel.state) {
- case initialized:
- channel.attach();
- case attaching:
- QueuedPresence queued = new QueuedPresence(msg, listener);
- pendingPresence.put(clientId, queued);
- break;
- case attached:
- ProtocolMessage message = new ProtocolMessage(ProtocolMessage.Action.presence, channel.name);
- message.presence = new PresenceMessage[] { msg };
- ConnectionManager connectionManager = ably.connection.connectionManager;
- connectionManager.send(message, ably.options.queueMessages, listener);
- break;
- default:
- throw AblyException.fromErrorInfo(new ErrorInfo("Unable to enter presence channel in detached or failed state", 400, 91001));
- }
- }
- }
-
- /************************************
- * history
- ************************************/
-
- /**
- * Obtain recent history for this channel using the REST API.
- * The history provided relates to all clients of this application,
- * not just this instance.
- * @param params: the request params. See the Ably REST API
- * documentation for more details.
- * @return: an array of Messgaes for this Channel.
- * @throws AblyException
- */
- public PaginatedResult history(Param[] params) throws AblyException {
- return historyImpl(params).sync();
- }
-
- public void historyAsync(Param[] params, Callback> callback) {
- historyImpl(params).async(callback);
- }
-
- private BasePaginatedQuery.ResultRequest historyImpl(Param[] params) {
- try {
- params = Channel.replacePlaceholderParams(channel, params);
- } catch (AblyException e) {
- return new BasePaginatedQuery.ResultRequest.Failed(e);
- }
-
- AblyRealtime ably = channel.ably;
- HttpCore.BodyHandler bodyHandler = PresenceSerializer.getPresenceResponseHandler(channel.options);
- return new BasePaginatedQuery(ably.http, channel.basePath + "/presence/history", HttpUtils.defaultAcceptHeaders(ably.options.useBinaryProtocol), params, bodyHandler).get();
- }
-
- /**
- * internal
- *
- */
- private static class QueuedPresence {
- public PresenceMessage msg;
- public CompletionListener listener;
- public QueuedPresence(PresenceMessage msg, CompletionListener listener) { this.msg = msg; this.listener = listener; }
- }
-
- private final Map pendingPresence = new HashMap();
-
- private void sendQueuedMessages() {
- Log.v(TAG, "sendQueuedMessages()");
- AblyRealtime ably = channel.ably;
- boolean queueMessages = ably.options.queueMessages;
- ConnectionManager connectionManager = ably.connection.connectionManager;
- int count = pendingPresence.size();
- if(count == 0)
- return;
-
- ProtocolMessage message = new ProtocolMessage(ProtocolMessage.Action.presence, channel.name);
- Iterator allQueued = pendingPresence.values().iterator();
- PresenceMessage[] presenceMessages = message.presence = new PresenceMessage[count];
- CompletionListener listener;
-
- if(count == 1) {
- QueuedPresence queued = allQueued.next();
- presenceMessages[0] = queued.msg;
- listener = queued.listener;
- } else {
- int idx = 0;
- CompletionListener.Multicaster mListener = new CompletionListener.Multicaster();
- while(allQueued.hasNext()) {
- QueuedPresence queued = allQueued.next();
- presenceMessages[idx++] = queued.msg;
- if(queued.listener != null)
- mListener.add(queued.listener);
- }
- listener = mListener.isEmpty() ? null : mListener;
- }
- pendingPresence.clear();
- try {
- connectionManager.send(message, queueMessages, listener);
- } catch(AblyException e) {
- Log.e(TAG, "sendQueuedMessages(): Unexpected exception sending message", e);
- if(listener != null)
- listener.onError(e.errorInfo);
- }
- }
-
- private void failQueuedMessages(ErrorInfo reason) {
- Log.v(TAG, "failQueuedMessages()");
- for(QueuedPresence msg : pendingPresence.values())
- if(msg.listener != null)
- try {
- msg.listener.onError(reason);
- } catch(Throwable t) {
- Log.e(TAG, "failQueuedMessages(): Unexpected exception calling listener", t);
- }
- pendingPresence.clear();
- }
-
-
- /************************************
- * attach / detach
- ************************************/
-
- void setAttached(boolean hasPresence) {
- /* Start sync, if hasPresence is not set end sync immediately dropping all the current presence members */
- presence.startSync();
- syncAsResultOfAttach = true;
- if (!hasPresence) {
- /*
- * RTP19a If the PresenceMap has existing members when an ATTACHED message is received without a
- * HAS_PRESENCE flag, the client library should emit a LEAVE event for each existing member ...
- */
- endSyncAndEmitLeaves();
- }
- sendQueuedMessages();
- }
-
- void setDetached(ErrorInfo reason) {
- /* Interrupt get() call if needed */
- synchronized (presence) {
- presence.notifyAll();
- }
-
- /**
- * (RTP5a) If the channel enters the DETACHED or FAILED state then all queued presence
- * messages will fail immediately, and the PresenceMap and internal PresenceMap is cleared.
- * The latter ensures members are not automatically re-entered if the Channel later becomes attached
- */
- failQueuedMessages(reason);
- presence.clear();
- internalPresence.clear();
- }
-
- void setSuspended(ErrorInfo reason) {
- /* Interrupt get() call if needed */
- synchronized (presence) {
- presence.notifyAll();
- }
-
- /*
- * (RTP5f) If the channel enters the SUSPENDED state then all queued presence messages will fail
- * immediately, and the PresenceMap is maintained
- */
- failQueuedMessages(reason);
- }
-
- /**
- * A class encapsulating a map of the members of this presence channel,
- * indexed by a String key that is a combination of connectionId and clientId.
- * This map synchronises the membership of the presence set by handling
- * sync messages from the service. Since sync messages can be out-of-order -
- * eg an enter sync event being received after that member has in fact left -
- * this map keeps "witness" entries, with absent Action, to remember the
- * fact that a leave event has been seen for a member. These entries are
- * cleared once the last set of updates of a sync sequence have been received.
- *
- */
- private class PresenceMap {
-
- /**
- * Wait for sync to be complete. If we are in attaching state wait for initial sync to
- * complete as well. Return false if wait was interrupted because channel transitioned to
- * state other than attached or attaching
- */
- synchronized void waitForSync() throws AblyException, InterruptedException {
- boolean syncIsComplete = false; /* temporary variable to avoid potential race conditions */
- while (channel.state == ChannelState.attaching) {
- wait();
- }
- if (channel.state == ChannelState.attached) {
- while (!(syncIsComplete = (!syncInProgress && syncComplete))) {
- wait();
- }
- }
-
- /* invalid channel state */
- int errorCode;
- String errorMessage;
-
- if (channel.state == ChannelState.suspended) {
- /* (RTP11d) If the Channel is in the SUSPENDED state then the get function will by default,
- * or if waitForSync is set to true, result in an error with code 91005 and a message stating
- * that the presence state is out of sync due to the channel being in a SUSPENDED state */
- errorCode = 91005;
- errorMessage = String.format("Channel %s: presence state is out of sync due to the channel being in a SUSPENDED state", channel.name);
- } else if(syncIsComplete) {
- return;
- } else {
- errorCode = 90001;
- errorMessage = String.format("Channel %s: cannot get presence state because channel is in invalid state", channel.name);
- }
- Log.v(TAG, errorMessage);
- throw AblyException.fromErrorInfo(new ErrorInfo(errorMessage, errorCode));
- }
-
- synchronized Collection get(Param[] params) throws AblyException, InterruptedException {
- boolean waitForSync = true;
- String clientId = null;
- String connectionId = null;
-
- for (Param param: params) {
- switch (param.key) {
- case GET_WAITFORSYNC:
- waitForSync = Boolean.valueOf(param.value);
- break;
- case GET_CLIENTID:
- clientId = param.value;
- break;
- case GET_CONNECTIONID:
- connectionId = param.value;
- break;
- }
- }
-
- HashSet result = new HashSet<>();
- if (waitForSync)
- waitForSync();
-
- for (Map.Entry entry: members.entrySet()) {
- PresenceMessage member = entry.getValue();
- if ((clientId == null || member.clientId.equals(clientId)) &&
- (connectionId == null || member.connectionId.equals(connectionId)))
- result.add(member);
- }
-
- return result;
- }
-
- /**
- * Add or update the presence state for a member
- * @param item
- * @return true if the given message represents a change;
- * false if the message is already superseded
- */
- synchronized boolean put(PresenceMessage item) {
- String key = item.memberKey();
- /* we've seen this member, so do not remove it at the end of sync */
- if(residualMembers != null)
- residualMembers.remove(key);
-
- /* check if there is a newer existing member (or absent witness) */
- if (hasNewerItem(key, item))
- return false;
-
- members.put(key, item);
- return true;
- }
-
- /**
- * Determine if there is a newer item already in the map
- * @param key key used to search the item in the map
- * @param item new presence message to be added
- * @return true if there is a newer item
- */
- synchronized boolean hasNewerItem(String key, PresenceMessage item) {
- PresenceMessage existingItem = members.get(key);
- if(existingItem == null)
- return false;
-
- /*
- * (RTP2b1) If either presence message has a connectionId which is not an initial substring
- * of its id, compare them by timestamp numerically. (This will be the case when one of them
- * is a 'synthesized leave' event sent by realtime to indicate a connection disconnected
- * unexpectedly 15s ago. Such messages will have an id that does not correspond to its
- * connectionId, as it wasn't actually published by that connection
- */
- if(item.connectionId != null && existingItem.connectionId != null &&
- (!item.id.startsWith(item.connectionId) || !existingItem.id.startsWith(existingItem.connectionId)))
- return existingItem.timestamp >= item.timestamp;
-
- /*
- * (RTP2b2) Else split the id of both presence messages (which will be of the form
- * connid:msgSerial:index, e.g. aaaaaa:0:0) on the separator :, and parse the latter two as
- * integers. Compare them first by msgSerial numerically, then (if @msgSerial@s are equal) by
- * index numerically, larger being newer in both cases
- */
- String[] itemComponents = item.id.split(":", 3);
- String[] existingItemComponents = existingItem.id.split(":", 3);
-
- if(itemComponents.length < 3 || existingItemComponents.length < 3)
- return false;
-
- try {
- long messageSerial = Long.valueOf(itemComponents[1]);
- long messageIndex = Long.valueOf(itemComponents[2]);
- long existingMessageSerial = Long.valueOf(existingItemComponents[1]);
- long existingMessageIndex = Long.valueOf(existingItemComponents[2]);
-
- return existingMessageSerial > messageSerial ||
- (existingMessageSerial == messageSerial && existingMessageIndex >= messageIndex);
- }
- catch(NumberFormatException e) {
- return false;
- }
- }
-
- /**
- * Get all members based on the current state (even if sync is in progress)
- * @return
- */
- synchronized Collection values() {
- try { return values(false); } catch (InterruptedException|AblyException e) { return null; }
- }
-
- /**
- * Get all members, optionally waiting if a sync is in progress.
- * @param wait
- * @return
- * @throws InterruptedException
- */
- synchronized Collection values(boolean wait) throws AblyException, InterruptedException {
- Set result = new HashSet();
- if(wait)
- waitForSync();
- result.addAll(members.values());
- for(Iterator it = result.iterator(); it.hasNext();) {
- PresenceMessage entry = it.next();
- if(entry.action == PresenceMessage.Action.absent) {
- it.remove();
- }
- }
- return result;
- }
-
- /**
- * Remove a member.
- * @param item
- * @return
- */
- synchronized boolean remove(PresenceMessage item) {
- String key = item.memberKey();
- if (hasNewerItem(key, item))
- return false;
- PresenceMessage existingItem = members.remove(key);
- if(existingItem != null && existingItem.action == PresenceMessage.Action.absent)
- return false;
- return true;
- }
-
- /**
- * Start a sync sequence.
- * Note that this is called each time a sync message is received that is not
- * the last.
- */
- synchronized void startSync() {
- Log.v(TAG, "startSync(); channel = " + channel.name + "; syncInProgress = " + syncInProgress);
- /* we might be called multiple times while a sync is in progress */
- if(!syncInProgress) {
- residualMembers = new HashSet(members.keySet());
- syncInProgress = true;
- }
- }
-
- /**
- * Finish a sync sequence. Returns "residual" items that were removed as a part of a sync
- */
- synchronized List endSync() {
- Log.v(TAG, "endSync(); channel = " + channel.name + "; syncInProgress = " + syncInProgress);
- ArrayList removedEntries = new ArrayList<>();
- if(syncInProgress) {
- /* we can now strip out the absent members, as we have
- * received all of the out-of-order sync messages */
- for(Iterator> it = members.entrySet().iterator(); it.hasNext();) {
- Map.Entry entry = it.next();
- if(entry.getValue().action == PresenceMessage.Action.absent) {
- it.remove();
- }
- }
- /* any members that were present at the start of the sync,
- * and have not been seen in sync, can be removed */
- for(String itemKey: residualMembers) {
- /* clone presence message as it still can be in the internal presence map */
- removedEntries.add((PresenceMessage)members.get(itemKey).clone());
- members.remove(itemKey);
- }
- residualMembers = null;
-
- /* finish, notifying any waiters */
- syncInProgress = false;
- }
- syncComplete = true;
- notifyAll();
- return removedEntries;
- }
-
- /**
- * Clear all entries
- */
- synchronized void clear() {
- members.clear();
- if(residualMembers != null)
- residualMembers.clear();
- }
-
- private boolean syncInProgress;
- private Collection residualMembers;
- private final HashMap members = new HashMap();
- }
-
- private final PresenceMap presence = new PresenceMap();
- private final PresenceMap internalPresence = new PresenceMap();
-
- /************************************
- * general
- ************************************/
-
- Presence(Channel channel) {
- this.channel = channel;
- }
-
- private static final String TAG = Channel.class.getName();
-
- private final Channel channel;
-
- /* channel serial if sync is in progress */
- private String currentSyncChannelSerial;
- /* Sync in progress is a result of attach operation */
- private boolean syncAsResultOfAttach;
-
- /**
- * (RTP13) Presence#syncComplete returns true if the initial SYNC operation has completed for
- * the members present on the channel
- */
- public boolean syncComplete;
+ /************************************
+ * subscriptions and PresenceListener
+ ************************************/
+
+ /**
+ * String parameter names for get() call with Param... as an argument
+ */
+ public final static String GET_WAITFORSYNC = "waitForSync";
+ public final static String GET_CLIENTID = "clientId";
+ public final static String GET_CONNECTIONID = "connectionId";
+
+ /**
+ * Get the presence state for this channel. Take Param[] array as an argument.
+ * Implicitly attaches the channel. However, if the channel is in or moves to the FAILED
+ * state before the operation succeeds, it will result in an error
+ * @param params
+ * @return
+ * @throws AblyException
+ * @throws InterruptedException
+ */
+ public synchronized PresenceMessage[] get(Param... params) throws AblyException {
+ if (channel.state == ChannelState.failed) {
+ throw AblyException.fromErrorInfo(new ErrorInfo("channel operation failed (invalid channel state)", 90001));
+ }
+
+ channel.attach();
+ try {
+ Collection values = presence.get(params);
+ return values.toArray(new PresenceMessage[values.size()]);
+ } catch (InterruptedException e) {
+ Log.v(TAG, String.format("Channel %s: get() operation interrupted", channel.name));
+ throw AblyException.fromThrowable(e);
+ }
+ }
+
+ /**
+ * Get the presence state for this Channel, optionally waiting for sync to complete.
+ * Implicitly attaches the Channel. However, if the channel is in or moves to the FAILED
+ * state before the operation succeeds, it will result in an error
+ * @return: the current present members.
+ * @throws AblyException
+ */
+ public synchronized PresenceMessage[] get(boolean wait) throws AblyException {
+ return get(new Param(GET_WAITFORSYNC, String.valueOf(wait)));
+ }
+
+ /**
+ * Get the presence state for a given clientId. Implicitly attaches the
+ * Channel. However, if the channel is in or moves to the FAILED
+ * state before the operation succeeds, it will result in an error
+ * @param wait
+ * @return
+ * @throws InterruptedException
+ * @throws AblyException
+ */
+ public synchronized PresenceMessage[] get(String clientId, boolean wait) throws AblyException {
+ return get(new Param(GET_WAITFORSYNC, String.valueOf(wait)), new Param(GET_CLIENTID, clientId));
+ }
+
+ /**
+ * An interface allowing a listener to be notified of arrival of a presence message.
+ */
+ public interface PresenceListener {
+ void onPresenceMessage(PresenceMessage message);
+ }
+
+ /**
+ * Subscribe to presence events on the associated Channel. This implicitly
+ * attaches the Channel if it is not already attached.
+ * @param listener: the listener to me notified on arrival of presence messages.
+ * @param completionListener listener to be called on success/failure
+ * @throws AblyException
+ */
+ public void subscribe(PresenceListener listener, CompletionListener completionListener) throws AblyException {
+ implicitAttachOnSubscribe(completionListener);
+ listeners.add(listener);
+ }
+
+ /**
+ * Same as above without completion listener
+ */
+ public void subscribe(PresenceListener listener) throws AblyException {
+ subscribe(listener, null);
+ }
+
+ /**
+ * Unsubscribe a previously subscribed presence listener for this channel.
+ * @param listener: the previously subscribed listener.
+ */
+ public void unsubscribe(PresenceListener listener) {
+ listeners.remove(listener);
+ for (Multicaster multicaster: eventListeners.values()) {
+ multicaster.remove(listener);
+ }
+ }
+
+ /**
+ * Subscribe to presence events with a specific action on the associated Channel.
+ * This implicitly attaches the Channel if it is not already attached.
+ *
+ * @param action to be observed
+ * @param listener
+ * @param completionListener listener to be called on success/failure
+ * @throws AblyException
+ */
+ public void subscribe(PresenceMessage.Action action, PresenceListener listener, CompletionListener completionListener) throws AblyException {
+ implicitAttachOnSubscribe(completionListener);
+ subscribeImpl(action, listener);
+ }
+
+ /**
+ * Same as above without completion listener
+ */
+ public void subscribe(PresenceMessage.Action action, PresenceListener listener) throws AblyException {
+ subscribe(action, listener, null);
+ }
+
+ /**
+ * Unsubscribe a previously subscribed presence listener for this channel from specific action.
+ *
+ * @param action
+ * @param listener
+ */
+ public void unsubscribe(PresenceMessage.Action action, PresenceListener listener) {
+ unsubscribeImpl(action, listener);
+ }
+
+ /**
+ * Subscribe to presence events with specific actions on the associated Channel.
+ * This implicitly attaches the Channel if it is not already attached.
+ *
+ * @param actions to be observed
+ * @param listener
+ * @param completionListener listener to be called on success/failure
+ * @throws AblyException
+ */
+ public void subscribe(EnumSet actions, PresenceListener listener, CompletionListener completionListener) throws AblyException {
+ implicitAttachOnSubscribe(completionListener);
+ for (PresenceMessage.Action action : actions) {
+ subscribeImpl(action, listener);
+ }
+ }
+
+ /**
+ * Same as above without completion listener
+ */
+ public void subscribe(EnumSet actions, PresenceListener listener) throws AblyException {
+ subscribe(actions, listener, null);
+ }
+
+ /**
+ * Unsubscribe a previously subscribed presence listener for this channel from specific actions.
+ *
+ * @param actions
+ * @param listener
+ */
+ public void unsubscribe(EnumSet actions, PresenceListener listener) {
+ for (PresenceMessage.Action action : actions) {
+ unsubscribeImpl(action, listener);
+ }
+ }
+
+ /**
+ * Unsubscribe all subscribed presence lisceners for this channel.
+ */
+ public void unsubscribe() {
+ listeners.clear();
+ eventListeners.clear();
+ }
+
+
+ /***
+ * internal
+ *
+ */
+
+ /**
+ * Implicitly attach channel on subscribe. Throw exception if channel is in failed state
+ * @param completionListener
+ * @throws AblyException
+ */
+ private void implicitAttachOnSubscribe(CompletionListener completionListener) throws AblyException {
+ if (channel.state == ChannelState.failed) {
+ String errorString = String.format("Channel %s: subscribe in FAILED channel state", channel.name);
+ Log.v(TAG, errorString);
+ ErrorInfo errorInfo = new ErrorInfo(errorString, 90001);
+ throw AblyException.fromErrorInfo(errorInfo);
+ }
+ channel.attach(completionListener);
+ }
+
+ /* End sync and emit leave messages for residual members */
+ private void endSyncAndEmitLeaves() {
+ currentSyncChannelSerial = null;
+ List residualMembers = presence.endSync();
+ for (PresenceMessage member: residualMembers) {
+ /*
+ * RTP19: ... The PresenceMessage published should contain the original attributes of the presence
+ * member with the action set to LEAVE, PresenceMessage#id set to null, and the timestamp set
+ * to the current time ...
+ */
+ member.action = PresenceMessage.Action.leave;
+ member.id = null;
+ member.timestamp = System.currentTimeMillis();
+ }
+ broadcastPresence(residualMembers.toArray(new PresenceMessage[residualMembers.size()]));
+
+ /**
+ * (RTP5c2) If a SYNC is initiated as part of the attach, then once the SYNC is complete,
+ * all members not present in the PresenceMap but present in the internal PresenceMap must
+ * be re-entered automatically by the client using the clientId and data attributes from
+ * each. The members re-entered automatically must be removed from the internal PresenceMap
+ * ensuring that members present on the channel are constructed from presence events sent
+ * from Ably since the channel became ATTACHED
+ */
+ if (syncAsResultOfAttach) {
+ syncAsResultOfAttach = false;
+ for (PresenceMessage item: internalPresence.values()) {
+ if (presence.put(item)) {
+ /* Message is new to presence map, send it */
+ final String clientId = item.clientId;
+ try {
+ PresenceMessage itemToSend = (PresenceMessage)item.clone();
+ itemToSend.action = PresenceMessage.Action.enter;
+ updatePresence(itemToSend, new CompletionListener() {
+ @Override
+ public void onSuccess() {
+ }
+
+ @Override
+ public void onError(ErrorInfo reason) {
+ /*
+ * (RTP5c3) If any of the automatic ENTER presence messages published
+ * in RTP5c2 fail, then an UPDATE event should be emitted on the channel
+ * with resumed set to true and reason set to an ErrorInfo object with error
+ * code value 91004 and the error message string containing the message
+ * received from Ably (if applicable), the code received from Ably
+ * (if applicable) and the explicit or implicit client_id of the PresenceMessage
+ */
+ String errorString = String.format("Cannot automatically re-enter %s on channel %s (%s)",
+ clientId, channel.name, reason.message);
+ Log.e(TAG, errorString);
+ channel.emitUpdate(new ErrorInfo(errorString, 91004), true);
+ }
+ });
+ } catch(AblyException e) {
+ String errorString = String.format("Cannot automatically re-enter %s on channel %s (%s)",
+ clientId, channel.name, e.errorInfo.message);
+ Log.e(TAG, errorString);
+ channel.emitUpdate(new ErrorInfo(errorString, 91004), true);
+ }
+ }
+ }
+ internalPresence.clear();
+ }
+ }
+
+ void setPresence(PresenceMessage[] messages, boolean broadcast, String syncChannelSerial) {
+ Log.v(TAG, "setPresence(); channel = " + channel.name + "; broadcast = " + broadcast + "; syncChannelSerial = " + syncChannelSerial);
+ String syncCursor = null;
+ if(syncChannelSerial != null) {
+ int colonPos = syncChannelSerial.indexOf(':');
+ String serial = colonPos >= 0 ? syncChannelSerial.substring(0, colonPos) : syncChannelSerial;
+ /* Discard incomplete sync if serial has changed */
+ if (presence.syncInProgress && currentSyncChannelSerial != null && !currentSyncChannelSerial.equals(serial))
+ endSyncAndEmitLeaves();
+ syncCursor = syncChannelSerial.substring(colonPos);
+ if(syncCursor.length() > 1) {
+ presence.startSync();
+ currentSyncChannelSerial = serial;
+ }
+ }
+ for(PresenceMessage update : messages) {
+ boolean updateInternalPresence = update.connectionId.equals(channel.ably.connection.id);
+ boolean broadcastThisUpdate = broadcast;
+ PresenceMessage originalUpdate = update;
+
+ switch(update.action) {
+ case enter:
+ case update:
+ update = (PresenceMessage)update.clone();
+ update.action = PresenceMessage.Action.present;
+ case present:
+ broadcastThisUpdate &= presence.put(update);
+ if(updateInternalPresence)
+ internalPresence.put(update);
+ break;
+ case leave:
+ broadcastThisUpdate &= presence.remove(update);
+ if(updateInternalPresence)
+ internalPresence.remove(update);
+ break;
+ case absent:
+ }
+
+ /*
+ * RTP2g: Any incoming presence message that passes the newness check should be emitted on the
+ * Presence object, with an event name set to its original action.
+ */
+ if (broadcastThisUpdate)
+ broadcastPresence(new PresenceMessage[]{originalUpdate});
+ }
+
+ /* if this is the last message in a sequence of sync updates, end the sync */
+ if(syncChannelSerial == null || syncCursor.length() <= 1) {
+ endSyncAndEmitLeaves();
+ }
+ }
+
+ private void broadcastPresence(PresenceMessage[] messages) {
+ for(PresenceMessage message : messages) {
+ listeners.onPresenceMessage(message);
+
+ Multicaster eventListener = eventListeners.get(message.action);
+ if(eventListener != null)
+ eventListener.onPresenceMessage(message);
+ }
+ }
+
+ private final Multicaster listeners = new Multicaster();
+ private final EnumMap eventListeners = new EnumMap<>(PresenceMessage.Action.class);
+
+ private static class Multicaster extends io.ably.lib.util.Multicaster implements PresenceListener {
+ @Override
+ public void onPresenceMessage(PresenceMessage message) {
+ for(PresenceListener member : members)
+ try {
+ member.onPresenceMessage(message);
+ } catch(Throwable t) {}
+ }
+ }
+
+ private void subscribeImpl(PresenceMessage.Action action, PresenceListener listener) {
+ Multicaster listeners = eventListeners.get(action);
+ if(listeners == null) {
+ listeners = new Multicaster();
+ eventListeners.put(action, listeners);
+ }
+ listeners.add(listener);
+ }
+
+ private void unsubscribeImpl(PresenceMessage.Action action, PresenceListener listener) {
+ Multicaster listeners = eventListeners.get(action);
+ if(listeners != null) {
+ listeners.remove(listener);
+ if(listeners.isEmpty()) {
+ eventListeners.remove(action);
+ }
+ }
+ }
+
+
+ /************************************
+ * enter/leave and pending messages
+ ************************************/
+
+ /**
+ * Enter this client into this channel. This client will be added to the presence set
+ * and presence subscribers will see an enter message for this client.
+ * @param data: optional data (eg a status message) for this member.
+ * See {@link io.ably.types.Data} for the supported data types.
+ * @param listener: a listener to be notified on completion of the operation.
+ * @throws AblyException
+ */
+ public void enter(Object data, CompletionListener listener) throws AblyException {
+ Log.v(TAG, "enter(); channel = " + channel.name);
+ updatePresence(new PresenceMessage(PresenceMessage.Action.enter, null, data), listener);
+ }
+
+ /**
+ * Update the presence data for this client. If the client is not already a member of
+ * the presence set it will be added, and presence subscribers will see an enter or
+ * update message for this client.
+ * @param data: optional data (eg a status message) for this member.
+ * See {@link io.ably.types.Data} for the supported data types.
+ * @param listener: a listener to be notified on completion of the operation.
+ * @throws AblyException
+ */
+ public void update(Object data, CompletionListener listener) throws AblyException {
+ Log.v(TAG, "update(); channel = " + channel.name);
+ updatePresence(new PresenceMessage(PresenceMessage.Action.update, null, data), listener);
+ }
+
+ /**
+ * Leave this client from this channel. This client will be removed from the presence
+ * set and presence subscribers will see a leave message for this client.
+ * @param data: optional data (eg a status message) for this member.
+ * See {@link io.ably.types.Data} for the supported data types.
+ * @param listener: a listener to be notified on completion of the operation.
+ * @throws AblyException
+ */
+ public void leave(Object data, CompletionListener listener) throws AblyException {
+ Log.v(TAG, "leave(); channel = " + channel.name);
+ updatePresence(new PresenceMessage(PresenceMessage.Action.leave, null, data), listener);
+ }
+
+ /**
+ * Leave this client from this channel. This client will be removed from the presence
+ * set and presence subscribers will see a leave message for this client.
+ * @param listener: a listener to be notified on completion of the operation.
+ * @throws AblyException
+ */
+ public void leave(CompletionListener listener) throws AblyException {
+ leave(null, listener);
+ }
+
+ /**
+ * Enter a specified client into this channel. The given clientId will be added to
+ * the presence set and presence subscribers will see a corresponding presence message
+ * with an empty data payload.
+ * This method is provided to support connections (eg connections from application
+ * server instances) that act on behalf of multiple clientIds. In order to be able to
+ * enter the channel with this method, the client library must have been instanced
+ * either with a key, or with a token bound to the wildcard clientId.
+ * @param clientId: the id of the client.
+ */
+ public void enterClient(String clientId) throws AblyException {
+ enterClient(clientId, null);
+ }
+
+ /**
+ * Enter a specified client into this channel. The given client will be added to the
+ * presence set and presence subscribers will see a corresponding presence message.
+ * This method is provided to support connections (eg connections from application
+ * server instances) that act on behalf of multiple clientIds. In order to be able to
+ * enter the channel with this method, the client library must have been instanced
+ * either with a key, or with a token bound to the wildcard clientId.
+ * @param clientId: the id of the client.
+ * @param data: optional data (eg a status message) for this member.
+ * @throws AblyException
+ */
+ public void enterClient(String clientId, Object data) throws AblyException {
+ enterClient(clientId, data, null);
+ }
+
+ /**
+ * Enter a specified client into this channel. The given client will be added to the
+ * presence set and presence subscribers will see a corresponding presence message.
+ * This method is provided to support connections (eg connections from application
+ * server instances) that act on behalf of multiple clientIds. In order to be able to
+ * enter the channel with this method, the client library must have been instanced
+ * either with a key, or with a token bound to the wildcard clientId.
+ * @param clientId: the id of the client.
+ * @param data: optional data (eg a status message) for this member.
+ * @param listener: a listener to be notified on completion of the operation.
+ * @throws AblyException
+ */
+ public void enterClient(String clientId, Object data, CompletionListener listener) throws AblyException {
+ if(clientId == null) {
+ String errorMessage = String.format("Channel %s: unable to enter presence channel (null clientId specified)", channel.name);
+ Log.v(TAG, errorMessage);
+ if(listener != null) {
+ listener.onError(new ErrorInfo(errorMessage, 40000));
+ return;
+ }
+ }
+ Log.v(TAG, "enterClient(); channel = " + channel.name + "; clientId = " + clientId);
+ updatePresence(new PresenceMessage(PresenceMessage.Action.enter, clientId, data), listener);
+ }
+
+ /**
+ * Update the presence data for a specified client into this channel.
+ * If the client is not already a member of the presence set it will be added,
+ * and presence subscribers will see a corresponding presence message
+ * with an empty data payload. As for #enterClient above, the connection
+ * must be authenticated in a way that enables it to represent an arbitrary clientId.
+ * @param clientId: the id of the client.
+ * @throws AblyException
+ */
+ public void updateClient(String clientId) throws AblyException {
+ updateClient(clientId, null);
+ }
+
+ /**
+ * Update the presence data for a specified client into this channel.
+ * If the client is not already a member of the presence set it will be added, and
+ * presence subscribers will see an enter or update message for this client.
+ * As for #enterClient above, the connection must be authenticated in a way that
+ * enables it to represent an arbitrary clientId.
+ * @param clientId: the id of the client.
+ * @param data: optional data (eg a status message) for this member.
+ * @throws AblyException
+ */
+ public void updateClient(String clientId, Object data) throws AblyException {
+ updateClient(clientId, data, null);
+ }
+
+ /**
+ * Update the presence data for a specified client into this channel.
+ * If the client is not already a member of the presence set it will be added, and
+ * presence subscribers will see an enter or update message for this client.
+ * As for #enterClient above, the connection must be authenticated in a way that
+ * enables it to represent an arbitrary clientId.
+ * @param clientId: the id of the client.
+ * @param data: optional data (eg a status message) for this member.
+ * @param listener: a listener to be notified on completion of the operation.
+ * @throws AblyException
+ */
+ public void updateClient(String clientId, Object data, CompletionListener listener) throws AblyException {
+ if(clientId == null) {
+ String errorMessage = String.format("Channel %s: unable to update presence channel (null clientId specified)", channel.name);
+ Log.v(TAG, errorMessage);
+ if(listener != null) {
+ listener.onError(new ErrorInfo(errorMessage, 40000));
+ return;
+ }
+ }
+ Log.v(TAG, "updateClient(); channel = " + channel.name + "; clientId = " + clientId);
+ updatePresence(new PresenceMessage(PresenceMessage.Action.update, clientId, data), listener);
+ }
+
+ /**
+ * Leave a given client from this channel. This client will be removed from the
+ * presence set and presence subscribers will see a corresponding presence message
+ * with an empty data payload.
+ * @param clientId: the id of the client.
+ * @throws AblyException
+ */
+ public void leaveClient(String clientId) throws AblyException {
+ leaveClient(clientId, null);
+ }
+
+ /**
+ * Leave a given client from this channel. This client will be removed from the
+ * presence set and presence subscribers will see a leave message for this client.
+ * @param clientId: the id of the client.
+ * @param data: optional data (eg a status message) for this member.
+ * @throws AblyException
+ */
+ public void leaveClient(String clientId, Object data) throws AblyException {
+ leaveClient(clientId, data, null);
+ }
+
+ /**
+ * Leave a given client from this channel. This client will be removed from the
+ * presence set and presence subscribers will see a leave message for this client.
+ * @param clientId: the id of the client.
+ * @param data: optional data (eg a status message) for this member.
+ * @param listener: a listener to be notified on completion of the operation.
+ * @throws AblyException
+ */
+ public void leaveClient(String clientId, Object data, CompletionListener listener) throws AblyException {
+ if(clientId == null) {
+ String errorMessage = String.format("Channel %s: unable to leave presence channel (null clientId specified)", channel.name);
+ Log.v(TAG, errorMessage);
+ if(listener != null) {
+ listener.onError(new ErrorInfo(errorMessage, 40000));
+ return;
+ }
+ }
+ Log.v(TAG, "leaveClient(); channel = " + channel.name + "; clientId = " + clientId);
+ updatePresence(new PresenceMessage(PresenceMessage.Action.leave, clientId, data), listener);
+ }
+
+ /**
+ * Update the presence for this channel with a given PresenceMessage update.
+ * The connection must be authenticated in a way that enables it to represent
+ * the clientId in the message.
+ * @param msg: the presence message
+ * @param listener: a listener to be notified on completion of the operation.
+ * @throws AblyException
+ */
+ public void updatePresence(PresenceMessage msg, CompletionListener listener) throws AblyException {
+ Log.v(TAG, "update(); channel = " + channel.name);
+
+ AblyRealtime ably = channel.ably;
+ boolean connected = (ably.connection.state == ConnectionState.connected);
+ String clientId;
+ try {
+ clientId = ably.auth.checkClientId(msg, false, connected);
+ } catch(AblyException e) {
+ if(listener != null) {
+ listener.onError(e.errorInfo);
+ }
+ return;
+ }
+
+ msg.encode(null);
+ synchronized(channel) {
+ switch(channel.state) {
+ case initialized:
+ channel.attach();
+ case attaching:
+ QueuedPresence queued = new QueuedPresence(msg, listener);
+ pendingPresence.put(clientId, queued);
+ break;
+ case attached:
+ ProtocolMessage message = new ProtocolMessage(ProtocolMessage.Action.presence, channel.name);
+ message.presence = new PresenceMessage[] { msg };
+ ConnectionManager connectionManager = ably.connection.connectionManager;
+ connectionManager.send(message, ably.options.queueMessages, listener);
+ break;
+ default:
+ throw AblyException.fromErrorInfo(new ErrorInfo("Unable to enter presence channel in detached or failed state", 400, 91001));
+ }
+ }
+ }
+
+ /************************************
+ * history
+ ************************************/
+
+ /**
+ * Obtain recent history for this channel using the REST API.
+ * The history provided relates to all clients of this application,
+ * not just this instance.
+ * @param params: the request params. See the Ably REST API
+ * documentation for more details.
+ * @return: an array of Messgaes for this Channel.
+ * @throws AblyException
+ */
+ public PaginatedResult history(Param[] params) throws AblyException {
+ return historyImpl(params).sync();
+ }
+
+ public void historyAsync(Param[] params, Callback> callback) {
+ historyImpl(params).async(callback);
+ }
+
+ private BasePaginatedQuery.ResultRequest historyImpl(Param[] params) {
+ try {
+ params = Channel.replacePlaceholderParams(channel, params);
+ } catch (AblyException e) {
+ return new BasePaginatedQuery.ResultRequest.Failed(e);
+ }
+
+ AblyRealtime ably = channel.ably;
+ HttpCore.BodyHandler bodyHandler = PresenceSerializer.getPresenceResponseHandler(channel.options);
+ return new BasePaginatedQuery(ably.http, channel.basePath + "/presence/history", HttpUtils.defaultAcceptHeaders(ably.options.useBinaryProtocol), params, bodyHandler).get();
+ }
+
+ /**
+ * internal
+ *
+ */
+ private static class QueuedPresence {
+ public PresenceMessage msg;
+ public CompletionListener listener;
+ QueuedPresence(PresenceMessage msg, CompletionListener listener) { this.msg = msg; this.listener = listener; }
+ }
+
+ private final Map pendingPresence = new HashMap();
+
+ private void sendQueuedMessages() {
+ Log.v(TAG, "sendQueuedMessages()");
+ AblyRealtime ably = channel.ably;
+ boolean queueMessages = ably.options.queueMessages;
+ ConnectionManager connectionManager = ably.connection.connectionManager;
+ int count = pendingPresence.size();
+ if(count == 0)
+ return;
+
+ ProtocolMessage message = new ProtocolMessage(ProtocolMessage.Action.presence, channel.name);
+ Iterator allQueued = pendingPresence.values().iterator();
+ PresenceMessage[] presenceMessages = message.presence = new PresenceMessage[count];
+ CompletionListener listener;
+
+ if(count == 1) {
+ QueuedPresence queued = allQueued.next();
+ presenceMessages[0] = queued.msg;
+ listener = queued.listener;
+ } else {
+ int idx = 0;
+ CompletionListener.Multicaster mListener = new CompletionListener.Multicaster();
+ while(allQueued.hasNext()) {
+ QueuedPresence queued = allQueued.next();
+ presenceMessages[idx++] = queued.msg;
+ if(queued.listener != null)
+ mListener.add(queued.listener);
+ }
+ listener = mListener.isEmpty() ? null : mListener;
+ }
+ pendingPresence.clear();
+ try {
+ connectionManager.send(message, queueMessages, listener);
+ } catch(AblyException e) {
+ Log.e(TAG, "sendQueuedMessages(): Unexpected exception sending message", e);
+ if(listener != null)
+ listener.onError(e.errorInfo);
+ }
+ }
+
+ private void failQueuedMessages(ErrorInfo reason) {
+ Log.v(TAG, "failQueuedMessages()");
+ for(QueuedPresence msg : pendingPresence.values())
+ if(msg.listener != null)
+ try {
+ msg.listener.onError(reason);
+ } catch(Throwable t) {
+ Log.e(TAG, "failQueuedMessages(): Unexpected exception calling listener", t);
+ }
+ pendingPresence.clear();
+ }
+
+
+ /************************************
+ * attach / detach
+ ************************************/
+
+ void setAttached(boolean hasPresence) {
+ /* Start sync, if hasPresence is not set end sync immediately dropping all the current presence members */
+ presence.startSync();
+ syncAsResultOfAttach = true;
+ if (!hasPresence) {
+ /*
+ * RTP19a If the PresenceMap has existing members when an ATTACHED message is received without a
+ * HAS_PRESENCE flag, the client library should emit a LEAVE event for each existing member ...
+ */
+ endSyncAndEmitLeaves();
+ }
+ sendQueuedMessages();
+ }
+
+ void setDetached(ErrorInfo reason) {
+ /* Interrupt get() call if needed */
+ synchronized (presence) {
+ presence.notifyAll();
+ }
+
+ /**
+ * (RTP5a) If the channel enters the DETACHED or FAILED state then all queued presence
+ * messages will fail immediately, and the PresenceMap and internal PresenceMap is cleared.
+ * The latter ensures members are not automatically re-entered if the Channel later becomes attached
+ */
+ failQueuedMessages(reason);
+ presence.clear();
+ internalPresence.clear();
+ }
+
+ void setSuspended(ErrorInfo reason) {
+ /* Interrupt get() call if needed */
+ synchronized (presence) {
+ presence.notifyAll();
+ }
+
+ /*
+ * (RTP5f) If the channel enters the SUSPENDED state then all queued presence messages will fail
+ * immediately, and the PresenceMap is maintained
+ */
+ failQueuedMessages(reason);
+ }
+
+ /**
+ * A class encapsulating a map of the members of this presence channel,
+ * indexed by a String key that is a combination of connectionId and clientId.
+ * This map synchronises the membership of the presence set by handling
+ * sync messages from the service. Since sync messages can be out-of-order -
+ * eg an enter sync event being received after that member has in fact left -
+ * this map keeps "witness" entries, with absent Action, to remember the
+ * fact that a leave event has been seen for a member. These entries are
+ * cleared once the last set of updates of a sync sequence have been received.
+ *
+ */
+ private class PresenceMap {
+
+ /**
+ * Wait for sync to be complete. If we are in attaching state wait for initial sync to
+ * complete as well. Return false if wait was interrupted because channel transitioned to
+ * state other than attached or attaching
+ */
+ synchronized void waitForSync() throws AblyException, InterruptedException {
+ boolean syncIsComplete = false; /* temporary variable to avoid potential race conditions */
+ while (channel.state == ChannelState.attaching) {
+ wait();
+ }
+ if (channel.state == ChannelState.attached) {
+ while (!(syncIsComplete = (!syncInProgress && syncComplete))) {
+ wait();
+ }
+ }
+
+ /* invalid channel state */
+ int errorCode;
+ String errorMessage;
+
+ if (channel.state == ChannelState.suspended) {
+ /* (RTP11d) If the Channel is in the SUSPENDED state then the get function will by default,
+ * or if waitForSync is set to true, result in an error with code 91005 and a message stating
+ * that the presence state is out of sync due to the channel being in a SUSPENDED state */
+ errorCode = 91005;
+ errorMessage = String.format("Channel %s: presence state is out of sync due to the channel being in a SUSPENDED state", channel.name);
+ } else if(syncIsComplete) {
+ return;
+ } else {
+ errorCode = 90001;
+ errorMessage = String.format("Channel %s: cannot get presence state because channel is in invalid state", channel.name);
+ }
+ Log.v(TAG, errorMessage);
+ throw AblyException.fromErrorInfo(new ErrorInfo(errorMessage, errorCode));
+ }
+
+ synchronized Collection get(Param[] params) throws AblyException, InterruptedException {
+ boolean waitForSync = true;
+ String clientId = null;
+ String connectionId = null;
+
+ for (Param param: params) {
+ switch (param.key) {
+ case GET_WAITFORSYNC:
+ waitForSync = Boolean.valueOf(param.value);
+ break;
+ case GET_CLIENTID:
+ clientId = param.value;
+ break;
+ case GET_CONNECTIONID:
+ connectionId = param.value;
+ break;
+ }
+ }
+
+ HashSet result = new HashSet<>();
+ if (waitForSync)
+ waitForSync();
+
+ for (Map.Entry entry: members.entrySet()) {
+ PresenceMessage member = entry.getValue();
+ if ((clientId == null || member.clientId.equals(clientId)) &&
+ (connectionId == null || member.connectionId.equals(connectionId)))
+ result.add(member);
+ }
+
+ return result;
+ }
+
+ /**
+ * Add or update the presence state for a member
+ * @param item
+ * @return true if the given message represents a change;
+ * false if the message is already superseded
+ */
+ synchronized boolean put(PresenceMessage item) {
+ String key = item.memberKey();
+ /* we've seen this member, so do not remove it at the end of sync */
+ if(residualMembers != null)
+ residualMembers.remove(key);
+
+ /* check if there is a newer existing member (or absent witness) */
+ if (hasNewerItem(key, item))
+ return false;
+
+ members.put(key, item);
+ return true;
+ }
+
+ /**
+ * Determine if there is a newer item already in the map
+ * @param key key used to search the item in the map
+ * @param item new presence message to be added
+ * @return true if there is a newer item
+ */
+ synchronized boolean hasNewerItem(String key, PresenceMessage item) {
+ PresenceMessage existingItem = members.get(key);
+ if(existingItem == null)
+ return false;
+
+ /*
+ * (RTP2b1) If either presence message has a connectionId which is not an initial substring
+ * of its id, compare them by timestamp numerically. (This will be the case when one of them
+ * is a 'synthesized leave' event sent by realtime to indicate a connection disconnected
+ * unexpectedly 15s ago. Such messages will have an id that does not correspond to its
+ * connectionId, as it wasn't actually published by that connection
+ */
+ if(item.connectionId != null && existingItem.connectionId != null &&
+ (!item.id.startsWith(item.connectionId) || !existingItem.id.startsWith(existingItem.connectionId)))
+ return existingItem.timestamp >= item.timestamp;
+
+ /*
+ * (RTP2b2) Else split the id of both presence messages (which will be of the form
+ * connid:msgSerial:index, e.g. aaaaaa:0:0) on the separator :, and parse the latter two as
+ * integers. Compare them first by msgSerial numerically, then (if @msgSerial@s are equal) by
+ * index numerically, larger being newer in both cases
+ */
+ String[] itemComponents = item.id.split(":", 3);
+ String[] existingItemComponents = existingItem.id.split(":", 3);
+
+ if(itemComponents.length < 3 || existingItemComponents.length < 3)
+ return false;
+
+ try {
+ long messageSerial = Long.valueOf(itemComponents[1]);
+ long messageIndex = Long.valueOf(itemComponents[2]);
+ long existingMessageSerial = Long.valueOf(existingItemComponents[1]);
+ long existingMessageIndex = Long.valueOf(existingItemComponents[2]);
+
+ return existingMessageSerial > messageSerial ||
+ (existingMessageSerial == messageSerial && existingMessageIndex >= messageIndex);
+ }
+ catch(NumberFormatException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Get all members based on the current state (even if sync is in progress)
+ * @return
+ */
+ synchronized Collection values() {
+ try { return values(false); } catch (InterruptedException|AblyException e) { return null; }
+ }
+
+ /**
+ * Get all members, optionally waiting if a sync is in progress.
+ * @param wait
+ * @return
+ * @throws InterruptedException
+ */
+ synchronized Collection values(boolean wait) throws AblyException, InterruptedException {
+ Set result = new HashSet();
+ if(wait)
+ waitForSync();
+ result.addAll(members.values());
+ for(Iterator it = result.iterator(); it.hasNext();) {
+ PresenceMessage entry = it.next();
+ if(entry.action == PresenceMessage.Action.absent) {
+ it.remove();
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Remove a member.
+ * @param item
+ * @return
+ */
+ synchronized boolean remove(PresenceMessage item) {
+ String key = item.memberKey();
+ if (hasNewerItem(key, item))
+ return false;
+ PresenceMessage existingItem = members.remove(key);
+ if(existingItem != null && existingItem.action == PresenceMessage.Action.absent)
+ return false;
+ return true;
+ }
+
+ /**
+ * Start a sync sequence.
+ * Note that this is called each time a sync message is received that is not
+ * the last.
+ */
+ synchronized void startSync() {
+ Log.v(TAG, "startSync(); channel = " + channel.name + "; syncInProgress = " + syncInProgress);
+ /* we might be called multiple times while a sync is in progress */
+ if(!syncInProgress) {
+ residualMembers = new HashSet(members.keySet());
+ syncInProgress = true;
+ }
+ }
+
+ /**
+ * Finish a sync sequence. Returns "residual" items that were removed as a part of a sync
+ */
+ synchronized List endSync() {
+ Log.v(TAG, "endSync(); channel = " + channel.name + "; syncInProgress = " + syncInProgress);
+ ArrayList removedEntries = new ArrayList<>();
+ if(syncInProgress) {
+ /* we can now strip out the absent members, as we have
+ * received all of the out-of-order sync messages */
+ for(Iterator> it = members.entrySet().iterator(); it.hasNext();) {
+ Map.Entry entry = it.next();
+ if(entry.getValue().action == PresenceMessage.Action.absent) {
+ it.remove();
+ }
+ }
+ /* any members that were present at the start of the sync,
+ * and have not been seen in sync, can be removed */
+ for(String itemKey: residualMembers) {
+ /* clone presence message as it still can be in the internal presence map */
+ removedEntries.add((PresenceMessage)members.get(itemKey).clone());
+ members.remove(itemKey);
+ }
+ residualMembers = null;
+
+ /* finish, notifying any waiters */
+ syncInProgress = false;
+ }
+ syncComplete = true;
+ notifyAll();
+ return removedEntries;
+ }
+
+ /**
+ * Clear all entries
+ */
+ synchronized void clear() {
+ members.clear();
+ if(residualMembers != null)
+ residualMembers.clear();
+ }
+
+ private boolean syncInProgress;
+ private Collection residualMembers;
+ private final HashMap members = new HashMap();
+ }
+
+ private final PresenceMap presence = new PresenceMap();
+ private final PresenceMap internalPresence = new PresenceMap();
+
+ /************************************
+ * general
+ ************************************/
+
+ Presence(Channel channel) {
+ this.channel = channel;
+ }
+
+ private static final String TAG = Channel.class.getName();
+
+ private final Channel channel;
+
+ /* channel serial if sync is in progress */
+ private String currentSyncChannelSerial;
+ /* Sync in progress is a result of attach operation */
+ private boolean syncAsResultOfAttach;
+
+ /**
+ * (RTP13) Presence#syncComplete returns true if the initial SYNC operation has completed for
+ * the members present on the channel
+ */
+ public boolean syncComplete;
}
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 a23c463e5..b4d91b592 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
@@ -37,3423 +37,3424 @@
import io.ably.lib.test.common.ParameterizedTest;
import io.ably.lib.test.util.MockWebsocketFactory;
import io.ably.lib.transport.ConnectionManager;
+import io.ably.lib.transport.Defaults;
import io.ably.lib.types.PresenceMessage.Action;
+import io.ably.lib.util.Log;
public class RealtimePresenceTest extends ParameterizedTest {
- private static final String testMessagesEncodingFile = "ably-common/test-resources/presence-messages-encoding.json";
- private static final String testClientId1 = "testClientId1";
- private static final String testClientId2 = "testClientId2";
- private Auth.TokenDetails token1;
- private Auth.TokenDetails token2;
- private Auth.TokenDetails wildcardToken;
-
- private static PresenceMessage contains(PresenceMessage[] messages, String clientId) {
- for(PresenceMessage message : messages)
- if(clientId.equals(message.clientId))
- return message;
- return null;
- }
-
- private PresenceMessage contains(PresenceMessage[] messages, String clientId, PresenceMessage.Action action) {
- for(PresenceMessage message : messages)
- if(clientId.equals(message.clientId) && action == message.action)
- return message;
- return null;
- }
-
- private static String random() {
- return UUID.randomUUID().toString();
- }
-
- private class TestChannel {
- TestChannel() {
- try {
- ClientOptions opts = createOptions(testVars.keys[0].keyStr);
- rest = new AblyRest(opts);
- restChannel = rest.channels.get(channelName);
- realtime = new AblyRealtime(opts);
- realtimeChannel = realtime.channels.get(channelName);
- realtimeChannel.attach();
- (new ChannelWaiter(realtimeChannel)).waitFor(ChannelState.attached);
- } catch(AblyException ae) {}
- }
-
- void dispose() {
- if(realtime != null)
- realtime.close();
- }
-
- String channelName = random();
- AblyRest rest;
- AblyRealtime realtime;
- io.ably.lib.rest.Channel restChannel;
- io.ably.lib.realtime.Channel realtimeChannel;
- }
-
- @Rule
- public Timeout testTimeout = Timeout.seconds(300);
-
- @Before
- public void setUpBefore() throws Exception {
- /* create tokens for specific clientIds */
- ClientOptions opts = createOptions(testVars.keys[0].keyStr);
- AblyRest rest = new AblyRest(opts);
- token1 = rest.auth.requestToken(new TokenParams() {{ clientId = testClientId1; }}, null);
- token2 = rest.auth.requestToken(new TokenParams() {{ clientId = testClientId2; }}, null);
- wildcardToken = rest.auth.requestToken(new TokenParams() {{ clientId = "*"; }}, null);
- }
-
- /**
- * Attach to channel, enter presence channel and await entered event
- */
- @Test
- public void enter_simple() {
- 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);
-
- /* let client1 enter the channel and wait for the entered event to be delivered */
- CompletionWaiter enterComplete = new CompletionWaiter();
- String enterString = "Test data (enter_simple)";
- client1Channel.presence.enter(enterString, enterComplete);
- presenceWaiter.waitFor(testClientId1, Action.enter);
- assertNotNull(presenceWaiter.contains(testClientId1, Action.enter));
- assertEquals(presenceWaiter.receivedMessages.get(0).data, enterString);
-
- /* 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 channel without prior attach and await entered event
- */
- @Test
- public void enter_before_attach() {
- 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 */
- Channel client1Channel = clientAbly1.channels.get(testChannel.channelName);
-
- /* let client1 enter the channel and wait for the entered event to be delivered */
- CompletionWaiter enterComplete = new CompletionWaiter();
- String enterString = "Test data (enter_before_attach)";
- client1Channel.presence.enter(enterString, enterComplete);
- presenceWaiter.waitFor(testClientId1, Action.enter);
- PresenceMessage expectedPresent = presenceWaiter.contains(testClientId1, clientAbly1.connection.id, Action.enter);
- assertNotNull(expectedPresent);
- assertEquals(expectedPresent.data, enterString);
-
- /* 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 channel without prior connect and await entered event
- */
- @Test
- public void enter_before_connect() {
- 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);
-
- /* get channel */
- Channel client1Channel = clientAbly1.channels.get(testChannel.channelName);
-
- /* let client1 enter the channel and wait for the entered event to be delivered */
- CompletionWaiter enterComplete = new CompletionWaiter();
- String enterString = "Test data (enter_before_connect)";
- client1Channel.presence.enter(enterString, enterComplete);
- presenceWaiter.waitFor(testClientId1, Action.enter);
- PresenceMessage expectedPresent = presenceWaiter.contains(testClientId1, clientAbly1.connection.id, Action.enter);
- assertNotNull(expectedPresent);
- assertEquals(expectedPresent.data, enterString);
-
- /* 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, then leave, presence channel and await leave event
- * Verify that the item is removed from the presence map (RTP2e)
- */
- @Test
- public void enter_leave_simple() {
- 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);
-
- /* get channel */
- Channel client1Channel = clientAbly1.channels.get(testChannel.channelName);
-
- /* let client1 enter the channel and wait for the entered event to be delivered */
- CompletionWaiter enterComplete = new CompletionWaiter();
- String enterString = "Test data (enter_before_connect)";
- client1Channel.presence.enter(enterString, enterComplete);
- presenceWaiter.waitFor(testClientId1, Action.enter);
- assertNotNull(presenceWaiter.contains(testClientId1, Action.enter));
- assertEquals(presenceWaiter.receivedMessages.get(0).data, enterString);
- presenceWaiter.reset();
-
- /* verify enter callback called on completion */
- enterComplete.waitFor();
- assertTrue("Verify enter callback called on completion", enterComplete.success);
-
- /* let client1 leave the channel and wait for the leave event to be delivered */
- CompletionWaiter leaveComplete = new CompletionWaiter();
- String leaveString = "Test data (enter_before_connect), leaving";
- client1Channel.presence.leave(leaveString, leaveComplete);
- presenceWaiter.waitFor(testClientId1, Action.leave);
- PresenceMessage expectedLeft = presenceWaiter.contains(testClientId1, clientAbly1.connection.id, Action.leave);
- assertNotNull(expectedLeft);
- assertEquals(expectedLeft.data, leaveString);
-
- /* verify leave callback called on completion */
- leaveComplete.waitFor();
- assertTrue("Verify leave callback called on completion", leaveComplete.success);
-
- assertEquals("Verify item is removed from the presence map", client1Channel.presence.get(testClientId1, false).length, 0);
-
- } catch(AblyException e) {
- e.printStackTrace();
- fail("Unexpected exception running test: " + e.getMessage());
- } finally {
- if(clientAbly1 != null)
- clientAbly1.close();
- if(testChannel != null)
- testChannel.dispose();
- }
- }
-
- /**
- * Enter, then enter again, expecting update event
- */
- @Test
- public void enter_enter_simple() {
- 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);
-
- /* get channel */
- Channel client1Channel = clientAbly1.channels.get(testChannel.channelName);
-
- /* let client1 enter the channel and wait for the entered event to be delivered */
- CompletionWaiter enterComplete = new CompletionWaiter();
- String enterString = "Test data (enter_enter_simple)";
- client1Channel.presence.enter(enterString, enterComplete);
- presenceWaiter.waitFor(testClientId1, Action.enter);
- assertNotNull(presenceWaiter.contains(testClientId1, Action.enter));
- assertEquals(presenceWaiter.receivedMessages.get(0).data, enterString);
- presenceWaiter.reset();
-
- /* verify enter callback called on completion */
- enterComplete.waitFor();
- assertTrue("Verify enter callback called on completion", enterComplete.success);
-
- /* let client1 reenter the channel and wait for the update event to be delivered */
- CompletionWaiter reenterComplete = new CompletionWaiter();
- String reenterString = "Test data (enter_enter_simple), reentering";
- client1Channel.presence.enter(reenterString, reenterComplete);
- presenceWaiter.waitFor(testClientId1, Action.update);
- assertNotNull(presenceWaiter.contains(testClientId1, Action.update));
- assertEquals(presenceWaiter.receivedMessages.get(0).data, reenterString);
-
- /* verify reenter callback called on completion */
- reenterComplete.waitFor();
- assertTrue("Verify reenter callback called on completion", reenterComplete.success);
-
- /* let client1 leave the channel and wait for the leave event to be delivered */
- CompletionWaiter leaveComplete = new CompletionWaiter();
- String leaveString = "Test data (enter_enter_simple), leaving";
- client1Channel.presence.leave(leaveString, leaveComplete);
- presenceWaiter.waitFor(testClientId1, Action.leave);
- PresenceMessage expectedLeft = presenceWaiter.contains(testClientId1, clientAbly1.connection.id, Action.leave);
- assertNotNull(expectedLeft);
- assertEquals(expectedLeft.data, leaveString);
-
- /* 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, then update, expecting update event
- */
- @Test
- public void enter_update_simple() {
- 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);
-
- /* get channel */
- Channel client1Channel = clientAbly1.channels.get(testChannel.channelName);
-
- /* let client1 enter the channel and wait for the entered event to be delivered */
- CompletionWaiter enterComplete = new CompletionWaiter();
- String enterString = "Test data (enter_update_simple)";
- client1Channel.presence.enter(enterString, enterComplete);
- presenceWaiter.waitFor(testClientId1, Action.enter);
- assertNotNull(presenceWaiter.contains(testClientId1, Action.enter));
- assertEquals(presenceWaiter.receivedMessages.get(0).data, enterString);
- presenceWaiter.reset();
-
- /* verify enter callback called on completion */
- enterComplete.waitFor();
- assertTrue("Verify enter callback called on completion", enterComplete.success);
-
- /* let client1 update the channel and wait for the update event to be delivered */
- CompletionWaiter updateComplete = new CompletionWaiter();
- String reenterString = "Test data (enter_update_simple), updating";
- client1Channel.presence.update(reenterString, updateComplete);
- presenceWaiter.waitFor(testClientId1, Action.update);
- assertNotNull(presenceWaiter.contains(testClientId1, Action.update));
- assertEquals(presenceWaiter.receivedMessages.get(0).data, reenterString);
-
- /* verify reenter callback called on completion */
- updateComplete.waitFor();
- assertTrue("Verify reenter callback called on completion", updateComplete.success);
-
- /* let client1 leave the channel and wait for the leave event to be delivered */
- CompletionWaiter leaveComplete = new CompletionWaiter();
- String leaveString = "Test data (enter_update_simple), leaving";
- client1Channel.presence.leave(leaveString, leaveComplete);
- presenceWaiter.waitFor(testClientId1, Action.leave);
- PresenceMessage expectedLeft = presenceWaiter.contains(testClientId1, clientAbly1.connection.id, Action.leave);
- assertNotNull(expectedLeft);
- assertEquals(expectedLeft.data, leaveString);
-
- /* 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, then update with null data, expecting previous data to be superseded
- */
- @Test
- public void enter_update_null() {
- 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);
- client1Opts.useBinaryProtocol = true;
- clientAbly1 = new AblyRealtime(client1Opts);
-
- /* get channel */
- Channel client1Channel = clientAbly1.channels.get(testChannel.channelName);
-
- /* let client1 enter the channel and wait for the entered event to be delivered */
- CompletionWaiter enterComplete = new CompletionWaiter();
- String enterString = "Test data (enter_update_null)";
- client1Channel.presence.enter(enterString, enterComplete);
- presenceWaiter.waitFor(testClientId1, Action.enter);
- assertNotNull(presenceWaiter.contains(testClientId1, Action.enter));
- assertEquals(presenceWaiter.receivedMessages.get(0).data, enterString);
- presenceWaiter.reset();
-
- /* verify enter callback called on completion */
- enterComplete.waitFor();
- assertTrue("Verify enter callback called on completion", enterComplete.success);
-
- /* let client1 update the channel and wait for the update event to be delivered */
- CompletionWaiter updateComplete = new CompletionWaiter();
- String updateString = null;
- client1Channel.presence.update(updateString, updateComplete);
- presenceWaiter.waitFor(testClientId1, Action.update);
- assertNotNull(presenceWaiter.contains(testClientId1, Action.update));
- assertEquals(presenceWaiter.receivedMessages.get(0).data, updateString);
-
- /* verify reenter callback called on completion */
- updateComplete.waitFor();
- assertTrue("Verify reenter callback called on completion", updateComplete.success);
-
- /* let client1 leave the channel and wait for the leave event to be delivered */
- CompletionWaiter leaveComplete = new CompletionWaiter();
- String leaveString = "Test data (enter_update_null), leaving";
- client1Channel.presence.leave(leaveString, leaveComplete);
- presenceWaiter.waitFor(testClientId1, Action.leave);
- PresenceMessage expectedLeft = presenceWaiter.contains(testClientId1, clientAbly1.connection.id, Action.leave);
- assertNotNull(expectedLeft);
- assertEquals(expectedLeft.data, leaveString);
-
- /* 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();
- }
- }
-
- /**
- * Update without having first entered, expecting enter event
- */
- @Test
- public void update_noenter() {
- 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);
-
- /* get channel */
- Channel client1Channel = clientAbly1.channels.get(testChannel.channelName);
-
- /* let client1 enter the channel and wait for the entered event to be delivered */
- CompletionWaiter enterComplete = new CompletionWaiter();
- String updateString = "Test data (update_noenter)";
- client1Channel.presence.update(updateString, enterComplete);
- presenceWaiter.waitFor(testClientId1, Action.enter);
- assertNotNull(presenceWaiter.contains(testClientId1, Action.enter));
- assertEquals(presenceWaiter.receivedMessages.get(0).data, updateString);
- presenceWaiter.reset();
-
- /* verify enter callback called on completion */
- enterComplete.waitFor();
- assertTrue("Verify enter callback called on completion", enterComplete.success);
-
- /* let client1 leave the channel and wait for the leave event to be delivered */
- CompletionWaiter leaveComplete = new CompletionWaiter();
- String leaveString = "Test data (update_noenter), leaving";
- client1Channel.presence.leave(leaveString, leaveComplete);
- presenceWaiter.waitFor(testClientId1, Action.leave);
- PresenceMessage expectedLeft = presenceWaiter.contains(testClientId1, clientAbly1.connection.id, Action.leave);
- assertNotNull(expectedLeft);
- assertEquals(expectedLeft.data, leaveString);
-
- /* 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, then leave (with no data) and await leave event,
- * expecting enter data to be in leave event
- */
- @Test
- public void enter_leave_nodata() {
- 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);
-
- /* get channel */
- Channel client1Channel = clientAbly1.channels.get(testChannel.channelName);
-
- /* let client1 enter the channel and wait for the entered event to be delivered */
- CompletionWaiter enterComplete = new CompletionWaiter();
- String enterString = "Test data (enter_leave_nodata)";
- client1Channel.presence.enter(enterString, enterComplete);
- presenceWaiter.waitFor(testClientId1, Action.enter);
- assertNotNull(presenceWaiter.contains(testClientId1, Action.enter));
- assertEquals(presenceWaiter.receivedMessages.get(0).data, enterString);
- presenceWaiter.reset();
-
- /* verify enter callback called on completion */
- enterComplete.waitFor();
- assertTrue("Verify enter callback called on completion", enterComplete.success);
-
- /* let client1 leave the channel and wait for the leave event to be delivered */
- CompletionWaiter leaveComplete = new CompletionWaiter();
- client1Channel.presence.leave(leaveComplete);
- presenceWaiter.waitFor(testClientId1, Action.leave);
- assertNotNull(presenceWaiter.contains(testClientId1, Action.leave));
- assertEquals(presenceWaiter.receivedMessages.get(0).data, enterString);
-
- /* verify leave callback called on completion */
- leaveComplete.waitFor();
- assertTrue("Verify leave callback called on completion", leaveComplete.success);
-
- } catch(AblyException e) {
- e.printStackTrace();
- } finally {
- if(clientAbly1 != null)
- clientAbly1.close();
- if(testChannel != null)
- testChannel.dispose();
- }
- }
-
- /**
- * Attach to channel, enter presence channel and get presence using realtime get()
- */
- @Test
- public void realtime_get_simple() {
- 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);
-
- /* let client1 enter the channel and wait for the success callback */
- CompletionWaiter enterComplete = new CompletionWaiter();
- String enterString = "Test data (get_simple)";
- client1Channel.presence.enter(enterString, enterComplete);
- enterComplete.waitFor();
- assertTrue("Verify enter callback called on completion", enterComplete.success);
-
- /* get presence set and verify client present */
- presenceWaiter.waitFor(testClientId1);
- PresenceMessage[] presences = testChannel.realtimeChannel.presence.get(false);
- PresenceMessage expectedPresent = contains(presences, testClientId1, Action.present);
- assertNotNull("Verify expected client is in presence set", expectedPresent);
- assertEquals(expectedPresent.data, enterString);
-
- } catch(AblyException e) {
- e.printStackTrace();
- fail("Unexpected exception running test: " + e.getMessage());
- } finally {
- if(clientAbly1 != null)
- clientAbly1.close();
- if(testChannel != null)
- testChannel.dispose();
- }
- }
-
- /**
- * Attach to channel, enter+leave presence channel and get presence with realtime get()
- */
- @Test
- public void realtime_get_leave() {
- 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);
-
- /* let client1 enter the channel and wait for the success callback */
- CompletionWaiter enterComplete = new CompletionWaiter();
- client1Channel.presence.enter("Test data (get_leave)", enterComplete);
- enterComplete.waitFor();
- assertTrue("Verify enter callback called on completion", enterComplete.success);
-
- /* let client1 leave the channel; wait for the success callback and event */
- CompletionWaiter leaveComplete = new CompletionWaiter();
- client1Channel.presence.leave(leaveComplete);
- leaveComplete.waitFor();
- assertTrue("Verify leave callback called on completion", leaveComplete.success);
- presenceWaiter.waitFor(testClientId1, Action.leave);
- assertTrue("Verify leave callback called on completion", leaveComplete.success);
-
- /* get presence set and verify client absent */
- PresenceMessage[] presences = testChannel.realtimeChannel.presence.get(false);
- assertNull("Verify expected client is in presence set", contains(presences, testClientId1));
-
- } catch(AblyException e) {
- e.printStackTrace();
- fail("Unexpected exception running test: " + e.getMessage());
- } finally {
- if(clientAbly1 != null)
- clientAbly1.close();
- if(testChannel != null)
- testChannel.dispose();
- }
- }
-
- /**
- * Attach to channel, enter presence channel, then initiate second
- * connection, seeing existing member in message subsequent to second attach response
- */
- @Test
- public void attach_enter_simple() {
- AblyRealtime clientAbly1 = null;
- AblyRealtime clientAbly2 = null;
- TestChannel testChannel = new TestChannel();
- try {
- /* subscribe for presence events in the anonymous connection */
- 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);
-
- /* let client1 enter the channel and wait for the success callback */
- CompletionWaiter enterComplete = new CompletionWaiter();
- String enterString = "Test data (attach_enter)";
- client1Channel.presence.enter(enterString, enterComplete);
- enterComplete.waitFor();
- assertTrue("Verify enter callback called on completion", enterComplete.success);
-
- /* set up a second connection with different clientId */
- ClientOptions client2Opts = new ClientOptions() {{
- tokenDetails = token2;
- clientId = testClientId2;
- }};
- fillInOptions(client2Opts);
- clientAbly2 = new AblyRealtime(client2Opts);
-
- /* wait until connected */
- (new ConnectionWaiter(clientAbly2.connection)).waitFor(ConnectionState.connected);
- assertEquals("Verify connected state reached", clientAbly2.connection.state, ConnectionState.connected);
-
- /* get channel and subscribe to presence */
- Channel client2Channel = clientAbly2.channels.get(testChannel.channelName);
- PresenceWaiter client2Waiter = new PresenceWaiter(client2Channel);
- client2Waiter.waitFor(testClientId1, Action.present);
-
- /* get presence set and verify client present */
- PresenceMessage[] presences = client2Channel.presence.get(false);
- PresenceMessage expectedPresent = contains(presences, testClientId1, Action.present);
- assertNotNull("Verify expected client is in presence set", expectedPresent);
- assertEquals(expectedPresent.data, enterString);
-
- } catch(AblyException e) {
- e.printStackTrace();
- fail("Unexpected exception running test: " + e.getMessage());
- } finally {
- if(clientAbly1 != null)
- clientAbly1.close();
- if(clientAbly2 != null)
- clientAbly2.close();
- if(testChannel != null)
- testChannel.dispose();
- }
- }
-
- /**
- * Attach to channel, enter presence channel with large number of clientIds,
- * then initiate second connection, seeing existing members in sync subsequent
- * to second attach response
- *
- * Test RTP4
- */
- @Test
- public void attach_enter_multiple() {
- AblyRealtime clientAbly1 = null;
- AblyRealtime clientAbly2 = null;
- TestChannel testChannel = new TestChannel();
- int clientCount = 250;
- try {
- /* subscribe for presence events in the anonymous connection */
- new PresenceWaiter(testChannel.realtimeChannel);
- /* set up a connection with specific clientId */
- 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);
-
- /* let client1 enter the channel for multiple clients and wait for the success callback */
- CompletionSet enterComplete = new CompletionSet();
- for(int i = 0; i < clientCount; i++) {
- client1Channel.presence.enterClient("client" + i, "Test data (attach_enter_multiple) " + i, enterComplete.add());
- }
- enterComplete.waitFor();
- assertTrue("Verify enter callback called on completion", enterComplete.pending.isEmpty());
- assertTrue("Verify no enter errors", enterComplete.errors.isEmpty());
-
- /* set up a second connection with different clientId */
- ClientOptions client2Opts = new ClientOptions() {{
- tokenDetails = token2;
- clientId = testClientId2;
- }};
- fillInOptions(client2Opts);
- clientAbly2 = new AblyRealtime(client2Opts);
-
- /* wait until connected */
- (new ConnectionWaiter(clientAbly2.connection)).waitFor(ConnectionState.connected);
- assertEquals("Verify connected state reached", clientAbly2.connection.state, ConnectionState.connected);
-
- /* get channel */
- Channel client2Channel = clientAbly2.channels.get(testChannel.channelName);
- client2Channel.attach();
- (new ChannelWaiter(client2Channel)).waitFor(ChannelState.attached);
- assertEquals("Verify attached state reached", client2Channel.state, ChannelState.attached);
-
- /* get presence set and verify client present */
- HashMap memberIndex = new HashMap();
- PresenceMessage[] members = client2Channel.presence.get(true);
- assertNotNull("Expected non-null messages", members);
- assertEquals("Expected " + clientCount + " messages", members.length, clientCount);
-
- /* index received messages */
- for(PresenceMessage member: members)
- memberIndex.put(member.clientId, member);
-
- /* verify that all clientIds were received */
- assertEquals("Expected " + clientCount + " members", memberIndex.size(), clientCount);
- for(int i = 0; i < clientCount; i++) {
- String clientId = "client" + i;
- assertTrue("Expected client with id " + clientId, memberIndex.containsKey(clientId));
- }
- } catch(AblyException e) {
- e.printStackTrace();
- fail("Unexpected exception running test: " + e.getMessage());
- } finally {
- if(clientAbly1 != null)
- clientAbly1.close();
- if(clientAbly2 != null)
- clientAbly2.close();
- testChannel.dispose();
- }
- }
-
- /**
- * Attach and enter channel on two connections, seeing
- * both members in presence returned by realtime get() */
- @Test
- public void realtime_enter_multiple() {
- AblyRealtime clientAbly1 = null;
- AblyRealtime clientAbly2 = null;
- TestChannel testChannel = new TestChannel();
- try {
- /* subscribe for presence events in the anonymous connection */
- PresenceWaiter waiter = 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);
-
- /* get channel and attach */
- Channel client1Channel = clientAbly1.channels.get(testChannel.channelName);
- CompletionWaiter enter1Complete = new CompletionWaiter();
- String enterString1 = "Test data (enter_multiple, clientId1)";
- client1Channel.presence.enter(enterString1, enter1Complete);
- enter1Complete.waitFor();
- assertTrue("Verify enter callback called on completion", enter1Complete.success);
-
- /* set up a second connection with different clientId */
- ClientOptions client2Opts = new ClientOptions() {{
- tokenDetails = token2;
- clientId = testClientId2;
- }};
- fillInOptions(client2Opts);
- clientAbly2 = new AblyRealtime(client2Opts);
-
- /* get channel and subscribe to presence */
- Channel client2Channel = clientAbly2.channels.get(testChannel.channelName);
- CompletionWaiter enter2Complete = new CompletionWaiter();
- String enterString2 = "Test data (enter_multiple, clientId2)";
- client2Channel.presence.enter(enterString2, enter2Complete);
- enter2Complete.waitFor();
- assertTrue("Verify enter callback called on completion", enter2Complete.success);
-
- /* verify enter events for both clients are received */
- waiter.waitFor(testClientId1, Action.enter);
- waiter.waitFor(testClientId2, Action.enter);
-
- /* get presence set and verify clients present */
- PresenceMessage[] presences = testChannel.realtimeChannel.presence.get(false);
- PresenceMessage expectedPresent1 = contains(presences, testClientId1, Action.present);
- PresenceMessage expectedPresent2 = contains(presences, testClientId2, Action.present);
- assertNotNull("Verify expected clients are in presence set", expectedPresent1);
- assertNotNull("Verify expected clients are in presence set", expectedPresent2);
- assertEquals(expectedPresent1.data, enterString1);
- assertEquals(expectedPresent2.data, enterString2);
-
- } catch(AblyException e) {
- e.printStackTrace();
- fail("Unexpected exception running test: " + e.getMessage());
- } finally {
- if(clientAbly1 != null)
- clientAbly1.close();
- if(clientAbly2 != null)
- clientAbly2.close();
- if(testChannel != null)
- testChannel.dispose();
- }
- }
-
- /**
- * Attach to channel, enter presence channel and get presence using rest get()
- */
- @Test
- public void rest_get_simple() {
- AblyRealtime clientAbly1 = null;
- TestChannel testChannel = new TestChannel();
- try {
- /* subscribe for presence events in the anonymous connection */
- 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);
-
- /* let client1 enter the channel and wait for the success callback */
- CompletionWaiter enterComplete = new CompletionWaiter();
- String enterString = "Test data (get_simple)";
- client1Channel.presence.enter(enterString, enterComplete);
- enterComplete.waitFor();
- assertTrue("Verify enter callback called on completion", enterComplete.success);
-
- /* get presence set and verify client present */
- PresenceMessage[] presences = testChannel.restChannel.presence.get(null).items();
- PresenceMessage expectedPresent = contains(presences, testClientId1, Action.present);
- assertNotNull("Verify expected client is in presence set", expectedPresent);
- assertEquals(expectedPresent.data, enterString);
-
- } catch(AblyException e) {
- e.printStackTrace();
- fail("Unexpected exception running test: " + e.getMessage());
- } finally {
- if(clientAbly1 != null)
- clientAbly1.close();
- if(testChannel != null)
- testChannel.dispose();
- }
- }
-
- /**
- * Attach to channel, enter+leave presence channel and get presence with rest get()
- */
- @Test
- public void rest_get_leave() {
- 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);
-
- /* let client1 enter the channel and wait for the success callback */
- CompletionWaiter enterComplete = new CompletionWaiter();
- client1Channel.presence.enter("Test data (get_leave)", enterComplete);
- enterComplete.waitFor();
- assertTrue("Verify enter callback called on completion", enterComplete.success);
-
- /* let client1 leave the channel; wait for the success callback and event */
- CompletionWaiter leaveComplete = new CompletionWaiter();
- client1Channel.presence.leave(leaveComplete);
- leaveComplete.waitFor();
- assertTrue("Verify leave callback called on completion", leaveComplete.success);
- presenceWaiter.waitFor(testClientId1, Action.leave);
- assertTrue("Verify leave callback called on completion", leaveComplete.success);
-
- /* get presence set and verify client absent */
- PresenceMessage[] presences = testChannel.restChannel.presence.get(null).items();
- assertNull("Verify expected client is in presence set", contains(presences, testClientId1));
-
- } catch(AblyException e) {
- e.printStackTrace();
- fail("Unexpected exception running test: " + e.getMessage());
- } finally {
- if(clientAbly1 != null)
- clientAbly1.close();
- if(testChannel != null)
- testChannel.dispose();
- }
- }
-
- /**
- * Attach and enter channel on two connections, seeing
- * both members in presence returned by rest get() */
- @Test
- public void rest_enter_multiple() {
- AblyRealtime clientAbly1 = null;
- AblyRealtime clientAbly2 = null;
- TestChannel testChannel = new TestChannel();
- try {
- /* subscribe for presence events in the anonymous connection */
- 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);
-
- /* get channel and attach */
- Channel client1Channel = clientAbly1.channels.get(testChannel.channelName);
- CompletionWaiter enter1Complete = new CompletionWaiter();
- String enterString1 = "Test data (enter_multiple, clientId1)";
- client1Channel.presence.enter(enterString1, enter1Complete);
- enter1Complete.waitFor();
- assertTrue("Verify enter callback called on completion", enter1Complete.success);
-
- /* set up a second connection with different clientId */
- ClientOptions client2Opts = new ClientOptions() {{
- tokenDetails = token2;
- clientId = testClientId2;
- }};
- fillInOptions(client2Opts);
- clientAbly2 = new AblyRealtime(client2Opts);
-
- /* get channel and subscribe to presence */
- Channel client2Channel = clientAbly2.channels.get(testChannel.channelName);
- CompletionWaiter enter2Complete = new CompletionWaiter();
- String enterString2 = "Test data (enter_multiple, clientId2)";
- client2Channel.presence.enter(enterString2, enter2Complete);
- enter2Complete.waitFor();
- assertTrue("Verify enter callback called on completion", enter2Complete.success);
-
- /* get presence set and verify client present */
- PresenceMessage[] presences = testChannel.restChannel.presence.get(null).items();
- PresenceMessage expectedPresent1 = contains(presences, testClientId1, Action.present);
- PresenceMessage expectedPresent2 = contains(presences, testClientId2, Action.present);
- assertNotNull("Verify expected clients are in presence set", expectedPresent1);
- assertNotNull("Verify expected clients are in presence set", expectedPresent2);
- assertEquals(expectedPresent1.data, enterString1);
- assertEquals(expectedPresent2.data, enterString2);
-
- } catch(AblyException e) {
- e.printStackTrace();
- fail("Unexpected exception running test: " + e.getMessage());
- } finally {
- if(clientAbly1 != null)
- clientAbly1.close();
- if(clientAbly2 != null)
- clientAbly2.close();
- if(testChannel != null)
- testChannel.dispose();
- }
- }
-
- /**
- * Attach and enter channel multiple times on a single connection,
- * retrieving members using paginated rest get() */
- @Test
- public void rest_paginated_get() {
- AblyRealtime clientAbly1 = null;
- TestChannel testChannel = new TestChannel();
- int clientCount = 30;
- long delay = 100L;
- try {
- /* subscribe for presence events in the anonymous connection */
- new PresenceWaiter(testChannel.realtimeChannel);
- /* set up a connection with specific clientId */
- ClientOptions client1Opts = new ClientOptions() {{
- tokenDetails = wildcardToken;
- }};
- fillInOptions(client1Opts);
- clientAbly1 = new AblyRealtime(client1Opts);
-
- /* get channel and attach */
- Channel client1Channel = clientAbly1.channels.get(testChannel.channelName);
-
- /* enter multiple clients */
- CompletionSet enterComplete = new CompletionSet();
- for(int i = 0; i < clientCount; i++) {
- client1Channel.presence.enterClient("client" + i, "Test data (rest_paginated_get) " + i, enterComplete.add());
- try { Thread.sleep(delay); } catch(InterruptedException e){}
- }
- enterComplete.waitFor();
- assertTrue("Verify enter callback called on completion", enterComplete.errors.isEmpty());
-
- /* get the presence for this channel */
- HashMap memberIndex = new HashMap();
- PaginatedResult members = testChannel.restChannel.presence.get(new Param[] { new Param("limit", "10") });
- assertNotNull("Expected non-null messages", members);
- assertEquals("Expected 10 messages", members.items().length, 10);
-
- /* index received messages */
- for(int i = 0; i < 10; i++) {
- PresenceMessage member = members.items()[i];
- memberIndex.put(member.clientId, member);
- }
-
- /* get next page */
- members = members.next();
- assertNotNull("Expected non-null messages", members);
- assertEquals("Expected 10 messages", members.items().length, 10);
-
- /* index received messages */
- for(int i = 0; i < 10; i++) {
- PresenceMessage member = members.items()[i];
- memberIndex.put(member.clientId, member);
- }
-
- /* get next page */
- members = members.next();
- assertNotNull("Expected non-null messages", members);
- assertEquals("Expected 10 messages", members.items().length, 10);
-
- /* index received messages */
- for(int i = 0; i < 10; i++) {
- PresenceMessage member = members.items()[i];
- memberIndex.put(member.clientId, member);
- }
-
- /* verify there is no next page */
- assertFalse("Expected null next page", members.hasNext());
-
- /* verify that all clientIds were received */
- assertEquals("Expected " + clientCount + " members", memberIndex.size(), clientCount);
- for(int i = 0; i < clientCount; i++) {
- String clientId = "client" + i;
- assertTrue("Expected client with id " + clientId, memberIndex.containsKey(clientId));
- }
- } catch(AblyException e) {
- e.printStackTrace();
- fail("Unexpected exception running test: " + e.getMessage());
- } finally {
- if(clientAbly1 != null)
- clientAbly1.close();
- if(testChannel != null)
- testChannel.dispose();
- }
- }
-
- /**
- * Attach to channel, enter presence channel, disconnect and await leave event
- */
- @Test
- public void disconnect_leave() {
- AblyRealtime clientAbly1 = null;
- TestChannel testChannel = new TestChannel();
- boolean requiresClose = false;
- 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);
- requiresClose = true;
-
- /* get channel */
- Channel client1Channel = clientAbly1.channels.get(testChannel.channelName);
-
- /* let client1 enter the channel and wait for the entered event to be delivered */
- CompletionWaiter enterComplete = new CompletionWaiter();
- String enterString = "Test data (disconnect_leave)";
- client1Channel.presence.enter(enterString, enterComplete);
- presenceWaiter.waitFor(testClientId1, Action.enter);
- PresenceMessage expectedPresent = presenceWaiter.contains(testClientId1, Action.enter);
- assertNotNull(expectedPresent);
- assertEquals(expectedPresent.data, enterString);
-
- /* verify enter callback called on completion */
- enterComplete.waitFor();
- assertTrue("Verify enter callback called on completion", enterComplete.success);
-
- /* close client1 connection and wait for the leave event to be delivered */
- clientAbly1.close();
- requiresClose = false;
- presenceWaiter.waitFor(testClientId1, Action.leave);
- PresenceMessage expectedLeft = presenceWaiter.contains(testClientId1, Action.leave);
- assertNotNull(expectedLeft);
- /* verify leave message contains data that was published with enter */
- assertEquals(expectedLeft.data, enterString);
-
- } catch(AblyException e) {
- e.printStackTrace();
- fail("Unexpected exception running test: " + e.getMessage());
- } finally {
- if(requiresClose)
- clientAbly1.close();
- if(testChannel != null)
- testChannel.dispose();
- }
- }
-
- /**
- *
- * Validates channel removes all subscribers,
- * when {@code Channel#unsubscribe()} with no argument gets called.
- *
- * Validates a client can observe presence messages of other client,
- * when they entered to the same channel and observing client subscribed
- * to multiple actions.
- *
- * Validates a client can observe presence messages of other client,
- * when they entered to the same channel and observing client subscribed
- * to multiple actions.
- *
- * Validates a client can observe presence messages of other client,
- * when they entered to the same channel and observing client subscribed
- * to a single action.
- *
- * Validate {@code Presence#subscribe(...)} will result in the listener not being
- * registered and an error being indicated, when the channel moves to the FAILED
- * state before the operation succeeds
- *
- *
- * Spec: RTP6c
- *
- *
- * @throws AblyException
- */
- @Test
- public void realtime_presence_attach_implicit_subscribe_fail() throws AblyException {
- AblyRealtime ably = null;
- try {
- ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr);
- final AblyRest ablyForToken = new AblyRest(optsForToken);
- final String channelName = "realtime_presence_attach_implicit_subscribe_fail" + testParams.name;
-
- /* get first token */
- Auth.TokenParams tokenParams = new Auth.TokenParams();
- Capability capability = new Capability();
- capability.addResource("otherchannel", "publish");
- tokenParams.capability = capability.toString();
- tokenParams.clientId = testClientId1;
-
- Auth.TokenDetails token = ablyForToken.auth.requestToken(tokenParams, null);
-
- /* get second token */
- Auth.TokenParams tokenParams2 = new Auth.TokenParams();
- Capability capability2 = new Capability();
- capability2.addResource(channelName, "publish");
- capability2.addOperation(channelName, "presence");
- capability2.addOperation(channelName, "subscribe");
- tokenParams2.capability = capability2.toString();
- tokenParams2.clientId = testClientId1;
-
- final Auth.TokenDetails token2 = ablyForToken.auth.requestToken(tokenParams2, null);
- assertNotNull("Expected token value", token2.token);
-
- ClientOptions opts = createOptions(testVars.keys[0].keyStr);
- opts.autoConnect = false;
- opts.tokenDetails = token;
- opts.clientId = testClientId1;
- ably = new AblyRealtime(opts);
-
- final ArrayList presenceMessages = new ArrayList<>();
- Presence.PresenceListener listener = new Presence.PresenceListener() {
- @Override
- public void onPresenceMessage(PresenceMessage message) {
- synchronized (presenceMessages) {
- presenceMessages.add(message);
- presenceMessages.notify();
- }
- }
- };
-
- /* create a channel and subscribe, implicitly initiate attach */
- CompletionWaiter completionWaiter = new CompletionWaiter();
- final Channel channel = ably.channels.get(channelName);
- channel.presence.subscribe(listener, completionWaiter);
-
- ably.connection.connect();
-
- completionWaiter.waitFor(1);
- assertFalse("Verify subscribe failed", completionWaiter.success);
- assertEquals("Verify subscribe failure error status", completionWaiter.error.statusCode, 401);
- assertEquals("Verify failed state reached", channel.state, ChannelState.failed);
-
- try {
- channel.presence.subscribe(new PresenceWaiter(channel));
- fail("Presence.subscribe() shouldn't succeed");
- } catch (AblyException e) {
- assertEquals("Verify failure error code", e.errorInfo.code, 90001);
- }
-
- /* Change token to allow channel subscription so we can enter client and verify listener was set despite the failure */
- final boolean[] authUpdated = new boolean[]{false};
- ably.connection.on(ConnectionEvent.update, new ConnectionStateListener() {
- @Override
- public void onConnectionStateChanged(ConnectionStateChange state) {
- synchronized (authUpdated) {
- authUpdated[0] = true;
- authUpdated.notify();
- }
- }
- });
-
-
- ably.auth.authorize(null, new Auth.AuthOptions() {{
- tokenDetails = token2;
- }});
-
- try {
- synchronized (authUpdated) {
- while (!authUpdated[0])
- authUpdated.wait();
- }
- } catch (InterruptedException e) {}
-
- channel.attach();
- new ChannelWaiter(channel).waitFor(ChannelState.attached);
-
- /* Now to ensure listener was set despite the error we enter a client */
- channel.presence.enter(null, null);
- try {
- synchronized (presenceMessages) {
- while (presenceMessages.size() == 0)
- presenceMessages.wait();
- }
- } catch (InterruptedException e) {}
-
- assertTrue("Verify listener was set despite channel attach failure",
- presenceMessages.size() == 1 &&
- presenceMessages.get(0).action == Action.enter && presenceMessages.get(0).clientId.equals(testClientId1));
-
- } finally {
- if(ably != null)
- ably.close();
- }
- }
-
- /**
- *
- * Validate {@code Presence#enter(...)} will result in the listener not being
- * registered and an error being indicated, when the channel moves to the
- * FAILED state before the operation succeeds
- *
- * Validate {@code Presence#enterClient(...)} will result in the listener not being
- * registered and an error being indicated, when the channel moves to the FAILED
- * state before the operation succeeds
- *
- * Validate {@code Presence#updateClient(...)} will result in the listener not being
- * registered and an error being indicated, when the channel is in or moves to the
- * FAILED state before the operation succeeds
- *
- * Validate {@code Presence#leaveClient(...)} will result in the listener not being
- * registered and an error being indicated, when the channel is in or moves to the
- * FAILED state before the operation succeeds
- *
- * Validate {@code Presence#get(...)} throws an exception, when the channel
- * is in the FAILED state
- *
- *
- * @throws AblyException
- */
- @Test
- public void realtime_presence_get_throws_when_channel_failed() throws AblyException {
- AblyRealtime ably = null;
- try {
- ClientOptions opts = createOptions(testVars.keys[1].keyStr);
- ably = new AblyRealtime(opts);
-
- /* wait until connected */
- new ConnectionWaiter(ably.connection).waitFor(ConnectionState.connected);
- assertEquals("Verify connected state reached", ably.connection.state, ConnectionState.connected);
-
- /* create a channel and subscribe */
- final Channel channel = ably.channels.get("get_fail");
- channel.attach();
- new ChannelWaiter(channel).waitFor(ChannelState.failed);
-
- try {
- channel.presence.get(false);
- fail("Presence#get(...) should throw an exception when channel is in failed state");
- } catch(AblyException e) {
- assertThat(e.errorInfo.code, is(equalTo(90001)));
- assertThat(e.errorInfo.message, is(equalTo("channel operation failed (invalid channel state)")));
- }
- } finally {
- if(ably != null)
- ably.close();
- }
- }
-
- /**
- * Test if after reattach when returning from suspended mode client re-enters the channel with the same data
- * @throws AblyException
- *
- * Tests RTP17, RTP19, RTP19a, RTP5f, RTP6b
- */
- @Test
- public void realtime_presence_suspended_reenter() throws AblyException {
- AblyRealtime ably = null;
- try {
- MockWebsocketFactory mockTransport = new MockWebsocketFactory();
- DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr);
- fillInOptions(opts);
- opts.transportFactory = mockTransport;
-
- for (int i=0; i<2; i++) {
- final String channelName = "presence_suspended_reenter" + testParams.name + String.valueOf(i);
-
- mockTransport.allowSend();
-
- ably = new AblyRealtime(opts);
-
- ConnectionWaiter connectionWaiter = new ConnectionWaiter(ably.connection);
- connectionWaiter.waitFor(ConnectionState.connected);
-
- final Channel channel = ably.channels.get(channelName);
- channel.attach();
- ChannelWaiter channelWaiter = new ChannelWaiter(channel);
-
- channelWaiter.waitFor(ChannelState.attached);
-
- final String presenceData = "PRESENCE_DATA";
- final String connId = ably.connection.id;
-
- /*
- * On the first run to test RTP19a we don't enter client1 so the server on
- * return from suspend sees no presence data and sends ATTACHED without HAS_PRESENCE
- * The client then should remove all the members from the presence map and then
- * re-enter client2. On the second loop run we enter client1 and receive ATTACHED with
- * HAS_PRESENCE
- */
- final boolean[] wrongPresenceEmitted = new boolean[] {false};
- if (i == 1) {
- CompletionWaiter completionWaiter = new CompletionWaiter();
- channel.presence.enterClient(testClientId1, presenceData, completionWaiter);
- completionWaiter.waitFor();
-
- // RTP5f: after this point there should be no presence event for client1
- channel.presence.subscribe(new Presence.PresenceListener() {
- @Override
- public void onPresenceMessage(PresenceMessage message) {
- if (message.clientId.equals(testClientId1))
- wrongPresenceEmitted[0] = true;
- }
- });
- }
-
- final ArrayList leaveMessages = new ArrayList<>();
- /* Subscribe for message type, test RTP6b */
- channel.presence.subscribe(Action.leave, new Presence.PresenceListener() {
- @Override
- public void onPresenceMessage(PresenceMessage message) {
- leaveMessages.add(message);
- }
- });
-
- /*
- * We put testClientId2 presence data into the client library presence map but we
- * don't send it to the server
- */
-
- mockTransport.blockSend();
- channel.presence.enterClient(testClientId2, presenceData);
-
- ProtocolMessage msg = new ProtocolMessage();
- msg.connectionId = connId;
- msg.action = ProtocolMessage.Action.sync;
- msg.channel = channelName;
- msg.presence = new PresenceMessage[]{
- new PresenceMessage() {{
- action = Action.present;
- id = String.format("%s:0:0", connId);
- timestamp = System.currentTimeMillis();
- clientId = testClientId2;
- connectionId = connId;
- data = presenceData;
- }}
- };
- ably.connection.connectionManager.onMessage(null, msg);
-
- mockTransport.allowSend();
-
- ably.connection.connectionManager.requestState(ConnectionState.suspended);
- channelWaiter.waitFor(ChannelState.suspended);
-
- /*
- * When restoring from suspended state server will send sync message erasing
- * testClientId2 record from the presence map. Client should re-send presence message
- * for testClientId2 and restore its presence data.
- */
-
- ably.connection.connectionManager.requestState(ConnectionState.connected);
- channelWaiter.waitFor(ChannelState.attached);
- long reconnectTimestamp = System.currentTimeMillis();
-
- try {
- Thread.sleep(500);
- } catch (InterruptedException e) {
- }
-
- AblyRest ablyRest = new AblyRest(opts);
- io.ably.lib.rest.Channel restChannel = ablyRest.channels.get(channelName);
- assertEquals("Verify presence data is received by the server",
- restChannel.presence.get(null).items().length, i==0 ? 1 : 2);
-
- /* In both cases we should have one leave message in the leaveMessages */
- assertEquals("Verify exactly one LEAVE message was generated", leaveMessages.size(), 1);
-
- PresenceMessage leaveMessage = leaveMessages.get(0);
- assertEquals("Verify LEAVE message follows specs",leaveMessage.action, Action.leave);
- assertEquals("Verify LEAVE message follows specs",leaveMessage.clientId, testClientId2);
- assertEquals("Verify LEAVE message follows specs",leaveMessage.data, presenceData);
- assertTrue("Verify LEAVE message follows specs", Math.abs(leaveMessage.timestamp-reconnectTimestamp) < 2000);
-
- /* According to RTP5f there should be no presence event emitted for client1 */
- assertFalse("Verify no presence event emitted on return from suspend on SYNC for client1",
- wrongPresenceEmitted[0]);
- }
- } finally {
- if(ably != null)
- ably.close();
- }
- }
-
- /**
- * Test presence message map behaviour (RTP2 features)
- * Tests RTP2a, RTP2b1, RTP2b2, RTP2c, RTP2d, RTP2g, RTP18c, RTP6a features
- */
- @Test
- public void realtime_presence_map_test() throws AblyException {
- AblyRealtime ably = null;
- try {
- ClientOptions opts = createOptions(testVars.keys[0].keyStr);
- ably = new AblyRealtime(opts);
- final String channelName = "newness_comparison_" + testParams.name;
- Channel channel = ably.channels.get(channelName);
- channel.attach();
- ChannelWaiter channelWaiter = new ChannelWaiter(channel);
- channelWaiter.waitFor(ChannelState.attached);
-
- final String wontPass = "Won't pass newness test";
-
- Presence presence = channel.presence;
- final ArrayList presenceMessages = new ArrayList<>();
- /* Subscribe for all the message types, test RTP6a */
- presence.subscribe(new Presence.PresenceListener() {
- @Override
- public void onPresenceMessage(PresenceMessage message) {
- synchronized (presenceMessages) {
- assertNotEquals("Verify wrong message didn't pass the newness test",
- message.data, wontPass);
- // To exclude leave messages that sometimes sneak in let's collect only enter and update messages
- if (message.action == Action.enter || message.action == Action.update) {
- presenceMessages.add(message);
- }
- }
- }
- });
-
- /* Test message newness criteria as described in RTP2b */
- final PresenceMessage[] testData = new PresenceMessage[] {
- new PresenceMessage() {{
- clientId = "1";
- action = Action.enter;
- connectionId = "1";
- id = "1:0";
- }},
- new PresenceMessage() {{
- clientId = "2";
- action = Action.enter;
- connectionId = "2";
- id = "2:1:0";
- }},
- /* Should be newer than previous one */
- new PresenceMessage() {{
- clientId = "2";
- action = Action.update;
- connectionId = "2";
- id = "2:2:1";
- timestamp = 1;
- }},
- /* Shouldn't pass newness test because of message serial, timestamp doesn't matter in this case */
- new PresenceMessage() {{
- clientId = "2";
- action = Action.update;
- connectionId = "2";
- id = "2:1:1";
- timestamp = 2;
- data = wontPass;
- }},
- /* Shouldn't pass because of message index */
- new PresenceMessage() {{
- clientId = "2";
- action = Action.update;
- connectionId = "2";
- id = "2:2:0";
- data = wontPass;
- }},
- /* Should pass because id is not in form connId:clientId:index and timestamp is greater */
- new PresenceMessage() {{
- clientId = "2";
- action = Action.update;
- connectionId = "2";
- id = "weird_id";
- timestamp = 1000;
- }},
- /* Shouldn't pass because of timestamp */
- new PresenceMessage() {{
- clientId = "2";
- action = Action.update;
- connectionId = "2";
- id = "2:3:1";
- timestamp = 500;
- data = wontPass;
- }}
- };
-
- for (final PresenceMessage msg: testData) {
- ProtocolMessage protocolMessage = new ProtocolMessage() {{
- channel = channelName;
- action = Action.presence;
- presence = new PresenceMessage[]{msg};
- }};
-
- ably.connection.connectionManager.onMessage(null, protocolMessage);
- }
-
- int n = 0;
- for (PresenceMessage testMsg: testData) {
- if (testMsg.data != wontPass) {
- PresenceMessage factualMsg = n < presenceMessages.size() ? presenceMessages.get(n++) : null;
- assertTrue("Verify message passed newness test",
- factualMsg != null && factualMsg.id.equals(testMsg.id));
- assertEquals("Verify message was emitted on the presence object with original action",
- factualMsg.action, testMsg.action);
- assertEquals("Verify message was added to the presence map and stored with PRESENT action",
- presence.get(testMsg.clientId, false)[0].action, Action.present);
- }
- }
- assertEquals("Verify nothing else passed the newness test", n, presenceMessages.size());
-
- /* Repeat the process now as a part of SYNC and verify everything is exactly the same */
- final String channel2Name = "sync_newness_comparison_" + testParams.name;
- Channel channel2 = ably.channels.get(channel2Name);
- channel2.attach();
- new ChannelWaiter(channel2).waitFor(ChannelState.attached);
-
- /* Send all the presence data in one SYNC message without channelSerial (RTP18c) */
- ProtocolMessage syncMessage = new ProtocolMessage() {{
- channel = channel2Name;
- action = Action.sync;
- presence = testData.clone();
- }};
- final ArrayList syncPresenceMessages = new ArrayList<>();
- channel2.presence.subscribe(new Presence.PresenceListener() {
- @Override
- public void onPresenceMessage(PresenceMessage message) {
- syncPresenceMessages.add(message);
- }
- });
- ably.connection.connectionManager.onMessage(null, syncMessage);
-
- assertEquals("Verify result is the same in case of SYNC", syncPresenceMessages.size(), presenceMessages.size());
- for (int i=0; i100) number of clients so there are several sync messages, disconnect transport
- * in the middle and verify channel is re-syncing presence messages after transport reconnect
- *
- * Tests RTP3
- */
- @Test
- public void reattach_resume_broken_sync() {
- AblyRealtime clientAbly1 = null;
- AblyRealtime clientAbly2 = null;
- TestChannel testChannel = new TestChannel();
- int clientCount = 150; /* Should be greater than 100 to break sync into several messages */
- try {
- /* subscribe for presence events in the anonymous connection */
- new PresenceWaiter(testChannel.realtimeChannel);
-
- /* set up a connection with specific clientId */
- ClientOptions client1Opts = new ClientOptions(testVars.keys[0].keyStr);
- fillInOptions(client1Opts);
- client1Opts.tokenDetails = wildcardToken;
- 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);
-
- /* let client1 enter the channel for multiple clients and wait for the success callback */
- CompletionSet enterComplete = new CompletionSet();
- for(int i = 0; i < clientCount; i++) {
- client1Channel.presence.enterClient("client" + i, "Test data (attach_enter_multiple) " + i, enterComplete.add());
- }
- enterComplete.waitFor();
- assertTrue("Verify enter callback called on completion", enterComplete.pending.isEmpty());
- assertTrue("Verify no enter errors", enterComplete.errors.isEmpty());
-
- /* set up a second connection with different clientId */
- final MockWebsocketFactory mockTransport20 = new MockWebsocketFactory();
- DebugOptions client2Opts = new DebugOptions(testVars.keys[0].keyStr);
- fillInOptions(client2Opts);
- client2Opts.transportFactory = mockTransport20;
- client2Opts.tokenDetails = token2;
- client2Opts.clientId = testClientId2;
- client2Opts.autoConnect = false;
-
- mockTransport20.allowSend();
- clientAbly2 = new AblyRealtime(client2Opts);
-
- /* wait until connected */
- ConnectionWaiter connectionWaiter = new ConnectionWaiter(clientAbly2.connection);
- clientAbly2.connection.connect();
- connectionWaiter.waitFor(ConnectionState.connected);
-
- /* get channel */
- final Channel client2Channel = clientAbly2.channels.get(testChannel.channelName);
- final ConnectionManager connectionManager = clientAbly2.connection.connectionManager;
- final boolean[] disconnectedTransport = new boolean[]{false};
- final int[] presenceCount = new int[]{0};
- client2Channel.attach(new CompletionListener() {
- @Override
- public void onSuccess() {
- try {
- client2Channel.presence.subscribe(new Presence.PresenceListener() {
- @Override
- public void onPresenceMessage(PresenceMessage message) {
- if (!disconnectedTransport[0]) {
- mockTransport20.lastCreatedTransport.close();
- connectionManager.onTransportUnavailable(mockTransport20.lastCreatedTransport, new ErrorInfo("Mock", 50000));
-
- }
- disconnectedTransport[0] = true;
- presenceCount[0]++;
- }
- });
- }
- catch (AblyException e) {
- }
- }
-
- @Override
- public void onError(ErrorInfo reason) {
- }
- });
-
- ChannelWaiter channelWaiter = new ChannelWaiter(client2Channel);
- channelWaiter.waitFor(ChannelState.attached);
-
- /* Wait for reconnect */
- connectionWaiter.waitFor(ConnectionState.connected, 2);
-
- client2Channel.presence.unsubscribe();
-
- /* Verify that channel received sync and all 150 presence messages are received */
- try {
- Thread.sleep(500);
- assertEquals("Verify number of received presence messages", client2Channel.presence.get(true).length, clientCount);
- } catch (InterruptedException e) {}
-
- } catch(AblyException e) {
- e.printStackTrace();
- fail("Unexpected exception running test: " + e.getMessage());
- } finally {
- if(clientAbly1 != null)
- clientAbly1.close();
- if(clientAbly2 != null)
- clientAbly2.close();
- testChannel.dispose();
- }
- }
-
- /**
- * Test if presence sync works as it should
- * Tests RTP18a, RTP18b, RTP2f
- */
- @Test
- public void presence_sync() {
- AblyRealtime ably = null;
- try {
- ClientOptions opts = createOptions(testVars.keys[0].keyStr);
- ably = new AblyRealtime(opts);
-
- final String channelName = "presence_sync_test" + testParams.name;
-
- final Channel channel = ably.channels.get(channelName);
- channel.attach();
- ChannelWaiter channelWaiter = new ChannelWaiter(channel);
- channelWaiter.waitFor(ChannelState.attached);
-
- final ArrayList presenceHistory = new ArrayList<>();
- channel.presence.subscribe(new Presence.PresenceListener() {
- @Override
- public void onPresenceMessage(PresenceMessage message) {
- presenceHistory.add(message);
- }
- });
-
- final PresenceMessage[] testPresence1 = new PresenceMessage[] {
- /* Will be discarded because we'll start new sync with different channelSerial */
- new PresenceMessage() {{
- clientId = "1";
- action = Action.enter;
- connectionId = "1";
- id = "1:0";
- }}
- };
-
- final PresenceMessage[] testPresence2 = new PresenceMessage[] {
- new PresenceMessage() {{
- clientId = "2";
- action = Action.enter;
- connectionId = "2";
- id = "2:1:0";
- }},
- /* Enter presence message here is newer than leave in the subsequent message */
- new PresenceMessage() {{
- clientId = "3";
- action = Action.enter;
- connectionId = "3";
- id = "3:1:0";
- }}
- };
-
- final PresenceMessage[] testPresence3 = new PresenceMessage[] {
- new PresenceMessage() {{
- clientId = "3";
- action = Action.leave;
- connectionId = "3";
- id = "3:0:0";
- }},
- new PresenceMessage() {{
- clientId = "4";
- action = Action.enter;
- connectionId = "4";
- id = "4:1:1";
- }},
- new PresenceMessage() {{
- clientId = "4";
- action = Action.leave;
- connectionId = "4";
- id = "4:2:2";
- }}
- };
-
- final boolean[] seenLeaveMessageAsAbsentForClient4 = new boolean[] {false};
- channel.presence.subscribe(Action.leave, new Presence.PresenceListener() {
- @Override
- public void onPresenceMessage(PresenceMessage message) {
- try {
- /*
- * Do not call it in states other than ATTACHED because of presence.get() side
- * effect of attaching channel
- */
- if (message.clientId.equals("4") && message.action == Action.leave && channel.state == ChannelState.attached) {
- /*
- * Client library won't return a presence message if it is stored as ABSENT
- * so the result of the presence.get() call should be empty. This is the
- * only case when get() called from PresenceListener.onPresenceMessage results
- * in an empty answer.
- */
- seenLeaveMessageAsAbsentForClient4[0] = channel.presence.get("4", false).length == 0;
- }
- } catch (AblyException e) {}
- }
- });
-
- ably.connection.connectionManager.onMessage(null, new ProtocolMessage() {{
- action = Action.sync;
- channel = channelName;
- channelSerial = "1:1";
- presence = testPresence1;
- }});
- ably.connection.connectionManager.onMessage(null, new ProtocolMessage() {{
- action = Action.sync;
- channel = channelName;
- channelSerial = "2:1";
- presence = testPresence2;
- }});
- ably.connection.connectionManager.onMessage(null, new ProtocolMessage() {{
- action = Action.sync;
- channel = channelName;
- channelSerial = "2:";
- presence = testPresence3;
- }});
-
- assertEquals("Verify incomplete sync was discarded", channel.presence.get("1", false).length, 0);
- assertEquals("Verify client with id==2 is in presence map", channel.presence.get("2", false).length, 1);
- assertEquals("Verify client with id==3 is in presence map", channel.presence.get("3", false).length, 1);
- assertEquals("Verify nothing else is in presence map", channel.presence.get(false).length, 2);
-
- assertTrue("Verify LEAVE message for client with id==4 was stored as ABSENT", seenLeaveMessageAsAbsentForClient4[0]);
-
- PresenceMessage[] correctPresenceHistory = new PresenceMessage[] {
- /* client 1 enters (will later be discarded) */
- new PresenceMessage(Action.enter, "1"),
- /* client 2 enters */
- new PresenceMessage(Action.enter, "2"),
- /* client 3 enters and never leaves because of newness comparison for LEAVE fails */
- new PresenceMessage(Action.enter, "3"),
- /* client 4 enters and leaves */
- new PresenceMessage(Action.enter, "4"),
- new PresenceMessage(Action.leave, "4"),
- /* client 1 is eliminated from the presence map because the first portion of SYNC is discarded */
- new PresenceMessage(Action.leave, "1")
- };
-
- assertEquals("Verify number of presence messages", presenceHistory.size(), correctPresenceHistory.length);
- for (int i=0; i sentPresence = new ArrayList<>();
-
- /* Allow send but record all the presence messages for later analysis */
- final MockWebsocketFactory mockTransport = new MockWebsocketFactory();
- mockTransport.allowSend(new MockWebsocketFactory.MessageFilter() {
- @Override
- public boolean matches(ProtocolMessage message) {
- if (message.action == ProtocolMessage.Action.presence && message.presence != null) {
- synchronized (sentPresence) {
- Collections.addAll(sentPresence, message.presence);
- sentPresence.notify();
- }
- }
- return true;
- }
- });
-
- DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr);
- fillInOptions(opts);
- opts.clientId = testClientId1;
- opts.transportFactory = mockTransport;
- ably = new AblyRealtime(opts);
-
- Channel channel = ably.channels.get("protocol_enter_message_format_" + testParams.name);
- /* using testClientId1 */
- channel.presence.enter(null, null);
-
- synchronized (sentPresence) {
- while (sentPresence.size() < 1)
- sentPresence.wait();
- }
-
- assertEquals("Verify number of presence messages sent", sentPresence.size(), 1);
- assertTrue("Verify presence messages follows spec",
- sentPresence.get(0).action == Action.enter &&
- sentPresence.get(0).clientId == null
- );
-
- channel.detach();
- new ChannelWaiter(channel).waitFor(ChannelState.detached);
-
- try {
- channel.presence.enter(null, null);
- fail("Presence.enter() shouldn't succeed in detached state");
- } catch (AblyException e) {
- assertEquals("Verify exception error code", e.errorInfo.code, 91001 /* unable to enter presence channel (invalid channel state) */);
- }
-
- } finally {
- if (ably != null)
- ably.close();
- }
- }
-
- /**
- * Verify protocol messages sent on Presence.enter() follow specs if sent from correct state and
- * the call fails if sent from DETACHED state
- *
- * Tests RTP8c, RTP8g
- */
- @Test
- public void protocol_enterclient_message_format() throws AblyException, InterruptedException {
- AblyRealtime ably = null;
-
- try {
- final ArrayList sentPresence = new ArrayList<>();
-
- /* Allow send but record all the presence messages for later analysis */
- final MockWebsocketFactory mockTransport = new MockWebsocketFactory();
- mockTransport.allowSend(new MockWebsocketFactory.MessageFilter() {
- @Override
- public boolean matches(ProtocolMessage message) {
- if (message.action == ProtocolMessage.Action.presence && message.presence != null) {
- synchronized (sentPresence) {
- Collections.addAll(sentPresence, message.presence);
- sentPresence.notify();
- }
- }
- return true;
- }
- });
-
- DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr);
- fillInOptions(opts);
- opts.transportFactory = mockTransport;
- ably = new AblyRealtime(opts);
-
- Channel channel = ably.channels.get("protocol_enterclient_message_format_" + testParams.name);
- /* using testClientId2 */
- channel.presence.enterClient(testClientId2);
-
- synchronized (sentPresence) {
- while (sentPresence.size() < 1)
- sentPresence.wait();
- }
-
- assertEquals("Verify number of presence messages sent", sentPresence.size(), 1);
- assertTrue("Verify presence messages follows spec",
- sentPresence.get(0).action == Action.enter &&
- sentPresence.get(0).clientId.equals(testClientId2)
- );
-
- channel.detach();
- new ChannelWaiter(channel).waitFor(ChannelState.detached);
-
- try {
- channel.presence.enterClient("testClient3");
- fail("Presence.enterClient() shouldn't succeed in detached state");
- } catch (AblyException e) {
- assertEquals("Verify exception error code", e.errorInfo.code, 91001 /* unable to enter presence channel (invalid channel state) */);
- }
-
- } finally {
- if (ably != null)
- ably.close();
- }
- }
-
- /*
- * Verify presence data is received and encoded/decoded correctly
- * Tests RTP8e, RTP6a
- */
- @Test
- public void presence_encoding() throws AblyException, InterruptedException {
- AblyRealtime ably1 = null, ably2 = null;
- try {
- /* Set up two connections: one for entering, one for listening */
- final String channelName = "presence_encoding" + testParams.name;
- ClientOptions opts = createOptions(testVars.keys[0].keyStr);
- ably1 = new AblyRealtime(opts);
- ably2 = new AblyRealtime(opts);
-
- Channel channel1 = ably1.channels.get(channelName);
- Channel channel2 = ably2.channels.get(channelName);
-
- channel2.attach();
- new ChannelWaiter(channel2).waitFor(ChannelState.attached);
- final ArrayList
*
*/
+ @Ignore("FIXME: fix exception")
@Test
public void transient_publish_connected() throws AblyException {
AblyRealtime pubAbly = null, subAbly = null;
@@ -1096,6 +1098,7 @@ public void transient_publish_connected() throws AblyException {
*
*
*/
+ @Ignore("FIXME: fix exception")
@Test
public void transient_publish_connecting() throws AblyException {
AblyRealtime pubAbly = null, subAbly = null;
diff --git a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeConnectFailTest.java b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeConnectFailTest.java
index 8709da80b..943114532 100644
--- a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeConnectFailTest.java
+++ b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeConnectFailTest.java
@@ -11,6 +11,7 @@
import java.util.Arrays;
import java.util.List;
+import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.Timeout;
@@ -304,6 +305,7 @@ public void onConnectionStateChanged(ConnectionStateChange state) {
* Verify that the connection fails when attempting to recover with a
* malformed connection id
*/
+ @Ignore("FIXME: fix exception")
@Test
public void connect_invalid_recover_fail() {
AblyRealtime ably = null;
diff --git a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeConnectTest.java b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeConnectTest.java
index 4c5ed455b..2e744f7df 100644
--- a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeConnectTest.java
+++ b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeConnectTest.java
@@ -6,6 +6,7 @@
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import org.junit.Ignore;
import org.junit.Test;
import io.ably.lib.debug.DebugOptions;
@@ -80,6 +81,7 @@ public void connect_heartbeat() {
* Perform a simple connect, close the connection, and verify that
* the connection can be re-established by calling connect().
*/
+ @Ignore("FIXME: fix exception")
@Test
public void connect_after_close() {
try {
diff --git a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeCryptoTest.java b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeCryptoTest.java
index 3f8eb9a11..add6d27bc 100644
--- a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeCryptoTest.java
+++ b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeCryptoTest.java
@@ -15,6 +15,7 @@
import javax.crypto.KeyGenerator;
+import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.Timeout;
@@ -45,6 +46,7 @@ public class RealtimeCryptoTest extends ParameterizedTest {
* and publish an encrypted message on that channel using
* the default cipher params
*/
+ @Ignore("FIXME: fix exception")
@Test
public void single_send() {
String channelName = "single_send_" + testParams.name;
@@ -102,6 +104,7 @@ public void single_send() {
* and publish an encrypted message on that channel using
* a 256-bit key
*/
+ @Ignore("FIXME: fix exception")
@Test
public void single_send_256() {
String channelName = "single_send_256_" + testParams.name;
@@ -234,6 +237,7 @@ private void _multiple_send(String channelName, int messageCount, long delay) {
}
}
+ @Ignore("FIXME: fix exception")
@Test
public void multiple_send_2_200() {
int messageCount = 2;
@@ -241,6 +245,7 @@ public void multiple_send_2_200() {
_multiple_send("multiple_send_binary_2_200_" + testParams.name, messageCount, delay);
}
+ @Ignore("FIXME: fix exception")
@Test
public void multiple_send_20_100() {
int messageCount = 20;
@@ -253,6 +258,7 @@ public void multiple_send_20_100() {
* and the text protocol. Publish an encrypted message on that channel using
* the default cipher params and verify correct receipt.
*/
+ @Ignore("FIXME: fix exception")
@Test
public void single_send_binary_text() {
String channelName = "single_send_binary_text_" + testParams.name;
@@ -330,6 +336,7 @@ public void single_send_binary_text() {
* the default cipher params and verify that the decrypt failure
* is noticed as bad recovered plaintext.
*/
+ @Ignore("FIXME: fix exception")
@Test
public void single_send_key_mismatch() {
AblyRealtime sender = null;
@@ -403,6 +410,7 @@ public void single_send_key_mismatch() {
* Publish an unencrypted message and verify that the receiving connection
* does not attempt to decrypt it.
*/
+ @Ignore("FIXME: fix exception")
@Test
public void single_send_unencrypted() {
AblyRealtime sender = null;
@@ -474,6 +482,7 @@ public void single_send_unencrypted() {
* Publish an unencrypted message and verify that the receiving connection
* does not attempt to decrypt it.
*/
+ @Ignore("FIXME: fix exception")
@Test
public void single_send_encrypted_unhandled() {
AblyRealtime sender = null;
@@ -544,6 +553,7 @@ public void single_send_encrypted_unhandled() {
* - publish with an updated key on the tx connection and verify that it is not decrypted by the rx connection;
* - publish with an updated key on the rx connection and verify connect receipt
*/
+ @Ignore("FIXME: fix exception")
@Test
public void set_cipher_params() {
AblyRealtime sender = null;
@@ -657,6 +667,7 @@ public void set_cipher_params() {
* been replaced with ChannelOptions.withCipherKey(...).
* @see apple = new LinkedHashMap<>();
diff --git a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeJWTTest.java b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeJWTTest.java
index a82b0f078..be6dbd95b 100644
--- a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeJWTTest.java
+++ b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeJWTTest.java
@@ -10,6 +10,7 @@
import io.ably.lib.test.common.Setup.Key;
import io.ably.lib.util.Log;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Test;
import io.ably.lib.types.*;
@@ -70,6 +71,7 @@ public void auth_clientid_match_the_one_requested_in_jwt() {
* Request a JWT with subscribe-only capabilities
* Verifies that publishing on a channel fails
*/
+ @Ignore("FIXME: fix exception")
@Test
public void auth_jwt_with_subscribe_only_capability() {
try {
diff --git a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeMessageTest.java b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeMessageTest.java
index babd880c4..ee2dd733e 100644
--- a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeMessageTest.java
+++ b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeMessageTest.java
@@ -15,6 +15,7 @@
import com.google.gson.*;
import io.ably.lib.types.MessageExtras;
import io.ably.lib.util.Serialisation;
+import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.Timeout;
@@ -57,6 +58,7 @@ public class RealtimeMessageTest extends ParameterizedTest {
/**
* Connect to the service and attach, subscribe to an event, and publish on that channel
*/
+ @Ignore("FIXME: fix exception")
@Test
public void single_send() {
AblyRealtime ably = null;
@@ -101,6 +103,7 @@ public void single_send() {
* attach, subscribe to an event, publish on one
* connection and confirm receipt on the other.
*/
+ @Ignore("FIXME: fix exception")
@Test
public void single_send_noecho() {
AblyRealtime txAbly = null;
@@ -159,6 +162,7 @@ public void single_send_noecho() {
* Get a channel and subscribe without explicitly attaching.
* Verify that the channel reaches the attached state.
*/
+ @Ignore("FIXME: fix exception")
@Test
public void subscribe_implicit_attach() {
AblyRealtime ably = null;
@@ -285,6 +289,7 @@ private void _multiple_send(String channelName, int messageCount, int msgSize, b
* Test right and wrong channel states to publish messages
* Tests RTL6c
*/
+ @Ignore("FIXME: fix exception")
@Test
public void publish_channel_state() {
AblyRealtime ably = null;
@@ -390,6 +395,7 @@ private void _multiple_send_batch(String channelName, int messageCount, int batc
}
}
+ @Ignore("FIXME: fix exception")
@Test
public void multiple_send_10_1000_16_string() {
int messageCount = 10;
@@ -397,6 +403,7 @@ public void multiple_send_10_1000_16_string() {
_multiple_send("multiple_send_10_1000_16_string_" + testParams.name, messageCount, 16, false, delay);
}
+ @Ignore("FIXME: fix exception")
@Test
public void multiple_send_10_1000_16_binary() {
int messageCount = 10;
@@ -404,6 +411,7 @@ public void multiple_send_10_1000_16_binary() {
_multiple_send("multiple_send_10_1000_16_binary_" + testParams.name, messageCount, 16, true, delay);
}
+ @Ignore("FIXME: fix exception")
@Test
public void multiple_send_10_1000_512_string() {
int messageCount = 10;
@@ -411,6 +419,7 @@ public void multiple_send_10_1000_512_string() {
_multiple_send("multiple_send_10_1000_512_string_" + testParams.name, messageCount, 512, false, delay);
}
+ @Ignore("FIXME: fix exception")
@Test
public void multiple_send_10_1000_512_binary() {
int messageCount = 10;
@@ -418,6 +427,7 @@ public void multiple_send_10_1000_512_binary() {
_multiple_send("multiple_send_10_1000_512_binary_" + testParams.name, messageCount, 512, true, delay);
}
+ @Ignore("FIXME: fix exception")
@Test
public void multiple_send_20_200() {
int messageCount = 20;
@@ -425,6 +435,7 @@ public void multiple_send_20_200() {
_multiple_send("multiple_send_20_200_" + testParams.name, messageCount, 256, true, delay);
}
+ @Ignore("FIXME: fix exception")
@Test
public void multiple_send_200_50() {
int messageCount = 200;
@@ -432,6 +443,7 @@ public void multiple_send_200_50() {
_multiple_send("multiple_send_binary_200_50_" + testParams.name, messageCount, 256, true, delay);
}
+ @Ignore("FIXME: fix exception")
@Test
public void multiple_send_1000_10() {
int messageCount = 1000;
@@ -506,6 +518,7 @@ public void ensure_disconnect_with_error_does_not_move_to_failed() {
}
}
+ @Ignore("FIXME: fix exception")
@Test
public void messages_encoding_fixtures() {
MessagesEncodingData fixtures;
@@ -570,6 +583,7 @@ public MessagesEncodingDataItem[] handleResponse(HttpCore.Response response, Err
}
}
+ @Ignore("FIXME: fix exception")
@Test
public void messages_msgpack_and_json_encoding_is_compatible() {
MessagesEncodingData fixtures;
@@ -871,6 +885,7 @@ public void message_from_encoded_json_object() throws AblyException {
* Refer Spec. TM3
* @throws AblyException
*/
+ @Ignore("FIXME: fix exception")
@Test
public void messages_from_encoded_json_array() throws AblyException {
JsonArray fixtures = null;
@@ -918,6 +933,7 @@ static class MessagesData {
*
* @see RSL6a2
*/
+ @Ignore("FIXME: fix exception")
@Test
public void opaque_message_extras() throws AblyException {
AblyRealtime ably = null;
diff --git a/lib/src/test/java/io/ably/lib/test/realtime/RealtimePresenceHistoryTest.java b/lib/src/test/java/io/ably/lib/test/realtime/RealtimePresenceHistoryTest.java
index 6a0e07f2d..85cc365f4 100644
--- a/lib/src/test/java/io/ably/lib/test/realtime/RealtimePresenceHistoryTest.java
+++ b/lib/src/test/java/io/ably/lib/test/realtime/RealtimePresenceHistoryTest.java
@@ -37,6 +37,7 @@
import java.util.Locale;
+@Ignore("FIXME: fix ably exception")
public class RealtimePresenceHistoryTest extends ParameterizedTest {
private static final String testClientId = "testClientId";
@@ -210,7 +211,7 @@ public void presencehistory_types_forward() {
* Connect twice to the service, each using the default (binary) protocol.
* Publish messages on one connection to a given channel; then attach
* the second connection to the same channel and verify a complete message
- * history can be obtained.
+ * history can be obtained.
*/
@Test
public void presencehistory_second_channel() {
@@ -684,7 +685,7 @@ public void presencehistory_time_b() {
rtOpts.clientId = testClientId;
ably = new AblyRealtime(rtOpts);
String channelName = "persisted:presencehistory_time_b_" + testParams.name;
-
+
/* create a channel */
final Channel channel = ably.channels.get(channelName);
@@ -1033,7 +1034,7 @@ public void presencehistory_paginate_first_b() {
* Connect twice to the service.
* Publish messages on one connection to a given channel; while in progress,
* attach the second connection to the same channel and verify a message
- * history up to the point of attachment can be obtained.
+ * history up to the point of attachment can be obtained.
*/
@Test
@Ignore("Fails due to issues in sandbox. See https://github.com/ably/realtime/issues/1845 for details.")
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 de8cf542e..66a60c839 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
@@ -52,6 +52,7 @@
import io.ably.lib.types.ProtocolMessage;
import io.ably.lib.util.Serialisation;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.Timeout;
@@ -138,6 +139,7 @@ public void setUpBefore() throws Exception {
/**
* Attach to channel, enter presence channel and await entered event
*/
+ @Ignore("FIXME: fix exception")
@Test
public void enter_simple() {
AblyRealtime clientAbly1 = null;
@@ -188,6 +190,7 @@ public void enter_simple() {
/**
* Enter presence channel without prior attach and await entered event
*/
+ @Ignore("FIXME: fix exception")
@Test
public void enter_before_attach() {
AblyRealtime clientAbly1 = null;
@@ -236,6 +239,7 @@ public void enter_before_attach() {
/**
* Enter presence channel without prior connect and await entered event
*/
+ @Ignore("FIXME: fix exception")
@Test
public void enter_before_connect() {
AblyRealtime clientAbly1 = null;
@@ -281,6 +285,7 @@ public void enter_before_connect() {
* Enter, then leave, presence channel and await leave event
* Verify that the item is removed from the presence map (RTP2e)
*/
+ @Ignore("FIXME: fix exception")
@Test
public void enter_leave_simple() {
AblyRealtime clientAbly1 = null;
@@ -341,6 +346,7 @@ public void enter_leave_simple() {
/**
* Enter, then enter again, expecting update event
*/
+ @Ignore("FIXME: fix exception")
@Test
public void enter_enter_simple() {
AblyRealtime clientAbly1 = null;
@@ -410,6 +416,7 @@ public void enter_enter_simple() {
/**
* Enter, then update, expecting update event
*/
+ @Ignore("FIXME: fix exception")
@Test
public void enter_update_simple() {
AblyRealtime clientAbly1 = null;
@@ -549,6 +556,7 @@ public void enter_update_null() {
/**
* Update without having first entered, expecting enter event
*/
+ @Ignore("FIXME: fix exception")
@Test
public void update_noenter() {
AblyRealtime clientAbly1 = null;
@@ -607,6 +615,7 @@ public void update_noenter() {
* Enter, then leave (with no data) and await leave event,
* expecting enter data to be in leave event
*/
+ @Ignore("FIXME: fix exception")
@Test
public void enter_leave_nodata() {
AblyRealtime clientAbly1 = null;
@@ -662,6 +671,7 @@ public void enter_leave_nodata() {
/**
* Attach to channel, enter presence channel and get presence using realtime get()
*/
+ @Ignore("FIXME: fix exception")
@Test
public void realtime_get_simple() {
AblyRealtime clientAbly1 = null;
@@ -716,6 +726,7 @@ public void realtime_get_simple() {
/**
* Attach to channel, enter+leave presence channel and get presence with realtime get()
*/
+ @Ignore("FIXME: fix exception")
@Test
public void realtime_get_leave() {
AblyRealtime clientAbly1 = null;
@@ -774,6 +785,7 @@ public void realtime_get_leave() {
* Attach to channel, enter presence channel, then initiate second
* connection, seeing existing member in message subsequent to second attach response
*/
+ @Ignore("FIXME: fix exception")
@Test
public void attach_enter_simple() {
AblyRealtime clientAbly1 = null;
@@ -850,6 +862,7 @@ public void attach_enter_simple() {
*
* Test RTP4
*/
+ @Ignore("FIXME: fix exception")
@Test
public void attach_enter_multiple() {
AblyRealtime clientAbly1 = null;
@@ -934,6 +947,7 @@ public void attach_enter_multiple() {
/**
* Attach and enter channel on two connections, seeing
* both members in presence returned by realtime get() */
+ @Ignore("FIXME: fix exception")
@Test
public void realtime_enter_multiple() {
AblyRealtime clientAbly1 = null;
@@ -1004,6 +1018,7 @@ public void realtime_enter_multiple() {
/**
* Attach to channel, enter presence channel and get presence using rest get()
*/
+ @Ignore("FIXME: fix exception")
@Test
public void rest_get_simple() {
AblyRealtime clientAbly1 = null;
@@ -1056,6 +1071,7 @@ public void rest_get_simple() {
/**
* Attach to channel, enter+leave presence channel and get presence with rest get()
*/
+ @Ignore("FIXME: fix exception")
@Test
public void rest_get_leave() {
AblyRealtime clientAbly1 = null;
@@ -1113,6 +1129,7 @@ public void rest_get_leave() {
/**
* Attach and enter channel on two connections, seeing
* both members in presence returned by rest get() */
+ @Ignore("FIXME: fix exception")
@Test
public void rest_enter_multiple() {
AblyRealtime clientAbly1 = null;
@@ -1178,6 +1195,7 @@ public void rest_enter_multiple() {
/**
* Attach and enter channel multiple times on a single connection,
* retrieving members using paginated rest get() */
+ @Ignore("FIXME: fix exception")
@Test
public void rest_paginated_get() {
AblyRealtime clientAbly1 = null;
@@ -1263,6 +1281,7 @@ public void rest_paginated_get() {
/**
* Attach to channel, enter presence channel, disconnect and await leave event
*/
+ @Ignore("FIXME: fix exception")
@Test
public void disconnect_leave() {
AblyRealtime clientAbly1 = null;
@@ -1404,6 +1423,7 @@ public Presence.PresenceListener setMessageStack(List messageSt
*
* @throws AblyException
*/
+ @Ignore("FIXME: flaky test")
@Test
public void realtime_presence_unsubscribe_single() throws AblyException {
/* Ably instance that will emit presence events */
@@ -1483,6 +1503,7 @@ public Presence.PresenceListener setMessageStack(List messageSt
*
* @throws AblyException
*/
+ @Ignore("FIXME: flaky test")
@Test
public void realtime_presence_subscribe_all() throws AblyException {
/* Ably instance that will emit presence events */
@@ -1558,6 +1579,7 @@ public Presence.PresenceListener setMessageStack(List messageSt
*
* @throws AblyException
*/
+ @Ignore("FIXME: fix exception")
@Test
public void realtime_presence_subscribe_multiple() throws AblyException {
/* Ably instance that will emit presence events */
@@ -1716,6 +1738,7 @@ public Presence.PresenceListener setMessageStack(List messageSt
*
* @throws AblyException
*/
+ @Ignore("FIXME: flaky test")
@Test
public void realtime_presence_attach_implicit_subscribe_fail() throws AblyException {
AblyRealtime ably = null;
@@ -2033,6 +2056,7 @@ public void realtime_presence_attach_implicit_leaveclient_fail() throws AblyExce
*
* @throws AblyException
*/
+ @Ignore("FIXME: fix exception")
@Test
public void realtime_presence_get_throws_when_channel_failed() throws AblyException {
AblyRealtime ably = null;
@@ -2068,6 +2092,7 @@ public void realtime_presence_get_throws_when_channel_failed() throws AblyExcept
*
* Tests RTP17, RTP19, RTP19a, RTP5f, RTP6b
*/
+ @Ignore("FIXME: fix exception")
@Test
public void realtime_presence_suspended_reenter() throws AblyException {
AblyRealtime ably = null;
@@ -2356,6 +2381,7 @@ public void onPresenceMessage(PresenceMessage message) {
*
* Tests RTP3
*/
+ @Ignore("FIXME: fix exception")
@Test
public void reattach_resume_broken_sync() {
AblyRealtime clientAbly1 = null;
@@ -2621,6 +2647,7 @@ public void onPresenceMessage(PresenceMessage message) {
* Test channel state change effect on presence
* Tests RTP5a, RTP5b, RTP5c3, RTP16b
*/
+ @Ignore("FIXME: fix exception")
@Test
public void presence_state_change () {
AblyRealtime ably = null;
@@ -2737,6 +2764,7 @@ public void onChannelStateChanged(ChannelStateChange stateChange) {
*
* Not functional yet
*/
+ @Ignore("FIXME: fix exception")
@Test
public void presence_without_subscribe_capability() throws AblyException {
String channelName = "presence_without_subscribe" + testParams.name;
@@ -2800,6 +2828,7 @@ public void onError(ErrorInfo reason) {
*
* Tests RTP13
*/
+ @Ignore("FIXME: fix exception")
@Test
public void sync_complete() {
AblyRealtime ably1 = null, ably2 = null;
@@ -2879,6 +2908,7 @@ public void presence_enter_without_permission() throws AblyException {
/**
* Enter wrong client (mismatching one set in the token), check exception
*/
+ @Ignore("FIXME: fix exception")
@Test
public void presence_enter_mismatched_clientid() throws AblyException {
String channelName = "presence_enter_mismatched_clientid" + testParams.name;
@@ -3120,6 +3150,7 @@ public boolean matches(ProtocolMessage message) {
* Verify presence data is received and encoded/decoded correctly
* Tests RTP8e, RTP6a
*/
+ @Ignore("FIXME: flaky test")
@Test
public void presence_encoding() throws AblyException, InterruptedException {
AblyRealtime ably1 = null, ably2 = null;
@@ -3190,6 +3221,7 @@ public void onPresenceMessage(PresenceMessage message) {
* Test Presence.get() filtering and syncToWait flag
* Tests RTP11b, RTP11c, RTP11d
*/
+ @Ignore("FIXME: fix exception")
@Test
public void presence_get() throws AblyException, InterruptedException {
AblyRealtime ably1 = null, ably2 = null;
@@ -3447,6 +3479,7 @@ public void message_from_encoded_json_object() throws AblyException {
* Refer Spec. TP4
* @throws AblyException
*/
+ @Ignore("FIXME: fix exception")
@Test
public void messages_from_encoded_json_array() throws AblyException {
JsonArray fixtures = null;
diff --git a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeRecoverTest.java b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeRecoverTest.java
index c8a97c478..389c2c210 100644
--- a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeRecoverTest.java
+++ b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeRecoverTest.java
@@ -20,6 +20,7 @@
import io.ably.lib.types.ClientOptions;
import io.ably.lib.types.ErrorInfo;
+import org.junit.Ignore;
import org.junit.Test;
import static org.hamcrest.Matchers.lessThan;
@@ -40,6 +41,7 @@ public class RealtimeRecoverTest extends ParameterizedTest {
* on recover
* Spec: RTN16a,RTN16b
*/
+ @Ignore("FIXME: fix exception")
@Test
public void recover_disconnected() {
AblyRealtime ablyRx = null, ablyTx = null, ablyRxRecover = null;
@@ -75,7 +77,7 @@ public void recover_disconnected() {
/* wait for the publish callback to be called */
ErrorInfo[] errors = msgComplete1.waitFor();
- assertTrue("Verify success from all message callbacks", errors.length == 0);
+ assertTrue("Verify success from all message callbacks", errors.length == 0);
/* wait for the subscription callback to be called */
messageWaiter.waitFor(messageCount);
@@ -100,7 +102,7 @@ public void recover_disconnected() {
/* wait for the publish callback to be called */
errors = msgComplete2.waitFor();
- assertTrue("Verify success from all message callbacks", errors.length == 0);
+ assertTrue("Verify success from all message callbacks", errors.length == 0);
/* establish a new rx connection with recover string, and wait for connection */
ClientOptions recoverOpts = createOptions(testVars.keys[0].keyStr);
@@ -139,6 +141,7 @@ public void recover_disconnected() {
* on recover
* Spec: RTN16a,RTN16b
*/
+ @Ignore("FIXME: fix exception")
@Test
public void recover_implicit_connect() {
AblyRealtime ablyRx = null, ablyTx = null, ablyRxRecover = null;
@@ -174,7 +177,7 @@ public void recover_implicit_connect() {
/* wait for the publish callback to be called */
ErrorInfo[] errors = msgComplete1.waitFor();
- assertTrue("Verify success from all message callbacks", errors.length == 0);
+ assertTrue("Verify success from all message callbacks", errors.length == 0);
/* wait for the subscription callback to be called */
messageWaiter.waitFor(messageCount);
@@ -199,7 +202,7 @@ public void recover_implicit_connect() {
/* wait for the publish callback to be called */
errors = msgComplete2.waitFor();
- assertTrue("Verify success from all message callbacks", errors.length == 0);
+ assertTrue("Verify success from all message callbacks", errors.length == 0);
/* establish a new rx connection with recover string, and wait for connection */
ClientOptions recoverOpts = createOptions(testVars.keys[0].keyStr);
@@ -233,6 +236,7 @@ public void recover_implicit_connect() {
* Disconnect+suspend and then reconnect the send connection; verify that
* each subsequent publish causes a CompletionListener call.
*/
+ @Ignore("FIXME: fix exception")
@Test
public void recover_verify_publish() {
AblyRealtime ablyRx = null, ablyTx = null;
@@ -268,7 +272,7 @@ public void recover_verify_publish() {
/* wait for the publish callback to be called */
ErrorInfo[] errors = msgComplete1.waitFor();
- assertTrue("Verify success from all message callbacks", errors.length == 0);
+ assertTrue("Verify success from all message callbacks", errors.length == 0);
/* wait for the subscription callback to be called */
messageWaiter.waitFor(messageCount);
@@ -310,7 +314,7 @@ public void recover_verify_publish() {
System.out.println("*** published. About to wait for callbacks");
errors = msgComplete2.waitFor();
System.out.println("*** done");
- assertTrue("Verify success from all message callbacks", errors.length == 0);
+ assertTrue("Verify success from all message callbacks", errors.length == 0);
/* wait for the subscription callback to be called */
messageWaiter.waitFor(messageCount);
diff --git a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeResumeTest.java b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeResumeTest.java
index 731987a89..93c69e3d2 100644
--- a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeResumeTest.java
+++ b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeResumeTest.java
@@ -7,6 +7,7 @@
import io.ably.lib.debug.DebugOptions;
import io.ably.lib.types.*;
import io.ably.lib.util.Log;
+import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.Timeout;
@@ -94,6 +95,7 @@ public void resume_none() {
* the connection continues to receive messages on attached
* channels after reconnection.
*/
+ @Ignore("FIXME: fix exception")
@Test
public void resume_simple() {
AblyRealtime ablyTx = null;
@@ -184,6 +186,7 @@ public void resume_simple() {
* verify that the messages sent whilst disconnected are delivered
* on resume
*/
+ @Ignore("FIXME: fix exception")
@Test
public void resume_disconnected() {
AblyRealtime ablyTx = null;
@@ -269,6 +272,7 @@ public void resume_disconnected() {
/**
* Verify resume behaviour with multiple channels
*/
+ @Ignore("FIXME: fix exception")
@Test
public void resume_multiple_channel() {
AblyRealtime ablyTx = null;
@@ -371,6 +375,7 @@ public void resume_multiple_channel() {
* Verify resume behaviour across disconnect periods covering
* multiple subminute intervals
*/
+ @Ignore("FIXME: fix exception")
@Test
public void resume_multiple_interval() {
AblyRealtime ablyTx = null;
@@ -459,6 +464,7 @@ public void resume_multiple_interval() {
* Disconnect and then reconnect the send connection; verify that
* each subsequent publish causes a CompletionListener call.
*/
+ @Ignore("FIXME: fix exception")
@Test
public void resume_verify_publish() {
AblyRealtime ablyTx = null;
@@ -564,6 +570,7 @@ public void resume_verify_publish() {
* round of messages which should be queued and published after
* we reconnect the sender.
*/
+ @Ignore("FIXME: fix exception")
@Test
public void resume_publish_queue() {
AblyRealtime receiver = null;
@@ -675,7 +682,7 @@ public void resume_publish_queue() {
}
}
- //RTL4j2
+ //RTL4j2
@Test
public void resume_rewind_1 ()
{
diff --git a/lib/src/test/java/io/ably/lib/test/rest/RestAuthAttributeTest.java b/lib/src/test/java/io/ably/lib/test/rest/RestAuthAttributeTest.java
index 107f25cf8..b2450dfd6 100644
--- a/lib/src/test/java/io/ably/lib/test/rest/RestAuthAttributeTest.java
+++ b/lib/src/test/java/io/ably/lib/test/rest/RestAuthAttributeTest.java
@@ -13,6 +13,7 @@
import java.util.List;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Test;
import io.ably.lib.rest.AblyRest;
@@ -47,6 +48,7 @@ public void setupClient() throws Exception {
* Spec: RSA10g,RSA10j
*
*/
+ @Ignore("FIXME: flaky test")
@Test
public void auth_stores_options_params() {
try {
diff --git a/lib/src/test/java/io/ably/lib/util/CryptoMessageTest.java b/lib/src/test/java/io/ably/lib/util/CryptoMessageTest.java
index 0412bdaeb..69962b538 100644
--- a/lib/src/test/java/io/ably/lib/util/CryptoMessageTest.java
+++ b/lib/src/test/java/io/ably/lib/util/CryptoMessageTest.java
@@ -9,6 +9,7 @@
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
+import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
@@ -22,6 +23,7 @@
import io.ably.lib.util.Crypto;
import io.ably.lib.util.Crypto.CipherParams;
+@Ignore("FIXME: Initialization is failing")
@RunWith(Parameterized.class)
public class CryptoMessageTest {
public enum FixtureSet {
diff --git a/lib/src/test/java/io/ably/lib/util/CryptoTest.java b/lib/src/test/java/io/ably/lib/util/CryptoTest.java
index 60673bbf1..a9fec56de 100644
--- a/lib/src/test/java/io/ably/lib/util/CryptoTest.java
+++ b/lib/src/test/java/io/ably/lib/util/CryptoTest.java
@@ -10,6 +10,7 @@
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
+import org.junit.Ignore;
import org.junit.Test;
import org.msgpack.core.MessagePack;
import org.msgpack.core.MessagePacker;
@@ -84,8 +85,9 @@ public void cipher_params() throws AblyException, NoSuchAlgorithmException {
*
* Equivalent to the following in ably-cocoa:
* testEncryptAndDecrypt in Spec/CryptoTest.m
- * @throws IOException
+ * @throws IOException
*/
+ @Ignore("FIXME: NullPointerException should be fixed")
@Test
public void encryptAndDecrypt() throws NoSuchAlgorithmException, AblyException, IOException {
final FixtureSet fixtureSet = FixtureSet.AES256;
@@ -111,10 +113,10 @@ public void encryptAndDecrypt() throws NoSuchAlgorithmException, AblyException,
writer.name("keyLength");
writer.value(256);
-
+
writer.name("key");
writer.value(Base64Coder.encodeToString(fixtureSet.key));
-
+
writer.name("iv");
writer.value(Base64Coder.encodeToString(fixtureSet.iv));
From 1e3d12be6f0716444659bd2c2dece92648661cb4 Mon Sep 17 00:00:00 2001
From: Sergii Zhevzhyk
Date: Fri, 22 Jan 2021 08:59:49 +0100
Subject: [PATCH 016/899] Mark the http_ably_execute_fallback test as flaky
---
lib/src/test/java/io/ably/lib/test/rest/HttpTest.java | 2 ++
1 file changed, 2 insertions(+)
diff --git a/lib/src/test/java/io/ably/lib/test/rest/HttpTest.java b/lib/src/test/java/io/ably/lib/test/rest/HttpTest.java
index fa3649bc3..81a7f44fb 100644
--- a/lib/src/test/java/io/ably/lib/test/rest/HttpTest.java
+++ b/lib/src/test/java/io/ably/lib/test/rest/HttpTest.java
@@ -31,6 +31,7 @@
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
+import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
@@ -101,6 +102,7 @@ public static void tearDown() {
*
* @throws Exception
*/
+ @Ignore("FIXME: flaky test")
@Test
public void http_ably_execute_fallback() throws AblyException {
ClientOptions options = new ClientOptions();
From f7bfa06771a95ca4ba5c194f4c8bdac838c8b353 Mon Sep 17 00:00:00 2001
From: Nik Silver
Date: Wed, 27 Jan 2021 10:32:32 +0000
Subject: [PATCH 017/899] Clarified ownership
---
MAINTAINERS.md | 1 +
1 file changed, 1 insertion(+)
create mode 100644 MAINTAINERS.md
diff --git a/MAINTAINERS.md b/MAINTAINERS.md
new file mode 100644
index 000000000..6edbb9593
--- /dev/null
+++ b/MAINTAINERS.md
@@ -0,0 +1 @@
+This repository is owned by the Ably SDK team.
From 4cd5f1bba3b2a7e7214ad1dc08a37c67fc64955a Mon Sep 17 00:00:00 2001
From: Quintin Willison
Date: Fri, 29 Jan 2021 15:45:52 +0000
Subject: [PATCH 018/899] Add status badges to top of the readme.
---
README.md | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/README.md b/README.md
index 478504bdb..1e377a2f0 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,9 @@
# [Ably](https://www.ably.io)
+
+
+
+
| Android | Java |
|---------|------|
| [  ](https://bintray.com/ably-io/ably/ably-android/_latestVersion) | [  ](https://bintray.com/ably-io/ably/ably-java/_latestVersion) |
From e4cc6e6fdeddcf4a411b8b374ac49a639fd4260e Mon Sep 17 00:00:00 2001
From: Nathaniel Dempkowski
Date: Wed, 10 Feb 2021 15:41:31 -0500
Subject: [PATCH 019/899] Update references from 1 -> l to match client spec
---
lib/src/main/java/io/ably/lib/transport/Defaults.java | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/lib/src/main/java/io/ably/lib/transport/Defaults.java b/lib/src/main/java/io/ably/lib/transport/Defaults.java
index 4d094ab65..b3b27fa7d 100644
--- a/lib/src/main/java/io/ably/lib/transport/Defaults.java
+++ b/lib/src/main/java/io/ably/lib/transport/Defaults.java
@@ -33,9 +33,9 @@ public class Defaults {
public static int TIMEOUT_DISCONNECT = 15000;
public static int TIMEOUT_CHANNEL_RETRY = 15000;
- /* TO313 */
+ /* TO3l3 */
public static int TIMEOUT_HTTP_OPEN = 4000;
- /* TO314 */
+ /* TO3l4 */
public static int TIMEOUT_HTTP_REQUEST = 15000;
/* DF1b */
public static long realtimeRequestTimeout = 10000L;
From 976433b8ca0f9476821ebe7e10743a4f84486513 Mon Sep 17 00:00:00 2001
From: Sergii Zhevzhyk
Date: Wed, 10 Feb 2021 22:18:43 +0100
Subject: [PATCH 020/899] Mark the test_consistent_presence_for_members test as
flaky
---
.../java/io/ably/lib/test/realtime/RealtimePresenceTest.java | 1 +
1 file changed, 1 insertion(+)
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 66a60c839..4342b580c 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
@@ -3317,6 +3317,7 @@ public void checkMembersWithChannelPresence(Channel testChannel) throws AblyExce
assertEquals("Members count with channel presence should be " + presenceMessages.length, presenceMessages.length, 1);
}
+ @Ignore
@Test
public void test_consistent_presence_for_members() {
AblyRealtime clientAbly1 = null;
From b0cf6f8261dd93a56f64eec36f1d4f1ad0b710c9 Mon Sep 17 00:00:00 2001
From: Sergii Zhevzhyk
Date: Wed, 17 Feb 2021 22:29:27 +0100
Subject: [PATCH 021/899] Remove empty capability query parameter
---
lib/src/main/java/io/ably/lib/rest/Auth.java | 3 +--
.../java/io/ably/lib/types/Capability.java | 10 ++++----
.../io/ably/lib/types/CapabilityTest.java | 23 +++++++++++++++++++
3 files changed, 28 insertions(+), 8 deletions(-)
create mode 100644 lib/src/test/java/io/ably/lib/types/CapabilityTest.java
diff --git a/lib/src/main/java/io/ably/lib/rest/Auth.java b/lib/src/main/java/io/ably/lib/rest/Auth.java
index 314238b0e..f25227eb6 100644
--- a/lib/src/main/java/io/ably/lib/rest/Auth.java
+++ b/lib/src/main/java/io/ably/lib/rest/Auth.java
@@ -708,8 +708,7 @@ public TokenRequest createTokenRequest(TokenParams params, AuthOptions options)
options = (options == null) ? this.authOptions : options.copy();
params = (params == null) ? this.tokenParams : params.copy();
- if(params.capability != null)
- params.capability = Capability.c14n(params.capability);
+ params.capability = Capability.c14n(params.capability);
TokenRequest request = new TokenRequest(params);
String key = options.key;
diff --git a/lib/src/main/java/io/ably/lib/types/Capability.java b/lib/src/main/java/io/ably/lib/types/Capability.java
index fabb3e186..d020e1444 100644
--- a/lib/src/main/java/io/ably/lib/types/Capability.java
+++ b/lib/src/main/java/io/ably/lib/types/Capability.java
@@ -19,21 +19,19 @@
public class Capability {
/**
- * Convenience method to canonicalise a JSON capability expression
+ * Convenience method to canonicalise a JSON capability expression.
*
* @param capability a capability string, which is the JSON text for the capability
* @return a capability string which has been canonicalised
* @throws AblyException if there is an error processing the given string
* (if for example it is not valid JSON)
*/
- public static final String c14n(String capability) throws AblyException {
- if(capability == null || capability.isEmpty()) return "";
+ public static String c14n(String capability) throws AblyException {
+ if (capability == null || capability.isEmpty()) return capability;
try {
JsonObject json = (JsonObject)gsonParser.parse(capability);
return (new Capability(json)).toString();
- } catch(ClassCastException e) {
- throw AblyException.fromThrowable(e);
- } catch(JsonParseException e) {
+ } catch(ClassCastException | JsonParseException e) {
throw AblyException.fromThrowable(e);
}
}
diff --git a/lib/src/test/java/io/ably/lib/types/CapabilityTest.java b/lib/src/test/java/io/ably/lib/types/CapabilityTest.java
new file mode 100644
index 000000000..bf3099da8
--- /dev/null
+++ b/lib/src/test/java/io/ably/lib/types/CapabilityTest.java
@@ -0,0 +1,23 @@
+package io.ably.lib.types;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import org.junit.Test;
+
+public class CapabilityTest {
+
+ @Test
+ public void c14n_sendNull_returnsNull() throws AblyException {
+ String returnedValue = Capability.c14n(null);
+
+ assertNull(returnedValue);
+ }
+
+ @Test
+ public void c14n_sendEmptyString_returnsEmptyString() throws AblyException {
+ String returnedValue = Capability.c14n("");
+
+ assertEquals("", returnedValue);
+ }
+}
From 4df8bf783f2a21c5e1f286e830194faac6a1ef1c Mon Sep 17 00:00:00 2001
From: Sergii Zhevzhyk
Date: Fri, 19 Feb 2021 00:31:01 +0100
Subject: [PATCH 022/899] Remove capability query parameter if it is empty
string
---
lib/src/main/java/io/ably/lib/types/Capability.java | 2 +-
lib/src/test/java/io/ably/lib/types/CapabilityTest.java | 3 +--
2 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/lib/src/main/java/io/ably/lib/types/Capability.java b/lib/src/main/java/io/ably/lib/types/Capability.java
index d020e1444..46a6f2419 100644
--- a/lib/src/main/java/io/ably/lib/types/Capability.java
+++ b/lib/src/main/java/io/ably/lib/types/Capability.java
@@ -27,7 +27,7 @@ public class Capability {
* (if for example it is not valid JSON)
*/
public static String c14n(String capability) throws AblyException {
- if (capability == null || capability.isEmpty()) return capability;
+ if (capability == null || capability.isEmpty()) return null;
try {
JsonObject json = (JsonObject)gsonParser.parse(capability);
return (new Capability(json)).toString();
diff --git a/lib/src/test/java/io/ably/lib/types/CapabilityTest.java b/lib/src/test/java/io/ably/lib/types/CapabilityTest.java
index bf3099da8..74efbee91 100644
--- a/lib/src/test/java/io/ably/lib/types/CapabilityTest.java
+++ b/lib/src/test/java/io/ably/lib/types/CapabilityTest.java
@@ -1,6 +1,5 @@
package io.ably.lib.types;
-import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import org.junit.Test;
@@ -18,6 +17,6 @@ public void c14n_sendNull_returnsNull() throws AblyException {
public void c14n_sendEmptyString_returnsEmptyString() throws AblyException {
String returnedValue = Capability.c14n("");
- assertEquals("", returnedValue);
+ assertNull(returnedValue);
}
}
From bc738a8501f6a61aa8dfe11e792d5caf13b7044e Mon Sep 17 00:00:00 2001
From: Quintin Willison
Date: Mon, 1 Mar 2021 17:35:34 +0000
Subject: [PATCH 023/899] Unregister the ConnectionWaiter instance (used when
the authentication token changes) when it falls out of scope.
Also refactors the code that calls this instance to make it more readable (avoiding use of continue, for example).
---
.../ably/lib/transport/ConnectionManager.java | 153 +++++++++++-------
1 file changed, 91 insertions(+), 62 deletions(-)
diff --git a/lib/src/main/java/io/ably/lib/transport/ConnectionManager.java b/lib/src/main/java/io/ably/lib/transport/ConnectionManager.java
index 249b932b4..c7cfb1b8f 100644
--- a/lib/src/main/java/io/ably/lib/transport/ConnectionManager.java
+++ b/lib/src/main/java/io/ably/lib/transport/ConnectionManager.java
@@ -453,14 +453,18 @@ public boolean isActive() {
return currentState.queueEvents || currentState.sendEvents;
}
- /*************************************
- * a class that listens for currentState change
- * events for in-place authorization
- *************************************/
-
+ /**
+ * Listens for connection state changes.
+ *
+ * The close() method must be called when the ConnectionWaiter is no longer needed.
+ */
private class ConnectionWaiter implements ConnectionStateListener {
private ConnectionStateChange change;
+ private boolean closed = false;
+ /**
+ * Create a ConnectionWaiter as a connection listener.
+ */
private ConnectionWaiter() {
connection.on(this);
}
@@ -469,6 +473,10 @@ private ConnectionWaiter() {
* Wait for a currentState change notification
*/
private synchronized ErrorInfo waitForChange() {
+ if (closed) {
+ throw new IllegalStateException("Already closed.");
+ }
+
Log.d(TAG, "ConnectionWaiter.waitFor()");
if (change == null) {
try { wait(); } catch(InterruptedException e) {}
@@ -483,11 +491,22 @@ private synchronized ErrorInfo waitForChange() {
* ConnectionStateListener interface
*/
@Override
- public void onConnectionStateChanged(ConnectionStateChange state) {
- synchronized(this) {
- change = state;
- notify();
+ public synchronized void onConnectionStateChanged(ConnectionStateChange state) {
+ change = state;
+ notify();
+ }
+
+ /**
+ * Remove this ConnectionWaiter as a connection listener.
+ */
+ private void close() {
+ // This method is explicitly not synchronized. There may be a case for this in the
+ // future, however its addition is designed to be lightweight with minimal impact.
+ if (closed) {
+ return;
}
+ closed = true;
+ connection.off(this);
}
}
@@ -881,64 +900,74 @@ public void run() {
* the current connection to use that token; or if not currently connected,
* to connect with the token.
*/
- public void onAuthUpdated(String token, boolean waitForResponse) throws AblyException {
- ConnectionWaiter waiter = new ConnectionWaiter();
- switch(currentState.state) {
- case connected:
- /* (RTC8a) If the connection is in the CONNECTED currentState and
- * auth.authorize is called or Ably requests a re-authentication
- * (see RTN22), the client must obtain a new token, then send an
- * AUTH ProtocolMessage to Ably with an auth attribute
- * containing an AuthDetails object with the token string. */
- try {
- ProtocolMessage msg = new ProtocolMessage(ProtocolMessage.Action.auth);
- msg.auth = new ProtocolMessage.AuthDetails(token);
- send(msg, false, null);
- } catch (AblyException e) {
- /* The send failed. Close the transport; if a subsequent
- * reconnect succeeds, it will be with the new token. */
- Log.v(TAG, "onAuthUpdated: closing transport after send failure");
- transport.close();
- }
- break;
+ public void onAuthUpdated(final String token, final boolean waitForResponse) throws AblyException {
+ final ConnectionWaiter waiter = new ConnectionWaiter();
+ try {
+ switch(currentState.state) {
+ case connected:
+ /* (RTC8a) If the connection is in the CONNECTED currentState and
+ * auth.authorize is called or Ably requests a re-authentication
+ * (see RTN22), the client must obtain a new token, then send an
+ * AUTH ProtocolMessage to Ably with an auth attribute
+ * containing an AuthDetails object with the token string. */
+ try {
+ ProtocolMessage msg = new ProtocolMessage(ProtocolMessage.Action.auth);
+ msg.auth = new ProtocolMessage.AuthDetails(token);
+ send(msg, false, null);
+ } catch (AblyException e) {
+ /* The send failed. Close the transport; if a subsequent
+ * reconnect succeeds, it will be with the new token. */
+ Log.v(TAG, "onAuthUpdated: closing transport after send failure");
+ transport.close();
+ }
+ break;
- case connecting:
- /* Close the connecting transport. */
- Log.v(TAG, "onAuthUpdated: closing connecting transport");
- ErrorInfo disconnectError = new ErrorInfo("Aborting incomplete connection with superseded auth params", 503, 80003);
- requestState(new StateIndication(ConnectionState.disconnected, disconnectError, null, null));
- /* Start a new connection attempt. */
- connect();
- break;
+ case connecting:
+ /* Close the connecting transport. */
+ Log.v(TAG, "onAuthUpdated: closing connecting transport");
+ ErrorInfo disconnectError = new ErrorInfo("Aborting incomplete connection with superseded auth params", 503, 80003);
+ requestState(new StateIndication(ConnectionState.disconnected, disconnectError, null, null));
+ /* Start a new connection attempt. */
+ connect();
+ break;
- default:
- /* Start a new connection attempt. */
- connect();
- break;
- }
+ default:
+ /* Start a new connection attempt. */
+ connect();
+ break;
+ }
- if(!waitForResponse) {
- return;
- }
+ if(!waitForResponse) {
+ return;
+ }
- /* Wait for a currentState transition into anything other than connecting or
- * disconnected. Note that this includes the case that the connection
- * was already connected, and the AUTH message prompted the server to
- * send another connected message. */
- for (;;) {
- ErrorInfo reason = waiter.waitForChange();
- switch (currentState.state) {
- case connected:
- Log.v(TAG, "onAuthUpdated: got connected");
- return;
- case connecting:
- case disconnected:
- continue;
- default:
- /* suspended/closed/error: throw the error. */
- Log.v(TAG, "onAuthUpdated: throwing exception");
- throw AblyException.fromErrorInfo(reason);
+ /* Wait for a currentState transition into anything other than connecting or
+ * disconnected. Note that this includes the case that the connection
+ * was already connected, and the AUTH message prompted the server to
+ * send another connected message. */
+ boolean waitingForConnected = true;
+ while (waitingForConnected) {
+ final ErrorInfo reason = waiter.waitForChange();
+ final ConnectionState connectionState = currentState.state;
+ switch (connectionState) {
+ case connected:
+ Log.v(TAG, "onAuthUpdated: got connected");
+ waitingForConnected = false;
+ break;
+
+ case connecting:
+ case disconnected:
+ Log.v(TAG, "onAuthUpdated: " + connectionState);
+ break;
+
+ default:
+ /* suspended/closed/error: throw the error. */
+ Log.v(TAG, "onAuthUpdated: throwing exception");
+ throw AblyException.fromErrorInfo(reason);
+ }
}
+ } finally {
+ waiter.close();
}
}
From dbb501437d5370149b29f14ba44f12f9afcf4075 Mon Sep 17 00:00:00 2001
From: Quintin Willison
Date: Tue, 2 Mar 2021 10:19:09 +0000
Subject: [PATCH 024/899] Bump version number (patch).
---
README.md | 20 +++++++++----------
common.gradle | 2 +-
.../test/realtime/RealtimeHttpHeaderTest.java | 2 +-
.../io/ably/lib/test/rest/HttpHeaderTest.java | 2 +-
4 files changed, 13 insertions(+), 13 deletions(-)
diff --git a/README.md b/README.md
index 1e377a2f0..6c7e91a31 100644
--- a/README.md
+++ b/README.md
@@ -34,13 +34,13 @@ Reference the library by including a compile dependency reference in your gradle
For [Java](https://bintray.com/ably-io/ably/ably-java/_latestVersion):
```
-compile 'io.ably:ably-java:1.2.3'
+compile 'io.ably:ably-java:1.2.4'
```
For [Android](https://bintray.com/ably-io/ably/ably-android/_latestVersion):
```
-compile 'io.ably:ably-android:1.2.3'
+compile 'io.ably:ably-android:1.2.4'
```
The library is hosted on the [Jcenter repository](https://bintray.com/ably-io/ably), so you need to ensure that the repo is referenced also; IDEs will typically include this by default:
@@ -609,15 +609,15 @@ Configuration of Run/Debug configurations for running the unit tests on Android
This library uses [semantic versioning](http://semver.org/). For each release, the following needs to be done:
-1. Create a branch for the release, named like `release/1.2.3`
+1. Create a branch for the release, named like `release/1.2.4`
2. Replace all references of the current version number with the new version number (check this file [README.md](./README.md) and [common.gradle](./common.gradle)) and commit the changes
3. Run [`github_changelog_generator`](https://github.com/skywinder/Github-Changelog-Generator) to update the [CHANGELOG](./CHANGELOG.md):
- * This might work: `github_changelog_generator -u ably -p ably-java --header-label="# Changelog" --release-branch=release/1.2.3 --future-release=v1.2.3`
- * But your mileage may vary as it can error. Perhaps more reliable is something like: `github_changelog_generator -u ably -p ably-java --since-tag v1.2.2 --output delta.md` and then manually merge the delta contents in to the main change log
+ * This might work: `github_changelog_generator -u ably -p ably-java --header-label="# Changelog" --release-branch=release/1.2.4 --future-release=v1.2.4`
+ * But your mileage may vary as it can error. Perhaps more reliable is something like: `github_changelog_generator -u ably -p ably-java --since-tag v1.2.3 --output delta.md` and then manually merge the delta contents in to the main change log
4. Commit [CHANGELOG](./CHANGELOG.md)
5. Make a PR against `main`
6. Once the PR is approved, merge it into `main`
-7. Add a tag and push to origin - e.g.: `git tag v1.2.3 && git push origin v1.2.3`
+7. Add a tag and push to origin - e.g.: `git tag v1.2.4 && git push origin v1.2.4`
8. Create the release on Github including populating the release notes (needed so JFrog can pull them in)
9. Assemble and Upload ([see below](#publishing-to-jcenter-and-maven-central) for details) - but the overall order to follow is:
1. Upload to Bintray and use the pushed tag, which will pull in the associated release notes
@@ -645,10 +645,10 @@ We publish to:
The `java` release process goes as follows:
-* Go to the home page for the package; eg https://bintray.com/ably-io/ably/ably-java. Select Add a version, enter the new version such as "1.2.3" in name and save
+* Go to the home page for the package; eg https://bintray.com/ably-io/ably/ably-java. Select Add a version, enter the new version such as "1.2.4" in name and save
* Run `./gradlew java:assembleRelease` locally to generate the files
-* Open local relative folder in Finder, such as `./java/build/release/1.2.3/io/ably/ably-java/1.2.3`
-* Go to the new version in JFrog Bintray; eg https://bintray.com/ably-io/ably/ably-java/1.2.3, then click on the link to upload via the UI in the "Upload files" section
+* Open local relative folder in Finder, such as `./java/build/release/1.2.4/io/ably/ably-java/1.2.4`
+* Go to the new version in JFrog Bintray; eg https://bintray.com/ably-io/ably/ably-java/1.2.4, then click on the link to upload via the UI in the "Upload files" section
* Drag in the files from Finder, just the `.jar` files and the `.pom` file. JFrog will fill in the "Target Path" box after you drop the files in. Click the "Upload" button.
* You will see a notice something like "4 unpublished files in your version. Will be deleted in 6 days and 22 hours. Publish all or Delete all unpublished files.", make sure you click "Publish all". Wait a few minutes and check that what's uploaded looks like what was uploaded for previous releases. The `maven-metadata` files are created by JFrog.
* Update the README text in Bintray (version number needs incrementing).
@@ -656,7 +656,7 @@ The `java` release process goes as follows:
Similarly for the `android` release at https://bintray.com/ably-io/ably/ably-android:
* Run `./gradlew android:assembleRelease` locally to generate the files, and drag in the files in
-`./android/build/release/1.2.3/io/ably/ably-android/1.2.3`.
+`./android/build/release/1.2.4/io/ably/ably-android/1.2.4`.
* In this case upload the `.jar` files, the `.pom` file and the `.aar` file.
#### Releasing to Maven Central (Sonatype Nexus)
diff --git a/common.gradle b/common.gradle
index b9f2e405c..6168e3837 100644
--- a/common.gradle
+++ b/common.gradle
@@ -4,7 +4,7 @@ repositories {
}
group = 'io.ably'
-version = '1.2.3'
+version = '1.2.4'
description = 'Ably java client library'
tasks.withType(Javadoc) {
diff --git a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeHttpHeaderTest.java b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeHttpHeaderTest.java
index ba320bf00..9a5c157ca 100644
--- a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeHttpHeaderTest.java
+++ b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeHttpHeaderTest.java
@@ -91,7 +91,7 @@ public void realtime_websocket_param_test() {
* Defaults.ABLY_LIB_PARAM, as ultimately the request param has been derived from those values.
*/
assertEquals("Verify correct lib version", requestParameters.get("lib"),
- Collections.singletonList("java-1.2.3"));
+ Collections.singletonList("java-1.2.4"));
/* Spec RTN2a */
assertEquals("Verify correct format", requestParameters.get("format"),
diff --git a/lib/src/test/java/io/ably/lib/test/rest/HttpHeaderTest.java b/lib/src/test/java/io/ably/lib/test/rest/HttpHeaderTest.java
index b227145f5..c4cd50d5b 100644
--- a/lib/src/test/java/io/ably/lib/test/rest/HttpHeaderTest.java
+++ b/lib/src/test/java/io/ably/lib/test/rest/HttpHeaderTest.java
@@ -84,7 +84,7 @@ public void header_lib_channel_publish() {
*/
Assert.assertNotNull("Expected headers", headers);
Assert.assertEquals(headers.get("x-ably-version"), "1.2");
- Assert.assertEquals(headers.get("x-ably-lib"), "java-1.2.3");
+ Assert.assertEquals(headers.get("x-ably-lib"), "java-1.2.4");
} catch (AblyException e) {
e.printStackTrace();
Assert.fail("header_lib_channel_publish: Unexpected exception");
From 5baa0207175824863f25d9d572e20c26f8075a32 Mon Sep 17 00:00:00 2001
From: Quintin Willison
Date: Tue, 2 Mar 2021 10:19:32 +0000
Subject: [PATCH 025/899] Add change log entry.
---
CHANGELOG.md | 26 ++++++++++++++++++++++++++
1 file changed, 26 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b7879cc83..6bdaa253d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,31 @@
# Change Log
+## [v1.2.4](https://github.com/ably/ably-java/tree/v1.2.4)
+
+[Full Changelog](https://github.com/ably/ably-java/compare/v1.2.3...v1.2.4)
+
+**Fixed bugs:**
+
+- Many instances of ConnectionWaiter spawned while app is running, with authentication token flow [\#651](https://github.com/ably/ably-java/issues/651)
+- capability tokendetails adds to HTTP Request as a query parameter [\#647](https://github.com/ably/ably-java/issues/647)
+- ClientOptions idempotentRestPublishing default may be wrong [\#590](https://github.com/ably/ably-java/issues/590)
+- Presence blocking get sometimes has missing members [\#467](https://github.com/ably/ably-java/issues/467)
+- Remove empty capability query parameter [\#648](https://github.com/ably/ably-java/pull/648) ([vzhikserg](https://github.com/vzhikserg))
+- Add unit test for idempotentRestPublishing in ClientOptions [\#636](https://github.com/ably/ably-java/pull/636) ([vzhikserg](https://github.com/vzhikserg))
+- Fix Member Presence [\#607](https://github.com/ably/ably-java/pull/607) ([sacOO7](https://github.com/sacOO7))
+
+**Merged pull requests:**
+
+- Unregister ConnectionWaiter listeners once connected [\#652](https://github.com/ably/ably-java/pull/652) ([QuintinWillison](https://github.com/QuintinWillison))
+- Update references from 1 -\> l to match client spec [\#646](https://github.com/ably/ably-java/pull/646) ([natdempk](https://github.com/natdempk))
+- Add workflow status badges [\#645](https://github.com/ably/ably-java/pull/645) ([QuintinWillison](https://github.com/QuintinWillison))
+- Add maintainers file [\#644](https://github.com/ably/ably-java/pull/644) ([niksilver](https://github.com/niksilver))
+- Add workflows [\#643](https://github.com/ably/ably-java/pull/643) ([QuintinWillison](https://github.com/QuintinWillison))
+- Fix CI pipeline [\#642](https://github.com/ably/ably-java/pull/642) ([vzhikserg](https://github.com/vzhikserg))
+- Fix/doc 233 update readme [\#641](https://github.com/ably/ably-java/pull/641) ([tbedford](https://github.com/tbedford))
+- Log error message to get clear understanding of exception [\#632](https://github.com/ably/ably-java/pull/632) ([sacOO7](https://github.com/sacOO7))
+- Refactor MessageExtras [\#595](https://github.com/ably/ably-java/pull/595) ([sacOO7](https://github.com/sacOO7))
+
## [v1.2.3](https://github.com/ably/ably-java/tree/v1.2.3)
[Full Changelog](https://github.com/ably/ably-java/compare/v1.2.2...v1.2.3)
From 723c43bc24d0e17e581f01358af9685d29536236 Mon Sep 17 00:00:00 2001
From: Quintin Willison
Date: Tue, 2 Mar 2021 11:06:20 +0000
Subject: [PATCH 026/899] Always use a concurrent hash map for channel maps.
---
.../java/io/ably/lib/realtime/AblyRealtime.java | 4 ----
.../main/java/io/ably/lib/rest/AblyBase.java | 4 ----
.../main/java/io/ably/lib/util/InternalMap.java | 17 ++++++++++++-----
3 files changed, 12 insertions(+), 13 deletions(-)
diff --git a/lib/src/main/java/io/ably/lib/realtime/AblyRealtime.java b/lib/src/main/java/io/ably/lib/realtime/AblyRealtime.java
index a97205cb6..a5da0f3f8 100644
--- a/lib/src/main/java/io/ably/lib/realtime/AblyRealtime.java
+++ b/lib/src/main/java/io/ably/lib/realtime/AblyRealtime.java
@@ -130,10 +130,6 @@ public interface Channels extends ReadOnlyMap {
}
private class InternalChannels extends InternalMap implements Channels, ConnectionManager.Channels {
- private InternalChannels() {
- super(new ConcurrentHashMap());
- }
-
/**
* Get the named channel; if it does not already exist,
* create it with default options.
diff --git a/lib/src/main/java/io/ably/lib/rest/AblyBase.java b/lib/src/main/java/io/ably/lib/rest/AblyBase.java
index d5be54d0c..8e8e8f763 100644
--- a/lib/src/main/java/io/ably/lib/rest/AblyBase.java
+++ b/lib/src/main/java/io/ably/lib/rest/AblyBase.java
@@ -105,10 +105,6 @@ public interface Channels extends ReadOnlyMap {
}
private class InternalChannels extends InternalMap implements Channels {
- InternalChannels() {
- super(new HashMap());
- }
-
@Override
public Channel get(String channelName) {
try {
diff --git a/lib/src/main/java/io/ably/lib/util/InternalMap.java b/lib/src/main/java/io/ably/lib/util/InternalMap.java
index 66d4c72c3..3ab325ea7 100644
--- a/lib/src/main/java/io/ably/lib/util/InternalMap.java
+++ b/lib/src/main/java/io/ably/lib/util/InternalMap.java
@@ -2,15 +2,22 @@
import java.util.Map;
import java.util.Map.Entry;
+import java.util.concurrent.ConcurrentHashMap;
import io.ably.lib.types.ReadOnlyMap;
+/**
+ * A map implemented using a {@link ConcurrentHashMap}. This class is a base class for other classes
+ * which are designed to be internal to the library, specifically as regards access to the map
+ * field.
+ *
+ * This class exposes a {@link ReadOnlyMap} which is safe to be exposed in our public API.
+ *
+ * @param Key type.
+ * @param Value type.
+ */
public abstract class InternalMap implements ReadOnlyMap {
- protected final Map map;
-
- public InternalMap(final Map map) {
- this.map = map;
- }
+ protected final Map map = new ConcurrentHashMap<>();
@Override
public final boolean containsKey(final Object key) {
From ef3d136cc7f2ab684142c0003428da2cf8cd977a Mon Sep 17 00:00:00 2001
From: Quintin Willison
Date: Tue, 2 Mar 2021 13:37:58 +0000
Subject: [PATCH 027/899] Improve the channel get implementation so that it
only makes a single call to the concurrent hash map.
---
.../io/ably/lib/realtime/AblyRealtime.java | 22 ++++++++++---------
.../java/io/ably/lib/util/InternalMap.java | 3 ++-
2 files changed, 14 insertions(+), 11 deletions(-)
diff --git a/lib/src/main/java/io/ably/lib/realtime/AblyRealtime.java b/lib/src/main/java/io/ably/lib/realtime/AblyRealtime.java
index a5da0f3f8..0ac891ead 100644
--- a/lib/src/main/java/io/ably/lib/realtime/AblyRealtime.java
+++ b/lib/src/main/java/io/ably/lib/realtime/AblyRealtime.java
@@ -2,7 +2,6 @@
import java.util.Iterator;
import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
import io.ably.lib.rest.AblyRest;
import io.ably.lib.transport.ConnectionManager;
@@ -144,21 +143,24 @@ public Channel get(String channelName) {
}
@Override
- public Channel get(String channelName, ChannelOptions channelOptions) throws AblyException {
- Channel channel = map.get(channelName);
- if (channel != null) {
+ public Channel get(final String channelName, final ChannelOptions channelOptions) throws AblyException {
+ // We're not using computeIfAbsent because that requires Java 1.8.
+ // Hence there's the slight inefficiency of creating newChannel when it may not be
+ // needed because there is an existingChannel.
+ final Channel newChannel = new Channel(AblyRealtime.this, channelName, channelOptions);
+ final Channel existingChannel = map.putIfAbsent(channelName, newChannel);
+
+ if (existingChannel != null) {
if (channelOptions != null) {
- if (channel.shouldReattachToSetOptions(channelOptions)) {
+ if (existingChannel.shouldReattachToSetOptions(channelOptions)) {
throw AblyException.fromErrorInfo(new ErrorInfo("Channels.get() cannot be used to set channel options that would cause the channel to reattach. Please, use Channel.setOptions() instead.", 40000, 400));
}
- channel.setOptions(channelOptions);
+ existingChannel.setOptions(channelOptions);
}
- return channel;
+ return existingChannel;
}
- channel = new Channel(AblyRealtime.this, channelName, channelOptions);
- map.put(channelName, channel);
- return channel;
+ return newChannel;
}
@Override
diff --git a/lib/src/main/java/io/ably/lib/util/InternalMap.java b/lib/src/main/java/io/ably/lib/util/InternalMap.java
index 3ab325ea7..ca5ddf5bf 100644
--- a/lib/src/main/java/io/ably/lib/util/InternalMap.java
+++ b/lib/src/main/java/io/ably/lib/util/InternalMap.java
@@ -3,6 +3,7 @@
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
import io.ably.lib.types.ReadOnlyMap;
@@ -17,7 +18,7 @@
* @param Value type.
*/
public abstract class InternalMap implements ReadOnlyMap {
- protected final Map map = new ConcurrentHashMap<>();
+ protected final ConcurrentMap map = new ConcurrentHashMap<>();
@Override
public final boolean containsKey(final Object key) {
From e497bdac19519962e71eadc30c1ade9da5e364e0 Mon Sep 17 00:00:00 2001
From: sacOO7
Date: Tue, 2 Mar 2021 22:49:01 +0530
Subject: [PATCH 028/899] Fixed random Message id, added test for the same
---
lib/src/main/java/io/ably/lib/util/Crypto.java | 2 +-
lib/src/test/java/io/ably/lib/util/CryptoTest.java | 6 ++++++
2 files changed, 7 insertions(+), 1 deletion(-)
diff --git a/lib/src/main/java/io/ably/lib/util/Crypto.java b/lib/src/main/java/io/ably/lib/util/Crypto.java
index b20915b02..56ea62fb9 100644
--- a/lib/src/main/java/io/ably/lib/util/Crypto.java
+++ b/lib/src/main/java/io/ably/lib/util/Crypto.java
@@ -321,7 +321,7 @@ private static int getPaddedLength(int plaintextLength) {
public static String getRandomMessageId() {
byte[] entropy = new byte[9];
secureRandom.nextBytes(entropy);
- return Base64Coder.encode(entropy).toString();
+ return Base64Coder.encodeToString(entropy);
}
/**
diff --git a/lib/src/test/java/io/ably/lib/util/CryptoTest.java b/lib/src/test/java/io/ably/lib/util/CryptoTest.java
index a9fec56de..f89541be3 100644
--- a/lib/src/test/java/io/ably/lib/util/CryptoTest.java
+++ b/lib/src/test/java/io/ably/lib/util/CryptoTest.java
@@ -199,4 +199,10 @@ private static byte[] msgPacked(final String name, final byte[] data, final Stri
return out.toByteArray();
}
+
+ @Test
+ public void getRandomId() {
+ String randomId = Crypto.getRandomMessageId();
+ assertEquals(12, randomId.length());
+ }
}
From e318ea3807e77705095c02cfe5652486db5f39f8 Mon Sep 17 00:00:00 2001
From: Quintin Willison
Date: Tue, 2 Mar 2021 19:25:44 +0000
Subject: [PATCH 029/899] Remove unused imports.
---
lib/src/main/java/io/ably/lib/rest/AblyBase.java | 2 --
lib/src/main/java/io/ably/lib/util/InternalMap.java | 1 -
2 files changed, 3 deletions(-)
diff --git a/lib/src/main/java/io/ably/lib/rest/AblyBase.java b/lib/src/main/java/io/ably/lib/rest/AblyBase.java
index 8e8e8f763..72c4d0392 100644
--- a/lib/src/main/java/io/ably/lib/rest/AblyBase.java
+++ b/lib/src/main/java/io/ably/lib/rest/AblyBase.java
@@ -1,7 +1,5 @@
package io.ably.lib.rest;
-import java.util.HashMap;
-
import io.ably.annotation.Experimental;
import io.ably.lib.http.AsyncHttpScheduler;
import io.ably.lib.http.Http;
diff --git a/lib/src/main/java/io/ably/lib/util/InternalMap.java b/lib/src/main/java/io/ably/lib/util/InternalMap.java
index ca5ddf5bf..d732e9b40 100644
--- a/lib/src/main/java/io/ably/lib/util/InternalMap.java
+++ b/lib/src/main/java/io/ably/lib/util/InternalMap.java
@@ -1,6 +1,5 @@
package io.ably.lib.util;
-import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
From 6981182ae826a8e523e9d49325dae3dae14ccff5 Mon Sep 17 00:00:00 2001
From: Quintin Willison
Date: Tue, 2 Mar 2021 19:41:37 +0000
Subject: [PATCH 030/899] Remove unnecessary private method, allowing more
private fields to be final.
---
.../java/io/ably/lib/transport/Hosts.java | 19 ++++++-------------
1 file changed, 6 insertions(+), 13 deletions(-)
diff --git a/lib/src/main/java/io/ably/lib/transport/Hosts.java b/lib/src/main/java/io/ably/lib/transport/Hosts.java
index 3ce5bcaa4..729ff0aa3 100644
--- a/lib/src/main/java/io/ably/lib/transport/Hosts.java
+++ b/lib/src/main/java/io/ably/lib/transport/Hosts.java
@@ -12,10 +12,10 @@
* Object to encapsulate primary host name and shuffled fallback host names.
*/
public class Hosts {
- private String primaryHost;
+ private final String primaryHost;
private String prefHost;
private long prefHostExpiry;
- boolean primaryHostIsDefault;
+ private final boolean primaryHostIsDefault;
private final String defaultHost;
private final String[] fallbackHosts;
private final boolean fallbackHostsIsDefault;
@@ -38,7 +38,7 @@ public class Hosts {
* code, but the results are ignored because ConnectionManager then calls
* setHost() and fallback is not used.
*/
- public Hosts(String primaryHost, String defaultHost, ClientOptions options) throws AblyException {
+ public Hosts(final String primaryHost, final String defaultHost, final ClientOptions options) throws AblyException {
this.defaultHost = defaultHost;
this.fallbackHostsUseDefault = options.fallbackHostsUseDefault;
boolean hasCustomPrimaryHost = primaryHost != null && !primaryHost.equalsIgnoreCase(defaultHost);
@@ -60,15 +60,16 @@ public Hosts(String primaryHost, String defaultHost, ClientOptions options) thro
}
if (hasCustomPrimaryHost) {
- setPrimaryHost(primaryHost);
+ this.primaryHost = primaryHost;
if (options.environment != null) {
/* TO3k2: It is never valid to provide both a restHost and environment value
* TO3k3: It is never valid to provide both a realtimeHost and environment value */
throw AblyException.fromErrorInfo(new ErrorInfo("cannot set both restHost/realtimeHost and environment options", 40000, 400));
}
} else {
- setPrimaryHost(isProduction ? defaultHost : options.environment + "-" + defaultHost);
+ this.primaryHost = isProduction ? defaultHost : options.environment + "-" + defaultHost;
}
+ primaryHostIsDefault = this.primaryHost.equalsIgnoreCase(defaultHost);
fallbackHostsIsDefault = Arrays.equals(Defaults.HOST_FALLBACKS, tempFallbackHosts);
fallbackHosts = tempFallbackHosts == null ? new String[] {} : tempFallbackHosts.clone();
@@ -77,14 +78,6 @@ public Hosts(String primaryHost, String defaultHost, ClientOptions options) thro
fallbackRetryTimeout = options.fallbackRetryTimeout;
}
- /**
- * set primary hostname
- */
- private void setPrimaryHost(String primaryHost) {
- this.primaryHost = primaryHost;
- primaryHostIsDefault = primaryHost.equalsIgnoreCase(defaultHost);
- }
-
/**
* set preferred hostname, which might not be the primary
*/
From c080c687fb6cfb05c50d68358ae27ece99db3ecf Mon Sep 17 00:00:00 2001
From: Quintin Willison
Date: Tue, 2 Mar 2021 20:02:24 +0000
Subject: [PATCH 031/899] Encapsulate mutable state.
---
.../java/io/ably/lib/transport/Hosts.java | 68 +++++++++++--------
1 file changed, 41 insertions(+), 27 deletions(-)
diff --git a/lib/src/main/java/io/ably/lib/transport/Hosts.java b/lib/src/main/java/io/ably/lib/transport/Hosts.java
index 729ff0aa3..bc788efff 100644
--- a/lib/src/main/java/io/ably/lib/transport/Hosts.java
+++ b/lib/src/main/java/io/ably/lib/transport/Hosts.java
@@ -13,8 +13,6 @@
*/
public class Hosts {
private final String primaryHost;
- private String prefHost;
- private long prefHostExpiry;
private final boolean primaryHostIsDefault;
private final String defaultHost;
private final String[] fallbackHosts;
@@ -22,6 +20,7 @@ public class Hosts {
private final boolean fallbackHostsUseDefault;
private final long fallbackRetryTimeout;
+ private Preferred preferred = new Preferred();
/**
* Create Hosts object
@@ -81,25 +80,19 @@ public Hosts(final String primaryHost, final String defaultHost, final ClientOpt
/**
* set preferred hostname, which might not be the primary
*/
- public void setPreferredHost(String prefHost, boolean temporary) {
- if(prefHost.equals(this.prefHost)) {
+ public void setPreferredHost(final String prefHost, final boolean temporary) {
+ if (preferred.isHost(prefHost)) {
/* a successful request against a fallback; don't update the expiry time */
return;
}
- if(prefHost.equals(this.primaryHost)) {
+ if(prefHost.equals(primaryHost)) {
/* a successful request against the primary host; reset */
- clearPreferredHost();
+ preferred.clear();
} else {
- this.prefHost = prefHost;
- this.prefHostExpiry = temporary ? System.currentTimeMillis() + fallbackRetryTimeout : 0;
+ preferred.setHost(prefHost, temporary ? System.currentTimeMillis() + fallbackRetryTimeout : 0);
}
}
- private void clearPreferredHost() {
- this.prefHost = null;
- this.prefHostExpiry = 0;
- }
-
/**
* Get primary host name
*/
@@ -111,17 +104,8 @@ public String getPrimaryHost() {
* Get preferred host name (taking into account any affinity to a fallback: see RSC15f)
*/
public String getPreferredHost() {
- checkPreferredHostExpiry();
- return (prefHost == null) ? primaryHost : prefHost;
- }
-
- private String checkPreferredHostExpiry() {
- /* reset if expired */
- if(prefHostExpiry > 0 && prefHostExpiry <= System.currentTimeMillis()) {
- prefHostExpiry = 0;
- prefHost = null;
- }
- return prefHost;
+ final String host = preferred.getHostOrClearIfExpired();
+ return (host == null) ? primaryHost : host;
}
/**
@@ -142,9 +126,9 @@ public String getFallback(String lastHost) {
if (!primaryHostIsDefault && !fallbackHostsUseDefault && fallbackHostsIsDefault)
return null;
idx = 0;
- } else if(lastHost.equals(checkPreferredHostExpiry())) {
+ } else if(lastHost.equals(preferred.getHostOrClearIfExpired())) {
/* RSC15f: there was a failure on an unexpired, cached fallback; so try again using the primary */
- clearPreferredHost();
+ preferred.clear();
return primaryHost;
} else {
/* Onto next fallback. */
@@ -164,9 +148,39 @@ public int fallbackHostsRemaining(String candidateHost) {
if(fallbackHosts == null) {
return 0;
}
- if(candidateHost.equals(primaryHost) || candidateHost.equals(prefHost)) {
+ if(candidateHost.equals(primaryHost) || candidateHost.equals(preferred.getHost())) {
return fallbackHosts.length;
}
return fallbackHosts.length - Arrays.asList(fallbackHosts).indexOf(candidateHost) - 1;
}
+
+ private static class Preferred {
+ private String host;
+ private long expiry;
+
+ public void clear() {
+ host = null;
+ expiry = 0;
+ }
+
+ public boolean isHost(final String host) {
+ return (this.host == null) ? (host == null) : this.host.equals(host);
+ }
+
+ public void setHost(final String host, final long expiry) {
+ this.host = host;
+ this.expiry = expiry;
+ }
+
+ public String getHostOrClearIfExpired() {
+ if(expiry > 0 && expiry <= System.currentTimeMillis()) {
+ clear(); // expired, so reset
+ }
+ return host;
+ }
+
+ public String getHost() {
+ return host;
+ }
+ }
}
From 0f28d7b1c070d1287beba243334be35ca9056807 Mon Sep 17 00:00:00 2001
From: Quintin Willison
Date: Tue, 2 Mar 2021 20:05:34 +0000
Subject: [PATCH 032/899] Make the Hosts class safe to be called from any
thread.
---
lib/src/main/java/io/ably/lib/transport/Hosts.java | 12 +++++++-----
1 file changed, 7 insertions(+), 5 deletions(-)
diff --git a/lib/src/main/java/io/ably/lib/transport/Hosts.java b/lib/src/main/java/io/ably/lib/transport/Hosts.java
index bc788efff..a4559b4f6 100644
--- a/lib/src/main/java/io/ably/lib/transport/Hosts.java
+++ b/lib/src/main/java/io/ably/lib/transport/Hosts.java
@@ -10,6 +10,8 @@
/**
* Object to encapsulate primary host name and shuffled fallback host names.
+ *
+ * Methods on this class are safe to be called from any thread.
*/
public class Hosts {
private final String primaryHost;
@@ -20,7 +22,7 @@ public class Hosts {
private final boolean fallbackHostsUseDefault;
private final long fallbackRetryTimeout;
- private Preferred preferred = new Preferred();
+ private final Preferred preferred = new Preferred();
/**
* Create Hosts object
@@ -80,7 +82,7 @@ public Hosts(final String primaryHost, final String defaultHost, final ClientOpt
/**
* set preferred hostname, which might not be the primary
*/
- public void setPreferredHost(final String prefHost, final boolean temporary) {
+ public synchronized void setPreferredHost(final String prefHost, final boolean temporary) {
if (preferred.isHost(prefHost)) {
/* a successful request against a fallback; don't update the expiry time */
return;
@@ -103,7 +105,7 @@ public String getPrimaryHost() {
/**
* Get preferred host name (taking into account any affinity to a fallback: see RSC15f)
*/
- public String getPreferredHost() {
+ public synchronized String getPreferredHost() {
final String host = preferred.getHostOrClearIfExpired();
return (host == null) ? primaryHost : host;
}
@@ -115,7 +117,7 @@ public String getPreferredHost() {
* @return Successor host that can be used as a fallback.
* null, if there is no successor fallback available.
*/
- public String getFallback(String lastHost) {
+ public synchronized String getFallback(String lastHost) {
if (fallbackHosts == null)
return null;
int idx;
@@ -144,7 +146,7 @@ public String getFallback(String lastHost) {
return fallbackHosts[idx];
}
- public int fallbackHostsRemaining(String candidateHost) {
+ public synchronized int fallbackHostsRemaining(String candidateHost) {
if(fallbackHosts == null) {
return 0;
}
From 1356ec8011559542a87d258bd3bba70c79201277 Mon Sep 17 00:00:00 2001
From: Quintin Willison
Date: Wed, 3 Mar 2021 14:01:32 +0000
Subject: [PATCH 033/899] Bump version number (patch).
---
README.md | 20 +++++++++----------
common.gradle | 2 +-
.../test/realtime/RealtimeHttpHeaderTest.java | 2 +-
.../io/ably/lib/test/rest/HttpHeaderTest.java | 2 +-
4 files changed, 13 insertions(+), 13 deletions(-)
diff --git a/README.md b/README.md
index 6c7e91a31..0cd5864f9 100644
--- a/README.md
+++ b/README.md
@@ -34,13 +34,13 @@ Reference the library by including a compile dependency reference in your gradle
For [Java](https://bintray.com/ably-io/ably/ably-java/_latestVersion):
```
-compile 'io.ably:ably-java:1.2.4'
+compile 'io.ably:ably-java:1.2.5'
```
For [Android](https://bintray.com/ably-io/ably/ably-android/_latestVersion):
```
-compile 'io.ably:ably-android:1.2.4'
+compile 'io.ably:ably-android:1.2.5'
```
The library is hosted on the [Jcenter repository](https://bintray.com/ably-io/ably), so you need to ensure that the repo is referenced also; IDEs will typically include this by default:
@@ -609,15 +609,15 @@ Configuration of Run/Debug configurations for running the unit tests on Android
This library uses [semantic versioning](http://semver.org/). For each release, the following needs to be done:
-1. Create a branch for the release, named like `release/1.2.4`
+1. Create a branch for the release, named like `release/1.2.5`
2. Replace all references of the current version number with the new version number (check this file [README.md](./README.md) and [common.gradle](./common.gradle)) and commit the changes
3. Run [`github_changelog_generator`](https://github.com/skywinder/Github-Changelog-Generator) to update the [CHANGELOG](./CHANGELOG.md):
- * This might work: `github_changelog_generator -u ably -p ably-java --header-label="# Changelog" --release-branch=release/1.2.4 --future-release=v1.2.4`
- * But your mileage may vary as it can error. Perhaps more reliable is something like: `github_changelog_generator -u ably -p ably-java --since-tag v1.2.3 --output delta.md` and then manually merge the delta contents in to the main change log
+ * This might work: `github_changelog_generator -u ably -p ably-java --header-label="# Changelog" --release-branch=release/1.2.5 --future-release=v1.2.5`
+ * But your mileage may vary as it can error. Perhaps more reliable is something like: `github_changelog_generator -u ably -p ably-java --since-tag v1.2.4 --output delta.md` and then manually merge the delta contents in to the main change log
4. Commit [CHANGELOG](./CHANGELOG.md)
5. Make a PR against `main`
6. Once the PR is approved, merge it into `main`
-7. Add a tag and push to origin - e.g.: `git tag v1.2.4 && git push origin v1.2.4`
+7. Add a tag and push to origin - e.g.: `git tag v1.2.5 && git push origin v1.2.5`
8. Create the release on Github including populating the release notes (needed so JFrog can pull them in)
9. Assemble and Upload ([see below](#publishing-to-jcenter-and-maven-central) for details) - but the overall order to follow is:
1. Upload to Bintray and use the pushed tag, which will pull in the associated release notes
@@ -645,10 +645,10 @@ We publish to:
The `java` release process goes as follows:
-* Go to the home page for the package; eg https://bintray.com/ably-io/ably/ably-java. Select Add a version, enter the new version such as "1.2.4" in name and save
+* Go to the home page for the package; eg https://bintray.com/ably-io/ably/ably-java. Select Add a version, enter the new version such as "1.2.5" in name and save
* Run `./gradlew java:assembleRelease` locally to generate the files
-* Open local relative folder in Finder, such as `./java/build/release/1.2.4/io/ably/ably-java/1.2.4`
-* Go to the new version in JFrog Bintray; eg https://bintray.com/ably-io/ably/ably-java/1.2.4, then click on the link to upload via the UI in the "Upload files" section
+* Open local relative folder in Finder, such as `./java/build/release/1.2.5/io/ably/ably-java/1.2.5`
+* Go to the new version in JFrog Bintray; eg https://bintray.com/ably-io/ably/ably-java/1.2.5, then click on the link to upload via the UI in the "Upload files" section
* Drag in the files from Finder, just the `.jar` files and the `.pom` file. JFrog will fill in the "Target Path" box after you drop the files in. Click the "Upload" button.
* You will see a notice something like "4 unpublished files in your version. Will be deleted in 6 days and 22 hours. Publish all or Delete all unpublished files.", make sure you click "Publish all". Wait a few minutes and check that what's uploaded looks like what was uploaded for previous releases. The `maven-metadata` files are created by JFrog.
* Update the README text in Bintray (version number needs incrementing).
@@ -656,7 +656,7 @@ The `java` release process goes as follows:
Similarly for the `android` release at https://bintray.com/ably-io/ably/ably-android:
* Run `./gradlew android:assembleRelease` locally to generate the files, and drag in the files in
-`./android/build/release/1.2.4/io/ably/ably-android/1.2.4`.
+`./android/build/release/1.2.5/io/ably/ably-android/1.2.5`.
* In this case upload the `.jar` files, the `.pom` file and the `.aar` file.
#### Releasing to Maven Central (Sonatype Nexus)
diff --git a/common.gradle b/common.gradle
index 6168e3837..e373251bd 100644
--- a/common.gradle
+++ b/common.gradle
@@ -4,7 +4,7 @@ repositories {
}
group = 'io.ably'
-version = '1.2.4'
+version = '1.2.5'
description = 'Ably java client library'
tasks.withType(Javadoc) {
diff --git a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeHttpHeaderTest.java b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeHttpHeaderTest.java
index 9a5c157ca..13e04343e 100644
--- a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeHttpHeaderTest.java
+++ b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeHttpHeaderTest.java
@@ -91,7 +91,7 @@ public void realtime_websocket_param_test() {
* Defaults.ABLY_LIB_PARAM, as ultimately the request param has been derived from those values.
*/
assertEquals("Verify correct lib version", requestParameters.get("lib"),
- Collections.singletonList("java-1.2.4"));
+ Collections.singletonList("java-1.2.5"));
/* Spec RTN2a */
assertEquals("Verify correct format", requestParameters.get("format"),
diff --git a/lib/src/test/java/io/ably/lib/test/rest/HttpHeaderTest.java b/lib/src/test/java/io/ably/lib/test/rest/HttpHeaderTest.java
index c4cd50d5b..79c9d4ac7 100644
--- a/lib/src/test/java/io/ably/lib/test/rest/HttpHeaderTest.java
+++ b/lib/src/test/java/io/ably/lib/test/rest/HttpHeaderTest.java
@@ -84,7 +84,7 @@ public void header_lib_channel_publish() {
*/
Assert.assertNotNull("Expected headers", headers);
Assert.assertEquals(headers.get("x-ably-version"), "1.2");
- Assert.assertEquals(headers.get("x-ably-lib"), "java-1.2.4");
+ Assert.assertEquals(headers.get("x-ably-lib"), "java-1.2.5");
} catch (AblyException e) {
e.printStackTrace();
Assert.fail("header_lib_channel_publish: Unexpected exception");
From cd4ae2e004a8a8b3dc8a4d8d60a90f5158391330 Mon Sep 17 00:00:00 2001
From: Quintin Willison
Date: Wed, 3 Mar 2021 14:08:34 +0000
Subject: [PATCH 034/899] Add change log entry.
---
CHANGELOG.md | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6bdaa253d..da0eb0ef5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,21 @@
# Change Log
+## [v1.2.5](https://github.com/ably/ably-java/tree/v1.2.5)
+
+[Full Changelog](https://github.com/ably/ably-java/compare/v1.2.4...v1.2.5)
+
+**Fixed bugs:**
+
+- Crypto.getRandomMessageId isn't working as intended [\#654](https://github.com/ably/ably-java/issues/654)
+- Hosts class is not thread safe [\#650](https://github.com/ably/ably-java/issues/650)
+- AblyBase.InternalChannels is not thread-safe [\#649](https://github.com/ably/ably-java/issues/649)
+
+**Merged pull requests:**
+
+- Makes the Hosts class safe to be called from any thread [\#657](https://github.com/ably/ably-java/pull/657) ([QuintinWillison](https://github.com/QuintinWillison))
+- Fix getRandomMessageId [\#656](https://github.com/ably/ably-java/pull/656) ([sacOO7](https://github.com/sacOO7))
+- Improve channel map operations in respect of thread-safety [\#655](https://github.com/ably/ably-java/pull/655) ([QuintinWillison](https://github.com/QuintinWillison))
+
## [v1.2.4](https://github.com/ably/ably-java/tree/v1.2.4)
[Full Changelog](https://github.com/ably/ably-java/compare/v1.2.3...v1.2.4)
From 6b4be7fe4be0bdff21b0853a006d99c46783b55b Mon Sep 17 00:00:00 2001
From: Quintin Willison
Date: Fri, 5 Mar 2021 06:26:56 +0000
Subject: [PATCH 035/899] Remove assemble status badge.
It will come back again when we work on this issue:
https://github.com/ably/ably-java/issues/659
---
README.md | 1 -
1 file changed, 1 deletion(-)
diff --git a/README.md b/README.md
index 0cd5864f9..bd450e196 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,6 @@


-
| Android | Java |
|---------|------|
From 6eb2af3d8c4ec78b0c6edf3d46d5fda69ad2f6f8 Mon Sep 17 00:00:00 2001
From: Quintin Willison
Date: Fri, 12 Mar 2021 11:14:10 +0000
Subject: [PATCH 036/899] Conform license and copyright.
---
COPYRIGHT | 1 +
LICENSE | 183 +++++++++++++++++++++++++++++++++++++++++++++++++++---
2 files changed, 174 insertions(+), 10 deletions(-)
create mode 100644 COPYRIGHT
diff --git a/COPYRIGHT b/COPYRIGHT
new file mode 100644
index 000000000..f40cc374a
--- /dev/null
+++ b/COPYRIGHT
@@ -0,0 +1 @@
+Copyright 2015-2021 Ably Real-time Ltd (ably.com)
diff --git a/LICENSE b/LICENSE
index bf523cafe..d9a10c0d8 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,13 +1,176 @@
-Copyright 2015-2020 Ably Real-time Ltd (ably.com)
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
- http://www.apache.org/licenses/LICENSE-2.0
+ 1. Definitions.
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
From bec842e6a0677b695686a941e2d08437ad303525 Mon Sep 17 00:00:00 2001
From: uherrw1
Date: Thu, 25 Mar 2021 12:23:04 -0500
Subject: [PATCH 037/899] Changing Capability.addResource() to take varargs as
last parameter
Before this change, there were three methods:
* `addResource(String resource)`
** Example: `addResource("foo")`
* `addResource(String resource, String op)`
** Example: `addResource("foo", "bar")"`
* `addResource(String resource, String[] ops)`
** Example: `addResource("foo", new String[] {"bar", "baz"})"`
With this change, all three are replaced with:
* `addResource(String resource, String... ops)`
Which allows the user to do all three of the above without modifications to their code:
* `addResource("foo")`
* `addResource("foo", "bar")"`
* `addResource("foo", new String[] {"bar", "baz"})"`
While also allowing:
* addResource("foo", "bar", "baz")"`
This will not break any existing code. It's also makes it consistent with other methods in this project such as `EventEmitter.emit(Event, Object...)`
---
.../java/io/ably/lib/types/Capability.java | 28 ++-----------------
.../lib/test/rest/RestCapabilityTest.java | 24 ++++++++--------
2 files changed, 15 insertions(+), 37 deletions(-)
diff --git a/lib/src/main/java/io/ably/lib/types/Capability.java b/lib/src/main/java/io/ably/lib/types/Capability.java
index 46a6f2419..369f1cd74 100644
--- a/lib/src/main/java/io/ably/lib/types/Capability.java
+++ b/lib/src/main/java/io/ably/lib/types/Capability.java
@@ -59,37 +59,15 @@ private Capability(JsonObject json) {
* it is wholly replaced by the given set of operations.
*
* @param resource the resource string
- * @param ops a String[] of the operations permitted for this resource;
- * the array does not need to be sorted
+ * @param ops a String varargs of the operations permitted for this resource;
+ * the arguments do not need to be sorted
*/
- public void addResource(String resource, String[] ops) {
+ public void addResource(String resource, String... ops) {
JsonArray jsonOps = (JsonArray)gson.toJsonTree(ops);
json.add(resource, jsonOps);
dirty = true;
}
- /**
- * Add a resource to an existing Capability instance with the
- * given single operation. If the resource already exists,
- * it is wholly replaced by the given set of operations.
- *
- * @param resource the resource string
- * @param op a single operation String to be permitted for this resource;
- */
- public void addResource(String resource, String op) {
- addResource(resource, new String[]{op});
- }
-
- /**
- * Add a resource to an existing Capability instance with an
- * empty set of operations. If the resource already exists,
- * the effect is to reset its set of operations to empty.
- *
- * @param resource the resource string
- */
- public void addResource(String resource) {
- addResource(resource, new String[0]);
- }
/**
* Remove a resource from an existing Capability instance
*
diff --git a/lib/src/test/java/io/ably/lib/test/rest/RestCapabilityTest.java b/lib/src/test/java/io/ably/lib/test/rest/RestCapabilityTest.java
index fbaa61e96..312f59c7c 100644
--- a/lib/src/test/java/io/ably/lib/test/rest/RestCapabilityTest.java
+++ b/lib/src/test/java/io/ably/lib/test/rest/RestCapabilityTest.java
@@ -106,7 +106,7 @@ public void authcapability3() {
}
/**
- * Non-empty ops intersection
+ * Non-empty ops intersection
*/
@Test
public void authcapability4() {
@@ -116,7 +116,7 @@ public void authcapability4() {
authOptions.key = key.keyStr;
TokenParams tokenParams = new TokenParams();
Capability requestedCapability = new Capability();
- requestedCapability.addResource("channel2", new String[]{"presence", "subscribe"});
+ requestedCapability.addResource("channel2", "presence", "subscribe");
tokenParams.capability = requestedCapability.toString();
TokenDetails tokenDetails = ably.auth.requestToken(tokenParams, authOptions);
Capability expectedCapability = new Capability();
@@ -130,7 +130,7 @@ public void authcapability4() {
}
/**
- * Non-empty paths intersection
+ * Non-empty paths intersection
*/
@Test
public void authcapability5() {
@@ -140,8 +140,8 @@ public void authcapability5() {
authOptions.key = key.keyStr;
TokenParams tokenParams = new TokenParams();
Capability requestedCapability = new Capability();
- requestedCapability.addResource("channel2", new String[]{"presence", "subscribe"});
- requestedCapability.addResource("channelx", new String[]{"presence", "subscribe"});
+ requestedCapability.addResource("channel2", "presence", "subscribe");
+ requestedCapability.addResource("channelx", "presence", "subscribe");
tokenParams.capability = requestedCapability.toString();
TokenDetails tokenDetails = ably.auth.requestToken(tokenParams, authOptions);
Capability expectedCapability = new Capability();
@@ -155,7 +155,7 @@ public void authcapability5() {
}
/**
- * Wildcard ops intersection
+ * Wildcard ops intersection
*/
@Test
public void authcapability6() {
@@ -169,7 +169,7 @@ public void authcapability6() {
tokenParams.capability = requestedCapability.toString();
TokenDetails tokenDetails = ably.auth.requestToken(tokenParams, authOptions);
Capability expectedCapability = new Capability();
- expectedCapability.addResource("channel2", new String[]{"publish", "subscribe"});
+ expectedCapability.addResource("channel2", "publish", "subscribe");
assertNotNull("Expected token value", tokenDetails.token);
assertEquals("Unexpected capability", tokenDetails.capability, expectedCapability.toString());
} catch (AblyException e) {
@@ -185,11 +185,11 @@ public void authcapability7() {
authOptions.key = key.keyStr;
TokenParams tokenParams = new TokenParams();
Capability requestedCapability = new Capability();
- requestedCapability.addResource("channel6", new String[]{"publish", "subscribe"});
+ requestedCapability.addResource("channel6", "publish", "subscribe");
tokenParams.capability = requestedCapability.toString();
TokenDetails tokenDetails = ably.auth.requestToken(tokenParams, authOptions);
Capability expectedCapability = new Capability();
- expectedCapability.addResource("channel6", new String[]{"publish", "subscribe"});
+ expectedCapability.addResource("channel6", "publish", "subscribe");
assertNotNull("Expected token value", tokenDetails.token);
assertEquals("Unexpected capability", tokenDetails.capability, expectedCapability.toString());
} catch (AblyException e) {
@@ -199,7 +199,7 @@ public void authcapability7() {
}
/**
- * Wildcard resources intersection
+ * Wildcard resources intersection
*/
@Test
public void authcapability8() {
@@ -276,7 +276,7 @@ public void authinvalid0() {
public void authinvalid1() {
TokenParams tokenParams = new TokenParams();
Capability invalidCapability = new Capability();
- invalidCapability.addResource("channel0", new String[]{"*", "publish"});
+ invalidCapability.addResource("channel0", "*", "publish");
tokenParams.capability = invalidCapability.toString();
try {
ably.auth.requestToken(tokenParams, null);
@@ -289,7 +289,7 @@ public void authinvalid1() {
public void authinvalid2() {
TokenParams tokenParams = new TokenParams();
Capability invalidCapability = new Capability();
- invalidCapability.addResource("channel0", new String[0]);
+ invalidCapability.addResource("channel0");
tokenParams.capability = invalidCapability.toString();
try {
ably.auth.requestToken(tokenParams, null);
From 68477899d2b67f564517c9368bec4e71c2f03547 Mon Sep 17 00:00:00 2001
From: Quintin Willison
Date: Thu, 25 Mar 2021 18:21:09 +0000
Subject: [PATCH 038/899] Remove the assemble workflow.
We knew it wasn't working yet and it's confusing contributors.
---
.github/workflows/assemble.yml | 12 ------------
1 file changed, 12 deletions(-)
delete mode 100644 .github/workflows/assemble.yml
diff --git a/.github/workflows/assemble.yml b/.github/workflows/assemble.yml
deleted file mode 100644
index 1c9b58da7..000000000
--- a/.github/workflows/assemble.yml
+++ /dev/null
@@ -1,12 +0,0 @@
-on:
- pull_request:
- push:
- branches:
- - main
-
-jobs:
- check:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
- - run: ./gradlew assemble :java:jar :java:fullJar :java:assembleRelease :android:assembleRelease
From 63954152fe7e2ad33c7e65b8f474037c9b4c6df5 Mon Sep 17 00:00:00 2001
From: Quintin Willison
Date: Mon, 19 Apr 2021 19:46:41 +0100
Subject: [PATCH 039/899] Instruct users to use `implementation` rather than
`compile` to add the Ably dependency.
The compile option has been deprecated for a while and it looks like it may have been removed for good with Gradle 7.0.
---
README.md | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index bd450e196..211d1b789 100644
--- a/README.md
+++ b/README.md
@@ -28,18 +28,18 @@ Visit https://www.ably.io/documentation for a complete API reference and more ex
## Installation ##
-Reference the library by including a compile dependency reference in your gradle build file.
+Include the library by adding an `implementation` reference to `dependencies` block in your Gradle build script.
For [Java](https://bintray.com/ably-io/ably/ably-java/_latestVersion):
```
-compile 'io.ably:ably-java:1.2.5'
+implementation 'io.ably:ably-java:1.2.5'
```
For [Android](https://bintray.com/ably-io/ably/ably-android/_latestVersion):
```
-compile 'io.ably:ably-android:1.2.5'
+implementation 'io.ably:ably-android:1.2.5'
```
The library is hosted on the [Jcenter repository](https://bintray.com/ably-io/ably), so you need to ensure that the repo is referenced also; IDEs will typically include this by default:
From 18797e99a762546a171d7619fbd089e299a3f489 Mon Sep 17 00:00:00 2001
From: Quintin Willison
Date: Mon, 19 Apr 2021 19:48:57 +0100
Subject: [PATCH 040/899] Replace Bintray with Maven Central in user
instructions.
---
README.md | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/README.md b/README.md
index 211d1b789..0cee952a9 100644
--- a/README.md
+++ b/README.md
@@ -30,27 +30,27 @@ Visit https://www.ably.io/documentation for a complete API reference and more ex
Include the library by adding an `implementation` reference to `dependencies` block in your Gradle build script.
-For [Java](https://bintray.com/ably-io/ably/ably-java/_latestVersion):
+For [Java](https://mvnrepository.com/artifact/io.ably/ably-java/latest):
```
implementation 'io.ably:ably-java:1.2.5'
```
-For [Android](https://bintray.com/ably-io/ably/ably-android/_latestVersion):
+For [Android](https://mvnrepository.com/artifact/io.ably/ably-android/latest):
```
implementation 'io.ably:ably-android:1.2.5'
```
-The library is hosted on the [Jcenter repository](https://bintray.com/ably-io/ably), so you need to ensure that the repo is referenced also; IDEs will typically include this by default:
+The library is hosted on [Maven Central](https://mvnrepository.com/repos/central), so you need to ensure that the repository is referenced also; IDEs will typically include this by default:
```
repositories {
- jcenter()
+ mavenCentral()
}
```
-Previous releases of the Java library included a downloadable JAR; however we now only support installation via Maven/Gradle from the Jcenter repository. If you want to use a standalone fat JAR for (ie containing all dependencies), it can be generated via a gradle task (see [building](#building) below); note that this is the "Java" (JRE) library variant only; Android is now supported via an AAR and there is no self-contained AAR build option.
+We only support installation via Maven/Gradle from the Maven Central repository. If you want to use a standalone fat JAR for (ie containing all dependencies), it can be generated via a gradle task (see [building](#building) below); note that this is the "Java" (JRE) library variant only; Android is now supported via an AAR and there is no self-contained AAR build option.
## Dependencies
From a7814c9b4a768c03c4819486ff38126eafb29951 Mon Sep 17 00:00:00 2001
From: Quintin Willison
Date: Mon, 19 Apr 2021 19:49:16 +0100
Subject: [PATCH 041/899] Remove soon-to-be-defunct Bintray badges.
---
README.md | 4 ----
1 file changed, 4 deletions(-)
diff --git a/README.md b/README.md
index 0cee952a9..e1cbc55fa 100644
--- a/README.md
+++ b/README.md
@@ -3,10 +3,6 @@


-| Android | Java |
-|---------|------|
-| [  ](https://bintray.com/ably-io/ably/ably-android/_latestVersion) | [  ](https://bintray.com/ably-io/ably/ably-java/_latestVersion) |
-
A Java Realtime and REST client library for [Ably Realtime](https://www.ably.io), the realtime messaging and data delivery service. This library currently targets the [Ably client library features spec](https://www.ably.io/documentation/client-lib-development-guide/features/) Version 1.2. You can jump to the '[Known Limitations](#known-limitations)' section to see the features this client library does not yet support or [view our client library SDKs feature support matrix](https://www.ably.io/download/sdk-feature-support-matrix) to see the list of all the available features.
## Supported Platforms
From d97eb30cacefa79d638e938b57c2cec4c282c5aa Mon Sep 17 00:00:00 2001
From: Quintin Willison
Date: Mon, 19 Apr 2021 19:49:39 +0100
Subject: [PATCH 042/899] Update release procedures to remove Bintray and just
publish to Maven Central.
---
README.md | 55 ++++++++++++-------------------------------------------
1 file changed, 12 insertions(+), 43 deletions(-)
diff --git a/README.md b/README.md
index e1cbc55fa..7912096cc 100644
--- a/README.md
+++ b/README.md
@@ -613,11 +613,16 @@ This library uses [semantic versioning](http://semver.org/). For each release, t
5. Make a PR against `main`
6. Once the PR is approved, merge it into `main`
7. Add a tag and push to origin - e.g.: `git tag v1.2.5 && git push origin v1.2.5`
-8. Create the release on Github including populating the release notes (needed so JFrog can pull them in)
-9. Assemble and Upload ([see below](#publishing-to-jcenter-and-maven-central) for details) - but the overall order to follow is:
- 1. Upload to Bintray and use the pushed tag, which will pull in the associated release notes
- 2. Comment out local `repository` lines in the two `maven.gradle` files temporarily (this is horrible but is [to be fixed soon](https://github.com/ably/ably-java/issues/566))
- 3. Repeat the assemble stages to this time to push to Maven Central
+8. Create the release on Github including populating the release notes
+9. Assemble and Upload ([see below](#publishing-to-maven-central) for details) - but the overall order to follow is:
+ 1. Comment out local `repository` lines in the two `maven.gradle` files temporarily (this is horrible but is [to be fixed soon](https://github.com/ably/ably-java/issues/566))
+ 2. Run `./gradlew java:assembleRelease` to build and upload `ably-java` to Nexus staging repository
+ 3. Run `./gradlew android:assembleRelease` build and upload `ably-android` to Nexus staging repository
+ 4. Find the new staging repository using the [Nexus Repository Manager](https://oss.sonatype.org/#stagingRepositories)
+ 5. Check that it contains Android and Java releases
+ 6. "Close" it - this will take a few minutes during which time it will say (after a refresh of your browser) that "Activity: Operation in Progress"
+ 7. Once it has closed you will have "Release" available. You can allow it to "automatically drop" after successful release. A refresh or two later of the browser and the staging repository will have disappeared from the list (i.e. it's been dropped which implies it was released successfully)
+ 8. A [search for Ably packages](https://oss.sonatype.org/#nexus-search;quick~io.ably) should now list the new version for both `ably-android` and `ably-java`
### Signing
@@ -629,45 +634,9 @@ You need to [configure Signatory credentials](https://docs.gradle.org/current/us
The GPG key file is internal and private to Ably.
-### Publishing to JCenter and Maven Central
+### Sonatype Nexus for Maven Central
-We publish to:
-
-* JCenter via JFrog's [Bintray](https://bintray.com/ably-io/ably)
-* Maven Central via Sonatype's [OSSRH](https://issues.sonatype.org/browse/OSSRH-52871) / [Nexus](https://oss.sonatype.org/#nexus-search;quick~io.ably)
-
-#### Releasing to JCenter (JFrog Bintray)
-
-The `java` release process goes as follows:
-
-* Go to the home page for the package; eg https://bintray.com/ably-io/ably/ably-java. Select Add a version, enter the new version such as "1.2.5" in name and save
-* Run `./gradlew java:assembleRelease` locally to generate the files
-* Open local relative folder in Finder, such as `./java/build/release/1.2.5/io/ably/ably-java/1.2.5`
-* Go to the new version in JFrog Bintray; eg https://bintray.com/ably-io/ably/ably-java/1.2.5, then click on the link to upload via the UI in the "Upload files" section
-* Drag in the files from Finder, just the `.jar` files and the `.pom` file. JFrog will fill in the "Target Path" box after you drop the files in. Click the "Upload" button.
-* You will see a notice something like "4 unpublished files in your version. Will be deleted in 6 days and 22 hours. Publish all or Delete all unpublished files.", make sure you click "Publish all". Wait a few minutes and check that what's uploaded looks like what was uploaded for previous releases. The `maven-metadata` files are created by JFrog.
-* Update the README text in Bintray (version number needs incrementing).
-
-Similarly for the `android` release at https://bintray.com/ably-io/ably/ably-android:
-
-* Run `./gradlew android:assembleRelease` locally to generate the files, and drag in the files in
-`./android/build/release/1.2.5/io/ably/ably-android/1.2.5`.
-* In this case upload the `.jar` files, the `.pom` file and the `.aar` file.
-
-#### Releasing to Maven Central (Sonatype Nexus)
-
-Bearing in mind the earlier instructions around commenting out lines in the `maven.gradle` files (temporary requirement) you then need to find the new staging repository in
-[Nexus Repository Manager](https://oss.sonatype.org/#stagingRepositories)
-and do a few things with it:
-
-1. Check that it contains Android and Java releases.
-2. "Close" it - this will take a few minutes during which time it will say (after a refresh of your browser) that "Activity: Operation in Progress".
-3. Once it has closed you will have "Release" available. You can allow it to "automatically drop" after successful release. A refresh or two later of the browser and the staging repository will have disappeared from the list (i.e. it's been dropped which implies it was released successfully).
-4. A [search for Ably packages](https://oss.sonatype.org/#nexus-search;quick~io.ably) should now list the new version for both `ably-android` and `ably-java`.
-
-### Creating the release on Github
-
-Visit [https://github.com/ably/ably-java/tags](https://github.com/ably/ably-java/tags) and `Add release notes` for the release including links to the changelog entry and the JCenter releases.
+We publish to Maven Central via Sonatype's [OSSRH](https://issues.sonatype.org/browse/OSSRH-52871) / [Nexus](https://oss.sonatype.org/#nexus-search;quick~io.ably)
## Support, feedback and troubleshooting
From 24d17a763650378887ccaecf7bc6c109218a7687 Mon Sep 17 00:00:00 2001
From: Quintin Willison
Date: Mon, 19 Apr 2021 20:01:33 +0100
Subject: [PATCH 043/899] Improve sentence structure.
---
README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 7912096cc..73c6c34e3 100644
--- a/README.md
+++ b/README.md
@@ -46,7 +46,7 @@ repositories {
}
```
-We only support installation via Maven/Gradle from the Maven Central repository. If you want to use a standalone fat JAR for (ie containing all dependencies), it can be generated via a gradle task (see [building](#building) below); note that this is the "Java" (JRE) library variant only; Android is now supported via an AAR and there is no self-contained AAR build option.
+We only support installation via Maven / Gradle from the Maven Central repository. If you want to use a standalone fat JAR (i.e. containing all dependencies), it can be generated via a Gradle task (see [building](#building) below), creating a "Java" (JRE) library variant only. There is no standalone / self-contained AAR build option.
## Dependencies
@@ -449,7 +449,7 @@ realtime.push.activate();
See https://www.ably.io/documentation/general/push/admin for details of the push admin API.
-## Building ##
+## Building
The library consists of JRE-specific library (in `java/`) and an Android-specific library (in `android/`). The libraries are largely common-sourced; the `lib/` directory contains the common parts.
From 9aadb8beadb791c043639412966c6f935defdb1c Mon Sep 17 00:00:00 2001
From: Quintin Willison
Date: Mon, 19 Apr 2021 20:02:46 +0100
Subject: [PATCH 044/899] Provide link to Gradle website.
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 73c6c34e3..9cf54845f 100644
--- a/README.md
+++ b/README.md
@@ -24,7 +24,7 @@ Visit https://www.ably.io/documentation for a complete API reference and more ex
## Installation ##
-Include the library by adding an `implementation` reference to `dependencies` block in your Gradle build script.
+Include the library by adding an `implementation` reference to `dependencies` block in your [Gradle](https://gradle.org/) build script.
For [Java](https://mvnrepository.com/artifact/io.ably/ably-java/latest):
From 235ff1a046008cd8e68a15c6361b900836ec89f1 Mon Sep 17 00:00:00 2001
From: Quintin Willison
Date: Mon, 19 Apr 2021 20:04:48 +0100
Subject: [PATCH 045/899] Remove superfluous and inconsistently applied markup
suffixes from headings.
---
README.md | 42 +++++++++++++++++++++---------------------
1 file changed, 21 insertions(+), 21 deletions(-)
diff --git a/README.md b/README.md
index 9cf54845f..c116c93b3 100644
--- a/README.md
+++ b/README.md
@@ -22,7 +22,7 @@ If you find any compatibility issues, please [do raise an issue](https://github.
Visit https://www.ably.io/documentation for a complete API reference and more examples.
-## Installation ##
+## Installation
Include the library by adding an `implementation` reference to `dependencies` block in your [Gradle](https://gradle.org/) build script.
@@ -59,9 +59,9 @@ For Android, 4.0 (API level 14) or later is required.
This library targets the Ably 1.2 client library specification and supports all principal 1.2 features.
-## Using the Realtime API ##
+## Using the Realtime API
-### Introduction ###
+### Introduction
Please refer to the [documentation](https://www.ably.io/documentation) for a full realtime API reference.
@@ -71,7 +71,7 @@ The examples below assume a client has been created as follows:
AblyRealtime ably = new AblyRealtime("xxxxx");
```
-### Connection ###
+### Connection
AblyRealtime will attempt to connect automatically once new instance is created. Also, it offers API for listening connection state changes.
@@ -94,7 +94,7 @@ ably.connection.on(new ConnectionStateListener() {
});
```
-### Subscribing to a channel ###
+### Subscribing to a channel
Given:
@@ -125,7 +125,7 @@ channel.subscribe(events, new MessageListener() {
});
```
-### Subscribing to a channel in delta mode ###
+### Subscribing to a channel in delta mode
Subscribing to a channel in delta mode enables [delta compression](https://www.ably.io/documentation/realtime/channels/channel-parameters/deltas). This is a way for a client to subscribe to a channel so that message payloads sent contain only the difference (ie the delta) between the present message and the previous message on the channel.
@@ -143,7 +143,7 @@ Beyond specifying channel options, the rest is transparent and requires no furth
If you would like to inspect the `Message` instances in order to identify whether the `data` they present was rendered from a delta message from Ably then you can see if `extras.getDelta().getFormat()` equals `"vcdiff"`.
-### Publishing to a channel ###
+### Publishing to a channel
```java
channel.publish("greeting", "Hello World!", new CompletionListener() {
@@ -159,7 +159,7 @@ channel.publish("greeting", "Hello World!", new CompletionListener() {
});
```
-### Querying the history ###
+### Querying the history
```java
PaginatedResult result = channel.history(null);
@@ -171,7 +171,7 @@ while(result.hasNext()) {
}
```
-### Presence on a channel ###
+### Presence on a channel
```java
channel.presence.enter("john.doe", new CompletionListener() {
@@ -187,7 +187,7 @@ channel.presence.enter("john.doe", new CompletionListener() {
});
```
-### Querying the presence history ###
+### Querying the presence history
```java
PaginatedResult result = channel.presence.history(null);
@@ -199,7 +199,7 @@ while(result.hasNext()) {
}
```
-### Channel state ###
+### Channel state
`Channel` extends `EventEmitter` that emits channel state changes, and listening those events is possible with `ChannelStateListener`
@@ -230,9 +230,9 @@ If you are interested with specific events, it is possible with providing extra
channel.on(ChannelState.attached, listener);
```
-## Using the REST API ##
+## Using the REST API
-### Introduction ###
+### Introduction
Please refer to the [documentation](https://www.ably.io/documentation) for a full REST API reference.
@@ -243,7 +243,7 @@ AblyRest ably = new AblyRest("xxxxx");
Channel channel = ably.channels.get("test");
```
-### Publishing a message to a channel ###
+### Publishing a message to a channel
Given the message below
@@ -273,7 +273,7 @@ channel.publishAsync(message, new CompletionListener() {
});
```
-### Querying the history ###
+### Querying the history
```java
PaginatedResult result = channel.history(null);
@@ -285,7 +285,7 @@ while(result.hasNext()) {
}
```
-### Presence on a channel ###
+### Presence on a channel
```java
PaginatedResult result = channel.presence.get(null);
@@ -297,7 +297,7 @@ while(result.hasNext()) {
}
```
-### Querying the presence history ###
+### Querying the presence history
```java
PaginatedResult result = channel.presence.history(null);
@@ -309,14 +309,14 @@ while(result.hasNext()) {
}
```
-### Generate a Token and Token Request ###
+### Generate a Token and Token Request
```java
TokenDetails tokenDetails = ably.auth.requestToken(null, null);
System.out.println("Success; token = " + tokenRequest);
```
-### Fetching your application's stats ###
+### Fetching your application's stats
```java
PaginatedResult stats = ably.stats(null);
@@ -328,13 +328,13 @@ while(result.hasNext()) {
}
```
-### Fetching the Ably service time ###
+### Fetching the Ably service time
```java
long serviceTime = ably.time();
```
-### Logging ###
+### Logging
You can get log output from the library by modifying the log level:
From 0ae71d3497981e1dbd3bf6e4470b1a7374b8662a Mon Sep 17 00:00:00 2001
From: sacOO7
Date: Tue, 11 May 2021 21:14:05 +0530
Subject: [PATCH 046/899] Added explicit sync complete check before waiting for
the presence
---
lib/src/main/java/io/ably/lib/realtime/Presence.java | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
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 a0aae58fa..dfe11c8e9 100644
--- a/lib/src/main/java/io/ably/lib/realtime/Presence.java
+++ b/lib/src/main/java/io/ably/lib/realtime/Presence.java
@@ -798,8 +798,10 @@ synchronized void waitForSync() throws AblyException, InterruptedException {
}
if (channel.state == ChannelState.attached) {
do {
- wait();
syncIsComplete = !syncInProgress && syncComplete;
+ if (!syncIsComplete) {
+ wait();
+ }
} while (!syncIsComplete);
}
From 87f698a7e7bfbc72a111b8d54b62395298201a85 Mon Sep 17 00:00:00 2001
From: Quintin Willison
Date: Wed, 12 May 2021 16:25:45 +0100
Subject: [PATCH 047/899] Bump version number (patch).
---
README.md | 12 ++++++------
common.gradle | 2 +-
.../lib/test/realtime/RealtimeHttpHeaderTest.java | 2 +-
.../java/io/ably/lib/test/rest/HttpHeaderTest.java | 2 +-
4 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/README.md b/README.md
index c116c93b3..27bf494fc 100644
--- a/README.md
+++ b/README.md
@@ -29,13 +29,13 @@ Include the library by adding an `implementation` reference to `dependencies` bl
For [Java](https://mvnrepository.com/artifact/io.ably/ably-java/latest):
```
-implementation 'io.ably:ably-java:1.2.5'
+implementation 'io.ably:ably-java:1.2.6'
```
For [Android](https://mvnrepository.com/artifact/io.ably/ably-android/latest):
```
-implementation 'io.ably:ably-android:1.2.5'
+implementation 'io.ably:ably-android:1.2.6'
```
The library is hosted on [Maven Central](https://mvnrepository.com/repos/central), so you need to ensure that the repository is referenced also; IDEs will typically include this by default:
@@ -604,15 +604,15 @@ Configuration of Run/Debug configurations for running the unit tests on Android
This library uses [semantic versioning](http://semver.org/). For each release, the following needs to be done:
-1. Create a branch for the release, named like `release/1.2.5`
+1. Create a branch for the release, named like `release/1.2.6`
2. Replace all references of the current version number with the new version number (check this file [README.md](./README.md) and [common.gradle](./common.gradle)) and commit the changes
3. Run [`github_changelog_generator`](https://github.com/skywinder/Github-Changelog-Generator) to update the [CHANGELOG](./CHANGELOG.md):
- * This might work: `github_changelog_generator -u ably -p ably-java --header-label="# Changelog" --release-branch=release/1.2.5 --future-release=v1.2.5`
- * But your mileage may vary as it can error. Perhaps more reliable is something like: `github_changelog_generator -u ably -p ably-java --since-tag v1.2.4 --output delta.md` and then manually merge the delta contents in to the main change log
+ * This might work: `github_changelog_generator -u ably -p ably-java --header-label="# Changelog" --release-branch=release/1.2.6 --future-release=v1.2.6`
+ * But your mileage may vary as it can error. Perhaps more reliable is something like: `github_changelog_generator -u ably -p ably-java --since-tag v1.2.5 --output delta.md` and then manually merge the delta contents in to the main change log
4. Commit [CHANGELOG](./CHANGELOG.md)
5. Make a PR against `main`
6. Once the PR is approved, merge it into `main`
-7. Add a tag and push to origin - e.g.: `git tag v1.2.5 && git push origin v1.2.5`
+7. Add a tag and push to origin - e.g.: `git tag v1.2.6 && git push origin v1.2.6`
8. Create the release on Github including populating the release notes
9. Assemble and Upload ([see below](#publishing-to-maven-central) for details) - but the overall order to follow is:
1. Comment out local `repository` lines in the two `maven.gradle` files temporarily (this is horrible but is [to be fixed soon](https://github.com/ably/ably-java/issues/566))
diff --git a/common.gradle b/common.gradle
index e373251bd..bdd14779d 100644
--- a/common.gradle
+++ b/common.gradle
@@ -4,7 +4,7 @@ repositories {
}
group = 'io.ably'
-version = '1.2.5'
+version = '1.2.6'
description = 'Ably java client library'
tasks.withType(Javadoc) {
diff --git a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeHttpHeaderTest.java b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeHttpHeaderTest.java
index 13e04343e..b6b120b54 100644
--- a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeHttpHeaderTest.java
+++ b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeHttpHeaderTest.java
@@ -91,7 +91,7 @@ public void realtime_websocket_param_test() {
* Defaults.ABLY_LIB_PARAM, as ultimately the request param has been derived from those values.
*/
assertEquals("Verify correct lib version", requestParameters.get("lib"),
- Collections.singletonList("java-1.2.5"));
+ Collections.singletonList("java-1.2.6"));
/* Spec RTN2a */
assertEquals("Verify correct format", requestParameters.get("format"),
diff --git a/lib/src/test/java/io/ably/lib/test/rest/HttpHeaderTest.java b/lib/src/test/java/io/ably/lib/test/rest/HttpHeaderTest.java
index 79c9d4ac7..9c642a418 100644
--- a/lib/src/test/java/io/ably/lib/test/rest/HttpHeaderTest.java
+++ b/lib/src/test/java/io/ably/lib/test/rest/HttpHeaderTest.java
@@ -84,7 +84,7 @@ public void header_lib_channel_publish() {
*/
Assert.assertNotNull("Expected headers", headers);
Assert.assertEquals(headers.get("x-ably-version"), "1.2");
- Assert.assertEquals(headers.get("x-ably-lib"), "java-1.2.5");
+ Assert.assertEquals(headers.get("x-ably-lib"), "java-1.2.6");
} catch (AblyException e) {
e.printStackTrace();
Assert.fail("header_lib_channel_publish: Unexpected exception");
From 77949ae140d12d72f23effeaa9db2a11b8388cec Mon Sep 17 00:00:00 2001
From: Quintin Willison
Date: Wed, 12 May 2021 16:33:59 +0100
Subject: [PATCH 048/899] Update change log.
---
CHANGELOG.md | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index da0eb0ef5..b229a9aa2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
# Change Log
+## [v1.2.6](https://github.com/ably/ably-java/tree/v1.2.6)
+
+[Full Changelog](https://github.com/ably/ably-java/compare/v1.2.5...v1.2.6)
+
+**Fixed bug:** channel presence members [\#669](https://github.com/ably/ably-java/pull/669) ([sacOO7](https://github.com/sacOO7))
+An issue affecting only users calling `get(boolean wait)` on `Presence` with `wait` set to `true`.
+
## [v1.2.5](https://github.com/ably/ably-java/tree/v1.2.5)
[Full Changelog](https://github.com/ably/ably-java/compare/v1.2.4...v1.2.5)
From c27c89f97e3762554c7a45e0c604e6b88e50c7e5 Mon Sep 17 00:00:00 2001
From: Kacper Kluka
Date: Tue, 18 May 2021 15:33:07 +0200
Subject: [PATCH 049/899] Add agent header entries validator
---
.../io/ably/lib/util/AblyAgentValidator.java | 45 ++++++++
.../ably/lib/util/AblyAgentValidatorTest.java | 105 ++++++++++++++++++
2 files changed, 150 insertions(+)
create mode 100644 lib/src/main/java/io/ably/lib/util/AblyAgentValidator.java
create mode 100644 lib/src/test/java/io/ably/lib/util/AblyAgentValidatorTest.java
diff --git a/lib/src/main/java/io/ably/lib/util/AblyAgentValidator.java b/lib/src/main/java/io/ably/lib/util/AblyAgentValidator.java
new file mode 100644
index 000000000..84cf0f32b
--- /dev/null
+++ b/lib/src/main/java/io/ably/lib/util/AblyAgentValidator.java
@@ -0,0 +1,45 @@
+package io.ably.lib.util;
+
+import java.util.Map;
+import java.util.regex.Pattern;
+
+public class AblyAgentValidator {
+ /**
+ * Agent name validation regex.
+ * Allow only lowercase letters and '-'.
+ */
+ private static final String AGENT_NAME_REGEX = "^[a-z\\-]+$";
+ private static final Pattern agentNamePattern = Pattern.compile(AGENT_NAME_REGEX);
+
+ /**
+ * Agent version validation regex.
+ * Suggested Semantic Versioning regex from the official site.
+ * https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
+ */
+ private static final String SEM_VER_REGEX = "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$";
+ private static final Pattern agentVersionPattern = Pattern.compile(SEM_VER_REGEX);
+
+ /**
+ * Checks if provided Ably agent values are valid.
+ *
+ * @return true if both agentName and agentVersion (if it's present) are valid values, false otherwise.
+ */
+ public static boolean isValid(String agentName, String agentVersion) {
+ return agentNamePattern.matcher(agentName).matches()
+ && (agentVersion == null || agentVersionPattern.matcher(agentVersion).matches());
+ }
+
+ /**
+ * Checks if all provided Ably agent values are valid.
+ *
+ * @return true if all agents are valid, false otherwise.
+ */
+ public static boolean areAllValid(Map agents) {
+ for (String agentName : agents.keySet()) {
+ if (!isValid(agentName, agents.get(agentName))) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/lib/src/test/java/io/ably/lib/util/AblyAgentValidatorTest.java b/lib/src/test/java/io/ably/lib/util/AblyAgentValidatorTest.java
new file mode 100644
index 000000000..f5ee969c5
--- /dev/null
+++ b/lib/src/test/java/io/ably/lib/util/AblyAgentValidatorTest.java
@@ -0,0 +1,105 @@
+package io.ably.lib.util;
+
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class AblyAgentValidatorTest {
+ @Test
+ public void should_return_false_if_agent_name_is_invalid() {
+ // given
+ String agentName = "invalid/name";
+ String agentVersion = "1.0.1";
+
+ // when
+ boolean isAgentValid = AblyAgentValidator.isValid(agentName, agentVersion);
+
+ // then
+ assertFalse(isAgentValid);
+ }
+
+ @Test
+ public void should_return_false_if_agent_version_is_invalid() {
+ // given
+ String agentName = "valid-name";
+ String agentVersion = "1v.23.ax";
+
+ // when
+ boolean isAgentValid = AblyAgentValidator.isValid(agentName, agentVersion);
+
+ // then
+ assertFalse(isAgentValid);
+ }
+
+ @Test
+ public void should_return_false_if_both_agent_name_and_version_are_invalid() {
+ // given
+ String agentName = "invalid/name";
+ String agentVersion = "1v.23.ax";
+
+ // when
+ boolean isAgentValid = AblyAgentValidator.isValid(agentName, agentVersion);
+
+ // then
+ assertFalse(isAgentValid);
+ }
+
+ @Test
+ public void should_return_true_if_agent_name_and_version_are_valid() {
+ // given
+ String agentName = "valid-name";
+ String agentVersion = "1.2.3-alpha.14";
+
+ // when
+ boolean isAgentValid = AblyAgentValidator.isValid(agentName, agentVersion);
+
+ // then
+ assertTrue(isAgentValid);
+ }
+
+ @Test
+ public void should_return_true_if_agent_name_is_valid_and_version_is_null() {
+ // given
+ String agentName = "valid-name";
+ String agentVersion = null;
+
+ // when
+ boolean isAgentValid = AblyAgentValidator.isValid(agentName, agentVersion);
+
+ // then
+ assertTrue(isAgentValid);
+ }
+
+ @Test
+ public void should_return_true_if_all_agents_are_valid() {
+ // given
+ Map agents = new HashMap<>();
+ agents.put("valid-name", "1.0.1");
+ agents.put("another-valid-name", null);
+
+ // when
+ boolean areAgentsValid = AblyAgentValidator.areAllValid(agents);
+
+ // then
+ assertTrue(areAgentsValid);
+ }
+
+ @Test
+ public void should_return_false_if_any_agent_is_invalid() {
+ // given
+ Map agents = new HashMap<>();
+ agents.put("valid-name", "1.0.1");
+ agents.put("invalid/name", "1.0.1");
+ agents.put("another-valid-name", null);
+
+ // when
+ boolean areAgentsValid = AblyAgentValidator.areAllValid(agents);
+
+ // then
+ assertFalse(areAgentsValid);
+ }
+}
From e37571f17dcf11db750a097b18fa88a441771228 Mon Sep 17 00:00:00 2001
From: Kacper Kluka
Date: Tue, 18 May 2021 15:34:06 +0200
Subject: [PATCH 050/899] Add agents map to client options
---
lib/src/main/java/io/ably/lib/rest/AblyBase.java | 10 ++++++----
lib/src/main/java/io/ably/lib/types/ClientOptions.java | 7 +++++++
2 files changed, 13 insertions(+), 4 deletions(-)
diff --git a/lib/src/main/java/io/ably/lib/rest/AblyBase.java b/lib/src/main/java/io/ably/lib/rest/AblyBase.java
index 72c4d0392..d908109f7 100644
--- a/lib/src/main/java/io/ably/lib/rest/AblyBase.java
+++ b/lib/src/main/java/io/ably/lib/rest/AblyBase.java
@@ -29,10 +29,7 @@
import io.ably.lib.types.ReadOnlyMap;
import io.ably.lib.types.Stats;
import io.ably.lib.types.StatsReader;
-import io.ably.lib.util.Crypto;
-import io.ably.lib.util.InternalMap;
-import io.ably.lib.util.Log;
-import io.ably.lib.util.Serialisation;
+import io.ably.lib.util.*;
/**
* AblyBase
@@ -74,6 +71,11 @@ public AblyBase(ClientOptions options) throws AblyException {
Log.e(getClass().getName(), msg);
throw AblyException.fromErrorInfo(new ErrorInfo(msg, 400, 40000));
}
+ if (options.agents != null && !AblyAgentValidator.areAllValid(options.agents)) {
+ String msg = "invalid agent provided";
+ Log.e(getClass().getName(), msg);
+ throw AblyException.fromErrorInfo(new ErrorInfo(msg, 400, 40000));
+ }
this.options = options;
/* process options */
diff --git a/lib/src/main/java/io/ably/lib/types/ClientOptions.java b/lib/src/main/java/io/ably/lib/types/ClientOptions.java
index 8444964b3..dc596cb0b 100644
--- a/lib/src/main/java/io/ably/lib/types/ClientOptions.java
+++ b/lib/src/main/java/io/ably/lib/types/ClientOptions.java
@@ -195,4 +195,11 @@ public ClientOptions(String key) throws AblyException {
* before responding.
*/
public boolean pushFullWait = false;
+
+ /**
+ * Map of agents that will be appended to the agent header.
+ * The keys represent agent names and its corresponding values represent agent versions.
+ * Agent versions are optional, if you don't want to specify it pass `null` as the map entry value.
+ */
+ public Map agents;
}
From b12504a6f8e3841b0ef6d5be9107675182ab6697 Mon Sep 17 00:00:00 2001
From: Kacper Kluka
Date: Tue, 18 May 2021 15:37:18 +0200
Subject: [PATCH 051/899] Replace Ably lib header with Ably agent header
---
.../ably/lib/test/android/AndroidSuite.java | 2 +-
.../main/java/io/ably/lib/http/HttpCore.java | 10 +--
.../java/io/ably/lib/transport/Defaults.java | 6 +-
.../io/ably/lib/transport/ITransport.java | 3 +-
.../io/ably/lib/util/AgentHeaderCreator.java | 61 +++++++++++++++++++
.../ably/lib/util/AgentHeaderCreatorTest.java | 54 ++++++++++++++++
6 files changed, 124 insertions(+), 12 deletions(-)
create mode 100644 lib/src/main/java/io/ably/lib/util/AgentHeaderCreator.java
create mode 100644 lib/src/test/java/io/ably/lib/util/AgentHeaderCreatorTest.java
diff --git a/android/src/androidTest/java/io/ably/lib/test/android/AndroidSuite.java b/android/src/androidTest/java/io/ably/lib/test/android/AndroidSuite.java
index 9359f2feb..299218ec1 100644
--- a/android/src/androidTest/java/io/ably/lib/test/android/AndroidSuite.java
+++ b/android/src/androidTest/java/io/ably/lib/test/android/AndroidSuite.java
@@ -64,7 +64,7 @@ public void android_http_header_test() {
Map headers = server.getHeaders();
assertNotNull("Verify ably server was reached", headers);
- String header = headers.get(Defaults.ABLY_LIB_HEADER.toLowerCase());
+ String header = headers.get(Defaults.ABLY_AGENT_HEADER.toLowerCase());
assertTrue("Verify correct library header was passed to the server", header != null && header.startsWith("android"));
}
catch (AblyException e) {
diff --git a/lib/src/main/java/io/ably/lib/http/HttpCore.java b/lib/src/main/java/io/ably/lib/http/HttpCore.java
index 744d44111..7ba3492c2 100644
--- a/lib/src/main/java/io/ably/lib/http/HttpCore.java
+++ b/lib/src/main/java/io/ably/lib/http/HttpCore.java
@@ -20,12 +20,8 @@
import io.ably.lib.rest.Auth;
import io.ably.lib.transport.Defaults;
import io.ably.lib.transport.Hosts;
-import io.ably.lib.types.AblyException;
-import io.ably.lib.types.ClientOptions;
-import io.ably.lib.types.ErrorInfo;
-import io.ably.lib.types.ErrorResponse;
-import io.ably.lib.types.Param;
-import io.ably.lib.types.ProxyOptions;
+import io.ably.lib.types.*;
+import io.ably.lib.util.AgentHeaderCreator;
import io.ably.lib.util.Log;
/**
@@ -216,7 +212,7 @@ T httpExecute(HttpURLConnection conn, String method, Param[] headers, Reques
/* pass required headers */
conn.setRequestProperty(Defaults.ABLY_VERSION_HEADER, Defaults.ABLY_VERSION);
- conn.setRequestProperty(Defaults.ABLY_LIB_HEADER, Defaults.ABLY_LIB_VERSION);
+ conn.setRequestProperty(Defaults.ABLY_AGENT_HEADER, AgentHeaderCreator.create(options.agents));
/* prepare request body */
byte[] body = null;
diff --git a/lib/src/main/java/io/ably/lib/transport/Defaults.java b/lib/src/main/java/io/ably/lib/transport/Defaults.java
index b3b27fa7d..b2871f3a3 100644
--- a/lib/src/main/java/io/ably/lib/transport/Defaults.java
+++ b/lib/src/main/java/io/ably/lib/transport/Defaults.java
@@ -11,15 +11,15 @@ public class Defaults {
/* versions */
public static final float ABLY_VERSION_NUMBER = 1.2f;
public static final String ABLY_VERSION = new DecimalFormat("0.0", new DecimalFormatSymbols(Locale.ENGLISH)).format(ABLY_VERSION_NUMBER);
- public static final String ABLY_LIB_VERSION = String.format("%s-%s", BuildConfig.LIBRARY_NAME, BuildConfig.VERSION);
+ public static final String ABLY_AGENT_VERSION = String.format("%s/%s", "ably-java", BuildConfig.VERSION);
/* params */
public static final String ABLY_VERSION_PARAM = "v";
- public static final String ABLY_LIB_PARAM = "lib";
+ public static final String ABLY_AGENT_PARAM = "agent";
/* Headers */
public static final String ABLY_VERSION_HEADER = "X-Ably-Version";
- public static final String ABLY_LIB_HEADER = "X-Ably-Lib";
+ public static final String ABLY_AGENT_HEADER = "Ably-Agent";
/* Hosts */
public static final String[] HOST_FALLBACKS = { "A.ably-realtime.com", "B.ably-realtime.com", "C.ably-realtime.com", "D.ably-realtime.com", "E.ably-realtime.com" };
diff --git a/lib/src/main/java/io/ably/lib/transport/ITransport.java b/lib/src/main/java/io/ably/lib/transport/ITransport.java
index e23ce3b67..15360b504 100644
--- a/lib/src/main/java/io/ably/lib/transport/ITransport.java
+++ b/lib/src/main/java/io/ably/lib/transport/ITransport.java
@@ -5,6 +5,7 @@
import io.ably.lib.types.ErrorInfo;
import io.ably.lib.types.Param;
import io.ably.lib.types.ProtocolMessage;
+import io.ably.lib.util.AgentHeaderCreator;
import io.ably.lib.util.Log;
import java.io.IOException;
@@ -88,7 +89,7 @@ public Param[] getConnectParams(Param[] baseParams) {
if(options.transportParams != null) {
paramList.addAll(Arrays.asList(options.transportParams));
}
- paramList.add(new Param(Defaults.ABLY_LIB_PARAM, Defaults.ABLY_LIB_VERSION));
+ paramList.add(new Param(Defaults.ABLY_AGENT_PARAM, AgentHeaderCreator.create(options.agents)));
Log.d(TAG, "getConnectParams: params = " + paramList);
return paramList.toArray(new Param[paramList.size()]);
}
diff --git a/lib/src/main/java/io/ably/lib/util/AgentHeaderCreator.java b/lib/src/main/java/io/ably/lib/util/AgentHeaderCreator.java
new file mode 100644
index 000000000..3e279f810
--- /dev/null
+++ b/lib/src/main/java/io/ably/lib/util/AgentHeaderCreator.java
@@ -0,0 +1,61 @@
+package io.ably.lib.util;
+
+import android.os.Build;
+import io.ably.lib.BuildConfig;
+import io.ably.lib.transport.Defaults;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+public class AgentHeaderCreator {
+ /**
+ * Separates agent entries from each other.
+ */
+ private static final String AGENT_ENTRY_SEPARATOR = " ";
+
+ /**
+ * Separates agent name from agent version.
+ */
+ private static final String AGENT_DIVIDER = "/";
+ private static final String ANDROID_LIBRARY_NAME = "android";
+
+ public static String create(Map additionalAgents) {
+ StringBuilder agentStringBuilder = new StringBuilder();
+ if (!additionalAgents.isEmpty()) {
+ agentStringBuilder.append(getAdditionalAgentEntries(additionalAgents));
+ }
+ agentStringBuilder.append(Defaults.ABLY_AGENT_VERSION);
+ if (BuildConfig.LIBRARY_NAME == ANDROID_LIBRARY_NAME) {
+ agentStringBuilder.append(AGENT_ENTRY_SEPARATOR);
+ agentStringBuilder.append(getAndroidAgent());
+ }
+ return agentStringBuilder.toString();
+ }
+
+ private static String getAdditionalAgentEntries(Map additionalAgents) {
+ StringBuilder additionalAgentsBuilder = new StringBuilder();
+ for (String additionalAgentName : getSortedAgentNames(additionalAgents)) {
+ String additionalAgentVersion = additionalAgents.get(additionalAgentName);
+ additionalAgentsBuilder.append(additionalAgentName);
+ if (additionalAgentVersion != null) {
+ additionalAgentsBuilder.append(AGENT_DIVIDER);
+ additionalAgentsBuilder.append(additionalAgentVersion);
+ }
+ additionalAgentsBuilder.append(AGENT_ENTRY_SEPARATOR);
+ }
+ return additionalAgentsBuilder.toString();
+ }
+
+
+ private static List getSortedAgentNames(Map agents) {
+ List agentNames = new ArrayList<>(agents.keySet());
+ Collections.sort(agentNames);
+ return agentNames;
+ }
+
+ private static String getAndroidAgent() {
+ return "android" + AGENT_DIVIDER + Build.VERSION.SDK_INT;
+ }
+}
diff --git a/lib/src/test/java/io/ably/lib/util/AgentHeaderCreatorTest.java b/lib/src/test/java/io/ably/lib/util/AgentHeaderCreatorTest.java
new file mode 100644
index 000000000..2f7be2aad
--- /dev/null
+++ b/lib/src/test/java/io/ably/lib/util/AgentHeaderCreatorTest.java
@@ -0,0 +1,54 @@
+package io.ably.lib.util;
+
+import android.os.Build;
+import io.ably.lib.transport.Defaults;
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.Assert.*;
+
+public class AgentHeaderCreatorTest {
+ private final static String PREDEFINED_AGENTS = Defaults.ABLY_AGENT_VERSION + " android/" + Build.VERSION.SDK_INT;
+
+ @Test
+ public void should_create_default_header_if_there_are_no_additional_agents() {
+ // given
+ Map agents = new HashMap<>();
+
+ // when
+ String agentHeaderValue = AgentHeaderCreator.create(agents);
+
+ // then
+ assertEquals(PREDEFINED_AGENTS, agentHeaderValue);
+ }
+
+ @Test
+ public void should_create_header_with_appended_agents_if_they_are_provided() {
+ // given
+ Map agents = new HashMap<>();
+ agents.put("library", "1.0.1");
+ agents.put("other", "0.8.2");
+
+ // when
+ String agentHeaderValue = AgentHeaderCreator.create(agents);
+
+ // then
+ assertEquals("library/1.0.1 other/0.8.2 " + PREDEFINED_AGENTS, agentHeaderValue);
+ }
+
+ @Test
+ public void should_create_header_with_appended_agents_without_versions() {
+ // given
+ Map agents = new HashMap<>();
+ agents.put("library", "1.0.1");
+ agents.put("no-version", null);
+
+ // when
+ String agentHeaderValue = AgentHeaderCreator.create(agents);
+
+ // then
+ assertEquals("library/1.0.1 no-version " + PREDEFINED_AGENTS, agentHeaderValue);
+ }
+}
From 08939d002da76695fced5909d81e04e8ef2119bc Mon Sep 17 00:00:00 2001
From: Martin Morek
Date: Fri, 21 May 2021 17:44:20 +0200
Subject: [PATCH 052/899] Checking if error code is 403 and failing connection
---
lib/src/main/java/io/ably/lib/rest/Auth.java | 2 +-
.../main/java/io/ably/lib/transport/ConnectionManager.java | 6 ++++++
2 files changed, 7 insertions(+), 1 deletion(-)
diff --git a/lib/src/main/java/io/ably/lib/rest/Auth.java b/lib/src/main/java/io/ably/lib/rest/Auth.java
index f25227eb6..1fd2b806a 100644
--- a/lib/src/main/java/io/ably/lib/rest/Auth.java
+++ b/lib/src/main/java/io/ably/lib/rest/Auth.java
@@ -491,7 +491,7 @@ public interface TokenCallback {
*
* - ttl: (optional) the requested life of any new token in ms. If none
* is specified a default of 1 hour is provided. The maximum lifetime
- * is 24hours; any request exceeeding that lifetime will be rejected
+ * is 24hours; any request exceeding that lifetime will be rejected
* with an error.
*
* - capability: (optional) the capability to associate with the access token.
diff --git a/lib/src/main/java/io/ably/lib/transport/ConnectionManager.java b/lib/src/main/java/io/ably/lib/transport/ConnectionManager.java
index c7cfb1b8f..916a09ee8 100644
--- a/lib/src/main/java/io/ably/lib/transport/ConnectionManager.java
+++ b/lib/src/main/java/io/ably/lib/transport/ConnectionManager.java
@@ -978,6 +978,12 @@ public void onAuthUpdated(final String token, final boolean waitForResponse) thr
*/
public void onAuthError(ErrorInfo errorInfo) {
Log.i(TAG, String.format("onAuthError: (%d) %s", errorInfo.code, errorInfo.message));
+
+ if(errorInfo.code == 403) {
+ this.connection.state = ConnectionState.failed;
+ return;
+ }
+
switch (currentState.state) {
case connecting:
ITransport transport = this.transport;
From cb9ddc2a767361beca6ad395730ff408942e3e6b Mon Sep 17 00:00:00 2001
From: Martin Morek
Date: Sat, 22 May 2021 18:59:02 +0200
Subject: [PATCH 053/899] Emiting failed state change instead setting it
directly
---
.../java/io/ably/lib/transport/ConnectionManager.java | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/lib/src/main/java/io/ably/lib/transport/ConnectionManager.java b/lib/src/main/java/io/ably/lib/transport/ConnectionManager.java
index 916a09ee8..c57bc1a0f 100644
--- a/lib/src/main/java/io/ably/lib/transport/ConnectionManager.java
+++ b/lib/src/main/java/io/ably/lib/transport/ConnectionManager.java
@@ -980,7 +980,14 @@ public void onAuthError(ErrorInfo errorInfo) {
Log.i(TAG, String.format("onAuthError: (%d) %s", errorInfo.code, errorInfo.message));
if(errorInfo.code == 403) {
- this.connection.state = ConnectionState.failed;
+ ConnectionStateChange failedStateChange =
+ new ConnectionStateChange(
+ connection.state,
+ ConnectionState.failed,
+ 0,
+ errorInfo);
+
+ this.connection.onConnectionStateChange(failedStateChange);
return;
}
From 9b683325229128f70ab53335012647e206978262 Mon Sep 17 00:00:00 2001
From: Kacper Kluka
Date: Wed, 26 May 2021 08:50:00 +0200
Subject: [PATCH 054/899] Revert using asterisk import
---
lib/src/main/java/io/ably/lib/http/HttpCore.java | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/lib/src/main/java/io/ably/lib/http/HttpCore.java b/lib/src/main/java/io/ably/lib/http/HttpCore.java
index 7ba3492c2..0b6ac95f5 100644
--- a/lib/src/main/java/io/ably/lib/http/HttpCore.java
+++ b/lib/src/main/java/io/ably/lib/http/HttpCore.java
@@ -20,7 +20,12 @@
import io.ably.lib.rest.Auth;
import io.ably.lib.transport.Defaults;
import io.ably.lib.transport.Hosts;
-import io.ably.lib.types.*;
+import io.ably.lib.types.AblyException;
+import io.ably.lib.types.ClientOptions;
+import io.ably.lib.types.ErrorInfo;
+import io.ably.lib.types.ErrorResponse;
+import io.ably.lib.types.Param;
+import io.ably.lib.types.ProxyOptions;
import io.ably.lib.util.AgentHeaderCreator;
import io.ably.lib.util.Log;
From ddd279cd03120efbf46b3f76f09e2fec7938bdcb Mon Sep 17 00:00:00 2001
From: Kacper Kluka
Date: Wed, 26 May 2021 08:50:20 +0200
Subject: [PATCH 055/899] Use equals instead of == when comparing strings
---
lib/src/main/java/io/ably/lib/util/AgentHeaderCreator.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/src/main/java/io/ably/lib/util/AgentHeaderCreator.java b/lib/src/main/java/io/ably/lib/util/AgentHeaderCreator.java
index 3e279f810..230dc6032 100644
--- a/lib/src/main/java/io/ably/lib/util/AgentHeaderCreator.java
+++ b/lib/src/main/java/io/ably/lib/util/AgentHeaderCreator.java
@@ -27,7 +27,7 @@ public static String create(Map additionalAgents) {
agentStringBuilder.append(getAdditionalAgentEntries(additionalAgents));
}
agentStringBuilder.append(Defaults.ABLY_AGENT_VERSION);
- if (BuildConfig.LIBRARY_NAME == ANDROID_LIBRARY_NAME) {
+ if (BuildConfig.LIBRARY_NAME.equals(ANDROID_LIBRARY_NAME)) {
agentStringBuilder.append(AGENT_ENTRY_SEPARATOR);
agentStringBuilder.append(getAndroidAgent());
}
From 522f29be7c671272604b4014d8c7d51002dc5e73 Mon Sep 17 00:00:00 2001
From: Kacper Kluka
Date: Wed, 26 May 2021 08:50:46 +0200
Subject: [PATCH 056/899] Do not sort additional agent names
---
.../java/io/ably/lib/util/AgentHeaderCreator.java | 12 +-----------
1 file changed, 1 insertion(+), 11 deletions(-)
diff --git a/lib/src/main/java/io/ably/lib/util/AgentHeaderCreator.java b/lib/src/main/java/io/ably/lib/util/AgentHeaderCreator.java
index 230dc6032..bc0b1c920 100644
--- a/lib/src/main/java/io/ably/lib/util/AgentHeaderCreator.java
+++ b/lib/src/main/java/io/ably/lib/util/AgentHeaderCreator.java
@@ -4,9 +4,6 @@
import io.ably.lib.BuildConfig;
import io.ably.lib.transport.Defaults;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
import java.util.Map;
public class AgentHeaderCreator {
@@ -36,7 +33,7 @@ public static String create(Map additionalAgents) {
private static String getAdditionalAgentEntries(Map additionalAgents) {
StringBuilder additionalAgentsBuilder = new StringBuilder();
- for (String additionalAgentName : getSortedAgentNames(additionalAgents)) {
+ for (String additionalAgentName : additionalAgents.keySet()) {
String additionalAgentVersion = additionalAgents.get(additionalAgentName);
additionalAgentsBuilder.append(additionalAgentName);
if (additionalAgentVersion != null) {
@@ -48,13 +45,6 @@ private static String getAdditionalAgentEntries(Map additionalAg
return additionalAgentsBuilder.toString();
}
-
- private static List getSortedAgentNames(Map agents) {
- List agentNames = new ArrayList<>(agents.keySet());
- Collections.sort(agentNames);
- return agentNames;
- }
-
private static String getAndroidAgent() {
return "android" + AGENT_DIVIDER + Build.VERSION.SDK_INT;
}
From a731f7f03280a76cd545a4c7009bbfc546842ee6 Mon Sep 17 00:00:00 2001
From: Martin Morek
Date: Wed, 26 May 2021 16:21:54 +0200
Subject: [PATCH 057/899] Added test, if realtime client fails with status code
403, connection state transitions to failed state
---
lib/src/main/java/io/ably/lib/rest/Auth.java | 2 +-
.../ably/lib/transport/ConnectionManager.java | 2 +-
.../lib/test/realtime/RealtimeAuthTest.java | 39 +++++++++++++++++++
3 files changed, 41 insertions(+), 2 deletions(-)
diff --git a/lib/src/main/java/io/ably/lib/rest/Auth.java b/lib/src/main/java/io/ably/lib/rest/Auth.java
index 1fd2b806a..0bb6455b9 100644
--- a/lib/src/main/java/io/ably/lib/rest/Auth.java
+++ b/lib/src/main/java/io/ably/lib/rest/Auth.java
@@ -658,7 +658,7 @@ public Object handleResponse(HttpCore.Response response, ErrorInfo error) throws
authUrlResponse = HttpHelpers.getUri(ably.httpCore, tokenOptions.authUrl, tokenOptions.authHeaders, HttpUtils.flattenParams(requestParams), responseHandler);
}
} catch(AblyException e) {
- throw AblyException.fromErrorInfo(e, new ErrorInfo("authUrl failed with an exception", 401, 80019));
+ throw AblyException.fromErrorInfo(e, new ErrorInfo("authUrl failed with an exception", e.errorInfo.statusCode, 80019));
}
if(authUrlResponse == null) {
throw AblyException.fromErrorInfo(null, new ErrorInfo("Empty response received from authUrl", 401, 80019));
diff --git a/lib/src/main/java/io/ably/lib/transport/ConnectionManager.java b/lib/src/main/java/io/ably/lib/transport/ConnectionManager.java
index c57bc1a0f..045c811a2 100644
--- a/lib/src/main/java/io/ably/lib/transport/ConnectionManager.java
+++ b/lib/src/main/java/io/ably/lib/transport/ConnectionManager.java
@@ -979,7 +979,7 @@ public void onAuthUpdated(final String token, final boolean waitForResponse) thr
public void onAuthError(ErrorInfo errorInfo) {
Log.i(TAG, String.format("onAuthError: (%d) %s", errorInfo.code, errorInfo.message));
- if(errorInfo.code == 403) {
+ if(errorInfo.statusCode == 403) {
ConnectionStateChange failedStateChange =
new ConnectionStateChange(
connection.state,
diff --git a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeAuthTest.java b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeAuthTest.java
index 9fd3ff020..38d0036ed 100644
--- a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeAuthTest.java
+++ b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeAuthTest.java
@@ -2,6 +2,7 @@
import io.ably.lib.realtime.*;
import io.ably.lib.test.common.Setup;
+import io.ably.lib.types.*;
import io.ably.lib.util.Log;
import io.ably.lib.debug.DebugOptions;
@@ -74,6 +75,44 @@ public void auth_client_match_tokendetails_null_clientId() {
}
}
+ /**
+ * RSA4d: If a request by a realtime client to an authUrl results in an HTTP 403 response,
+ * or any of an authUrl request, an authCallback, or a request to Ably to exchange
+ * a TokenRequest for a TokenDetails result in an ErrorInfo with statusCode 403,
+ * as part of an attempt by the realtime client to authenticate, then the client library
+ * should transition to the FAILED state, with an ErrorInfo (with code 80019, statusCode 403,
+ * and cause set to the underlying cause) emitted with the state change and set as the connection
+ * errorReason
+ *
+ * Verify end connection state is failed
+ */
+ @Test
+ public void auth_client_fails_authorize_server_forbidden() {
+ try {
+ ClientOptions opts = createOptions();
+ opts.autoConnect = false;
+ opts.useTokenAuth = true;
+ opts.authUrl = "https://echo.ably.io/respondwith";
+ opts.authParams = new Param[]{ new Param("status", 403)};
+
+ AblyRealtime ablyRealtime = new AblyRealtime(opts);
+
+ try {
+ ablyRealtime.auth.authorize(null, null);
+ } catch (AblyException e) {
+ assertEquals(403, e.errorInfo.statusCode);
+ }
+
+ /* wait for failed state */
+ Helpers.ConnectionWaiter connectionWaiter = new Helpers.ConnectionWaiter(ablyRealtime.connection);
+ connectionWaiter.waitFor(ConnectionState.failed);
+ assertEquals("Verify connected state has failed", ConnectionState.failed, ablyRealtime.connection.state);
+ } catch (AblyException e) {
+ e.printStackTrace();
+ fail();
+ }
+ }
+
/**
* RSA12a: The clientId attribute of a TokenRequest or TokenDetails
* used for authentication is null, or ConnectionDetails#clientId is null
From c3e290fb6a169977ca7d5d5f346b4f558b16a93e Mon Sep 17 00:00:00 2001
From: Kacper Kluka
Date: Thu, 27 May 2021 15:33:22 +0200
Subject: [PATCH 058/899] Fix tests so they don't care about additional agents
order
---
.../ably/lib/util/AgentHeaderCreatorTest.java | 31 +++++++++++++++++--
1 file changed, 28 insertions(+), 3 deletions(-)
diff --git a/lib/src/test/java/io/ably/lib/util/AgentHeaderCreatorTest.java b/lib/src/test/java/io/ably/lib/util/AgentHeaderCreatorTest.java
index 2f7be2aad..15e699b6b 100644
--- a/lib/src/test/java/io/ably/lib/util/AgentHeaderCreatorTest.java
+++ b/lib/src/test/java/io/ably/lib/util/AgentHeaderCreatorTest.java
@@ -4,7 +4,9 @@
import io.ably.lib.transport.Defaults;
import org.junit.Test;
+import java.util.Arrays;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import static org.junit.Assert.*;
@@ -21,7 +23,7 @@ public void should_create_default_header_if_there_are_no_additional_agents() {
String agentHeaderValue = AgentHeaderCreator.create(agents);
// then
- assertEquals(PREDEFINED_AGENTS, agentHeaderValue);
+ assertMatchingAgentHeaders(PREDEFINED_AGENTS, agentHeaderValue);
}
@Test
@@ -35,7 +37,7 @@ public void should_create_header_with_appended_agents_if_they_are_provided() {
String agentHeaderValue = AgentHeaderCreator.create(agents);
// then
- assertEquals("library/1.0.1 other/0.8.2 " + PREDEFINED_AGENTS, agentHeaderValue);
+ assertMatchingAgentHeaders("library/1.0.1 other/0.8.2 " + PREDEFINED_AGENTS, agentHeaderValue);
}
@Test
@@ -49,6 +51,29 @@ public void should_create_header_with_appended_agents_without_versions() {
String agentHeaderValue = AgentHeaderCreator.create(agents);
// then
- assertEquals("library/1.0.1 no-version " + PREDEFINED_AGENTS, agentHeaderValue);
+ assertMatchingAgentHeaders("library/1.0.1 no-version " + PREDEFINED_AGENTS, agentHeaderValue);
+ }
+
+ private void assertMatchingAgentHeaders(String expectedAgentHeader, String actualAgentHeader) {
+ assertPredefinedAgentsAreAtTheEnd(actualAgentHeader);
+ assertAllExpectedAgentsArePresentInActualAgents(expectedAgentHeader, actualAgentHeader);
+ }
+
+ private void assertPredefinedAgentsAreAtTheEnd(String actualAgentHeader) {
+ assertTrue(
+ actualAgentHeader + " does not end with the library predefined agents",
+ actualAgentHeader.endsWith(PREDEFINED_AGENTS)
+ );
+ }
+
+ private void assertAllExpectedAgentsArePresentInActualAgents(String expectedAgentHeader, String actualAgentHeader) {
+ List actualAgents = Arrays.asList(actualAgentHeader.split(" "));
+ String[] expectedAgents = expectedAgentHeader.split(" ");
+ for (String expectedAgent : expectedAgents) {
+ assertTrue(
+ actualAgentHeader + " does not include " + expectedAgent,
+ actualAgents.contains(expectedAgent)
+ );
+ }
}
}
From 3fb4f4978826d5db3dcc1e1176e5c5b63a582202 Mon Sep 17 00:00:00 2001
From: Kacper Kluka
Date: Thu, 27 May 2021 15:49:36 +0200
Subject: [PATCH 059/899] Changed the agent name validation regex
---
lib/src/main/java/io/ably/lib/util/AblyAgentValidator.java | 4 ++--
.../test/java/io/ably/lib/util/AblyAgentValidatorTest.java | 5 +++--
2 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/lib/src/main/java/io/ably/lib/util/AblyAgentValidator.java b/lib/src/main/java/io/ably/lib/util/AblyAgentValidator.java
index 84cf0f32b..e49bcbc4a 100644
--- a/lib/src/main/java/io/ably/lib/util/AblyAgentValidator.java
+++ b/lib/src/main/java/io/ably/lib/util/AblyAgentValidator.java
@@ -6,9 +6,9 @@
public class AblyAgentValidator {
/**
* Agent name validation regex.
- * Allow only lowercase letters and '-'.
+ * Allow only lowercase letters, digits and characters from the set [ !#$%&'*+-.^_`|~ ].
*/
- private static final String AGENT_NAME_REGEX = "^[a-z\\-]+$";
+ private static final String AGENT_NAME_REGEX = "^[a-z0-9!#$%&'*+\\-.^_`|~]+$";
private static final Pattern agentNamePattern = Pattern.compile(AGENT_NAME_REGEX);
/**
diff --git a/lib/src/test/java/io/ably/lib/util/AblyAgentValidatorTest.java b/lib/src/test/java/io/ably/lib/util/AblyAgentValidatorTest.java
index f5ee969c5..0c44da0e1 100644
--- a/lib/src/test/java/io/ably/lib/util/AblyAgentValidatorTest.java
+++ b/lib/src/test/java/io/ably/lib/util/AblyAgentValidatorTest.java
@@ -79,7 +79,8 @@ public void should_return_true_if_all_agents_are_valid() {
// given
Map agents = new HashMap<>();
agents.put("valid-name", "1.0.1");
- agents.put("another-valid-name", null);
+ agents.put("another-valid-name123", null);
+ agents.put("fully-123-valid-!#$%&'*+.^_`|~-name", null);
// when
boolean areAgentsValid = AblyAgentValidator.areAllValid(agents);
@@ -94,7 +95,7 @@ public void should_return_false_if_any_agent_is_invalid() {
Map agents = new HashMap<>();
agents.put("valid-name", "1.0.1");
agents.put("invalid/name", "1.0.1");
- agents.put("another-valid-name", null);
+ agents.put("another-valid-name123", null);
// when
boolean areAgentsValid = AblyAgentValidator.areAllValid(agents);
From 22258979d0770976c06c9e689d193214b817c78b Mon Sep 17 00:00:00 2001
From: Kacper Kluka
Date: Fri, 28 May 2021 11:15:41 +0200
Subject: [PATCH 060/899] Place the library agent header at the start of agent
headers
---
.../java/io/ably/lib/util/AgentHeaderCreator.java | 5 +++--
.../io/ably/lib/util/AgentHeaderCreatorTest.java | 12 ++++++------
2 files changed, 9 insertions(+), 8 deletions(-)
diff --git a/lib/src/main/java/io/ably/lib/util/AgentHeaderCreator.java b/lib/src/main/java/io/ably/lib/util/AgentHeaderCreator.java
index bc0b1c920..fd69704ee 100644
--- a/lib/src/main/java/io/ably/lib/util/AgentHeaderCreator.java
+++ b/lib/src/main/java/io/ably/lib/util/AgentHeaderCreator.java
@@ -20,10 +20,11 @@ public class AgentHeaderCreator {
public static String create(Map additionalAgents) {
StringBuilder agentStringBuilder = new StringBuilder();
+ agentStringBuilder.append(Defaults.ABLY_AGENT_VERSION);
if (!additionalAgents.isEmpty()) {
+ agentStringBuilder.append(AGENT_ENTRY_SEPARATOR);
agentStringBuilder.append(getAdditionalAgentEntries(additionalAgents));
}
- agentStringBuilder.append(Defaults.ABLY_AGENT_VERSION);
if (BuildConfig.LIBRARY_NAME.equals(ANDROID_LIBRARY_NAME)) {
agentStringBuilder.append(AGENT_ENTRY_SEPARATOR);
agentStringBuilder.append(getAndroidAgent());
@@ -42,7 +43,7 @@ private static String getAdditionalAgentEntries(Map additionalAg
}
additionalAgentsBuilder.append(AGENT_ENTRY_SEPARATOR);
}
- return additionalAgentsBuilder.toString();
+ return additionalAgentsBuilder.toString().trim();
}
private static String getAndroidAgent() {
diff --git a/lib/src/test/java/io/ably/lib/util/AgentHeaderCreatorTest.java b/lib/src/test/java/io/ably/lib/util/AgentHeaderCreatorTest.java
index 15e699b6b..de2d85367 100644
--- a/lib/src/test/java/io/ably/lib/util/AgentHeaderCreatorTest.java
+++ b/lib/src/test/java/io/ably/lib/util/AgentHeaderCreatorTest.java
@@ -37,7 +37,7 @@ public void should_create_header_with_appended_agents_if_they_are_provided() {
String agentHeaderValue = AgentHeaderCreator.create(agents);
// then
- assertMatchingAgentHeaders("library/1.0.1 other/0.8.2 " + PREDEFINED_AGENTS, agentHeaderValue);
+ assertMatchingAgentHeaders(PREDEFINED_AGENTS + " library/1.0.1 other/0.8.2", agentHeaderValue);
}
@Test
@@ -51,18 +51,18 @@ public void should_create_header_with_appended_agents_without_versions() {
String agentHeaderValue = AgentHeaderCreator.create(agents);
// then
- assertMatchingAgentHeaders("library/1.0.1 no-version " + PREDEFINED_AGENTS, agentHeaderValue);
+ assertMatchingAgentHeaders(PREDEFINED_AGENTS + " library/1.0.1 no-version", agentHeaderValue);
}
private void assertMatchingAgentHeaders(String expectedAgentHeader, String actualAgentHeader) {
- assertPredefinedAgentsAreAtTheEnd(actualAgentHeader);
+ assertPredefinedAgentsAreAtTheStart(actualAgentHeader);
assertAllExpectedAgentsArePresentInActualAgents(expectedAgentHeader, actualAgentHeader);
}
- private void assertPredefinedAgentsAreAtTheEnd(String actualAgentHeader) {
+ private void assertPredefinedAgentsAreAtTheStart(String actualAgentHeader) {
assertTrue(
- actualAgentHeader + " does not end with the library predefined agents",
- actualAgentHeader.endsWith(PREDEFINED_AGENTS)
+ actualAgentHeader + " does not start with the library predefined agents",
+ actualAgentHeader.startsWith(PREDEFINED_AGENTS)
);
}
From 2561db3064b8e97b6a9454070038eee1bef6db3e Mon Sep 17 00:00:00 2001
From: Kacper Kluka
Date: Fri, 28 May 2021 11:25:59 +0200
Subject: [PATCH 061/899] Add Android platform information only in the
ably-android library
---
.../main/java/io/ably/lib/rest/AblyRest.java | 4 ++++
.../io/ably/lib/util/AgentHeaderCreator.java | 23 +++++++++++++------
.../ably/lib/util/AgentHeaderCreatorTest.java | 23 +++++++++++++++++--
3 files changed, 41 insertions(+), 9 deletions(-)
diff --git a/android/src/main/java/io/ably/lib/rest/AblyRest.java b/android/src/main/java/io/ably/lib/rest/AblyRest.java
index a53af4954..51ec35e84 100644
--- a/android/src/main/java/io/ably/lib/rest/AblyRest.java
+++ b/android/src/main/java/io/ably/lib/rest/AblyRest.java
@@ -1,9 +1,11 @@
package io.ably.lib.rest;
import android.content.Context;
+import android.os.Build;
import io.ably.lib.push.LocalDevice;
import io.ably.lib.types.AblyException;
import io.ably.lib.types.ClientOptions;
+import io.ably.lib.util.AgentHeaderCreator;
import io.ably.lib.util.Log;
public class AblyRest extends AblyBase {
@@ -17,6 +19,7 @@ public class AblyRest extends AblyBase {
*/
public AblyRest(String key) throws AblyException {
super(key);
+ AgentHeaderCreator.setAndroidPlatformAgent(Build.VERSION.SDK_INT);
}
/**
@@ -26,6 +29,7 @@ public AblyRest(String key) throws AblyException {
*/
public AblyRest(ClientOptions options) throws AblyException {
super(options);
+ AgentHeaderCreator.setAndroidPlatformAgent(Build.VERSION.SDK_INT);
}
/**
diff --git a/lib/src/main/java/io/ably/lib/util/AgentHeaderCreator.java b/lib/src/main/java/io/ably/lib/util/AgentHeaderCreator.java
index fd69704ee..98ef1d841 100644
--- a/lib/src/main/java/io/ably/lib/util/AgentHeaderCreator.java
+++ b/lib/src/main/java/io/ably/lib/util/AgentHeaderCreator.java
@@ -1,7 +1,5 @@
package io.ably.lib.util;
-import android.os.Build;
-import io.ably.lib.BuildConfig;
import io.ably.lib.transport.Defaults;
import java.util.Map;
@@ -16,7 +14,11 @@ public class AgentHeaderCreator {
* Separates agent name from agent version.
*/
private static final String AGENT_DIVIDER = "/";
- private static final String ANDROID_LIBRARY_NAME = "android";
+
+ /**
+ * Optional platform agent, e.g. "android/24"
+ */
+ private static String platformAgent = null;
public static String create(Map additionalAgents) {
StringBuilder agentStringBuilder = new StringBuilder();
@@ -25,9 +27,9 @@ public static String create(Map additionalAgents) {
agentStringBuilder.append(AGENT_ENTRY_SEPARATOR);
agentStringBuilder.append(getAdditionalAgentEntries(additionalAgents));
}
- if (BuildConfig.LIBRARY_NAME.equals(ANDROID_LIBRARY_NAME)) {
+ if (platformAgent != null) {
agentStringBuilder.append(AGENT_ENTRY_SEPARATOR);
- agentStringBuilder.append(getAndroidAgent());
+ agentStringBuilder.append(platformAgent);
}
return agentStringBuilder.toString();
}
@@ -46,7 +48,14 @@ private static String getAdditionalAgentEntries(Map additionalAg
return additionalAgentsBuilder.toString().trim();
}
- private static String getAndroidAgent() {
- return "android" + AGENT_DIVIDER + Build.VERSION.SDK_INT;
+ public static void setAndroidPlatformAgent(int platformVersion) {
+ platformAgent = "android" + AGENT_DIVIDER + platformVersion;
+ }
+
+ /**
+ * Added to clear AgentHeaderCreator state for unit tests.
+ */
+ public static void clearPlatformAgent() {
+ platformAgent = null;
}
}
diff --git a/lib/src/test/java/io/ably/lib/util/AgentHeaderCreatorTest.java b/lib/src/test/java/io/ably/lib/util/AgentHeaderCreatorTest.java
index de2d85367..9b62da9f4 100644
--- a/lib/src/test/java/io/ably/lib/util/AgentHeaderCreatorTest.java
+++ b/lib/src/test/java/io/ably/lib/util/AgentHeaderCreatorTest.java
@@ -1,7 +1,7 @@
package io.ably.lib.util;
-import android.os.Build;
import io.ably.lib.transport.Defaults;
+import org.junit.Before;
import org.junit.Test;
import java.util.Arrays;
@@ -12,7 +12,12 @@
import static org.junit.Assert.*;
public class AgentHeaderCreatorTest {
- private final static String PREDEFINED_AGENTS = Defaults.ABLY_AGENT_VERSION + " android/" + Build.VERSION.SDK_INT;
+ private final static String PREDEFINED_AGENTS = Defaults.ABLY_AGENT_VERSION;
+
+ @Before
+ public void beforeEach() {
+ AgentHeaderCreator.clearPlatformAgent();
+ }
@Test
public void should_create_default_header_if_there_are_no_additional_agents() {
@@ -54,6 +59,20 @@ public void should_create_header_with_appended_agents_without_versions() {
assertMatchingAgentHeaders(PREDEFINED_AGENTS + " library/1.0.1 no-version", agentHeaderValue);
}
+ @Test
+ public void should_create_header_with_platform_agent_if_it_is_provided() {
+ // given
+ Map agents = new HashMap<>();
+ agents.put("library", "1.0.1");
+ AgentHeaderCreator.setAndroidPlatformAgent(25);
+
+ // when
+ String agentHeaderValue = AgentHeaderCreator.create(agents);
+
+ // then
+ assertMatchingAgentHeaders(PREDEFINED_AGENTS + " android/25 library/1.0.1", agentHeaderValue);
+ }
+
private void assertMatchingAgentHeaders(String expectedAgentHeader, String actualAgentHeader) {
assertPredefinedAgentsAreAtTheStart(actualAgentHeader);
assertAllExpectedAgentsArePresentInActualAgents(expectedAgentHeader, actualAgentHeader);
From 6db763f7b49a17f8b47e1382f117d05616013b92 Mon Sep 17 00:00:00 2001
From: Kacper Kluka
Date: Fri, 28 May 2021 11:40:12 +0200
Subject: [PATCH 062/899] Fix asterisk imports
---
lib/src/main/java/io/ably/lib/rest/AblyBase.java | 6 +++++-
.../test/java/io/ably/lib/util/AgentHeaderCreatorTest.java | 2 +-
2 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/lib/src/main/java/io/ably/lib/rest/AblyBase.java b/lib/src/main/java/io/ably/lib/rest/AblyBase.java
index d908109f7..9aba0f921 100644
--- a/lib/src/main/java/io/ably/lib/rest/AblyBase.java
+++ b/lib/src/main/java/io/ably/lib/rest/AblyBase.java
@@ -29,7 +29,11 @@
import io.ably.lib.types.ReadOnlyMap;
import io.ably.lib.types.Stats;
import io.ably.lib.types.StatsReader;
-import io.ably.lib.util.*;
+import io.ably.lib.util.AblyAgentValidator;
+import io.ably.lib.util.Crypto;
+import io.ably.lib.util.InternalMap;
+import io.ably.lib.util.Log;
+import io.ably.lib.util.Serialisation;
/**
* AblyBase
diff --git a/lib/src/test/java/io/ably/lib/util/AgentHeaderCreatorTest.java b/lib/src/test/java/io/ably/lib/util/AgentHeaderCreatorTest.java
index 9b62da9f4..d65b94abb 100644
--- a/lib/src/test/java/io/ably/lib/util/AgentHeaderCreatorTest.java
+++ b/lib/src/test/java/io/ably/lib/util/AgentHeaderCreatorTest.java
@@ -9,7 +9,7 @@
import java.util.List;
import java.util.Map;
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertTrue;
public class AgentHeaderCreatorTest {
private final static String PREDEFINED_AGENTS = Defaults.ABLY_AGENT_VERSION;
From 8be201a2df90b9f054b9d347cfb67b81c1d46bf8 Mon Sep 17 00:00:00 2001
From: Kacper Kluka
Date: Fri, 28 May 2021 14:29:47 +0200
Subject: [PATCH 063/899] Fix failing tests
---
.../java/io/ably/lib/util/AgentHeaderCreator.java | 2 +-
.../java/io/ably/lib/test/rest/HttpHeaderTest.java | 10 ++++++----
.../java/io/ably/lib/util/AgentHeaderCreatorTest.java | 11 +++++++++++
3 files changed, 18 insertions(+), 5 deletions(-)
diff --git a/lib/src/main/java/io/ably/lib/util/AgentHeaderCreator.java b/lib/src/main/java/io/ably/lib/util/AgentHeaderCreator.java
index 98ef1d841..fcc6ea6c3 100644
--- a/lib/src/main/java/io/ably/lib/util/AgentHeaderCreator.java
+++ b/lib/src/main/java/io/ably/lib/util/AgentHeaderCreator.java
@@ -23,7 +23,7 @@ public class AgentHeaderCreator {
public static String create(Map additionalAgents) {
StringBuilder agentStringBuilder = new StringBuilder();
agentStringBuilder.append(Defaults.ABLY_AGENT_VERSION);
- if (!additionalAgents.isEmpty()) {
+ if (additionalAgents != null && !additionalAgents.isEmpty()) {
agentStringBuilder.append(AGENT_ENTRY_SEPARATOR);
agentStringBuilder.append(getAdditionalAgentEntries(additionalAgents));
}
diff --git a/lib/src/test/java/io/ably/lib/test/rest/HttpHeaderTest.java b/lib/src/test/java/io/ably/lib/test/rest/HttpHeaderTest.java
index 9c642a418..71fd6b8cc 100644
--- a/lib/src/test/java/io/ably/lib/test/rest/HttpHeaderTest.java
+++ b/lib/src/test/java/io/ably/lib/test/rest/HttpHeaderTest.java
@@ -17,6 +17,8 @@
import io.ably.lib.types.AblyException;
import io.ably.lib.types.ClientOptions;
+import static io.ably.lib.transport.Defaults.ABLY_AGENT_VERSION;
+
/**
* Created by VOstopolets on 8/17/16.
*/
@@ -46,11 +48,11 @@ public static void tearDown() {
}
/**
- * The header X-Ably-Lib: [lib][.optional variant]?-[version]
+ * The header Ably-Agent: [lib]/[version]
* should be included in all REST requests to the Ably endpoint
- * see {@link io.ably.lib.http.HttpUtils#ABLY_LIB_VERSION}
+ * see {@link io.ably.lib.http.HttpUtils#ABLY_AGENT_VERSION}
*
- * Spec: RSC7b, G4
+ * Spec: RSC7d, G4
*
*
* Spec: RSC7a: Must have the header X-Ably-Version: 1.0 (or whatever the
@@ -84,7 +86,7 @@ public void header_lib_channel_publish() {
*/
Assert.assertNotNull("Expected headers", headers);
Assert.assertEquals(headers.get("x-ably-version"), "1.2");
- Assert.assertEquals(headers.get("x-ably-lib"), "java-1.2.6");
+ Assert.assertEquals(headers.get("ably-agent"), ABLY_AGENT_VERSION);
} catch (AblyException e) {
e.printStackTrace();
Assert.fail("header_lib_channel_publish: Unexpected exception");
diff --git a/lib/src/test/java/io/ably/lib/util/AgentHeaderCreatorTest.java b/lib/src/test/java/io/ably/lib/util/AgentHeaderCreatorTest.java
index d65b94abb..9fc07dd4c 100644
--- a/lib/src/test/java/io/ably/lib/util/AgentHeaderCreatorTest.java
+++ b/lib/src/test/java/io/ably/lib/util/AgentHeaderCreatorTest.java
@@ -31,6 +31,17 @@ public void should_create_default_header_if_there_are_no_additional_agents() {
assertMatchingAgentHeaders(PREDEFINED_AGENTS, agentHeaderValue);
}
+ @Test
+ public void should_create_default_header_if_additional_agents_are_null() {
+ // given
+
+ // when
+ String agentHeaderValue = AgentHeaderCreator.create(null);
+
+ // then
+ assertMatchingAgentHeaders(PREDEFINED_AGENTS, agentHeaderValue);
+ }
+
@Test
public void should_create_header_with_appended_agents_if_they_are_provided() {
// given
From c23b62e0639326121d013e523f9be14474d70dca Mon Sep 17 00:00:00 2001
From: Kacper Kluka
Date: Fri, 28 May 2021 15:10:33 +0200
Subject: [PATCH 064/899] Fix realtime tests failing
---
.../ably/lib/test/realtime/RealtimeHttpHeaderTest.java | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeHttpHeaderTest.java b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeHttpHeaderTest.java
index b6b120b54..0eeb6ad2e 100644
--- a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeHttpHeaderTest.java
+++ b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeHttpHeaderTest.java
@@ -86,12 +86,12 @@ public void realtime_websocket_param_test() {
assertEquals("Verify correct version", requestParameters.get("v"),
Collections.singletonList("1.2"));
- /* Spec RTN2g
- * This test should not directly validate version against Defaults.ABLY_LIB_VERSION, nor
- * Defaults.ABLY_LIB_PARAM, as ultimately the request param has been derived from those values.
+ /* Spec RSC7d3
+ * This test should not directly validate version against Defaults.ABLY_AGENT_VERSION, nor
+ * Defaults.ABLY_AGENT_PARAM, as ultimately the request param has been derived from those values.
*/
- assertEquals("Verify correct lib version", requestParameters.get("lib"),
- Collections.singletonList("java-1.2.6"));
+ assertEquals("Verify correct lib version", requestParameters.get("agent"),
+ Collections.singletonList("ably-java/1.2.6"));
/* Spec RTN2a */
assertEquals("Verify correct format", requestParameters.get("format"),
From 2664364c0f34ca9ce3ccf4607daee51120769533 Mon Sep 17 00:00:00 2001
From: Martin Morek
Date: Fri, 28 May 2021 15:47:14 +0200
Subject: [PATCH 065/899] Adjusted logic to not automatically select token
authorization if clientId is set
---
lib/src/main/java/io/ably/lib/rest/Auth.java | 3 +--
.../test/java/io/ably/lib/test/rest/RestAuthTest.java | 10 +++++-----
2 files changed, 6 insertions(+), 7 deletions(-)
diff --git a/lib/src/main/java/io/ably/lib/rest/Auth.java b/lib/src/main/java/io/ably/lib/rest/Auth.java
index f25227eb6..27fef523a 100644
--- a/lib/src/main/java/io/ably/lib/rest/Auth.java
+++ b/lib/src/main/java/io/ably/lib/rest/Auth.java
@@ -868,8 +868,7 @@ public void onAuthError(ErrorInfo err) {
/* decide default auth method (spec: RSA4) */
if(authOptions.key != null) {
- if(options.clientId == null &&
- !options.useTokenAuth &&
+ if(!options.useTokenAuth &&
options.token == null &&
options.tokenDetails == null &&
options.authCallback == null &&
diff --git a/lib/src/test/java/io/ably/lib/test/rest/RestAuthTest.java b/lib/src/test/java/io/ably/lib/test/rest/RestAuthTest.java
index b1844d4bc..1c87757c6 100644
--- a/lib/src/test/java/io/ably/lib/test/rest/RestAuthTest.java
+++ b/lib/src/test/java/io/ably/lib/test/rest/RestAuthTest.java
@@ -191,20 +191,20 @@ public String getTokenRequest(TokenParams params) throws AblyException {
}
/**
- * Init library with a key and clientId; expect token auth to be chosen
- * Spec: RSA4, RSC17, RSA7b1
+ * Init library with a key and clientId; expect basic auth to be chosen
+ * Spec: RSC17, RSA7b1
*/
@Test
- public void authinit_clientId_implies_token() {
+ public void authinit_clientId_auth_basic() {
try {
ClientOptions opts = createOptions(testVars.keys[0].keyStr);
opts.clientId = "testClientId";
AblyRest ably = new AblyRest(opts);
- assertEquals("Unexpected Auth method mismatch", ably.auth.getAuthMethod(), AuthMethod.token);
+ assertEquals("Unexpected Auth method mismatch", ably.auth.getAuthMethod(), AuthMethod.basic);
assertEquals("Unexpected clientId mismatch", ably.auth.clientId, "testClientId");
} catch (AblyException e) {
e.printStackTrace();
- fail("authinit_clientId_implies_token: Unexpected exception instantiating library");
+ fail("authinit_clientId_auth_basic: Unexpected exception instantiating library");
}
}
From 682ac145d6d70761a753fe38332157dca21151ab Mon Sep 17 00:00:00 2001
From: Martin Morek
Date: Sat, 29 May 2021 17:13:15 +0200
Subject: [PATCH 066/899] Implemented PR comments - improved test
---
.../lib/test/realtime/RealtimeAuthTest.java | 30 ++++++++++++++++---
1 file changed, 26 insertions(+), 4 deletions(-)
diff --git a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeAuthTest.java b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeAuthTest.java
index 38d0036ed..88e8c4390 100644
--- a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeAuthTest.java
+++ b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeAuthTest.java
@@ -85,28 +85,50 @@ public void auth_client_match_tokendetails_null_clientId() {
* errorReason
*
* Verify end connection state is failed
+ * Spec: RSA4d, RSA4d1
*/
@Test
public void auth_client_fails_authorize_server_forbidden() {
try {
- ClientOptions opts = createOptions();
+ ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr);
+ AblyRest ablyForToken = new AblyRest(optsForToken);
+ TokenDetails tokenDetails = ablyForToken.auth.requestToken(null, null);
+
+ ClientOptions opts = createOptions(testVars.keys[0].keyStr);
opts.autoConnect = false;
+ opts.tokenDetails = tokenDetails;
opts.useTokenAuth = true;
opts.authUrl = "https://echo.ably.io/respondwith";
opts.authParams = new Param[]{ new Param("status", 403)};
- AblyRealtime ablyRealtime = new AblyRealtime(opts);
+ final AblyRealtime ablyRealtime = new AblyRealtime(opts);
+ ablyRealtime.connection.connect();
+
+ Helpers.ConnectionWaiter connectionWaiter = new Helpers.ConnectionWaiter(ablyRealtime.connection);
+ connectionWaiter.waitFor(ConnectionState.connected);
+
+ ablyRealtime.connection.once(ConnectionEvent.failed, new ConnectionStateListener() {
+ @Override
+ public void onConnectionStateChanged(ConnectionStateChange stateChange) {
+ assertEquals(stateChange.previous, ConnectionState.connected);
+ assertEquals(stateChange.reason.code, 80019);
+ assertEquals(80019, ablyRealtime.connection.reason.code);
+ assertEquals(403, ablyRealtime.connection.reason.statusCode);
+ }
+ });
try {
- ablyRealtime.auth.authorize(null, null);
+ opts.tokenDetails = null;
+ ablyRealtime.auth.authorize(null, opts);
} catch (AblyException e) {
assertEquals(403, e.errorInfo.statusCode);
+ assertEquals(80019, e.errorInfo.code);
}
/* wait for failed state */
- Helpers.ConnectionWaiter connectionWaiter = new Helpers.ConnectionWaiter(ablyRealtime.connection);
connectionWaiter.waitFor(ConnectionState.failed);
assertEquals("Verify connected state has failed", ConnectionState.failed, ablyRealtime.connection.state);
+ assertEquals("Check correct cause error code", 403, ablyRealtime.connection.reason.statusCode);
} catch (AblyException e) {
e.printStackTrace();
fail();
From 08c9e6281a647e981516dd23ae75452cb23da29c Mon Sep 17 00:00:00 2001
From: Kacper Kluka
Date: Mon, 31 May 2021 12:26:04 +0200
Subject: [PATCH 067/899] Create and pass an instance of a platform specific
agent provider instead of using static fields
---
.../main/java/io/ably/lib/rest/AblyRest.java | 9 ++---
.../util/AndroidPlatformAgentProvider.java | 10 ++++++
.../main/java/io/ably/lib/rest/AblyRest.java | 5 +--
.../lib/util/JavaPlatformAgentProvider.java | 8 +++++
.../main/java/io/ably/lib/http/HttpCore.java | 7 ++--
.../io/ably/lib/realtime/AblyRealtime.java | 2 +-
.../java/io/ably/lib/realtime/Connection.java | 5 +--
.../main/java/io/ably/lib/rest/AblyBase.java | 13 ++++---
.../ably/lib/transport/ConnectionManager.java | 12 ++++---
.../io/ably/lib/transport/ITransport.java | 7 ++--
.../io/ably/lib/util/AgentHeaderCreator.java | 21 ++---------
.../ably/lib/util/PlatformAgentProvider.java | 10 ++++++
.../test/realtime/ConnectionManagerTest.java | 3 +-
.../java/io/ably/lib/test/rest/HttpTest.java | 35 ++++++++++---------
.../test/util/EmptyPlatformAgentProvider.java | 10 ++++++
.../lib/test/util/MockWebsocketFactory.java | 2 +-
.../ably/lib/util/AgentHeaderCreatorTest.java | 24 +++++++------
17 files changed, 113 insertions(+), 70 deletions(-)
create mode 100644 android/src/main/java/io/ably/lib/util/AndroidPlatformAgentProvider.java
create mode 100644 java/src/main/java/io/ably/lib/util/JavaPlatformAgentProvider.java
create mode 100644 lib/src/main/java/io/ably/lib/util/PlatformAgentProvider.java
create mode 100644 lib/src/test/java/io/ably/lib/test/util/EmptyPlatformAgentProvider.java
diff --git a/android/src/main/java/io/ably/lib/rest/AblyRest.java b/android/src/main/java/io/ably/lib/rest/AblyRest.java
index 51ec35e84..da9e0893f 100644
--- a/android/src/main/java/io/ably/lib/rest/AblyRest.java
+++ b/android/src/main/java/io/ably/lib/rest/AblyRest.java
@@ -1,11 +1,10 @@
package io.ably.lib.rest;
import android.content.Context;
-import android.os.Build;
import io.ably.lib.push.LocalDevice;
import io.ably.lib.types.AblyException;
import io.ably.lib.types.ClientOptions;
-import io.ably.lib.util.AgentHeaderCreator;
+import io.ably.lib.util.AndroidPlatformAgentProvider;
import io.ably.lib.util.Log;
public class AblyRest extends AblyBase {
@@ -18,8 +17,7 @@ public class AblyRest extends AblyBase {
* @throws AblyException
*/
public AblyRest(String key) throws AblyException {
- super(key);
- AgentHeaderCreator.setAndroidPlatformAgent(Build.VERSION.SDK_INT);
+ super(key, new AndroidPlatformAgentProvider());
}
/**
@@ -28,8 +26,7 @@ public AblyRest(String key) throws AblyException {
* @throws AblyException
*/
public AblyRest(ClientOptions options) throws AblyException {
- super(options);
- AgentHeaderCreator.setAndroidPlatformAgent(Build.VERSION.SDK_INT);
+ super(options, new AndroidPlatformAgentProvider());
}
/**
diff --git a/android/src/main/java/io/ably/lib/util/AndroidPlatformAgentProvider.java b/android/src/main/java/io/ably/lib/util/AndroidPlatformAgentProvider.java
new file mode 100644
index 000000000..000d858da
--- /dev/null
+++ b/android/src/main/java/io/ably/lib/util/AndroidPlatformAgentProvider.java
@@ -0,0 +1,10 @@
+package io.ably.lib.util;
+
+import android.os.Build;
+
+public class AndroidPlatformAgentProvider implements PlatformAgentProvider {
+ @Override
+ public String createPlatformAgent() {
+ return "android" + AgentHeaderCreator.AGENT_DIVIDER + Build.VERSION.SDK_INT;
+ }
+}
diff --git a/java/src/main/java/io/ably/lib/rest/AblyRest.java b/java/src/main/java/io/ably/lib/rest/AblyRest.java
index 9bd9d6b32..94edb72ad 100644
--- a/java/src/main/java/io/ably/lib/rest/AblyRest.java
+++ b/java/src/main/java/io/ably/lib/rest/AblyRest.java
@@ -2,6 +2,7 @@
import io.ably.lib.types.AblyException;
import io.ably.lib.types.ClientOptions;
+import io.ably.lib.util.JavaPlatformAgentProvider;
public class AblyRest extends AblyBase {
/**
@@ -13,7 +14,7 @@ public class AblyRest extends AblyBase {
* @throws AblyException
*/
public AblyRest(String key) throws AblyException {
- super(key);
+ super(key, new JavaPlatformAgentProvider());
}
/**
@@ -22,6 +23,6 @@ public AblyRest(String key) throws AblyException {
* @throws AblyException
*/
public AblyRest(ClientOptions options) throws AblyException {
- super(options);
+ super(options, new JavaPlatformAgentProvider());
}
}
diff --git a/java/src/main/java/io/ably/lib/util/JavaPlatformAgentProvider.java b/java/src/main/java/io/ably/lib/util/JavaPlatformAgentProvider.java
new file mode 100644
index 000000000..6d88c6b4d
--- /dev/null
+++ b/java/src/main/java/io/ably/lib/util/JavaPlatformAgentProvider.java
@@ -0,0 +1,8 @@
+package io.ably.lib.util;
+
+public class JavaPlatformAgentProvider implements PlatformAgentProvider {
+ @Override
+ public String createPlatformAgent() {
+ return null;
+ }
+}
diff --git a/lib/src/main/java/io/ably/lib/http/HttpCore.java b/lib/src/main/java/io/ably/lib/http/HttpCore.java
index 0b6ac95f5..606462bab 100644
--- a/lib/src/main/java/io/ably/lib/http/HttpCore.java
+++ b/lib/src/main/java/io/ably/lib/http/HttpCore.java
@@ -28,6 +28,7 @@
import io.ably.lib.types.ProxyOptions;
import io.ably.lib.util.AgentHeaderCreator;
import io.ably.lib.util.Log;
+import io.ably.lib.util.PlatformAgentProvider;
/**
* HttpCore performs authenticated HTTP synchronously. Internal; use Http or HttpScheduler instead.
@@ -38,9 +39,10 @@ public class HttpCore {
* Public API
*************************/
- public HttpCore(ClientOptions options, Auth auth) throws AblyException {
+ public HttpCore(ClientOptions options, Auth auth, PlatformAgentProvider platformAgentProvider) throws AblyException {
this.options = options;
this.auth = auth;
+ this.platformAgentProvider = platformAgentProvider;
this.scheme = options.tls ? "https://" : "http://";
this.port = Defaults.getPort(options);
this.hosts = new Hosts(options.restHost, Defaults.HOST_REST, options);
@@ -217,7 +219,7 @@ T httpExecute(HttpURLConnection conn, String method, Param[] headers, Reques
/* pass required headers */
conn.setRequestProperty(Defaults.ABLY_VERSION_HEADER, Defaults.ABLY_VERSION);
- conn.setRequestProperty(Defaults.ABLY_AGENT_HEADER, AgentHeaderCreator.create(options.agents));
+ conn.setRequestProperty(Defaults.ABLY_AGENT_HEADER, AgentHeaderCreator.create(options.agents, platformAgentProvider));
/* prepare request body */
byte[] body = null;
@@ -518,6 +520,7 @@ private Proxy getProxy(String host) {
private HttpAuth proxyAuth;
private Proxy proxy = Proxy.NO_PROXY;
private boolean isDisposed;
+ private final PlatformAgentProvider platformAgentProvider;
private static final String TAG = HttpCore.class.getName();
diff --git a/lib/src/main/java/io/ably/lib/realtime/AblyRealtime.java b/lib/src/main/java/io/ably/lib/realtime/AblyRealtime.java
index 0ac891ead..5ec5a7a37 100644
--- a/lib/src/main/java/io/ably/lib/realtime/AblyRealtime.java
+++ b/lib/src/main/java/io/ably/lib/realtime/AblyRealtime.java
@@ -51,7 +51,7 @@ public AblyRealtime(ClientOptions options) throws AblyException {
super(options);
final InternalChannels channels = new InternalChannels();
this.channels = channels;
- connection = new Connection(this, channels);
+ connection = new Connection(this, channels, platformAgentProvider);
/* remove all channels when the connection is closed, to avoid stalled state */
connection.on(ConnectionEvent.closed, new ConnectionStateListener() {
diff --git a/lib/src/main/java/io/ably/lib/realtime/Connection.java b/lib/src/main/java/io/ably/lib/realtime/Connection.java
index d15e6c6c8..fa7eb83a4 100644
--- a/lib/src/main/java/io/ably/lib/realtime/Connection.java
+++ b/lib/src/main/java/io/ably/lib/realtime/Connection.java
@@ -6,6 +6,7 @@
import io.ably.lib.types.ErrorInfo;
import io.ably.lib.util.EventEmitter;
import io.ably.lib.util.Log;
+import io.ably.lib.util.PlatformAgentProvider;
/**
* A class representing the connection associated with an AblyRealtime instance.
@@ -76,10 +77,10 @@ public void close() {
* internal
*****************/
- Connection(AblyRealtime ably, ConnectionManager.Channels channels) throws AblyException {
+ Connection(AblyRealtime ably, ConnectionManager.Channels channels, PlatformAgentProvider platformAgentProvider) throws AblyException {
this.ably = ably;
this.state = ConnectionState.initialized;
- this.connectionManager = new ConnectionManager(ably, this, channels);
+ this.connectionManager = new ConnectionManager(ably, this, channels, platformAgentProvider);
}
public void onConnectionStateChange(ConnectionStateChange stateChange) {
diff --git a/lib/src/main/java/io/ably/lib/rest/AblyBase.java b/lib/src/main/java/io/ably/lib/rest/AblyBase.java
index 9aba0f921..daf653ee5 100644
--- a/lib/src/main/java/io/ably/lib/rest/AblyBase.java
+++ b/lib/src/main/java/io/ably/lib/rest/AblyBase.java
@@ -33,6 +33,7 @@
import io.ably.lib.util.Crypto;
import io.ably.lib.util.InternalMap;
import io.ably.lib.util.Log;
+import io.ably.lib.util.PlatformAgentProvider;
import io.ably.lib.util.Serialisation;
/**
@@ -50,6 +51,7 @@ public abstract class AblyBase {
public final Channels channels;
public final Platform platform;
public final Push push;
+ protected final PlatformAgentProvider platformAgentProvider;
/**
* Instance the Ably library using a key only.
@@ -57,18 +59,20 @@ public abstract class AblyBase {
* simplest case of instancing the library with a key
* for basic authentication and no other options.
* @param key String key (obtained from application dashboard)
+ * @param platformAgentProvider provides platform agent for the agent header.
* @throws AblyException
*/
- public AblyBase(String key) throws AblyException {
- this(new ClientOptions(key));
+ public AblyBase(String key, PlatformAgentProvider platformAgentProvider) throws AblyException {
+ this(new ClientOptions(key), platformAgentProvider);
}
/**
* Instance the Ably library with the given options.
* @param options see {@link io.ably.lib.types.ClientOptions} for options
+ * @param platformAgentProvider provides platform agent for the agent header.
* @throws AblyException
*/
- public AblyBase(ClientOptions options) throws AblyException {
+ public AblyBase(ClientOptions options, PlatformAgentProvider platformAgentProvider) throws AblyException {
/* normalise options */
if(options == null) {
String msg = "no options provided";
@@ -87,8 +91,9 @@ public AblyBase(ClientOptions options) throws AblyException {
Log.setHandler(options.logHandler);
Log.i(getClass().getName(), "started");
+ this.platformAgentProvider = platformAgentProvider;
auth = new Auth(this, options);
- httpCore = new HttpCore(options, auth);
+ httpCore = new HttpCore(options, auth, this.platformAgentProvider);
http = new Http(new AsyncHttpScheduler(httpCore, options), new SyncHttpScheduler(httpCore));
channels = new InternalChannels();
diff --git a/lib/src/main/java/io/ably/lib/transport/ConnectionManager.java b/lib/src/main/java/io/ably/lib/transport/ConnectionManager.java
index c7cfb1b8f..61a307e00 100644
--- a/lib/src/main/java/io/ably/lib/transport/ConnectionManager.java
+++ b/lib/src/main/java/io/ably/lib/transport/ConnectionManager.java
@@ -20,6 +20,8 @@
import io.ably.lib.types.ProtocolSerializer;
import io.ably.lib.util.Log;
import io.ably.lib.transport.NetworkConnectivity.NetworkConnectivityListener;
+import io.ably.lib.util.PlatformAgentProvider;
+
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashMap;
@@ -715,10 +717,11 @@ public void run() {
* ConnectionManager
***********************/
- public ConnectionManager(final AblyRealtime ably, final Connection connection, final Channels channels) throws AblyException {
+ public ConnectionManager(final AblyRealtime ably, final Connection connection, final Channels channels, final PlatformAgentProvider platformAgentProvider) throws AblyException {
this.ably = ably;
this.connection = connection;
this.channels = channels;
+ this.platformAgentProvider = platformAgentProvider;
ClientOptions options = ably.options;
this.hosts = new Hosts(options.realtimeHost, Defaults.HOST_REALTIME, options);
@@ -1314,8 +1317,8 @@ public synchronized void onTransportUnavailable(ITransport transport, ErrorInfo
}
private class ConnectParams extends TransportParams {
- ConnectParams(ClientOptions options) {
- super(options);
+ ConnectParams(ClientOptions options, PlatformAgentProvider platformAgentProvider) {
+ super(options, platformAgentProvider);
this.connectionKey = connection.key;
this.connectionSerial = String.valueOf(connection.serial);
this.port = Defaults.getPort(options);
@@ -1335,7 +1338,7 @@ private void connectImpl(StateIndication request) {
host = hosts.getPreferredHost();
}
checkConnectionStale();
- pendingConnect = new ConnectParams(ably.options);
+ pendingConnect = new ConnectParams(ably.options, platformAgentProvider);
pendingConnect.host = host;
lastUsedHost = host;
@@ -1695,6 +1698,7 @@ private boolean isFatalError(ErrorInfo err) {
private final HashSet heartbeatWaiters = new HashSet();
private final ActionQueue actionQueue = new ActionQueue();
private final Hosts hosts;
+ private final PlatformAgentProvider platformAgentProvider;
private Thread handlerThread;
private final Map states = new HashMap<>();
diff --git a/lib/src/main/java/io/ably/lib/transport/ITransport.java b/lib/src/main/java/io/ably/lib/transport/ITransport.java
index 15360b504..364c03abd 100644
--- a/lib/src/main/java/io/ably/lib/transport/ITransport.java
+++ b/lib/src/main/java/io/ably/lib/transport/ITransport.java
@@ -7,6 +7,7 @@
import io.ably.lib.types.ProtocolMessage;
import io.ably.lib.util.AgentHeaderCreator;
import io.ably.lib.util.Log;
+import io.ably.lib.util.PlatformAgentProvider;
import java.io.IOException;
import java.util.ArrayList;
@@ -41,9 +42,11 @@ class TransportParams {
protected String connectionSerial;
protected Mode mode;
protected boolean heartbeats;
+ private final PlatformAgentProvider platformAgentProvider;
- public TransportParams(ClientOptions options) {
+ public TransportParams(ClientOptions options, PlatformAgentProvider platformAgentProvider) {
this.options = options;
+ this.platformAgentProvider = platformAgentProvider;
heartbeats = true; /* default to requiring Ably heartbeats */
}
@@ -89,7 +92,7 @@ public Param[] getConnectParams(Param[] baseParams) {
if(options.transportParams != null) {
paramList.addAll(Arrays.asList(options.transportParams));
}
- paramList.add(new Param(Defaults.ABLY_AGENT_PARAM, AgentHeaderCreator.create(options.agents)));
+ paramList.add(new Param(Defaults.ABLY_AGENT_PARAM, AgentHeaderCreator.create(options.agents, platformAgentProvider)));
Log.d(TAG, "getConnectParams: params = " + paramList);
return paramList.toArray(new Param[paramList.size()]);
}
diff --git a/lib/src/main/java/io/ably/lib/util/AgentHeaderCreator.java b/lib/src/main/java/io/ably/lib/util/AgentHeaderCreator.java
index fcc6ea6c3..be13caef9 100644
--- a/lib/src/main/java/io/ably/lib/util/AgentHeaderCreator.java
+++ b/lib/src/main/java/io/ably/lib/util/AgentHeaderCreator.java
@@ -13,20 +13,16 @@ public class AgentHeaderCreator {
/**
* Separates agent name from agent version.
*/
- private static final String AGENT_DIVIDER = "/";
+ public static final String AGENT_DIVIDER = "/";
- /**
- * Optional platform agent, e.g. "android/24"
- */
- private static String platformAgent = null;
-
- public static String create(Map additionalAgents) {
+ public static String create(Map additionalAgents, PlatformAgentProvider platformAgentProvider) {
StringBuilder agentStringBuilder = new StringBuilder();
agentStringBuilder.append(Defaults.ABLY_AGENT_VERSION);
if (additionalAgents != null && !additionalAgents.isEmpty()) {
agentStringBuilder.append(AGENT_ENTRY_SEPARATOR);
agentStringBuilder.append(getAdditionalAgentEntries(additionalAgents));
}
+ String platformAgent = platformAgentProvider.createPlatformAgent();
if (platformAgent != null) {
agentStringBuilder.append(AGENT_ENTRY_SEPARATOR);
agentStringBuilder.append(platformAgent);
@@ -47,15 +43,4 @@ private static String getAdditionalAgentEntries(Map additionalAg
}
return additionalAgentsBuilder.toString().trim();
}
-
- public static void setAndroidPlatformAgent(int platformVersion) {
- platformAgent = "android" + AGENT_DIVIDER + platformVersion;
- }
-
- /**
- * Added to clear AgentHeaderCreator state for unit tests.
- */
- public static void clearPlatformAgent() {
- platformAgent = null;
- }
}
diff --git a/lib/src/main/java/io/ably/lib/util/PlatformAgentProvider.java b/lib/src/main/java/io/ably/lib/util/PlatformAgentProvider.java
new file mode 100644
index 000000000..5a082040c
--- /dev/null
+++ b/lib/src/main/java/io/ably/lib/util/PlatformAgentProvider.java
@@ -0,0 +1,10 @@
+package io.ably.lib.util;
+
+public interface PlatformAgentProvider {
+ /**
+ * Creates the platform agent for agent header {@link AgentHeaderCreator}.
+ *
+ * @return Platform agent string or null if not available.
+ */
+ String createPlatformAgent();
+}
diff --git a/lib/src/test/java/io/ably/lib/test/realtime/ConnectionManagerTest.java b/lib/src/test/java/io/ably/lib/test/realtime/ConnectionManagerTest.java
index c29a28a33..e76c75d04 100644
--- a/lib/src/test/java/io/ably/lib/test/realtime/ConnectionManagerTest.java
+++ b/lib/src/test/java/io/ably/lib/test/realtime/ConnectionManagerTest.java
@@ -20,6 +20,7 @@
import java.util.concurrent.Executors;
import io.ably.lib.debug.DebugOptions;
+import io.ably.lib.test.util.EmptyPlatformAgentProvider;
import io.ably.lib.test.util.MockWebsocketFactory;
import io.ably.lib.transport.Hosts;
import io.ably.lib.util.Log;
@@ -134,7 +135,7 @@ public void connectionmanager_fallback_none_withoutconnection() throws AblyExcep
Connection connection = Mockito.mock(Connection.class);
final ConnectionManager.Channels channels = Mockito.mock(ConnectionManager.Channels.class);
- ConnectionManager connectionManager = new ConnectionManager(ably, connection, channels) {
+ ConnectionManager connectionManager = new ConnectionManager(ably, connection, channels, new EmptyPlatformAgentProvider()) {
@Override
protected boolean checkConnectivity() {
return false;
diff --git a/lib/src/test/java/io/ably/lib/test/rest/HttpTest.java b/lib/src/test/java/io/ably/lib/test/rest/HttpTest.java
index 81a7f44fb..b8c6dda8d 100644
--- a/lib/src/test/java/io/ably/lib/test/rest/HttpTest.java
+++ b/lib/src/test/java/io/ably/lib/test/rest/HttpTest.java
@@ -23,9 +23,11 @@
import java.util.List;
import io.ably.lib.http.*;
+import io.ably.lib.test.util.EmptyPlatformAgentProvider;
import io.ably.lib.test.util.TimeHandler;
import io.ably.lib.types.*;
import io.ably.lib.util.Log;
+import io.ably.lib.util.PlatformAgentProvider;
import org.hamcrest.Description;
import org.hamcrest.TypeSafeMatcher;
import org.junit.AfterClass;
@@ -57,6 +59,7 @@ public class HttpTest {
private static final String[] CUSTOM_HOSTS = { "f.ably-realtime.com", "g.ably-realtime.com", "h.ably-realtime.com", "i.ably-realtime.com", "j.ably-realtime.com", "k.ably-realtime.com" };
private static final String TEST_SERVER_HOST = "localhost";
private static final int TEST_SERVER_PORT = 27331;
+ private static final PlatformAgentProvider platformAgentProvider = new EmptyPlatformAgentProvider();
@Rule
public Timeout testTimeout = Timeout.seconds(60);
@@ -118,7 +121,7 @@ public void http_ably_execute_fallback() throws AblyException {
/*
* Extend the httpCore, so that we can capture provided url arguments without mocking and changing its organic behavior.
*/
- HttpCore httpCore = new HttpCore(options, null) {
+ HttpCore httpCore = new HttpCore(options, null, platformAgentProvider) {
/* Store only string representations to avoid try/catch blocks */
List urlArgumentStack;
@@ -183,7 +186,7 @@ public void http_ably_execute_null_fallbacks() throws AblyException {
ArrayList urlHostArgumentStack = new ArrayList<>();
- HttpCore httpCore = new HttpCore(options, null) {
+ HttpCore httpCore = new HttpCore(options, null, platformAgentProvider) {
List urlArgumentStack;
@Override
@@ -243,7 +246,7 @@ public void http_ably_execute_first_attempt_to_default() throws AblyException {
options.fallbackRetryTimeout = 100;
AblyRest ably = new AblyRest(options);
- HttpCore httpCore = Mockito.spy(new HttpCore(ably.options, ably.auth));
+ HttpCore httpCore = Mockito.spy(new HttpCore(ably.options, ably.auth, platformAgentProvider));
String responseExpected = "Lorem Ipsum";
ArgumentCaptor url = ArgumentCaptor.forClass(URL.class);
@@ -332,7 +335,7 @@ public void http_ably_execute_overriden_host() throws AblyException {
options.restHost = fakeHost;
AblyRest ably = new AblyRest(options);
- HttpCore httpCore = Mockito.spy(new HttpCore(ably.options, ably.auth));
+ HttpCore httpCore = Mockito.spy(new HttpCore(ably.options, ably.auth, platformAgentProvider));
String responseExpected = "Lorem Ipsum";
ArgumentCaptor url = ArgumentCaptor.forClass(URL.class);
@@ -428,7 +431,7 @@ public void http_ably_execute_empty_fallback_array() throws AblyException {
options.fallbackHosts = new String[0];
AblyRest ably = new AblyRest(options);
- HttpCore httpCore = Mockito.spy(new HttpCore(ably.options, ably.auth));
+ HttpCore httpCore = Mockito.spy(new HttpCore(ably.options, ably.auth, platformAgentProvider));
String responseExpected = "Lorem Ipsum";
ArgumentCaptor url = ArgumentCaptor.forClass(URL.class);
@@ -508,7 +511,7 @@ public void http_ably_execute_custom_fallback_array() throws AblyException {
int expectedCallCount = options.httpMaxRetryCount + 1;
AblyRest ably = new AblyRest(options);
- HttpCore httpCore = Mockito.spy(new HttpCore(ably.options, ably.auth));
+ HttpCore httpCore = Mockito.spy(new HttpCore(ably.options, ably.auth, platformAgentProvider));
String responseExpected = "Lorem Ipsum";
ArgumentCaptor url = ArgumentCaptor.forClass(URL.class);
@@ -581,7 +584,7 @@ public void http_ably_execute_custom_fallback() throws AblyException {
ArrayList urlHostArgumentStack = new ArrayList<>();
- HttpCore httpCore = new HttpCore(options, null) {
+ HttpCore httpCore = new HttpCore(options, null, platformAgentProvider) {
/* Store only string representations to avoid try/catch blocks */
List urlArgumentStack;
@@ -643,7 +646,7 @@ public HttpCore setUrlArgumentStack(List urlArgumentStack) {
*/
@Test
public void http_execute_nofallback() throws Exception {
- HttpCore httpCore = Mockito.spy(new HttpCore(new ClientOptions(), null));
+ HttpCore httpCore = Mockito.spy(new HttpCore(new ClientOptions(), null, platformAgentProvider));
String responseExpected = "Lorem Ipsum";
String hostExpected = Defaults.HOST_REST;
@@ -706,7 +709,7 @@ public void http_execute_nofallback() throws Exception {
*/
@Test
public void http_execute_singlefallback() throws Exception {
- HttpCore httpCore = Mockito.spy(new HttpCore(new ClientOptions(), null));
+ HttpCore httpCore = Mockito.spy(new HttpCore(new ClientOptions(), null, platformAgentProvider));
String hostExpectedPattern = PATTERN_HOST_FALLBACK;
String responseExpected = "Lorem Ipsum";
@@ -777,7 +780,7 @@ public void http_execute_singlefallback() throws Exception {
*/
@Test
public void http_execute_multiplefallback() throws Exception {
- HttpCore httpCore = Mockito.spy(new HttpCore(new ClientOptions(), null));
+ HttpCore httpCore = Mockito.spy(new HttpCore(new ClientOptions(), null, platformAgentProvider));
String hostExpectedPattern = PATTERN_HOST_FALLBACK;
String responseExpected = "Lorem Ipsum";
@@ -857,7 +860,7 @@ public void http_execute_multiplefallback() throws Exception {
public void http_execute_fallback_success_timeout_unexpired() throws Exception {
ClientOptions opts = new ClientOptions();
opts.fallbackRetryTimeout = 2000L;
- HttpCore httpCore = Mockito.spy(new HttpCore(opts, null));
+ HttpCore httpCore = Mockito.spy(new HttpCore(opts, null, platformAgentProvider));
String hostExpected = Defaults.HOST_REST;
ArgumentCaptor url = ArgumentCaptor.forClass(URL.class);
@@ -944,7 +947,7 @@ public void http_execute_fallback_success_timeout_unexpired() throws Exception {
public void http_execute_fallback_failure_timeout_unexpired() throws Exception {
ClientOptions opts = new ClientOptions();
opts.fallbackRetryTimeout = 2000L;
- HttpCore httpCore = Mockito.spy(new HttpCore(opts, null));
+ HttpCore httpCore = Mockito.spy(new HttpCore(opts, null, platformAgentProvider));
String primaryHost = Defaults.HOST_REST;
ArgumentCaptor url = ArgumentCaptor.forClass(URL.class);
@@ -1035,7 +1038,7 @@ public void http_execute_fallback_failure_timeout_unexpired() throws Exception {
public void http_execute_fallback_timeout_expired() throws Exception {
ClientOptions opts = new ClientOptions();
opts.fallbackRetryTimeout = 2000L;
- HttpCore httpCore = Mockito.spy(new HttpCore(opts, null));
+ HttpCore httpCore = Mockito.spy(new HttpCore(opts, null, platformAgentProvider));
String hostExpected = Defaults.HOST_REST;
ArgumentCaptor url = ArgumentCaptor.forClass(URL.class);
@@ -1119,7 +1122,7 @@ public void http_execute_fallback_timeout_expired() throws Exception {
@Test
public void http_execute_excessivefallback() throws AblyException {
ClientOptions options = new ClientOptions();
- HttpCore httpCore = Mockito.spy(new HttpCore(options, null));
+ HttpCore httpCore = Mockito.spy(new HttpCore(options, null, platformAgentProvider));
ArgumentCaptor url = ArgumentCaptor.forClass(URL.class);
int excessiveFallbackCount = options.httpMaxRetryCount + 1;
@@ -1175,7 +1178,7 @@ public void http_execute_excessivefallback() throws AblyException {
@Test
public void http_execute_response_50x() throws AblyException, MalformedURLException {
URL url;
- HttpCore httpCore = new HttpCore(new ClientOptions(), null);
+ HttpCore httpCore = new HttpCore(new ClientOptions(), null, platformAgentProvider);
AblyException.HostFailedException hfe;
@@ -1210,7 +1213,7 @@ public void http_execute_response_50x() throws AblyException, MalformedURLExcept
@Test
public void http_execute_response_non5xx() throws AblyException, MalformedURLException {
URL url;
- HttpCore httpCore = new HttpCore(new ClientOptions(), null);
+ HttpCore httpCore = new HttpCore(new ClientOptions(), null, platformAgentProvider);
/* Informational 1xx */
diff --git a/lib/src/test/java/io/ably/lib/test/util/EmptyPlatformAgentProvider.java b/lib/src/test/java/io/ably/lib/test/util/EmptyPlatformAgentProvider.java
new file mode 100644
index 000000000..fbc3ea59b
--- /dev/null
+++ b/lib/src/test/java/io/ably/lib/test/util/EmptyPlatformAgentProvider.java
@@ -0,0 +1,10 @@
+package io.ably.lib.test.util;
+
+import io.ably.lib.util.PlatformAgentProvider;
+
+public class EmptyPlatformAgentProvider implements PlatformAgentProvider {
+ @Override
+ public String createPlatformAgent() {
+ return null;
+ }
+}
diff --git a/lib/src/test/java/io/ably/lib/test/util/MockWebsocketFactory.java b/lib/src/test/java/io/ably/lib/test/util/MockWebsocketFactory.java
index 0f59a2196..3d0b0c440 100644
--- a/lib/src/test/java/io/ably/lib/test/util/MockWebsocketFactory.java
+++ b/lib/src/test/java/io/ably/lib/test/util/MockWebsocketFactory.java
@@ -46,7 +46,7 @@ public static class TransformParams extends ITransport.TransportParams {
private HostTransform hostTransform;
TransformParams(ITransport.TransportParams src, HostTransform hostTransform) {
- super(src.getClientOptions());
+ super(src.getClientOptions(), new EmptyPlatformAgentProvider());
this.hostTransform = hostTransform;
this.host = hostTransform.transformHost(src.getHost());
this.port = src.getPort();
diff --git a/lib/src/test/java/io/ably/lib/util/AgentHeaderCreatorTest.java b/lib/src/test/java/io/ably/lib/util/AgentHeaderCreatorTest.java
index 9fc07dd4c..a25a86902 100644
--- a/lib/src/test/java/io/ably/lib/util/AgentHeaderCreatorTest.java
+++ b/lib/src/test/java/io/ably/lib/util/AgentHeaderCreatorTest.java
@@ -1,5 +1,6 @@
package io.ably.lib.util;
+import io.ably.lib.test.util.EmptyPlatformAgentProvider;
import io.ably.lib.transport.Defaults;
import org.junit.Before;
import org.junit.Test;
@@ -13,11 +14,7 @@
public class AgentHeaderCreatorTest {
private final static String PREDEFINED_AGENTS = Defaults.ABLY_AGENT_VERSION;
-
- @Before
- public void beforeEach() {
- AgentHeaderCreator.clearPlatformAgent();
- }
+ private final PlatformAgentProvider emptyPlatformAgentProvider = new EmptyPlatformAgentProvider();
@Test
public void should_create_default_header_if_there_are_no_additional_agents() {
@@ -25,7 +22,7 @@ public void should_create_default_header_if_there_are_no_additional_agents() {
Map agents = new HashMap<>();
// when
- String agentHeaderValue = AgentHeaderCreator.create(agents);
+ String agentHeaderValue = AgentHeaderCreator.create(agents, emptyPlatformAgentProvider);
// then
assertMatchingAgentHeaders(PREDEFINED_AGENTS, agentHeaderValue);
@@ -36,7 +33,7 @@ public void should_create_default_header_if_additional_agents_are_null() {
// given
// when
- String agentHeaderValue = AgentHeaderCreator.create(null);
+ String agentHeaderValue = AgentHeaderCreator.create(null, emptyPlatformAgentProvider);
// then
assertMatchingAgentHeaders(PREDEFINED_AGENTS, agentHeaderValue);
@@ -50,7 +47,7 @@ public void should_create_header_with_appended_agents_if_they_are_provided() {
agents.put("other", "0.8.2");
// when
- String agentHeaderValue = AgentHeaderCreator.create(agents);
+ String agentHeaderValue = AgentHeaderCreator.create(agents, emptyPlatformAgentProvider);
// then
assertMatchingAgentHeaders(PREDEFINED_AGENTS + " library/1.0.1 other/0.8.2", agentHeaderValue);
@@ -64,7 +61,7 @@ public void should_create_header_with_appended_agents_without_versions() {
agents.put("no-version", null);
// when
- String agentHeaderValue = AgentHeaderCreator.create(agents);
+ String agentHeaderValue = AgentHeaderCreator.create(agents, emptyPlatformAgentProvider);
// then
assertMatchingAgentHeaders(PREDEFINED_AGENTS + " library/1.0.1 no-version", agentHeaderValue);
@@ -75,10 +72,15 @@ public void should_create_header_with_platform_agent_if_it_is_provided() {
// given
Map agents = new HashMap<>();
agents.put("library", "1.0.1");
- AgentHeaderCreator.setAndroidPlatformAgent(25);
+ PlatformAgentProvider androidPlatformAgentProvider = new PlatformAgentProvider() {
+ @Override
+ public String createPlatformAgent() {
+ return "android/25";
+ }
+ };
// when
- String agentHeaderValue = AgentHeaderCreator.create(agents);
+ String agentHeaderValue = AgentHeaderCreator.create(agents, androidPlatformAgentProvider);
// then
assertMatchingAgentHeaders(PREDEFINED_AGENTS + " android/25 library/1.0.1", agentHeaderValue);
From 0840afc6c6eb1798f475401aa2aff2d5fb72c663 Mon Sep 17 00:00:00 2001
From: Martin Morek
Date: Tue, 1 Jun 2021 10:33:25 +0200
Subject: [PATCH 068/899] Correctly set asserts in test
---
.../lib/test/realtime/RealtimeAuthTest.java | 17 +++++------------
1 file changed, 5 insertions(+), 12 deletions(-)
diff --git a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeAuthTest.java b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeAuthTest.java
index 88e8c4390..430a715d5 100644
--- a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeAuthTest.java
+++ b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeAuthTest.java
@@ -1,11 +1,7 @@
package io.ably.lib.test.realtime;
-import io.ably.lib.realtime.*;
-import io.ably.lib.test.common.Setup;
-import io.ably.lib.types.*;
-import io.ably.lib.util.Log;
-
import io.ably.lib.debug.DebugOptions;
+import io.ably.lib.realtime.*;
import io.ably.lib.rest.AblyRest;
import io.ably.lib.rest.Auth;
import io.ably.lib.rest.Auth.TokenDetails;
@@ -14,11 +10,8 @@
import io.ably.lib.test.common.Helpers.CompletionSet;
import io.ably.lib.test.common.Helpers.ConnectionWaiter;
import io.ably.lib.test.common.ParameterizedTest;
-import io.ably.lib.types.AblyException;
-import io.ably.lib.types.ClientOptions;
-import io.ably.lib.types.ErrorInfo;
-import io.ably.lib.types.Message;
-import io.ably.lib.types.ProtocolMessage;
+import io.ably.lib.test.common.Setup;
+import io.ably.lib.types.*;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
@@ -110,8 +103,8 @@ public void auth_client_fails_authorize_server_forbidden() {
ablyRealtime.connection.once(ConnectionEvent.failed, new ConnectionStateListener() {
@Override
public void onConnectionStateChanged(ConnectionStateChange stateChange) {
- assertEquals(stateChange.previous, ConnectionState.connected);
- assertEquals(stateChange.reason.code, 80019);
+ assertEquals(ConnectionState.connected, stateChange.previous);
+ assertEquals(80019, stateChange.reason.code);
assertEquals(80019, ablyRealtime.connection.reason.code);
assertEquals(403, ablyRealtime.connection.reason.statusCode);
}
From 37ab3f84c43b30a01c4cb0c30cc3190120ab551e Mon Sep 17 00:00:00 2001
From: Martin Morek
Date: Tue, 1 Jun 2021 15:01:43 +0200
Subject: [PATCH 069/899] Extended test documentation
---
.../ably/lib/test/realtime/RealtimeAuthTest.java | 14 ++++++++++++--
1 file changed, 12 insertions(+), 2 deletions(-)
diff --git a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeAuthTest.java b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeAuthTest.java
index 430a715d5..89dcb0f6f 100644
--- a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeAuthTest.java
+++ b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeAuthTest.java
@@ -69,7 +69,7 @@ public void auth_client_match_tokendetails_null_clientId() {
}
/**
- * RSA4d: If a request by a realtime client to an authUrl results in an HTTP 403 response,
+ * RSA4d: If a request by a realtime client to an authUrl results in an HTTP 403 response,
* or any of an authUrl request, an authCallback, or a request to Ably to exchange
* a TokenRequest for a TokenDetails result in an ErrorInfo with statusCode 403,
* as part of an attempt by the realtime client to authenticate, then the client library
@@ -77,16 +77,21 @@ public void auth_client_match_tokendetails_null_clientId() {
* and cause set to the underlying cause) emitted with the state change and set as the connection
* errorReason
*
- * Verify end connection state is failed
+ * Verify that if server responses with 403 error code on authorization attempt,
+ * end connection state is failed.
+ *
* Spec: RSA4d, RSA4d1
*/
@Test
public void auth_client_fails_authorize_server_forbidden() {
try {
+ /* init ably for token */
ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr);
AblyRest ablyForToken = new AblyRest(optsForToken);
+ /* get token */
TokenDetails tokenDetails = ablyForToken.auth.requestToken(null, null);
+ /* create ably realtime with tokenDetails and auth url which returns 403 error code */
ClientOptions opts = createOptions(testVars.keys[0].keyStr);
opts.autoConnect = false;
opts.tokenDetails = tokenDetails;
@@ -97,12 +102,15 @@ public void auth_client_fails_authorize_server_forbidden() {
final AblyRealtime ablyRealtime = new AblyRealtime(opts);
ablyRealtime.connection.connect();
+ /* wait for connected state */
Helpers.ConnectionWaiter connectionWaiter = new Helpers.ConnectionWaiter(ablyRealtime.connection);
connectionWaiter.waitFor(ConnectionState.connected);
+ /* create listener for ConnectionEvent.failed */
ablyRealtime.connection.once(ConnectionEvent.failed, new ConnectionStateListener() {
@Override
public void onConnectionStateChanged(ConnectionStateChange stateChange) {
+ /* assert that state changes correctly */
assertEquals(ConnectionState.connected, stateChange.previous);
assertEquals(80019, stateChange.reason.code);
assertEquals(80019, ablyRealtime.connection.reason.code);
@@ -112,8 +120,10 @@ public void onConnectionStateChanged(ConnectionStateChange stateChange) {
try {
opts.tokenDetails = null;
+ /* try to authorize */
ablyRealtime.auth.authorize(null, opts);
} catch (AblyException e) {
+ /* check expected error codes */
assertEquals(403, e.errorInfo.statusCode);
assertEquals(80019, e.errorInfo.code);
}
From d9b43429d9358b653e4bcbd694fcd6bac6b5d594 Mon Sep 17 00:00:00 2001
From: Martin Morek
Date: Tue, 15 Jun 2021 12:16:36 +0200
Subject: [PATCH 070/899] Added 'request_id' query param when
ClientOptions.addRequestIds is enabled
---
.../ably/lib/push/ActivationStateMachine.java | 13 +++--
.../java/io/ably/lib/push/PushChannel.java | 4 +-
.../main/java/io/ably/lib/push/PushBase.java | 20 +++++++-
.../main/java/io/ably/lib/rest/AblyBase.java | 8 +--
.../java/io/ably/lib/rest/ChannelBase.java | 14 ++++--
.../java/io/ably/lib/types/ClientOptions.java | 6 +++
.../main/java/io/ably/lib/types/Param.java | 4 ++
.../main/java/io/ably/lib/util/Crypto.java | 13 ++++-
.../io/ably/lib/test/rest/RestClientTest.java | 50 +++++++++++++++++++
.../java/io/ably/lib/util/CryptoTest.java | 2 +-
10 files changed, 117 insertions(+), 17 deletions(-)
create mode 100644 lib/src/test/java/io/ably/lib/test/rest/RestClientTest.java
diff --git a/android/src/main/java/io/ably/lib/push/ActivationStateMachine.java b/android/src/main/java/io/ably/lib/push/ActivationStateMachine.java
index d478b4549..712ccf6ee 100644
--- a/android/src/main/java/io/ably/lib/push/ActivationStateMachine.java
+++ b/android/src/main/java/io/ably/lib/push/ActivationStateMachine.java
@@ -7,9 +7,6 @@
import android.content.SharedPreferences;
import android.support.v4.content.LocalBroadcastManager;
-import com.google.android.gms.tasks.OnCompleteListener;
-import com.google.android.gms.tasks.Task;
-import com.google.firebase.iid.InstanceIdResult;
import com.google.gson.JsonObject;
import java.lang.reflect.Constructor;
@@ -21,6 +18,7 @@
import io.ably.lib.rest.AblyRest;
import io.ably.lib.rest.DeviceDetails;
import io.ably.lib.types.*;
+import io.ably.lib.util.Crypto;
import io.ably.lib.util.IntentUtils;
import io.ably.lib.util.Log;
import io.ably.lib.util.Serialisation;
@@ -432,6 +430,9 @@ public void execute(HttpScheduler http, Callback callback) throws AblyExce
if (ably.options.pushFullWait) {
params = Param.push(params, "fullWait", "true");
}
+ if(ably.options.addRequestIds) { // RSC7c
+ Param.set(params, Crypto.generateRandomRequestId());
+ }
http.patch("/push/deviceRegistrations/" + device.id, ably.push.pushRequestHeaders(true), params, body, null, false, callback);
}
@@ -480,6 +481,9 @@ public void execute(HttpScheduler http, Callback callback) throws Ab
if (ably.options.pushFullWait) {
params = Param.push(params, "fullWait", "true");
}
+ if(ably.options.addRequestIds) { // RSC7c
+ Param.set(params, Crypto.generateRandomRequestId());
+ }
final HttpCore.RequestBody body = HttpUtils.requestBodyFromGson(device.toJsonObject(), ably.options.useBinaryProtocol);
http.put("/push/deviceRegistrations/" + device.id, ably.push.pushRequestHeaders(true), params, body, new Serialisation.HttpResponseHandler(), true, callback);
@@ -528,6 +532,9 @@ public void execute(HttpScheduler http, Callback callback) throws AblyExce
if (ably.options.pushFullWait) {
params = Param.push(params, "fullWait", "true");
}
+ if(ably.options.addRequestIds) { // RSC7c
+ Param.set(params, Crypto.generateRandomRequestId());
+ }
http.del("/push/deviceRegistrations/" + device.id, ably.push.pushRequestHeaders(true), params, null, true, callback);
}
}).async(new Callback() {
diff --git a/android/src/main/java/io/ably/lib/push/PushChannel.java b/android/src/main/java/io/ably/lib/push/PushChannel.java
index 5765a7a83..e19767021 100644
--- a/android/src/main/java/io/ably/lib/push/PushChannel.java
+++ b/android/src/main/java/io/ably/lib/push/PushChannel.java
@@ -1,6 +1,5 @@
package io.ably.lib.push;
-import android.content.Context;
import com.google.gson.JsonObject;
import io.ably.lib.http.*;
import io.ably.lib.realtime.CompletionListener;
@@ -8,6 +7,7 @@
import io.ably.lib.rest.Channel;
import io.ably.lib.rest.DeviceDetails;
import io.ably.lib.types.*;
+import io.ably.lib.util.Crypto;
public class PushChannel {
protected final Channel channel;
@@ -112,7 +112,7 @@ protected Http.Request delSubscription(Param[] params) {
if (rest.options.pushFullWait) {
params = Param.push(params, "fullWait", "true");
}
- final Param[] finalParams = params;
+ final Param[] finalParams = rest.options.addRequestIds ? Param.set(params, Crypto.generateRandomRequestId()) : params; // RSC7c
return rest.http.request(new Http.Execute() {
@Override
public void execute(HttpScheduler http, Callback callback) throws AblyException {
diff --git a/lib/src/main/java/io/ably/lib/push/PushBase.java b/lib/src/main/java/io/ably/lib/push/PushBase.java
index c735df2c5..221345e66 100644
--- a/lib/src/main/java/io/ably/lib/push/PushBase.java
+++ b/lib/src/main/java/io/ably/lib/push/PushBase.java
@@ -15,6 +15,7 @@
import io.ably.lib.types.Callback;
import io.ably.lib.types.PaginatedResult;
import io.ably.lib.types.Param;
+import io.ably.lib.util.Crypto;
import io.ably.lib.util.Log;
import io.ably.lib.util.Serialisation;
import io.ably.lib.util.StringUtils;
@@ -76,6 +77,9 @@ public void execute(HttpScheduler http, Callback callback) throws AblyExce
if (rest.options.pushFullWait) {
params = Param.push(params, "fullWait", "true");
}
+ if (rest.options.addRequestIds) {
+ params = Param.set(params, Crypto.generateRandomRequestId()); // RSC7c
+ }
http.post("/push/publish", HttpUtils.defaultAcceptHeaders(rest.options.useBinaryProtocol), params, body, null, true, callback);
}
@@ -106,6 +110,9 @@ public void execute(HttpScheduler http, Callback callback) throws
if (rest.options.pushFullWait) {
params = Param.push(params, "fullWait", "true");
}
+ if(rest.options.addRequestIds) { // RSC7c
+ Param.set(params, Crypto.generateRandomRequestId());
+ }
http.put("/push/deviceRegistrations/" + device.id, rest.push.pushRequestHeaders(device.id), params, body, DeviceDetails.httpResponseHandler, true, callback);
}
});
@@ -128,6 +135,9 @@ public void execute(HttpScheduler http, Callback callback) throws
if (rest.options.pushFullWait) {
params = Param.push(params, "fullWait", "true");
}
+ if (rest.options.addRequestIds) { // RSC7c
+ Param.set(params, Crypto.generateRandomRequestId());
+ }
http.get("/push/deviceRegistrations/" + deviceId, rest.push.pushRequestHeaders(deviceId), params, DeviceDetails.httpResponseHandler, true, callback);
}
});
@@ -171,6 +181,9 @@ public void execute(HttpScheduler http, Callback callback) throws AblyExce
if (rest.options.pushFullWait) {
params = Param.push(params, "fullWait", "true");
}
+ if(rest.options.addRequestIds) { // RSC7c
+ Param.set(params, Crypto.generateRandomRequestId());
+ }
http.del("/push/deviceRegistrations/" + deviceId, rest.push.pushRequestHeaders(deviceId), params, null, true, callback);
}
});
@@ -189,7 +202,7 @@ protected Http.Request removeWhereImpl(Param[] params) {
if (rest.options.pushFullWait) {
params = Param.push(params, "fullWait", "true");
}
- final Param[] finalParams = params;
+ final Param[] finalParams = rest.options.addRequestIds ? Param.set(params, Crypto.generateRandomRequestId()) : params; // RSC7c
return rest.http.request(new Http.Execute() {
@Override
public void execute(HttpScheduler http, Callback callback) throws AblyException {
@@ -226,6 +239,9 @@ public void execute(HttpScheduler http, Callback callback)
if (rest.options.pushFullWait) {
params = Param.push(params, "fullWait", "true");
}
+ if (rest.options.addRequestIds) {
+ params = Param.set(params, Crypto.generateRandomRequestId()); // RSC7c
+ }
http.post("/push/channelSubscriptions", rest.push.pushRequestHeaders(subscription.deviceId), params, body, ChannelSubscription.httpResponseHandler, true, callback);
}
});
@@ -283,7 +299,7 @@ protected Http.Request removeWhereImpl(Param[] params) {
params = Param.push(params, "fullWait", "true");
}
final Param[] finalHeaders = rest.push.pushRequestHeaders(deviceId);
- final Param[] finalParams = params;
+ final Param[] finalParams = rest.options.addRequestIds ? Param.set(params, Crypto.generateRandomRequestId()) : params; // RSC7c
return rest.http.request(new Http.Execute() {
@Override
public void execute(HttpScheduler http, Callback callback) throws AblyException {
diff --git a/lib/src/main/java/io/ably/lib/rest/AblyBase.java b/lib/src/main/java/io/ably/lib/rest/AblyBase.java
index 72c4d0392..f9f3a9456 100644
--- a/lib/src/main/java/io/ably/lib/rest/AblyBase.java
+++ b/lib/src/main/java/io/ably/lib/rest/AblyBase.java
@@ -154,10 +154,11 @@ public void timeAsync(Callback callback) {
}
private Http.Request timeImpl() {
+ final Param[] params = this.options.addRequestIds ? Param.array(Crypto.generateRandomRequestId()) : null; // RSC7c
return http.request(new Http.Execute() {
@Override
public void execute(HttpScheduler http, Callback callback) throws AblyException {
- http.get("/time", HttpUtils.defaultAcceptHeaders(false), null, new HttpCore.ResponseHandler() {
+ http.get("/time", HttpUtils.defaultAcceptHeaders(false), params, new HttpCore.ResponseHandler() {
@Override
public Long handleResponse(HttpCore.Response response, ErrorInfo error) throws AblyException {
if(error != null) {
@@ -251,7 +252,7 @@ public void publishBatchAsync(Message.Batch[] pubSpecs, ChannelOptions channelOp
publishBatchImpl(pubSpecs, channelOptions, params).async(callback);
}
- private Http.Request publishBatchImpl(final Message.Batch[] pubSpecs, ChannelOptions channelOptions, final Param[] params) throws AblyException {
+ private Http.Request publishBatchImpl(final Message.Batch[] pubSpecs, ChannelOptions channelOptions, final Param[] initialParams) throws AblyException {
boolean hasClientSuppliedId = false;
for(Message.Batch spec : pubSpecs) {
for(Message message : spec.messages) {
@@ -264,7 +265,7 @@ private Http.Request publishBatchImpl(final Message.Batch[] p
}
if(!hasClientSuppliedId && options.idempotentRestPublishing) {
/* RSL1k1: populate the message id with a library-generated id */
- String messageId = Crypto.getRandomMessageId();
+ String messageId = Crypto.getRandomId();
for (int i = 0; i < spec.messages.length; i++) {
spec.messages[i].id = messageId + ':' + i;
}
@@ -274,6 +275,7 @@ private Http.Request publishBatchImpl(final Message.Batch[] p
@Override
public void execute(HttpScheduler http, final Callback callback) throws AblyException {
HttpCore.RequestBody requestBody = options.useBinaryProtocol ? MessageSerializer.asMsgpackRequest(pubSpecs) : MessageSerializer.asJSONRequest(pubSpecs);
+ final Param[] params = options.addRequestIds ? Param.set(initialParams, Crypto.generateRandomRequestId()) : initialParams ; // RSC7c
http.post("/messages", HttpUtils.defaultAcceptHeaders(options.useBinaryProtocol), params, requestBody, new HttpCore.ResponseHandler() {
@Override
public PublishResponse[] handleResponse(HttpCore.Response response, ErrorInfo error) throws AblyException {
diff --git a/lib/src/main/java/io/ably/lib/rest/ChannelBase.java b/lib/src/main/java/io/ably/lib/rest/ChannelBase.java
index 9de0289f9..859f4415b 100644
--- a/lib/src/main/java/io/ably/lib/rest/ChannelBase.java
+++ b/lib/src/main/java/io/ably/lib/rest/ChannelBase.java
@@ -103,15 +103,16 @@ public void execute(HttpScheduler http, final Callback callback) throws Ab
}
if(!hasClientSuppliedId && ably.options.idempotentRestPublishing) {
/* RSL1k1: populate the message id with a library-generated id */
- String messageId = Crypto.getRandomMessageId();
+ String messageId = Crypto.getRandomId();
for (int i = 0; i < messages.length; i++) {
messages[i].id = messageId + ':' + i;
}
}
HttpCore.RequestBody requestBody = ably.options.useBinaryProtocol ? MessageSerializer.asMsgpackRequest(messages) : MessageSerializer.asJsonRequest(messages);
+ final Param[] params = ably.options.addRequestIds ? Param.array(Crypto.generateRandomRequestId()) : null; // RSC7c
- http.post(basePath + "/messages", HttpUtils.defaultAcceptHeaders(ably.options.useBinaryProtocol), null, requestBody, null, true, callback);
+ http.post(basePath + "/messages", HttpUtils.defaultAcceptHeaders(ably.options.useBinaryProtocol), params, requestBody, null, true, callback);
}
});
}
@@ -139,8 +140,9 @@ public void historyAsync(Param[] params, Callback>
historyImpl(params).async(callback);
}
- private BasePaginatedQuery.ResultRequest historyImpl(Param[] params) {
+ private BasePaginatedQuery.ResultRequest historyImpl(Param[] initialParams) {
HttpCore.BodyHandler bodyHandler = MessageSerializer.getMessageResponseHandler(options);
+ final Param[] params = ably.options.addRequestIds ? Param.set(initialParams, Crypto.generateRandomRequestId()) : initialParams; // RSC7c
return (new BasePaginatedQuery(ably.http, basePath + "/messages", HttpUtils.defaultAcceptHeaders(ably.options.useBinaryProtocol), params, bodyHandler)).get();
}
@@ -169,8 +171,9 @@ public void getAsync(Param[] params, Callback getImpl(Param[] params) {
+ private BasePaginatedQuery.ResultRequest getImpl(Param[] initialParams) {
HttpCore.BodyHandler bodyHandler = PresenceSerializer.getPresenceResponseHandler(options);
+ final Param[] params = ably.options.addRequestIds ? Param.set(initialParams, Crypto.generateRandomRequestId()) : initialParams; // RSC7c
return (new BasePaginatedQuery(ably.http, basePath + "/presence", HttpUtils.defaultAcceptHeaders(ably.options.useBinaryProtocol), params, bodyHandler)).get();
}
@@ -195,8 +198,9 @@ public void historyAsync(Param[] params, Callback historyImpl(Param[] params) {
+ private BasePaginatedQuery.ResultRequest historyImpl(Param[] initialParams) {
HttpCore.BodyHandler bodyHandler = PresenceSerializer.getPresenceResponseHandler(options);
+ final Param[] params = ably.options.addRequestIds ? Param.set(initialParams, Crypto.generateRandomRequestId()) : initialParams; // RSC7c
return (new BasePaginatedQuery(ably.http, basePath + "/presence/history", HttpUtils.defaultAcceptHeaders(ably.options.useBinaryProtocol), params, bodyHandler)).get();
}
diff --git a/lib/src/main/java/io/ably/lib/types/ClientOptions.java b/lib/src/main/java/io/ably/lib/types/ClientOptions.java
index 8444964b3..d7a548465 100644
--- a/lib/src/main/java/io/ably/lib/types/ClientOptions.java
+++ b/lib/src/main/java/io/ably/lib/types/ClientOptions.java
@@ -195,4 +195,10 @@ public ClientOptions(String key) throws AblyException {
* before responding.
*/
public boolean pushFullWait = false;
+
+ /**
+ If enabled, every REST request to Ably includes a `request_id` query string parameter. This request ID
+ remain the same if a request is retried to a fallback host.
+ */
+ public boolean addRequestIds = false;
}
diff --git a/lib/src/main/java/io/ably/lib/types/Param.java b/lib/src/main/java/io/ably/lib/types/Param.java
index aca9ffc85..8a2e85365 100644
--- a/lib/src/main/java/io/ably/lib/types/Param.java
+++ b/lib/src/main/java/io/ably/lib/types/Param.java
@@ -10,6 +10,10 @@ public class Param {
public String key;
public String value;
+ public static Param[] array(final Param val) {
+ return new Param[] { val };
+ }
+
public static Param[] push(Param[] params, Param val) {
if (params == null) {
return new Param[] { val };
diff --git a/lib/src/main/java/io/ably/lib/util/Crypto.java b/lib/src/main/java/io/ably/lib/util/Crypto.java
index 56ea62fb9..1680f0d29 100644
--- a/lib/src/main/java/io/ably/lib/util/Crypto.java
+++ b/lib/src/main/java/io/ably/lib/util/Crypto.java
@@ -16,6 +16,7 @@
import io.ably.lib.types.AblyException;
import io.ably.lib.types.ChannelOptions;
import io.ably.lib.types.ErrorInfo;
+import io.ably.lib.types.Param;
/**
* Utility classes and interfaces for message payload encryption.
@@ -318,12 +319,22 @@ private static int getPaddedLength(int plaintextLength) {
};
}
- public static String getRandomMessageId() {
+ public static String getRandomId() {
byte[] entropy = new byte[9];
secureRandom.nextBytes(entropy);
return Base64Coder.encodeToString(entropy);
}
+ /**
+ * Returns a "request_id" query param, based on a sequence of 9 random bytes
+ * which have been base64 encoded.
+ *
+ * Spec: RSC7c
+ */
+ public static Param generateRandomRequestId() {
+ return new Param("request_id", Crypto.getRandomId());
+ }
+
/**
* Determine whether or not 256-bit AES is supported. (If this determines that
* it is not supported, install the JCE unlimited strength JCE extensions).
diff --git a/lib/src/test/java/io/ably/lib/test/rest/RestClientTest.java b/lib/src/test/java/io/ably/lib/test/rest/RestClientTest.java
new file mode 100644
index 000000000..5fec3a3b3
--- /dev/null
+++ b/lib/src/test/java/io/ably/lib/test/rest/RestClientTest.java
@@ -0,0 +1,50 @@
+package io.ably.lib.test.rest;
+
+import io.ably.lib.debug.DebugOptions;
+import io.ably.lib.rest.AblyRest;
+import io.ably.lib.test.common.Helpers;
+import io.ably.lib.test.common.ParameterizedTest;
+import io.ably.lib.types.AblyException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.Timeout;
+
+import static org.junit.Assert.assertEquals;
+
+public class RestClientTest extends ParameterizedTest {
+
+ @Rule
+ public Timeout testTimeout = Timeout.seconds(30);
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ /**
+ * Include `request_id` if addRequestIds in client options is enabled
+ * Spec: RSC7c
+ */
+ @Test
+ public void request_contains_request_id() throws AblyException {
+ DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr);
+ fillInOptions(opts);
+ Helpers.RawHttpTracker httpListener = new Helpers.RawHttpTracker();
+
+ opts.httpListener = httpListener;
+ /* disable addRequestIds */
+ opts.addRequestIds = false;
+ AblyRest ablyA = new AblyRest(opts);
+
+ ablyA.channels.get("test").publish("foo", "bar");
+ /* verify client_id is not a part of url query */
+ assertEquals("Verify clientId is not present in query", null, httpListener.getFirstRequest().url.getQuery());
+
+ /* enable addRequestIds */
+ opts.addRequestIds = true;
+ AblyRest ablyB = new AblyRest(opts);
+
+ ablyB.channels.get("test").publish("foo", "bar");
+ /* verify client_id is a part of url query */
+ assertEquals("Verify clientId is present in query", true, httpListener.getLastRequest().url.getQuery().contains("request_id"));
+ }
+}
diff --git a/lib/src/test/java/io/ably/lib/util/CryptoTest.java b/lib/src/test/java/io/ably/lib/util/CryptoTest.java
index f89541be3..e4b0cb53e 100644
--- a/lib/src/test/java/io/ably/lib/util/CryptoTest.java
+++ b/lib/src/test/java/io/ably/lib/util/CryptoTest.java
@@ -202,7 +202,7 @@ private static byte[] msgPacked(final String name, final byte[] data, final Stri
@Test
public void getRandomId() {
- String randomId = Crypto.getRandomMessageId();
+ String randomId = Crypto.getRandomId();
assertEquals(12, randomId.length());
}
}
From b4ec7e15ae0ee7bcb17d8b38d7779e509d348e56 Mon Sep 17 00:00:00 2001
From: Martin Morek
Date: Fri, 18 Jun 2021 15:48:59 +0200
Subject: [PATCH 071/899] Added request_id to error info
---
.../java/io/ably/lib/http/HttpScheduler.java | 9 +++++
.../io/ably/lib/test/rest/RestClientTest.java | 33 +++++++++++++++++--
2 files changed, 39 insertions(+), 3 deletions(-)
diff --git a/lib/src/main/java/io/ably/lib/http/HttpScheduler.java b/lib/src/main/java/io/ably/lib/http/HttpScheduler.java
index 54046b995..c0cac1ba1 100644
--- a/lib/src/main/java/io/ably/lib/http/HttpScheduler.java
+++ b/lib/src/main/java/io/ably/lib/http/HttpScheduler.java
@@ -202,17 +202,26 @@ public void run() {
break;
} catch (AblyException.HostFailedException e) {
if(--retryCountRemaining < 0) {
+ if(Param.getFirst(params, "request_id") != null) {
+ e.errorInfo.message += String.format(", request_id %s", Param.getFirst(params, "request_id"));
+ }
setError(e.errorInfo);
break;
}
Log.d(TAG, "Connection failed to host `" + candidateHost + "`. Searching for new host...");
candidateHost = httpCore.hosts.getFallback(candidateHost);
if (candidateHost == null) {
+ if(Param.getFirst(params, "request_id") != null) {
+ e.errorInfo.message += String.format(", request_id %s", Param.getFirst(params, "request_id"));
+ }
setError(e.errorInfo);
break;
}
Log.d(TAG, "Switched to `" + candidateHost + "`.");
} catch(AblyException e) {
+ if(Param.getFirst(params, "request_id") != null) {
+ e.errorInfo.message += String.format(", request_id %s", Param.getFirst(params, "request_id"));
+ }
setError(e.errorInfo);
break;
} finally {
diff --git a/lib/src/test/java/io/ably/lib/test/rest/RestClientTest.java b/lib/src/test/java/io/ably/lib/test/rest/RestClientTest.java
index 5fec3a3b3..412837ca2 100644
--- a/lib/src/test/java/io/ably/lib/test/rest/RestClientTest.java
+++ b/lib/src/test/java/io/ably/lib/test/rest/RestClientTest.java
@@ -10,7 +10,8 @@
import org.junit.rules.ExpectedException;
import org.junit.rules.Timeout;
-import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
public class RestClientTest extends ParameterizedTest {
@@ -37,7 +38,7 @@ public void request_contains_request_id() throws AblyException {
ablyA.channels.get("test").publish("foo", "bar");
/* verify client_id is not a part of url query */
- assertEquals("Verify clientId is not present in query", null, httpListener.getFirstRequest().url.getQuery());
+ assertNull("Verify clientId is not present in query", httpListener.getFirstRequest().url.getQuery());
/* enable addRequestIds */
opts.addRequestIds = true;
@@ -45,6 +46,32 @@ public void request_contains_request_id() throws AblyException {
ablyB.channels.get("test").publish("foo", "bar");
/* verify client_id is a part of url query */
- assertEquals("Verify clientId is present in query", true, httpListener.getLastRequest().url.getQuery().contains("request_id"));
+ assertTrue("Verify clientId is present in query", httpListener.getLastRequest().url.getQuery().contains("request_id"));
+ }
+
+ /**
+ * Include `request_id` in ErrorInfo if addRequestIds in client options is enabled
+ * Spec: RSC7c
+ */
+ @Test
+ public void error_info_contains_request_id() throws AblyException {
+ DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr);
+ fillInOptions(opts);
+
+ Helpers.RawHttpTracker httpListener = new Helpers.RawHttpTracker();
+ opts.httpListener = httpListener;
+ opts.addRequestIds = true;
+ opts.environment = null;
+ opts.restHost = "";
+ AblyRest ably = new AblyRest(opts);
+
+ try{
+ ably.channels.get("test").publish("foo", "bar");
+ } catch (AblyException e) {
+ assertTrue(e.errorInfo.message.contains("request_id"));
+ }
+
+ /* verify client_id is a part of url query */
+ assertTrue("Verify clientId is present in query", httpListener.getFirstRequest().url.getQuery().contains("request_id"));
}
}
From c2dccd96971f2c39a8a0471634ebf1f4e679957a Mon Sep 17 00:00:00 2001
From: Martin Morek
Date: Wed, 23 Jun 2021 09:08:57 +0200
Subject: [PATCH 072/899] Added clientId query paramter where it was missing
---
.../java/io/ably/lib/push/ActivationStateMachine.java | 9 ++++++---
android/src/main/java/io/ably/lib/push/PushChannel.java | 3 +++
lib/src/main/java/io/ably/lib/push/PushBase.java | 6 +++---
3 files changed, 12 insertions(+), 6 deletions(-)
diff --git a/android/src/main/java/io/ably/lib/push/ActivationStateMachine.java b/android/src/main/java/io/ably/lib/push/ActivationStateMachine.java
index 712ccf6ee..07d386ee7 100644
--- a/android/src/main/java/io/ably/lib/push/ActivationStateMachine.java
+++ b/android/src/main/java/io/ably/lib/push/ActivationStateMachine.java
@@ -140,6 +140,9 @@ public void execute(HttpScheduler http, Callback callback) throws Ab
if(ably.options.pushFullWait) {
params = Param.push(null, "fullWait", "true");
}
+ if(ably.options.addRequestIds) { // RSC7c
+ params = Param.set(params, Crypto.generateRandomRequestId());
+ }
/* this is authenticated using the Ably library credentials, plus the deviceSecret in the request body */
http.post("/push/deviceRegistrations", HttpUtils.defaultAcceptHeaders(ably.options.useBinaryProtocol), params, body, new Serialisation.HttpResponseHandler(), true, callback);
}
@@ -431,7 +434,7 @@ public void execute(HttpScheduler http, Callback callback) throws AblyExce
params = Param.push(params, "fullWait", "true");
}
if(ably.options.addRequestIds) { // RSC7c
- Param.set(params, Crypto.generateRandomRequestId());
+ params = Param.set(params, Crypto.generateRandomRequestId());
}
http.patch("/push/deviceRegistrations/" + device.id, ably.push.pushRequestHeaders(true), params, body, null, false, callback);
@@ -482,7 +485,7 @@ public void execute(HttpScheduler http, Callback callback) throws Ab
params = Param.push(params, "fullWait", "true");
}
if(ably.options.addRequestIds) { // RSC7c
- Param.set(params, Crypto.generateRandomRequestId());
+ params = Param.set(params, Crypto.generateRandomRequestId());
}
final HttpCore.RequestBody body = HttpUtils.requestBodyFromGson(device.toJsonObject(), ably.options.useBinaryProtocol);
@@ -533,7 +536,7 @@ public void execute(HttpScheduler http, Callback callback) throws AblyExce
params = Param.push(params, "fullWait", "true");
}
if(ably.options.addRequestIds) { // RSC7c
- Param.set(params, Crypto.generateRandomRequestId());
+ params = Param.set(params, Crypto.generateRandomRequestId());
}
http.del("/push/deviceRegistrations/" + device.id, ably.push.pushRequestHeaders(true), params, null, true, callback);
}
diff --git a/android/src/main/java/io/ably/lib/push/PushChannel.java b/android/src/main/java/io/ably/lib/push/PushChannel.java
index e19767021..c1b49d0e0 100644
--- a/android/src/main/java/io/ably/lib/push/PushChannel.java
+++ b/android/src/main/java/io/ably/lib/push/PushChannel.java
@@ -68,6 +68,9 @@ public void execute(HttpScheduler http, Callback callback) throws AblyExce
if (rest.options.pushFullWait) {
params = Param.push(params, "fullWait", "true");
}
+ if(rest.options.addRequestIds) { // RSC7c
+ params = Param.set(params, Crypto.generateRandomRequestId());
+ }
http.post("/push/channelSubscriptions", rest.push.pushRequestHeaders(true), params, body, null, true, callback);
}
});
diff --git a/lib/src/main/java/io/ably/lib/push/PushBase.java b/lib/src/main/java/io/ably/lib/push/PushBase.java
index 221345e66..4f798425e 100644
--- a/lib/src/main/java/io/ably/lib/push/PushBase.java
+++ b/lib/src/main/java/io/ably/lib/push/PushBase.java
@@ -111,7 +111,7 @@ public void execute(HttpScheduler http, Callback callback) throws
params = Param.push(params, "fullWait", "true");
}
if(rest.options.addRequestIds) { // RSC7c
- Param.set(params, Crypto.generateRandomRequestId());
+ params = Param.set(params, Crypto.generateRandomRequestId());
}
http.put("/push/deviceRegistrations/" + device.id, rest.push.pushRequestHeaders(device.id), params, body, DeviceDetails.httpResponseHandler, true, callback);
}
@@ -136,7 +136,7 @@ public void execute(HttpScheduler http, Callback callback) throws
params = Param.push(params, "fullWait", "true");
}
if (rest.options.addRequestIds) { // RSC7c
- Param.set(params, Crypto.generateRandomRequestId());
+ params = Param.set(params, Crypto.generateRandomRequestId());
}
http.get("/push/deviceRegistrations/" + deviceId, rest.push.pushRequestHeaders(deviceId), params, DeviceDetails.httpResponseHandler, true, callback);
}
@@ -182,7 +182,7 @@ public void execute(HttpScheduler http, Callback callback) throws AblyExce
params = Param.push(params, "fullWait", "true");
}
if(rest.options.addRequestIds) { // RSC7c
- Param.set(params, Crypto.generateRandomRequestId());
+ params = Param.set(params, Crypto.generateRandomRequestId());
}
http.del("/push/deviceRegistrations/" + deviceId, rest.push.pushRequestHeaders(deviceId), params, null, true, callback);
}
From bed7b21b1e484de3eb59a86dcfaa2356d2f2ad06 Mon Sep 17 00:00:00 2001
From: Martin Morek
Date: Thu, 24 Jun 2021 18:01:59 +0200
Subject: [PATCH 073/899] Added test to verify unchanged request_id on retried
fallbacks
---
.../java/io/ably/lib/http/HttpScheduler.java | 22 ++++++-------
.../io/ably/lib/test/rest/RestClientTest.java | 33 +++++++++++++++++++
2 files changed, 44 insertions(+), 11 deletions(-)
diff --git a/lib/src/main/java/io/ably/lib/http/HttpScheduler.java b/lib/src/main/java/io/ably/lib/http/HttpScheduler.java
index c0cac1ba1..9c07d7146 100644
--- a/lib/src/main/java/io/ably/lib/http/HttpScheduler.java
+++ b/lib/src/main/java/io/ably/lib/http/HttpScheduler.java
@@ -189,6 +189,12 @@ private AblyRequestWithFallback(
this.path = path;
this.requireAblyAuth = requireAblyAuth;
}
+
+ private String extendMessage(String msg) {
+ return Param.getFirst(params, "request_id") == null ?
+ msg : String.format("%s request_id=%s", msg, Param.getFirst(params, "request_id"));
+ }
+
@Override
public void run() {
String candidateHost = httpCore.hosts.getPreferredHost();
@@ -202,26 +208,20 @@ public void run() {
break;
} catch (AblyException.HostFailedException e) {
if(--retryCountRemaining < 0) {
- if(Param.getFirst(params, "request_id") != null) {
- e.errorInfo.message += String.format(", request_id %s", Param.getFirst(params, "request_id"));
- }
+ e.errorInfo.message = extendMessage(e.errorInfo.message);
setError(e.errorInfo);
break;
}
- Log.d(TAG, "Connection failed to host `" + candidateHost + "`. Searching for new host...");
+ Log.d(TAG, extendMessage("Connection failed to host `" + candidateHost + "`. Searching for new host..."));
candidateHost = httpCore.hosts.getFallback(candidateHost);
if (candidateHost == null) {
- if(Param.getFirst(params, "request_id") != null) {
- e.errorInfo.message += String.format(", request_id %s", Param.getFirst(params, "request_id"));
- }
+ e.errorInfo.message = extendMessage(e.errorInfo.message);
setError(e.errorInfo);
break;
}
- Log.d(TAG, "Switched to `" + candidateHost + "`.");
+ Log.d(TAG, extendMessage("Switched to `" + candidateHost + "`."));
} catch(AblyException e) {
- if(Param.getFirst(params, "request_id") != null) {
- e.errorInfo.message += String.format(", request_id %s", Param.getFirst(params, "request_id"));
- }
+ e.errorInfo.message = extendMessage(e.errorInfo.message);
setError(e.errorInfo);
break;
} finally {
diff --git a/lib/src/test/java/io/ably/lib/test/rest/RestClientTest.java b/lib/src/test/java/io/ably/lib/test/rest/RestClientTest.java
index 412837ca2..34c9624bc 100644
--- a/lib/src/test/java/io/ably/lib/test/rest/RestClientTest.java
+++ b/lib/src/test/java/io/ably/lib/test/rest/RestClientTest.java
@@ -10,6 +10,7 @@
import org.junit.rules.ExpectedException;
import org.junit.rules.Timeout;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
@@ -63,6 +64,7 @@ public void error_info_contains_request_id() throws AblyException {
opts.addRequestIds = true;
opts.environment = null;
opts.restHost = "";
+ opts.fallbackHosts = new String[]{"ably.com"};
AblyRest ably = new AblyRest(opts);
try{
@@ -74,4 +76,35 @@ public void error_info_contains_request_id() throws AblyException {
/* verify client_id is a part of url query */
assertTrue("Verify clientId is present in query", httpListener.getFirstRequest().url.getQuery().contains("request_id"));
}
+
+ /**
+ * `clientId` remain the same if a request is retried to a fallback host
+ * Spec: RSC7c
+ */
+ @Test
+ public void request_id_remain_same_retried_fallbacks() throws AblyException {
+ DebugOptions opts = new DebugOptions(testVars.keys[0].keyStr);
+ fillInOptions(opts);
+
+ Helpers.RawHttpTracker httpListener = new Helpers.RawHttpTracker();
+ opts.httpListener = httpListener;
+ opts.addRequestIds = true;
+ opts.environment = null;
+ opts.restHost = "invalid-host1.com";
+ opts.fallbackHosts = new String[]{"invalid-host2.com", "invalid-host3.com"};
+ AblyRest ably = new AblyRest(opts);
+
+ try{
+ ably.channels.get("test").publish("foo", "bar");
+ } catch (AblyException e) { }
+
+ /* verify client_id is a part of url query */
+ assertTrue("Verify clientId is present in query", httpListener.getFirstRequest().url.getQuery().contains("request_id"));
+ String query = httpListener.getFirstRequest().url.getQuery();
+ /* verify request was retried 3 times */
+ assertEquals(3, httpListener.values().size());
+ for (Helpers.RawHttpRequest rawHttpRequest : httpListener.values()) {
+ assertTrue("Verify clientId remain the same if a request is retried to a fallback host", rawHttpRequest.url.getQuery().contains(query));
+ }
+ }
}
From 343095ea30e0f21624a41b2d4615a19016e75ed5 Mon Sep 17 00:00:00 2001
From: Kacper Kluka
Date: Wed, 30 Jun 2021 12:47:24 +0200
Subject: [PATCH 074/899] Update ClientOptions.agents javadoc
---
lib/src/main/java/io/ably/lib/types/ClientOptions.java | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/lib/src/main/java/io/ably/lib/types/ClientOptions.java b/lib/src/main/java/io/ably/lib/types/ClientOptions.java
index dc596cb0b..3b6e64ec2 100644
--- a/lib/src/main/java/io/ably/lib/types/ClientOptions.java
+++ b/lib/src/main/java/io/ably/lib/types/ClientOptions.java
@@ -198,6 +198,11 @@ public ClientOptions(String key) throws AblyException {
/**
* Map of agents that will be appended to the agent header.
+ *
+ * This should only be used by Ably-authored SDKs.
+ * If you need to use this then you have to add the agent to the agents.json file:
+ * https://github.com/ably/ably-common/blob/main/protocol/agents.json
+ *
* The keys represent agent names and its corresponding values represent agent versions.
* Agent versions are optional, if you don't want to specify it pass `null` as the map entry value.
*/
From 111628b7196f0409254b951e38ffd0b22c4a17eb Mon Sep 17 00:00:00 2001
From: Kacper Kluka
Date: Wed, 30 Jun 2021 12:53:01 +0200
Subject: [PATCH 075/899] Remove AblyAgentValidator class
---
.../main/java/io/ably/lib/rest/AblyBase.java | 6 -
.../io/ably/lib/util/AblyAgentValidator.java | 45 --------
.../ably/lib/util/AblyAgentValidatorTest.java | 106 ------------------
3 files changed, 157 deletions(-)
delete mode 100644 lib/src/main/java/io/ably/lib/util/AblyAgentValidator.java
delete mode 100644 lib/src/test/java/io/ably/lib/util/AblyAgentValidatorTest.java
diff --git a/lib/src/main/java/io/ably/lib/rest/AblyBase.java b/lib/src/main/java/io/ably/lib/rest/AblyBase.java
index daf653ee5..0fec8d053 100644
--- a/lib/src/main/java/io/ably/lib/rest/AblyBase.java
+++ b/lib/src/main/java/io/ably/lib/rest/AblyBase.java
@@ -29,7 +29,6 @@
import io.ably.lib.types.ReadOnlyMap;
import io.ably.lib.types.Stats;
import io.ably.lib.types.StatsReader;
-import io.ably.lib.util.AblyAgentValidator;
import io.ably.lib.util.Crypto;
import io.ably.lib.util.InternalMap;
import io.ably.lib.util.Log;
@@ -79,11 +78,6 @@ public AblyBase(ClientOptions options, PlatformAgentProvider platformAgentProvid
Log.e(getClass().getName(), msg);
throw AblyException.fromErrorInfo(new ErrorInfo(msg, 400, 40000));
}
- if (options.agents != null && !AblyAgentValidator.areAllValid(options.agents)) {
- String msg = "invalid agent provided";
- Log.e(getClass().getName(), msg);
- throw AblyException.fromErrorInfo(new ErrorInfo(msg, 400, 40000));
- }
this.options = options;
/* process options */
diff --git a/lib/src/main/java/io/ably/lib/util/AblyAgentValidator.java b/lib/src/main/java/io/ably/lib/util/AblyAgentValidator.java
deleted file mode 100644
index e49bcbc4a..000000000
--- a/lib/src/main/java/io/ably/lib/util/AblyAgentValidator.java
+++ /dev/null
@@ -1,45 +0,0 @@
-package io.ably.lib.util;
-
-import java.util.Map;
-import java.util.regex.Pattern;
-
-public class AblyAgentValidator {
- /**
- * Agent name validation regex.
- * Allow only lowercase letters, digits and characters from the set [ !#$%&'*+-.^_`|~ ].
- */
- private static final String AGENT_NAME_REGEX = "^[a-z0-9!#$%&'*+\\-.^_`|~]+$";
- private static final Pattern agentNamePattern = Pattern.compile(AGENT_NAME_REGEX);
-
- /**
- * Agent version validation regex.
- * Suggested Semantic Versioning regex from the official site.
- * https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
- */
- private static final String SEM_VER_REGEX = "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$";
- private static final Pattern agentVersionPattern = Pattern.compile(SEM_VER_REGEX);
-
- /**
- * Checks if provided Ably agent values are valid.
- *
- * @return true if both agentName and agentVersion (if it's present) are valid values, false otherwise.
- */
- public static boolean isValid(String agentName, String agentVersion) {
- return agentNamePattern.matcher(agentName).matches()
- && (agentVersion == null || agentVersionPattern.matcher(agentVersion).matches());
- }
-
- /**
- * Checks if all provided Ably agent values are valid.
- *
- * @return true if all agents are valid, false otherwise.
- */
- public static boolean areAllValid(Map agents) {
- for (String agentName : agents.keySet()) {
- if (!isValid(agentName, agents.get(agentName))) {
- return false;
- }
- }
- return true;
- }
-}
diff --git a/lib/src/test/java/io/ably/lib/util/AblyAgentValidatorTest.java b/lib/src/test/java/io/ably/lib/util/AblyAgentValidatorTest.java
deleted file mode 100644
index 0c44da0e1..000000000
--- a/lib/src/test/java/io/ably/lib/util/AblyAgentValidatorTest.java
+++ /dev/null
@@ -1,106 +0,0 @@
-package io.ably.lib.util;
-
-import org.junit.Test;
-
-import java.util.HashMap;
-import java.util.Map;
-
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
-public class AblyAgentValidatorTest {
- @Test
- public void should_return_false_if_agent_name_is_invalid() {
- // given
- String agentName = "invalid/name";
- String agentVersion = "1.0.1";
-
- // when
- boolean isAgentValid = AblyAgentValidator.isValid(agentName, agentVersion);
-
- // then
- assertFalse(isAgentValid);
- }
-
- @Test
- public void should_return_false_if_agent_version_is_invalid() {
- // given
- String agentName = "valid-name";
- String agentVersion = "1v.23.ax";
-
- // when
- boolean isAgentValid = AblyAgentValidator.isValid(agentName, agentVersion);
-
- // then
- assertFalse(isAgentValid);
- }
-
- @Test
- public void should_return_false_if_both_agent_name_and_version_are_invalid() {
- // given
- String agentName = "invalid/name";
- String agentVersion = "1v.23.ax";
-
- // when
- boolean isAgentValid = AblyAgentValidator.isValid(agentName, agentVersion);
-
- // then
- assertFalse(isAgentValid);
- }
-
- @Test
- public void should_return_true_if_agent_name_and_version_are_valid() {
- // given
- String agentName = "valid-name";
- String agentVersion = "1.2.3-alpha.14";
-
- // when
- boolean isAgentValid = AblyAgentValidator.isValid(agentName, agentVersion);
-
- // then
- assertTrue(isAgentValid);
- }
-
- @Test
- public void should_return_true_if_agent_name_is_valid_and_version_is_null() {
- // given
- String agentName = "valid-name";
- String agentVersion = null;
-
- // when
- boolean isAgentValid = AblyAgentValidator.isValid(agentName, agentVersion);
-
- // then
- assertTrue(isAgentValid);
- }
-
- @Test
- public void should_return_true_if_all_agents_are_valid() {
- // given
- Map agents = new HashMap<>();
- agents.put("valid-name", "1.0.1");
- agents.put("another-valid-name123", null);
- agents.put("fully-123-valid-!#$%&'*+.^_`|~-name", null);
-
- // when
- boolean areAgentsValid = AblyAgentValidator.areAllValid(agents);
-
- // then
- assertTrue(areAgentsValid);
- }
-
- @Test
- public void should_return_false_if_any_agent_is_invalid() {
- // given
- Map agents = new HashMap<>();
- agents.put("valid-name", "1.0.1");
- agents.put("invalid/name", "1.0.1");
- agents.put("another-valid-name123", null);
-
- // when
- boolean areAgentsValid = AblyAgentValidator.areAllValid(agents);
-
- // then
- assertFalse(areAgentsValid);
- }
-}
From c4e3edb9321f81f1c10a293e69f591779f798264 Mon Sep 17 00:00:00 2001
From: Martin Morek
Date: Fri, 2 Jul 2021 16:35:01 +0200
Subject: [PATCH 076/899] Added posibility to provide custom storage
implementation instead of default SharedPreferences
---
.../io/ably/lib/push/ActivationContext.java | 5 +-
.../java/io/ably/lib/push/LocalDevice.java | 70 ++++++-------------
.../lib/push/SharedPreferenceStorage.java | 52 ++++++++++++++
.../main/java/io/ably/lib/push/Storage.java | 16 +++++
.../java/io/ably/lib/types/ClientOptions.java | 7 ++
5 files changed, 101 insertions(+), 49 deletions(-)
create mode 100644 android/src/main/java/io/ably/lib/push/SharedPreferenceStorage.java
create mode 100644 lib/src/main/java/io/ably/lib/push/Storage.java
diff --git a/android/src/main/java/io/ably/lib/push/ActivationContext.java b/android/src/main/java/io/ably/lib/push/ActivationContext.java
index c89e2f287..78a29c30d 100644
--- a/android/src/main/java/io/ably/lib/push/ActivationContext.java
+++ b/android/src/main/java/io/ably/lib/push/ActivationContext.java
@@ -5,7 +5,6 @@
import android.preference.PreferenceManager;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
-import com.google.firebase.FirebaseApp;
import com.google.firebase.iid.FirebaseInstanceId;
import com.google.firebase.iid.InstanceIdResult;
import io.ably.lib.rest.AblyRest;
@@ -31,7 +30,9 @@ Context getContext() {
public synchronized LocalDevice getLocalDevice() {
if(localDevice == null) {
Log.v(TAG, "getLocalDevice(): creating new instance and returning that");
- localDevice = new LocalDevice(this);
+ Storage storage = ably != null ? ably.options.storage : null;
+
+ localDevice = new LocalDevice(this, storage);
} else {
Log.v(TAG, "getLocalDevice(): returning existing instance");
}
diff --git a/android/src/main/java/io/ably/lib/push/LocalDevice.java b/android/src/main/java/io/ably/lib/push/LocalDevice.java
index 8f82fa334..a6c4288f6 100644
--- a/android/src/main/java/io/ably/lib/push/LocalDevice.java
+++ b/android/src/main/java/io/ably/lib/push/LocalDevice.java
@@ -1,38 +1,34 @@
package io.ably.lib.push;
import android.content.Context;
-import android.content.SharedPreferences;
import android.content.res.Configuration;
-import android.preference.PreferenceManager;
-
import com.google.gson.JsonObject;
-
-import java.lang.reflect.Field;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
-
import io.ably.lib.rest.DeviceDetails;
-import io.ably.lib.types.AblyException;
+import io.ably.lib.types.Param;
import io.ably.lib.types.RegistrationToken;
import io.ably.lib.util.Base64Coder;
import io.ably.lib.util.Log;
-import io.ably.lib.types.Param;
import io.azam.ulidj.ULID;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+
public class LocalDevice extends DeviceDetails {
public String deviceSecret;
public String deviceIdentityToken;
+ private final Storage storage;
private final ActivationContext activationContext;
- public LocalDevice(ActivationContext activationContext) {
+ public LocalDevice(ActivationContext activationContext, Storage storage) {
super();
Log.v(TAG, "LocalDevice(): initialising");
this.platform = "android";
this.formFactor = isTablet(activationContext.getContext()) ? "tablet" : "phone";
this.activationContext = activationContext;
this.push = new DeviceDetails.Push();
+ this.storage = storage != null ? storage : new SharedPreferenceStorage(activationContext);
loadPersisted();
}
@@ -47,26 +43,24 @@ public JsonObject toJsonObject() {
private void loadPersisted() {
/* Spec: RSH8a */
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activationContext.getContext());
-
- String id = prefs.getString(SharedPrefKeys.DEVICE_ID, null);
+ String id = storage.getString(SharedPrefKeys.DEVICE_ID, null);
this.id = id;
if(id != null) {
Log.v(TAG, "loadPersisted(): existing deviceId found; id: " + id);
- deviceSecret = prefs.getString(SharedPrefKeys.DEVICE_SECRET, null);
+ deviceSecret = storage.getString(SharedPrefKeys.DEVICE_SECRET, null);
} else {
Log.v(TAG, "loadPersisted(): existing deviceId not found.");
}
- this.clientId = prefs.getString(SharedPrefKeys.CLIENT_ID, null);
- this.deviceIdentityToken = prefs.getString(SharedPrefKeys.DEVICE_TOKEN, null);
+ this.clientId = storage.getString(SharedPrefKeys.CLIENT_ID, null);
+ this.deviceIdentityToken = storage.getString(SharedPrefKeys.DEVICE_TOKEN, null);
RegistrationToken.Type type = RegistrationToken.Type.fromOrdinal(
- prefs.getInt(SharedPrefKeys.TOKEN_TYPE, -1));
+ storage.getInt(SharedPrefKeys.TOKEN_TYPE, -1));
Log.d(TAG, "loadPersisted(): token type = " + type);
if(type != null) {
RegistrationToken token = null;
- String tokenString = prefs.getString(SharedPrefKeys.TOKEN, null);
+ String tokenString = storage.getString(SharedPrefKeys.TOKEN, null);
Log.d(TAG, "loadPersisted(): token string = " + tokenString);
if(tokenString != null) {
token = new RegistrationToken(type, tokenString);
@@ -103,42 +97,32 @@ private void clearRegistrationToken() {
void setAndPersistRegistrationToken(RegistrationToken token) {
Log.v(TAG, "setAndPersistRegistrationToken(): token=" + token);
setRegistrationToken(token);
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activationContext.getContext());
- prefs.edit()
- .putInt(SharedPrefKeys.TOKEN_TYPE, token.type.ordinal())
- .putString(SharedPrefKeys.TOKEN, token.token)
- .apply();
+ storage.putInt(SharedPrefKeys.TOKEN_TYPE, token.type.ordinal());
+ storage.putString(SharedPrefKeys.TOKEN, token.token);
}
void setClientId(String clientId) {
Log.v(TAG, "setClientId(): clientId=" + clientId);
this.clientId = clientId;
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activationContext.getContext());
- prefs.edit().putString(SharedPrefKeys.CLIENT_ID, clientId).apply();
+ storage.putString(SharedPrefKeys.CLIENT_ID, clientId);
}
public void setDeviceIdentityToken(String token) {
Log.v(TAG, "setDeviceIdentityToken(): token=" + token);
this.deviceIdentityToken = token;
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activationContext.getContext());
- prefs.edit().putString(SharedPrefKeys.DEVICE_TOKEN, token).apply();
+ storage.putString(SharedPrefKeys.DEVICE_TOKEN, token);
}
boolean isCreated() {
return id != null;
}
- boolean create() {
+ void create() {
/* Spec: RSH8b */
Log.v(TAG, "create()");
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activationContext.getContext());
- SharedPreferences.Editor editor = prefs.edit();
-
- editor.putString(SharedPrefKeys.DEVICE_ID, (id = ULID.random()));
- editor.putString(SharedPrefKeys.CLIENT_ID, (clientId = activationContext.clientId));
- editor.putString(SharedPrefKeys.DEVICE_SECRET, (deviceSecret = generateSecret()));
-
- return editor.commit();
+ storage.putString(SharedPrefKeys.DEVICE_ID, (id = ULID.random()));
+ storage.putString(SharedPrefKeys.CLIENT_ID, (clientId = activationContext.clientId));
+ storage.putString(SharedPrefKeys.DEVICE_SECRET, (deviceSecret = generateSecret()));
}
public void reset() {
@@ -149,15 +133,7 @@ public void reset() {
this.clientId = null;
this.clearRegistrationToken();
- SharedPreferences.Editor editor = activationContext.getPreferences().edit();
- for (Field f : SharedPrefKeys.class.getDeclaredFields()) {
- try {
- editor.remove((String) f.get(null));
- } catch (IllegalAccessException e) {
- throw new RuntimeException(e);
- }
- }
- editor.commit();
+ storage.reset(SharedPrefKeys.class.getDeclaredFields());
}
boolean isRegistered() {
diff --git a/android/src/main/java/io/ably/lib/push/SharedPreferenceStorage.java b/android/src/main/java/io/ably/lib/push/SharedPreferenceStorage.java
new file mode 100644
index 000000000..3df775569
--- /dev/null
+++ b/android/src/main/java/io/ably/lib/push/SharedPreferenceStorage.java
@@ -0,0 +1,52 @@
+package io.ably.lib.push;
+
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+
+import java.lang.reflect.Field;
+
+public class SharedPreferenceStorage implements Storage{
+
+ private final ActivationContext activationContext;
+
+ public SharedPreferenceStorage(ActivationContext activationContext) {
+ this.activationContext = activationContext;
+ }
+
+ @Override
+ public void putString(String key, String value) {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activationContext.getContext());
+ prefs.edit().putString(key, value).apply();
+ }
+
+ @Override
+ public void putInt(String key, int value) {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activationContext.getContext());
+ prefs.edit().putInt(key, value).apply();
+ }
+
+ @Override
+ public String getString(String key, String defaultValue) {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activationContext.getContext());
+ return prefs.getString(key, defaultValue);
+ }
+
+ @Override
+ public int getInt(String key, int defValue) {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activationContext.getContext());
+ return prefs.getInt(key, defValue);
+ }
+
+ @Override
+ public void reset(Field[] fields) {
+ SharedPreferences.Editor editor = activationContext.getPreferences().edit();
+ for (Field f : fields) {
+ try {
+ editor.remove((String) f.get(null));
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ editor.commit();
+ }
+}
diff --git a/lib/src/main/java/io/ably/lib/push/Storage.java b/lib/src/main/java/io/ably/lib/push/Storage.java
new file mode 100644
index 000000000..eb7b363f1
--- /dev/null
+++ b/lib/src/main/java/io/ably/lib/push/Storage.java
@@ -0,0 +1,16 @@
+package io.ably.lib.push;
+
+import java.lang.reflect.Field;
+
+public interface Storage {
+
+ void putString(String key, String value);
+
+ void putInt(String key, int value);
+
+ String getString(String key, String defaultValue);
+
+ int getInt(String key, int defValue);
+
+ void reset(Field[] fields);
+}
diff --git a/lib/src/main/java/io/ably/lib/types/ClientOptions.java b/lib/src/main/java/io/ably/lib/types/ClientOptions.java
index 8444964b3..6340bd013 100644
--- a/lib/src/main/java/io/ably/lib/types/ClientOptions.java
+++ b/lib/src/main/java/io/ably/lib/types/ClientOptions.java
@@ -1,5 +1,6 @@
package io.ably.lib.types;
+import io.ably.lib.push.Storage;
import io.ably.lib.rest.Auth.AuthOptions;
import io.ably.lib.rest.Auth.TokenParams;
import io.ably.lib.transport.Defaults;
@@ -195,4 +196,10 @@ public ClientOptions(String key) throws AblyException {
* before responding.
*/
public boolean pushFullWait = false;
+
+ /**
+ * Allows provide custom Local Device storage. In a case nothing is provided default implementation
+ * using SharedPreferences is used.
+ */
+ public Storage storage = null;
}
From 89ecf93d93076f20c54eef931f324b20bd445068 Mon Sep 17 00:00:00 2001
From: Martin Morek
Date: Mon, 5 Jul 2021 16:49:41 +0200
Subject: [PATCH 077/899] Refactored out common functionality
---
.../ably/lib/push/ActivationStateMachine.java | 36 ++----------
.../java/io/ably/lib/push/PushChannel.java | 15 +----
.../main/java/io/ably/lib/push/PushBase.java | 55 +++----------------
.../java/io/ably/lib/util/ParamsUtils.java | 18 ++++++
4 files changed, 35 insertions(+), 89 deletions(-)
create mode 100644 lib/src/main/java/io/ably/lib/util/ParamsUtils.java
diff --git a/android/src/main/java/io/ably/lib/push/ActivationStateMachine.java b/android/src/main/java/io/ably/lib/push/ActivationStateMachine.java
index 07d386ee7..1df994d54 100644
--- a/android/src/main/java/io/ably/lib/push/ActivationStateMachine.java
+++ b/android/src/main/java/io/ably/lib/push/ActivationStateMachine.java
@@ -18,9 +18,9 @@
import io.ably.lib.rest.AblyRest;
import io.ably.lib.rest.DeviceDetails;
import io.ably.lib.types.*;
-import io.ably.lib.util.Crypto;
import io.ably.lib.util.IntentUtils;
import io.ably.lib.util.Log;
+import io.ably.lib.util.ParamsUtils;
import io.ably.lib.util.Serialisation;
public class ActivationStateMachine {
@@ -136,13 +136,7 @@ public ActivationStateMachine.State transition(final ActivationStateMachine.Even
ably.http.request(new Http.Execute() {
@Override
public void execute(HttpScheduler http, Callback callback) throws AblyException {
- Param[] params = null;
- if(ably.options.pushFullWait) {
- params = Param.push(null, "fullWait", "true");
- }
- if(ably.options.addRequestIds) { // RSC7c
- params = Param.set(params, Crypto.generateRandomRequestId());
- }
+ Param[] params = ParamsUtils.enrichParams(null, ably.options);
/* this is authenticated using the Ably library credentials, plus the deviceSecret in the request body */
http.post("/push/deviceRegistrations", HttpUtils.defaultAcceptHeaders(ably.options.useBinaryProtocol), params, body, new Serialisation.HttpResponseHandler(), true, callback);
}
@@ -429,14 +423,7 @@ private void updateRegistration() {
ably.http.request(new Http.Execute() {
@Override
public void execute(HttpScheduler http, Callback callback) throws AblyException {
- Param[] params = null;
- if (ably.options.pushFullWait) {
- params = Param.push(params, "fullWait", "true");
- }
- if(ably.options.addRequestIds) { // RSC7c
- params = Param.set(params, Crypto.generateRandomRequestId());
- }
-
+ Param[] params = ParamsUtils.enrichParams(null, ably.options);
http.patch("/push/deviceRegistrations/" + device.id, ably.push.pushRequestHeaders(true), params, body, null, false, callback);
}
}).async(new Callback() {
@@ -480,14 +467,7 @@ private void validateRegistration() {
ably.http.request(new Http.Execute() {
@Override
public void execute(HttpScheduler http, Callback callback) throws AblyException {
- Param[] params = null;
- if (ably.options.pushFullWait) {
- params = Param.push(params, "fullWait", "true");
- }
- if(ably.options.addRequestIds) { // RSC7c
- params = Param.set(params, Crypto.generateRandomRequestId());
- }
-
+ Param[] params = ParamsUtils.enrichParams(null, ably.options);
final HttpCore.RequestBody body = HttpUtils.requestBodyFromGson(device.toJsonObject(), ably.options.useBinaryProtocol);
http.put("/push/deviceRegistrations/" + device.id, ably.push.pushRequestHeaders(true), params, body, new Serialisation.HttpResponseHandler(), true, callback);
}
@@ -531,13 +511,7 @@ private void deregister() {
ably.http.request(new Http.Execute() {
@Override
public void execute(HttpScheduler http, Callback callback) throws AblyException {
- Param[] params = new Param[0];
- if (ably.options.pushFullWait) {
- params = Param.push(params, "fullWait", "true");
- }
- if(ably.options.addRequestIds) { // RSC7c
- params = Param.set(params, Crypto.generateRandomRequestId());
- }
+ Param[] params = ParamsUtils.enrichParams(new Param[0], ably.options);
http.del("/push/deviceRegistrations/" + device.id, ably.push.pushRequestHeaders(true), params, null, true, callback);
}
}).async(new Callback() {
diff --git a/android/src/main/java/io/ably/lib/push/PushChannel.java b/android/src/main/java/io/ably/lib/push/PushChannel.java
index c1b49d0e0..ce655d2ce 100644
--- a/android/src/main/java/io/ably/lib/push/PushChannel.java
+++ b/android/src/main/java/io/ably/lib/push/PushChannel.java
@@ -7,7 +7,7 @@
import io.ably.lib.rest.Channel;
import io.ably.lib.rest.DeviceDetails;
import io.ably.lib.types.*;
-import io.ably.lib.util.Crypto;
+import io.ably.lib.util.ParamsUtils;
public class PushChannel {
protected final Channel channel;
@@ -64,13 +64,7 @@ protected Http.Request postSubscription(JsonObject bodyJson) {
return rest.http.request(new Http.Execute() {
@Override
public void execute(HttpScheduler http, Callback callback) throws AblyException {
- Param[] params = null;
- if (rest.options.pushFullWait) {
- params = Param.push(params, "fullWait", "true");
- }
- if(rest.options.addRequestIds) { // RSC7c
- params = Param.set(params, Crypto.generateRandomRequestId());
- }
+ Param[] params = ParamsUtils.enrichParams(null, rest.options);
http.post("/push/channelSubscriptions", rest.push.pushRequestHeaders(true), params, body, null, true, callback);
}
});
@@ -112,10 +106,7 @@ protected Http.Request unsubscribeDeviceImpl() {
}
protected Http.Request delSubscription(Param[] params) {
- if (rest.options.pushFullWait) {
- params = Param.push(params, "fullWait", "true");
- }
- final Param[] finalParams = rest.options.addRequestIds ? Param.set(params, Crypto.generateRandomRequestId()) : params; // RSC7c
+ final Param[] finalParams = ParamsUtils.enrichParams(params, rest.options);
return rest.http.request(new Http.Execute() {
@Override
public void execute(HttpScheduler http, Callback callback) throws AblyException {
diff --git a/lib/src/main/java/io/ably/lib/push/PushBase.java b/lib/src/main/java/io/ably/lib/push/PushBase.java
index 4f798425e..5541925a8 100644
--- a/lib/src/main/java/io/ably/lib/push/PushBase.java
+++ b/lib/src/main/java/io/ably/lib/push/PushBase.java
@@ -15,8 +15,8 @@
import io.ably.lib.types.Callback;
import io.ably.lib.types.PaginatedResult;
import io.ably.lib.types.Param;
-import io.ably.lib.util.Crypto;
import io.ably.lib.util.Log;
+import io.ably.lib.util.ParamsUtils;
import io.ably.lib.util.Serialisation;
import io.ably.lib.util.StringUtils;
@@ -72,14 +72,7 @@ public void execute(HttpScheduler http, Callback callback) throws AblyExce
bodyJson.add(entry.getKey(), entry.getValue());
}
HttpCore.RequestBody body = HttpUtils.requestBodyFromGson(bodyJson, rest.options.useBinaryProtocol);
-
- Param[] params = null;
- if (rest.options.pushFullWait) {
- params = Param.push(params, "fullWait", "true");
- }
- if (rest.options.addRequestIds) {
- params = Param.set(params, Crypto.generateRandomRequestId()); // RSC7c
- }
+ Param[] params = ParamsUtils.enrichParams(null, rest.options);
http.post("/push/publish", HttpUtils.defaultAcceptHeaders(rest.options.useBinaryProtocol), params, body, null, true, callback);
}
@@ -105,14 +98,8 @@ protected Http.Request saveImpl(final DeviceDetails device) {
final HttpCore.RequestBody body = HttpUtils.requestBodyFromGson(device.toJsonObject(), rest.options.useBinaryProtocol);
return rest.http.request(new Http.Execute() {
@Override
- public void execute(HttpScheduler http, Callback callback) throws AblyException {
- Param[] params = null;
- if (rest.options.pushFullWait) {
- params = Param.push(params, "fullWait", "true");
- }
- if(rest.options.addRequestIds) { // RSC7c
- params = Param.set(params, Crypto.generateRandomRequestId());
- }
+ public void execute(HttpScheduler http, Callback callback) {
+ Param[] params = ParamsUtils.enrichParams(null, rest.options);
http.put("/push/deviceRegistrations/" + device.id, rest.push.pushRequestHeaders(device.id), params, body, DeviceDetails.httpResponseHandler, true, callback);
}
});
@@ -131,13 +118,7 @@ protected Http.Request getImpl(final String deviceId) {
return rest.http.request(new Http.Execute() {
@Override
public void execute(HttpScheduler http, Callback callback) throws AblyException {
- Param[] params = null;
- if (rest.options.pushFullWait) {
- params = Param.push(params, "fullWait", "true");
- }
- if (rest.options.addRequestIds) { // RSC7c
- params = Param.set(params, Crypto.generateRandomRequestId());
- }
+ Param[] params = ParamsUtils.enrichParams(null, rest.options);
http.get("/push/deviceRegistrations/" + deviceId, rest.push.pushRequestHeaders(deviceId), params, DeviceDetails.httpResponseHandler, true, callback);
}
});
@@ -177,13 +158,7 @@ protected Http.Request removeImpl(final String deviceId) {
return rest.http.request(new Http.Execute() {
@Override
public void execute(HttpScheduler http, Callback callback) throws AblyException {
- Param[] params = null;
- if (rest.options.pushFullWait) {
- params = Param.push(params, "fullWait", "true");
- }
- if(rest.options.addRequestIds) { // RSC7c
- params = Param.set(params, Crypto.generateRandomRequestId());
- }
+ Param[] params = ParamsUtils.enrichParams(null, rest.options);
http.del("/push/deviceRegistrations/" + deviceId, rest.push.pushRequestHeaders(deviceId), params, null, true, callback);
}
});
@@ -199,10 +174,7 @@ public void removeWhereAsync(Param[] params, CompletionListener listener) {
protected Http.Request removeWhereImpl(Param[] params) {
Log.v(TAG, "removeWhereImpl(): params=" + Arrays.toString(params));
- if (rest.options.pushFullWait) {
- params = Param.push(params, "fullWait", "true");
- }
- final Param[] finalParams = rest.options.addRequestIds ? Param.set(params, Crypto.generateRandomRequestId()) : params; // RSC7c
+ final Param[] finalParams = ParamsUtils.enrichParams(params, rest.options);
return rest.http.request(new Http.Execute() {
@Override
public void execute(HttpScheduler http, Callback callback) throws AblyException {
@@ -235,13 +207,7 @@ protected Http.Request saveImpl(final ChannelSubscription s
return rest.http.request(new Http.Execute() {
@Override
public void execute(HttpScheduler http, Callback callback) throws AblyException {
- Param[] params = null;
- if (rest.options.pushFullWait) {
- params = Param.push(params, "fullWait", "true");
- }
- if (rest.options.addRequestIds) {
- params = Param.set(params, Crypto.generateRandomRequestId()); // RSC7c
- }
+ Param[] params = ParamsUtils.enrichParams(null, rest.options);
http.post("/push/channelSubscriptions", rest.push.pushRequestHeaders(subscription.deviceId), params, body, ChannelSubscription.httpResponseHandler, true, callback);
}
});
@@ -295,11 +261,8 @@ public void removeWhereAsync(Param[] params, CompletionListener listener) {
protected Http.Request removeWhereImpl(Param[] params) {
Log.v(TAG, "removeWhereImpl(): params=" + Arrays.toString(params));
String deviceId = HttpUtils.getParam(params, "deviceId");
- if (rest.options.pushFullWait) {
- params = Param.push(params, "fullWait", "true");
- }
+ final Param[] finalParams = ParamsUtils.enrichParams(params, rest.options);
final Param[] finalHeaders = rest.push.pushRequestHeaders(deviceId);
- final Param[] finalParams = rest.options.addRequestIds ? Param.set(params, Crypto.generateRandomRequestId()) : params; // RSC7c
return rest.http.request(new Http.Execute() {
@Override
public void execute(HttpScheduler http, Callback callback) throws AblyException {
diff --git a/lib/src/main/java/io/ably/lib/util/ParamsUtils.java b/lib/src/main/java/io/ably/lib/util/ParamsUtils.java
new file mode 100644
index 000000000..2cc51f5ea
--- /dev/null
+++ b/lib/src/main/java/io/ably/lib/util/ParamsUtils.java
@@ -0,0 +1,18 @@
+package io.ably.lib.util;
+
+import io.ably.lib.types.ClientOptions;
+import io.ably.lib.types.Param;
+
+public class ParamsUtils {
+
+ public static Param[] enrichParams(Param[] params, ClientOptions options) {
+ if (options.pushFullWait) {
+ params = Param.push(params, "fullWait", "true");
+ }
+ if (options.addRequestIds) { // RSC7c
+ params = Param.set(params, Crypto.generateRandomRequestId());
+ }
+
+ return params;
+ }
+}
From 8c5bd79a200f71a71148fffdfd71481ac523b5af Mon Sep 17 00:00:00 2001
From: Martin Morek
Date: Tue, 6 Jul 2021 14:34:03 +0200
Subject: [PATCH 078/899] Test correct storage is used
---
.../ably/lib/push/LocalDeviceStorageTest.java | 149 ++++++++++++++++++
.../lib/push/SharedPreferenceStorage.java | 4 +-
.../main/java/io/ably/lib/push/Storage.java | 2 +-
3 files changed, 152 insertions(+), 3 deletions(-)
create mode 100644 android/src/androidTest/java/io/ably/lib/push/LocalDeviceStorageTest.java
diff --git a/android/src/androidTest/java/io/ably/lib/push/LocalDeviceStorageTest.java b/android/src/androidTest/java/io/ably/lib/push/LocalDeviceStorageTest.java
new file mode 100644
index 000000000..7297a485a
--- /dev/null
+++ b/android/src/androidTest/java/io/ably/lib/push/LocalDeviceStorageTest.java
@@ -0,0 +1,149 @@
+package io.ably.lib.push;
+
+import android.content.Context;
+import android.test.AndroidTestCase;
+import io.ably.lib.types.RegistrationToken;
+import junit.extensions.TestSetup;
+import junit.framework.TestSuite;
+import org.junit.BeforeClass;
+
+import java.lang.reflect.Field;
+import java.util.HashMap;
+
+public class LocalDeviceStorageTest extends AndroidTestCase {
+ private Context context;
+ private ActivationContext activationContext;
+
+
+ private HashMap hashMap = new HashMap<>();
+
+ private Storage inMemoryStorage = new Storage() {
+ @Override
+ public void putString(String key, String value) {
+ hashMap.put(key, value);
+ }
+
+ @Override
+ public void putInt(String key, int value) {
+ hashMap.put(key, value);
+ }
+
+ @Override
+ public String getString(String key, String defaultValue) {
+ Object value = hashMap.get(key);
+ return value != null ? (String) value : defaultValue;
+ }
+
+ @Override
+ public int getInt(String key, int defaultValue) {
+ Object value = hashMap.get(key);
+ return value != null ? (int) value : defaultValue;
+ }
+
+ @Override
+ public void reset(Field[] fields) {
+ hashMap = new HashMap<>();
+ }
+ };
+
+ @BeforeClass
+ public void setUp() {
+ context = getContext();
+ activationContext = new ActivationContext(context.getApplicationContext());
+ }
+
+ public static junit.framework.Test suite() {
+ TestSuite suite = new TestSuite();
+ suite.addTest(new TestSetup(new TestSuite(LocalDeviceStorageTest.class)) {});
+ return suite;
+ }
+
+ public void test_shared_preferences_storage_used_by_default() {
+ LocalDevice localDevice = new LocalDevice(activationContext, null);
+ /* initialize properties in storage */
+ localDevice.create();
+
+ /* verify custom storage is not used */
+ assertTrue(hashMap.isEmpty());
+
+ /* load properties */
+ assertNotNull(localDevice.id);
+ assertNotNull(localDevice.deviceSecret);
+ }
+
+ public void test_shared_preferences_storage_works_correctly() {
+ LocalDevice localDevice = new LocalDevice(activationContext, null);
+
+ RegistrationToken registrationToken= new RegistrationToken(RegistrationToken.Type.FCM, "ABLY");
+ /* initialize properties in storage */
+ localDevice.create();
+ localDevice.setAndPersistRegistrationToken(registrationToken);
+
+ /* verify custom storage is not used */
+ assertTrue(hashMap.isEmpty());
+
+ /* load properties */
+ assertNotNull(localDevice.id);
+ assertNotNull(localDevice.deviceSecret);
+ assertTrue(localDevice.isCreated());
+ assertEquals("FCM", localDevice.getRegistrationToken().type.name());
+ assertEquals("ABLY", localDevice.getRegistrationToken().token);
+
+ /* reset all properties */
+ localDevice.reset();
+
+ /* properties were cleared */
+ assertNull(localDevice.id);
+ assertNull(localDevice.deviceSecret);
+ assertNull(localDevice.getRegistrationToken());
+ }
+
+ public void test_custom_storage_used_if_provided() {
+ LocalDevice localDevice = new LocalDevice(activationContext, inMemoryStorage);
+ /* initialize properties in storage */
+ localDevice.create();
+
+ /* verify in memory storage is used */
+ assertFalse(hashMap.isEmpty());
+
+ /* load properties */
+ assertNotNull(localDevice.id);
+ assertNotNull(localDevice.deviceSecret);
+
+ String deviceId = localDevice.id;
+ String deviceSecret = localDevice.deviceSecret;
+
+ /* values are the same */
+ assertEquals(deviceId, hashMap.get("ABLY_DEVICE_ID"));
+ assertEquals(deviceSecret, hashMap.get("ABLY_DEVICE_SECRET"));
+ }
+
+ public void test_custom_storage_works_correctly() {
+ LocalDevice localDevice = new LocalDevice(activationContext, inMemoryStorage);
+
+ RegistrationToken registrationToken= new RegistrationToken(RegistrationToken.Type.FCM, "ABLY");
+ /* initialize properties in storage */
+ localDevice.create();
+ localDevice.setAndPersistRegistrationToken(registrationToken);
+
+ /* verify custom storage is used */
+ assertFalse(hashMap.isEmpty());
+
+ /* load properties */
+ assertNotNull(localDevice.id);
+ assertNotNull(localDevice.deviceSecret);
+ assertTrue(localDevice.isCreated());
+ assertEquals("FCM", localDevice.getRegistrationToken().type.name());
+ assertEquals("ABLY", localDevice.getRegistrationToken().token);
+
+ /* reset all properties */
+ localDevice.reset();
+ /* verify custom storage was cleared out */
+ assertTrue(hashMap.isEmpty());
+
+ /* properties were cleared */
+ assertNull(localDevice.id);
+ assertNull(localDevice.deviceSecret);
+ assertNull(localDevice.getRegistrationToken());
+ }
+}
diff --git a/android/src/main/java/io/ably/lib/push/SharedPreferenceStorage.java b/android/src/main/java/io/ably/lib/push/SharedPreferenceStorage.java
index 3df775569..52146fbdb 100644
--- a/android/src/main/java/io/ably/lib/push/SharedPreferenceStorage.java
+++ b/android/src/main/java/io/ably/lib/push/SharedPreferenceStorage.java
@@ -32,9 +32,9 @@ public String getString(String key, String defaultValue) {
}
@Override
- public int getInt(String key, int defValue) {
+ public int getInt(String key, int defaultValue) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activationContext.getContext());
- return prefs.getInt(key, defValue);
+ return prefs.getInt(key, defaultValue);
}
@Override
diff --git a/lib/src/main/java/io/ably/lib/push/Storage.java b/lib/src/main/java/io/ably/lib/push/Storage.java
index eb7b363f1..e26ccaec7 100644
--- a/lib/src/main/java/io/ably/lib/push/Storage.java
+++ b/lib/src/main/java/io/ably/lib/push/Storage.java
@@ -10,7 +10,7 @@ public interface Storage {
String getString(String key, String defaultValue);
- int getInt(String key, int defValue);
+ int getInt(String key, int defaultValue);
void reset(Field[] fields);
}
From 6dcd8a850c7c1658eb50bd9278ccaa4c8ff420a7 Mon Sep 17 00:00:00 2001
From: Martin Morek
Date: Wed, 7 Jul 2021 18:23:51 +0200
Subject: [PATCH 079/899] Renamed method reset to clear, added documentation
comment
---
.../java/io/ably/lib/push/LocalDeviceStorageTest.java | 2 +-
android/src/main/java/io/ably/lib/push/LocalDevice.java | 2 +-
.../main/java/io/ably/lib/push/SharedPreferenceStorage.java | 2 +-
lib/src/main/java/io/ably/lib/push/Storage.java | 6 +++++-
4 files changed, 8 insertions(+), 4 deletions(-)
diff --git a/android/src/androidTest/java/io/ably/lib/push/LocalDeviceStorageTest.java b/android/src/androidTest/java/io/ably/lib/push/LocalDeviceStorageTest.java
index 7297a485a..3ae9e56ab 100644
--- a/android/src/androidTest/java/io/ably/lib/push/LocalDeviceStorageTest.java
+++ b/android/src/androidTest/java/io/ably/lib/push/LocalDeviceStorageTest.java
@@ -41,7 +41,7 @@ public int getInt(String key, int defaultValue) {
}
@Override
- public void reset(Field[] fields) {
+ public void clear(Field[] fields) {
hashMap = new HashMap<>();
}
};
diff --git a/android/src/main/java/io/ably/lib/push/LocalDevice.java b/android/src/main/java/io/ably/lib/push/LocalDevice.java
index a6c4288f6..41ebe3f1c 100644
--- a/android/src/main/java/io/ably/lib/push/LocalDevice.java
+++ b/android/src/main/java/io/ably/lib/push/LocalDevice.java
@@ -133,7 +133,7 @@ public void reset() {
this.clientId = null;
this.clearRegistrationToken();
- storage.reset(SharedPrefKeys.class.getDeclaredFields());
+ storage.clear(SharedPrefKeys.class.getDeclaredFields());
}
boolean isRegistered() {
diff --git a/android/src/main/java/io/ably/lib/push/SharedPreferenceStorage.java b/android/src/main/java/io/ably/lib/push/SharedPreferenceStorage.java
index 52146fbdb..54a5a99d3 100644
--- a/android/src/main/java/io/ably/lib/push/SharedPreferenceStorage.java
+++ b/android/src/main/java/io/ably/lib/push/SharedPreferenceStorage.java
@@ -38,7 +38,7 @@ public int getInt(String key, int defaultValue) {
}
@Override
- public void reset(Field[] fields) {
+ public void clear(Field[] fields) {
SharedPreferences.Editor editor = activationContext.getPreferences().edit();
for (Field f : fields) {
try {
diff --git a/lib/src/main/java/io/ably/lib/push/Storage.java b/lib/src/main/java/io/ably/lib/push/Storage.java
index e26ccaec7..dfc2bd4ca 100644
--- a/lib/src/main/java/io/ably/lib/push/Storage.java
+++ b/lib/src/main/java/io/ably/lib/push/Storage.java
@@ -2,6 +2,10 @@
import java.lang.reflect.Field;
+/**
+ * Interface for an entity that supplies key value store
+ * - methods getString and getInt have to return default value if requested key is not found
+ */
public interface Storage {
void putString(String key, String value);
@@ -12,5 +16,5 @@ public interface Storage {
int getInt(String key, int defaultValue);
- void reset(Field[] fields);
+ void clear(Field[] fields);
}
From 74ff1469383a111b58cfdbb605fc6e196c3f87d7 Mon Sep 17 00:00:00 2001
From: Martin Morek
Date: Thu, 8 Jul 2021 15:38:25 +0200
Subject: [PATCH 080/899] Extracted code repetition into separate method
---
.../ably/lib/push/SharedPreferenceStorage.java | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/android/src/main/java/io/ably/lib/push/SharedPreferenceStorage.java b/android/src/main/java/io/ably/lib/push/SharedPreferenceStorage.java
index 54a5a99d3..2cbe0710d 100644
--- a/android/src/main/java/io/ably/lib/push/SharedPreferenceStorage.java
+++ b/android/src/main/java/io/ably/lib/push/SharedPreferenceStorage.java
@@ -13,28 +13,28 @@ public SharedPreferenceStorage(ActivationContext activationContext) {
this.activationContext = activationContext;
}
+ private SharedPreferences sharedPreferences() {
+ return PreferenceManager.getDefaultSharedPreferences(activationContext.getContext());
+ }
+
@Override
public void putString(String key, String value) {
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activationContext.getContext());
- prefs.edit().putString(key, value).apply();
+ sharedPreferences().edit().putString(key, value).apply();
}
@Override
public void putInt(String key, int value) {
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activationContext.getContext());
- prefs.edit().putInt(key, value).apply();
+ sharedPreferences().edit().putInt(key, value).apply();
}
@Override
public String getString(String key, String defaultValue) {
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activationContext.getContext());
- return prefs.getString(key, defaultValue);
+ return sharedPreferences().getString(key, defaultValue);
}
@Override
public int getInt(String key, int defaultValue) {
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activationContext.getContext());
- return prefs.getInt(key, defaultValue);
+ return sharedPreferences().getInt(key, defaultValue);
}
@Override
From d14c07013d0e520d6e7e279c816f1f4e1c6a5409 Mon Sep 17 00:00:00 2001
From: Martin Morek
Date: Thu, 8 Jul 2021 16:18:57 +0200
Subject: [PATCH 081/899] Unified custom toString() method implementations to
use curly brackets insted of square brackets
---
.../main/java/io/ably/lib/transport/WebSocketTransport.java | 2 +-
lib/src/main/java/io/ably/lib/types/ErrorInfo.java | 4 ++--
lib/src/main/java/io/ably/lib/types/Message.java | 4 ++--
lib/src/main/java/io/ably/lib/types/PresenceMessage.java | 4 ++--
4 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/lib/src/main/java/io/ably/lib/transport/WebSocketTransport.java b/lib/src/main/java/io/ably/lib/transport/WebSocketTransport.java
index a56fc149c..c709890b1 100644
--- a/lib/src/main/java/io/ably/lib/transport/WebSocketTransport.java
+++ b/lib/src/main/java/io/ably/lib/transport/WebSocketTransport.java
@@ -303,7 +303,7 @@ private synchronized void schedule(TimerTask task, long delay) {
}
public String toString() {
- return WebSocketTransport.class.getName() + " [" + getURL() + "]";
+ return WebSocketTransport.class.getName() + " {" + getURL() + "}";
}
public String getURL() {
diff --git a/lib/src/main/java/io/ably/lib/types/ErrorInfo.java b/lib/src/main/java/io/ably/lib/types/ErrorInfo.java
index 6fa3d72b0..e266fe080 100644
--- a/lib/src/main/java/io/ably/lib/types/ErrorInfo.java
+++ b/lib/src/main/java/io/ably/lib/types/ErrorInfo.java
@@ -66,7 +66,7 @@ public ErrorInfo(String message, int statusCode, int code) {
}
public String toString() {
- StringBuilder result = new StringBuilder("[ErrorInfo");
+ StringBuilder result = new StringBuilder("{ErrorInfo");
result.append(" message=").append(logMessage());
if(code > 0) {
result.append(" code=").append(code);
@@ -77,7 +77,7 @@ public String toString() {
if(href != null) {
result.append(" href=").append(href);
}
- result.append(']');
+ result.append('}');
return result.toString();
}
diff --git a/lib/src/main/java/io/ably/lib/types/Message.java b/lib/src/main/java/io/ably/lib/types/Message.java
index e8ab42942..203cf7121 100644
--- a/lib/src/main/java/io/ably/lib/types/Message.java
+++ b/lib/src/main/java/io/ably/lib/types/Message.java
@@ -101,11 +101,11 @@ public Message(String name, Object data, String clientId, MessageExtras extras)
* @return string
*/
public String toString() {
- StringBuilder result = new StringBuilder("[Message");
+ StringBuilder result = new StringBuilder("{Message");
super.getDetails(result);
if(name != null)
result.append(" name=").append(name);
- result.append(']');
+ result.append('}');
return result.toString();
}
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 51523a31e..bc615ea08 100644
--- a/lib/src/main/java/io/ably/lib/types/PresenceMessage.java
+++ b/lib/src/main/java/io/ably/lib/types/PresenceMessage.java
@@ -70,10 +70,10 @@ public PresenceMessage(Action action, String clientId, Object data) {
* @return string
*/
public String toString() {
- StringBuilder result = new StringBuilder("[PresenceMessage");
+ StringBuilder result = new StringBuilder("{PresenceMessage");
super.getDetails(result);
result.append(" action=").append(action.name());
- result.append(']');
+ result.append('}');
return result.toString();
}
From 872859ff99ab1526bd47bac1df3919593ee479c4 Mon Sep 17 00:00:00 2001
From: Martin Morek
Date: Mon, 12 Jul 2021 17:15:07 +0200
Subject: [PATCH 082/899] Added missing documentation
---
.../main/java/io/ably/lib/push/Storage.java | 27 ++++++++++++++++++-
1 file changed, 26 insertions(+), 1 deletion(-)
diff --git a/lib/src/main/java/io/ably/lib/push/Storage.java b/lib/src/main/java/io/ably/lib/push/Storage.java
index dfc2bd4ca..5fcbafc92 100644
--- a/lib/src/main/java/io/ably/lib/push/Storage.java
+++ b/lib/src/main/java/io/ably/lib/push/Storage.java
@@ -4,17 +4,42 @@
/**
* Interface for an entity that supplies key value store
- * - methods getString and getInt have to return default value if requested key is not found
*/
public interface Storage {
+ /**
+ * Insert string value in to storage
+ * @param key name under which value is stored
+ * @param value stored string value
+ */
void putString(String key, String value);
+ /**
+ * Insert integer value in to storage
+ * @param key name after which value is stored
+ * @param value stored integer value
+ */
void putInt(String key, int value);
+ /**
+ * Returns string value based on key from storage
+ * @param key name under value is stored
+ * @param defaultValue value which is returned if key is not found
+ * @return value stored under key or default value if key is not found
+ */
String getString(String key, String defaultValue);
+ /**
+ * Returns integer value based on key from storage
+ * @param key name under value is stored
+ * @param defaultValue value which is returned if key is not found
+ * @return value stored under key or default value if key is not found
+ */
int getInt(String key, int defaultValue);
+ /**
+ * Removes fields from storage
+ * @param fields array of keys which values should be removed from storage
+ */
void clear(Field[] fields);
}
From 541e822ddc89ba6c4cb094faaeff1538f74950b1 Mon Sep 17 00:00:00 2001
From: Martin Morek
Date: Fri, 16 Jul 2021 16:24:08 +0200
Subject: [PATCH 083/899] Updated Firebase cloud messaging dependency
---
android/build.gradle | 2 +-
.../main/java/io/ably/lib/push/ActivationContext.java | 11 +++++------
.../java/io/ably/lib/push/ActivationStateMachine.java | 5 +----
3 files changed, 7 insertions(+), 11 deletions(-)
diff --git a/android/build.gradle b/android/build.gradle
index c2037065a..7c9d2fda6 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -84,7 +84,7 @@ tasks.withType(com.android.build.gradle.internal.tasks.AndroidTestTask) { task -
apply from: '../dependencies.gradle'
apply from: './dependencies.gradle'
dependencies {
- implementation 'com.google.firebase:firebase-messaging:17.3.4'
+ implementation 'com.google.firebase:firebase-messaging:22.0.0'
androidTestImplementation 'com.android.support.test:runner:0.5'
androidTestImplementation 'com.android.support.test:rules:0.5'
androidTestImplementation 'com.crittercism.dexmaker:dexmaker:1.4'
diff --git a/android/src/main/java/io/ably/lib/push/ActivationContext.java b/android/src/main/java/io/ably/lib/push/ActivationContext.java
index c89e2f287..c70f4280e 100644
--- a/android/src/main/java/io/ably/lib/push/ActivationContext.java
+++ b/android/src/main/java/io/ably/lib/push/ActivationContext.java
@@ -5,9 +5,8 @@
import android.preference.PreferenceManager;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
-import com.google.firebase.FirebaseApp;
-import com.google.firebase.iid.FirebaseInstanceId;
-import com.google.firebase.iid.InstanceIdResult;
+import com.google.firebase.installations.FirebaseInstallations;
+import com.google.firebase.installations.InstallationTokenResult;
import io.ably.lib.rest.AblyRest;
import io.ably.lib.types.AblyException;
import io.ably.lib.types.Callback;
@@ -151,10 +150,10 @@ public static ActivationContext getActivationContext(Context applicationContext,
protected void getRegistrationToken(final Callback callback) {
Log.v(TAG, "getRegistrationToken(): callback=" + callback);
- FirebaseInstanceId.getInstance().getInstanceId()
- .addOnCompleteListener(new OnCompleteListener() {
+ FirebaseInstallations.getInstance().getToken(true)
+ .addOnCompleteListener(new OnCompleteListener() {
@Override
- public void onComplete(Task task) {
+ public void onComplete(Task task) {
Log.v(TAG, "getRegistrationToken(): firebase called onComplete(): task=" + task);
if(task.isSuccessful()) {
/* Get new Instance ID token */
diff --git a/android/src/main/java/io/ably/lib/push/ActivationStateMachine.java b/android/src/main/java/io/ably/lib/push/ActivationStateMachine.java
index d478b4549..9c397f716 100644
--- a/android/src/main/java/io/ably/lib/push/ActivationStateMachine.java
+++ b/android/src/main/java/io/ably/lib/push/ActivationStateMachine.java
@@ -5,11 +5,8 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
-import android.support.v4.content.LocalBroadcastManager;
-import com.google.android.gms.tasks.OnCompleteListener;
-import com.google.android.gms.tasks.Task;
-import com.google.firebase.iid.InstanceIdResult;
+import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import com.google.gson.JsonObject;
import java.lang.reflect.Constructor;
From 2e870ac7a94225ada965fe5d6e962325d8b493f0 Mon Sep 17 00:00:00 2001
From: Kacper Kluka
Date: Mon, 19 Jul 2021 08:52:09 +0200
Subject: [PATCH 084/899] Add JRE for the Java platform agent
---
.../main/java/io/ably/lib/util/JavaPlatformAgentProvider.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/java/src/main/java/io/ably/lib/util/JavaPlatformAgentProvider.java b/java/src/main/java/io/ably/lib/util/JavaPlatformAgentProvider.java
index 6d88c6b4d..e3fa31035 100644
--- a/java/src/main/java/io/ably/lib/util/JavaPlatformAgentProvider.java
+++ b/java/src/main/java/io/ably/lib/util/JavaPlatformAgentProvider.java
@@ -3,6 +3,6 @@
public class JavaPlatformAgentProvider implements PlatformAgentProvider {
@Override
public String createPlatformAgent() {
- return null;
+ return "jre" + AgentHeaderCreator.AGENT_DIVIDER + System.getProperty("java.version");
}
}
From 7eb7cacd039f68fe216e490b40b6c97153a40904 Mon Sep 17 00:00:00 2001
From: Martin Morek
Date: Fri, 23 Jul 2021 14:55:40 +0200
Subject: [PATCH 085/899] Removed reflection forName method and replaced by
switch constructing Event object based on name
---
.../ably/lib/push/ActivationStateMachine.java | 82 +++++++++++++------
1 file changed, 56 insertions(+), 26 deletions(-)
diff --git a/android/src/main/java/io/ably/lib/push/ActivationStateMachine.java b/android/src/main/java/io/ably/lib/push/ActivationStateMachine.java
index d478b4549..5ded00ef5 100644
--- a/android/src/main/java/io/ably/lib/push/ActivationStateMachine.java
+++ b/android/src/main/java/io/ably/lib/push/ActivationStateMachine.java
@@ -6,25 +6,27 @@
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.support.v4.content.LocalBroadcastManager;
-
-import com.google.android.gms.tasks.OnCompleteListener;
-import com.google.android.gms.tasks.Task;
-import com.google.firebase.iid.InstanceIdResult;
import com.google.gson.JsonObject;
-
-import java.lang.reflect.Constructor;
-import java.lang.reflect.Field;
-import java.util.ArrayDeque;
-
import com.google.gson.JsonPrimitive;
-import io.ably.lib.http.*;
+import io.ably.lib.http.Http;
+import io.ably.lib.http.HttpCore;
+import io.ably.lib.http.HttpScheduler;
+import io.ably.lib.http.HttpUtils;
import io.ably.lib.rest.AblyRest;
import io.ably.lib.rest.DeviceDetails;
-import io.ably.lib.types.*;
+import io.ably.lib.types.AblyException;
+import io.ably.lib.types.Callback;
+import io.ably.lib.types.ErrorInfo;
+import io.ably.lib.types.Param;
+import io.ably.lib.types.RegistrationToken;
import io.ably.lib.util.IntentUtils;
import io.ably.lib.util.Log;
import io.ably.lib.util.Serialisation;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.util.ArrayDeque;
+
public class ActivationStateMachine {
public static class CalledActivate extends ActivationStateMachine.Event {
public static ActivationStateMachine.CalledActivate useCustomRegistrar(boolean useCustomRegistrar, SharedPreferences prefs) {
@@ -67,7 +69,44 @@ public static class DeregistrationFailed extends ActivationStateMachine.ErrorEve
public DeregistrationFailed(ErrorInfo reason) { super(reason); }
}
- public abstract static class Event {}
+ public abstract static class Event {
+ public static Event constructEventByName(String className) throws ClassNotFoundException, InstantiationException {
+ ActivationStateMachine.Event event;
+
+ switch (className) {
+ case "CalledActivate":
+ event = new CalledActivate();
+ break;
+ case "CalledDeactivate":
+ event = new CalledDeactivate();
+ break;
+ case "GotPushDeviceDetails":
+ event = new GotPushDeviceDetails();
+ break;
+ case "RegistrationSynced":
+ event = new RegistrationSynced();
+ break;
+ case "Deregistered":
+ event = new Deregistered();
+ break;
+
+ // We aren't properly persisting events with a non-nullary constructor. Those events
+ // are supposed to be handled by states that aren't persisted (until
+ // https://github.com/ably/ably-java/issues/546 is fixed), so it should be safe to
+ // just drop them.
+ case "GotDeviceRegistration":
+ case "GettingDeviceRegistrationFailed":
+ case "GettingPushDeviceDetailsFailed":
+ case "SyncRegistrationFailed":
+ case "DeregistrationFailed":
+ case "ErrorEvent":
+ throw new InstantiationException(String.format("%s has non-nullary constructor", className));
+ default:
+ throw new ClassNotFoundException(String.format("%s class cannot be found", className));
+ }
+ return event;
+ }
+ }
public abstract static class ErrorEvent extends ActivationStateMachine.Event {
public final ErrorInfo reason;
@@ -678,21 +717,12 @@ private ArrayDeque getPersistedPendingEvents() {
for (int i = 0; i < length; i++) {
try {
String className = activationContext.getPreferences().getString(String.format("%s[%d]", ActivationStateMachine.PersistKeys.PENDING_EVENTS_PREFIX, i), "");
- Class