Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -5,6 +5,7 @@ import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator
import android.os.Build
import android.view.View
import android.view.animation.PathInterpolator
import cloud.mindbox.mobile_sdk.models.Milliseconds
import cloud.mindbox.mobile_sdk.utils.Constants

Expand All @@ -19,10 +20,13 @@ internal open class EmbeddedBlockRevealAnimation(
val isEnabled: Boolean
get() = areSystemAnimationsEnabled()

private val revealInterpolator = PathInterpolator(0.42f, 0f, 0.58f, 1f)
Comment thread
sergeysozinov marked this conversation as resolved.

open fun crossfade(incoming: View, outgoing: View?, onEnd: () -> Unit): Animator =
ValueAnimator.ofFloat(0f, 1f).apply {
incoming.alpha = 0f
this.duration = this@EmbeddedBlockRevealAnimation.duration.interval
interpolator = revealInterpolator
addUpdateListener { animator ->
val shown = animator.animatedValue as Float
incoming.alpha = shown
Expand All @@ -43,6 +47,7 @@ internal open class EmbeddedBlockRevealAnimation(
open fun growHeight(view: View, targetHeight: Int, onEnd: () -> Unit): Animator =
ValueAnimator.ofInt(0, targetHeight).apply {
this.duration = this@EmbeddedBlockRevealAnimation.duration.interval
interpolator = revealInterpolator
addUpdateListener { animator ->
view.layoutParams?.let { params ->
params.height = animator.animatedValue as Int
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ import cloud.mindbox.mobile_sdk.inapp.data.managers.SessionStorageManager
import cloud.mindbox.mobile_sdk.inapp.data.mapper.InAppMapper
import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.managers.GeoSerializationManager
import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.repositories.InAppGeoRepository
import cloud.mindbox.mobile_sdk.inapp.domain.models.GeoError
import cloud.mindbox.mobile_sdk.inapp.domain.models.GeoFetchStatus
import cloud.mindbox.mobile_sdk.inapp.domain.models.GeoTargeting
import cloud.mindbox.mobile_sdk.managers.DbManager
import cloud.mindbox.mobile_sdk.managers.GatewayManager
import cloud.mindbox.mobile_sdk.repository.MindboxPreferences
import cloud.mindbox.mobile_sdk.utils.LoggingExceptionHandler
import cloud.mindbox.mobile_sdk.utils.Constants
import com.android.volley.TimeoutError
import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
Expand All @@ -26,18 +30,23 @@ internal class InAppGeoRepositoryImpl(
private val geoMutex = Mutex()

override suspend fun fetchGeo() = geoMutex.withLock {
if (sessionStorageManager.state.geoFetchStatus == GeoFetchStatus.GEO_FETCH_SUCCESS) {
val state = sessionStorageManager.state
if (state.geoFetchStatus != GeoFetchStatus.GEO_NOT_FETCHED) {
return@withLock
}
val configuration = DbManager.listenConfigurations().first()
val geoTargeting = inAppMapper.mapGeoTargetingDtoToGeoTargeting(
geoTargetingDto = gatewayManager.checkGeoTargeting(
configuration = configuration
)
)
val geoTargetingDto = try {
withTimeoutOrNull(Constants.targetingFetchWaitLimit.interval) {
gatewayManager.checkGeoTargeting(configuration = configuration)
} ?: throw GeoError(TimeoutError())
} catch (error: GeoError) {
state.geoFetchStatus = GeoFetchStatus.GEO_FETCH_ERROR
throw error
}
Comment thread
sergeysozinov marked this conversation as resolved.
val geoTargeting = inAppMapper.mapGeoTargetingDtoToGeoTargeting(geoTargetingDto = geoTargetingDto)
MindboxPreferences.inAppGeo =
geoSerializationManager.serializeToGeoString(geoTargeting)
sessionStorageManager.state.geoFetchStatus = GeoFetchStatus.GEO_FETCH_SUCCESS
state.geoFetchStatus = GeoFetchStatus.GEO_FETCH_SUCCESS
}

override fun setGeoStatus(status: GeoFetchStatus) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cloud.mindbox.mobile_sdk.inapp.data.repositories
import cloud.mindbox.mobile_sdk.inapp.data.managers.SessionStorageManager
import cloud.mindbox.mobile_sdk.inapp.data.mapper.InAppMapper
import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.repositories.InAppSegmentationRepository
import cloud.mindbox.mobile_sdk.inapp.domain.models.CustomerSegmentationError
import cloud.mindbox.mobile_sdk.inapp.domain.models.CustomerSegmentationFetchStatus
import cloud.mindbox.mobile_sdk.inapp.domain.models.CustomerSegmentationInApp
import cloud.mindbox.mobile_sdk.inapp.domain.models.ProductSegmentationFetchStatus
Expand All @@ -11,6 +12,9 @@ import cloud.mindbox.mobile_sdk.logger.MindboxLoggerImpl
import cloud.mindbox.mobile_sdk.managers.DbManager
import cloud.mindbox.mobile_sdk.managers.GatewayManager
import cloud.mindbox.mobile_sdk.utils.LoggingExceptionHandler
import cloud.mindbox.mobile_sdk.utils.Constants
import com.android.volley.TimeoutError
import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
Expand All @@ -24,35 +28,35 @@ internal class InAppSegmentationRepositoryImpl(
private val customerSegmentationsMutex = Mutex()

override suspend fun fetchCustomerSegmentations() = customerSegmentationsMutex.withLock {
if (sessionStorageManager.state.customerSegmentationFetchStatus ==
CustomerSegmentationFetchStatus.SEGMENTATION_FETCH_SUCCESS
) {
val state = sessionStorageManager.state
if (state.customerSegmentationFetchStatus != CustomerSegmentationFetchStatus.SEGMENTATION_NOT_FETCHED) {
return@withLock
}
if (sessionStorageManager.state.currentSessionInApps.isEmpty()) {
if (state.currentSessionInApps.isEmpty()) {
MindboxLoggerImpl.d(
this,
"No unshown inapps. Do not request segmentations"
)
sessionStorageManager.state.customerSegmentationFetchStatus =
CustomerSegmentationFetchStatus.SEGMENTATION_FETCH_ERROR
return@withLock
}
MindboxLoggerImpl.d(
this,
"Request segmentations"
)
val configuration = DbManager.listenConfigurations().first()
val response = gatewayManager.checkCustomerSegmentations(
configuration = configuration,
segmentationCheckRequest = inAppMapper.mapToCustomerSegmentationCheckRequest(
sessionStorageManager.state.currentSessionInApps
)
)
sessionStorageManager.state.inAppCustomerSegmentations =
inAppMapper.mapToSegmentationCheck(response)
sessionStorageManager.state.customerSegmentationFetchStatus =
CustomerSegmentationFetchStatus.SEGMENTATION_FETCH_SUCCESS
val response = try {
withTimeoutOrNull(Constants.targetingFetchWaitLimit.interval) {
gatewayManager.checkCustomerSegmentations(
configuration = configuration,
segmentationCheckRequest = inAppMapper.mapToCustomerSegmentationCheckRequest(state.currentSessionInApps)
)
} ?: throw CustomerSegmentationError(TimeoutError())
} catch (error: CustomerSegmentationError) {
state.customerSegmentationFetchStatus = CustomerSegmentationFetchStatus.SEGMENTATION_FETCH_ERROR
throw error
}
Comment thread
sergeysozinov marked this conversation as resolved.
state.inAppCustomerSegmentations = inAppMapper.mapToSegmentationCheck(response)
state.customerSegmentationFetchStatus = CustomerSegmentationFetchStatus.SEGMENTATION_FETCH_SUCCESS
return@withLock
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.repositories.InAppReposi
import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.repositories.MobileConfigRepository
import cloud.mindbox.mobile_sdk.inapp.domain.models.Frequency
import cloud.mindbox.mobile_sdk.inapp.domain.models.InApp
import cloud.mindbox.mobile_sdk.inapp.domain.models.Layer
import cloud.mindbox.mobile_sdk.inapp.domain.models.InAppType
import cloud.mindbox.mobile_sdk.inapp.domain.models.TargetingDataWrapper
import cloud.mindbox.mobile_sdk.logger.MindboxLog
import cloud.mindbox.mobile_sdk.millisToTimeSpan
import cloud.mindbox.mobile_sdk.models.Milliseconds
Expand Down Expand Up @@ -54,9 +54,6 @@ internal class InAppInteractorImpl(

private val inAppTargetingChannel = Channel<InAppEventType>(Channel.UNLIMITED)

private val placeRequestTargetingData =
TargetingDataWrapper(InAppEventType.EmbeddedPlaceRequested.EVENT_NAME)

override suspend fun processEventAndConfig(): Flow<Pair<InApp, Milliseconds>> {
val inApps: List<InApp> = mobileConfigRepository.getInAppsSection()
.let { inApps ->
Expand Down Expand Up @@ -113,6 +110,9 @@ internal class InAppInteractorImpl(
inApp?.let {
sessionStorageManager.state.inAppTriggerEvent = event
}
if (inApp?.overlayHostsWebPage() == true) {
inAppProcessingManager.prefetchTargetingDependencies(inAppRepository.getCurrentSessionInApps())
}
inApp?.let { inapp -> inapp to timeProvider.elapsedSince(triggerTimeMillis) }
}
.onEach { pair ->
Expand All @@ -125,15 +125,28 @@ internal class InAppInteractorImpl(
override suspend fun selectInAppForPlace(
placeSystemName: PlaceKey,
triggerEvent: InAppEventType,
): EmbeddedResolveOutcome = selectInAppForPlace(placeSystemName, triggerEvent, isRetryAfterSessionReset = false)

private suspend fun selectInAppForPlace(
placeSystemName: PlaceKey,
triggerEvent: InAppEventType,
isRetryAfterSessionReset: Boolean,
): EmbeddedResolveOutcome {
val inApps = mobileConfigRepository.getInAppsSectionIfAvailable() ?: run {
logI("Place '$placeSystemName': the config is unavailable, the SDK has no answer for the place")
return EmbeddedResolveOutcome.ConfigUnavailable
}
val session = sessionStorageManager.state
inAppRepository.saveCurrentSessionInApps(inApps)
val trigger = placeTrigger(placeSystemName, triggerEvent)
val candidates = inAppFilteringManager.filterEmbeddedInAppsByPlace(inApps, placeSystemName)
.let { inAppFilteringManager.filterOutDirectCallInApps(it) }
if (candidates.isNotEmpty()) {
inAppProcessingManager.prefetchTargetingDependencies(inApps)
if (sessionStorageManager.state !== session && !isRetryAfterSessionReset) {
return resolveAgainForNewSession(placeSystemName, "while its targeting dependencies were fetched")
}
}
val matched = candidates.filter { candidate ->
inAppProcessingManager.matchesTargeting(candidate, trigger)
}
Expand All @@ -144,6 +157,9 @@ internal class InAppInteractorImpl(
.let { inAppFrequencyManager.filterInAppsFrequency(it) }
.sortByPriority()
.firstOrNull { candidate -> candidate.embeddedVariantFor(placeSystemName) != null }
if (sessionStorageManager.state !== session && !isRetryAfterSessionReset) {
return resolveAgainForNewSession(placeSystemName, "while its candidates were checked")
}

sendPlaceTargetings(placeSystemName, matched, winner)
if (winner == null) {
Expand All @@ -162,6 +178,12 @@ internal class InAppInteractorImpl(
return EmbeddedResolveOutcome.Content(variant = variant, delayTime = delayTime)
}

private suspend fun resolveAgainForNewSession(placeSystemName: PlaceKey, moment: String): EmbeddedResolveOutcome {
logI("Place '$placeSystemName': the session was reset $moment, resolving again as a place request of the new session")
inAppFailureTracker.clearFailures()
return selectInAppForPlace(placeSystemName, InAppEventType.EmbeddedPlaceRequested(placeSystemName), isRetryAfterSessionReset = true)
}

private fun placeTrigger(place: PlaceKey, triggerEvent: InAppEventType): InAppEventType {
if (triggerEvent is InAppEventType.OrdinalEvent) {
sessionStorageManager.state.embeddedLastOperationByPlace[place] = triggerEvent
Expand Down Expand Up @@ -197,6 +219,16 @@ internal class InAppInteractorImpl(
}
}

private fun InApp.overlayHostsWebPage(): Boolean {
val layers = when (val variant = firstOverlayVariant()) {
is InAppType.WebView -> variant.layers
is InAppType.ModalWindow -> variant.layers
is InAppType.Snackbar -> variant.layers
else -> return false
}
return layers.any { layer -> layer is Layer.WebViewLayer }
}

private fun InApp.embeddedVariantFor(place: PlaceKey): InAppType.Embedded? =
form.variants
.filterIsInstance<InAppType.Embedded>()
Expand Down Expand Up @@ -258,7 +290,11 @@ internal class InAppInteractorImpl(

override suspend fun filterShowableInAppIds(hostInAppId: String, inAppIds: List<String>): List<String> {
if (inAppIds.isEmpty()) return emptyList()
val inApps = mobileConfigRepository.getInAppsSection()
val inApps = mobileConfigRepository.getInAppsSectionIfAvailable() ?: run {
logI("Page of $hostInAppId asks about ${inAppIds.size} id(s) while the config is unavailable, cutting them all")
return emptyList()
}
inAppRepository.saveCurrentSessionInApps(inApps)
val inAppsPool = inAppABTestLogic.getInAppsPool(inApps.map { it.id })
val showableIds = inAppFilteringManager.filterABTestsInApps(inApps, inAppsPool)
.map { inApp -> inApp.id }
Expand Down Expand Up @@ -302,14 +338,7 @@ internal class InAppInteractorImpl(
}

private suspend fun matchesRequestedTargeting(inApp: InApp): Boolean =
runCatching {
inApp.targeting.fetchTargetingInfo(placeRequestTargetingData)
inApp.targeting.checkTargeting(placeRequestTargetingData)
}
.getOrElse { error ->
logI("Requested id ${inApp.id} targeting could not be checked ($error), cutting it")
false
}
inAppProcessingManager.matchesRequestedTargeting(inApp)
.also { matches -> if (!matches) logI("Requested id ${inApp.id} targeting did not match, cutting it") }

override fun reservePlaceShow(placeSystemName: PlaceKey, content: InAppType.Embedded): Boolean {
Expand Down
Loading
Loading