From 9c8ffb012a5de453547f675b8a483b4b0f133a52 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara <9083456+MohamadJaara@users.noreply.github.com> Date: Tue, 11 Aug 2026 18:08:46 +0200 Subject: [PATCH] feat(session): gate remaining session observers --- .../wire/android/GlobalObserversManager.kt | 18 ++++++- .../com/wire/android/WireApplication.kt | 52 +++++++++++++++---- .../ObserveCurrentSessionAnalyticsUseCase.kt | 38 ++++++++------ .../ConversationAudioMessagePlayer.kt | 44 +++++++++++----- .../util/lifecycle/SyncLifecycleManager.kt | 17 +++++- .../android/GlobalObserversManagerTest.kt | 8 +++ .../ConversationAudioMessagePlayerTest.kt | 16 +++++- ...gMessagesAfterForegroundSyncUseCaseTest.kt | 8 +++ .../lifecycle/SyncLifecycleManagerTest.kt | 8 +++ 9 files changed, 165 insertions(+), 44 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/GlobalObserversManager.kt b/app/src/main/kotlin/com/wire/android/GlobalObserversManager.kt index c095759aed7..c3c71ed0aa6 100644 --- a/app/src/main/kotlin/com/wire/android/GlobalObserversManager.kt +++ b/app/src/main/kotlin/com/wire/android/GlobalObserversManager.kt @@ -23,6 +23,8 @@ import com.wire.android.di.KaliumCoreLogic import com.wire.android.notification.NotificationChannelsManager import com.wire.android.notification.WireNotificationManager import com.wire.android.services.SendPendingMessagesAfterForegroundSyncUseCase +import com.wire.android.session.AppUserSessionPreparationResult +import com.wire.android.session.UserSessionPreparationGate import com.wire.android.util.CurrentScreenManager import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.kalium.logic.CoreLogic @@ -69,6 +71,7 @@ class GlobalObserversManager @Inject constructor( ) { // TODO(tests): refactor so scope/dispatcher can be injected and properly stopped private val scope = CoroutineScope(SupervisorJob() + dispatcherProvider.io()) + private val userSessionPreparationGate by lazy { UserSessionPreparationGate(coreLogic) } fun observe() { scope.launch { setUpNotifications() } @@ -76,7 +79,9 @@ class GlobalObserversManager @Inject constructor( coreLogic.getGlobalScope().observeValidAccounts().distinctUntilChanged().collectLatest { coroutineScope { it.forEach { - launch { coreLogic.getSessionScope(it.first.id).calls.endCallOnConversationChange() } + launch { + preparedSessionScope(it.first.id)?.calls?.endCallOnConversationChange() + } } } } @@ -165,7 +170,7 @@ class GlobalObserversManager @Inject constructor( emptyFlow() } } - .collect { userId -> coreLogic.getSessionScope(userId).messages.deleteEphemeralMessageEndDate() } + .collect { userId -> preparedSessionScope(userId)?.messages?.deleteEphemeralMessageEndDate() } } } @@ -200,4 +205,13 @@ class GlobalObserversManager @Inject constructor( private companion object { private const val TAG = "GlobalObserversManager" } + + private suspend fun preparedSessionScope(userId: UserId) = + when (val result = userSessionPreparationGate.prepare(userId)) { + is AppUserSessionPreparationResult.Ready -> result.sessionScope + is AppUserSessionPreparationResult.Failed -> { + appLogger.w("$TAG skipping database observer for ${userId.toLogString()}: ${result.reason}") + null + } + } } diff --git a/app/src/main/kotlin/com/wire/android/WireApplication.kt b/app/src/main/kotlin/com/wire/android/WireApplication.kt index a53ddb057d4..dd6454e7646 100644 --- a/app/src/main/kotlin/com/wire/android/WireApplication.kt +++ b/app/src/main/kotlin/com/wire/android/WireApplication.kt @@ -42,6 +42,8 @@ import com.wire.android.feature.analytics.AnonymousAnalyticsRecorderImpl import com.wire.android.feature.analytics.globalAnalyticsManager import com.wire.android.feature.analytics.model.AnalyticsEvent import com.wire.android.feature.analytics.model.AnalyticsSettings +import com.wire.android.session.AppUserSessionPreparationResult +import com.wire.android.session.UserSessionPreparationGate import com.wire.android.util.AppNameUtil import com.wire.android.util.CurrentScreenManager import com.wire.android.util.DataDogLogger @@ -54,6 +56,8 @@ import com.wire.kalium.common.logger.CoreLogger import com.wire.kalium.logger.KaliumLogLevel import com.wire.kalium.logger.KaliumLogger import com.wire.kalium.logic.CoreLogic +import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.feature.UserSessionScope import com.wire.kalium.logic.feature.session.CurrentSessionResult import com.wire.kalium.logic.feature.session.GetAllSessionsResult import kotlinx.coroutines.CoroutineScope @@ -61,14 +65,17 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout import dev.zacsweers.metro.Inject +import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.atomic.AtomicBoolean import kotlin.collections.filter @@ -113,6 +120,8 @@ class WireApplication : BaseApp() { @Inject lateinit var analyticsManager: Lazy + private val userSessionPreparationGate by lazy { UserSessionPreparationGate(coreLogic.value) } + @Inject lateinit var workManager: WorkManager @@ -192,7 +201,7 @@ class WireApplication : BaseApp() { ::Pair ).collect { (isAppVisible, validSessions) -> validSessions.forEach { - coreLogic.value.getSessionScope(it.userId).calls.setBackground(!isAppVisible) + preparedSessionScope(it.userId)?.calls?.setBackground(!isAppVisible) } } } @@ -201,7 +210,11 @@ class WireApplication : BaseApp() { coreLogic.value.getGlobalScope().session.currentSessionFlow().filterIsInstance(CurrentSessionResult.Success::class) .filter { session -> session.accountInfo.isValid() } .flatMapLatest { session -> - coreLogic.value.getSessionScope(session.accountInfo.userId).calls.observeRecentlyEndedCallMetadata() + flow { + preparedSessionScope(session.accountInfo.userId)?.let { sessionScope -> + emitAll(sessionScope.calls.observeRecentlyEndedCallMetadata()) + } + } } .collect { metadata -> analyticsManager.value.sendEvent(AnalyticsEvent.RecentlyEndedCallEvent(metadata)) @@ -213,7 +226,11 @@ class WireApplication : BaseApp() { .filterIsInstance() .map { it.accountInfo.userId } .flatMapLatest { - coreLogic.value.getSessionScope(it).messages.observeAssetUploadState() + flow { + preparedSessionScope(it)?.let { sessionScope -> + emitAll(sessionScope.messages.observeAssetUploadState()) + } + } } .collect { uploadInProgress -> if (uploadInProgress) { @@ -360,6 +377,7 @@ class WireApplication : BaseApp() { initializeAnonymousAnalytics() } + @Suppress("LongMethod") private fun initializeAnonymousAnalytics() { if (!BuildConfig.ANALYTICS_ENABLED) return @@ -370,22 +388,29 @@ class WireApplication : BaseApp() { enableDebugLogging = BuildConfig.DEBUG ) + val analyticsSessionScopes = ConcurrentHashMap() val analyticsResultFlow = ObserveCurrentSessionAnalyticsUseCase( currentSessionFlow = coreLogic.value.getGlobalScope().session.currentSessionFlow(), getAnalyticsContactsData = { userId -> - coreLogic.value.getSessionScope(userId).getAnalyticsContactsData() + checkNotNull(analyticsSessionScopes[userId]).getAnalyticsContactsData() }, observeAnalyticsTrackingIdentifierStatusFlow = { userId -> - coreLogic.value.getSessionScope(userId).observeAnalyticsTrackingIdentifierStatus() + checkNotNull(analyticsSessionScopes[userId]).observeAnalyticsTrackingIdentifierStatus() }, analyticsIdentifierManagerProvider = { userId -> - coreLogic.value.getSessionScope(userId).analyticsIdentifierManager + checkNotNull(analyticsSessionScopes[userId]).analyticsIdentifierManager }, userDataStoreProvider = userDataStoreProvider.value, globalDataStore = globalDataStore.value, currentBackend = { userId -> - coreLogic.value.getSessionScope(userId).users.serverLinks() - } + checkNotNull(analyticsSessionScopes[userId]).users.serverLinks() + }, + prepareSession = { userId -> + preparedSessionScope(userId)?.let { sessionScope -> + analyticsSessionScopes[userId] = sessionScope + true + } ?: false + }, ).invoke() AnonymousAnalyticsManagerImpl.init( @@ -411,7 +436,7 @@ class WireApplication : BaseApp() { .collect { val currentSessionResult = coreLogic.value.getGlobalScope().session.currentSessionFlow().first() val isTeamMember = if (currentSessionResult is CurrentSessionResult.Success) { - coreLogic.value.getSessionScope(currentSessionResult.accountInfo.userId).team.isSelfATeamMember() + preparedSessionScope(currentSessionResult.accountInfo.userId)?.team?.isSelfATeamMember() } else { null } @@ -440,6 +465,15 @@ class WireApplication : BaseApp() { Log.i(TAG, "startup:$event$elapsed") } + private suspend fun preparedSessionScope(userId: UserId) = + when (val result = userSessionPreparationGate.prepare(userId)) { + is AppUserSessionPreparationResult.Ready -> result.sessionScope + is AppUserSessionPreparationResult.Failed -> { + appLogger.w("Skipping application database observer for ${userId.toLogString()}: ${result.reason}") + null + } + } + override fun onTrimMemory(level: Int) { super.onTrimMemory(level) appLogger.w( diff --git a/app/src/main/kotlin/com/wire/android/analytics/ObserveCurrentSessionAnalyticsUseCase.kt b/app/src/main/kotlin/com/wire/android/analytics/ObserveCurrentSessionAnalyticsUseCase.kt index 3eefd5b79ca..82f8b28d2f9 100644 --- a/app/src/main/kotlin/com/wire/android/analytics/ObserveCurrentSessionAnalyticsUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/analytics/ObserveCurrentSessionAnalyticsUseCase.kt @@ -52,10 +52,11 @@ fun ObserveCurrentSessionAnalyticsUseCase( currentSessionFlow: Flow, getAnalyticsContactsData: suspend (UserId) -> AnalyticsContactsData, observeAnalyticsTrackingIdentifierStatusFlow: suspend (UserId) -> Flow, - analyticsIdentifierManagerProvider: (UserId) -> AnalyticsIdentifierManager, + analyticsIdentifierManagerProvider: suspend (UserId) -> AnalyticsIdentifierManager, userDataStoreProvider: UserDataStoreProvider, globalDataStore: GlobalDataStore, - currentBackend: suspend (UserId) -> SelfServerConfigUseCase.Result + currentBackend: suspend (UserId) -> SelfServerConfigUseCase.Result, + prepareSession: suspend (UserId) -> Boolean = { true }, ) = object : ObserveCurrentSessionAnalyticsUseCase { private var previousAnalyticsResult: AnalyticsIdentifierResult? = null @@ -89,6 +90,9 @@ fun ObserveCurrentSessionAnalyticsUseCase( if (currentSession is CurrentSessionResult.Success && currentSession.accountInfo.isValid()) { val userId = currentSession.accountInfo.userId + if (!prepareSession(userId)) { + return@flatMapLatest flowOf(disabledAnalyticsResult()) + } val analyticsIdentifierManager = analyticsIdentifierManagerProvider(userId) combine( observeAnalyticsTrackingIdentifierStatusFlow(userId) @@ -131,22 +135,22 @@ fun ObserveCurrentSessionAnalyticsUseCase( ) } } else { - flowOf( - AnalyticsResult( - identifierResult = AnalyticsIdentifierResult.Disabled, - profileProperties = { - AnalyticsProfileProperties( - isTeamMember = false, - teamId = null, - contactsAmount = null, - teamMembersAmount = null, - isEnterprise = null - ) - }, - manager = null - ) - ) + flowOf(disabledAnalyticsResult()) } }.distinctUntilChanged() } } + +private fun disabledAnalyticsResult() = AnalyticsResult( + identifierResult = AnalyticsIdentifierResult.Disabled, + profileProperties = { + AnalyticsProfileProperties( + isTeamMember = false, + teamId = null, + contactsAmount = null, + teamMembersAmount = null, + isEnterprise = null, + ) + }, + manager = null, +) diff --git a/app/src/main/kotlin/com/wire/android/media/audiomessage/ConversationAudioMessagePlayer.kt b/app/src/main/kotlin/com/wire/android/media/audiomessage/ConversationAudioMessagePlayer.kt index 32d4a4ccf41..e6decc465b4 100644 --- a/app/src/main/kotlin/com/wire/android/media/audiomessage/ConversationAudioMessagePlayer.kt +++ b/app/src/main/kotlin/com/wire/android/media/audiomessage/ConversationAudioMessagePlayer.kt @@ -24,10 +24,13 @@ import android.net.Uri import com.wire.android.di.ApplicationScope import com.wire.android.di.KaliumCoreLogic import com.wire.android.services.ServicesManager +import com.wire.android.session.AppUserSessionPreparationResult +import com.wire.android.session.UserSessionPreparationGate import com.wire.android.ui.common.R as commonR import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.android.util.extension.intervalFlow import com.wire.android.util.ui.UIText +import com.wire.kalium.common.error.CoreFailure import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.user.UserId @@ -73,6 +76,8 @@ class ConversationAudioMessagePlayer @ApplicationScope private val scope: CoroutineScope, private val dispatchers: DispatcherProvider, ) { + private val userSessionPreparationGate by lazy { UserSessionPreparationGate(coreLogic) } + private companion object { const val UPDATE_POSITION_INTERVAL_IN_MS = 1000L } @@ -398,13 +403,21 @@ class ConversationAudioMessagePlayer conversationId: ConversationId, messageId: String, ): MessageAssetResult = withContext(dispatchers.io()) { + val preparation = userSessionPreparationGate.prepare(userId) + val sessionScope = when (preparation) { + is AppUserSessionPreparationResult.Ready -> preparation.sessionScope + is AppUserSessionPreparationResult.Failed -> return@withContext MessageAssetResult.Failure( + CoreFailure.Unknown(IllegalStateException("User session preparation failed: ${preparation.reason}")), + preparation.canRetry, + ) + } val key = GetAssetMessageKey(userId, conversationId, messageId) getAssetMessageMutex.withLock { // keep deferred in the map to prevent multiple calls to the same asset at the same time, instead just reuse the existing one val deferredResult = getAssetMessageDeferredMap[key] // if no deferred exists or the existing one is already completed with failure, create a new one if (deferredResult == null || (deferredResult.isCompleted && deferredResult.getCompleted() is MessageAssetResult.Failure)) { - coreLogic.getSessionScope(userId).messages.getAssetMessage(conversationId, messageId).also { + sessionScope.messages.getAssetMessage(conversationId, messageId).also { getAssetMessageDeferredMap[key] = it } } else { @@ -415,7 +428,7 @@ class ConversationAudioMessagePlayer // this is to handle the case when the file has been uploaded and the file name has changed from temporary to proper one if (result is MessageAssetResult.Success && !result.decodedAssetPath.toFile().exists()) { getAssetMessageMutex.withLock { - coreLogic.getSessionScope(userId).messages.getAssetMessage(conversationId, messageId).also { + sessionScope.messages.getAssetMessage(conversationId, messageId).also { getAssetMessageDeferredMap[key] = it } }.await() @@ -463,30 +476,37 @@ class ConversationAudioMessagePlayer _audioSpeed.emit(currentSpeed) } + @Suppress("NestedBlockDepth") private suspend fun tryToPlayNextAudio(currentMessageIdWrapper: MessageIdWrapper): Boolean { val (conversationId, currentMessageId) = currentMessageIdWrapper val currentAccountResult = coreLogic.getGlobalScope().session.currentSession() if (currentAccountResult is CurrentSessionResult.Success) { - coreLogic - .getSessionScope((currentAccountResult).accountInfo.userId) - .messages - .getNextAudioMessageInConversation(conversationId, currentMessageId).let { nextAudio -> - if (nextAudio is GetNextAudioMessageInConversationUseCase.Result.Success) { - playAudio(conversationId, nextAudio.messageId) - return true + val preparation = userSessionPreparationGate.prepare(currentAccountResult.accountInfo.userId) + if (preparation is AppUserSessionPreparationResult.Ready) { + preparation.sessionScope + .messages + .getNextAudioMessageInConversation(conversationId, currentMessageId).let { nextAudio -> + if (nextAudio is GetNextAudioMessageInConversationUseCase.Result.Success) { + playAudio(conversationId, nextAudio.messageId) + return true + } } - } + } } return false } + @Suppress("ReturnCount") private suspend fun getSenderNameByMessageId(conversationId: ConversationId, messageId: String): String? { val currentAccountResult = coreLogic.getGlobalScope().session.currentSession() if (currentAccountResult is CurrentSessionResult.Failure) return null - val senderNameResult = coreLogic - .getSessionScope((currentAccountResult as CurrentSessionResult.Success).accountInfo.userId) + val preparation = userSessionPreparationGate.prepare( + (currentAccountResult as CurrentSessionResult.Success).accountInfo.userId + ) + if (preparation !is AppUserSessionPreparationResult.Ready) return null + val senderNameResult = preparation.sessionScope .messages .getSenderNameByMessageId(conversationId, messageId) diff --git a/app/src/main/kotlin/com/wire/android/util/lifecycle/SyncLifecycleManager.kt b/app/src/main/kotlin/com/wire/android/util/lifecycle/SyncLifecycleManager.kt index 56ff97af614..00e1b0676d8 100644 --- a/app/src/main/kotlin/com/wire/android/util/lifecycle/SyncLifecycleManager.kt +++ b/app/src/main/kotlin/com/wire/android/util/lifecycle/SyncLifecycleManager.kt @@ -20,6 +20,8 @@ package com.wire.android.util.lifecycle import com.wire.android.appLogger import com.wire.android.di.KaliumCoreLogic +import com.wire.android.session.AppUserSessionPreparationResult +import com.wire.android.session.UserSessionPreparationGate import com.wire.android.util.CurrentScreenManager import com.wire.kalium.logger.KaliumLogLevel import com.wire.kalium.logger.KaliumLogger.Companion.ApplicationFlow.SYNC @@ -51,6 +53,7 @@ class SyncLifecycleManager @Inject constructor( private val currentScreenManager: CurrentScreenManager, @KaliumCoreLogic private val coreLogic: CoreLogic, ) { + private val userSessionPreparationGate by lazy { UserSessionPreparationGate(coreLogic) } private val logger by lazy { appLogger.withFeatureId(SYNC).withTextTag("SyncLifecycleManager") } @@ -91,7 +94,8 @@ class SyncLifecycleManager @Inject constructor( ) logger.logAppSyncTelemetry(AppSyncTelemetryEvent.APP_SYNC_REQUEST_STARTED, requestData) try { - coreLogic.getSessionScope(userId).syncExecutor.request { + val sessionScope = preparedSessionScope(userId) ?: return@launch + sessionScope.syncExecutor.request { awaitCancellation() } } finally { @@ -136,7 +140,7 @@ class SyncLifecycleManager @Inject constructor( ) logger.logAppSyncTelemetry(AppSyncTelemetryEvent.APP_SYNC_REQUEST_STARTED, requestData) try { - coreLogic.getSessionScope(userId).run { + preparedSessionScope(userId)?.run { syncExecutor.request { logger.d("Waiting until live") val syncRequestResult = if (waitForNextSyncState) { @@ -172,4 +176,13 @@ class SyncLifecycleManager @Inject constructor( private fun UserId.toTelemetryString(): String = "${value.obfuscateId()}@${domain.obfuscateDomain()}" + + private suspend fun preparedSessionScope(userId: UserId) = + when (val result = userSessionPreparationGate.prepare(userId)) { + is AppUserSessionPreparationResult.Ready -> result.sessionScope + is AppUserSessionPreparationResult.Failed -> { + logger.w("Skipping sync because user-session preparation failed: ${result.reason}") + null + } + } } diff --git a/app/src/test/kotlin/com/wire/android/GlobalObserversManagerTest.kt b/app/src/test/kotlin/com/wire/android/GlobalObserversManagerTest.kt index 26cfc5ec86c..75a9d06a94e 100644 --- a/app/src/test/kotlin/com/wire/android/GlobalObserversManagerTest.kt +++ b/app/src/test/kotlin/com/wire/android/GlobalObserversManagerTest.kt @@ -29,6 +29,7 @@ import com.wire.android.services.SendPendingMessagesAfterForegroundSyncUseCase import com.wire.android.util.CurrentScreenManager import com.wire.kalium.common.error.CoreFailure import com.wire.kalium.logic.CoreLogic +import com.wire.kalium.logic.PrepareUserSessionResult import com.wire.kalium.logic.data.auth.AccountInfo import com.wire.kalium.logic.data.auth.PersistentWebSocketStatus import com.wire.kalium.logic.data.logout.LogoutReason @@ -48,6 +49,7 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.impl.annotations.MockK +import io.mockk.mockk import io.mockk.verify import junit.framework.TestCase.assertEquals import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -386,6 +388,7 @@ class GlobalObserversManagerTest { every { notificationChannelsManager.createUserNotificationChannels(any()) } returns Unit every { coreLogic.getGlobalScope().logoutCallbackManager } returns logoutCallbackManager every { coreLogic.getSessionScope(any()) } returns userSessionScope + coEvery { coreLogic.prepareUserSession(any()) } returns preparationSuccess(userSessionScope) every { callsScope.endCallOnConversationChange } returns endCallOnConversationChangeUseCase coEvery { endCallOnConversationChangeUseCase.invoke() } returns Unit every { userSessionScope.calls } returns callsScope @@ -400,6 +403,11 @@ class GlobalObserversManagerTest { withNetworkState(NetworkState.ConnectedWithInternet) } + private fun preparationSuccess(sessionScope: UserSessionScope): PrepareUserSessionResult.Success = + mockk().also { result -> + every { result.sessionScope } returns sessionScope + } + fun withValidAccounts(list: List>): Arrangement = apply { coEvery { coreLogic.getGlobalScope().observeValidAccounts() } returns flowOf(list) } diff --git a/app/src/test/kotlin/com/wire/android/media/audiomessage/ConversationAudioMessagePlayerTest.kt b/app/src/test/kotlin/com/wire/android/media/audiomessage/ConversationAudioMessagePlayerTest.kt index a8b8f12becc..3576cebf969 100644 --- a/app/src/test/kotlin/com/wire/android/media/audiomessage/ConversationAudioMessagePlayerTest.kt +++ b/app/src/test/kotlin/com/wire/android/media/audiomessage/ConversationAudioMessagePlayerTest.kt @@ -27,11 +27,13 @@ import com.wire.android.media.audiomessage.ConversationAudioMessagePlayer.Messag import com.wire.android.services.ServicesManager import com.wire.kalium.common.error.NetworkFailure import com.wire.kalium.logic.CoreLogic +import com.wire.kalium.logic.PrepareUserSessionResult import com.wire.kalium.logic.data.auth.AccountInfo import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.asset.GetMessageAssetUseCase import com.wire.kalium.logic.feature.asset.MessageAssetResult +import com.wire.kalium.logic.feature.UserSessionScope import com.wire.kalium.logic.feature.message.GetMessageByIdUseCase import com.wire.kalium.logic.feature.session.CurrentSessionResult import io.mockk.MockKAnnotations @@ -39,6 +41,7 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.impl.annotations.MockK +import io.mockk.mockk import io.mockk.verify import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope @@ -629,6 +632,9 @@ class Arrangement(private val tempDir: File) { @MockK lateinit var coreLogic: CoreLogic + @MockK + lateinit var userSessionScope: UserSessionScope + @MockK lateinit var mediaPlayer: MediaPlayer @@ -661,8 +667,9 @@ class Arrangement(private val tempDir: File) { init { MockKAnnotations.init(this, relaxed = true) - every { coreLogic.getSessionScope(any()).messages.getAssetMessage } returns getAssetMessage - every { coreLogic.getSessionScope(any()).messages.getMessageById } returns getMessageById + coEvery { coreLogic.prepareUserSession(any()) } returns preparationSuccess(userSessionScope) + every { userSessionScope.messages.getAssetMessage } returns getAssetMessage + every { userSessionScope.messages.getMessageById } returns getMessageById every { mediaPlayer.currentPosition } returns 100 every { servicesManager.stopPlayingAudioMessageService() } returns Unit @@ -673,6 +680,11 @@ class Arrangement(private val tempDir: File) { every { audioFocusHelper.request() } returns true } + private fun preparationSuccess(sessionScope: UserSessionScope): PrepareUserSessionResult.Success = + mockk().also { result -> + every { result.sessionScope } returns sessionScope + } + fun withCurrentSession() = apply { coEvery { coreLogic.getGlobalScope().session.currentSession.invoke() } returns CurrentSessionResult.Success( AccountInfo.Valid(UserId("some-user-value", "some.user.domain")) diff --git a/app/src/test/kotlin/com/wire/android/services/SendPendingMessagesAfterForegroundSyncUseCaseTest.kt b/app/src/test/kotlin/com/wire/android/services/SendPendingMessagesAfterForegroundSyncUseCaseTest.kt index 915dd7ac62f..dc32083852a 100644 --- a/app/src/test/kotlin/com/wire/android/services/SendPendingMessagesAfterForegroundSyncUseCaseTest.kt +++ b/app/src/test/kotlin/com/wire/android/services/SendPendingMessagesAfterForegroundSyncUseCaseTest.kt @@ -22,6 +22,7 @@ import com.wire.android.util.CurrentScreenManager import com.wire.android.util.lifecycle.SyncLifecycleManager import com.wire.kalium.common.error.CoreFailure import com.wire.kalium.logic.CoreLogic +import com.wire.kalium.logic.PrepareUserSessionResult import com.wire.kalium.logic.data.sync.SyncState import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.UserSessionScope @@ -33,6 +34,7 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.impl.annotations.MockK +import io.mockk.mockk import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals @@ -90,12 +92,18 @@ class SendPendingMessagesAfterForegroundSyncUseCaseTest { init { MockKAnnotations.init(this, relaxUnitFun = true) every { coreLogic.getSessionScope(USER_ID) } returns userSessionScope + coEvery { coreLogic.prepareUserSession(USER_ID) } returns preparationSuccess(userSessionScope) every { userSessionScope.syncManager } returns syncStateObserver every { syncStateObserver.syncState } returns MutableStateFlow(SyncState.Live) every { userSessionScope.sendPendingMessages } returns sendPendingMessages coEvery { sendPendingMessages() } returns SendPendingMessagesUseCase.Result.Success } + private fun preparationSuccess(sessionScope: UserSessionScope): PrepareUserSessionResult.Success = + mockk().also { result -> + every { result.sessionScope } returns sessionScope + } + fun withNextSyncRequestResult(syncRequestResult: SyncRequestResult) = apply { syncExecutor = object : FakeSyncExecutor() { override fun onWaitUntilNextLiveOrFailure(): SyncRequestResult = diff --git a/app/src/test/kotlin/com/wire/android/util/lifecycle/SyncLifecycleManagerTest.kt b/app/src/test/kotlin/com/wire/android/util/lifecycle/SyncLifecycleManagerTest.kt index d81471bcac0..4088b10574f 100644 --- a/app/src/test/kotlin/com/wire/android/util/lifecycle/SyncLifecycleManagerTest.kt +++ b/app/src/test/kotlin/com/wire/android/util/lifecycle/SyncLifecycleManagerTest.kt @@ -23,6 +23,7 @@ import com.wire.android.framework.fake.FakeSyncExecutor import com.wire.android.util.CurrentScreenManager import com.wire.kalium.common.error.CoreFailure import com.wire.kalium.logic.CoreLogic +import com.wire.kalium.logic.PrepareUserSessionResult import com.wire.kalium.logic.data.auth.AccountInfo import com.wire.kalium.logic.feature.UserSessionScope import com.wire.kalium.logic.feature.session.GetAllSessionsResult @@ -32,6 +33,7 @@ import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.every import io.mockk.impl.annotations.MockK +import io.mockk.mockk import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf @@ -274,6 +276,7 @@ class SyncLifecycleManagerTest { MockKAnnotations.init(this, relaxUnitFun = true) every { coreLogic.getGlobalScope().observeAllValidSessionsFlow } returns observeValidAccountsUseCase every { coreLogic.getSessionScope(TestUser.SELF_USER_ID) } returns userSessionScope + coEvery { coreLogic.prepareUserSession(TestUser.SELF_USER_ID) } returns preparationSuccess(userSessionScope) every { currentScreenManager.isAppVisibleFlow() } returns appVisibility coEvery { observeValidAccountsUseCase.invoke() } returns flowOf( GetAllSessionsResult.Success( @@ -284,6 +287,11 @@ class SyncLifecycleManagerTest { ) } + private fun preparationSuccess(sessionScope: UserSessionScope): PrepareUserSessionResult.Success = + mockk().also { result -> + every { result.sessionScope } returns sessionScope + } + fun withAppInTheBackground() = apply { updateAppVisibility(false) }