diff --git a/messaginginapp/api/messaginginapp.api b/messaginginapp/api/messaginginapp.api index 5727ca994..b84839abb 100644 --- a/messaginginapp/api/messaginginapp.api +++ b/messaginginapp/api/messaginginapp.api @@ -94,17 +94,19 @@ public final class io/customer/messaginginapp/gist/data/listeners/Queue : io/cus } public final class io/customer/messaginginapp/gist/data/model/GistProperties { - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/customer/messaginginapp/gist/data/model/MessagePosition;Z)V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/customer/messaginginapp/gist/data/model/MessagePosition;ZLjava/lang/String;)V public final fun component1 ()Ljava/lang/String; public final fun component2 ()Ljava/lang/String; public final fun component3 ()Ljava/lang/String; public final fun component4 ()Lio/customer/messaginginapp/gist/data/model/MessagePosition; public final fun component5 ()Z - public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/customer/messaginginapp/gist/data/model/MessagePosition;Z)Lio/customer/messaginginapp/gist/data/model/GistProperties; - public static synthetic fun copy$default (Lio/customer/messaginginapp/gist/data/model/GistProperties;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/customer/messaginginapp/gist/data/model/MessagePosition;ZILjava/lang/Object;)Lio/customer/messaginginapp/gist/data/model/GistProperties; + public final fun component6 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/customer/messaginginapp/gist/data/model/MessagePosition;ZLjava/lang/String;)Lio/customer/messaginginapp/gist/data/model/GistProperties; + public static synthetic fun copy$default (Lio/customer/messaginginapp/gist/data/model/GistProperties;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/customer/messaginginapp/gist/data/model/MessagePosition;ZLjava/lang/String;ILjava/lang/Object;)Lio/customer/messaginginapp/gist/data/model/GistProperties; public fun equals (Ljava/lang/Object;)Z public final fun getCampaignId ()Ljava/lang/String; public final fun getElementId ()Ljava/lang/String; + public final fun getOverlayColor ()Ljava/lang/String; public final fun getPersistent ()Z public final fun getPosition ()Lio/customer/messaginginapp/gist/data/model/MessagePosition; public final fun getRouteRule ()Ljava/lang/String; diff --git a/messaginginapp/src/main/java/io/customer/messaginginapp/gist/data/model/Message.kt b/messaginginapp/src/main/java/io/customer/messaginginapp/gist/data/model/Message.kt index 4d96cb8b3..d2d6f9903 100644 --- a/messaginginapp/src/main/java/io/customer/messaginginapp/gist/data/model/Message.kt +++ b/messaginginapp/src/main/java/io/customer/messaginginapp/gist/data/model/Message.kt @@ -13,7 +13,9 @@ data class GistProperties( val elementId: String?, val campaignId: String?, val position: MessagePosition, - val persistent: Boolean + val persistent: Boolean, + // This color is formated as #RRGGBBAA + val overlayColor: String? ) data class Message( @@ -32,6 +34,7 @@ data class Message( var campaignId: String? = null var position: MessagePosition = MessagePosition.CENTER var persistent = false + var overlayColor: String? = null (properties?.get("gist") as? Map)?.let { gistProperties -> gistProperties["routeRuleAndroid"]?.let { rule -> @@ -59,8 +62,20 @@ data class Message( persistent = persistentValue } } + gistProperties["overlayColor"]?.let { id -> + (id as? String)?.let { color -> + overlayColor = color + } + } } - return GistProperties(routeRule = routeRule, elementId = elementId, campaignId = campaignId, position = position, persistent = persistent) + return GistProperties( + routeRule = routeRule, + elementId = elementId, + campaignId = campaignId, + position = position, + persistent = persistent, + overlayColor = overlayColor + ) } override fun toString(): String { diff --git a/messaginginapp/src/main/java/io/customer/messaginginapp/gist/presentation/GistModalActivity.kt b/messaginginapp/src/main/java/io/customer/messaginginapp/gist/presentation/GistModalActivity.kt index f8056e2e5..5f9db1f6c 100644 --- a/messaginginapp/src/main/java/io/customer/messaginginapp/gist/presentation/GistModalActivity.kt +++ b/messaginginapp/src/main/java/io/customer/messaginginapp/gist/presentation/GistModalActivity.kt @@ -1,6 +1,5 @@ package io.customer.messaginginapp.gist.presentation -import android.animation.AnimatorInflater import android.content.Context import android.content.Intent import android.os.Bundle @@ -11,12 +10,13 @@ import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.AppCompatActivity import androidx.core.animation.doOnEnd import com.google.gson.Gson -import io.customer.messaginginapp.R import io.customer.messaginginapp.databinding.ActivityGistBinding import io.customer.messaginginapp.di.inAppMessagingManager import io.customer.messaginginapp.gist.data.model.Message import io.customer.messaginginapp.gist.data.model.MessagePosition import io.customer.messaginginapp.gist.utilities.ElapsedTimer +import io.customer.messaginginapp.gist.utilities.MessageOverlayColorParser +import io.customer.messaginginapp.gist.utilities.ModalAnimationUtil import io.customer.messaginginapp.state.InAppMessagingAction import io.customer.messaginginapp.state.InAppMessagingState import io.customer.messaginginapp.state.MessageState @@ -128,14 +128,13 @@ class GistModalActivity : AppCompatActivity(), GistViewListener, TrackableScreen override fun finish() { logger.debug("GistModelActivity finish") runOnUiThread { - val animation = if (messagePosition == MessagePosition.TOP) { - AnimatorInflater.loadAnimator(this, R.animator.animate_out_to_top) + val animationSet = if (messagePosition == MessagePosition.TOP) { + ModalAnimationUtil.createAnimationSetOutToTop(binding.modalGistViewLayout) } else { - AnimatorInflater.loadAnimator(this, R.animator.animate_out_to_bottom) + ModalAnimationUtil.createAnimationSetOutToBottom(binding.modalGistViewLayout) } - animation.setTarget(binding.modalGistViewLayout) - animation.start() - animation.doOnEnd { + animationSet.start() + animationSet.doOnEnd { logger.debug("GistModelActivity finish animation completed") super.finish() } @@ -169,14 +168,16 @@ class GistModalActivity : AppCompatActivity(), GistViewListener, TrackableScreen runOnUiThread { window.clearFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE) binding.modalGistViewLayout.visibility = View.VISIBLE - val animation = if (messagePosition == MessagePosition.TOP) { - AnimatorInflater.loadAnimator(this, R.animator.animate_in_from_top) + + val overlayColor = MessageOverlayColorParser.parseColor(message.gistProperties.overlayColor) + ?: ModalAnimationUtil.FALLBACK_COLOR_STRING + val animatorSet = if (messagePosition == MessagePosition.TOP) { + ModalAnimationUtil.createAnimationSetInFromTop(binding.modalGistViewLayout, overlayColor) } else { - AnimatorInflater.loadAnimator(this, R.animator.animate_in_from_bottom) + ModalAnimationUtil.createAnimationSetInFromBottom(binding.modalGistViewLayout, overlayColor) } - animation.setTarget(binding.modalGistViewLayout) - animation.start() - animation.doOnEnd { + animatorSet.start() + animatorSet.doOnEnd { logger.debug("GistModelActivity Message Animation Completed: $message") elapsedTimer.end() } @@ -189,7 +190,7 @@ class GistModalActivity : AppCompatActivity(), GistViewListener, TrackableScreen binding.gistView.stopLoading() } // and finish the activity without performing any further actions - super.finish() + finish() } override fun onGistViewSizeChanged(width: Int, height: Int) { diff --git a/messaginginapp/src/main/java/io/customer/messaginginapp/gist/utilities/MessageOverlayColorParser.kt b/messaginginapp/src/main/java/io/customer/messaginginapp/gist/utilities/MessageOverlayColorParser.kt new file mode 100644 index 000000000..2c3c54234 --- /dev/null +++ b/messaginginapp/src/main/java/io/customer/messaginginapp/gist/utilities/MessageOverlayColorParser.kt @@ -0,0 +1,31 @@ +package io.customer.messaginginapp.gist.utilities + +internal object MessageOverlayColorParser { + + /** + * The expected color is formatted as #RRGGBBAA with alpha channel at the end, we need + * to reformat it to be #AARRGGBB to be usable on Android + */ + fun parseColor(color: String?): String? { + if (color == null) { + return null + } + + val cleanColor = color.removePrefix("#") + + if (doesNotHaveExpectedColorCharCount(cleanColor)) { + return null + } + + val red = cleanColor.substring(0, 2) + val green = cleanColor.substring(2, 4) + val blue = cleanColor.substring(4, 6) + val alpha = if (cleanColor.length == 8) cleanColor.substring(6, 8) else "" + + return "#$alpha$red$green$blue" + } + + private fun doesNotHaveExpectedColorCharCount(color: String): Boolean { + return color.length != 6 && color.length != 8 + } +} diff --git a/messaginginapp/src/main/java/io/customer/messaginginapp/gist/utilities/ModalAnimationUtil.kt b/messaginginapp/src/main/java/io/customer/messaginginapp/gist/utilities/ModalAnimationUtil.kt new file mode 100644 index 000000000..d3ab2bad4 --- /dev/null +++ b/messaginginapp/src/main/java/io/customer/messaginginapp/gist/utilities/ModalAnimationUtil.kt @@ -0,0 +1,119 @@ +package io.customer.messaginginapp.gist.utilities + +import android.animation.AnimatorSet +import android.animation.ObjectAnimator +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.view.View +import androidx.annotation.ColorInt +import io.customer.sdk.core.di.SDKComponent + +internal object ModalAnimationUtil { + + const val FALLBACK_COLOR_STRING = "#33000000" + + private const val TRANSLATION_ANIMATION_DURATION = 150L + private const val ALPHA_ANIMATION_DURATION = 150L + private const val COLOR_ANIMATION_DURATION = 100L + + private val logger = SDKComponent.logger + + fun createAnimationSetInFromTop(target: View, overlayEndColor: String): AnimatorSet { + return createEnterAnimation(target, overlayEndColor, -100f) + } + + fun createAnimationSetInFromBottom(target: View, overlayEndColor: String): AnimatorSet { + return createEnterAnimation(target, overlayEndColor, 100f) + } + + fun createAnimationSetOutToTop(target: View): AnimatorSet { + return createExitAnimation(target, -100f) + } + + fun createAnimationSetOutToBottom(target: View): AnimatorSet { + return createExitAnimation(target, 100f) + } + + private fun createEnterAnimation( + target: View, + overlayEndColor: String, + translationYStart: Float + ): AnimatorSet { + val translationYAnimator = ObjectAnimator.ofFloat(target, View.TRANSLATION_Y, translationYStart, 0f).apply { + duration = TRANSLATION_ANIMATION_DURATION + } + val alphaAnimator = ObjectAnimator.ofFloat(target, View.ALPHA, 0f, 1f).apply { + duration = ALPHA_ANIMATION_DURATION + } + val translationAndAlphaSet = AnimatorSet().apply { + playTogether(translationYAnimator, alphaAnimator) + } + target.alpha = 0f + + val backgroundColorAnimator = ObjectAnimator.ofArgb( + target, + "backgroundColor", + Color.TRANSPARENT, + parseColorSafely(overlayEndColor) + ).apply { + duration = COLOR_ANIMATION_DURATION + startDelay = 0 + } + val colorSet = AnimatorSet().apply { + play(backgroundColorAnimator) + } + + return AnimatorSet().apply { + playSequentially(translationAndAlphaSet, colorSet) + } + } + + private fun createExitAnimation(target: View, translationYEnd: Float): AnimatorSet { + val backgroundColor = extractBackgroundColor(target) + val backgroundColorAnimator = ObjectAnimator.ofArgb( + target, + "backgroundColor", + parseColorSafely(backgroundColor), + Color.TRANSPARENT + ).apply { + duration = COLOR_ANIMATION_DURATION + startDelay = 0 + } + val colorSet = AnimatorSet().apply { + play(backgroundColorAnimator) + } + + val translationYAnimator = ObjectAnimator.ofFloat(target, View.TRANSLATION_Y, 0f, translationYEnd).apply { + duration = TRANSLATION_ANIMATION_DURATION + } + val alphaAnimator = ObjectAnimator.ofFloat(target, View.ALPHA, 1f, 0f).apply { + duration = ALPHA_ANIMATION_DURATION + } + val translationAndAlphaSet = AnimatorSet().apply { + playTogether(translationYAnimator, alphaAnimator) + } + + return AnimatorSet().apply { + playSequentially(colorSet, translationAndAlphaSet) + } + } + + @ColorInt + private fun parseColorSafely(color: String): Int { + return try { + Color.parseColor(color) + } catch (ignored: IllegalArgumentException) { + logger.error(ignored.message ?: "Error parsing in-app overlay color") + Color.parseColor(FALLBACK_COLOR_STRING) + } + } + + private fun extractBackgroundColor(target: View): String { + val backgroundDrawable = target.background + if (backgroundDrawable is ColorDrawable) { + return String.format("#%08X", backgroundDrawable.color) + } + + return FALLBACK_COLOR_STRING + } +} diff --git a/messaginginapp/src/main/res/animator/animate_in_from_bottom.xml b/messaginginapp/src/main/res/animator/animate_in_from_bottom.xml deleted file mode 100644 index 32e9ebb3f..000000000 --- a/messaginginapp/src/main/res/animator/animate_in_from_bottom.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/messaginginapp/src/main/res/animator/animate_in_from_top.xml b/messaginginapp/src/main/res/animator/animate_in_from_top.xml deleted file mode 100644 index 6ed03d648..000000000 --- a/messaginginapp/src/main/res/animator/animate_in_from_top.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/messaginginapp/src/main/res/animator/animate_out_to_bottom.xml b/messaginginapp/src/main/res/animator/animate_out_to_bottom.xml deleted file mode 100644 index 190e446cc..000000000 --- a/messaginginapp/src/main/res/animator/animate_out_to_bottom.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/messaginginapp/src/main/res/animator/animate_out_to_top.xml b/messaginginapp/src/main/res/animator/animate_out_to_top.xml deleted file mode 100644 index b3cf509b6..000000000 --- a/messaginginapp/src/main/res/animator/animate_out_to_top.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/messaginginapp/src/test/java/io/customer/messaginginapp/gist/utilities/MessageOverlayColorParserTest.kt b/messaginginapp/src/test/java/io/customer/messaginginapp/gist/utilities/MessageOverlayColorParserTest.kt new file mode 100644 index 000000000..8a7c4f118 --- /dev/null +++ b/messaginginapp/src/test/java/io/customer/messaginginapp/gist/utilities/MessageOverlayColorParserTest.kt @@ -0,0 +1,47 @@ +package io.customer.messaginginapp.gist.utilities + +import org.amshove.kluent.shouldBeEqualTo +import org.amshove.kluent.shouldBeNull +import org.junit.jupiter.api.Test + +class MessageOverlayColorParserTest { + + @Test + fun parseColor_givenInputColorIsNull_expectNullAsResult() { + MessageOverlayColorParser.parseColor(null).shouldBeNull() + } + + @Test + fun parseColor_givenInputColorIsEmpty_expectNullAsResult() { + MessageOverlayColorParser.parseColor("").shouldBeNull() + } + + @Test + fun parseColor_givenInputColorHasUnexpectedCharCount_expectNullAsResult() { + // Only colors with 6 or 8 chars are accepted + MessageOverlayColorParser.parseColor("#").shouldBeNull() + MessageOverlayColorParser.parseColor("#FF11F").shouldBeNull() + MessageOverlayColorParser.parseColor("#FF11FF1").shouldBeNull() + MessageOverlayColorParser.parseColor("#FF11FF11F").shouldBeNull() + } + + @Test + fun parseColor_givenValidInputColorWithoutAlpha_expectCorrectResult() { + MessageOverlayColorParser.parseColor("#0f5edb").shouldBeEqualTo("#0f5edb") + } + + @Test + fun parseColor_givenValidInputColorWithoutHashAndWithoutAlpha_expectCorrectResult() { + MessageOverlayColorParser.parseColor("0f5edb").shouldBeEqualTo("#0f5edb") + } + + @Test + fun parseColor_givenValidInputColorWithAlpha_expectCorrectResult() { + MessageOverlayColorParser.parseColor("#0f5edbff").shouldBeEqualTo("#ff0f5edb") + } + + @Test + fun parseColor_givenValidInputColorWithoutHashAndWithAlpha_expectCorrectResult() { + MessageOverlayColorParser.parseColor("0f5edbff").shouldBeEqualTo("#ff0f5edb") + } +}