Coverage Summary for Class: InAppMessageDelayedManager (cloud.mindbox.mobile_sdk.inapp.presentation)

Class Method, % Branch, % Line, % Instruction, %
InAppMessageDelayedManager 85.7% (6/7) 81.8% (18/22) 96% (48/50) 96.7% (147/152)
InAppMessageDelayedManager$clearSession$$inlined$launchWithLock$1 0% (0/1)
InAppMessageDelayedManager$Companion
InAppMessageDelayedManager$PendingInApp 100% (1/1) 100% (5/5) 100% (22/22)
InAppMessageDelayedManager$process-FEttOiU$$inlined$launchWithLock$1 0% (0/1)
InAppMessageDelayedManager$processQueue$$inlined$launchWithLock$1 0% (0/1)
InAppMessageDelayedManager$scheduleNextProcess$1$1 100% (1/1) 100% (2/2) 100% (15/15)
InAppMessageDelayedManager$special$$inlined$compareBy$1 0% (0/1)
InAppMessageDelayedManager$special$$inlined$thenBy$1 0% (0/1)
InAppMessageDelayedManager$special$$inlined$thenByDescending$1 0% (0/1)
Total 53.3% (8/15) 81.8% (18/22) 96.5% (55/57) 97.4% (184/189)


 package cloud.mindbox.mobile_sdk.inapp.presentation
 
 import cloud.mindbox.mobile_sdk.Mindbox
 import cloud.mindbox.mobile_sdk.inapp.domain.models.InApp
 import cloud.mindbox.mobile_sdk.logger.mindboxLogD
 import cloud.mindbox.mobile_sdk.models.Milliseconds
 import cloud.mindbox.mobile_sdk.logger.mindboxLogI
 import cloud.mindbox.mobile_sdk.pollIf
 import cloud.mindbox.mobile_sdk.utils.TimeProvider
 import cloud.mindbox.mobile_sdk.utils.launchWithLock
 import kotlinx.coroutines.*
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.asSharedFlow
 import kotlinx.coroutines.sync.Mutex
 import java.util.concurrent.PriorityBlockingQueue
 import java.util.concurrent.atomic.AtomicLong
 
 internal class InAppMessageDelayedManager(private val timeProvider: TimeProvider, dispatcher: CoroutineDispatcher) {
 
     companion object {
         private const val DEFAULT_INITIAL_CAPACITY = 2
     }
 
     private val sequenceNumber = AtomicLong(0)
     private val coroutineScope = CoroutineScope(dispatcher + SupervisorJob() + Mindbox.coroutineExceptionHandler)
     private var nextProcessQueueJob: Job? = null
     private val processingMutex = Mutex()
     private val pendingInAppComparator = compareBy<PendingInApp> { it.showTimeMillis }
         .thenByDescending { it.inApp.isPriority }
         .thenBy { it.sequenceNumber }
 
     private val pendingInApps = PriorityBlockingQueue(
         DEFAULT_INITIAL_CAPACITY,
         pendingInAppComparator
     )
 
     private val _inAppToShowFlow = MutableSharedFlow<Pair<InApp, Milliseconds>>()
     val inAppToShowFlow = _inAppToShowFlow.asSharedFlow()
 
     private data class PendingInApp(
         val inApp: InApp,
         val showTimeMillis: Long,
         val sequenceNumber: Long,
         val preparedTimeMs: Milliseconds,
     )
 
     internal fun process(inApp: InApp, preparedTimeMs: Milliseconds) {
         coroutineScope.launchWithLock(processingMutex) {
             mindboxLogD("Processing In-App: ${inApp.id}, Priority: ${inApp.isPriority}, Delay: ${inApp.delayTime}")
             val delay = inApp.delayTime?.interval ?: 0L
             val showTime = timeProvider.currentTimeMillis() + delay
 
             pendingInApps.put(
                 PendingInApp(
                     inApp = inApp,
                     showTimeMillis = showTime,
                     sequenceNumber = sequenceNumber.getAndIncrement(),
                     preparedTimeMs = preparedTimeMs,
                 )
             )
             processQueue()
         }
     }
 
     internal fun onAppResumed() {
         mindboxLogI("App resumed, re-evaluating scheduled In-Apps.")
         processQueue()
     }
 
     private fun processQueue() {
         if (pendingInApps.isEmpty()) return
         coroutineScope.launchWithLock(processingMutex) {
             nextProcessQueueJob?.cancel()
 
             val now = timeProvider.currentTimeMillis()
 
             pendingInApps.pollIf { it.showTimeMillis <= now }?.let { showCandidate ->
                 mindboxLogI("Winner found: ${showCandidate.inApp.id}. Emitting to show.")
                 _inAppToShowFlow.emit(showCandidate.inApp to showCandidate.preparedTimeMs)
 
                 do {
                     val inApp = pendingInApps.pollIf { it.showTimeMillis <= now }.also { discarded ->
                         mindboxLogI("Discarding other ready In-App: ${discarded?.inApp?.id}")
                     }
                 } while (inApp != null)
             }
             scheduleNextProcess()
         }
     }
 
     private fun scheduleNextProcess() {
         pendingInApps.peek()?.let { nextInApp ->
             val now = timeProvider.currentTimeMillis()
             val delay = (nextInApp.showTimeMillis - now).coerceAtLeast(0)
             mindboxLogI("Scheduling next In-App ${nextInApp.inApp.id} with delay: $delay ms.")
             nextProcessQueueJob = coroutineScope.launch {
                 delay(delay)
                 processQueue()
             }
         }
     }
 
     internal fun clearSession() {
         coroutineScope.launchWithLock(processingMutex) {
             nextProcessQueueJob?.cancel()
             pendingInApps.clear()
         }
     }
 }