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: add toggle to opt-out analytics (WPB-8922) #3008

Merged
merged 7 commits into from
May 15, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ class GlobalDataStore @Inject constructor(@ApplicationContext private val contex

val APP_THEME_OPTION = stringPreferencesKey("app_theme_option")
val RECORD_AUDIO_EFFECTS_CHECKBOX = booleanPreferencesKey("record_audio_effects_checkbox")
val ANONYMOUS_USAGE_DATA_STATUS = booleanPreferencesKey("anonymous_usage_data_status")

private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = PREFERENCES_NAME)

Expand Down Expand Up @@ -104,6 +105,13 @@ class GlobalDataStore @Inject constructor(@ApplicationContext private val contex
context.dataStore.edit { it[RECORD_AUDIO_EFFECTS_CHECKBOX] = enabled }
}

fun isAnonymousUsageDataEnabled(): Flow<Boolean> =
getBooleanPreference(ANONYMOUS_USAGE_DATA_STATUS, true)

suspend fun setAnonymousUsageDataEnabled(enabled: Boolean) {
context.dataStore.edit { it[ANONYMOUS_USAGE_DATA_STATUS] = enabled }
}

fun isEncryptedProteusStorageEnabled(): Flow<Boolean> =
getBooleanPreference(
IS_ENCRYPTED_PROTEUS_STORAGE_ENABLED,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,25 +46,29 @@ fun PrivacySettingsConfigScreen(
) {
with(viewModel) {
PrivacySettingsScreenContent(
isAnonymousUsageDataEnabled = state.isAnonymousUsageDataEnabled,
areReadReceiptsEnabled = state.areReadReceiptsEnabled,
setReadReceiptsState = ::setReadReceiptsState,
isTypingIndicatorEnabled = state.isTypingIndicatorEnabled,
setTypingIndicatorState = ::setTypingIndicatorState,
screenshotCensoringConfig = state.screenshotCensoringConfig,
setScreenshotCensoringConfig = ::setScreenshotCensoringConfig,
setAnonymousUsageDataEnabled = ::setAnonymousUsageDataEnabled,
onBackPressed = navigator::navigateBack,
)
}
}

@Composable
fun PrivacySettingsScreenContent(
isAnonymousUsageDataEnabled: Boolean,
areReadReceiptsEnabled: Boolean,
setReadReceiptsState: (Boolean) -> Unit,
isTypingIndicatorEnabled: Boolean,
setTypingIndicatorState: (Boolean) -> Unit,
screenshotCensoringConfig: ScreenshotCensoringConfig,
setScreenshotCensoringConfig: (Boolean) -> Unit,
setAnonymousUsageDataEnabled: (Boolean) -> Unit,
onBackPressed: () -> Unit,
) {
WireScaffold(topBar = {
Expand All @@ -79,6 +83,12 @@ fun PrivacySettingsScreenContent(
.fillMaxSize()
.padding(internalPadding)
) {
GroupConversationOptionsItem(
title = stringResource(id = R.string.settings_send_anonymous_usage_data_title),
switchState = SwitchState.Enabled(value = isAnonymousUsageDataEnabled, onCheckedChange = setAnonymousUsageDataEnabled),
arrowType = ArrowType.NONE,
subtitle = stringResource(id = R.string.settings_send_anonymous_usage_data_description)
)
GroupConversationOptionsItem(
title = stringResource(R.string.settings_send_read_receipts),
switchState = SwitchState.Enabled(value = areReadReceiptsEnabled, onCheckedChange = setReadReceiptsState),
Expand Down Expand Up @@ -119,12 +129,14 @@ fun PrivacySettingsScreenContent(
@Preview
fun PreviewSendReadReceipts() {
PrivacySettingsScreenContent(
isAnonymousUsageDataEnabled = true,
areReadReceiptsEnabled = true,
setReadReceiptsState = {},
isTypingIndicatorEnabled = true,
setTypingIndicatorState = {},
screenshotCensoringConfig = ScreenshotCensoringConfig.DISABLED,
setScreenshotCensoringConfig = {},
setAnonymousUsageDataEnabled = {},
onBackPressed = {},
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
package com.wire.android.ui.home.settings.privacy

data class PrivacySettingsState(
val isAnonymousUsageDataEnabled: Boolean = true,
val areReadReceiptsEnabled: Boolean = true,
val isTypingIndicatorEnabled: Boolean = true,
val screenshotCensoringConfig: ScreenshotCensoringConfig = ScreenshotCensoringConfig.ENABLED_BY_USER,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ 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.util.dispatchers.DispatcherProvider
import com.wire.kalium.logic.feature.user.readReceipts.ObserveReadReceiptsEnabledUseCase
import com.wire.kalium.logic.feature.user.readReceipts.PersistReadReceiptsStatusConfigUseCase
Expand All @@ -50,6 +51,7 @@ class PrivacySettingsViewModel @Inject constructor(
private val observeScreenshotCensoringConfig: ObserveScreenshotCensoringConfigUseCase,
private val persistTypingIndicatorStatusConfig: PersistTypingIndicatorStatusConfigUseCase,
private val observeTypingIndicatorEnabled: ObserveTypingIndicatorEnabledUseCase,
private val globalDataStore: GlobalDataStore
) : ViewModel() {

var state by mutableStateOf(PrivacySettingsState())
Expand All @@ -61,8 +63,10 @@ class PrivacySettingsViewModel @Inject constructor(
observeReadReceiptsEnabled(),
observeTypingIndicatorEnabled(),
observeScreenshotCensoringConfig(),
) { readReceiptsEnabled, typingIndicatorEnabled, screenshotCensoringConfig ->
globalDataStore.isAnonymousUsageDataEnabled()
) { readReceiptsEnabled, typingIndicatorEnabled, screenshotCensoringConfig, anonymousUsageDataEnabled ->
PrivacySettingsState(
isAnonymousUsageDataEnabled = anonymousUsageDataEnabled,
areReadReceiptsEnabled = readReceiptsEnabled,
isTypingIndicatorEnabled = typingIndicatorEnabled,
screenshotCensoringConfig = when (screenshotCensoringConfig) {
Expand Down Expand Up @@ -127,4 +131,13 @@ class PrivacySettingsViewModel @Inject constructor(
}
}
}

fun setAnonymousUsageDataEnabled(enabled: Boolean) {
viewModelScope.launch {
globalDataStore.setAnonymousUsageDataEnabled(enabled)
}
state = state.copy(
isAnonymousUsageDataEnabled = enabled
)
}
}
2 changes: 2 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1000,6 +1000,8 @@
<string name="settings_show_typing_indicator_title">Typing indicator</string>
<string name="settings_show_typing_indicator_description">When this is off, you won\'t be able to see when other people are typing, and others won\'t see when you are typing. This setting applies to all conversations on this device.</string>
<string name="settings_app_lock_title">Lock with passcode</string>
<string name="settings_send_anonymous_usage_data_title">Send anonymous usage data</string>
<string name="settings_send_anonymous_usage_data_description">Usage data allow Wire to understand how the app is being used, and how it can be improved. The data is anonymous and doesn’t contains the content of your communications (such as messages, files, location, or calls)</string>
<!--Privacy Settings, App lock -->
<string name="settings_set_lock_screen_title">Set app lock passcode</string>
<string name="settings_set_lock_screen_description">The app will lock itself after %1$s of inactivity. To unlock the app you need to enter this passcode or use biometric authentication.\n\nMake sure to remember this passcode as there is no way to recover it.</string>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/*
* Wire
* Copyright (C) 2024 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.ui.home.settings.privacy

import com.wire.android.config.CoroutineTestExtension
import com.wire.android.config.TestDispatcherProvider
import com.wire.android.datastore.GlobalDataStore
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 com.wire.kalium.logic.feature.user.typingIndicator.ObserveTypingIndicatorEnabledUseCase
import com.wire.kalium.logic.feature.user.typingIndicator.PersistTypingIndicatorStatusConfigUseCase
import com.wire.kalium.logic.feature.user.typingIndicator.TypingIndicatorConfigResult
import io.mockk.MockKAnnotations
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.internal.assertEquals
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith

@ExtendWith(CoroutineTestExtension::class)
class PrivacySettingsViewModelTest {

@Test
fun `given user opens privacy settings screen, when viewing anonymous usage data, then its value is enabled`() = runTest {
// given
val (_, viewModel) = Arrangement()
.withEnabledAnonymousUsageData()
.arrange()

// when
// then
assertEquals(
true,
viewModel.state.isAnonymousUsageDataEnabled
)
}

@Test
fun `given user opens privacy settings screen, when viewing anonymous usage data, then its value is not enabled`() = runTest {
// given
val (_, viewModel) = Arrangement()
.withDisabledAnonymousUsageData()
.arrange()

// when
// then
assertEquals(
false,
viewModel.state.isAnonymousUsageDataEnabled
)
}

@Test
fun `given anonymous usage data is set to true, when setting to false, then correct value is propagated`() = runTest {
// given
val (_, viewModel) = Arrangement()
.withEnabledAnonymousUsageData()
.arrange()

// when
viewModel.setAnonymousUsageDataEnabled(enabled = false)

// then
assertEquals(
false,
viewModel.state.isAnonymousUsageDataEnabled
)
}

@Test
fun `given anonymous usage data is set to false, when setting to true, then correct value is propagated`() = runTest {
// given
val (_, viewModel) = Arrangement()
.withDisabledAnonymousUsageData()
.arrange()

// when
viewModel.setAnonymousUsageDataEnabled(enabled = true)

// then
assertEquals(
true,
viewModel.state.isAnonymousUsageDataEnabled
)
}

private class Arrangement {
val persistReadReceiptsStatusConfig = mockk<PersistReadReceiptsStatusConfigUseCase>()
val observeReadReceiptsEnabled = mockk<ObserveReadReceiptsEnabledUseCase>()
val persistScreenshotCensoringConfig = mockk<PersistScreenshotCensoringConfigUseCase>()
val observeScreenshotCensoringConfig = mockk<ObserveScreenshotCensoringConfigUseCase>()
val persistTypingIndicatorStatusConfig = mockk<PersistTypingIndicatorStatusConfigUseCase>()
val observeTypingIndicatorEnabled = mockk<ObserveTypingIndicatorEnabledUseCase>()
val globalDataStore = mockk<GlobalDataStore>()

val viewModel by lazy {
PrivacySettingsViewModel(
dispatchers = TestDispatcherProvider(),
persistReadReceiptsStatusConfig = persistReadReceiptsStatusConfig,
observeReadReceiptsEnabled = observeReadReceiptsEnabled,
persistScreenshotCensoringConfig = persistScreenshotCensoringConfig,
observeScreenshotCensoringConfig = observeScreenshotCensoringConfig,
persistTypingIndicatorStatusConfig = persistTypingIndicatorStatusConfig,
observeTypingIndicatorEnabled = observeTypingIndicatorEnabled,
globalDataStore = globalDataStore
)
}

init {
MockKAnnotations.init(this, relaxUnitFun = true)

coEvery { persistReadReceiptsStatusConfig.invoke(true) } returns ReadReceiptStatusConfigResult.Success
coEvery { observeReadReceiptsEnabled() } returns flowOf(true)
coEvery { persistScreenshotCensoringConfig.invoke(true) } returns PersistScreenshotCensoringConfigResult.Success
coEvery { observeScreenshotCensoringConfig() } returns
flowOf(ObserveScreenshotCensoringConfigResult.Enabled.ChosenByUser)
coEvery { persistTypingIndicatorStatusConfig.invoke(true) } returns TypingIndicatorConfigResult.Success
coEvery { observeTypingIndicatorEnabled() } returns flowOf(true)
coEvery { globalDataStore.setAnonymousUsageDataEnabled(any()) } returns Unit
}

fun withEnabledAnonymousUsageData() = apply {
every { globalDataStore.isAnonymousUsageDataEnabled() } returns flowOf(true)
}

fun withDisabledAnonymousUsageData() = apply {
every { globalDataStore.isAnonymousUsageDataEnabled() } returns flowOf(false)
}

fun arrange() = this to viewModel
}
}
Loading