Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: screenshot censoring [WPB-2880] #1950

Merged
merged 14 commits into from
Jul 18, 2023
16 changes: 16 additions & 0 deletions app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Original file line number Diff line number Diff line change
@@ -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
saleniuk marked this conversation as resolved.
Show resolved Hide resolved

@AssistedFactory
interface Factory {
fun create(userId: UserId): ObserveScreenshotCensoringConfigUseCaseProvider
}
}
22 changes: 21 additions & 1 deletion app/src/main/kotlin/com/wire/android/ui/WireActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@

package com.wire.android.ui

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
Expand All @@ -37,6 +39,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
Expand Down Expand Up @@ -137,7 +140,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 {
Expand All @@ -149,6 +153,7 @@ class WireActivity : AppCompatActivity() {
startDestination = startDestination,
onComplete = onComplete
)
handleScreenshotCensoring()
handleDialogs()
}
}
Expand Down Expand Up @@ -209,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)
Expand Down Expand Up @@ -436,3 +452,7 @@ class WireActivity : AppCompatActivity() {
private const val HANDLED_DEEPLINK_FLAG = "deeplink_handled_flag_key"
}
}

val LocalActivity = staticCompositionLocalOf<Activity> {
error("No Activity provided")
}
34 changes: 33 additions & 1 deletion app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -69,6 +70,7 @@ 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.webSocketStatus.ObservePersistentWebSocketConnectionStatusUseCase
import com.wire.kalium.util.DateTimeUtil.toIsoDateTimeString
import dagger.hilt.android.lifecycle.HiltViewModel
Expand Down Expand Up @@ -109,6 +111,7 @@ class WireActivityViewModel @Inject constructor(
private val observeNewClients: ObserveNewClientsUseCase,
private val clearNewClientsForUser: ClearNewClientsForUserUseCase,
private val currentScreenManager: CurrentScreenManager,
private val observeScreenshotCensoringConfigUseCaseProviderFactory: ObserveScreenshotCensoringConfigUseCaseProvider.Factory,
) : ViewModel() {

var globalAppState: GlobalAppState by mutableStateOf(GlobalAppState())
Expand Down Expand Up @@ -138,6 +141,13 @@ class WireActivityViewModel @Inject constructor(
val observeSyncFlowState: StateFlow<SyncState?> = _observeSyncFlowState

init {
observeSyncState()
observeUpdateAppState()
observeNewClientState()
observeScreenshotCensoringConfigState()
}

private fun observeSyncState() {
viewModelScope.launch(dispatchers.io()) {
observeUserId
.flatMapLatest {
Expand All @@ -146,13 +156,19 @@ class WireActivityViewModel @Inject constructor(
.distinctUntilChanged()
.collect { _observeSyncFlowState.emit(it) }
}
}

private fun observeUpdateAppState() {
viewModelScope.launch(dispatchers.io()) {
observeIfAppUpdateRequired(BuildConfig.VERSION_CODE)
.distinctUntilChanged()
.collect {
globalAppState = globalAppState.copy(updateAppDialog = it)
}
}
}

private fun observeNewClientState() {
viewModelScope.launch(dispatchers.io()) {
currentScreenManager.observeCurrentScreen(this)
.flatMapLatest {
Expand All @@ -166,6 +182,21 @@ class WireActivityViewModel @Inject constructor(
}
}

private fun observeScreenshotCensoringConfigState() {
viewModelScope.launch(dispatchers.io()) {
observeUserId
.flatMapLatest {
it?.let {
observeScreenshotCensoringConfigUseCaseProviderFactory.create(it).observeScreenshotCensoringConfig()
} ?: flowOf(ObserveScreenshotCensoringConfigResult.Disabled)
}.collect {
globalAppState = globalAppState.copy(
screenshotCensoringEnabled = it is ObserveScreenshotCensoringConfigResult.Enabled
)
}
}
}

private suspend fun handleInvalidSession(logoutReason: LogoutReason) {
withContext(dispatchers.main()) {
when (logoutReason) {
Expand Down Expand Up @@ -527,5 +558,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,
)
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,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,
Expand All @@ -74,6 +74,10 @@ class ConversationInfoViewModel @Inject constructor(
private lateinit var selfUserId: UserId

init {
getSelfUserId()
}

private fun getSelfUserId() {
viewModelScope.launch {
selfUserId = observerSelfUser().first().id
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ 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,
)

sealed class ConversationDetailsData {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ fun PrivacySettingsConfigScreen(
PrivacySettingsScreenContent(
isReadReceiptsEnabled = state.isReadReceiptsEnabled,
setReadReceiptsState = ::setReadReceiptsState,
screenshotCensoringConfig = state.screenshotCensoringConfig,
setScreenshotCensoringConfig = ::setScreenshotCensoringConfig,
onBackPressed = ::navigateBack
)
}
Expand All @@ -53,6 +55,8 @@ fun PrivacySettingsConfigScreen(
fun PrivacySettingsScreenContent(
isReadReceiptsEnabled: Boolean,
setReadReceiptsState: (Boolean) -> Unit,
screenshotCensoringConfig: ScreenshotCensoringConfig,
setScreenshotCensoringConfig: (Boolean) -> Unit,
onBackPressed: () -> Unit
) {
Scaffold(topBar = {
Expand All @@ -73,12 +77,32 @@ 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
}
)
)
}
}
}

@Composable
@Preview
fun PreviewSendReadReceipts() {
PrivacySettingsScreenContent(true, {}, {})
PrivacySettingsScreenContent(true, {}, ScreenshotCensoringConfig.DISABLED, {}, {})
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@ 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.combine
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
Expand All @@ -42,15 +47,33 @@ 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())
private set

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
}
)
}
}
}
Expand All @@ -73,4 +96,18 @@ class PrivacySettingsViewModel @Inject constructor(
}
}
}

fun setScreenshotCensoringConfig(isEnabled: Boolean) {
viewModelScope.launch {
when (persistScreenshotCensoringConfig(isEnabled)) {
is PersistScreenshotCensoringConfigResult.Failure -> {
appLogger.e("Something went wrong while updating screenshot censoring config")
}

is PersistScreenshotCensoringConfigResult.Success -> {
appLogger.d("Screenshot censoring config changed")
}
}
}
}
}
3 changes: 3 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,9 @@
<!--Privacy Settings -->
<string name="settings_send_read_receipts">Send read receipts</string>
<string name="settings_send_read_receipts_description">If this is OFF, you won’t be able to see read receipts from other people. This setting does not apply to group conversations.</string>
<string name="settings_censor_screenshots">Censor screenshots</string>
<string name="settings_censor_screenshots_description">If this is ON, then the content of the messages will not be visible on the screenshot or screen recording.</string>
<string name="settings_censor_screenshots_enforced_by_team_description">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.</string>
<!--Devices -->
<string name="devices_title">Your Devices</string>
<string name="current_device_label">Current Device</string>
Expand Down
Loading