Skip to content

Commit ced191d

Browse files
authored
Merge pull request #994 from ably/ECO-4706/fix-push-corner-cases
[ECO-4706] fix: push notifications corner cases
2 parents f706160 + d44e4f6 commit ced191d

File tree

4 files changed

+163
-9
lines changed

4 files changed

+163
-9
lines changed

android/src/main/java/io/ably/lib/push/ActivationContext.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44
import android.content.SharedPreferences;
55
import android.preference.PreferenceManager;
66

7+
import androidx.annotation.VisibleForTesting;
78
import com.google.firebase.messaging.FirebaseMessaging;
89

910
import java.util.WeakHashMap;
1011

1112
import io.ably.lib.rest.AblyRest;
1213
import io.ably.lib.types.AblyException;
1314
import io.ably.lib.types.Callback;
15+
import io.ably.lib.types.ClientOptions;
1416
import io.ably.lib.types.ErrorInfo;
1517
import io.ably.lib.types.RegistrationToken;
1618
import io.ably.lib.util.Log;
@@ -63,6 +65,8 @@ AblyRest getAbly() throws AblyException {
6365
Log.v(TAG, "getAbly(): returning existing Ably instance");
6466
return ably;
6567
} else {
68+
// In this case, we received a new FCM token while the app is offline,
69+
// so we have to initialize the Ably client to send it to the server.
6670
Log.v(TAG, "getAbly(): creating new Ably instance");
6771
}
6872

@@ -72,9 +76,21 @@ AblyRest getAbly() throws AblyException {
7276
throw AblyException.fromErrorInfo(new ErrorInfo("Unable to get Ably library instance; no device identity token", 40000, 400));
7377
}
7478
Log.v(TAG, "getAbly(): returning Ably instance using deviceIdentityToken");
79+
// TODO: We need to persist Ably client options such as the environment with `deviceIdentityToken` and use these options during initialization.
7580
return (ably = new AblyRest(deviceIdentityToken));
7681
}
7782

83+
/**
84+
* @return AblyRest instance with device identity token auth. We use this instance to perform
85+
* deregistration calls in push activation flow.
86+
*/
87+
AblyRest getDeviceIdentityTokenBasedAblyClient(String deviceIdentityToken) throws AblyException {
88+
ClientOptions clientOptions = ably.options.copy();
89+
clientOptions.clearAuthOptions();
90+
clientOptions.token = deviceIdentityToken;
91+
return new AblyRest(clientOptions);
92+
}
93+
7894
public boolean setClientId(String clientId, boolean propagateGotPushDeviceDetails) {
7995
Log.v(TAG, "setClientId(): clientId=" + clientId + ", propagateGotPushDeviceDetails=" + propagateGotPushDeviceDetails);
8096
boolean updated = !clientId.equals(this.clientId);
@@ -113,6 +129,10 @@ public void onNewRegistrationToken(RegistrationToken.Type type, String token) {
113129
getActivationStateMachine().handleEvent(new ActivationStateMachine.GotPushDeviceDetails());
114130
}
115131

132+
/**
133+
* Should be used in tests only
134+
*/
135+
@VisibleForTesting
116136
public void reset() {
117137
Log.v(TAG, "reset()");
118138

android/src/main/java/io/ably/lib/push/ActivationStateMachine.java

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -227,8 +227,18 @@ public String toString() {
227227

228228
public ActivationStateMachine.State transition(ActivationStateMachine.Event event) {
229229
if (event instanceof ActivationStateMachine.CalledDeactivate) {
230-
machine.callDeactivatedCallback(null);
231-
return this;
230+
LocalDevice device = machine.getDevice();
231+
232+
// RSH3a1c
233+
if (device.isRegistered()) {
234+
machine.deregister();
235+
return new ActivationStateMachine.WaitingForDeregistration(machine, this);
236+
// RSH3a1d
237+
} else {
238+
device.reset();
239+
machine.callDeactivatedCallback(null);
240+
return this;
241+
}
232242
} else if (event instanceof ActivationStateMachine.CalledActivate) {
233243
LocalDevice device = machine.getDevice();
234244

@@ -388,7 +398,6 @@ public ActivationStateMachine.State transition(ActivationStateMachine.Event even
388398
machine.callActivatedCallback(null);
389399
return this;
390400
} else if (event instanceof ActivationStateMachine.CalledDeactivate) {
391-
LocalDevice device = machine.getDevice();
392401
machine.deregister();
393402
return new ActivationStateMachine.WaitingForDeregistration(machine, this);
394403
} else if (event instanceof ActivationStateMachine.GotPushDeviceDetails) {
@@ -742,7 +751,8 @@ private void deregister() {
742751
} else {
743752
final AblyRest ably;
744753
try {
745-
ably = activationContext.getAbly();
754+
// RSH3d2b: use `deviceIdentityToken` to perform request
755+
ably = activationContext.getDeviceIdentityTokenBasedAblyClient(device.deviceIdentityToken);
746756
} catch(AblyException ae) {
747757
ErrorInfo reason = ae.errorInfo;
748758
Log.e(TAG, "exception registering " + device.id + ": " + reason.toString());
@@ -751,9 +761,11 @@ private void deregister() {
751761
}
752762
ably.http.request(new Http.Execute<Void>() {
753763
@Override
754-
public void execute(HttpScheduler http, Callback<Void> callback) throws AblyException {
764+
public void execute(HttpScheduler http, Callback<Void> callback) {
755765
Param[] params = ParamsUtils.enrichParams(new Param[0], ably.options);
756-
http.del("/push/deviceRegistrations/" + device.id, ably.push.pushRequestHeaders(true), params, null, true, callback);
766+
Param[] headers = HttpUtils.defaultAcceptHeaders(ably.options.useBinaryProtocol);
767+
final Param[] deviceIdentityHeaders = device.deviceIdentityHeaders();
768+
http.del("/push/deviceRegistrations/" + device.id, HttpUtils.mergeHeaders(headers, deviceIdentityHeaders), params, null, true, callback);
757769
}
758770
}).async(new Callback<Void>() {
759771
@Override
@@ -763,8 +775,14 @@ public void onSuccess(Void response) {
763775
}
764776
@Override
765777
public void onError(ErrorInfo reason) {
766-
Log.e(TAG, "error deregistering " + device.id + ": " + reason.toString());
767-
handleEvent(new ActivationStateMachine.DeregistrationFailed(reason));
778+
// RSH3d2c1: ignore unauthorized or invalid credentials errors
779+
if (reason.statusCode == 401 || reason.code == 40005) {
780+
Log.w(TAG, "unauthorized error during deregistration " + device.id + ": " + reason);
781+
handleEvent(new ActivationStateMachine.Deregistered());
782+
} else {
783+
Log.e(TAG, "error deregistering " + device.id + ": " + reason);
784+
handleEvent(new ActivationStateMachine.DeregistrationFailed(reason));
785+
}
768786
}
769787
});
770788
}

lib/src/main/java/io/ably/lib/debug/DebugOptions.java

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,53 @@ public interface RawHttpListener {
3131
public RawProtocolListener protocolListener;
3232
public RawHttpListener httpListener;
3333
public ITransport.Factory transportFactory;
34+
35+
public DebugOptions copy() {
36+
DebugOptions copied = new DebugOptions();
37+
copied.protocolListener = protocolListener;
38+
copied.httpListener = httpListener;
39+
copied.transportFactory = transportFactory;
40+
copied.clientId = clientId;
41+
copied.logLevel = logLevel;
42+
copied.logHandler = logHandler;
43+
copied.tls = tls;
44+
copied.restHost = restHost;
45+
copied.realtimeHost = realtimeHost;
46+
copied.port = port;
47+
copied.tlsPort = tlsPort;
48+
copied.autoConnect = autoConnect;
49+
copied.useBinaryProtocol = useBinaryProtocol;
50+
copied.queueMessages = queueMessages;
51+
copied.echoMessages = echoMessages;
52+
copied.recover = recover;
53+
copied.proxy = proxy;
54+
copied.environment = environment;
55+
copied.idempotentRestPublishing = idempotentRestPublishing;
56+
copied.httpOpenTimeout = httpOpenTimeout;
57+
copied.httpRequestTimeout = httpRequestTimeout;
58+
copied.httpMaxRetryDuration = httpMaxRetryDuration;
59+
copied.httpMaxRetryCount = httpMaxRetryCount;
60+
copied.realtimeRequestTimeout = realtimeRequestTimeout;
61+
copied.disconnectedRetryTimeout = disconnectedRetryTimeout;
62+
copied.suspendedRetryTimeout = suspendedRetryTimeout;
63+
copied.fallbackHostsUseDefault = fallbackHostsUseDefault;
64+
copied.fallbackRetryTimeout = fallbackRetryTimeout;
65+
copied.defaultTokenParams = defaultTokenParams;
66+
copied.channelRetryTimeout = channelRetryTimeout;
67+
copied.asyncHttpThreadpoolSize = asyncHttpThreadpoolSize;
68+
copied.pushFullWait = pushFullWait;
69+
copied.localStorage = localStorage;
70+
copied.addRequestIds = addRequestIds;
71+
copied.authCallback = authCallback;
72+
copied.authUrl = authUrl;
73+
copied.authMethod = authMethod;
74+
copied.key = key;
75+
copied.token = token;
76+
copied.tokenDetails = tokenDetails;
77+
copied.authHeaders = authHeaders;
78+
copied.authParams = authParams;
79+
copied.queryTime = queryTime;
80+
copied.useTokenAuth = useTokenAuth;
81+
return copied;
82+
}
3483
}

lib/src/main/java/io/ably/lib/types/ClientOptions.java

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
/**
1313
* Passes additional client-specific properties to the {@link io.ably.lib.rest.AblyRest} or the {@link io.ably.lib.realtime.AblyRealtime}.
14-
*
14+
* <p>
1515
* Extends an {@link AuthOptions} object.
1616
* <p>
1717
* Spec: TO3j
@@ -25,6 +25,7 @@ public ClientOptions() {}
2525

2626
/**
2727
* Creates a ClientOptions instance used to configure Rest and Realtime clients
28+
*
2829
* @param key the key obtained from the application dashboard.
2930
* @throws AblyException if the key is not in a valid format
3031
*/
@@ -322,4 +323,70 @@ public ClientOptions(String key) throws AblyException {
322323
* Spec: RSC7d6
323324
*/
324325
public Map<String, String> agents;
326+
327+
/**
328+
* Internal method
329+
*
330+
* @return copy of client options
331+
*/
332+
public ClientOptions copy() {
333+
ClientOptions copied = new ClientOptions();
334+
copied.clientId = clientId;
335+
copied.logLevel = logLevel;
336+
copied.logHandler = logHandler;
337+
copied.tls = tls;
338+
copied.restHost = restHost;
339+
copied.realtimeHost = realtimeHost;
340+
copied.port = port;
341+
copied.tlsPort = tlsPort;
342+
copied.autoConnect = autoConnect;
343+
copied.useBinaryProtocol = useBinaryProtocol;
344+
copied.queueMessages = queueMessages;
345+
copied.echoMessages = echoMessages;
346+
copied.recover = recover;
347+
copied.proxy = proxy;
348+
copied.environment = environment;
349+
copied.idempotentRestPublishing = idempotentRestPublishing;
350+
copied.httpOpenTimeout = httpOpenTimeout;
351+
copied.httpRequestTimeout = httpRequestTimeout;
352+
copied.httpMaxRetryDuration = httpMaxRetryDuration;
353+
copied.httpMaxRetryCount = httpMaxRetryCount;
354+
copied.realtimeRequestTimeout = realtimeRequestTimeout;
355+
copied.disconnectedRetryTimeout = disconnectedRetryTimeout;
356+
copied.suspendedRetryTimeout = suspendedRetryTimeout;
357+
copied.fallbackHostsUseDefault = fallbackHostsUseDefault;
358+
copied.fallbackRetryTimeout = fallbackRetryTimeout;
359+
copied.defaultTokenParams = defaultTokenParams;
360+
copied.channelRetryTimeout = channelRetryTimeout;
361+
copied.asyncHttpThreadpoolSize = asyncHttpThreadpoolSize;
362+
copied.pushFullWait = pushFullWait;
363+
copied.localStorage = localStorage;
364+
copied.addRequestIds = addRequestIds;
365+
copied.authCallback = authCallback;
366+
copied.authUrl = authUrl;
367+
copied.authMethod = authMethod;
368+
copied.key = key;
369+
copied.token = token;
370+
copied.tokenDetails = tokenDetails;
371+
copied.authHeaders = authHeaders;
372+
copied.authParams = authParams;
373+
copied.queryTime = queryTime;
374+
copied.useTokenAuth = useTokenAuth;
375+
return copied;
376+
}
377+
378+
/**
379+
* Internal method
380+
* <p>
381+
* clears all auth options
382+
*/
383+
public void clearAuthOptions() {
384+
key = null;
385+
token = null;
386+
tokenDetails = null;
387+
authHeaders = null;
388+
authParams = null;
389+
queryTime = false;
390+
useTokenAuth = false;
391+
}
325392
}

0 commit comments

Comments
 (0)