diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNotification.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNotification.kt index 5cf4b380a..15df9e637 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNotification.kt +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNotification.kt @@ -11,6 +11,7 @@ import com.clevertap.android.sdk.inapp.CTInAppNotificationMedia.Companion.create import com.clevertap.android.sdk.inapp.customtemplates.CustomTemplateInAppData import com.clevertap.android.sdk.inapp.customtemplates.CustomTemplateInAppData.CREATOR.createFromJson import com.clevertap.android.sdk.utils.getStringOrNull +import com.clevertap.android.sdk.utils.toValidColorOrFallback import org.json.JSONException import org.json.JSONObject import kotlin.reflect.KClass @@ -192,10 +193,10 @@ class CTInAppNotification : Parcelable { } type = parcel.readString() title = parcel.readString() - titleColor = parcel.readString() ?: titleColor - backgroundColor = parcel.readString() ?: backgroundColor + titleColor = (parcel.readString() ?: titleColor).toValidColorOrFallback(titleColor); + backgroundColor = (parcel.readString() ?: backgroundColor).toValidColorOrFallback(backgroundColor); message = parcel.readString() - messageColor = parcel.readString() ?: messageColor + messageColor = (parcel.readString() ?: messageColor).toValidColorOrFallback(messageColor); try { _buttons = parcel.createTypedArrayList(CTInAppNotificationButton.CREATOR) @@ -352,7 +353,7 @@ class CTInAppNotification : Parcelable { maxPerSession = jsonObject.optInt(Constants.INAPP_MAX_DISPLAY_COUNT, -1) inAppType = CTInAppType.fromString(type) isTablet = jsonObject.optBoolean(Constants.KEY_IS_TABLET, false) - backgroundColor = jsonObject.optString(Constants.KEY_BG, backgroundColor) + backgroundColor = jsonObject.optString(Constants.KEY_BG, backgroundColor).toValidColorOrFallback(backgroundColor); isPortrait = !jsonObject.has(Constants.KEY_PORTRAIT) || jsonObject.getBoolean( Constants.KEY_PORTRAIT ) @@ -362,13 +363,13 @@ class CTInAppNotification : Parcelable { val titleObject = jsonObject.optJSONObject(Constants.KEY_TITLE) if (titleObject != null) { title = titleObject.optString(Constants.KEY_TEXT, "") - titleColor = titleObject.optString(Constants.KEY_COLOR, titleColor) + titleColor = titleObject.optString(Constants.KEY_COLOR, titleColor).toValidColorOrFallback(titleColor); } val msgObject = jsonObject.optJSONObject(Constants.KEY_MESSAGE) if (msgObject != null) { message = msgObject.optString(Constants.KEY_TEXT, "") - messageColor = msgObject.optString(Constants.KEY_COLOR, messageColor) + messageColor = msgObject.optString(Constants.KEY_COLOR, messageColor).toValidColorOrFallback(messageColor); } isHideCloseButton = jsonObject.optBoolean(Constants.KEY_HIDE_CLOSE, false) diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/utils/ColorUtils.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/utils/ColorUtils.kt new file mode 100644 index 000000000..b064aa2b0 --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/utils/ColorUtils.kt @@ -0,0 +1,129 @@ +package com.clevertap.android.sdk.utils + +import android.graphics.Color as GraphicColor +import androidx.annotation.ColorInt +import androidx.core.graphics.toColorInt + +/** + * Safely converts a [String] to a color integer. + * + * This function attempts to parse a color string (e.g., "#RRGGBB" or "#AARRGGBB") + * into an Android color integer. If the string is null, blank, or invalid, it returns + * the provided [defaultColor]. + * + * Example: + * ``` + * val color = "#FF0000".toColorIntOrDefault() // Returns red + * val invalid = "redcolor".toColorIntOrDefault(Color.GRAY) // Returns gray + * ``` + * + * @receiver The color string to parse (e.g., "#FFFFFF", "#80FF0000"). + * @param defaultColor The fallback color if parsing fails (default is [GraphicColor.WHITE]). + * @return The parsed color integer or [defaultColor] if parsing fails. + */ +@ColorInt +fun String?.toColorIntOrDefault(@ColorInt defaultColor: Int = GraphicColor.WHITE): Int { + // If the string is null or empty, return the default color immediately + if (this.isNullOrBlank()) return defaultColor + + return try { + // Try converting the string to a color integer + this.toColorInt() + } catch (e: IllegalArgumentException) { + // Handles invalid color format (e.g., "abc123") + defaultColor + } catch (e: StringIndexOutOfBoundsException) { + // Handles malformed color strings (e.g., "#F") + defaultColor + } +} + +/** + * Safely validates a color string and returns a valid color string. + * + * If this string is a valid color (e.g. "#RRGGBB" or "#AARRGGBB"), it's returned as-is. + * If it's null, blank, or invalid, the provided [fallback] color string is returned instead. + * + * Example: + * ``` + * val valid = "#FF0000".toValidColorOrFallback("#FFFFFF") // "#FF0000" + * val invalid = "notacolor".toValidColorOrFallback("#FFFFFF") // "#FFFFFF" + * val empty = "".toValidColorOrFallback("#FFFFFF") // "#FFFFFF" + * ``` + * + * @receiver The color string to validate (e.g. "#FFFFFF", "#AARRGGBB") + * @param fallback The fallback color string if invalid + * @return A valid color string + */ +fun String?.toValidColorOrFallback(fallback: String): String { + if (this.isNullOrBlank()) return fallback + + return try { + this.toColorInt() // Validate by trying to convert + this // Valid, return same string + } catch (e: IllegalArgumentException) { + fallback + } catch (e: StringIndexOutOfBoundsException) { + fallback + } +} + +/** + * Safely parses a color string into an Android color integer. + * + * This utility provides two overloads: + * - [parseColor(colorString, defaultColor)] → returns the parsed color or a provided fallback color. + * - [parseColor(colorString)] → same as above but defaults to [Color.WHITE] on failure. + * + * Supported formats: + * ``` + * #RRGGBB + * #AARRGGBB + * ``` + * + * Example: + * ``` + * val red = parseColor("#FF0000") // Returns red + * val semiTransparent = parseColor("#80FF0000") // Returns semi-transparent red + * val invalid = parseColor("notacolor", Color.GRAY) // Returns gray + * ``` + */ +object Color { + + /** + * Safely parses a color string into a color integer. + * + * @param colorString The color string to parse (e.g. "#FFFFFF", "#AARRGGBB"). + * @param defaultColor The fallback color if parsing fails. + * @return The parsed color integer, or [defaultColor] if invalid or null. + */ + @ColorInt + fun parseColor(colorString: String?, @ColorInt defaultColor: Int): Int { + // Return default color if the string is null or blank + if (colorString.isNullOrBlank()) { + return defaultColor + } + + return try { + // Attempt to parse the color string + colorString.toColorInt() + } catch (e: IllegalArgumentException) { + // Thrown if the string is not a valid color format + defaultColor + } catch (e: StringIndexOutOfBoundsException) { + // Thrown if the string is malformed (e.g. incomplete hex) + defaultColor + } + } + + /** + * Parses a color string, defaulting to [GraphicColor.WHITE] if parsing fails. + * + * @param colorString The color string to parse. + * @return The parsed color integer, or [GraphicColor.WHITE] if invalid or null. + */ + @ColorInt + fun parseColor(colorString: String?): Int { + return parseColor(colorString, GraphicColor.WHITE) + } +}