Basic settings functionality for message backup.

This commit is contained in:
Clark
2024-05-03 12:20:09 -04:00
committed by Alex Hart
parent 0a3f96935a
commit bc527a2bc1
18 changed files with 443 additions and 268 deletions

View File

@@ -60,8 +60,12 @@ class ChatsSettingsViewModel @JvmOverloads constructor(
fun refresh() {
val backupsEnabled = SignalStore.settings().isBackupEnabled && BackupUtil.canUserAccessBackupDirectory(ApplicationDependencies.getApplication())
if (store.state.localBackupsEnabled != backupsEnabled) {
store.update { it.copy(localBackupsEnabled = backupsEnabled) }
val remoteBackupsEnabled = SignalStore.backup().areBackupsEnabled
if (store.state.localBackupsEnabled != backupsEnabled ||
store.state.remoteBackupsEnabled != remoteBackupsEnabled
) {
store.update { it.copy(localBackupsEnabled = backupsEnabled, remoteBackupsEnabled = remoteBackupsEnabled) }
}
}
}

View File

@@ -6,6 +6,8 @@
package org.thoughtcrime.securesms.components.settings.app.chats.backups
import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
@@ -17,8 +19,10 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
@@ -34,7 +38,9 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.fragment.findNavController
import kotlinx.collections.immutable.persistentListOf
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import org.signal.core.ui.Buttons
import org.signal.core.ui.Dialogs
import org.signal.core.ui.Dividers
@@ -44,18 +50,18 @@ import org.signal.core.ui.Scaffolds
import org.signal.core.ui.SignalPreview
import org.signal.core.ui.Snackbars
import org.signal.core.ui.Texts
import org.signal.core.util.money.FiatMoney
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.v2.BackupV2Event
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsFlowActivity
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsFrequency
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
import org.thoughtcrime.securesms.backup.v2.ui.subscription.getTierDetails
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.conversation.v2.registerForLifecycle
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.navigation.safeNavigate
import org.thoughtcrime.securesms.util.viewModel
import java.math.BigDecimal
import java.util.Currency
import java.util.Locale
/**
@@ -75,13 +81,14 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
val callbacks = remember { Callbacks() }
RemoteBackupsSettingsContent(
messageBackupsType = state.messageBackupsType,
messageBackupTier = state.messageBackupsTier,
lastBackupTimestamp = state.lastBackupTimestamp,
canBackUpUsingCellular = state.canBackUpUsingCellular,
backupsFrequency = state.backupsFrequency,
requestedDialog = state.dialog,
requestedSnackbar = state.snackbar,
contentCallbacks = callbacks
contentCallbacks = callbacks,
backupProgress = state.backupProgress
)
}
@@ -104,7 +111,7 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
}
override fun onBackupNowClick() {
// TODO [message-backups] Enqueue immediate backup
viewModel.onBackupNowClick()
}
override fun onTurnOffAndDeleteBackupsClick() {
@@ -135,6 +142,16 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
findNavController().safeNavigate(R.id.action_remoteBackupsSettingsFragment_to_backupsTypeSettingsFragment)
}
}
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
fun onEvent(backupEvent: BackupV2Event) {
viewModel.updateBackupProgress(backupEvent)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
EventBus.getDefault().registerForLifecycle(subscriber = this, lifecycleOwner = viewLifecycleOwner)
}
}
/**
@@ -157,13 +174,14 @@ private interface ContentCallbacks {
@Composable
private fun RemoteBackupsSettingsContent(
messageBackupsType: MessageBackupsType?,
messageBackupTier: MessageBackupTier?,
lastBackupTimestamp: Long,
canBackUpUsingCellular: Boolean,
backupsFrequency: MessageBackupsFrequency,
requestedDialog: RemoteBackupsSettingsState.Dialog,
requestedSnackbar: RemoteBackupsSettingsState.Snackbar,
contentCallbacks: ContentCallbacks
contentCallbacks: ContentCallbacks,
backupProgress: BackupV2Event?
) {
val snackbarHostState = remember {
SnackbarHostState()
@@ -183,13 +201,13 @@ private fun RemoteBackupsSettingsContent(
) {
item {
BackupTypeRow(
messageBackupsType = messageBackupsType,
messageBackupTier = messageBackupTier,
onEnableBackupsClick = contentCallbacks::onEnableBackupsClick,
onChangeBackupsTypeClick = contentCallbacks::onBackupsTypeClick
)
}
if (messageBackupsType == null) {
if (messageBackupTier == null) {
item {
Rows.TextRow(
text = "Payment history",
@@ -205,11 +223,17 @@ private fun RemoteBackupsSettingsContent(
Texts.SectionHeader(text = "Backup Details")
}
item {
LastBackupRow(
lastBackupTimestamp = lastBackupTimestamp,
onBackupNowClick = {}
)
if (backupProgress == null || backupProgress.type == BackupV2Event.Type.FINISHED) {
item {
LastBackupRow(
lastBackupTimestamp = lastBackupTimestamp,
onBackupNowClick = contentCallbacks::onBackupNowClick
)
}
} else {
item {
InProgressBackupRow(progress = backupProgress.count.toInt(), totalProgress = backupProgress.estimatedTotalCount.toInt())
}
}
item {
@@ -326,14 +350,16 @@ private fun RemoteBackupsSettingsContent(
@Composable
private fun BackupTypeRow(
messageBackupsType: MessageBackupsType?,
messageBackupTier: MessageBackupTier?,
onEnableBackupsClick: () -> Unit,
onChangeBackupsTypeClick: () -> Unit
) {
val messageBackupsType = if (messageBackupTier != null) getTierDetails(messageBackupTier) else null
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(enabled = messageBackupsType != null, onClick = onChangeBackupsTypeClick)
.clickable(enabled = messageBackupTier != null, onClick = onChangeBackupsTypeClick)
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
.padding(top = 16.dp, bottom = 14.dp)
) {
@@ -372,6 +398,34 @@ private fun BackupTypeRow(
}
}
@Composable
private fun InProgressBackupRow(
progress: Int?,
totalProgress: Int?
) {
Row(
modifier = Modifier
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
.padding(top = 16.dp, bottom = 14.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
if (totalProgress == null || totalProgress == 0) {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
} else {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth(), progress = ((progress ?: 0) / totalProgress).toFloat())
}
Text(
text = "$progress/$totalProgress",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
@Composable
private fun LastBackupRow(
lastBackupTimestamp: Long,
@@ -448,46 +502,48 @@ private fun BackupFrequencyDialog(
AlertDialog(
onDismissRequest = onDismiss
) {
Column(
modifier = Modifier
.background(
color = AlertDialogDefaults.containerColor,
shape = AlertDialogDefaults.shape
)
.fillMaxWidth()
) {
Text(
text = "Backup frequency",
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(24.dp)
)
MessageBackupsFrequency.values().forEach {
Rows.RadioRow(
selected = selected == it,
text = getTextForFrequency(backupsFrequency = it),
label = when (it) {
MessageBackupsFrequency.NEVER -> "By tapping \"Back up now\""
else -> null
},
modifier = Modifier
.padding(end = 24.dp)
.clickable(onClick = {
onSelected(it)
onDismiss()
})
)
}
Box(
contentAlignment = Alignment.CenterEnd,
Surface {
Column(
modifier = Modifier
.background(
color = AlertDialogDefaults.containerColor,
shape = AlertDialogDefaults.shape
)
.fillMaxWidth()
.padding(horizontal = 24.dp)
.padding(bottom = 24.dp)
) {
TextButton(onClick = onDismiss) {
Text(text = stringResource(id = android.R.string.cancel))
Text(
text = "Backup frequency",
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(24.dp)
)
MessageBackupsFrequency.values().forEach {
Rows.RadioRow(
selected = selected == it,
text = getTextForFrequency(backupsFrequency = it),
label = when (it) {
MessageBackupsFrequency.NEVER -> "By tapping \"Back up now\""
else -> null
},
modifier = Modifier
.padding(end = 24.dp)
.clickable(onClick = {
onSelected(it)
onDismiss()
})
)
}
Box(
contentAlignment = Alignment.CenterEnd,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp)
.padding(bottom = 24.dp)
) {
TextButton(onClick = onDismiss) {
Text(text = stringResource(id = android.R.string.cancel))
}
}
}
}
@@ -509,13 +565,14 @@ private fun getTextForFrequency(backupsFrequency: MessageBackupsFrequency): Stri
private fun RemoteBackupsSettingsContentPreview() {
Previews.Preview {
RemoteBackupsSettingsContent(
messageBackupsType = null,
messageBackupTier = null,
lastBackupTimestamp = -1,
canBackUpUsingCellular = false,
backupsFrequency = MessageBackupsFrequency.NEVER,
requestedDialog = RemoteBackupsSettingsState.Dialog.NONE,
requestedSnackbar = RemoteBackupsSettingsState.Snackbar.NONE,
contentCallbacks = object : ContentCallbacks {}
contentCallbacks = object : ContentCallbacks {},
backupProgress = null
)
}
}
@@ -525,11 +582,7 @@ private fun RemoteBackupsSettingsContentPreview() {
private fun BackupTypeRowPreview() {
Previews.Preview {
BackupTypeRow(
messageBackupsType = MessageBackupsType(
title = "Text + all media",
pricePerMonth = FiatMoney(BigDecimal.valueOf(3L), Currency.getInstance(Locale.US)),
features = persistentListOf()
),
messageBackupTier = MessageBackupTier.PAID,
onChangeBackupsTypeClick = {},
onEnableBackupsClick = {}
)
@@ -547,6 +600,14 @@ private fun LastBackupRowPreview() {
}
}
@SignalPreview
@Composable
private fun InProgressRowPreview() {
Previews.Preview {
InProgressBackupRow(50, 100)
}
}
@SignalPreview
@Composable
private fun TurnOffAndDeleteBackupsDialogPreview() {

View File

@@ -5,17 +5,19 @@
package org.thoughtcrime.securesms.components.settings.app.chats.backups
import org.thoughtcrime.securesms.backup.v2.BackupV2Event
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsFrequency
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
data class RemoteBackupsSettingsState(
val messageBackupsType: MessageBackupsType? = null,
val messageBackupsTier: MessageBackupTier? = null,
val canBackUpUsingCellular: Boolean = false,
val backupSize: Long = 0,
val backupsFrequency: MessageBackupsFrequency = MessageBackupsFrequency.DAILY,
val lastBackupTimestamp: Long = 0,
val dialog: Dialog = Dialog.NONE,
val snackbar: Snackbar = Snackbar.NONE
val snackbar: Snackbar = Snackbar.NONE,
val backupProgress: BackupV2Event? = null
) {
enum class Dialog {
NONE,

View File

@@ -8,13 +8,30 @@ package org.thoughtcrime.securesms.components.settings.app.chats.backups
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import org.thoughtcrime.securesms.backup.v2.BackupV2Event
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsFrequency
import org.thoughtcrime.securesms.jobs.BackupMessagesJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
/**
* ViewModel for state management of RemoteBackupsSettingsFragment
*/
class RemoteBackupsSettingsViewModel : ViewModel() {
private val internalState = mutableStateOf(RemoteBackupsSettingsState())
private val internalState = mutableStateOf(
RemoteBackupsSettingsState(
messageBackupsTier = if (SignalStore.backup().areBackupsEnabled) {
if (SignalStore.backup().canReadWriteToArchiveCdn) {
MessageBackupTier.PAID
} else {
MessageBackupTier.FREE
}
} else {
null
},
lastBackupTimestamp = SignalStore.backup().lastBackupTime
)
)
val state: State<RemoteBackupsSettingsState> = internalState
@@ -38,6 +55,17 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
fun turnOffAndDeleteBackups() {
// TODO [message-backups] -- Delete.
SignalStore.backup().areBackupsEnabled = false
internalState.value = state.value.copy(snackbar = RemoteBackupsSettingsState.Snackbar.BACKUP_DELETED_AND_TURNED_OFF)
}
fun updateBackupProgress(backupEvent: BackupV2Event?) {
internalState.value = state.value.copy(backupProgress = backupEvent, lastBackupTimestamp = SignalStore.backup().lastBackupTime)
}
fun onBackupNowClick() {
if (state.value.backupProgress == null || state.value.backupProgress?.type == BackupV2Event.Type.FINISHED) {
BackupMessagesJob.enqueue()
}
}
}

View File

@@ -27,6 +27,7 @@ import org.signal.core.ui.SignalPreview
import org.signal.core.util.money.FiatMoney
import org.signal.donations.PaymentSourceType
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsFlowActivity
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
import org.thoughtcrime.securesms.compose.ComposeFragment
@@ -186,6 +187,7 @@ private fun BackupsTypeSettingsContentPreview() {
BackupsTypeSettingsContent(
state = BackupsTypeSettingsState(
backupsType = MessageBackupsType(
tier = MessageBackupTier.PAID,
pricePerMonth = FiatMoney(BigDecimal.valueOf(3), Currency.getInstance("USD")),
title = "Text + all media",
features = persistentListOf()