Repository: ably/ably-flutter
Issue: Android only - Returning a TokenDetails with 'issued' and 'expires' values in epoch milliseconds throws java error

Setup:


* Device: Pixel 3XL 
* Android: 11
* Flutter: 2.10.3
* Ably package: 1.2.12

Configuration:
Ably with token authentication

{{authCallback}} property in {{ClientOptions}} pointing to a function that returns a {{TokenDetails}} object. As per documentation the object contains {{issued}} and {{expires}} properties set to an epoch timestamp in milliseconds (along the rest of the properties), for clarification, the following code works flawlessly on iOS, and its equivalent in the Javascript SDK also works correctly.

Code:


{code:dart}
  import 'package:ably_flutter/ably_flutter.dart' as Ably;


  String _clientId = '';
  Future<Ably.TokenDetails> _getTokenDetails(Ably.TokenParams params) async {
    final result = await MyAPIController.getAblyAuthTokenFor(_clientId);

    return Ably.TokenDetails(
      result.data.token,
      issued: result.data.issued,  // result.data.issued is an int with epoch timestamp in milliseconds (e.g.: 1648218289847)
      expires: result.data.expires,  // result.data.expires same as result.data.issued but in the future
      clientId: result.data.clientId,
      capability: result.data.capability,
    );
  }

  void initializeAbly(String clientId) {
    _clientId = clientId;

    final clientOptions = Ably.ClientOptions()
      ..clientId = _clientId
      ..tls = true
      ..useTokenAuth = true
      ..authCallback = _getTokenDetails;

    _realtime = Ably.Realtime(options: clientOptions);
    _realtime.connection
        .on(Ably.ConnectionEvent.connected)
        .listen((Ably.ConnectionStateChange stateChange) {
     // SUBSCRIBE TO CHANNELS
      }
    });
  }
{code}
Error::


{code}
E/MethodChannel#io.ably.flutter.plugin( 3539): Failed to handle method call result
E/MethodChannel#io.ably.flutter.plugin( 3539): java.lang.ClassCastException: java.lang.Long cannot be cast to java.lang.Integer
E/MethodChannel#io.ably.flutter.plugin( 3539): 	at io.ably.flutter.plugin.AblyMessageCodec.lambda$decodeTokenDetails$32(AblyMessageCodec.java:354)
E/MethodChannel#io.ably.flutter.plugin( 3539): 	at io.ably.flutter.plugin.-$$Lambda$AblyMessageCodec$EZVHcAHH13F6P7lENmPZqTXnZg8.accept(Unknown Source:2)
E/MethodChannel#io.ably.flutter.plugin( 3539): 	at io.ably.flutter.plugin.AblyMessageCodec.readValueFromJson(AblyMessageCodec.java:160)
E/MethodChannel#io.ably.flutter.plugin( 3539): 	at io.ably.flutter.plugin.AblyMessageCodec.decodeTokenDetails(AblyMessageCodec.java:354)
E/MethodChannel#io.ably.flutter.plugin( 3539): 	at io.ably.flutter.plugin.AblyMessageCodec.access$2300(AblyMessageCodec.java:47)
E/MethodChannel#io.ably.flutter.plugin( 3539): 	at io.ably.flutter.plugin.AblyMessageCodec$1.lambda$new$5(AblyMessageCodec.java:102)
E/MethodChannel#io.ably.flutter.plugin( 3539): 	at io.ably.flutter.plugin.-$$Lambda$AblyMessageCodec$1$DqnBUFL6jWi43rRYX8P4My0ezrE.decode(Unknown Source:2)
E/MethodChannel#io.ably.flutter.plugin( 3539): 	at io.ably.flutter.plugin.AblyMessageCodec$CodecPair.decode(AblyMessageCodec.java:80)
E/MethodChannel#io.ably.flutter.plugin( 3539): 	at io.ably.flutter.plugin.AblyMessageCodec.readValueOfType(AblyMessageCodec.java:152)
E/MethodChannel#io.ably.flutter.plugin( 3539): 	at io.flutter.plugin.common.StandardMessageCodec.readValue(StandardMessageCodec.java:333)
E/MethodChannel#io.ably.flutter.plugin( 3539): 	at io.flutter.plugin.common.StandardMethodCodec.decodeEnvelope(StandardMethodCodec.java:107)
E/MethodChannel#io.ably.flutter.plugin( 3539): 	at io.flutter.plugin.common.MethodChannel$IncomingResultHandler.reply(MethodChannel.java:239)
E/MethodChannel#io.ably.flutter.plugin( 3539): 	at io.flutter.embedding.engine.dart.DartMessenger.handlePlatformMessageResponse(DartMessenger.java:376)
E/MethodChannel#io.ably.flutter.plugin( 3539): 	at io.flutter.embedding.engine.FlutterJNI.handlePlatformMessageResponse(FlutterJNI.java:993)
E/MethodChannel#io.ably.flutter.plugin( 3539): 	at android.os.MessageQueue.nativePollOnce(Native Method)
E/MethodChannel#io.ably.flutter.plugin( 3539): 	at android.os.MessageQueue.next(MessageQueue.java:335)
E/MethodChannel#io.ably.flutter.plugin( 3539): 	at android.os.Looper.loopOnce(Looper.java:161)
E/MethodChannel#io.ably.flutter.plugin( 3539): 	at android.os.Looper.loop(Looper.java:288)
E/MethodChannel#io.ably.flutter.plugin( 3539): 	at android.app.ActivityThread.main(ActivityThread.java:7842)
E/MethodChannel#io.ably.flutter.plugin( 3539): 	at java.lang.reflect.Method.invoke(Native Method)
E/MethodChannel#io.ably.flutter.plugin( 3539): 	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
E/MethodChannel#io.ably.flutter.plugin( 3539): 	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)
{code}
It seems Java is trying to cast assign a {{long}} value as in {{int}} in the following function (lines 354 and 355):

https://github.com/ably/ably-flutter/blob/4da3baa587165e32f21566144a42361f435aeaf3/android/src/main/java/io/ably/flutter/plugin/AblyMessageCodec.java#L350-L357

In my dart code, when I truncate the the values in {{issued}} and {{expires}} to be epoch in seconds instead of milliseconds the error goes away and ably connects successfully to the server. The token is renewed at the correct time as well.

Thanks

Changed files:
android/src/main/java/io/ably/flutter/plugin/AblyMessageCodec.java

diff --git a/android/src/main/java/io/ably/flutter/plugin/AblyMessageCodec.java b/android/src/main/java/io/ably/flutter/plugin/AblyMessageCodec.java
index 27447133..455a5f3a 100644
--- a/android/src/main/java/io/ably/flutter/plugin/AblyMessageCodec.java
+++ b/android/src/main/java/io/ably/flutter/plugin/AblyMessageCodec.java
@@ -351,8 +351,8 @@ public class AblyMessageCodec extends StandardMessageCodec {
     if (jsonMap == null) return null;
     final TokenDetails o = new TokenDetails();
     readValueFromJson(jsonMap, PlatformConstants.TxTokenDetails.token, v -> o.token = (String) v);
-    readValueFromJson(jsonMap, PlatformConstants.TxTokenDetails.expires, v -> o.expires = (int) v);
-    readValueFromJson(jsonMap, PlatformConstants.TxTokenDetails.issued, v -> o.issued = (int) v);
+    readValueFromJson(jsonMap, PlatformConstants.TxTokenDetails.expires, v -> o.expires = (long) v);
+    readValueFromJson(jsonMap, PlatformConstants.TxTokenDetails.issued, v -> o.issued = (long) v);
     readValueFromJson(jsonMap, PlatformConstants.TxTokenDetails.capability, v -> o.capability = (String) v);
     readValueFromJson(jsonMap, PlatformConstants.TxTokenDetails.clientId, v -> o.clientId = (String) v);