From 0c167603f1df3ca098e861489f01711d2ade094b Mon Sep 17 00:00:00 2001 From: Rehan Date: Tue, 26 Nov 2024 16:23:48 +0500 Subject: [PATCH] chore: refactor native android call forwarding --- .../customer_io/CustomerIOExtensions.kt | 27 ---- .../customer/customer_io/CustomerIOPlugin.kt | 123 ++++++------------ .../bridge/MethodCallExtensions.kt | 56 ++++++++ .../customer_io/bridge/NativeModuleBridge.kt | 9 ++ .../io/customer/customer_io/constant/Keys.kt | 35 ----- .../CustomerIOInAppMessaging.kt | 18 +-- .../messagingpush/CustomerIOPushMessaging.kt | 38 ++---- 7 files changed, 124 insertions(+), 182 deletions(-) delete mode 100644 android/src/main/kotlin/io/customer/customer_io/CustomerIOExtensions.kt create mode 100644 android/src/main/kotlin/io/customer/customer_io/bridge/MethodCallExtensions.kt delete mode 100644 android/src/main/kotlin/io/customer/customer_io/constant/Keys.kt diff --git a/android/src/main/kotlin/io/customer/customer_io/CustomerIOExtensions.kt b/android/src/main/kotlin/io/customer/customer_io/CustomerIOExtensions.kt deleted file mode 100644 index b6256fc..0000000 --- a/android/src/main/kotlin/io/customer/customer_io/CustomerIOExtensions.kt +++ /dev/null @@ -1,27 +0,0 @@ -package io.customer.customer_io - -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel - -/** - * Invokes lambda method that can be used to call matching native method conveniently. The lambda - * expression receives function parameters as arguments and should return the desired result. Any - * exception in the lambda will cause the invoked method to fail with error. - */ -internal fun MethodCall.invokeNative( - result: MethodChannel.Result, - performAction: (params: Map) -> R, -) { - try { - @Suppress("UNCHECKED_CAST") - val params = this.arguments as? Map ?: emptyMap() - val actionResult = performAction(params) - if (actionResult is Unit) { - result.success(true) - } else { - result.success(actionResult) - } - } catch (ex: Exception) { - result.error(this.method, ex.localizedMessage, ex) - } -} diff --git a/android/src/main/kotlin/io/customer/customer_io/CustomerIOPlugin.kt b/android/src/main/kotlin/io/customer/customer_io/CustomerIOPlugin.kt index 9da23c2..f99720b 100644 --- a/android/src/main/kotlin/io/customer/customer_io/CustomerIOPlugin.kt +++ b/android/src/main/kotlin/io/customer/customer_io/CustomerIOPlugin.kt @@ -4,7 +4,8 @@ import android.app.Application import android.content.Context import androidx.annotation.NonNull import io.customer.customer_io.bridge.NativeModuleBridge -import io.customer.customer_io.constant.Keys +import io.customer.customer_io.bridge.nativeMapArgs +import io.customer.customer_io.bridge.nativeNoArgs import io.customer.customer_io.messaginginapp.CustomerIOInAppMessaging import io.customer.customer_io.messagingpush.CustomerIOPushMessaging import io.customer.customer_io.utils.getAs @@ -58,85 +59,28 @@ class CustomerIOPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } } - private fun MethodCall.toNativeMethodCall( - result: Result, performAction: (params: Map) -> Unit - ) { - try { - val params = this.arguments as? Map ?: emptyMap() - performAction(params) - result.success(true) - } catch (e: Exception) { - result.error(this.method, e.localizedMessage, null) - } - } - - override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { + override fun onMethodCall(call: MethodCall, result: Result) { when (call.method) { - Keys.Methods.INITIALIZE -> { - call.toNativeMethodCall(result) { - initialize(it) - } - } - - Keys.Methods.IDENTIFY -> { - call.toNativeMethodCall(result) { - identify(it) - } - } - - Keys.Methods.SCREEN -> { - call.toNativeMethodCall(result) { - screen(it) - } - } - - Keys.Methods.TRACK -> { - call.toNativeMethodCall(result) { - track(it) - } - } - - Keys.Methods.TRACK_METRIC -> { - call.toNativeMethodCall(result) { - trackMetric(it) - } - } - - Keys.Methods.REGISTER_DEVICE_TOKEN -> { - call.toNativeMethodCall(result) { - registerDeviceToken(it) - } - } - - Keys.Methods.SET_DEVICE_ATTRIBUTES -> { - call.toNativeMethodCall(result) { - setDeviceAttributes(it) - } - } - - Keys.Methods.SET_PROFILE_ATTRIBUTES -> { - call.toNativeMethodCall(result) { - setProfileAttributes(it) - } - } - - Keys.Methods.CLEAR_IDENTIFY -> { - clearIdentity() - } - - else -> { - result.notImplemented() - } + "clearIdentify" -> call.nativeNoArgs(result, ::clearIdentify) + "identify" -> call.nativeMapArgs(result, ::identify) + "initialize" -> call.nativeMapArgs(result, ::initialize) + "registerDeviceToken" -> call.nativeMapArgs(result, ::registerDeviceToken) + "screen" -> call.nativeMapArgs(result, ::screen) + "setDeviceAttributes" -> call.nativeMapArgs(result, ::setDeviceAttributes) + "setProfileAttributes" -> call.nativeMapArgs(result, ::setProfileAttributes) + "track" -> call.nativeMapArgs(result, ::track) + "trackMetric" -> call.nativeMapArgs(result, ::trackMetric) + else -> result.notImplemented() } } - private fun clearIdentity() { + private fun clearIdentify() { CustomerIO.instance().clearIdentify() } private fun identify(params: Map) { - val userId = params.getAs(Keys.Tracking.USER_ID) - val traits = params.getAs>(Keys.Tracking.TRAITS) ?: emptyMap() + val userId = params.getAs(Args.USER_ID) + val traits = params.getAs>(Args.TRAITS) ?: emptyMap() if (userId == null && traits.isEmpty()) { logger.error("Please provide either an ID or traits to identify.") @@ -153,10 +97,10 @@ class CustomerIOPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } private fun track(params: Map) { - val name = requireNotNull(params.getAs(Keys.Tracking.NAME)) { + val name = requireNotNull(params.getAs(Args.NAME)) { "Event name is missing in params: $params" } - val properties = params.getAs>(Keys.Tracking.PROPERTIES) + val properties = params.getAs>(Args.PROPERTIES) if (properties.isNullOrEmpty()) { CustomerIO.instance().track(name) @@ -166,16 +110,16 @@ class CustomerIOPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } private fun registerDeviceToken(params: Map) { - val token = requireNotNull(params.getAs(Keys.Tracking.TOKEN)) { + val token = requireNotNull(params.getAs(Args.TOKEN)) { "Device token is missing in params: $params" } CustomerIO.instance().registerDeviceToken(token) } private fun trackMetric(params: Map) { - val deliveryId = params.getAs(Keys.Tracking.DELIVERY_ID) - val deliveryToken = params.getAs(Keys.Tracking.DELIVERY_TOKEN) - val eventName = params.getAs(Keys.Tracking.METRIC_EVENT) + val deliveryId = params.getAs(Args.DELIVERY_ID) + val deliveryToken = params.getAs(Args.DELIVERY_TOKEN) + val eventName = params.getAs(Args.METRIC_EVENT) if (deliveryId == null || deliveryToken == null || eventName == null) { throw IllegalArgumentException("Missing required parameters") @@ -193,7 +137,7 @@ class CustomerIOPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } private fun setDeviceAttributes(params: Map) { - val attributes = params.getAs>(Keys.Tracking.ATTRIBUTES) + val attributes = params.getAs>(Args.ATTRIBUTES) if (attributes.isNullOrEmpty()) { logger.error("Device attributes are missing in params: $params") @@ -204,7 +148,7 @@ class CustomerIOPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } private fun setProfileAttributes(params: Map) { - val attributes = params.getAs>(Keys.Tracking.ATTRIBUTES) + val attributes = params.getAs>(Args.ATTRIBUTES) if (attributes.isNullOrEmpty()) { logger.error("Profile attributes are missing in params: $params") @@ -215,10 +159,10 @@ class CustomerIOPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } private fun screen(params: Map) { - val title = requireNotNull(params.getAs(Keys.Tracking.TITLE)) { + val title = requireNotNull(params.getAs(Args.TITLE)) { "Screen title is missing in params: $params" } - val properties = params.getAs>(Keys.Tracking.PROPERTIES) + val properties = params.getAs>(Args.PROPERTIES) if (properties.isNullOrEmpty()) { CustomerIO.instance().screen(title) @@ -310,4 +254,19 @@ class CustomerIOPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { it.onDetachedFromActivity() } } + + companion object { + object Args { + const val ATTRIBUTES = "attributes" + const val DELIVERY_ID = "deliveryId" + const val DELIVERY_TOKEN = "deliveryToken" + const val METRIC_EVENT = "metricEvent" + const val NAME = "name" + const val PROPERTIES = "properties" + const val TITLE = "title" + const val TOKEN = "token" + const val TRAITS = "traits" + const val USER_ID = "userId" + } + } } diff --git a/android/src/main/kotlin/io/customer/customer_io/bridge/MethodCallExtensions.kt b/android/src/main/kotlin/io/customer/customer_io/bridge/MethodCallExtensions.kt new file mode 100644 index 0000000..9510c8f --- /dev/null +++ b/android/src/main/kotlin/io/customer/customer_io/bridge/MethodCallExtensions.kt @@ -0,0 +1,56 @@ +package io.customer.customer_io.bridge + +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel + +/** + * Handles native method call by transforming the arguments and invoking the handler. + * + * @param result The result object to send the response back to Flutter. + * @param transformer A function to transform the incoming arguments. + * @param handler A function to handle the transformed arguments and produce a result. + * + * - If the handler returns `Unit`, it sends `true` to Flutter to avoid errors. + * - Catches and sends any exceptions as errors to Flutter. + */ +internal fun MethodCall.native( + result: MethodChannel.Result, + transformer: (Any?) -> Arguments, + handler: (Arguments) -> Result, +) = runCatching { + val args = transformer(arguments) + val response = handler(args) + // If the result is Unit, then return true to the Flutter side + // As returning Unit will throw an error on the Flutter side + result.success( + when (response) { + is Unit -> true + else -> response + } + ) +}.onFailure { ex -> + result.error(method, ex.localizedMessage, ex) +} + +/** + * Handles a native method call that requires no arguments. + * + * @param result The result object to send the response back to Flutter. + * @param handler A function to handle the call and produce a result. + */ +internal fun MethodCall.nativeNoArgs( + result: MethodChannel.Result, + handler: () -> Result, +) = native(result, { }, { handler() }) + +/** + * Handles a native method call with arguments passed as a map. + * + * @param result The result object to send the response back to Flutter. + * @param handler A function to handle the map arguments and produce a result. + */ +@Suppress("UNCHECKED_CAST") +internal fun MethodCall.nativeMapArgs( + result: MethodChannel.Result, + handler: (Map) -> Result, +) = native(result, { it as? Map ?: emptyMap() }, handler) diff --git a/android/src/main/kotlin/io/customer/customer_io/bridge/NativeModuleBridge.kt b/android/src/main/kotlin/io/customer/customer_io/bridge/NativeModuleBridge.kt index d891ad5..5eb193f 100644 --- a/android/src/main/kotlin/io/customer/customer_io/bridge/NativeModuleBridge.kt +++ b/android/src/main/kotlin/io/customer/customer_io/bridge/NativeModuleBridge.kt @@ -4,6 +4,7 @@ import io.customer.sdk.CustomerIOBuilder import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding +import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel /** @@ -40,6 +41,14 @@ internal interface NativeModuleBridge : MethodChannel.MethodCallHandler, Activit flutterCommunicationChannel.setMethodCallHandler(null) } + /** + * Handles incoming method calls from Flutter and invokes the appropriate native method handler. + * If the method is not implemented, the result is marked as not implemented. + */ + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + result.notImplemented() + } + fun configureModule(builder: CustomerIOBuilder, config: Map) override fun onDetachedFromActivity() {} diff --git a/android/src/main/kotlin/io/customer/customer_io/constant/Keys.kt b/android/src/main/kotlin/io/customer/customer_io/constant/Keys.kt deleted file mode 100644 index 2afabea..0000000 --- a/android/src/main/kotlin/io/customer/customer_io/constant/Keys.kt +++ /dev/null @@ -1,35 +0,0 @@ -package io.customer.customer_io.constant - -// TODO: Cleanup this file later when all commented methods are implemented -internal object Keys { - - object Methods { - const val INITIALIZE = "initialize" - const val IDENTIFY = "identify" - const val CLEAR_IDENTIFY = "clearIdentify" - const val TRACK = "track" - const val SCREEN = "screen" - const val SET_DEVICE_ATTRIBUTES = "setDeviceAttributes" - const val SET_PROFILE_ATTRIBUTES = "setProfileAttributes" - const val REGISTER_DEVICE_TOKEN = "registerDeviceToken" - const val TRACK_METRIC = "trackMetric" - const val ON_MESSAGE_RECEIVED = "onMessageReceived" - const val DISMISS_MESSAGE = "dismissMessage" - const val GET_REGISTERED_DEVICE_TOKEN = "getRegisteredDeviceToken" - } - - object Tracking { - const val USER_ID = "userId" - const val TRAITS = "traits" - const val ATTRIBUTES = "attributes" - const val EVENT_NAME = "eventName" - const val TOKEN = "token" - const val DELIVERY_ID = "deliveryId" - const val DELIVERY_TOKEN = "deliveryToken" - const val METRIC_EVENT = "metricEvent" - - const val NAME = "name" - const val PROPERTIES = "properties" - const val TITLE = "title" - } -} diff --git a/android/src/main/kotlin/io/customer/customer_io/messaginginapp/CustomerIOInAppMessaging.kt b/android/src/main/kotlin/io/customer/customer_io/messaginginapp/CustomerIOInAppMessaging.kt index ece79f6..ca28b5e 100644 --- a/android/src/main/kotlin/io/customer/customer_io/messaginginapp/CustomerIOInAppMessaging.kt +++ b/android/src/main/kotlin/io/customer/customer_io/messaginginapp/CustomerIOInAppMessaging.kt @@ -2,8 +2,7 @@ package io.customer.customer_io.messaginginapp import android.app.Activity import io.customer.customer_io.bridge.NativeModuleBridge -import io.customer.customer_io.constant.Keys -import io.customer.customer_io.invokeNative +import io.customer.customer_io.bridge.nativeNoArgs import io.customer.customer_io.utils.getAs import io.customer.messaginginapp.MessagingInAppModuleConfig import io.customer.messaginginapp.ModuleMessagingInApp @@ -51,18 +50,15 @@ internal class CustomerIOInAppMessaging( override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { - Keys.Methods.DISMISS_MESSAGE -> { - call.invokeNative(result) { - CustomerIO.instance().inAppMessaging().dismissMessage() - } - } - - else -> { - result.notImplemented() - } + "dismissMessage" -> call.nativeNoArgs(result, ::dismissMessage) + else -> super.onMethodCall(call, result) } } + private fun dismissMessage() { + CustomerIO.instance().inAppMessaging().dismissMessage() + } + /** * Adds in-app module to native Android SDK based on the configuration provided by * customer app. diff --git a/android/src/main/kotlin/io/customer/customer_io/messagingpush/CustomerIOPushMessaging.kt b/android/src/main/kotlin/io/customer/customer_io/messagingpush/CustomerIOPushMessaging.kt index ae78ef8..947a0bd 100644 --- a/android/src/main/kotlin/io/customer/customer_io/messagingpush/CustomerIOPushMessaging.kt +++ b/android/src/main/kotlin/io/customer/customer_io/messagingpush/CustomerIOPushMessaging.kt @@ -2,8 +2,8 @@ package io.customer.customer_io.messagingpush import android.content.Context import io.customer.customer_io.bridge.NativeModuleBridge -import io.customer.customer_io.constant.Keys -import io.customer.customer_io.invokeNative +import io.customer.customer_io.bridge.nativeMapArgs +import io.customer.customer_io.bridge.nativeNoArgs import io.customer.customer_io.utils.getAs import io.customer.customer_io.utils.takeIfNotBlank import io.customer.messagingpush.CustomerIOFirebaseMessagingService @@ -34,24 +34,9 @@ internal class CustomerIOPushMessaging( override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { - Keys.Methods.GET_REGISTERED_DEVICE_TOKEN -> { - call.invokeNative(result) { - return@invokeNative getRegisteredDeviceToken() - } - } - - Keys.Methods.ON_MESSAGE_RECEIVED -> { - call.invokeNative(result) { args -> - return@invokeNative onMessageReceived( - message = args.getAs>("message"), - handleNotificationTrigger = args.getAs("handleNotificationTrigger") - ) - } - } - - else -> { - result.notImplemented() - } + "getRegisteredDeviceToken" -> call.nativeNoArgs(result, ::getRegisteredDeviceToken) + "onMessageReceived" -> call.nativeMapArgs(result, ::onMessageReceived) + else -> super.onMethodCall(call, result) } } @@ -62,15 +47,14 @@ internal class CustomerIOPushMessaging( /** * Handles push notification received. This is helpful in processing push notifications * received outside the CIO SDK. - * - * @param message push payload received from FCM. - * @param handleNotificationTrigger indicating if the local notification should be triggered. */ - private fun onMessageReceived( - message: Map?, - handleNotificationTrigger: Boolean?, - ): Boolean { + private fun onMessageReceived(args: Map): Boolean { try { + // Push payload received from FCM + val message = args.getAs>("message") + // Flag to indicate if local notification should be triggered + val handleNotificationTrigger = args.getAs("handleNotificationTrigger") + if (message == null) { throw IllegalArgumentException("Message cannot be null") }