diff --git a/mindbox_android/android/src/main/kotlin/cloud/mindbox/mindbox_android/MindboxAndroidPlugin.kt b/mindbox_android/android/src/main/kotlin/cloud/mindbox/mindbox_android/MindboxAndroidPlugin.kt index f33d0e9..be7d742 100644 --- a/mindbox_android/android/src/main/kotlin/cloud/mindbox/mindbox_android/MindboxAndroidPlugin.kt +++ b/mindbox_android/android/src/main/kotlin/cloud/mindbox/mindbox_android/MindboxAndroidPlugin.kt @@ -18,13 +18,15 @@ import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result import io.flutter.plugin.common.PluginRegistry.NewIntentListener +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference /** MindboxAndroidPlugin */ class MindboxAndroidPlugin : FlutterPlugin, MethodCallHandler, ActivityAware, NewIntentListener { private lateinit var context: Activity private var binding: ActivityPluginBinding? = null - private var deviceUuidSubscription: String? = null - private var tokenSubscription: String? = null + private val deviceUuidSubscriptions = mutableListOf() + private val tokenSubscriptions = mutableListOf() private lateinit var channel: MethodChannel inner class InAppCallbackImpl : InAppCallback { @@ -83,27 +85,76 @@ class MindboxAndroidPlugin : FlutterPlugin, MethodCallHandler, ActivityAware, Ne } } "getDeviceUUID" -> { - if (deviceUuidSubscription != null) { - Mindbox.disposeDeviceUuidSubscription(deviceUuidSubscription!!) + val subscriptionRef = AtomicReference(null) + val isResultSent = AtomicBoolean(false) + + val subscriptionId = Mindbox.subscribeDeviceUuid { uuid -> + if (isResultSent.compareAndSet(false, true)) { + result.success(uuid) + + val id = subscriptionRef.get() + if (id != null) { + Mindbox.disposeDeviceUuidSubscription(id) + deviceUuidSubscriptions.remove(id) + } + } } - deviceUuidSubscription = Mindbox.subscribeDeviceUuid { uuid -> - result.success(uuid) + + subscriptionRef.set(subscriptionId) + deviceUuidSubscriptions.add(subscriptionId) + + // If callback was synchronous, unsubscribe immediately + if (isResultSent.get()) { + Mindbox.disposeDeviceUuidSubscription(subscriptionId) + deviceUuidSubscriptions.remove(subscriptionId) } } "getToken" -> { - if (tokenSubscription != null) { - Mindbox.disposePushTokenSubscription(tokenSubscription!!) + val subscriptionRef = AtomicReference(null) + val isResultSent = AtomicBoolean(false) + + val subscriptionId = Mindbox.subscribePushToken { token -> + if (isResultSent.compareAndSet(false, true)) { + result.success(token) + + val id = subscriptionRef.get() + if (id != null) { + Mindbox.disposePushTokenSubscription(id) + tokenSubscriptions.remove(id) + } + } } - tokenSubscription = Mindbox.subscribePushToken { token -> - result.success(token) + + subscriptionRef.set(subscriptionId) + tokenSubscriptions.add(subscriptionId) + + if (isResultSent.get()) { + Mindbox.disposePushTokenSubscription(subscriptionId) + tokenSubscriptions.remove(subscriptionId) } } "getTokens" -> { - if (tokenSubscription != null) { - Mindbox.disposePushTokenSubscription(tokenSubscription!!) + val subscriptionRef = AtomicReference(null) + val isResultSent = AtomicBoolean(false) + + val subscriptionId = Mindbox.subscribePushTokens { token -> + if (isResultSent.compareAndSet(false, true)) { + result.success(token) + + val id = subscriptionRef.get() + if (id != null) { + Mindbox.disposePushTokenSubscription(id) + tokenSubscriptions.remove(id) + } + } } - tokenSubscription = Mindbox.subscribePushTokens { token -> - result.success(token) + + subscriptionRef.set(subscriptionId) + tokenSubscriptions.add(subscriptionId) + + if (isResultSent.get()) { + Mindbox.disposePushTokenSubscription(subscriptionId) + tokenSubscriptions.remove(subscriptionId) } } "executeAsyncOperation" -> { diff --git a/mindbox_platform_interface/lib/src/types/mindbox_method_handler.dart b/mindbox_platform_interface/lib/src/types/mindbox_method_handler.dart index 7bd58f1..78c537c 100644 --- a/mindbox_platform_interface/lib/src/types/mindbox_method_handler.dart +++ b/mindbox_platform_interface/lib/src/types/mindbox_method_handler.dart @@ -84,8 +84,13 @@ class MindboxMethodHandler { final pendingCallbackMethodsCopy = List<_PendingCallbackMethod>.from(_pendingCallbackMethods); for (final callbackMethod in pendingCallbackMethodsCopy) { - callbackMethod.callback( - await channel.invokeMethod(callbackMethod.methodName) ?? 'null'); + channel + .invokeMethod(callbackMethod.methodName) + .then((result) { + callbackMethod.callback(result ?? 'null'); + }).catchError((e) { + _logError('Error processing pending method ${callbackMethod.methodName}: $e'); + }); } final pendingOperationsCopy = List<_PendingOperations>.from(_pendingOperations); @@ -99,6 +104,8 @@ class MindboxMethodHandler { if (operation.errorCallback != null) { final mindboxError = _convertPlatformExceptionToMindboxError(e); operation.errorCallback!(mindboxError); + } else { + _logError('Error processing pending operation ${operation.methodName}: $e'); } }); } diff --git a/mindbox_platform_interface/test/src/types/mindbox_method_handler_test.dart b/mindbox_platform_interface/test/src/types/mindbox_method_handler_test.dart index 7bdb5a1..dd04d93 100644 --- a/mindbox_platform_interface/test/src/types/mindbox_method_handler_test.dart +++ b/mindbox_platform_interface/test/src/types/mindbox_method_handler_test.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mindbox_platform_interface/mindbox_platform_interface.dart'; @@ -405,6 +406,72 @@ void main() { expect(() => completer.future, throwsA(isA())); }, ); + + test( + 'Verify that init completes even if pending getDeviceUUID hangs, allowing retries', + () async { + int getDeviceUUIDCallCount = 0; + + // Mock handler that hangs on first getDeviceUUID + Future slowMockMethodCallHandler(MethodCall methodCall) async { + switch (methodCall.method) { + case 'init': + return Future.value(true); + case 'getDeviceUUID': + getDeviceUUIDCallCount++; + if (getDeviceUUIDCallCount == 1) { + // First call hangs (pending one) + return Completer().future; + } else { + // Subsequent calls succeed + return Future.value('retry-uuid'); + } + case 'writeNativeLog': + return Future.value(null); + default: + return 'dummy-response'; + } + } + + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, slowMockMethodCallHandler); + + // 1. Call getDeviceUUID before init. It goes to pending. + bool callback1Called = false; + handler.getDeviceUUID(callback: (uuid) { + callback1Called = true; + }); + + // 2. Call init. + final validConfig = Configuration( + domain: 'domain', + endpointIos: 'endpointIos', + endpointAndroid: 'endpointAndroid', + subscribeCustomerIfCreated: true, + ); + + // This should now complete even though the first getDeviceUUID is hanging + // Adding timeout to fail faster if regression occurs (hangs indefinitely) + await handler + .init(configuration: validConfig) + .timeout(const Duration(seconds: 5)); + + expect(getDeviceUUIDCallCount, equals(1), + reason: 'First call should have been triggered'); + expect(callback1Called, isFalse, + reason: 'First callback is still hanging'); + + // 3. Call getDeviceUUID again (retry). + // This should succeed because init is complete. + final completer = Completer(); + handler.getDeviceUUID(callback: (uuid) => completer.complete(uuid)); + + final result = await completer.future.timeout(const Duration(seconds: 1)); + + expect(result, equals('retry-uuid')); + expect(getDeviceUUIDCallCount, equals(2)); + }, + ); } class StubMindboxMethodHandler {