Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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 @@ -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
Expand Down Expand Up @@ -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>(CTInAppNotificationButton.CREATOR)
Expand Down Expand Up @@ -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
)
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package com.clevertap.android.sdk.utils

import android.graphics.Color
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 [Color.WHITE]).
* @return The parsed color integer or [defaultColor] if parsing fails.
*/
@ColorInt
fun String?.toColorIntOrDefault(@ColorInt defaultColor: Int = Color.WHITE): Int {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@suriya

Making default colour as white for everywhere won't work since the text might have default colour as black

Also we have defined default colours in CTInappNotification.kt
Again defining separate default colours in the extension function will be duplicate. In the future if I have to change the default colour I will have to change it in 2 places. A developer might miss this and hence could be erroneous

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay @Anush-Shand, Doing as per the comments and making changes considering the default colors in CTInappNotification.kt.

// 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 [Color.WHITE] if parsing fails.
*
* @param colorString The color string to parse.
* @return The parsed color integer, or [Color.WHITE] if invalid or null.
*/
@ColorInt
fun parseColor(colorString: String?): Int {
return parseColor(colorString, Color.WHITE)
}
}
Loading