Skip to content

Commit

Permalink
Merge branch 'master' into retenogit/master
Browse files Browse the repository at this point in the history
  • Loading branch information
Ilia-Solodiankin committed Apr 3, 2024
2 parents f93f726 + 7e39967 commit 5f05341
Show file tree
Hide file tree
Showing 19 changed files with 368 additions and 10 deletions.
11 changes: 10 additions & 1 deletion RetenoSdkCore/src/main/java/com/reteno/core/Reteno.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.reteno.core.domain.model.user.User
import com.reteno.core.domain.model.user.UserAttributesAnonymous
import com.reteno.core.lifecycle.ScreenTrackingConfig
import com.reteno.core.features.recommendation.Recommendation
import com.reteno.core.view.iam.callback.InAppLifecycleCallback


interface Reteno {
Expand Down Expand Up @@ -80,10 +81,18 @@ interface Reteno {
fun updatePushPermissionStatus()

/**
*
* Pause or unpause In-App messages showing during app runtime.
*/
fun pauseInAppMessages(isPaused: Boolean)

/**
* Add a callback to callbacks each time In-App message is displayed.
*
* @param inAppLifecycleCallback pass an implementation of [InAppLifecycleCallback] interface
* or [null] to stop listening to callbacks.
*/
fun setInAppLifecycleCallback(inAppLifecycleCallback: InAppLifecycleCallback?)

/**
* Sends stored data without waiting for a send queue.
* But not more often than [com.reteno.core.domain.controller.ScheduleController.Companion.FORCE_PUSH_MIN_DELAY] millis.
Expand Down
6 changes: 5 additions & 1 deletion RetenoSdkCore/src/main/java/com/reteno/core/RetenoImpl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import android.app.Activity
import android.app.Application
import android.content.ComponentName
import android.content.Intent
import android.util.Log
import com.reteno.core.di.ServiceLocator
import com.reteno.core.domain.controller.ScreenTrackingController
import com.reteno.core.domain.model.ecom.EcomEvent
Expand All @@ -18,6 +17,7 @@ import com.reteno.core.util.*
import com.reteno.core.util.Constants.BROADCAST_ACTION_PUSH_PERMISSION_CHANGED
import com.reteno.core.util.Constants.BROADCAST_ACTION_RETENO_APP_RESUME
import com.reteno.core.view.iam.IamView
import com.reteno.core.view.iam.callback.InAppLifecycleCallback


class RetenoImpl @JvmOverloads constructor(
Expand Down Expand Up @@ -254,6 +254,10 @@ class RetenoImpl @JvmOverloads constructor(
iamController.pauseInAppMessages(isPaused)
}

override fun setInAppLifecycleCallback(inAppLifecycleCallback: InAppLifecycleCallback?) {
iamView.setInAppLifecycleCallback(inAppLifecycleCallback)
}

override fun forcePushData() {
if (!isOsVersionSupported()) {
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ import com.reteno.core.data.remote.mapper.toJson
import com.reteno.core.data.remote.mapper.toRemote
import com.reteno.core.domain.ResponseCallback
import com.reteno.core.domain.model.ecom.EcomEvent
import com.reteno.core.domain.model.ecom.RemoteConstants
import com.reteno.core.domain.model.event.Event
import com.reteno.core.domain.model.logevent.LogLevel
import com.reteno.core.domain.model.logevent.RetenoLogEvent
import com.reteno.core.util.Logger
import com.reteno.core.util.Util.formatToRemote
import com.reteno.core.util.Util.fromRemote
import com.reteno.core.util.isNonRepeatableError
import java.time.ZonedDateTime

Expand All @@ -26,6 +28,12 @@ internal class EventsRepositoryImpl(
private val configRepository: ConfigRepository
) : EventsRepository {

/**
* Types that are mentioned in this list will be pushed to backend with collectLatest principle,
* so the most latest item will be included and others are discarded
* */
private val distinctEventTypes = listOf(RemoteConstants.EcomEvent.EVENT_TYPE_CART_UPDATED)

override fun saveEvent(event: Event) {
/*@formatter:off*/ Logger.i(TAG, "saveEvent(): ", "event = [" , event , "]")
/*@formatter:on*/
Expand Down Expand Up @@ -68,9 +76,11 @@ internal class EventsRepositoryImpl(
/*@formatter:off*/ Logger.i(TAG, "pushEvents(): ", "events = [" , events , "]")
/*@formatter:on*/

val eventsToSend = events.distinctBy(distinctEventTypes)

apiClient.post(
ApiContract.MobileApi.Events,
events.toRemote().toJson(),
eventsToSend.toRemote().toJson(),
object : ResponseCallback {

override fun onSuccess(response: String) {
Expand Down Expand Up @@ -127,6 +137,24 @@ internal class EventsRepositoryImpl(
}
}

/**
* Take latest event for types that exist in [types]
* @param types - list of types in which only latest event should be selected
* */
private fun EventsDb.distinctBy(types: List<String>): EventsDb {
val distinctEventList = eventList
.groupBy { it.eventTypeKey }
.mapValues { entry ->
if (!types.contains(entry.key)) entry.value
listOf(entry.value.maxBy { it.occurred.fromRemote() })
}
.values
.flatten()
return copy(
eventList = distinctEventList
)
}

companion object {
private val TAG = EventsRepositoryImpl::class.java.simpleName

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import com.reteno.core.util.Logger

internal class ScreenTrackingController(private val retenoActivityHelper: RetenoActivityHelper, private val eventController: EventController) {

private var screenTrackingConfig = ScreenTrackingConfig(true)
private var screenTrackingConfig = ScreenTrackingConfig(enable = false)

private val fragmentLifecycleCallbacks = object : FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentStarted(fragmentManager: FragmentManager, fragment: Fragment) {
Expand Down
19 changes: 17 additions & 2 deletions RetenoSdkCore/src/main/java/com/reteno/core/util/BitmapUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package com.reteno.core.util
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Color
import android.util.DisplayMetrics
import android.view.WindowManager
import com.reteno.core.RetenoImpl
Expand All @@ -24,8 +26,14 @@ object BitmapUtil {
/**
* Create a scaled bitmap.
*
* Aspect ratio of the source image stays unchanged.
* If aspect ratio of a source image doesn't conform to 2:1 rule,
* centerInside scale type applied and other areas of an image will be transparent
*
* @see android.widget.ImageView.ScaleType.CENTER_INSIDE
*
* @param imageUrl The string of URL image.
* @return The scaled bitmap.
* @return The scaled bitmap with 2:1 aspect ratio.
*/
fun getScaledBitmap(imageUrl: String): Bitmap? {
val context = RetenoImpl.application
Expand All @@ -42,7 +50,14 @@ object BitmapUtil {
val pixelsWidth = min(2 * pixelsHeight, displayMetrics.widthPixels)
var bitmap: Bitmap? = getBitmapFromUrl(imageUrl, pixelsWidth, pixelsHeight)
try {
bitmap = Bitmap.createScaledBitmap(bitmap!!, pixelsWidth, pixelsHeight, true)
bitmap = resize(bitmap = bitmap!!, maxWidth = pixelsWidth, maxHeight = pixelsHeight)
val targetBitmap = Bitmap.createBitmap(pixelsWidth, pixelsHeight, Bitmap.Config.ARGB_8888)
val canvas = Canvas(targetBitmap)
canvas.drawColor(Color.TRANSPARENT)
val xOffset = (targetBitmap.width - bitmap.width) / 2f
val yOffset = (targetBitmap.height - bitmap.height) / 2f
canvas.drawBitmap(bitmap, xOffset, yOffset, null)
bitmap = targetBitmap
} catch (e: Exception) {
Logger.e(TAG, "Failed on scale image $imageUrl to ($pixelsWidth, $pixelsHeight)", e)
}
Expand Down
4 changes: 4 additions & 0 deletions RetenoSdkCore/src/main/java/com/reteno/core/util/Util.kt
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,10 @@ object Util {
return formatter.format(this)
}

fun String.fromRemote():ZonedDateTime {
return ZonedDateTime.parse(this, formatter)
}

fun formatSqlDateToTimestamp(sqlDate: String): Long {
return sqlToTimestampFormat.parse(sqlDate)?.time ?: 0L
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ package com.reteno.core.view.iam

import android.app.Activity
import com.reteno.core.data.remote.model.iam.message.InAppMessage
import com.reteno.core.data.remote.model.iam.message.InAppMessageResponse
import com.reteno.core.data.remote.model.iam.message.InAppMessageContent
import com.reteno.core.view.iam.callback.InAppLifecycleCallback

interface IamView {

Expand All @@ -16,4 +15,6 @@ interface IamView {
fun resume(activity: Activity)

fun pause(activity: Activity)

fun setInAppLifecycleCallback(inAppLifecycleCallback: InAppLifecycleCallback?)
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ import com.reteno.core.lifecycle.RetenoActivityHelper
import com.reteno.core.util.Constants
import com.reteno.core.util.Logger
import com.reteno.core.util.queryBroadcastReceivers
import com.reteno.core.view.iam.callback.InAppCloseAction
import com.reteno.core.view.iam.callback.InAppCloseData
import com.reteno.core.view.iam.callback.InAppData
import com.reteno.core.view.iam.callback.InAppErrorData
import com.reteno.core.view.iam.callback.InAppLifecycleCallback
import com.reteno.core.view.iam.callback.InAppSource
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancelChildren
Expand Down Expand Up @@ -64,7 +70,11 @@ internal class IamViewImpl(

private val isViewShown = AtomicBoolean(false)

private var inAppLifecycleCallback: InAppLifecycleCallback? = null

private var inAppSource: InAppSource? = null
private var interactionId: String? = null
private var messageId: Long? = null
private var messageInstanceId: Long? = null
private lateinit var parentLayout: FrameLayout
private lateinit var popupWindow: PopupWindow
Expand Down Expand Up @@ -101,6 +111,7 @@ internal class IamViewImpl(
/*@formatter:off*/ Logger.i(TAG, "onWidgetInitSuccess(): ", "")
/*@formatter:on*/
showIamPopupWindowOnceReady(DELAY_UI_ATTEMPTS)
inAppLifecycleCallback?.onDisplay(createInAppData())
messageInstanceId?.let { instanceId ->
val newInteractionId = UUID.randomUUID().toString()
interactionId = newInteractionId
Expand All @@ -111,6 +122,7 @@ internal class IamViewImpl(
private fun onWidgetInitFailed(jsEvent: IamJsEvent) {
/*@formatter:off*/ Logger.i(TAG, "onWidgetInitFailed(): ", "jsEvent = [", jsEvent, "]")
/*@formatter:on*/
inAppLifecycleCallback?.onError(createInAppErrorData())
iamController.widgetInitFailed(jsEvent)
messageInstanceId?.let { instanceId ->
val newInteractionId = UUID.randomUUID().toString()
Expand Down Expand Up @@ -145,7 +157,9 @@ internal class IamViewImpl(
private fun closeWidget(payload: IamJsPayload?) {
/*@formatter:off*/ Logger.i(TAG, "closeWidget(): ", "payload = [", payload, "]")
/*@formatter:on*/
inAppLifecycleCallback?.beforeClose(createInAppCloseData(InAppCloseAction.CLOSE_BUTTON))
teardown()
inAppLifecycleCallback?.afterClose(createInAppCloseData(InAppCloseAction.CLOSE_BUTTON))
}

override fun isViewShown(): Boolean {
Expand All @@ -162,6 +176,8 @@ internal class IamViewImpl(
teardown()
}
this.interactionId = interactionId
inAppSource = InAppSource.PUSH_NOTIFICATION
messageId = null
messageInstanceId = null

OperationQueue.addUiOperation {
Expand All @@ -173,6 +189,7 @@ internal class IamViewImpl(
initViewOnResume = true
}
}
inAppLifecycleCallback?.beforeDisplay(createInAppData())
iamController.fetchIamFullHtml(interactionId)
}
} catch (e: Exception) {
Expand All @@ -195,8 +212,11 @@ internal class IamViewImpl(
activityHelper.currentActivity?.let {
createIamInActivity(it)
}
messageId = inAppMessage.messageId
messageInstanceId = inAppMessage.messageInstanceId
inAppSource = InAppSource.DISPLAY_RULES
interactionId = null
inAppLifecycleCallback?.beforeDisplay(createInAppData())
iamController.fetchIamFullHtml(inAppMessage.content)
}
} catch (e: Exception) {
Expand Down Expand Up @@ -237,6 +257,9 @@ internal class IamViewImpl(
}
}

override fun setInAppLifecycleCallback(inAppLifecycleCallback: InAppLifecycleCallback?) {
this.inAppLifecycleCallback = inAppLifecycleCallback
}

private fun showIamPopupWindowOnceReady(attempts: Int) {
/*@formatter:off*/ Logger.i(TAG, "showIamPopupWindowOnceReady(): ", "attempts = [", attempts, "]")
Expand Down Expand Up @@ -399,6 +422,16 @@ internal class IamViewImpl(
private fun tryHandleCustomData(url: String?, customData: Map<String, String>?): Boolean {
val bundle = Bundle()
bundle.putString("url", url)
inAppSource?.let { source ->
bundle.putString("inapp_source", source.name)
if (source == InAppSource.PUSH_NOTIFICATION) {
interactionId?.let { bundle.putString("inapp_id", it) }
} else {
messageId?.let { bundle.putString("inapp_id", it.toString()) }
}

}

customData?.entries?.forEach { entry ->
bundle.putString(entry.key, entry.value)
}
Expand Down Expand Up @@ -434,6 +467,30 @@ internal class IamViewImpl(
}
}

private fun createInAppData(): InAppData {
return when (inAppSource) {
InAppSource.PUSH_NOTIFICATION -> InAppData(InAppSource.PUSH_NOTIFICATION, interactionId)
InAppSource.DISPLAY_RULES -> InAppData(InAppSource.DISPLAY_RULES, messageInstanceId?.toString())
else -> InAppData(InAppSource.DISPLAY_RULES, messageInstanceId?.toString())
}
}

private fun createInAppCloseData(closeAction: InAppCloseAction): InAppCloseData {
return when (inAppSource) {
InAppSource.PUSH_NOTIFICATION -> InAppCloseData(InAppSource.PUSH_NOTIFICATION, interactionId, closeAction)
InAppSource.DISPLAY_RULES -> InAppCloseData(InAppSource.DISPLAY_RULES, messageInstanceId?.toString(), closeAction)
else -> InAppCloseData(InAppSource.DISPLAY_RULES, messageInstanceId?.toString(), closeAction)
}
}

private fun createInAppErrorData(): InAppErrorData {
return when (inAppSource) {
InAppSource.PUSH_NOTIFICATION -> InAppErrorData(InAppSource.PUSH_NOTIFICATION, interactionId, ERROR_MESSAGE)
InAppSource.DISPLAY_RULES -> InAppErrorData(InAppSource.DISPLAY_RULES, messageInstanceId?.toString(), ERROR_MESSAGE)
else -> InAppErrorData(InAppSource.DISPLAY_RULES, messageInstanceId?.toString(), ERROR_MESSAGE)
}
}

companion object {
private val TAG: String = IamViewImpl::class.java.simpleName

Expand All @@ -444,6 +501,8 @@ internal class IamViewImpl(
private const val ENCODING = "base64"
private const val JS_INTERFACE_NAME = "RetenoAndroidHandler"

private const val ERROR_MESSAGE = "Failed to load In-App message."

private fun dpToPx(dp: Int): Int {
return (dp * Resources.getSystem().displayMetrics.density).toInt()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.reteno.core.view.iam.callback

enum class InAppCloseAction {
OPEN_URL, BUTTON, CLOSE_BUTTON
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.reteno.core.view.iam.callback

class InAppCloseData(
source: InAppSource,
id: String?,
val closeAction: InAppCloseAction
) : InAppData(source, id)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.reteno.core.view.iam.callback

open class InAppData(
val source: InAppSource,
val id: String?
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.reteno.core.view.iam.callback

class InAppErrorData(
source: InAppSource,
id: String?,
val errorMessage: String
): InAppData(source, id)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.reteno.core.view.iam.callback

interface InAppLifecycleCallback {
fun beforeDisplay(inAppData: InAppData)
fun onDisplay(inAppData: InAppData)
fun beforeClose(closeData: InAppCloseData)
fun afterClose(closeData: InAppCloseData)
fun onError(errorData: InAppErrorData)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.reteno.core.view.iam.callback

enum class InAppSource {
PUSH_NOTIFICATION, DISPLAY_RULES
}

0 comments on commit 5f05341

Please sign in to comment.