From fbc38354c0864022f0d5378c581ce231b8a07050 Mon Sep 17 00:00:00 2001 From: AndroidBob Date: Mon, 15 Jan 2024 14:17:35 +0100 Subject: [PATCH] fix: missing ServerConfig crashes after session expired / logout [WPB-5960] (#2581) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: MichaƂ Saleniuk --- .../wire/android/GlobalObserversManager.kt | 27 ++- .../com/wire/android/di/CoreLogicModule.kt | 6 + .../android/feature/AccountSwitchUseCase.kt | 25 ++- .../com/wire/android/ui/WireActivity.kt | 3 - .../wire/android/ui/WireActivityDialogs.kt | 2 + .../wire/android/ui/WireActivityViewModel.kt | 17 +- .../appLock/set/SetLockScreenViewModel.kt | 2 +- .../sync/FeatureFlagNotificationViewModel.kt | 170 +++++++++--------- .../android/ui/sharing/ImportMediaScreen.kt | 2 - .../self/SelfUserProfileViewModel.kt | 2 +- .../android/GlobalObserversManagerTest.kt | 7 +- .../android/ui/WireActivityViewModelTest.kt | 22 ++- .../FeatureFlagNotificationViewModelTest.kt | 53 ++---- 13 files changed, 199 insertions(+), 139 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/GlobalObserversManager.kt b/app/src/main/kotlin/com/wire/android/GlobalObserversManager.kt index 7b256ac4ac..954fe2a4a7 100644 --- a/app/src/main/kotlin/com/wire/android/GlobalObserversManager.kt +++ b/app/src/main/kotlin/com/wire/android/GlobalObserversManager.kt @@ -18,18 +18,25 @@ package com.wire.android +import com.wire.android.datastore.UserDataStoreProvider import com.wire.android.di.KaliumCoreLogic import com.wire.android.notification.NotificationChannelsManager import com.wire.android.notification.WireNotificationManager import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.kalium.logic.CoreLogic +import com.wire.kalium.logic.data.logout.LogoutReason +import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.feature.auth.LogoutCallback import com.wire.kalium.logic.feature.user.webSocketStatus.ObservePersistentWebSocketConnectionStatusUseCase import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.launch import javax.inject.Inject import javax.inject.Singleton @@ -43,7 +50,8 @@ class GlobalObserversManager @Inject constructor( dispatcherProvider: DispatcherProvider, @KaliumCoreLogic private val coreLogic: CoreLogic, private val notificationManager: WireNotificationManager, - private val notificationChannelsManager: NotificationChannelsManager + private val notificationChannelsManager: NotificationChannelsManager, + private val userDataStoreProvider: UserDataStoreProvider, ) { private val scope = CoroutineScope(SupervisorJob() + dispatcherProvider.io()) @@ -56,6 +64,7 @@ class GlobalObserversManager @Inject constructor( } } } + scope.handleLogouts() } private suspend fun setUpNotifications() { @@ -89,4 +98,20 @@ class GlobalObserversManager @Inject constructor( // but we can't start PersistentWebSocketService here, to avoid ForegroundServiceStartNotAllowedException } } + + private fun CoroutineScope.handleLogouts() { + callbackFlow { + val callback: LogoutCallback = object : LogoutCallback { + override suspend fun invoke(userId: UserId, reason: LogoutReason) { + notificationManager.stopObservingOnLogout(userId) + notificationChannelsManager.deleteChannelGroup(userId) + if (reason != LogoutReason.SELF_SOFT_LOGOUT) { + userDataStoreProvider.getOrCreate(userId).clear() + } + } + } + coreLogic.getGlobalScope().logoutCallbackManager.register(callback) + awaitClose { coreLogic.getGlobalScope().logoutCallbackManager.unregister(callback) } + }.launchIn(this) + } } diff --git a/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt b/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt index 8adb6e3d7a..1e88123954 100644 --- a/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt @@ -41,6 +41,7 @@ import com.wire.kalium.logic.feature.selfDeletingMessages.ObserveTeamSettingsSel import com.wire.kalium.logic.feature.selfDeletingMessages.PersistNewSelfDeletionTimerUseCase import com.wire.kalium.logic.feature.server.ServerConfigForAccountUseCase import com.wire.kalium.logic.feature.session.CurrentSessionResult +import com.wire.kalium.logic.feature.session.DoesValidSessionExistUseCase import com.wire.kalium.logic.feature.session.GetSessionsUseCase import com.wire.kalium.logic.feature.session.UpdateCurrentSessionUseCase import com.wire.kalium.logic.feature.user.MarkFileSharingChangeAsNotifiedUseCase @@ -310,6 +311,11 @@ class UseCaseModule { fun provideObserveValidAccountsUseCase(@KaliumCoreLogic coreLogic: CoreLogic): ObserveValidAccountsUseCase = coreLogic.getGlobalScope().observeValidAccounts + @ViewModelScoped + @Provides + fun provideDoesValidSessionExistsUseCase(@KaliumCoreLogic coreLogic: CoreLogic): DoesValidSessionExistUseCase = + coreLogic.getGlobalScope().doesValidSessionExist + @ViewModelScoped @Provides fun observeSecurityClassificationLabelUseCase( diff --git a/app/src/main/kotlin/com/wire/android/feature/AccountSwitchUseCase.kt b/app/src/main/kotlin/com/wire/android/feature/AccountSwitchUseCase.kt index 11d572a32e..307cf2be77 100644 --- a/app/src/main/kotlin/com/wire/android/feature/AccountSwitchUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/feature/AccountSwitchUseCase.kt @@ -18,6 +18,7 @@ package com.wire.android.feature +import com.wire.android.appLogger import com.wire.android.di.ApplicationScope import com.wire.android.di.AuthServerConfigProvider import com.wire.android.navigation.BackStackMode @@ -38,6 +39,8 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.async import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import javax.inject.Inject import javax.inject.Singleton @@ -62,11 +65,12 @@ class AccountSwitchUseCase @Inject constructor( } suspend operator fun invoke(params: SwitchAccountParam): SwitchAccountResult { - val current = currentAccount + val current = currentAccount.await() + appLogger.i("$TAG Switching account invoked: ${params.toLogString()}, current account: ${current?.userId?.toLogString() ?: "-"}") return when (params) { - is SwitchAccountParam.SwitchToAccount -> switch(params.userId, current.await()) - SwitchAccountParam.TryToSwitchToNextAccount -> getNextAccountIfPossibleAndSwitch(current.await()) - SwitchAccountParam.Clear -> switch(null, current.await()) + is SwitchAccountParam.SwitchToAccount -> switch(params.userId, current) + SwitchAccountParam.TryToSwitchToNextAccount -> getNextAccountIfPossibleAndSwitch(current) + SwitchAccountParam.Clear -> switch(null, current) } } @@ -81,6 +85,8 @@ class AccountSwitchUseCase @Inject constructor( }?.userId } } + if (nextSessionId == null) appLogger.i("$TAG No next account to switch to") + else appLogger.i("$TAG Switching to next account: ${nextSessionId.toLogString()}") return switch(nextSessionId, current) } @@ -102,6 +108,7 @@ class AccountSwitchUseCase @Inject constructor( } private suspend fun updateAuthServer(current: UserId) { + appLogger.i("$TAG Updating auth server config for account: ${current.toLogString()}") serverConfigForAccountUseCase(current).let { when (it) { is ServerConfigForAccountUseCase.Result.Success -> authServerConfigProvider.updateAuthServer(it.config) @@ -124,6 +131,7 @@ class AccountSwitchUseCase @Inject constructor( } private suspend fun handleInvalidSession(invalidAccount: AccountInfo.Invalid) { + appLogger.i("$TAG Handling invalid account: ${invalidAccount.userId.toLogString()}") when (invalidAccount.logoutReason) { LogoutReason.SELF_SOFT_LOGOUT, LogoutReason.SELF_HARD_LOGOUT -> { deleteSession(invalidAccount.userId) @@ -135,14 +143,21 @@ class AccountSwitchUseCase @Inject constructor( } private companion object { + const val TAG = "AccountSwitch" const val DELETE_USER_SESSION_TIMEOUT = 3000L } } sealed class SwitchAccountParam { - object TryToSwitchToNextAccount : SwitchAccountParam() + data object TryToSwitchToNextAccount : SwitchAccountParam() data class SwitchToAccount(val userId: UserId) : SwitchAccountParam() data object Clear : SwitchAccountParam() + private fun toLogMap(): Map = when (this) { + is Clear -> mutableMapOf("value" to "CLEAR") + is SwitchToAccount -> mutableMapOf("value" to "SWITCH_TO_ACCOUNT", "userId" to userId.toLogString()) + is TryToSwitchToNextAccount -> mutableMapOf("value" to "TRY_TO_SWITCH_TO_NEXT_ACCOUNT") + } + fun toLogString(): String = Json.encodeToString(toLogMap()) } sealed class SwitchAccountResult { diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt index 4cee5ab37a..5e7381dae3 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt @@ -261,9 +261,6 @@ class WireActivity : AppCompatActivity() { @Suppress("ComplexMethod") @Composable private fun handleDialogs(navigate: (NavigationCommand) -> Unit) { - LaunchedEffect(Unit) { - featureFlagNotificationViewModel.loadInitialSync() - } val context = LocalContext.current with(featureFlagNotificationViewModel.featureFlagState) { if (shouldShowTeamAppLockDialog) { diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivityDialogs.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivityDialogs.kt index 62feb8c582..180c054480 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivityDialogs.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivityDialogs.kt @@ -24,6 +24,7 @@ import androidx.annotation.StringRes import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.res.stringResource +import androidx.compose.ui.window.DialogProperties import com.wire.android.BuildConfig import com.wire.android.R import com.wire.android.appLogger @@ -294,6 +295,7 @@ private fun accountLoggedOutDialog(reason: CurrentSessionErrorState, navigateAwa title = stringResource(id = title), text = text, onDismiss = remember { { } }, + properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false, usePlatformDefaultWidth = false), optionButton1Properties = WireDialogButtonProperties( text = stringResource(R.string.label_ok), onClick = navigateAway, diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt index 868d825d24..5d934eb6b9 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt @@ -63,6 +63,8 @@ import com.wire.kalium.logic.feature.server.GetServerConfigResult import com.wire.kalium.logic.feature.server.GetServerConfigUseCase import com.wire.kalium.logic.feature.session.CurrentSessionFlowUseCase import com.wire.kalium.logic.feature.session.CurrentSessionResult +import com.wire.kalium.logic.feature.session.DoesValidSessionExistResult +import com.wire.kalium.logic.feature.session.DoesValidSessionExistUseCase import com.wire.kalium.logic.feature.session.GetAllSessionsResult import com.wire.kalium.logic.feature.session.GetSessionsUseCase import com.wire.kalium.logic.feature.user.screenshotCensoring.ObserveScreenshotCensoringConfigResult @@ -93,6 +95,7 @@ class WireActivityViewModel @Inject constructor( @KaliumCoreLogic private val coreLogic: CoreLogic, private val dispatchers: DispatcherProvider, private val currentSessionFlow: CurrentSessionFlowUseCase, + private val doesValidSessionExist: DoesValidSessionExistUseCase, private val getServerConfigUseCase: GetServerConfigUseCase, private val deepLinkProcessor: DeepLinkProcessor, private val authServerConfigProvider: AuthServerConfigProvider, @@ -113,6 +116,7 @@ class WireActivityViewModel @Inject constructor( private set private val observeUserId = currentSessionFlow() + .distinctUntilChanged() .onEach { if (it is CurrentSessionResult.Success) { if (it.accountInfo.isValid().not()) { @@ -130,7 +134,10 @@ class WireActivityViewModel @Inject constructor( } else { null } - }.distinctUntilChanged().flowOn(dispatchers.io()).shareIn(viewModelScope, SharingStarted.WhileSubscribed(), 1) + } + .distinctUntilChanged() + .flowOn(dispatchers.io()) + .shareIn(viewModelScope, SharingStarted.WhileSubscribed(), 1) private val _observeSyncFlowState: MutableStateFlow = MutableStateFlow(null) val observeSyncFlowState: StateFlow = _observeSyncFlowState @@ -284,7 +291,13 @@ class WireActivityViewModel @Inject constructor( fun dismissNewClientsDialog(userId: UserId) { globalAppState = globalAppState.copy(newClientDialog = null) - viewModelScope.launch { clearNewClientsForUser(userId) } + viewModelScope.launch { + doesValidSessionExist(userId).let { + if (it is DoesValidSessionExistResult.Success && it.doesValidSessionExist) { + clearNewClientsForUser(userId) + } + } + } } fun switchAccount(userId: UserId, actions: SwitchAccountActions, onComplete: () -> Unit) { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/appLock/set/SetLockScreenViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/appLock/set/SetLockScreenViewModel.kt index d78ea6acff..2d8be05671 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/appLock/set/SetLockScreenViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/appLock/set/SetLockScreenViewModel.kt @@ -56,7 +56,7 @@ class SetLockScreenViewModel @Inject constructor( observeAppLockConfig(), observeIsAppLockEditable() ) { config, isEditable -> - SetLockCodeViewState( + state.copy( timeout = config.timeout, isEditable = isEditable ) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt index 84e5a43b3d..39f0e77dc0 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt @@ -24,6 +24,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.wire.android.appLogger import com.wire.android.datastore.GlobalDataStore import com.wire.android.di.KaliumCoreLogic import com.wire.android.feature.AppLockSource @@ -41,21 +42,13 @@ import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.e2ei.usecase.E2EIEnrollmentResult import com.wire.kalium.logic.feature.session.CurrentSessionFlowUseCase import com.wire.kalium.logic.feature.session.CurrentSessionResult -import com.wire.kalium.logic.feature.session.CurrentSessionUseCase import com.wire.kalium.logic.feature.user.E2EIRequiredResult import com.wire.kalium.logic.functional.fold import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.launch import javax.inject.Inject @@ -63,7 +56,6 @@ import javax.inject.Inject @HiltViewModel class FeatureFlagNotificationViewModel @Inject constructor( @KaliumCoreLogic private val coreLogic: CoreLogic, - private val currentSessionUseCase: CurrentSessionUseCase, private val currentSessionFlow: CurrentSessionFlowUseCase, private val globalDataStore: GlobalDataStore, private val disableAppLockUseCase: DisableAppLockUseCase, @@ -75,6 +67,10 @@ class FeatureFlagNotificationViewModel @Inject constructor( private var currentUserId by mutableStateOf(null) + init { + viewModelScope.launch { initialSync() } + } + /** * The FeatureFlagNotificationViewModel is an attempt to encapsulate the logic regarding the different user feature flags, like for * example the file sharing one. This means that this VM could be invoked as an extension from outside the general app lifecycle (for @@ -84,40 +80,51 @@ class FeatureFlagNotificationViewModel @Inject constructor( * it until the sync state is live. Once the sync state is live, it sets whether the file sharing feature is enabled or not on the VM * state. */ - fun loadInitialSync() { - val validUserIdFlow = getValidUserIdFlow().shareIn(viewModelScope, SharingStarted.WhileSubscribed(), 1) - - viewModelScope.launch { validUserIdFlow.flatMapLatest { setE2EIRequiredState(it) }.collect() } - viewModelScope.launch { validUserIdFlow.flatMapLatest { setFileSharingState(it) }.collect() } - viewModelScope.launch { validUserIdFlow.flatMapLatest { observeTeamSettingsSelfDeletionStatus(it) }.collect() } - viewModelScope.launch { validUserIdFlow.flatMapLatest { setGuestRoomLinkFeatureFlag(it) }.collect() } - viewModelScope.launch { validUserIdFlow.flatMapLatest { setTeamAppLockFeatureFlag(it) }.collect() } - viewModelScope.launch { validUserIdFlow.flatMapLatest { observeCallEndedBecauseOfConversationDegraded(it) }.collect() } + private suspend fun initialSync() { + currentSessionFlow() + .distinctUntilChanged() + .collectLatest { currentSessionResult -> + when { + currentSessionResult is CurrentSessionResult.Failure -> { + currentUserId = null + appLogger.i("$TAG: Failure while getting current session") + featureFlagState = FeatureFlagState( // no session, clear feature flag state to default and set NO_USER + fileSharingRestrictedState = FeatureFlagState.SharingRestrictedState.NO_USER + ) + } + currentSessionResult is CurrentSessionResult.Success && !currentSessionResult.accountInfo.isValid() -> { + appLogger.i("$TAG: Invalid current session") + featureFlagState = FeatureFlagState( // invalid session, clear feature flag state to default and set NO_USER + fileSharingRestrictedState = FeatureFlagState.SharingRestrictedState.NO_USER + ) + } + currentSessionResult is CurrentSessionResult.Success && currentSessionResult.accountInfo.isValid() -> { + featureFlagState = FeatureFlagState() // new session, clear feature flag state to default and wait until synced + currentSessionResult.accountInfo.userId.let { userId -> + currentUserId = userId + coreLogic.getSessionScope(userId).observeSyncState() + .firstOrNull { it == SyncState.Live }?.let { + observeStatesAfterInitialSync(userId) + } + } + } + } + } } - /** - * @return [Flow] of [UserId] that emits only if current user presents and is valid (not logged out) - * AND after sync went to [SyncState.Live] at least once. - * - * Also updates val [currentUserId] and hides all the feature dialogs when needed. - */ - private fun getValidUserIdFlow() = currentSessionFlow() - .onEach { hideAllDialogsIfLoggedOut(it) } - .filterIsInstance() - .filter { it.accountInfo.isValid() } - .map { currentSessionResult -> - val userId = currentSessionResult.accountInfo.userId - coreLogic.getSessionScope(userId).observeSyncState() - .firstOrNull { it == SyncState.Live } - ?.let { - currentUserId = userId - userId - } + private suspend fun observeStatesAfterInitialSync(userId: UserId) { + coroutineScope { + launch { setFileSharingState(userId) } + launch { observeTeamSettingsSelfDeletionStatus(userId) } + launch { setGuestRoomLinkFeatureFlag(userId) } + launch { setE2EIRequiredState(userId) } + launch { setTeamAppLockFeatureFlag(userId) } + launch { observeCallEndedBecauseOfConversationDegraded(userId) } } - .filterNotNull() + } - private fun setFileSharingState(userId: UserId) = - coreLogic.getSessionScope(userId).observeFileSharingStatus().onEach { fileSharingStatus -> + private suspend fun setFileSharingState(userId: UserId) { + coreLogic.getSessionScope(userId).observeFileSharingStatus().collect { fileSharingStatus -> fileSharingStatus.state?.let { // TODO: handle restriction when sending assets val (fileSharingRestrictedState, state) = if (it is FileSharingStatus.Value.EnabledAll) { @@ -135,39 +142,42 @@ class FeatureFlagNotificationViewModel @Inject constructor( featureFlagState = featureFlagState.copy(showFileSharingDialog = it) } } + } - private suspend fun setGuestRoomLinkFeatureFlag(userId: UserId) = - coreLogic.getSessionScope(userId).observeGuestRoomLinkFeatureFlag() - .onEach { guestRoomLinkStatus -> - guestRoomLinkStatus.isGuestRoomLinkEnabled?.let { - featureFlagState = featureFlagState.copy(isGuestRoomLinkEnabled = it) - } - guestRoomLinkStatus.isStatusChanged?.let { - featureFlagState = featureFlagState.copy(shouldShowGuestRoomLinkDialog = it) + private suspend fun setGuestRoomLinkFeatureFlag(userId: UserId) { + coreLogic.getSessionScope(userId).observeGuestRoomLinkFeatureFlag() + .collect { guestRoomLinkStatus -> + guestRoomLinkStatus.isGuestRoomLinkEnabled?.let { + featureFlagState = featureFlagState.copy(isGuestRoomLinkEnabled = it) + } + guestRoomLinkStatus.isStatusChanged?.let { + featureFlagState = featureFlagState.copy(shouldShowGuestRoomLinkDialog = it) + } } - } + } - private fun setTeamAppLockFeatureFlag(userId: UserId) = - coreLogic.getSessionScope(userId).appLockTeamFeatureConfigObserver() - .distinctUntilChanged() - .onEach { appLockConfig -> - appLockConfig?.isStatusChanged?.let { isStatusChanged -> - val shouldBlockApp = if (isStatusChanged) { - true - } else { - (!isUserAppLockSet() && appLockConfig.isEnforced) - } + private suspend fun setTeamAppLockFeatureFlag(userId: UserId) { + coreLogic.getSessionScope(userId).appLockTeamFeatureConfigObserver() + .distinctUntilChanged() + .collectLatest { appLockConfig -> + appLockConfig?.isStatusChanged?.let { isStatusChanged -> + val shouldBlockApp = if (isStatusChanged) { + true + } else { + (!isUserAppLockSet() && appLockConfig.isEnforced) + } - featureFlagState = featureFlagState.copy( - isTeamAppLockEnabled = appLockConfig.isEnforced, - shouldShowTeamAppLockDialog = shouldBlockApp - ) + featureFlagState = featureFlagState.copy( + isTeamAppLockEnabled = appLockConfig.isEnforced, + shouldShowTeamAppLockDialog = shouldBlockApp + ) + } } - } + } - private suspend fun observeTeamSettingsSelfDeletionStatus(userId: UserId) = + private suspend fun observeTeamSettingsSelfDeletionStatus(userId: UserId) { coreLogic.getSessionScope(userId).observeTeamSettingsSelfDeletionStatus() - .onEach { teamSettingsSelfDeletingStatus -> + .collect { teamSettingsSelfDeletingStatus -> val areSelfDeletedMessagesEnabled = teamSettingsSelfDeletingStatus.enforcedSelfDeletionTimer !is TeamSelfDeleteTimer.Disabled val shouldShowSelfDeletingMessagesDialog = @@ -187,9 +197,10 @@ class FeatureFlagNotificationViewModel @Inject constructor( enforcedTimeoutDuration = enforcedTimeoutDuration ) } + } - private fun setE2EIRequiredState(userId: UserId) = - coreLogic.getSessionScope(userId).observeE2EIRequired().onEach { result -> + private suspend fun setE2EIRequiredState(userId: UserId) { + coreLogic.getSessionScope(userId).observeE2EIRequired().collect { result -> val state = when (result) { E2EIRequiredResult.NoGracePeriod.Create -> FeatureFlagState.E2EIRequired.NoGracePeriod.Create E2EIRequiredResult.NoGracePeriod.Renew -> FeatureFlagState.E2EIRequired.NoGracePeriod.Renew @@ -205,20 +216,13 @@ class FeatureFlagNotificationViewModel @Inject constructor( } featureFlagState = featureFlagState.copy(e2EIRequired = state) } + } private suspend fun observeCallEndedBecauseOfConversationDegraded(userId: UserId) = - coreLogic.getSessionScope(userId).calls.observeEndCallDialog().onEach { + coreLogic.getSessionScope(userId).calls.observeEndCallDialog().collect { featureFlagState = featureFlagState.copy(showCallEndedBecauseOfConversationDegraded = true) } - private fun hideAllDialogsIfLoggedOut(currentSessionResult: CurrentSessionResult) { - if ((currentSessionResult is CurrentSessionResult.Success && currentSessionResult.accountInfo.isValid().not()) || - currentSessionResult is CurrentSessionResult.Failure - ) { - featureFlagState = FeatureFlagState(fileSharingRestrictedState = FeatureFlagState.SharingRestrictedState.NO_USER) - } - } - fun dismissSelfDeletingMessagesDialog() { featureFlagState = featureFlagState.copy(shouldShowSelfDeletingMessagesDialog = false) viewModelScope.launch { @@ -250,10 +254,8 @@ class FeatureFlagNotificationViewModel @Inject constructor( fun markTeamAppLockStatusAsNot() { viewModelScope.launch { - val currentSession = currentSessionUseCase() - if (currentSession is CurrentSessionResult.Success) { - coreLogic.getSessionScope(currentSession.accountInfo.userId) - .markTeamAppLockStatusAsNotified() + currentUserId?.let { + coreLogic.getSessionScope(it).markTeamAppLockStatusAsNotified() } } } @@ -323,4 +325,8 @@ class FeatureFlagNotificationViewModel @Inject constructor( fun dismissSuccessE2EIdDialog() { featureFlagState = featureFlagState.copy(e2EIResult = null) } + + companion object { + private const val TAG = "FeatureFlagNotificationViewModel" + } } diff --git a/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaScreen.kt b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaScreen.kt index 3df6c546cc..97eedec154 100644 --- a/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaScreen.kt @@ -98,8 +98,6 @@ fun ImportMediaScreen( navigator: Navigator, featureFlagNotificationViewModel: FeatureFlagNotificationViewModel = hiltViewModel() ) { - featureFlagNotificationViewModel.loadInitialSync() - when (val fileSharingRestrictedState = featureFlagNotificationViewModel.featureFlagState.fileSharingRestrictedState) { FeatureFlagState.SharingRestrictedState.NO_USER -> { diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt index 47a362eeae..72f21828e4 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt @@ -214,7 +214,7 @@ class SelfUserProfileViewModel @Inject constructor( }.join() val logoutReason = if (wipeData) LogoutReason.SELF_HARD_LOGOUT else LogoutReason.SELF_SOFT_LOGOUT - logout(logoutReason) + logout(logoutReason, waitUntilCompletes = true) if (wipeData) { // TODO this should be moved to some service that will clear all the data in the app dataStore.clear() diff --git a/app/src/test/kotlin/com/wire/android/GlobalObserversManagerTest.kt b/app/src/test/kotlin/com/wire/android/GlobalObserversManagerTest.kt index 86502c189b..3e75aac6c0 100644 --- a/app/src/test/kotlin/com/wire/android/GlobalObserversManagerTest.kt +++ b/app/src/test/kotlin/com/wire/android/GlobalObserversManagerTest.kt @@ -20,6 +20,7 @@ package com.wire.android import com.wire.android.config.CoroutineTestExtension import com.wire.android.config.TestDispatcherProvider import com.wire.android.config.mockUri +import com.wire.android.datastore.UserDataStoreProvider import com.wire.android.framework.TestUser import com.wire.android.notification.NotificationChannelsManager import com.wire.android.notification.WireNotificationManager @@ -97,12 +98,16 @@ class GlobalObserversManagerTest { @MockK lateinit var notificationManager: WireNotificationManager + @MockK + lateinit var userDataStoreProvider: UserDataStoreProvider + private val manager by lazy { GlobalObserversManager( dispatcherProvider = TestDispatcherProvider(), coreLogic = coreLogic, notificationChannelsManager = notificationChannelsManager, - notificationManager = notificationManager + notificationManager = notificationManager, + userDataStoreProvider = userDataStoreProvider, ) } diff --git a/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt index 0519f45118..7c16353127 100644 --- a/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt @@ -55,6 +55,8 @@ import com.wire.kalium.logic.feature.server.GetServerConfigResult import com.wire.kalium.logic.feature.server.GetServerConfigUseCase import com.wire.kalium.logic.feature.session.CurrentSessionFlowUseCase import com.wire.kalium.logic.feature.session.CurrentSessionResult +import com.wire.kalium.logic.feature.session.DoesValidSessionExistResult +import com.wire.kalium.logic.feature.session.DoesValidSessionExistUseCase import com.wire.kalium.logic.feature.session.GetSessionsUseCase import com.wire.kalium.logic.feature.user.screenshotCensoring.ObserveScreenshotCensoringConfigResult import com.wire.kalium.logic.feature.user.screenshotCensoring.ObserveScreenshotCensoringConfigUseCase @@ -515,8 +517,9 @@ class WireActivityViewModelTest { } @Test - fun `when dismissNewClientsDialog is called, then cleared NewClients for user`() = runTest { + fun `given session exists, when dismissNewClientsDialog is called, then cleared NewClients for user`() = runTest { val (arrangement, viewModel) = Arrangement() + .withSomeCurrentSession() .arrange() viewModel.dismissNewClientsDialog(USER_ID) @@ -524,6 +527,17 @@ class WireActivityViewModelTest { coVerify(exactly = 1) { arrangement.clearNewClientsForUser(USER_ID) } } + @Test + fun `given session does not exist, when dismissNewClientsDialog is called, then do nothing`() = runTest { + val (arrangement, viewModel) = Arrangement() + .withNoCurrentSession() + .arrange() + + viewModel.dismissNewClientsDialog(USER_ID) + + coVerify(exactly = 0) { arrangement.clearNewClientsForUser(USER_ID) } + } + @Test fun `given session and screenshot censoring disabled, when observing it, then set state to false`() = runTest { val (_, viewModel) = Arrangement() @@ -599,6 +613,9 @@ class WireActivityViewModelTest { @MockK lateinit var currentSessionFlow: CurrentSessionFlowUseCase + @MockK + lateinit var doesValidSessionExist: DoesValidSessionExistUseCase + @MockK lateinit var getServerConfigUseCase: GetServerConfigUseCase @@ -660,6 +677,7 @@ class WireActivityViewModelTest { coreLogic = coreLogic, dispatchers = TestDispatcherProvider(), currentSessionFlow = currentSessionFlow, + doesValidSessionExist = doesValidSessionExist, getServerConfigUseCase = getServerConfigUseCase, deepLinkProcessor = deepLinkProcessor, authServerConfigProvider = authServerConfigProvider, @@ -680,11 +698,13 @@ class WireActivityViewModelTest { fun withSomeCurrentSession(): Arrangement = apply { coEvery { currentSessionFlow() } returns flowOf(CurrentSessionResult.Success(TEST_ACCOUNT_INFO)) coEvery { coreLogic.getGlobalScope().session.currentSession() } returns CurrentSessionResult.Success(TEST_ACCOUNT_INFO) + coEvery { doesValidSessionExist(any()) } returns DoesValidSessionExistResult.Success(true) } fun withNoCurrentSession(): Arrangement { coEvery { currentSessionFlow() } returns flowOf(CurrentSessionResult.Failure.SessionNotFound) coEvery { coreLogic.getGlobalScope().session.currentSession() } returns CurrentSessionResult.Failure.SessionNotFound + coEvery { doesValidSessionExist(any()) } returns DoesValidSessionExistResult.Success(false) return this } diff --git a/app/src/test/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModelTest.kt index 04f55f8542..fa8363628a 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModelTest.kt @@ -34,7 +34,6 @@ import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.applock.AppLockTeamFeatureConfigObserver import com.wire.kalium.logic.feature.session.CurrentSessionFlowUseCase import com.wire.kalium.logic.feature.session.CurrentSessionResult -import com.wire.kalium.logic.feature.session.CurrentSessionUseCase import com.wire.kalium.logic.feature.user.E2EIRequiredResult import com.wire.kalium.logic.feature.user.MarkEnablingE2EIAsNotifiedUseCase import com.wire.kalium.logic.feature.user.MarkSelfDeletionStatusAsNotifiedUseCase @@ -67,7 +66,6 @@ class FeatureFlagNotificationViewModelTest { val (_, viewModel) = Arrangement() .withCurrentSessionsFlow(flowOf(CurrentSessionResult.Failure.SessionNotFound)) .arrange() - viewModel.loadInitialSync() advanceUntilIdle() assertEquals( @@ -82,7 +80,6 @@ class FeatureFlagNotificationViewModelTest { .withCurrentSessionsFlow(flowOf(CurrentSessionResult.Success(AccountInfo.Valid(TestUser.USER_ID)))) .withFileSharingStatus(flowOf(FileSharingStatus(FileSharingStatus.Value.Disabled, false))) .arrange() - viewModel.loadInitialSync() advanceUntilIdle() assertEquals( @@ -97,7 +94,6 @@ class FeatureFlagNotificationViewModelTest { .withCurrentSessionsFlow(flowOf(CurrentSessionResult.Success(AccountInfo.Valid(UserId("value", "domain"))))) .withGuestRoomLinkFeatureFlag(flowOf(GuestRoomLinkStatus(true, false))) .arrange() - viewModel.loadInitialSync() advanceUntilIdle() viewModel.dismissGuestRoomLinkDialog() advanceUntilIdle() @@ -115,7 +111,6 @@ class FeatureFlagNotificationViewModelTest { .withCurrentSessionsFlow(flowOf(CurrentSessionResult.Success(AccountInfo.Valid(TestUser.USER_ID)))) .withFileSharingStatus(flowOf(FileSharingStatus(FileSharingStatus.Value.EnabledAll, false))) .arrange() - viewModel.loadInitialSync() advanceUntilIdle() assertEquals( @@ -129,7 +124,6 @@ class FeatureFlagNotificationViewModelTest { val (arrangement, viewModel) = Arrangement() .withCurrentSessionsFlow(flowOf(CurrentSessionResult.Success(AccountInfo.Valid(UserId("value", "domain"))))) .arrange() - viewModel.loadInitialSync() advanceUntilIdle() viewModel.dismissSelfDeletingMessagesDialog() advanceUntilIdle() @@ -145,8 +139,6 @@ class FeatureFlagNotificationViewModelTest { .withIsAppLockSetup(false) .withTeamAppLockEnforce(AppLockTeamConfig(true, Duration.ZERO, false)) .arrange() - - viewModel.loadInitialSync() advanceUntilIdle() assertTrue(viewModel.featureFlagState.shouldShowTeamAppLockDialog) @@ -157,7 +149,6 @@ class FeatureFlagNotificationViewModelTest { val (arrangement, viewModel) = Arrangement() .withE2EIRequiredSettings(E2EIRequiredResult.NoGracePeriod.Create) .arrange() - viewModel.loadInitialSync() advanceUntilIdle() assertEquals(FeatureFlagState.E2EIRequired.NoGracePeriod.Create, viewModel.featureFlagState.e2EIRequired) @@ -169,7 +160,6 @@ class FeatureFlagNotificationViewModelTest { val (arrangement, viewModel) = Arrangement() .withE2EIRequiredSettings(E2EIRequiredResult.WithGracePeriod.Create(gracePeriod)) .arrange() - viewModel.loadInitialSync() advanceUntilIdle() viewModel.snoozeE2EIdRequiredDialog(FeatureFlagState.E2EIRequired.WithGracePeriod.Create(gracePeriod)) @@ -199,7 +189,6 @@ class FeatureFlagNotificationViewModelTest { val (arrangement, viewModel) = Arrangement() .withE2EIRequiredSettings(E2EIRequiredResult.NoGracePeriod.Renew) .arrange() - viewModel.loadInitialSync() advanceUntilIdle() assertEquals(FeatureFlagState.E2EIRequired.NoGracePeriod.Renew, viewModel.featureFlagState.e2EIRequired) @@ -211,7 +200,6 @@ class FeatureFlagNotificationViewModelTest { val (arrangement, viewModel) = Arrangement() .withE2EIRequiredSettings(E2EIRequiredResult.WithGracePeriod.Renew(gracePeriod)) .arrange() - viewModel.loadInitialSync() advanceUntilIdle() viewModel.snoozeE2EIdRequiredDialog(FeatureFlagState.E2EIRequired.WithGracePeriod.Renew(gracePeriod)) @@ -241,8 +229,6 @@ class FeatureFlagNotificationViewModelTest { val (_, viewModel) = Arrangement() .withEndCallDialog() .arrange() - - viewModel.loadInitialSync() advanceUntilIdle() assertEquals(true, viewModel.featureFlagState.showCallEndedBecauseOfConversationDegraded) @@ -255,7 +241,6 @@ class FeatureFlagNotificationViewModelTest { .withAppLockSource(AppLockSource.TeamEnforced) .withDisableAppLockUseCase() .arrange() - viewModel.loadInitialSync() advanceUntilIdle() viewModel.confirmAppLockNotEnforced() @@ -270,7 +255,6 @@ class FeatureFlagNotificationViewModelTest { .withCurrentSessionsFlow(flowOf(CurrentSessionResult.Success(AccountInfo.Valid(TestUser.USER_ID)))) .withAppLockSource(AppLockSource.Manual) .arrange() - viewModel.loadInitialSync() advanceUntilIdle() viewModel.confirmAppLockNotEnforced() @@ -286,7 +270,6 @@ class FeatureFlagNotificationViewModelTest { .withE2EIRequiredSettings(E2EIRequiredResult.NoGracePeriod.Create) .withCurrentSessionsFlow(currentSessionsFlow) .arrange() - viewModel.loadInitialSync() currentSessionsFlow.emit(CurrentSessionResult.Success(AccountInfo.Valid(TestUser.USER_ID))) advanceUntilIdle() @@ -302,16 +285,6 @@ class FeatureFlagNotificationViewModelTest { } private inner class Arrangement { - init { - MockKAnnotations.init(this, relaxUnitFun = true) - coEvery { currentSession() } returns CurrentSessionResult.Success(AccountInfo.Valid(TestUser.USER_ID)) - coEvery { currentSessionFlow() } returns flowOf(CurrentSessionResult.Success(AccountInfo.Valid(TestUser.USER_ID))) - coEvery { coreLogic.getSessionScope(any()).observeSyncState() } returns flowOf(SyncState.Live) - coEvery { coreLogic.getSessionScope(any()).observeTeamSettingsSelfDeletionStatus() } returns flowOf() - } - - @MockK - lateinit var currentSession: CurrentSessionUseCase @MockK lateinit var currentSessionFlow: CurrentSessionFlowUseCase @@ -337,16 +310,20 @@ class FeatureFlagNotificationViewModelTest { @MockK lateinit var globalDataStore: GlobalDataStore - val viewModel: FeatureFlagNotificationViewModel = FeatureFlagNotificationViewModel( - coreLogic = coreLogic, - currentSessionUseCase = currentSession, - currentSessionFlow = currentSessionFlow, - globalDataStore = globalDataStore, - disableAppLockUseCase = disableAppLockUseCase, - dispatcherProvider = TestDispatcherProvider() - ) - + val viewModel: FeatureFlagNotificationViewModel by lazy { + FeatureFlagNotificationViewModel( + coreLogic = coreLogic, + currentSessionFlow = currentSessionFlow, + globalDataStore = globalDataStore, + disableAppLockUseCase = disableAppLockUseCase, + dispatcherProvider = TestDispatcherProvider() + ) + } init { + MockKAnnotations.init(this, relaxUnitFun = true) + coEvery { currentSessionFlow() } returns flowOf(CurrentSessionResult.Success(AccountInfo.Valid(TestUser.USER_ID))) + coEvery { coreLogic.getSessionScope(any()).observeSyncState() } returns flowOf(SyncState.Live) + coEvery { coreLogic.getSessionScope(any()).observeTeamSettingsSelfDeletionStatus() } returns flowOf() every { coreLogic.getSessionScope(any()).markGuestLinkFeatureFlagAsNotChanged } returns markGuestLinkFeatureFlagAsNotChanged every { coreLogic.getSessionScope(any()).markSelfDeletingMessagesAsNotified } returns markSelfDeletingStatusAsNotified every { coreLogic.getSessionScope(any()).markE2EIRequiredAsNotified } returns markE2EIRequiredAsNotified @@ -358,10 +335,6 @@ class FeatureFlagNotificationViewModelTest { coEvery { ppLockTeamFeatureConfigObserver() } returns flowOf(null) } - fun withCurrentSessions(result: CurrentSessionResult) = apply { - coEvery { currentSession() } returns result - } - fun withCurrentSessionsFlow(result: Flow) = apply { coEvery { currentSessionFlow() } returns result }