diff --git a/android/src/main/java/io/ably/flutter/plugin/AblyEventStreamHandler.java b/android/src/main/java/io/ably/flutter/plugin/AblyEventStreamHandler.java index 3d44a0092..75c3ef97c 100644 --- a/android/src/main/java/io/ably/flutter/plugin/AblyEventStreamHandler.java +++ b/android/src/main/java/io/ably/flutter/plugin/AblyEventStreamHandler.java @@ -5,7 +5,9 @@ import java.util.Map; +import io.ably.flutter.plugin.dto.EnrichedConnectionStateChange; import io.ably.flutter.plugin.generated.PlatformConstants; +import io.ably.lib.realtime.AblyRealtime; import io.ably.lib.realtime.Channel; import io.ably.lib.realtime.ChannelStateListener; import io.ably.lib.realtime.ConnectionStateListener; @@ -76,12 +78,15 @@ static private class Listener { static private class PluginConnectionStateListener extends Listener implements ConnectionStateListener { - PluginConnectionStateListener(EventChannel.EventSink eventSink) { + private final AblyRealtime realtime; + + PluginConnectionStateListener(EventChannel.EventSink eventSink, AblyRealtime realtime) { super(eventSink); + this.realtime = realtime; } public void onConnectionStateChanged(ConnectionStateChange stateChange) { - eventSink.success(stateChange); + eventSink.success(new EnrichedConnectionStateChange(stateChange, realtime.connection.id, realtime.connection.key)); } } @@ -132,8 +137,9 @@ public void onListen(Object object, EventChannel.EventSink uiThreadEventSink) { try { switch (eventName) { case PlatformConstants.PlatformMethod.onRealtimeConnectionStateChanged: - connectionStateListener = new PluginConnectionStateListener(eventSink); - instanceStore.getRealtime(ablyMessage.handle).connection.on(connectionStateListener); + final AblyRealtime realtime = instanceStore.getRealtime(ablyMessage.handle); + connectionStateListener = new PluginConnectionStateListener(eventSink, realtime); + realtime.connection.on(connectionStateListener); break; case PlatformConstants.PlatformMethod.onRealtimeChannelStateChanged: assert eventPayload != null : "onRealtimeChannelStateChanged: event message is missing"; 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 53c0af1aa..5f3e6fcb9 100644 --- a/android/src/main/java/io/ably/flutter/plugin/AblyMessageCodec.java +++ b/android/src/main/java/io/ably/flutter/plugin/AblyMessageCodec.java @@ -17,6 +17,7 @@ import java.util.HashMap; import java.util.Map; +import io.ably.flutter.plugin.dto.EnrichedConnectionStateChange; import io.ably.flutter.plugin.generated.PlatformConstants; import io.ably.flutter.plugin.types.SerializationException; import io.ably.flutter.plugin.types.PlatformClientOptions; @@ -30,7 +31,6 @@ import io.ably.lib.realtime.ChannelStateListener; import io.ably.lib.realtime.ConnectionEvent; import io.ably.lib.realtime.ConnectionState; -import io.ably.lib.realtime.ConnectionStateListener; import io.ably.lib.rest.Auth; import io.ably.lib.rest.Auth.TokenDetails; import io.ably.lib.rest.DeviceDetails; @@ -186,7 +186,7 @@ private Byte getType(Object value) { return PlatformConstants.CodecTypes.tokenParams; } else if (value instanceof AsyncPaginatedResult) { return PlatformConstants.CodecTypes.paginatedResult; - } else if (value instanceof ConnectionStateListener.ConnectionStateChange) { + } else if (value instanceof EnrichedConnectionStateChange) { return PlatformConstants.CodecTypes.connectionStateChange; } else if (value instanceof ChannelStateListener.ChannelStateChange) { return PlatformConstants.CodecTypes.channelStateChange; @@ -821,14 +821,16 @@ private Map encodePaginatedResult(AsyncPaginatedResult c return jsonMap; } - private Map encodeConnectionStateChange(ConnectionStateListener.ConnectionStateChange c) { + private Map encodeConnectionStateChange(EnrichedConnectionStateChange c) { if (c == null) return null; final HashMap jsonMap = new HashMap<>(); - writeValueToJson(jsonMap, PlatformConstants.TxConnectionStateChange.current, encodeConnectionState(c.current)); - writeValueToJson(jsonMap, PlatformConstants.TxConnectionStateChange.previous, encodeConnectionState(c.previous)); - writeValueToJson(jsonMap, PlatformConstants.TxConnectionStateChange.event, encodeConnectionEvent(c.event)); - writeValueToJson(jsonMap, PlatformConstants.TxConnectionStateChange.retryIn, c.retryIn); - writeValueToJson(jsonMap, PlatformConstants.TxConnectionStateChange.reason, encodeErrorInfo(c.reason)); + writeValueToJson(jsonMap, PlatformConstants.TxConnectionStateChange.current, encodeConnectionState(c.stateChange.current)); + writeValueToJson(jsonMap, PlatformConstants.TxConnectionStateChange.previous, encodeConnectionState(c.stateChange.previous)); + writeValueToJson(jsonMap, PlatformConstants.TxConnectionStateChange.event, encodeConnectionEvent(c.stateChange.event)); + writeValueToJson(jsonMap, PlatformConstants.TxConnectionStateChange.retryIn, c.stateChange.retryIn); + writeValueToJson(jsonMap, PlatformConstants.TxConnectionStateChange.reason, encodeErrorInfo(c.stateChange.reason)); + writeValueToJson(jsonMap, PlatformConstants.TxConnectionStateChange.connectionId, c.connectionId); + writeValueToJson(jsonMap, PlatformConstants.TxConnectionStateChange.connectionKey,c.connectionKey); return jsonMap; } diff --git a/android/src/main/java/io/ably/flutter/plugin/dto/EnrichedConnectionStateChange.java b/android/src/main/java/io/ably/flutter/plugin/dto/EnrichedConnectionStateChange.java new file mode 100644 index 000000000..dbee3bbcf --- /dev/null +++ b/android/src/main/java/io/ably/flutter/plugin/dto/EnrichedConnectionStateChange.java @@ -0,0 +1,15 @@ +package io.ably.flutter.plugin.dto; + +import io.ably.lib.realtime.ConnectionStateListener; + +public class EnrichedConnectionStateChange { + public final ConnectionStateListener.ConnectionStateChange stateChange; + public final String connectionId; + public final String connectionKey; + + public EnrichedConnectionStateChange(ConnectionStateListener.ConnectionStateChange stateChange, String connectionId, String connectionKey) { + this.stateChange = stateChange; + this.connectionId = connectionId; + this.connectionKey = connectionKey; + } +} diff --git a/android/src/main/java/io/ably/flutter/plugin/generated/PlatformConstants.java b/android/src/main/java/io/ably/flutter/plugin/generated/PlatformConstants.java index 18be4726a..0daae8124 100644 --- a/android/src/main/java/io/ably/flutter/plugin/generated/PlatformConstants.java +++ b/android/src/main/java/io/ably/flutter/plugin/generated/PlatformConstants.java @@ -309,6 +309,8 @@ static final public class TxConnectionStateChange { public static final String event = "event"; public static final String retryIn = "retryIn"; public static final String reason = "reason"; + public static final String connectionId = "connectionId"; + public static final String connectionKey = "connectionKey"; } static final public class TxChannelStateChange { diff --git a/example/lib/ui/realtime_sliver.dart b/example/lib/ui/realtime_sliver.dart index de6310e95..0481c9175 100644 --- a/example/lib/ui/realtime_sliver.dart +++ b/example/lib/ui/realtime_sliver.dart @@ -133,14 +133,16 @@ class RealtimeSliver extends HookWidget { ); Widget buildReleaseRealtimeChannelButton( - ValueNotifier connectionState, - ValueNotifier channelState) => + ValueNotifier connectionState, + ValueNotifier channelState, + ValueNotifier connectionId, + ) => TextButton( onPressed: () async { await channel.detach(); realtime.channels.release(Constants.channelName); channel = realtime.channels.get(Constants.channelName); - setupListeners(connectionState, channelState); + setupListeners(connectionState, channelState, connectionId); }, child: const Text('Release'), ); @@ -176,6 +178,7 @@ class RealtimeSliver extends HookWidget { Widget build(BuildContext context) { final connectionState = useState(realtime.connection.state); + final connectionId = useState(realtime.connection.id); final channelState = useState(channel.state); final latestMessage = useState(null); final channelSubscription = @@ -185,7 +188,7 @@ class RealtimeSliver extends HookWidget { useEffect(() { realtime.time().then((value) => realtimeTime.value = value); - setupListeners(connectionState, channelState); + setupListeners(connectionState, channelState, connectionId); return dispose; }, []); @@ -198,6 +201,7 @@ class RealtimeSliver extends HookWidget { ), Text('Realtime time: ${realtimeTime.value}'), Text('Connection State: ${connectionState.value}'), + Text('Connection Id: ${connectionId.value ?? '-'}'), buildEncryptionSwitch(useEncryption), Row( children: [ @@ -222,7 +226,7 @@ class RealtimeSliver extends HookWidget { Expanded(child: buildChannelDetachButton(channelState.value)), Expanded( child: buildReleaseRealtimeChannelButton( - connectionState, channelState)), + connectionState, channelState, connectionId)), ], ), Row( @@ -286,8 +290,10 @@ class RealtimeSliver extends HookWidget { ); } - void setupListeners(ValueNotifier connectionState, - ValueNotifier channelState) { + void setupListeners( + ValueNotifier connectionState, + ValueNotifier channelState, + ValueNotifier connectionId) { dispose(); final connectionSubscription = realtime.connection.on().listen((connectionStateChange) { @@ -295,6 +301,7 @@ class RealtimeSliver extends HookWidget { logAndDisplayError(connectionStateChange.reason); } connectionState.value = connectionStateChange.current; + connectionId.value = realtime.connection.id; print('${DateTime.now()}:' ' ConnectionStateChange event: ${connectionStateChange.event}' '\nReason: ${connectionStateChange.reason}'); diff --git a/ios/Classes/AblyFlutterStreamHandler.h b/ios/Classes/AblyFlutterStreamHandler.h index 734571aa3..739c0a55c 100644 --- a/ios/Classes/AblyFlutterStreamHandler.h +++ b/ios/Classes/AblyFlutterStreamHandler.h @@ -2,8 +2,20 @@ @import Flutter; #import "AblyFlutter.h" +@class ARTConnectionStateChange; + NS_ASSUME_NONNULL_BEGIN +@interface _AblyConnectionStateChange : NSObject + +@property(nonatomic, readonly) ARTConnectionStateChange *value; +@property(nonatomic, readonly) NSString *connectionId; +@property(nonatomic, readonly) NSString *connectionKey; + +- (instancetype)initWithStateChange:(ARTConnectionStateChange *)stateChange connectionId:(NSString *)connectionId connectionKey:(NSString *)connectionKey; + +@end + @interface AblyFlutterStreamHandler : NSObject @property(nonatomic, readonly) AblyInstanceStore * instanceStore; diff --git a/ios/Classes/AblyFlutterStreamHandler.m b/ios/Classes/AblyFlutterStreamHandler.m index 081351d57..a8d0cd4db 100644 --- a/ios/Classes/AblyFlutterStreamHandler.m +++ b/ios/Classes/AblyFlutterStreamHandler.m @@ -5,6 +5,20 @@ #import "AblyFlutterMessage.h" #import "codec/AblyPlatformConstants.h" +@implementation _AblyConnectionStateChange + +- (instancetype)initWithStateChange:(ARTConnectionStateChange *)stateChange connectionId:(NSString *)connectionId connectionKey:(NSString *)connectionKey { + self = [super init]; + if (self) { + _value = stateChange; + _connectionId = connectionId; + _connectionKey = connectionKey; + } + return self; +} + +@end + @implementation AblyFlutterStreamHandler{ ARTEventListener *listener; } @@ -42,7 +56,8 @@ - (void) startListening:(AblyFlutterMessage *const)message emitter:(FlutterEvent details:eventName ]); } else { - emitter(stateChange); + _AblyConnectionStateChange * const _stateChange = [[_AblyConnectionStateChange alloc] initWithStateChange:stateChange connectionId:realtime.connection.id connectionKey:realtime.connection.key]; + emitter(_stateChange); } }]; } else if ([AblyPlatformMethod_onRealtimeChannelStateChanged isEqual: eventName]) { diff --git a/ios/Classes/codec/AblyFlutterWriter.m b/ios/Classes/codec/AblyFlutterWriter.m index b5caf9c56..b2427091a 100644 --- a/ios/Classes/codec/AblyFlutterWriter.m +++ b/ios/Classes/codec/AblyFlutterWriter.m @@ -5,6 +5,7 @@ #import "AblyFlutterMessage.h" #import "AblyFlutterReader.h" #import "AblyPlatformConstants.h" +#import "AblyFlutterStreamHandler.h" NS_ASSUME_NONNULL_BEGIN @@ -21,7 +22,7 @@ + (UInt8) getType:(id)value{ return CodecTypeAblyMessage; }else if([value isKindOfClass:[ARTErrorInfo class]]){ return CodecTypeErrorInfo; - }else if([value isKindOfClass:[ARTConnectionStateChange class]]){ + } else if([value isKindOfClass:[_AblyConnectionStateChange class]]){ return CodecTypeConnectionStateChange; }else if([value isKindOfClass:[ARTChannelStateChange class]]){ return CodecTypeChannelStateChange; @@ -207,19 +208,21 @@ +(NSString *) encodeChannelEvent: (ARTChannelEvent) event { } } -static AblyCodecEncoder encodeConnectionStateChange = ^NSMutableDictionary*(ARTConnectionStateChange *const stateChange) { +static AblyCodecEncoder encodeConnectionStateChange = ^NSMutableDictionary*(_AblyConnectionStateChange *const stateChange) { NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init]; WRITE_VALUE(dictionary, TxConnectionStateChange_current, - [AblyFlutterWriter encodeConnectionState: [stateChange current]]); + [AblyFlutterWriter encodeConnectionState: [stateChange.value current]]); WRITE_VALUE(dictionary, TxConnectionStateChange_previous, - [AblyFlutterWriter encodeConnectionState: [stateChange previous]]); + [AblyFlutterWriter encodeConnectionState: [stateChange.value previous]]); WRITE_VALUE(dictionary, TxConnectionStateChange_event, - [AblyFlutterWriter encodeConnectionEvent: [stateChange event]]); - WRITE_VALUE(dictionary, TxConnectionStateChange_retryIn, [stateChange retryIn]?@((int)([stateChange retryIn] * 1000)):nil); - WRITE_VALUE(dictionary, TxConnectionStateChange_reason, encodeErrorInfo([stateChange reason])); + [AblyFlutterWriter encodeConnectionEvent: [stateChange.value event]]); + WRITE_VALUE(dictionary, TxConnectionStateChange_retryIn, [stateChange.value retryIn]?@((int)([stateChange.value retryIn] * 1000)):nil); + WRITE_VALUE(dictionary, TxConnectionStateChange_reason, encodeErrorInfo([stateChange.value reason])); + WRITE_VALUE(dictionary, TxConnectionStateChange_connectionId, stateChange.connectionId); + WRITE_VALUE(dictionary, TxConnectionStateChange_connectionKey, stateChange.connectionKey); return dictionary; }; diff --git a/ios/Classes/codec/AblyPlatformConstants.h b/ios/Classes/codec/AblyPlatformConstants.h index ba6253f10..c49d316e4 100644 --- a/ios/Classes/codec/AblyPlatformConstants.h +++ b/ios/Classes/codec/AblyPlatformConstants.h @@ -283,6 +283,8 @@ extern NSString *const TxConnectionStateChange_previous; extern NSString *const TxConnectionStateChange_event; extern NSString *const TxConnectionStateChange_retryIn; extern NSString *const TxConnectionStateChange_reason; +extern NSString *const TxConnectionStateChange_connectionId; +extern NSString *const TxConnectionStateChange_connectionKey; // key constants for ChannelStateChange extern NSString *const TxChannelStateChange_current; diff --git a/ios/Classes/codec/AblyPlatformConstants.m b/ios/Classes/codec/AblyPlatformConstants.m index 9f4761111..128c368ed 100644 --- a/ios/Classes/codec/AblyPlatformConstants.m +++ b/ios/Classes/codec/AblyPlatformConstants.m @@ -252,6 +252,8 @@ NSString *const TxConnectionStateChange_event = @"event"; NSString *const TxConnectionStateChange_retryIn = @"retryIn"; NSString *const TxConnectionStateChange_reason = @"reason"; +NSString *const TxConnectionStateChange_connectionId = @"connectionId"; +NSString *const TxConnectionStateChange_connectionKey = @"connectionKey"; // key constants for ChannelStateChange NSString *const TxChannelStateChange_current = @"current"; diff --git a/lib/src/generated/platform_constants.dart b/lib/src/generated/platform_constants.dart index e8f113192..35c47bb76 100644 --- a/lib/src/generated/platform_constants.dart +++ b/lib/src/generated/platform_constants.dart @@ -311,6 +311,8 @@ class TxConnectionStateChange { static const String event = 'event'; static const String retryIn = 'retryIn'; static const String reason = 'reason'; + static const String connectionId = 'connectionId'; + static const String connectionKey = 'connectionKey'; } class TxChannelStateChange { diff --git a/lib/src/platform/src/codec.dart b/lib/src/platform/src/codec.dart index 5f65dc194..dcd4e41fe 100644 --- a/lib/src/platform/src/codec.dart +++ b/lib/src/platform/src/codec.dart @@ -129,7 +129,8 @@ class Codec extends StandardMessageCodec { // Events - Connection CodecTypes.connectionStateChange: - _CodecPair(null, _decodeConnectionStateChange), + _CodecPair( + null, _decodeConnectionStateChange), // Events - Channel CodecTypes.channelStateChange: @@ -1096,9 +1097,14 @@ class Codec extends StandardMessageCodec { /// @nodoc /// Decodes value [jsonMap] to [ConnectionStateChange] /// returns null if [jsonMap] is null - ConnectionStateChange _decodeConnectionStateChange( + EnrichedConnectionStateChange _decodeConnectionStateChange( Map jsonMap, ) { + final connectionId = + _readFromJson(jsonMap, TxConnectionStateChange.connectionId); + final connectionKey = + _readFromJson(jsonMap, TxConnectionStateChange.connectionKey); + final current = _decodeConnectionState( _readFromJson(jsonMap, TxConnectionStateChange.current)); final previous = _decodeConnectionState( @@ -1112,12 +1118,16 @@ class Codec extends StandardMessageCodec { TxConnectionStateChange.reason, )); final reason = (errorInfo == null) ? null : _decodeErrorInfo(errorInfo); - return ConnectionStateChange( - current: current, - previous: previous, - event: event, - retryIn: retryIn, - reason: reason, + return EnrichedConnectionStateChange( + stateChange: ConnectionStateChange( + current: current, + previous: previous, + event: event, + retryIn: retryIn, + reason: reason, + ), + connectionId: connectionId, + connectionKey: connectionKey, ); } diff --git a/lib/src/platform/src/realtime/connection.dart b/lib/src/platform/src/realtime/connection.dart index 26881be46..0777555d0 100644 --- a/lib/src/platform/src/realtime/connection.dart +++ b/lib/src/platform/src/realtime/connection.dart @@ -13,6 +13,10 @@ class Connection extends PlatformObject { ErrorInfo? _errorReason; + String? _id; + + String? _key; + /// @nodoc /// Instantiates a connection with [realtime] client instance. /// @@ -21,9 +25,11 @@ class Connection extends PlatformObject { Connection(this.realtime) : _state = ConnectionState.initialized, super() { - on().listen((event) { - _state = event.current; - _errorReason = event.reason; + _onConnectionStateChange().listen((event) { + _state = event.stateChange.current; + _errorReason = event.stateChange.reason; + _id = event.connectionId; + _key = event.connectionKey; }); } @@ -37,7 +43,7 @@ class Connection extends PlatformObject { /// A unique public identifier for this connection, used to identify this /// member. - String? id; + String? get id => _id; /// A unique private connection key used to recover or resume a connection, /// assigned by Ably. @@ -48,7 +54,7 @@ class Connection extends PlatformObject { /// to publish on behalf of this client. See the /// [publishing over REST on behalf of a realtime client docs](https://ably.com/docs/rest/channels#publish-on-behalf) /// for more info. - String? key; + String? get key => _key; /// The recovery key string can be used by another client to recover this /// connection's state in the recover client options property. @@ -69,13 +75,18 @@ class Connection extends PlatformObject { /// The current [ConnectionState] of the connection. ConnectionState get state => _state; + /// @nodoc + Stream _onConnectionStateChange() => + listen( + PlatformMethod.onRealtimeConnectionStateChanged, + ); + /// Stream of connection events with specified [ConnectionEvent] type. Stream on([ConnectionEvent? connectionEvent]) => - listen( - PlatformMethod.onRealtimeConnectionStateChanged, - ).where((connectionStateChange) => - connectionEvent == null || - connectionStateChange.event == connectionEvent); + _onConnectionStateChange().map((event) => event.stateChange).where( + (connectionStateChange) => + connectionEvent == null || + connectionStateChange.event == connectionEvent); /// Causes the connection to close, entering the [ConnectionState.closing] /// state. diff --git a/lib/src/realtime/src/connection_state_change.dart b/lib/src/realtime/src/connection_state_change.dart index 20c81f7b1..06efd7b00 100644 --- a/lib/src/realtime/src/connection_state_change.dart +++ b/lib/src/realtime/src/connection_state_change.dart @@ -35,3 +35,20 @@ class ConnectionStateChange { this.retryIn, }); } + +/// @nodoc +@immutable +class EnrichedConnectionStateChange { + /// @nodoc + final ConnectionStateChange stateChange; + + /// @nodoc + final String? connectionId; + + /// @nodoc + final String? connectionKey; + + /// @nodoc + const EnrichedConnectionStateChange( + {required this.stateChange, this.connectionId, this.connectionKey}); +}