Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Platform encapsulation #235

Merged
merged 14 commits into from
Nov 29, 2021
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ public void onListen(Object object, EventChannel.EventSink uiThreadEventSink) {
ablyLibrary.getRealtime(ablyMessage.handle).connection.on(connectionStateListener);
break;
case PlatformConstants.PlatformMethod.onRealtimeChannelStateChanged:
assert eventPayload != null : "event message is missing";
assert eventPayload != null : "onRealtimeChannelStateChanged: event message is missing";
channelStateListener = new PluginChannelStateListener(eventSink);
ablyLibrary
.getRealtime(ablyMessage.handle)
Expand All @@ -163,7 +163,7 @@ public void onListen(Object object, EventChannel.EventSink uiThreadEventSink) {
.on(channelStateListener);
break;
case PlatformConstants.PlatformMethod.onRealtimeChannelMessage:
assert eventPayload != null : "event message is missing";
assert eventPayload != null : "onRealtimeChannelMessage: event message is missing";
try {
channelMessageListener = new PluginChannelMessageListener(eventSink);
ablyLibrary
Expand All @@ -176,7 +176,7 @@ public void onListen(Object object, EventChannel.EventSink uiThreadEventSink) {
}
break;
case PlatformConstants.PlatformMethod.onRealtimePresenceMessage:
assert eventPayload != null : "event message is missing";
assert eventPayload != null : "onRealtimePresenceMessage: event message is missing";
try {
channelPresenceMessageListener = new PluginChannelPresenceMessageListener(eventSink);
ablyLibrary
Expand Down Expand Up @@ -213,23 +213,23 @@ public void onCancel(Object object) {
case PlatformConstants.PlatformMethod.onRealtimeChannelStateChanged:
// Note: this and all other assert statements in this onCancel method are
// left as is as there is no way of propagating this error to flutter side
assert eventPayload != null : "event message is missing";
assert eventPayload != null : "onRealtimeChannelStateChanged: event message is missing";
ablyLibrary
.getRealtime(ablyMessage.handle)
.channels
.get((String) eventPayload.get(PlatformConstants.TxTransportKeys.channelName))
.off(channelStateListener);
break;
case PlatformConstants.PlatformMethod.onRealtimeChannelMessage:
assert eventPayload != null : "event message is missing";
assert eventPayload != null : "onRealtimeChannelMessage: event message is missing";
ablyLibrary
.getRealtime(ablyMessage.handle)
.channels
.get((String) eventPayload.get(PlatformConstants.TxTransportKeys.channelName))
.unsubscribe(channelMessageListener);
break;
case PlatformConstants.PlatformMethod.onRealtimePresenceMessage:
assert eventPayload != null : "event message is missing";
assert eventPayload != null : "onRealtimePresenceMessage: event message is missing";
ablyLibrary
.getRealtime(ablyMessage.handle)
.channels
Expand Down
4 changes: 2 additions & 2 deletions lib/src/crypto/src/crypto.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class Crypto {
throw AblyException(keyTypeErrorMessage);
}

return Platform.invokePlatformMethodNonNull<CipherParams>(
return Platform().invokePlatformMethodNonNull<CipherParams>(
PlatformMethod.cryptoGetParams, {
TxCryptoGetParams.algorithm: defaultAlgorithm,
TxCryptoGetParams.key: key,
Expand All @@ -55,6 +55,6 @@ class Crypto {
/// this key with other clients, there is no way to decrypt the messages.
static Future<Uint8List> generateRandomKey(
{keyLength = defaultKeyLengthInBits}) =>
Platform.invokePlatformMethodNonNull<Uint8List>(
Platform().invokePlatformMethodNonNull<Uint8List>(
PlatformMethod.cryptoGenerateRandomKey, keyLength);
}
26 changes: 15 additions & 11 deletions lib/src/platform/src/background_android_isolate_platform.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,11 @@ import 'package:flutter/services.dart';
/// called when a separate isolate is launched by the app, not for apps
/// launched with Activity.
class BackgroundIsolateAndroidPlatform {
/// Instantiates if required.
factory BackgroundIsolateAndroidPlatform() {
_platform ??= BackgroundIsolateAndroidPlatform._internal();
return _platform!;
}

static BackgroundIsolateAndroidPlatform? _platform;
BackgroundIsolateAndroidPlatform._internal();

BackgroundIsolateAndroidPlatform._internal() {
methodChannel.setMethodCallHandler((call) async {
/// Start listening to messages from Java side.
void setupCallHandler() {
_methodChannel.setMethodCallHandler((call) async {
switch (call.method) {
case PlatformMethod.pushOnBackgroundMessage:
return _onPushBackgroundMessage(call.arguments as RemoteMessage);
Expand All @@ -31,16 +26,25 @@ class BackgroundIsolateAndroidPlatform {
});
}

static final BackgroundIsolateAndroidPlatform _platform =
BackgroundIsolateAndroidPlatform._internal();

/// Singleton instance of BackgroundIsolateAndroidPlatform
factory BackgroundIsolateAndroidPlatform() => _platform;

/// A method channel used to communicate with the user's app isolate
/// we explicitly launched when a RemoteMessage is received.
/// Used only on Android.
static final MethodChannel methodChannel =
MethodChannel('io.ably.flutter.plugin.background', Platform.codec);
final MethodChannel _methodChannel = MethodChannel(
'io.ably.flutter.plugin.background', StandardMethodCodec(Codec()));

final PushNotificationEventsNative _pushNotificationEvents =
PushNative.notificationEvents as PushNotificationEventsNative;

Future<Object?> _onPushBackgroundMessage(RemoteMessage remoteMessage) async {
_pushNotificationEvents.handleBackgroundMessage(remoteMessage);
}

Future<T?> invokeMethod<T>(String method, [arguments]) async =>
_methodChannel.invokeMethod<T>(method, arguments);
}
7 changes: 3 additions & 4 deletions lib/src/platform/src/info.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ import 'package:ably_flutter/ably_flutter.dart';
import 'package:ably_flutter/src/platform/platform_internal.dart';

/// Get android/iOS platform version
Future<String> platformVersion() async =>
(await Platform.invokePlatformMethod<String>(
PlatformMethod.getPlatformVersion))!;
Future<String> platformVersion() async => Platform()
.invokePlatformMethodNonNull<String>(PlatformMethod.getPlatformVersion);

/// Get ably library version
Future<String> version() async =>
(await Platform.invokePlatformMethod<String>(PlatformMethod.getVersion))!;
Platform().invokePlatformMethodNonNull<String>(PlatformMethod.getVersion);
3 changes: 2 additions & 1 deletion lib/src/platform/src/method_call_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ class AblyMethodCallHandler {
return onOpenSettingsFor();
default:
throw PlatformException(
code: 'invalid_method', message: 'No such method ${call.method}');
code: 'Received invalid method channel call from Platform side',
message: 'No such method ${call.method}');
}
});
}
Expand Down
69 changes: 36 additions & 33 deletions lib/src/platform/src/platform.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,55 +5,49 @@ import 'package:ably_flutter/src/platform/platform_internal.dart';
import 'package:flutter/services.dart';

class Platform {
Platform._internal({MethodChannel? methodChannel}) {
_methodChannel = methodChannel;
if (methodChannel == null) {
_methodChannel = MethodChannel('io.ably.flutter.plugin', _codec);
}
_streamsChannel = StreamsChannel('io.ably.flutter.stream', _codec);
AblyMethodCallHandler(_methodChannel!);
BackgroundIsolateAndroidPlatform().setupCallHandler();
invokePlatformMethod(PlatformMethod.resetAblyClients);
}

static Platform? _platform;

/// Singleton instance of Platform
factory Platform({MethodChannel? methodChannel}) =>
_platform ??= Platform._internal(methodChannel: methodChannel);

/// instance of [StandardMethodCodec] with custom [MessageCodec] for
/// exchanging Ably types with platform via platform channels
/// viz., [MethodChannel] and [StreamsChannel]
static StandardMethodCodec codec = StandardMethodCodec(Codec());
final StandardMethodCodec _codec = StandardMethodCodec(Codec());

/// instance of method channel to interact with android/ios code
static final MethodChannel methodChannel =
MethodChannel('io.ably.flutter.plugin', codec);
MethodChannel? _methodChannel;

/// instance of method channel to listen to android/ios events
static final StreamsChannel streamsChannel =
StreamsChannel('io.ably.flutter.stream', codec);

/// This field will be set to true when the app is restarted or
/// launched. This allows us to detect if an app has been restarted,
/// or a hot restart (not hot reload) has happened.
static bool _shouldResetAblyClients = true;
late final StreamsChannel? _streamsChannel;

/// Clears instances on the Platform side
static Future<void> _resetOldAblyClients() async {
if (_shouldResetAblyClients) {
AblyMethodCallHandler(methodChannel);
BackgroundIsolateAndroidPlatform();
await methodChannel.invokeMethod(PlatformMethod.resetAblyClients);
_shouldResetAblyClients = false;
}
}

/// invokes a platform [method] with [arguments]
///
/// calls [_resetOldAblyClients] before invoking any method to handle any
/// cleanup tasks that are especially required while performing hot-restart
/// (as hot-restart does not automatically reset platform instances)
static Future<T?> invokePlatformMethod<T>(String method,
[Object? arguments]) async {
await _resetOldAblyClients();
Future<T?> invokePlatformMethod<T>(String method, [Object? arguments]) async {
try {
return await methodChannel.invokeMethod<T>(method, arguments);
} on PlatformException catch (pe) {
if (pe.details is ErrorInfo) {
throw AblyException.fromPlatformException(pe);
return await _methodChannel!.invokeMethod<T>(method, arguments);
} on PlatformException catch (platformException) {
// Convert some PlatformExceptions into AblyException
if (platformException.details is ErrorInfo) {
throw AblyException.fromPlatformException(platformException);
} else {
rethrow;
}
}
}
lukasz-szyszkowski marked this conversation as resolved.
Show resolved Hide resolved

/// Call a platform method which always provides a result.
static Future<T> invokePlatformMethodNonNull<T>(String method,
Future<T> invokePlatformMethodNonNull<T>(String method,
[Object? arguments]) async {
final result = await invokePlatformMethod<T>(method, arguments);
if (result == null) {
Expand All @@ -63,4 +57,13 @@ class Platform {
return result;
}
}

Stream<T> receiveBroadcastStream<T>(String methodName, int handle,
[final Object? payload]) =>
_streamsChannel!.receiveBroadcastStream<T>(
AblyMessage(
AblyEventMessage(methodName, payload),
handle: handle,
),
);
}
31 changes: 8 additions & 23 deletions lib/src/platform/src/platform_object.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@ import 'package:ably_flutter/ably_flutter.dart';
import 'package:flutter/services.dart';
import 'package:meta/meta.dart';

/// An object which has a live counterpart in the Platform client library SDK,
/// where that live counterpart is held as a strong reference by the plugin
/// implementation.
/// A representation of a Platform side instance (Android, iOS).
abstract class PlatformObject {
Future<int>? _handle;
final Platform _platform = Platform();
int? _handleValue; // Only for logging. Otherwise use _handle instead.

/// immediately instantiates an object on platform side by calling
Expand All @@ -22,7 +21,7 @@ abstract class PlatformObject {
}

@override
String toString() => 'Ably Platform Object $_handleValue';
String toString() => 'Ably Flutter PlatformObject with handle: $_handleValue';

/// creates an instance of this object on platform side
Future<int?> createPlatformInstance();
Expand All @@ -36,19 +35,13 @@ abstract class PlatformObject {
Future<int> _acquireHandle() =>
createPlatformInstance().then((value) => (_handleValue = value)!);

/// [MethodChannel] to make method calls to platform side
MethodChannel get methodChannel => Platform.methodChannel;

/// [EventChannel] to register events on platform side
StreamsChannel get eventChannel => Platform.streamsChannel;

/// invoke platform method channel without AblyMessage encapsulation
@protected
Future<T?> invokeRaw<T>(
final String method, [
final Object? arguments,
]) async =>
Platform.invokePlatformMethod<T>(method, arguments);
_platform.invokePlatformMethod<T>(method, arguments);

/// invoke platform method channel with AblyMessage encapsulation
///
Expand All @@ -74,23 +67,15 @@ abstract class PlatformObject {
return invokeRaw<T>(method, message);
}

Future<Stream<T>> _listen<T>(
final String eventName, [
final Object? payload,
]) async =>
eventChannel.receiveBroadcastStream<T>(
AblyMessage(
AblyEventMessage(eventName, payload),
handle: await handle,
),
);

/// Listen for events
@protected
Stream<T> listen<T>(final String method, [final Object? payload]) {
// ignore: close_sinks, will be closed by listener
final controller = StreamController<T>();
_listen<T>(method, payload).then(controller.addStream);
handle
.then((handle) =>
_platform.receiveBroadcastStream<T>(method, handle, payload))
.then(controller.addStream);
return controller.stream;
}
}
6 changes: 4 additions & 2 deletions lib/src/platform/src/push_notification_events_native.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class PushNotificationEventsNative implements PushNotificationEvents {

@override
Future<RemoteMessage?> get notificationTapLaunchedAppFromTerminated =>
Platform.methodChannel.invokeMethod(
Platform().invokePlatformMethod<RemoteMessage>(
PlatformMethod.pushNotificationTapLaunchedAppFromTerminated);

@override
Expand Down Expand Up @@ -56,7 +56,9 @@ class PushNotificationEventsNative implements PushNotificationEvents {
Future<void> setOnBackgroundMessage(BackgroundMessageHandler handler) async {
_onBackgroundMessage = handler;
if (io.Platform.isAndroid) {
await BackgroundIsolateAndroidPlatform.methodChannel.invokeMethod(
// Inform Android side that the Flutter application
// is ready to receive push messages.
await BackgroundIsolateAndroidPlatform().invokeMethod(
PlatformMethod.pushBackgroundFlutterApplicationReadyOnAndroid);
}
}
Expand Down
3 changes: 1 addition & 2 deletions lib/src/platform/src/realtime/realtime.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,7 @@ class Realtime extends PlatformObject {
// If this happens, we won't be able to identify which realtime client
// the authCallback belongs to. Instead, on Android, we set autoConnect
// to false, and call connect immediately once we get the handle.
await Platform.invokePlatformMethod(
PlatformMethod.connectRealtime, handle);
await invokeRaw(PlatformMethod.connectRealtime, handle);
}
return handle;
}
Expand Down
5 changes: 4 additions & 1 deletion test/ably_flutter_plugin_test.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import 'package:ably_flutter/ably_flutter.dart';
import 'package:ably_flutter/src/platform/platform_internal.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
final channel = Platform.methodChannel;
final channel =
MethodChannel('io.ably.flutter.plugin', StandardMethodCodec(Codec()));

TestWidgetsFlutterBinding.ensureInitialized();
var counter = 0;
Expand Down Expand Up @@ -33,6 +35,7 @@ void main() {
return null;
}
});
Platform(methodChannel: channel);
});

tearDown(() {
Expand Down
Loading