From 1d1ea01cc14020d35ea9b4dcb8dff80fa46aec9f Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Thu, 1 Aug 2024 15:52:51 -0300 Subject: [PATCH] Add optimize storage setting and sheet. --- .../MessageBackupsTypeSelectionScreen.kt | 10 +- .../storage/ManageStorageSettingsFragment.kt | 31 +++- .../storage/ManageStorageSettingsViewModel.kt | 47 ++++- .../UpgradeToEnableOptimizedStorageSheet.kt | 160 ++++++++++++++++++ ...pgradeToEnableOptimizedStorageViewModel.kt | 33 ++++ app/src/main/res/values/strings.xml | 14 ++ 6 files changed, 288 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/settings/app/storage/UpgradeToEnableOptimizedStorageSheet.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/settings/app/storage/UpgradeToEnableOptimizedStorageViewModel.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsTypeSelectionScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsTypeSelectionScreen.kt index a6af2f8c83..5e8e71db0b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsTypeSelectionScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsTypeSelectionScreen.kt @@ -261,6 +261,12 @@ fun MessageBackupsTypeBlock( style = MaterialTheme.typography.titleMedium ) + val featureIconTint = if (isSelected) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + } + Column( verticalArrangement = spacedBy(4.dp), modifier = Modifier @@ -268,7 +274,7 @@ fun MessageBackupsTypeBlock( .padding(horizontal = 16.dp) ) { messageBackupsType.features.forEach { - MessageBackupsTypeFeatureRow(messageBackupsTypeFeature = it) + MessageBackupsTypeFeatureRow(messageBackupsTypeFeature = it, iconTint = featureIconTint) } } } @@ -292,7 +298,7 @@ private fun formatCostPerMonth(pricePerMonth: FiatMoney): String { } } -private fun testBackupTypes(): List { +fun testBackupTypes(): List { return listOf( MessageBackupsType( tier = MessageBackupTier.FREE, diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/storage/ManageStorageSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/storage/ManageStorageSettingsFragment.kt index faa109bad1..db8201876e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/storage/ManageStorageSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/storage/ManageStorageSettingsFragment.kt @@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -67,6 +68,7 @@ import org.thoughtcrime.securesms.keyvalue.KeepMessagesDuration import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity import org.thoughtcrime.securesms.preferences.widgets.StorageGraphView import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.util.BottomSheetUtil import org.thoughtcrime.securesms.util.TextSecurePreferences import org.thoughtcrime.securesms.util.Util import org.thoughtcrime.securesms.util.viewModel @@ -79,6 +81,7 @@ class ManageStorageSettingsFragment : ComposeFragment() { private val viewModel by viewModel { ManageStorageSettingsViewModel() } + @ExperimentalMaterial3Api @Composable override fun FragmentContent() { val state by viewModel.state.collectAsState() @@ -102,7 +105,14 @@ class ManageStorageSettingsFragment : ComposeFragment() { onSetKeepMessages = { navController.navigate("set-keep-messages") }, onSetChatLengthLimit = { navController.navigate("set-chat-length-limit") }, onSyncTrimThreadDeletes = { viewModel.setSyncTrimDeletes(it) }, - onDeleteChatHistory = { navController.navigate("confirm-delete-chat-history") } + onDeleteChatHistory = { navController.navigate("confirm-delete-chat-history") }, + onToggleOnDeviceStorageOptimization = { + if (state.onDeviceStorageOptimizationState == ManageStorageSettingsViewModel.OnDeviceStorageOptimizationState.REQUIRES_PAID_TIER) { + UpgradeToEnableOptimizedStorageSheet().show(parentFragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG) + } else { + viewModel.setOptimizeStorage(it) + } + } ) } @@ -236,7 +246,8 @@ private fun ManageStorageSettingsScreen( onSetKeepMessages: () -> Unit = {}, onSetChatLengthLimit: () -> Unit = {}, onSyncTrimThreadDeletes: (Boolean) -> Unit = {}, - onDeleteChatHistory: () -> Unit = {} + onDeleteChatHistory: () -> Unit = {}, + onToggleOnDeviceStorageOptimization: (Boolean) -> Unit = {} ) { Scaffolds.Settings( title = stringResource(id = R.string.preferences__storage), @@ -252,6 +263,19 @@ private fun ManageStorageSettingsScreen( StorageOverview(state.breakdown, onReviewStorage) + if (state.onDeviceStorageOptimizationState > ManageStorageSettingsViewModel.OnDeviceStorageOptimizationState.FEATURE_NOT_AVAILABLE) { + Dividers.Default() + + Texts.SectionHeader(text = stringResource(id = R.string.ManageStorageSettingsFragment__on_device_storage)) + + Rows.ToggleRow( + checked = state.onDeviceStorageOptimizationState == ManageStorageSettingsViewModel.OnDeviceStorageOptimizationState.ENABLED, + text = stringResource(id = R.string.ManageStorageSettingsFragment__optimize_on_device_storage), + label = stringResource(id = R.string.ManageStorageSettingsFragment__unused_media_will_be_offloaded), + onCheckChanged = onToggleOnDeviceStorageOptimization + ) + } + Dividers.Default() Texts.SectionHeader(text = stringResource(id = R.string.ManageStorageSettingsFragment_chat_limit)) @@ -510,7 +534,8 @@ private fun ManageStorageSettingsScreenPreview() { ManageStorageSettingsScreen( state = ManageStorageSettingsViewModel.ManageStorageState( keepMessagesDuration = KeepMessagesDuration.FOREVER, - lengthLimit = ManageStorageSettingsViewModel.ManageStorageState.NO_LIMIT + lengthLimit = ManageStorageSettingsViewModel.ManageStorageState.NO_LIMIT, + onDeviceStorageOptimizationState = ManageStorageSettingsViewModel.OnDeviceStorageOptimizationState.DISABLED ) ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/storage/ManageStorageSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/storage/ManageStorageSettingsViewModel.kt index e3dfe8788a..031b3212a7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/storage/ManageStorageSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/storage/ManageStorageSettingsViewModel.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.signal.core.util.concurrent.SignalExecutors +import org.thoughtcrime.securesms.backup.v2.MessageBackupTier import org.thoughtcrime.securesms.database.MediaTable import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.SignalDatabase.Companion.media @@ -20,6 +21,7 @@ import org.thoughtcrime.securesms.database.ThreadTable import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.keyvalue.KeepMessagesDuration import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.util.RemoteConfig class ManageStorageSettingsViewModel : ViewModel() { @@ -27,7 +29,8 @@ class ManageStorageSettingsViewModel : ViewModel() { ManageStorageState( keepMessagesDuration = SignalStore.settings.keepMessagesDuration, lengthLimit = if (SignalStore.settings.isTrimByLengthEnabled) SignalStore.settings.threadTrimLength else ManageStorageState.NO_LIMIT, - syncTrimDeletes = SignalStore.settings.shouldSyncThreadTrimDeletes() + syncTrimDeletes = SignalStore.settings.shouldSyncThreadTrimDeletes(), + onDeviceStorageOptimizationState = getOnDeviceStorageOptimizationState() ) ) val state = store.asStateFlow() @@ -88,16 +91,56 @@ class ManageStorageSettingsViewModel : ViewModel() { store.update { it.copy(syncTrimDeletes = syncTrimDeletes) } } + fun setOptimizeStorage(enabled: Boolean) { + val storageState = getOnDeviceStorageOptimizationState() + if (storageState >= OnDeviceStorageOptimizationState.DISABLED) { + SignalStore.backup.optimizeStorage = enabled + store.update { it.copy(onDeviceStorageOptimizationState = if (enabled) OnDeviceStorageOptimizationState.ENABLED else OnDeviceStorageOptimizationState.DISABLED) } + } + } + private fun isRestrictingLengthLimitChange(newLimit: Int): Boolean { return state.value.lengthLimit == ManageStorageState.NO_LIMIT || (newLimit != ManageStorageState.NO_LIMIT && newLimit < state.value.lengthLimit) } + private fun getOnDeviceStorageOptimizationState(): OnDeviceStorageOptimizationState { + return when { + !RemoteConfig.messageBackups -> OnDeviceStorageOptimizationState.FEATURE_NOT_AVAILABLE + !SignalStore.backup.areBackupsEnabled || SignalStore.backup.backupTier != MessageBackupTier.PAID -> OnDeviceStorageOptimizationState.REQUIRES_PAID_TIER + SignalStore.backup.optimizeStorage -> OnDeviceStorageOptimizationState.ENABLED + else -> OnDeviceStorageOptimizationState.DISABLED + } + } + + enum class OnDeviceStorageOptimizationState { + /** + * The entire feature is not available and the option should not be displayed to the user. + */ + FEATURE_NOT_AVAILABLE, + + /** + * The feature is available, but the user is not on the paid backups plan. + */ + REQUIRES_PAID_TIER, + + /** + * The user is on the paid backups plan but optimized storage is disabled. + */ + DISABLED, + + /** + * The user is on the paid backups plan and optimized storage is enabled. + */ + ENABLED + } + @Immutable data class ManageStorageState( val keepMessagesDuration: KeepMessagesDuration = KeepMessagesDuration.FOREVER, val lengthLimit: Int = NO_LIMIT, val syncTrimDeletes: Boolean = true, - val breakdown: MediaTable.StorageBreakdown? = null + val breakdown: MediaTable.StorageBreakdown? = null, + val onDeviceStorageOptimizationState: OnDeviceStorageOptimizationState ) { companion object { const val NO_LIMIT = 0 diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/storage/UpgradeToEnableOptimizedStorageSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/storage/UpgradeToEnableOptimizedStorageSheet.kt new file mode 100644 index 0000000000..f4db241721 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/storage/UpgradeToEnableOptimizedStorageSheet.kt @@ -0,0 +1,160 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.settings.app.storage + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.fragment.app.viewModels +import org.signal.core.ui.BottomSheets +import org.signal.core.ui.Buttons +import org.signal.core.ui.Icons +import org.signal.core.ui.Previews +import org.signal.core.ui.SignalPreview +import org.signal.donations.InAppPaymentType +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.backup.v2.ui.BackupsIconColors +import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType +import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsTypeBlock +import org.thoughtcrime.securesms.backup.v2.ui.subscription.testBackupTypes +import org.thoughtcrime.securesms.components.settings.app.subscription.donate.CheckoutFlowActivity +import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment + +/** + * Sheet describing how users must upgrade to enable optimized storage. + */ +class UpgradeToEnableOptimizedStorageSheet : ComposeBottomSheetDialogFragment() { + + override val peekHeightPercentage: Float = 1f + + private val viewModel: UpgradeToEnableOptimizedStorageViewModel by viewModels() + + @Composable + override fun SheetContent() { + val type by viewModel.messageBackupsType + UpgradeToEnableOptimizedStorageSheetContent( + messageBackupsType = type, + onUpgradeNowClick = { + startActivity(CheckoutFlowActivity.createIntent(requireContext(), InAppPaymentType.RECURRING_BACKUP)) + dismissAllowingStateLoss() + }, + onCancelClick = { + dismissAllowingStateLoss() + } + ) + } +} + +@Composable +private fun UpgradeToEnableOptimizedStorageSheetContent( + messageBackupsType: MessageBackupsType?, + onUpgradeNowClick: () -> Unit = {}, + onCancelClick: () -> Unit = {} +) { + if (messageBackupsType == null) { + // TODO [message-backups] -- network error? + return + } + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth() + ) { + BottomSheets.Handle() + + Icons.BrushedForeground( + painter = painterResource(id = R.drawable.symbol_backup_light), + contentDescription = null, + foregroundBrush = BackupsIconColors.Normal.foreground, + modifier = Modifier + .padding(top = 8.dp, bottom = 12.dp) + .size(88.dp) + .background( + color = BackupsIconColors.Normal.background, + shape = CircleShape + ) + .padding(20.dp) + ) + + Text( + text = stringResource(id = R.string.UpgradeToEnableOptimizedStorageSheet__upgrade_to_enable_this_feature), + style = MaterialTheme.typography.titleLarge, + textAlign = TextAlign.Center, + modifier = Modifier + .padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter)) + ) + + Text( + text = stringResource(id = R.string.UpgradeToEnableOptimizedStorageSheet__storage_optimization_can_only_be_used), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + modifier = Modifier + .padding(top = 10.dp, bottom = 28.dp) + .padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter)) + ) + + MessageBackupsTypeBlock( + messageBackupsType = messageBackupsType, + isCurrent = false, + isSelected = false, + onSelected = {}, + enabled = false, + modifier = Modifier + .padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter)) + .padding(bottom = 50.dp) + ) + + Buttons.LargePrimary( + onClick = onUpgradeNowClick, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter)) + .padding(bottom = 8.dp) + ) { + Text( + text = stringResource(id = R.string.UpgradeToEnableOptimizedStorageSheet__upgrade_now) + ) + } + + TextButton( + onClick = onCancelClick, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter)) + .padding(bottom = 16.dp) + ) { + Text( + text = stringResource(id = android.R.string.cancel) + ) + } + } +} + +@SignalPreview +@Composable +private fun UpgradeToEnableOptimizedStorageSheetContentPreview() { + Previews.BottomSheetPreview { + UpgradeToEnableOptimizedStorageSheetContent( + messageBackupsType = testBackupTypes()[1] + ) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/storage/UpgradeToEnableOptimizedStorageViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/storage/UpgradeToEnableOptimizedStorageViewModel.kt new file mode 100644 index 0000000000..8bce64105f --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/storage/UpgradeToEnableOptimizedStorageViewModel.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.thoughtcrime.securesms.components.settings.app.storage + +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.thoughtcrime.securesms.backup.v2.BackupRepository +import org.thoughtcrime.securesms.backup.v2.MessageBackupTier +import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType + +class UpgradeToEnableOptimizedStorageViewModel : ViewModel() { + private val internalMessageBackupsType = mutableStateOf(null) + val messageBackupsType: State = internalMessageBackupsType + + init { + viewModelScope.launch { + val backupsType = withContext(Dispatchers.IO) { + BackupRepository.getBackupsType(MessageBackupTier.PAID) + } + + withContext(Dispatchers.Main) { + internalMessageBackupsType.value = backupsType + } + } + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e66053ea37..ed57a1bda6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -7217,6 +7217,20 @@ Apply limits to linked devices When enabled, chat limits will also delete messages from your linked devices. + + On-device storage + + Optimize on-device storage + + Unused media will be offloaded, but can be downloaded from your backup anytime. + + + + Upgrade to enable this feature + + Storage optimization can only be used with the paid tier of Signal Backups. Upgrade your backup plan to start using this feature. + + Upgrade now Deleting is now synced across all of your devices