diff --git a/CHANGELOG.md b/CHANGELOG.md index f4121c78b..adddf01ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## Unreleased +- Fixed IterableEmbeddedView not having an empty constructor and causing crashes + ## [3.6.4] ### Fixed - Updated `customPayload` of In-App Messages to be `@Nullable` diff --git a/integration-tests/src/androidTest/java/com/iterable/integration/tests/EmbeddedMessageIntegrationTest.kt b/integration-tests/src/androidTest/java/com/iterable/integration/tests/EmbeddedMessageIntegrationTest.kt index d29b53fcd..74f634bc6 100644 --- a/integration-tests/src/androidTest/java/com/iterable/integration/tests/EmbeddedMessageIntegrationTest.kt +++ b/integration-tests/src/androidTest/java/com/iterable/integration/tests/EmbeddedMessageIntegrationTest.kt @@ -214,7 +214,8 @@ class EmbeddedMessageIntegrationTest : BaseIntegrationTest() { .firstOrNull() as? EmbeddedMessageTestActivity if (activity != null) { - val fragment = IterableEmbeddedView(IterableEmbeddedViewType.BANNER, message, null) + val fragment = + IterableEmbeddedView.newInstance(IterableEmbeddedViewType.BANNER, message, null) activity.supportFragmentManager.beginTransaction() .replace(R.id.embedded_message_container, fragment) .commitNow() diff --git a/integration-tests/src/main/java/com/iterable/integration/tests/activities/EmbeddedMessageTestActivity.kt b/integration-tests/src/main/java/com/iterable/integration/tests/activities/EmbeddedMessageTestActivity.kt index a4902ed25..7f9d05935 100644 --- a/integration-tests/src/main/java/com/iterable/integration/tests/activities/EmbeddedMessageTestActivity.kt +++ b/integration-tests/src/main/java/com/iterable/integration/tests/activities/EmbeddedMessageTestActivity.kt @@ -118,7 +118,7 @@ class EmbeddedMessageTestActivity : AppCompatActivity() { if (messages.isNotEmpty()) { val firstMessage = messages.first() - val fragment = IterableEmbeddedView(IterableEmbeddedViewType.BANNER, firstMessage, null) + val fragment = IterableEmbeddedView.newInstance(IterableEmbeddedViewType.BANNER, firstMessage, null) supportFragmentManager.beginTransaction() .replace(R.id.embedded_message_container, fragment) .commitNowAllowingStateLoss() diff --git a/iterableapi-ui/src/main/java/com/iterable/iterableapi/ui/embedded/IterableEmbeddedView.kt b/iterableapi-ui/src/main/java/com/iterable/iterableapi/ui/embedded/IterableEmbeddedView.kt index b8701f9d5..1d9c22553 100644 --- a/iterableapi-ui/src/main/java/com/iterable/iterableapi/ui/embedded/IterableEmbeddedView.kt +++ b/iterableapi-ui/src/main/java/com/iterable/iterableapi/ui/embedded/IterableEmbeddedView.kt @@ -7,22 +7,49 @@ import android.view.View import android.view.ViewGroup import android.widget.Button import android.widget.ImageView -import android.widget.LinearLayout import android.widget.TextView import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import com.bumptech.glide.Glide -import com.google.android.flexbox.FlexboxLayout import com.iterable.iterableapi.EmbeddedMessageElementsButton import com.iterable.iterableapi.IterableApi import com.iterable.iterableapi.IterableEmbeddedMessage import com.iterable.iterableapi.ui.R -class IterableEmbeddedView( - private var viewType: IterableEmbeddedViewType, - private var message: IterableEmbeddedMessage, - private var config: IterableEmbeddedViewConfig? -): Fragment() { +class IterableEmbeddedView() : Fragment() { + + private lateinit var viewType: IterableEmbeddedViewType + private lateinit var message: IterableEmbeddedMessage + private var config: IterableEmbeddedViewConfig? = null + + /** + * @deprecated This constructor violates Android Fragment best practices and will cause crashes + * when the Fragment is recreated by the system (e.g., after configuration changes or process death). + * Use [newInstance] factory method instead. + * + * Migration example: + * ``` + * // Old (unstable / not-recommended): + * val fragment = IterableEmbeddedView(viewType, message, config) + * + * // New (more stable / recommended): + * val fragment = IterableEmbeddedView.newInstance(viewType, message, config) + * ``` + * + * This constructor will be removed in a future version. + */ + @Deprecated( + message = "Use newInstance() factory method instead. This constructor causes crashes when Fragment is recreated by the system.", + replaceWith = ReplaceWith("IterableEmbeddedView.newInstance(viewType, message, config)"), + level = DeprecationLevel.WARNING + ) + constructor( + viewType: IterableEmbeddedViewType, + message: IterableEmbeddedMessage, + config: IterableEmbeddedViewConfig? + ) : this() { + arguments = IterableEmbeddedViewArguments.toBundle(viewType, message, config) + } private val defaultBackgroundColor : Int by lazy { getDefaultColor(viewType, R.color.notification_background_color, R.color.banner_background_color, R.color.banner_background_color) } private val defaultBorderColor : Int by lazy { getDefaultColor(viewType, R.color.notification_border_color, R.color.banner_border_color, R.color.banner_border_color) } @@ -35,6 +62,39 @@ class IterableEmbeddedView( private val defaultBorderWidth = 1 private val defaultBorderCornerRadius = 8f + companion object { + /** + * Factory method to create a new instance of IterableEmbeddedView with the required parameters. + * + * @param viewType The type of embedded view to display + * @param message The embedded message to display + * @param config Optional configuration for customizing the view appearance + * @return A new instance of IterableEmbeddedView + */ + @JvmStatic + fun newInstance( + viewType: IterableEmbeddedViewType, + message: IterableEmbeddedMessage, + config: IterableEmbeddedViewConfig? = null + ): IterableEmbeddedView { + return IterableEmbeddedView().apply { + arguments = IterableEmbeddedViewArguments.toBundle(viewType, message, config) + } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + arguments?.let { args -> + viewType = IterableEmbeddedViewArguments.getViewType(args) + message = IterableEmbeddedViewArguments.getMessage(args) + config = IterableEmbeddedViewArguments.getConfig(args) + } ?: throw IllegalStateException( + "IterableEmbeddedView requires arguments. Use newInstance() factory method to create this fragment." + ) + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, diff --git a/iterableapi-ui/src/main/java/com/iterable/iterableapi/ui/embedded/IterableEmbeddedViewArguments.kt b/iterableapi-ui/src/main/java/com/iterable/iterableapi/ui/embedded/IterableEmbeddedViewArguments.kt new file mode 100644 index 000000000..a85850dd4 --- /dev/null +++ b/iterableapi-ui/src/main/java/com/iterable/iterableapi/ui/embedded/IterableEmbeddedViewArguments.kt @@ -0,0 +1,123 @@ +package com.iterable.iterableapi.ui.embedded + +import android.os.Bundle +import com.iterable.iterableapi.IterableEmbeddedMessage +import com.iterable.iterableapi.IterableLogger +import org.json.JSONException +import org.json.JSONObject + +internal object IterableEmbeddedViewArguments { + + private const val TAG = "IterableEmbeddedViewArgs" + + // Argument keys + private const val KEY_VIEW_TYPE = "view_type" + private const val KEY_MESSAGE_JSON = "message_json" + private const val KEY_BG_COLOR = "bg_color" + private const val KEY_BORDER_COLOR = "border_color" + private const val KEY_BORDER_WIDTH = "border_width" + private const val KEY_BORDER_RADIUS = "border_radius" + private const val KEY_PRIMARY_BTN_BG = "primary_btn_bg" + private const val KEY_PRIMARY_BTN_TEXT = "primary_btn_text" + private const val KEY_SECONDARY_BTN_BG = "secondary_btn_bg" + private const val KEY_SECONDARY_BTN_TEXT = "secondary_btn_text" + private const val KEY_TITLE_COLOR = "title_color" + private const val KEY_BODY_COLOR = "body_color" + + fun toBundle( + viewType: IterableEmbeddedViewType, + message: IterableEmbeddedMessage, + config: IterableEmbeddedViewConfig? + ): Bundle { + return Bundle().apply { + putString(KEY_VIEW_TYPE, viewType.name) + putString(KEY_MESSAGE_JSON, IterableEmbeddedMessage.toJSONObject(message).toString()) + putConfig(config) + } + } + + fun getViewType(arguments: Bundle): IterableEmbeddedViewType { + val viewTypeName = arguments.getString(KEY_VIEW_TYPE) + return viewTypeName?.let { + try { + IterableEmbeddedViewType.valueOf(it) + } catch (e: IllegalArgumentException) { + IterableLogger.e(TAG, "Invalid view type: $it, defaulting to BANNER") + IterableEmbeddedViewType.BANNER + } + } ?: IterableEmbeddedViewType.BANNER + } + + fun getMessage(arguments: Bundle): IterableEmbeddedMessage { + val messageJsonString = arguments.getString(KEY_MESSAGE_JSON) + return if (messageJsonString != null) { + try { + val messageJson = JSONObject(messageJsonString) + IterableEmbeddedMessage.fromJSONObject(messageJson) + } catch (e: JSONException) { + IterableLogger.e(TAG, "Failed to parse message JSON", e) + throw IllegalStateException( + "IterableEmbeddedView failed to restore message from saved state. Use newInstance() factory method to create this fragment." + ) + } + } else { + throw IllegalStateException( + "IterableEmbeddedView requires a message argument. Use newInstance() factory method to create this fragment." + ) + } + } + + fun getConfig(arguments: Bundle): IterableEmbeddedViewConfig? { + // Check if any config properties exist + val hasConfig = arguments.containsKey(KEY_BG_COLOR) || + arguments.containsKey(KEY_BORDER_COLOR) || + arguments.containsKey(KEY_BORDER_WIDTH) || + arguments.containsKey(KEY_BORDER_RADIUS) || + arguments.containsKey(KEY_PRIMARY_BTN_BG) || + arguments.containsKey(KEY_PRIMARY_BTN_TEXT) || + arguments.containsKey(KEY_SECONDARY_BTN_BG) || + arguments.containsKey(KEY_SECONDARY_BTN_TEXT) || + arguments.containsKey(KEY_TITLE_COLOR) || + arguments.containsKey(KEY_BODY_COLOR) + + return if (hasConfig) { + IterableEmbeddedViewConfig( + backgroundColor = arguments.getIntOrNull(KEY_BG_COLOR), + borderColor = arguments.getIntOrNull(KEY_BORDER_COLOR), + borderWidth = arguments.getIntOrNull(KEY_BORDER_WIDTH), + borderCornerRadius = arguments.getFloatOrNull(KEY_BORDER_RADIUS), + primaryBtnBackgroundColor = arguments.getIntOrNull(KEY_PRIMARY_BTN_BG), + primaryBtnTextColor = arguments.getIntOrNull(KEY_PRIMARY_BTN_TEXT), + secondaryBtnBackgroundColor = arguments.getIntOrNull(KEY_SECONDARY_BTN_BG), + secondaryBtnTextColor = arguments.getIntOrNull(KEY_SECONDARY_BTN_TEXT), + titleTextColor = arguments.getIntOrNull(KEY_TITLE_COLOR), + bodyTextColor = arguments.getIntOrNull(KEY_BODY_COLOR) + ) + } else { + null + } + } + + private fun Bundle.putConfig(config: IterableEmbeddedViewConfig?) { + config?.let { cfg -> + cfg.backgroundColor?.let { putInt(KEY_BG_COLOR, it) } + cfg.borderColor?.let { putInt(KEY_BORDER_COLOR, it) } + cfg.borderWidth?.let { putInt(KEY_BORDER_WIDTH, it) } + cfg.borderCornerRadius?.let { putFloat(KEY_BORDER_RADIUS, it) } + cfg.primaryBtnBackgroundColor?.let { putInt(KEY_PRIMARY_BTN_BG, it) } + cfg.primaryBtnTextColor?.let { putInt(KEY_PRIMARY_BTN_TEXT, it) } + cfg.secondaryBtnBackgroundColor?.let { putInt(KEY_SECONDARY_BTN_BG, it) } + cfg.secondaryBtnTextColor?.let { putInt(KEY_SECONDARY_BTN_TEXT, it) } + cfg.titleTextColor?.let { putInt(KEY_TITLE_COLOR, it) } + cfg.bodyTextColor?.let { putInt(KEY_BODY_COLOR, it) } + } + } + + private fun Bundle.getIntOrNull(key: String): Int? { + return if (containsKey(key)) getInt(key) else null + } + + private fun Bundle.getFloatOrNull(key: String): Float? { + return if (containsKey(key)) getFloat(key) else null + } +} diff --git a/iterableapi-ui/src/main/java/com/iterable/iterableapi/ui/embedded/IterableEmbeddedViewConfig.kt b/iterableapi-ui/src/main/java/com/iterable/iterableapi/ui/embedded/IterableEmbeddedViewConfig.kt index 07527a409..7b0b9f5f1 100644 --- a/iterableapi-ui/src/main/java/com/iterable/iterableapi/ui/embedded/IterableEmbeddedViewConfig.kt +++ b/iterableapi-ui/src/main/java/com/iterable/iterableapi/ui/embedded/IterableEmbeddedViewConfig.kt @@ -1,7 +1,5 @@ package com.iterable.iterableapi.ui.embedded -import android.graphics.Color - data class IterableEmbeddedViewConfig( val backgroundColor: Int?, val borderColor: Int?,