From 55b775615865284b264a007221f216e6048c87fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= Date: Fri, 14 Jul 2023 15:48:45 +0200 Subject: [PATCH 01/10] feat: screenshot censoring [WPB-2880] --- .../com/wire/android/di/CoreLogicModule.kt | 16 +++++++ .../com/wire/android/ui/WireActivity.kt | 9 +++- .../home/conversations/ConversationScreen.kt | 16 +++++++ .../info/ConversationInfoViewModel.kt | 18 ++++++++ .../info/ConversationInfoViewState.kt | 3 +- .../settings/privacy/PrivacySettingsScreen.kt | 26 ++++++++++- .../settings/privacy/PrivacySettingsState.kt | 5 ++ .../privacy/PrivacySettingsViewModel.kt | 46 ++++++++++++++++++- app/src/main/res/values/strings.xml | 3 ++ kalium | 2 +- 10 files changed, 138 insertions(+), 6 deletions(-) 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 5f2bb410f10..58d12d5bf16 100644 --- a/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt @@ -121,6 +121,8 @@ import com.wire.kalium.logic.feature.user.ObserveValidAccountsUseCase import com.wire.kalium.logic.feature.user.SelfServerConfigUseCase import com.wire.kalium.logic.feature.user.UpdateDisplayNameUseCase import com.wire.kalium.logic.feature.user.UpdateEmailUseCase +import com.wire.kalium.logic.feature.user.screenshotCensoring.ObserveScreenshotCensoringConfigUseCase +import com.wire.kalium.logic.feature.user.screenshotCensoring.PersistScreenshotCensoringConfigUseCase import com.wire.kalium.logic.featureFlags.KaliumConfigs import dagger.Module import dagger.Provides @@ -1199,4 +1201,18 @@ class UseCaseModule { @CurrentAccount currentAccount: UserId ): DeleteAccountUseCase = coreLogic.getSessionScope(currentAccount).users.deleteAccount + + @ViewModelScoped + @Provides + fun providePersistScreenshotCensoringConfigUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId + ): PersistScreenshotCensoringConfigUseCase = coreLogic.getSessionScope(currentAccount).persistScreenshotCensoringConfig + + @ViewModelScoped + @Provides + fun provideObserveScreenshotCensoringConfigUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId + ): ObserveScreenshotCensoringConfigUseCase = coreLogic.getSessionScope(currentAccount).observeScreenshotCensoringConfig } 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 3bcb160b25c..04425a0dd83 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt @@ -20,6 +20,7 @@ package com.wire.android.ui +import android.app.Activity import android.content.Intent import android.net.Uri import android.os.Bundle @@ -37,6 +38,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource @@ -137,7 +139,8 @@ class WireActivity : AppCompatActivity() { LocalFeatureVisibilityFlags provides FeatureVisibilityFlags, LocalSyncStateObserver provides SyncStateObserver(viewModel.observeSyncFlowState), LocalCustomUiConfigurationProvider provides CustomUiConfigurationProvider, - LocalSnackbarHostState provides snackbarHostState + LocalSnackbarHostState provides snackbarHostState, + LocalActivity provides this ) { WireTheme { Column { @@ -436,3 +439,7 @@ class WireActivity : AppCompatActivity() { private const val HANDLED_DEEPLINK_FLAG = "deeplink_handled_flag_key" } } + +val LocalActivity = staticCompositionLocalOf { + error("No Activity provided") +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt index c1249404a5e..c5da3455470 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt @@ -20,7 +20,9 @@ package com.wire.android.ui.home.conversations +import android.app.Activity import android.net.Uri +import android.view.WindowManager import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxHeight @@ -31,6 +33,7 @@ import androidx.compose.foundation.lazy.LazyListState import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarResult import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf @@ -51,6 +54,7 @@ import com.wire.android.appLogger import com.wire.android.media.audiomessage.AudioState import com.wire.android.model.SnackBarMessage import com.wire.android.navigation.hiltSavedStateViewModel +import com.wire.android.ui.LocalActivity import com.wire.android.ui.common.bottomsheet.MenuModalSheetHeader import com.wire.android.ui.common.bottomsheet.MenuModalSheetLayout import com.wire.android.ui.common.dialogs.calling.CallingFeatureUnavailableDialog @@ -122,6 +126,18 @@ fun ConversationScreen( val coroutineScope = rememberCoroutineScope() val showDialog = remember { mutableStateOf(ConversationScreenDialogType.NONE) } + val activity = LocalActivity.current + DisposableEffect(conversationInfoViewModel.conversationInfoViewState.screenshotCensoringEnabled) { + if (conversationInfoViewModel.conversationInfoViewState.screenshotCensoringEnabled) { + activity.window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) + } else { + activity.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) + } + onDispose { + activity.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) + } + } + val startCallAudioPermissionCheck = StartCallAudioBluetoothPermissionCheckFlow { conversationCallViewModel.navigateToInitiatingCallScreen() } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModel.kt index 8c024184565..dcedf7bef53 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModel.kt @@ -48,6 +48,8 @@ import com.wire.kalium.logic.data.user.ConnectionState import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase import com.wire.kalium.logic.feature.user.GetSelfUserUseCase +import com.wire.kalium.logic.feature.user.screenshotCensoring.ObserveScreenshotCensoringConfigResult +import com.wire.kalium.logic.feature.user.screenshotCensoring.ObserveScreenshotCensoringConfigUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch @@ -61,6 +63,7 @@ class ConversationInfoViewModel @Inject constructor( private val navigationManager: NavigationManager, private val observeConversationDetails: ObserveConversationDetailsUseCase, private val observerSelfUser: GetSelfUserUseCase, + private val observeScreenshotCensoringConfigUseCase: ObserveScreenshotCensoringConfigUseCase, private val wireSessionImageLoader: WireSessionImageLoader, private val dispatchers: DispatcherProvider ) : SavedStateViewModel(savedStateHandle) { @@ -74,11 +77,26 @@ class ConversationInfoViewModel @Inject constructor( private lateinit var selfUserId: UserId init { + getSelfUserId() + observeScreenshotCensoringConfig() + } + + private fun getSelfUserId() { viewModelScope.launch { selfUserId = observerSelfUser().first().id } } + private fun observeScreenshotCensoringConfig() { + viewModelScope.launch { + observeScreenshotCensoringConfigUseCase().collect { + conversationInfoViewState = conversationInfoViewState.copy( + screenshotCensoringEnabled = it is ObserveScreenshotCensoringConfigResult.Enabled + ) + } + } + } + /* If this would be collected in the scope of this ViewModel (in `init` for instance) then there would be a race condition. [MessageComposerViewModel] handles the navigating back after removing a group and here it would navigate to home if the group diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewState.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewState.kt index d09835bfb66..c3d37deadc5 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewState.kt @@ -33,7 +33,8 @@ data class ConversationInfoViewState( val conversationDetailsData: ConversationDetailsData = ConversationDetailsData.None, val conversationAvatar: ConversationAvatar = ConversationAvatar.None, val hasUserPermissionToEdit: Boolean = false, - val conversationType: Conversation.Type = Conversation.Type.ONE_ON_ONE + val conversationType: Conversation.Type = Conversation.Type.ONE_ON_ONE, + val screenshotCensoringEnabled: Boolean = true, ) sealed class ConversationDetailsData { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/privacy/PrivacySettingsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/privacy/PrivacySettingsScreen.kt index 83f1ffd9c61..8e5ba950032 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/privacy/PrivacySettingsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/privacy/PrivacySettingsScreen.kt @@ -44,6 +44,8 @@ fun PrivacySettingsConfigScreen( PrivacySettingsScreenContent( isReadReceiptsEnabled = state.isReadReceiptsEnabled, setReadReceiptsState = ::setReadReceiptsState, + screenshotCensoringConfig = state.screenshotCensoringConfig, + setScreenshotCensoringConfig = ::setScreenshotCensoringConfig, onBackPressed = ::navigateBack ) } @@ -53,6 +55,8 @@ fun PrivacySettingsConfigScreen( fun PrivacySettingsScreenContent( isReadReceiptsEnabled: Boolean, setReadReceiptsState: (Boolean) -> Unit, + screenshotCensoringConfig: ScreenshotCensoringConfig, + setScreenshotCensoringConfig: (Boolean) -> Unit, onBackPressed: () -> Unit ) { Scaffold(topBar = { @@ -73,6 +77,26 @@ fun PrivacySettingsScreenContent( arrowType = ArrowType.NONE, subtitle = stringResource(id = R.string.settings_send_read_receipts_description) ) + GroupConversationOptionsItem( + title = stringResource(R.string.settings_censor_screenshots), + switchState = when (screenshotCensoringConfig) { + ScreenshotCensoringConfig.DISABLED -> + SwitchState.Enabled(value = false, onCheckedChange = setScreenshotCensoringConfig) + + ScreenshotCensoringConfig.ENABLED_BY_USER -> + SwitchState.Enabled(value = true, onCheckedChange = setScreenshotCensoringConfig) + + ScreenshotCensoringConfig.ENFORCED_BY_TEAM -> + SwitchState.Disabled(value = true) + }, + arrowType = ArrowType.NONE, + subtitle = stringResource( + id = when (screenshotCensoringConfig) { + ScreenshotCensoringConfig.ENFORCED_BY_TEAM -> R.string.settings_censor_screenshots_enforced_by_team_description + else -> R.string.settings_censor_screenshots_description + } + ) + ) } } } @@ -80,5 +104,5 @@ fun PrivacySettingsScreenContent( @Composable @Preview fun PreviewSendReadReceipts() { - PrivacySettingsScreenContent(true, {}, {}) + PrivacySettingsScreenContent(true, {}, ScreenshotCensoringConfig.DISABLED, {}, {}) } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/privacy/PrivacySettingsState.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/privacy/PrivacySettingsState.kt index a137acbcac4..92e7473d987 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/privacy/PrivacySettingsState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/privacy/PrivacySettingsState.kt @@ -22,4 +22,9 @@ package com.wire.android.ui.home.settings.privacy data class PrivacySettingsState( val isReadReceiptsEnabled: Boolean = true, + val screenshotCensoringConfig: ScreenshotCensoringConfig = ScreenshotCensoringConfig.ENABLED_BY_USER ) + +enum class ScreenshotCensoringConfig { + DISABLED, ENABLED_BY_USER, ENFORCED_BY_TEAM +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/privacy/PrivacySettingsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/privacy/PrivacySettingsViewModel.kt index 6811c7da27b..4e81fb9f800 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/privacy/PrivacySettingsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/privacy/PrivacySettingsViewModel.kt @@ -31,9 +31,19 @@ import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.kalium.logic.feature.user.readReceipts.ObserveReadReceiptsEnabledUseCase import com.wire.kalium.logic.feature.user.readReceipts.PersistReadReceiptsStatusConfigUseCase import com.wire.kalium.logic.feature.user.readReceipts.ReadReceiptStatusConfigResult +import com.wire.kalium.logic.feature.user.screenshotCensoring.ObserveScreenshotCensoringConfigResult +import com.wire.kalium.logic.feature.user.screenshotCensoring.ObserveScreenshotCensoringConfigUseCase +import com.wire.kalium.logic.feature.user.screenshotCensoring.PersistScreenshotCensoringConfigResult +import com.wire.kalium.logic.feature.user.screenshotCensoring.PersistScreenshotCensoringConfigUseCase import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout import javax.inject.Inject @HiltViewModel @@ -42,6 +52,8 @@ class PrivacySettingsViewModel @Inject constructor( private val dispatchers: DispatcherProvider, private val persistReadReceiptsStatusConfig: PersistReadReceiptsStatusConfigUseCase, private val observeReadReceiptsEnabled: ObserveReadReceiptsEnabledUseCase, + private val persistScreenshotCensoringConfig: PersistScreenshotCensoringConfigUseCase, + private val observeScreenshotCensoringConfig: ObserveScreenshotCensoringConfigUseCase ) : ViewModel() { var state by mutableStateOf(PrivacySettingsState()) @@ -49,8 +61,24 @@ class PrivacySettingsViewModel @Inject constructor( init { viewModelScope.launch { - observeReadReceiptsEnabled().collect { - state = state.copy(isReadReceiptsEnabled = it) + combine( + observeReadReceiptsEnabled(), + observeScreenshotCensoringConfig(), + ::Pair + ).collect { (readReceiptsEnabled, screenshotCensoringConfig) -> + state = state.copy( + isReadReceiptsEnabled = readReceiptsEnabled, + screenshotCensoringConfig = when (screenshotCensoringConfig) { + ObserveScreenshotCensoringConfigResult.Disabled -> + ScreenshotCensoringConfig.DISABLED + + ObserveScreenshotCensoringConfigResult.Enabled.ChosenByUser -> + ScreenshotCensoringConfig.ENABLED_BY_USER + + ObserveScreenshotCensoringConfigResult.Enabled.EnforcedByTeamSelfDeletingSettings -> + ScreenshotCensoringConfig.ENFORCED_BY_TEAM + } + ) } } } @@ -73,4 +101,18 @@ class PrivacySettingsViewModel @Inject constructor( } } } + + fun setScreenshotCensoringConfig(isEnabled: Boolean) { + viewModelScope.launch { + when (withContext(dispatchers.io()) { persistScreenshotCensoringConfig(isEnabled) }) { + is PersistScreenshotCensoringConfigResult.Failure -> { + appLogger.e("Something went wrong while updating screenshot censoring config") + } + + is PersistScreenshotCensoringConfigResult.Success -> { + appLogger.d("Screenshot censoring config changed") + } + } + } + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7c08260ad27..6665bb9459c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -844,6 +844,9 @@ Send read receipts If this is OFF, you won’t be able to see read receipts from other people. This setting does not apply to group conversations. + Censor screenshots + If this is ON, then the content of the messages will not be visible on the screenshot or screen recording. + This is enforced by the self-deleting message team setting and cannot be changed.\nThe content of the messages will not be visible on the screenshot or screen recording. Your Devices Current Device diff --git a/kalium b/kalium index 3282688ab87..99b96daaeb5 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 3282688ab87f23c393be198764661f227776695a +Subproject commit 99b96daaeb5373657dfd2f079f9240126adf6949 From 8eead352b3c1cce6f09bc314aef19c408fc5b33b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= Date: Fri, 14 Jul 2023 15:54:40 +0200 Subject: [PATCH 02/10] change kalium ref --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index 99b96daaeb5..7de2f52dc14 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 99b96daaeb5373657dfd2f079f9240126adf6949 +Subproject commit 7de2f52dc146143fe1cf48a490f541298c7454fc From d6ac5a8c011b2d2c95187033bc7a20cd13588e1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= Date: Fri, 14 Jul 2023 16:01:34 +0200 Subject: [PATCH 03/10] detekt --- .../wire/android/ui/home/conversations/ConversationScreen.kt | 1 - .../ui/home/conversations/info/ConversationInfoViewModel.kt | 2 +- .../ui/home/settings/privacy/PrivacySettingsViewModel.kt | 5 ----- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt index b81ecf27511..e7473d6455e 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt @@ -20,7 +20,6 @@ package com.wire.android.ui.home.conversations -import android.app.Activity import android.net.Uri import android.view.WindowManager import androidx.compose.foundation.layout.Box diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModel.kt index dcedf7bef53..3ebf2a2e1a5 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModel.kt @@ -55,7 +55,7 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import javax.inject.Inject -@Suppress("LongParameterList") +@Suppress("LongParameterList", "TooManyFunctions") @HiltViewModel class ConversationInfoViewModel @Inject constructor( private val qualifiedIdMapper: QualifiedIdMapper, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/privacy/PrivacySettingsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/privacy/PrivacySettingsViewModel.kt index 4e81fb9f800..3d498a4482f 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/privacy/PrivacySettingsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/privacy/PrivacySettingsViewModel.kt @@ -36,14 +36,9 @@ import com.wire.kalium.logic.feature.user.screenshotCensoring.ObserveScreenshotC import com.wire.kalium.logic.feature.user.screenshotCensoring.PersistScreenshotCensoringConfigResult import com.wire.kalium.logic.feature.user.screenshotCensoring.PersistScreenshotCensoringConfigUseCase import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext -import kotlinx.coroutines.withTimeout import javax.inject.Inject @HiltViewModel From b5603ca631cf8f8900ffc5e0a95592f9a6af3103 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= Date: Fri, 14 Jul 2023 16:24:00 +0200 Subject: [PATCH 04/10] add tests --- .../ConversationInfoViewModelArrangement.kt | 11 +++++++ .../info/ConversationInfoViewModelTest.kt | 31 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelArrangement.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelArrangement.kt index aee0badf9d1..2627deff9c3 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelArrangement.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelArrangement.kt @@ -32,6 +32,8 @@ import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.data.id.QualifiedIdMapper import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase import com.wire.kalium.logic.feature.user.GetSelfUserUseCase +import com.wire.kalium.logic.feature.user.screenshotCensoring.ObserveScreenshotCensoringConfigResult +import com.wire.kalium.logic.feature.user.screenshotCensoring.ObserveScreenshotCensoringConfigUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.every @@ -66,6 +68,9 @@ class ConversationInfoViewModelArrangement { @MockK private lateinit var wireSessionImageLoader: WireSessionImageLoader + @MockK + private lateinit var observeScreenshotCensoringConfigUseCase: ObserveScreenshotCensoringConfigUseCase + private val viewModel: ConversationInfoViewModel by lazy { ConversationInfoViewModel( qualifiedIdMapper, @@ -73,6 +78,7 @@ class ConversationInfoViewModelArrangement { navigationManager, observeConversationDetails, observerSelfUser, + observeScreenshotCensoringConfigUseCase, wireSessionImageLoader, TestDispatcherProvider() ) @@ -88,6 +94,7 @@ class ConversationInfoViewModelArrangement { coEvery { observeConversationDetails(any()) } returns conversationDetailsChannel.consumeAsFlow().map { ObserveConversationDetailsUseCase.Result.Success(it) } + coEvery { observeScreenshotCensoringConfigUseCase() } returns flowOf(ObserveScreenshotCensoringConfigResult.Disabled) } suspend fun withConversationDetailUpdate(conversationDetails: ConversationDetails) = apply { @@ -101,5 +108,9 @@ class ConversationInfoViewModelArrangement { coEvery { observerSelfUser() } returns flowOf(TestUser.SELF_USER) } + suspend fun withScreenshotCensoringConfig(result: ObserveScreenshotCensoringConfigResult) = apply { + coEvery { observeScreenshotCensoringConfigUseCase() } returns flowOf(result) + } + fun arrange() = this to viewModel } diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelTest.kt index 9ca5d9aad19..0cc8b3112d5 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelTest.kt @@ -32,6 +32,7 @@ import com.wire.kalium.logic.data.conversation.ConversationDetails import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.feature.user.screenshotCensoring.ObserveScreenshotCensoringConfigResult import io.mockk.coEvery import io.mockk.coVerify import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -269,4 +270,34 @@ class ConversationInfoViewModelTest { cancel() } } + + @Test + fun `given screenshot censoring disabled, when observing it, then set screenshotCensoringEnabled state to false`() = runTest { + val (_, viewModel) = ConversationInfoViewModelArrangement() + .withSelfUser() + .withScreenshotCensoringConfig(ObserveScreenshotCensoringConfigResult.Disabled) + .arrange() + advanceUntilIdle() + assertEquals(false, viewModel.conversationInfoViewState.screenshotCensoringEnabled) + } + + @Test + fun `given screenshot censoring enabled by user, when observing it, then set screenshotCensoringEnabled state to true`() = runTest { + val (_, viewModel) = ConversationInfoViewModelArrangement() + .withSelfUser() + .withScreenshotCensoringConfig(ObserveScreenshotCensoringConfigResult.Enabled.ChosenByUser) + .arrange() + advanceUntilIdle() + assertEquals(true, viewModel.conversationInfoViewState.screenshotCensoringEnabled) + } + + @Test + fun `given screenshot censoring enforced by team, when observing it, then set screenshotCensoringEnabled state to true`() = runTest { + val (_, viewModel) = ConversationInfoViewModelArrangement() + .withSelfUser() + .withScreenshotCensoringConfig(ObserveScreenshotCensoringConfigResult.Enabled.EnforcedByTeamSelfDeletingSettings) + .arrange() + advanceUntilIdle() + assertEquals(true, viewModel.conversationInfoViewState.screenshotCensoringEnabled) + } } From ce915e052c91c784d11766c78faf97fb492bc734 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= Date: Fri, 14 Jul 2023 16:31:12 +0200 Subject: [PATCH 05/10] detekt --- .../conversations/info/ConversationInfoViewModelArrangement.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelArrangement.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelArrangement.kt index 2627deff9c3..862e2c32765 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelArrangement.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelArrangement.kt @@ -43,7 +43,6 @@ import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map - class ConversationInfoViewModelArrangement { val conversationId: ConversationId = ConversationId("some-dummy-value", "some.dummy.domain") From 1f204264b32431539f5d7b2edbc87eabb593b309 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= Date: Fri, 14 Jul 2023 18:02:44 +0200 Subject: [PATCH 06/10] change after review --- .../ui/home/settings/privacy/PrivacySettingsViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/privacy/PrivacySettingsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/privacy/PrivacySettingsViewModel.kt index 3d498a4482f..bf1928170b3 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/privacy/PrivacySettingsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/privacy/PrivacySettingsViewModel.kt @@ -99,7 +99,7 @@ class PrivacySettingsViewModel @Inject constructor( fun setScreenshotCensoringConfig(isEnabled: Boolean) { viewModelScope.launch { - when (withContext(dispatchers.io()) { persistScreenshotCensoringConfig(isEnabled) }) { + when (persistScreenshotCensoringConfig(isEnabled)) { is PersistScreenshotCensoringConfigResult.Failure -> { appLogger.e("Something went wrong while updating screenshot censoring config") } From 771ed2f2083fcc540ade0de7b70c7baee97ef906 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= Date: Mon, 17 Jul 2023 17:35:12 +0200 Subject: [PATCH 07/10] move screenshot censoring to the WireActivity --- .../com/wire/android/ui/WireActivity.kt | 13 +++++++++ .../wire/android/ui/WireActivityViewModel.kt | 29 ++++++++++++++++++- .../home/conversations/ConversationScreen.kt | 15 ---------- .../info/ConversationInfoViewModel.kt | 14 --------- .../info/ConversationInfoViewState.kt | 1 - 5 files changed, 41 insertions(+), 31 deletions(-) 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 0b223055ac6..bdf981afa78 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt @@ -24,6 +24,7 @@ import android.app.Activity import android.content.Intent import android.net.Uri import android.os.Bundle +import android.view.WindowManager import androidx.activity.compose.setContent import androidx.activity.viewModels import androidx.annotation.StringRes @@ -152,6 +153,7 @@ class WireActivity : AppCompatActivity() { startDestination = startDestination, onComplete = onComplete ) + handleScreenshotCensoring() handleDialogs() } } @@ -212,6 +214,17 @@ class WireActivity : AppCompatActivity() { } } + @Composable + private fun handleScreenshotCensoring() { + LaunchedEffect(viewModel.globalAppState.screenshotCensoringEnabled) { + if (viewModel.globalAppState.screenshotCensoringEnabled) { + window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) + } else { + window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) + } + } + } + @Composable private fun handleDialogs() { updateAppDialog({ updateTheApp() }, viewModel.globalAppState.updateAppDialog) 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 ed31bcf403e..1c6e743a9bb 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt @@ -69,6 +69,8 @@ import com.wire.kalium.logic.feature.session.CurrentSessionFlowUseCase import com.wire.kalium.logic.feature.session.CurrentSessionResult 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 +import com.wire.kalium.logic.feature.user.screenshotCensoring.ObserveScreenshotCensoringConfigUseCase import com.wire.kalium.logic.feature.user.webSocketStatus.ObservePersistentWebSocketConnectionStatusUseCase import com.wire.kalium.util.DateTimeUtil.toIsoDateTimeString import dagger.hilt.android.lifecycle.HiltViewModel @@ -109,6 +111,7 @@ class WireActivityViewModel @Inject constructor( private val observeNewClients: ObserveNewClientsUseCase, private val clearNewClientsForUser: ClearNewClientsForUserUseCase, private val currentScreenManager: CurrentScreenManager, + private val observeScreenshotCensoringConfigUseCase: ObserveScreenshotCensoringConfigUseCase, ) : ViewModel() { var globalAppState: GlobalAppState by mutableStateOf(GlobalAppState()) @@ -138,6 +141,13 @@ class WireActivityViewModel @Inject constructor( val observeSyncFlowState: StateFlow = _observeSyncFlowState init { + observeSyncState() + observeUpdateAppState() + observeNewClientState() + observeScreenshotCensoringConfigState() + } + + private fun observeSyncState() { viewModelScope.launch(dispatchers.io()) { observeUserId .flatMapLatest { @@ -146,6 +156,9 @@ class WireActivityViewModel @Inject constructor( .distinctUntilChanged() .collect { _observeSyncFlowState.emit(it) } } + } + + private fun observeUpdateAppState() { viewModelScope.launch(dispatchers.io()) { observeIfAppUpdateRequired(BuildConfig.VERSION_CODE) .distinctUntilChanged() @@ -153,6 +166,9 @@ class WireActivityViewModel @Inject constructor( globalAppState = globalAppState.copy(updateAppDialog = it) } } + } + + private fun observeNewClientState() { viewModelScope.launch(dispatchers.io()) { currentScreenManager.observeCurrentScreen(this) .flatMapLatest { @@ -166,6 +182,16 @@ class WireActivityViewModel @Inject constructor( } } + private fun observeScreenshotCensoringConfigState() { + viewModelScope.launch { + observeScreenshotCensoringConfigUseCase().collect { + globalAppState = globalAppState.copy( + screenshotCensoringEnabled = it is ObserveScreenshotCensoringConfigResult.Enabled + ) + } + } + } + private suspend fun handleInvalidSession(logoutReason: LogoutReason) { withContext(dispatchers.main()) { when (logoutReason) { @@ -527,5 +553,6 @@ data class GlobalAppState( val blockUserUI: CurrentSessionErrorState? = null, val updateAppDialog: Boolean = false, val conversationJoinedDialog: JoinConversationViaCodeState? = null, - val newClientDialog: NewClientsData? = null + val newClientDialog: NewClientsData? = null, + val screenshotCensoringEnabled: Boolean = true, ) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt index e7473d6455e..74413860189 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt @@ -21,7 +21,6 @@ package com.wire.android.ui.home.conversations import android.net.Uri -import android.view.WindowManager import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxHeight @@ -33,7 +32,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarResult import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf @@ -54,7 +52,6 @@ import com.wire.android.appLogger import com.wire.android.media.audiomessage.AudioState import com.wire.android.model.SnackBarMessage import com.wire.android.navigation.hiltSavedStateViewModel -import com.wire.android.ui.LocalActivity import com.wire.android.ui.common.bottomsheet.MenuModalSheetHeader import com.wire.android.ui.common.bottomsheet.MenuModalSheetLayout import com.wire.android.ui.common.dialogs.calling.CallingFeatureUnavailableDialog @@ -126,18 +123,6 @@ fun ConversationScreen( val coroutineScope = rememberCoroutineScope() val showDialog = remember { mutableStateOf(ConversationScreenDialogType.NONE) } - val activity = LocalActivity.current - DisposableEffect(conversationInfoViewModel.conversationInfoViewState.screenshotCensoringEnabled) { - if (conversationInfoViewModel.conversationInfoViewState.screenshotCensoringEnabled) { - activity.window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) - } else { - activity.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) - } - onDispose { - activity.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) - } - } - val startCallAudioPermissionCheck = StartCallAudioBluetoothPermissionCheckFlow { conversationCallViewModel.navigateToInitiatingCallScreen() } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModel.kt index 3ebf2a2e1a5..fca36a2e336 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModel.kt @@ -48,8 +48,6 @@ import com.wire.kalium.logic.data.user.ConnectionState import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase import com.wire.kalium.logic.feature.user.GetSelfUserUseCase -import com.wire.kalium.logic.feature.user.screenshotCensoring.ObserveScreenshotCensoringConfigResult -import com.wire.kalium.logic.feature.user.screenshotCensoring.ObserveScreenshotCensoringConfigUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch @@ -63,7 +61,6 @@ class ConversationInfoViewModel @Inject constructor( private val navigationManager: NavigationManager, private val observeConversationDetails: ObserveConversationDetailsUseCase, private val observerSelfUser: GetSelfUserUseCase, - private val observeScreenshotCensoringConfigUseCase: ObserveScreenshotCensoringConfigUseCase, private val wireSessionImageLoader: WireSessionImageLoader, private val dispatchers: DispatcherProvider ) : SavedStateViewModel(savedStateHandle) { @@ -78,7 +75,6 @@ class ConversationInfoViewModel @Inject constructor( init { getSelfUserId() - observeScreenshotCensoringConfig() } private fun getSelfUserId() { @@ -87,16 +83,6 @@ class ConversationInfoViewModel @Inject constructor( } } - private fun observeScreenshotCensoringConfig() { - viewModelScope.launch { - observeScreenshotCensoringConfigUseCase().collect { - conversationInfoViewState = conversationInfoViewState.copy( - screenshotCensoringEnabled = it is ObserveScreenshotCensoringConfigResult.Enabled - ) - } - } - } - /* If this would be collected in the scope of this ViewModel (in `init` for instance) then there would be a race condition. [MessageComposerViewModel] handles the navigating back after removing a group and here it would navigate to home if the group diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewState.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewState.kt index c3d37deadc5..6a4146dc59a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewState.kt @@ -34,7 +34,6 @@ data class ConversationInfoViewState( val conversationAvatar: ConversationAvatar = ConversationAvatar.None, val hasUserPermissionToEdit: Boolean = false, val conversationType: Conversation.Type = Conversation.Type.ONE_ON_ONE, - val screenshotCensoringEnabled: Boolean = true, ) sealed class ConversationDetailsData { From 8a4f29af538bad25fafdf482c45e2965fd314514 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= Date: Mon, 17 Jul 2023 18:04:13 +0200 Subject: [PATCH 08/10] move and fix tests --- .../android/ui/WireActivityViewModelTest.kt | 46 ++++++++++++++++++- .../ConversationInfoViewModelArrangement.kt | 11 ----- .../info/ConversationInfoViewModelTest.kt | 31 ------------- 3 files changed, 45 insertions(+), 43 deletions(-) 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 461b9bd5de1..75de4ce17d2 100644 --- a/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt @@ -21,6 +21,7 @@ package com.wire.android.ui import android.content.Intent +import com.wire.android.config.CoroutineTestExtension import com.wire.android.config.TestDispatcherProvider import com.wire.android.config.mockUri import com.wire.android.di.AuthServerConfigProvider @@ -58,6 +59,8 @@ 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.GetSessionsUseCase +import com.wire.kalium.logic.feature.user.screenshotCensoring.ObserveScreenshotCensoringConfigResult +import com.wire.kalium.logic.feature.user.screenshotCensoring.ObserveScreenshotCensoringConfigUseCase import com.wire.kalium.logic.feature.user.webSocketStatus.ObservePersistentWebSocketConnectionStatusUseCase import com.wire.kalium.logic.sync.ObserveSyncStateUseCase import io.mockk.MockKAnnotations @@ -77,9 +80,11 @@ import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.amshove.kluent.internal.assertEquals import org.amshove.kluent.`should be equal to` -import org.junit.Test +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith @OptIn(ExperimentalCoroutinesApi::class) +@ExtendWith(CoroutineTestExtension::class) class WireActivityViewModelTest { @Test @@ -638,6 +643,34 @@ class WireActivityViewModelTest { coVerify(exactly = 1) { arrangement.clearNewClientsForUser(USER_ID) } } + + @Test + fun `given screenshot censoring disabled, when observing it, then set screenshotCensoringEnabled state to false`() = runTest { + val (_, viewModel) = Arrangement() + .withScreenshotCensoringConfig(ObserveScreenshotCensoringConfigResult.Disabled) + .arrange() + advanceUntilIdle() + assertEquals(false, viewModel.globalAppState.screenshotCensoringEnabled) + } + + @Test + fun `given screenshot censoring enabled by user, when observing it, then set screenshotCensoringEnabled state to true`() = runTest { + val (_, viewModel) = Arrangement() + .withScreenshotCensoringConfig(ObserveScreenshotCensoringConfigResult.Enabled.ChosenByUser) + .arrange() + advanceUntilIdle() + assertEquals(true, viewModel.globalAppState.screenshotCensoringEnabled) + } + + @Test + fun `given screenshot censoring enforced by team, when observing it, then set screenshotCensoringEnabled state to true`() = runTest { + val (_, viewModel) = Arrangement() + .withScreenshotCensoringConfig(ObserveScreenshotCensoringConfigResult.Enabled.EnforcedByTeamSelfDeletingSettings) + .arrange() + advanceUntilIdle() + assertEquals(true, viewModel.globalAppState.screenshotCensoringEnabled) + } + private class Arrangement { init { // Tests setup @@ -655,6 +688,8 @@ class WireActivityViewModelTest { every { observeSyncStateUseCase() } returns emptyFlow() coEvery { observeIfAppUpdateRequired(any()) } returns flowOf(false) coEvery { observeNewClients() } returns flowOf() + coEvery { observeScreenshotCensoringConfigUseCase() } returns flowOf(ObserveScreenshotCensoringConfigResult.Disabled) + coEvery { currentScreenManager.observeCurrentScreen(any()) } returns MutableStateFlow(CurrentScreen.SomeOther) } @MockK @@ -704,6 +739,9 @@ class WireActivityViewModelTest { @MockK lateinit var currentScreenManager: CurrentScreenManager + @MockK + private lateinit var observeScreenshotCensoringConfigUseCase: ObserveScreenshotCensoringConfigUseCase + private val viewModel by lazy { WireActivityViewModel( coreLogic = coreLogic, @@ -722,6 +760,7 @@ class WireActivityViewModelTest { observeNewClients = observeNewClients, clearNewClientsForUser = clearNewClientsForUser, currentScreenManager = currentScreenManager, + observeScreenshotCensoringConfigUseCase = observeScreenshotCensoringConfigUseCase ) } @@ -732,6 +771,7 @@ class WireActivityViewModelTest { fun withNoCurrentSession(): Arrangement { coEvery { currentSessionFlow() } returns flowOf(CurrentSessionResult.Failure.SessionNotFound) + coEvery { coreLogic.getGlobalScope().session.currentSession() } returns CurrentSessionResult.Failure.SessionNotFound return this } @@ -804,6 +844,10 @@ class WireActivityViewModelTest { coEvery { currentScreenManager.observeCurrentScreen(any()) } returns currentScreenFlow } + suspend fun withScreenshotCensoringConfig(result: ObserveScreenshotCensoringConfigResult) = apply { + coEvery { observeScreenshotCensoringConfigUseCase() } returns flowOf(result) + } + fun arrange() = this to viewModel } diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelArrangement.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelArrangement.kt index 862e2c32765..5d5145b92c8 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelArrangement.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelArrangement.kt @@ -32,8 +32,6 @@ import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.data.id.QualifiedIdMapper import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase import com.wire.kalium.logic.feature.user.GetSelfUserUseCase -import com.wire.kalium.logic.feature.user.screenshotCensoring.ObserveScreenshotCensoringConfigResult -import com.wire.kalium.logic.feature.user.screenshotCensoring.ObserveScreenshotCensoringConfigUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.every @@ -67,9 +65,6 @@ class ConversationInfoViewModelArrangement { @MockK private lateinit var wireSessionImageLoader: WireSessionImageLoader - @MockK - private lateinit var observeScreenshotCensoringConfigUseCase: ObserveScreenshotCensoringConfigUseCase - private val viewModel: ConversationInfoViewModel by lazy { ConversationInfoViewModel( qualifiedIdMapper, @@ -77,7 +72,6 @@ class ConversationInfoViewModelArrangement { navigationManager, observeConversationDetails, observerSelfUser, - observeScreenshotCensoringConfigUseCase, wireSessionImageLoader, TestDispatcherProvider() ) @@ -93,7 +87,6 @@ class ConversationInfoViewModelArrangement { coEvery { observeConversationDetails(any()) } returns conversationDetailsChannel.consumeAsFlow().map { ObserveConversationDetailsUseCase.Result.Success(it) } - coEvery { observeScreenshotCensoringConfigUseCase() } returns flowOf(ObserveScreenshotCensoringConfigResult.Disabled) } suspend fun withConversationDetailUpdate(conversationDetails: ConversationDetails) = apply { @@ -107,9 +100,5 @@ class ConversationInfoViewModelArrangement { coEvery { observerSelfUser() } returns flowOf(TestUser.SELF_USER) } - suspend fun withScreenshotCensoringConfig(result: ObserveScreenshotCensoringConfigResult) = apply { - coEvery { observeScreenshotCensoringConfigUseCase() } returns flowOf(result) - } - fun arrange() = this to viewModel } diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelTest.kt index 0cc8b3112d5..9ca5d9aad19 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelTest.kt @@ -32,7 +32,6 @@ import com.wire.kalium.logic.data.conversation.ConversationDetails import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.data.user.UserId -import com.wire.kalium.logic.feature.user.screenshotCensoring.ObserveScreenshotCensoringConfigResult import io.mockk.coEvery import io.mockk.coVerify import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -270,34 +269,4 @@ class ConversationInfoViewModelTest { cancel() } } - - @Test - fun `given screenshot censoring disabled, when observing it, then set screenshotCensoringEnabled state to false`() = runTest { - val (_, viewModel) = ConversationInfoViewModelArrangement() - .withSelfUser() - .withScreenshotCensoringConfig(ObserveScreenshotCensoringConfigResult.Disabled) - .arrange() - advanceUntilIdle() - assertEquals(false, viewModel.conversationInfoViewState.screenshotCensoringEnabled) - } - - @Test - fun `given screenshot censoring enabled by user, when observing it, then set screenshotCensoringEnabled state to true`() = runTest { - val (_, viewModel) = ConversationInfoViewModelArrangement() - .withSelfUser() - .withScreenshotCensoringConfig(ObserveScreenshotCensoringConfigResult.Enabled.ChosenByUser) - .arrange() - advanceUntilIdle() - assertEquals(true, viewModel.conversationInfoViewState.screenshotCensoringEnabled) - } - - @Test - fun `given screenshot censoring enforced by team, when observing it, then set screenshotCensoringEnabled state to true`() = runTest { - val (_, viewModel) = ConversationInfoViewModelArrangement() - .withSelfUser() - .withScreenshotCensoringConfig(ObserveScreenshotCensoringConfigResult.Enabled.EnforcedByTeamSelfDeletingSettings) - .arrange() - advanceUntilIdle() - assertEquals(true, viewModel.conversationInfoViewState.screenshotCensoringEnabled) - } } From 1a66d4b9e95d4a19adad7bf05fb182d4ef6cc6e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= Date: Mon, 17 Jul 2023 18:08:48 +0200 Subject: [PATCH 09/10] detekt --- .../test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt | 1 - 1 file changed, 1 deletion(-) 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 75de4ce17d2..56b1993279d 100644 --- a/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt @@ -643,7 +643,6 @@ class WireActivityViewModelTest { coVerify(exactly = 1) { arrangement.clearNewClientsForUser(USER_ID) } } - @Test fun `given screenshot censoring disabled, when observing it, then set screenshotCensoringEnabled state to false`() = runTest { val (_, viewModel) = Arrangement() From 5b81aaeea425bad1a85a10f85226804e5c7febf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= Date: Mon, 17 Jul 2023 18:50:09 +0200 Subject: [PATCH 10/10] observe current session changes --- ...creenshotCensoringConfigUseCaseProvider.kt | 38 +++++++++++++++++++ .../wire/android/ui/WireActivityViewModel.kt | 21 ++++++---- .../android/ui/WireActivityViewModelTest.kt | 35 +++++++++++++---- 3 files changed, 78 insertions(+), 16 deletions(-) create mode 100644 app/src/main/kotlin/com/wire/android/di/ObserveScreenshotCensoringConfigUseCaseProvider.kt diff --git a/app/src/main/kotlin/com/wire/android/di/ObserveScreenshotCensoringConfigUseCaseProvider.kt b/app/src/main/kotlin/com/wire/android/di/ObserveScreenshotCensoringConfigUseCaseProvider.kt new file mode 100644 index 00000000000..5ede4b40d05 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/di/ObserveScreenshotCensoringConfigUseCaseProvider.kt @@ -0,0 +1,38 @@ +/* + * Wire + * Copyright (C) 2023 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.di + +import com.wire.kalium.logic.CoreLogic +import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.feature.user.screenshotCensoring.ObserveScreenshotCensoringConfigUseCase +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +class ObserveScreenshotCensoringConfigUseCaseProvider @AssistedInject constructor( + @KaliumCoreLogic private val coreLogic: CoreLogic, + @Assisted private val userId: UserId +) { + val observeScreenshotCensoringConfig: ObserveScreenshotCensoringConfigUseCase + get() = coreLogic.getSessionScope(userId).observeScreenshotCensoringConfig + + @AssistedFactory + interface Factory { + fun create(userId: UserId): ObserveScreenshotCensoringConfigUseCaseProvider + } +} 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 1c6e743a9bb..3c74b8abfd7 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt @@ -30,6 +30,7 @@ import com.wire.android.BuildConfig import com.wire.android.appLogger import com.wire.android.di.AuthServerConfigProvider import com.wire.android.di.KaliumCoreLogic +import com.wire.android.di.ObserveScreenshotCensoringConfigUseCaseProvider import com.wire.android.di.ObserveSyncStateUseCaseProvider import com.wire.android.feature.AccountSwitchUseCase import com.wire.android.feature.SwitchAccountParam @@ -70,7 +71,6 @@ import com.wire.kalium.logic.feature.session.CurrentSessionResult 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 -import com.wire.kalium.logic.feature.user.screenshotCensoring.ObserveScreenshotCensoringConfigUseCase import com.wire.kalium.logic.feature.user.webSocketStatus.ObservePersistentWebSocketConnectionStatusUseCase import com.wire.kalium.util.DateTimeUtil.toIsoDateTimeString import dagger.hilt.android.lifecycle.HiltViewModel @@ -111,7 +111,7 @@ class WireActivityViewModel @Inject constructor( private val observeNewClients: ObserveNewClientsUseCase, private val clearNewClientsForUser: ClearNewClientsForUserUseCase, private val currentScreenManager: CurrentScreenManager, - private val observeScreenshotCensoringConfigUseCase: ObserveScreenshotCensoringConfigUseCase, + private val observeScreenshotCensoringConfigUseCaseProviderFactory: ObserveScreenshotCensoringConfigUseCaseProvider.Factory, ) : ViewModel() { var globalAppState: GlobalAppState by mutableStateOf(GlobalAppState()) @@ -183,12 +183,17 @@ class WireActivityViewModel @Inject constructor( } private fun observeScreenshotCensoringConfigState() { - viewModelScope.launch { - observeScreenshotCensoringConfigUseCase().collect { - globalAppState = globalAppState.copy( - screenshotCensoringEnabled = it is ObserveScreenshotCensoringConfigResult.Enabled - ) - } + viewModelScope.launch(dispatchers.io()) { + observeUserId + .flatMapLatest { + it?.let { + observeScreenshotCensoringConfigUseCaseProviderFactory.create(it).observeScreenshotCensoringConfig() + } ?: flowOf(ObserveScreenshotCensoringConfigResult.Disabled) + }.collect { + globalAppState = globalAppState.copy( + screenshotCensoringEnabled = it is ObserveScreenshotCensoringConfigResult.Enabled + ) + } } } 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 56b1993279d..c6967e4ffcb 100644 --- a/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt @@ -25,6 +25,7 @@ import com.wire.android.config.CoroutineTestExtension import com.wire.android.config.TestDispatcherProvider import com.wire.android.config.mockUri import com.wire.android.di.AuthServerConfigProvider +import com.wire.android.di.ObserveScreenshotCensoringConfigUseCaseProvider import com.wire.android.di.ObserveSyncStateUseCaseProvider import com.wire.android.feature.AccountSwitchUseCase import com.wire.android.framework.TestClient @@ -644,30 +645,43 @@ class WireActivityViewModelTest { } @Test - fun `given screenshot censoring disabled, when observing it, then set screenshotCensoringEnabled state to false`() = runTest { + fun `given session and screenshot censoring disabled, when observing it, then set state to false`() = runTest { + val (_, viewModel) = Arrangement() + .withSomeCurrentSession() + .withScreenshotCensoringConfig(ObserveScreenshotCensoringConfigResult.Disabled) + .arrange() + advanceUntilIdle() + assertEquals(false, viewModel.globalAppState.screenshotCensoringEnabled) + } + + @Test + fun `given session and screenshot censoring enabled by user, when observing it, then set state to true`() = runTest { val (_, viewModel) = Arrangement() - .withScreenshotCensoringConfig(ObserveScreenshotCensoringConfigResult.Disabled) + .withSomeCurrentSession() + .withScreenshotCensoringConfig(ObserveScreenshotCensoringConfigResult.Enabled.ChosenByUser) .arrange() advanceUntilIdle() - assertEquals(false, viewModel.globalAppState.screenshotCensoringEnabled) + assertEquals(true, viewModel.globalAppState.screenshotCensoringEnabled) } @Test - fun `given screenshot censoring enabled by user, when observing it, then set screenshotCensoringEnabled state to true`() = runTest { + fun `given session and screenshot censoring enforced by team, when observing it, then set state to true`() = runTest { val (_, viewModel) = Arrangement() - .withScreenshotCensoringConfig(ObserveScreenshotCensoringConfigResult.Enabled.ChosenByUser) + .withSomeCurrentSession() + .withScreenshotCensoringConfig(ObserveScreenshotCensoringConfigResult.Enabled.EnforcedByTeamSelfDeletingSettings) .arrange() advanceUntilIdle() assertEquals(true, viewModel.globalAppState.screenshotCensoringEnabled) } @Test - fun `given screenshot censoring enforced by team, when observing it, then set screenshotCensoringEnabled state to true`() = runTest { + fun `given no session, when observing screenshot censoring, then set state to false`() = runTest { val (_, viewModel) = Arrangement() + .withNoCurrentSession() .withScreenshotCensoringConfig(ObserveScreenshotCensoringConfigResult.Enabled.EnforcedByTeamSelfDeletingSettings) .arrange() advanceUntilIdle() - assertEquals(true, viewModel.globalAppState.screenshotCensoringEnabled) + assertEquals(false, viewModel.globalAppState.screenshotCensoringEnabled) } private class Arrangement { @@ -687,6 +701,8 @@ class WireActivityViewModelTest { every { observeSyncStateUseCase() } returns emptyFlow() coEvery { observeIfAppUpdateRequired(any()) } returns flowOf(false) coEvery { observeNewClients() } returns flowOf() + every { observeScreenshotCensoringConfigUseCaseProviderFactory.create(any()).observeScreenshotCensoringConfig } returns + observeScreenshotCensoringConfigUseCase coEvery { observeScreenshotCensoringConfigUseCase() } returns flowOf(ObserveScreenshotCensoringConfigResult.Disabled) coEvery { currentScreenManager.observeCurrentScreen(any()) } returns MutableStateFlow(CurrentScreen.SomeOther) } @@ -741,6 +757,9 @@ class WireActivityViewModelTest { @MockK private lateinit var observeScreenshotCensoringConfigUseCase: ObserveScreenshotCensoringConfigUseCase + @MockK + private lateinit var observeScreenshotCensoringConfigUseCaseProviderFactory: ObserveScreenshotCensoringConfigUseCaseProvider.Factory + private val viewModel by lazy { WireActivityViewModel( coreLogic = coreLogic, @@ -759,7 +778,7 @@ class WireActivityViewModelTest { observeNewClients = observeNewClients, clearNewClientsForUser = clearNewClientsForUser, currentScreenManager = currentScreenManager, - observeScreenshotCensoringConfigUseCase = observeScreenshotCensoringConfigUseCase + observeScreenshotCensoringConfigUseCaseProviderFactory = observeScreenshotCensoringConfigUseCaseProviderFactory ) }