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

@@ -650,3 +650,8 @@ class BackupMetadata(
val usedSpace: Long,
val mediaCount: Long
)
enum class MessageBackupTier {
FREE,
PAID
}

View File

@@ -0,0 +1,12 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2
class BackupV2Event(val type: Type, val count: Long, val estimatedTotalCount: Long) {
enum class Type {
PROGRESS_MESSAGES, PROGRESS_ATTACHMENTS, FINISHED
}
}

View File

@@ -30,23 +30,20 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.view.updateLayoutParams
import kotlinx.collections.immutable.persistentListOf
import org.signal.core.ui.BottomSheets
import org.signal.core.ui.Buttons
import org.signal.core.ui.Previews
import org.signal.core.util.money.FiatMoney
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayResponse
import org.thoughtcrime.securesms.components.settings.app.subscription.models.GooglePayButton
import org.thoughtcrime.securesms.databinding.PaypalButtonBinding
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
import java.math.BigDecimal
import java.util.Currency
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MessageBackupsCheckoutSheet(
messageBackupsType: MessageBackupsType,
messageBackupTier: MessageBackupTier,
availablePaymentGateways: List<GatewayResponse.Gateway>,
onDismissRequest: () -> Unit,
onPaymentGatewaySelected: (GatewayResponse.Gateway) -> Unit
@@ -57,7 +54,7 @@ fun MessageBackupsCheckoutSheet(
modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
) {
SheetContent(
messageBackupsType = messageBackupsType,
messageBackupTier = messageBackupTier,
availablePaymentGateways = availablePaymentGateways,
onPaymentGatewaySelected = onPaymentGatewaySelected
)
@@ -66,13 +63,16 @@ fun MessageBackupsCheckoutSheet(
@Composable
private fun SheetContent(
messageBackupsType: MessageBackupsType,
messageBackupTier: MessageBackupTier,
availablePaymentGateways: List<GatewayResponse.Gateway>,
onPaymentGatewaySelected: (GatewayResponse.Gateway) -> Unit
) {
val resources = LocalContext.current.resources
val formattedPrice = remember(messageBackupsType.pricePerMonth) {
FiatMoneyUtil.format(resources, messageBackupsType.pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
val backupTypeDetails = remember(messageBackupTier) {
getTierDetails(messageBackupTier)
}
val formattedPrice = remember(backupTypeDetails.pricePerMonth) {
FiatMoneyUtil.format(resources, backupTypeDetails.pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
}
Text(
@@ -88,7 +88,7 @@ private fun SheetContent(
)
MessageBackupsTypeBlock(
messageBackupsType = messageBackupsType,
messageBackupsType = backupTypeDetails,
isSelected = false,
onSelected = {},
enabled = false,
@@ -221,29 +221,6 @@ private fun CreditOrDebitCardButton(
@Preview
@Composable
private fun MessageBackupsCheckoutSheetPreview() {
val paidTier = MessageBackupsType(
pricePerMonth = FiatMoney(BigDecimal.valueOf(3), Currency.getInstance("USD")),
title = "Text + All your media",
features = persistentListOf(
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
label = "Full text message backup"
),
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_album_compact_bold_16,
label = "Full media backup"
),
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
label = "1TB of storage (~250K photos)"
),
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_heart_compact_bold_16,
label = "Thanks for supporting Signal!"
)
)
)
val availablePaymentGateways = GatewayResponse.Gateway.values().toList()
Previews.Preview {
@@ -252,7 +229,7 @@ private fun MessageBackupsCheckoutSheetPreview() {
modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
) {
SheetContent(
messageBackupsType = paidTier,
messageBackupTier = MessageBackupTier.PAID,
availablePaymentGateways = availablePaymentGateways,
onPaymentGatewaySelected = {}
)

View File

@@ -32,6 +32,10 @@ class MessageBackupsFlowActivity : PassphraseRequiredActivity() {
fun MessageBackupsScreen.next() {
val nextScreen = viewModel.goToNextScreen(this)
if (nextScreen == MessageBackupsScreen.COMPLETED) {
finishAfterTransition()
return
}
if (nextScreen != this) {
navController.navigate(nextScreen.name)
}
@@ -88,9 +92,9 @@ class MessageBackupsFlowActivity : PassphraseRequiredActivity() {
composable(route = MessageBackupsScreen.TYPE_SELECTION.name) {
MessageBackupsTypeSelectionScreen(
selectedBackupsType = state.selectedMessageBackupsType,
availableBackupsTypes = state.availableBackupsTypes,
onMessageBackupsTypeSelected = viewModel::onMessageBackupsTypeUpdated,
selectedBackupTier = state.selectedMessageBackupTier,
availableBackupTiers = state.availableBackupTiers,
onMessageBackupsTierSelected = viewModel::onMessageBackupTierUpdated,
onNavigationClick = navController::popOrFinish,
onReadMoreClicked = {},
onNextClicked = { MessageBackupsScreen.TYPE_SELECTION.next() }
@@ -99,7 +103,7 @@ class MessageBackupsFlowActivity : PassphraseRequiredActivity() {
dialog(route = MessageBackupsScreen.CHECKOUT_SHEET.name) {
MessageBackupsCheckoutSheet(
messageBackupsType = state.selectedMessageBackupsType!!,
messageBackupTier = state.selectedMessageBackupTier!!,
availablePaymentGateways = state.availablePaymentGateways,
onDismissRequest = navController::popOrFinish,
onPaymentGatewaySelected = {

View File

@@ -5,13 +5,14 @@
package org.thoughtcrime.securesms.backup.v2.ui.subscription
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayResponse
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
data class MessageBackupsFlowState(
val selectedMessageBackupsType: MessageBackupsType? = null,
val availableBackupsTypes: List<MessageBackupsType> = emptyList(),
val selectedMessageBackupTier: MessageBackupTier? = null,
val availableBackupTiers: List<MessageBackupTier> = emptyList(),
val selectedPaymentGateway: GatewayResponse.Gateway? = null,
val availablePaymentGateways: List<GatewayResponse.Gateway> = emptyList(),
val pin: String = "",

View File

@@ -5,14 +5,28 @@
package org.thoughtcrime.securesms.backup.v2.ui.subscription
import android.text.TextUtils
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayResponse
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
import org.thoughtcrime.securesms.lock.v2.SvrConstants
import org.thoughtcrime.securesms.util.FeatureFlags
import org.whispersystems.signalservice.api.kbs.PinHashUtil.verifyLocalPinHash
class MessageBackupsFlowViewModel : ViewModel() {
private val internalState = mutableStateOf(MessageBackupsFlowState())
private val internalState = mutableStateOf(
MessageBackupsFlowState(
availableBackupTiers = if (!FeatureFlags.messageBackups()) {
emptyList()
} else {
listOf(MessageBackupTier.FREE, MessageBackupTier.PAID)
}
)
)
val state: State<MessageBackupsFlowState> = internalState
@@ -40,16 +54,27 @@ class MessageBackupsFlowViewModel : ViewModel() {
internalState.value = state.value.copy(selectedPaymentGateway = gateway)
}
fun onMessageBackupsTypeUpdated(messageBackupsType: MessageBackupsType) {
internalState.value = state.value.copy(selectedMessageBackupsType = messageBackupsType)
fun onMessageBackupTierUpdated(messageBackupTier: MessageBackupTier) {
internalState.value = state.value.copy(selectedMessageBackupTier = messageBackupTier)
}
private fun validatePinAndUpdateState(): MessageBackupsScreen {
val pinHash = SignalStore.svr().localPinHash
val pin = state.value.pin
if (pinHash == null || TextUtils.isEmpty(pin) || pin.length < SvrConstants.MINIMUM_PIN_LENGTH) return MessageBackupsScreen.PIN_CONFIRMATION
if (!verifyLocalPinHash(pinHash, pin)) {
return MessageBackupsScreen.PIN_CONFIRMATION
}
return MessageBackupsScreen.TYPE_SELECTION
}
private fun validateTypeAndUpdateState(): MessageBackupsScreen {
return MessageBackupsScreen.CHECKOUT_SHEET
SignalStore.backup().canReadWriteToArchiveCdn = state.value.selectedMessageBackupTier == MessageBackupTier.PAID
SignalStore.backup().areBackupsEnabled = true
return MessageBackupsScreen.COMPLETED
// return MessageBackupsScreen.CHECKOUT_SHEET TODO [message-backups] Switch back to payment flow
}
private fun validateGatewayAndUpdateState(): MessageBackupsScreen {

View File

@@ -16,6 +16,7 @@ import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
@@ -32,6 +33,7 @@ import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@@ -52,93 +54,95 @@ fun MessageBackupsPinConfirmationScreen(
onNextClick: () -> Unit
) {
val focusRequester = remember { FocusRequester() }
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
) {
LazyColumn(
Surface {
Column(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.fillMaxSize()
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
) {
item {
Text(
text = "Enter your PIN", // TODO [message-backups] Finalized copy
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(top = 40.dp)
)
}
item {
Text(
text = "Enter your Signal PIN to enable backups", // TODO [message-backups] Finalized copy
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 16.dp)
)
}
item {
// TODO [message-backups] Confirm default focus state
val keyboardType = remember(pinKeyboardType) {
when (pinKeyboardType) {
PinKeyboardType.NUMERIC -> KeyboardType.NumberPassword
PinKeyboardType.ALPHA_NUMERIC -> KeyboardType.Password
}
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
item {
Text(
text = "Enter your PIN", // TODO [message-backups] Finalized copy
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(top = 40.dp)
)
}
TextField(
value = pin,
onValueChange = onPinChanged,
textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center),
keyboardActions = KeyboardActions(
onDone = { onNextClick() }
),
keyboardOptions = KeyboardOptions(
keyboardType = keyboardType
),
modifier = Modifier
.padding(top = 72.dp)
.fillMaxWidth()
.focusRequester(focusRequester)
)
item {
Text(
text = "Enter your Signal PIN to enable backups", // TODO [message-backups] Finalized copy
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 16.dp)
)
}
item {
// TODO [message-backups] Confirm default focus state
val keyboardType = remember(pinKeyboardType) {
when (pinKeyboardType) {
PinKeyboardType.NUMERIC -> KeyboardType.NumberPassword
PinKeyboardType.ALPHA_NUMERIC -> KeyboardType.Password
}
}
TextField(
value = pin,
onValueChange = onPinChanged,
textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center),
keyboardActions = KeyboardActions(
onDone = { onNextClick() }
),
keyboardOptions = KeyboardOptions(
keyboardType = keyboardType
),
modifier = Modifier
.padding(top = 72.dp)
.fillMaxWidth()
.focusRequester(focusRequester),
visualTransformation = PasswordVisualTransformation()
)
}
item {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxWidth()
.padding(top = 48.dp)
) {
PinKeyboardTypeToggle(
pinKeyboardType = pinKeyboardType,
onPinKeyboardTypeSelected = onPinKeyboardTypeSelected
)
}
}
}
item {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxWidth()
.padding(top = 48.dp)
Box(
contentAlignment = Alignment.BottomEnd,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp)
) {
Buttons.LargeTonal(
onClick = onNextClick
) {
PinKeyboardTypeToggle(
pinKeyboardType = pinKeyboardType,
onPinKeyboardTypeSelected = onPinKeyboardTypeSelected
Text(
text = "Next" // TODO [message-backups] Finalized copy
)
}
}
}
Box(
contentAlignment = Alignment.BottomEnd,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp)
) {
Buttons.LargeTonal(
onClick = onNextClick
) {
Text(
text = "Next" // TODO [message-backups] Finalized copy
)
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
}
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
}
}

View File

@@ -96,22 +96,22 @@ fun MessageBackupsPinEducationScreen(
}
Buttons.LargePrimary(
onClick = onGeneratePinClick,
onClick = onUseCurrentPinClick,
modifier = Modifier.fillMaxWidth()
) {
Text(
text = "Generate a new $recommendedPinSize-digit PIN" // TODO [message-backups] Finalized copy
text = "Use current Signal PIN" // TODO [message-backups] Finalized copy
)
}
TextButton(
onClick = onUseCurrentPinClick,
onClick = onGeneratePinClick,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp)
) {
Text(
text = "Use current Signal PIN" // TODO [message-backups] Finalized copy
text = "Generate a new $recommendedPinSize-digit PIN" // TODO [message-backups] Finalized copy
)
}
}

View File

@@ -39,16 +39,17 @@ import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withAnnotation
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import org.signal.core.ui.Buttons
import org.signal.core.ui.Previews
import org.signal.core.ui.Scaffolds
import org.signal.core.ui.SignalPreview
import org.signal.core.ui.theme.SignalTheme
import org.signal.core.util.money.FiatMoney
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
import java.math.BigDecimal
import java.util.Currency
@@ -59,9 +60,9 @@ import java.util.Currency
@OptIn(ExperimentalTextApi::class)
@Composable
fun MessageBackupsTypeSelectionScreen(
selectedBackupsType: MessageBackupsType?,
availableBackupsTypes: List<MessageBackupsType>,
onMessageBackupsTypeSelected: (MessageBackupsType) -> Unit,
selectedBackupTier: MessageBackupTier?,
availableBackupTiers: List<MessageBackupTier>,
onMessageBackupsTierSelected: (MessageBackupTier) -> Unit,
onNavigationClick: () -> Unit,
onReadMoreClicked: () -> Unit,
onNextClicked: () -> Unit
@@ -128,13 +129,16 @@ fun MessageBackupsTypeSelectionScreen(
}
itemsIndexed(
availableBackupsTypes,
{ _, item -> item.title }
availableBackupTiers,
{ _, item -> item }
) { index, item ->
val type = remember(item) {
getTierDetails(item)
}
MessageBackupsTypeBlock(
messageBackupsType = item,
isSelected = item == selectedBackupsType,
onSelected = { onMessageBackupsTypeSelected(item) },
messageBackupsType = type,
isSelected = item == selectedBackupTier,
onSelected = { onMessageBackupsTierSelected(item) },
modifier = Modifier.padding(top = if (index == 0) 20.dp else 18.dp)
)
}
@@ -154,54 +158,16 @@ fun MessageBackupsTypeSelectionScreen(
}
}
@Preview
@SignalPreview
@Composable
private fun MessageBackupsTypeSelectionScreenPreview() {
val freeTier = MessageBackupsType(
pricePerMonth = FiatMoney(BigDecimal.ZERO, Currency.getInstance("USD")),
title = "Text + 30 days of media",
features = persistentListOf(
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
label = "Full text message backup"
),
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_album_compact_bold_16,
label = "Last 30 days of media"
)
)
)
val paidTier = MessageBackupsType(
pricePerMonth = FiatMoney(BigDecimal.valueOf(3), Currency.getInstance("USD")),
title = "Text + All your media",
features = persistentListOf(
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
label = "Full text message backup"
),
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_album_compact_bold_16,
label = "Full media backup"
),
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
label = "1TB of storage (~250K photos)"
),
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_heart_compact_bold_16,
label = "Thanks for supporting Signal!"
)
)
)
var selectedBackupsType by remember { mutableStateOf(freeTier) }
var selectedBackupsType by remember { mutableStateOf(MessageBackupTier.FREE) }
Previews.Preview {
MessageBackupsTypeSelectionScreen(
selectedBackupsType = selectedBackupsType,
availableBackupsTypes = listOf(freeTier, paidTier),
onMessageBackupsTypeSelected = { selectedBackupsType = it },
selectedBackupTier = MessageBackupTier.FREE,
availableBackupTiers = listOf(MessageBackupTier.FREE, MessageBackupTier.PAID),
onMessageBackupsTierSelected = { selectedBackupsType = it },
onNavigationClick = {},
onReadMoreClicked = {},
onNextClicked = {}
@@ -272,7 +238,51 @@ private fun formatCostPerMonth(pricePerMonth: FiatMoney): String {
@Stable
data class MessageBackupsType(
val tier: MessageBackupTier,
val pricePerMonth: FiatMoney,
val title: String,
val features: ImmutableList<MessageBackupsTypeFeature>
)
fun getTierDetails(tier: MessageBackupTier): MessageBackupsType {
return when (tier) {
MessageBackupTier.FREE -> MessageBackupsType(
tier = MessageBackupTier.FREE,
pricePerMonth = FiatMoney(BigDecimal.ZERO, Currency.getInstance("USD")),
title = "Text + 30 days of media",
features = persistentListOf(
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
label = "Full text message backup"
),
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_album_compact_bold_16,
label = "Last 30 days of media"
)
)
)
MessageBackupTier.PAID -> MessageBackupsType(
tier = MessageBackupTier.PAID,
pricePerMonth = FiatMoney(BigDecimal.valueOf(3), Currency.getInstance("USD")),
title = "Text + All your media",
features = persistentListOf(
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
label = "Full text message backup"
),
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_album_compact_bold_16,
label = "Full media backup"
),
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
label = "1TB of storage (~250K photos)"
),
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_heart_compact_bold_16,
label = "Thanks for supporting Signal!"
)
)
)
}
}

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()

View File

@@ -5,6 +5,7 @@
package org.thoughtcrime.securesms.jobs
import org.greenrobot.eventbus.EventBus
import org.signal.core.util.logging.Log
import org.signal.protos.resumableuploads.ResumableUpload
import org.thoughtcrime.securesms.attachments.Attachment
@@ -13,6 +14,7 @@ import org.thoughtcrime.securesms.attachments.AttachmentUploadUtil
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.attachments.PointerAttachment
import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.backup.v2.BackupV2Event
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
@@ -34,7 +36,9 @@ import kotlin.time.Duration.Companion.days
class ArchiveAttachmentBackfillJob private constructor(
parameters: Parameters,
private var attachmentId: AttachmentId?,
private var uploadSpec: ResumableUpload?
private var uploadSpec: ResumableUpload?,
private var totalCount: Int?,
private var progress: Int?
) : Job(parameters) {
companion object {
private val TAG = Log.tag(ArchiveAttachmentBackfillJob::class.java)
@@ -42,7 +46,7 @@ class ArchiveAttachmentBackfillJob private constructor(
const val KEY = "ArchiveAttachmentBackfillJob"
}
constructor() : this(
constructor(progress: Int? = null, totalCount: Int? = null) : this(
parameters = Parameters.Builder()
.setQueue("ArchiveAttachmentBackfillJob")
.setMaxInstancesForQueue(2)
@@ -51,7 +55,9 @@ class ArchiveAttachmentBackfillJob private constructor(
.addConstraint(NetworkConstraint.KEY)
.build(),
attachmentId = null,
uploadSpec = null
uploadSpec = null,
totalCount = totalCount,
progress = progress
)
override fun serialize(): ByteArray {
@@ -64,6 +70,7 @@ class ArchiveAttachmentBackfillJob private constructor(
override fun getFactoryKey(): String = KEY
override fun run(): Result {
EventBus.getDefault().postSticky(BackupV2Event(BackupV2Event.Type.PROGRESS_ATTACHMENTS, progress?.toLong() ?: 0, totalCount?.toLong() ?: 0))
var attachmentRecord: DatabaseAttachment? = if (attachmentId != null) {
Log.i(TAG, "Retrying $attachmentId")
SignalDatabase.attachments.getAttachment(attachmentId!!)
@@ -73,7 +80,7 @@ class ArchiveAttachmentBackfillJob private constructor(
if (attachmentRecord == null && attachmentId != null) {
Log.w(TAG, "Attachment $attachmentId was not found! Was likely deleted during the process of archiving. Re-enqueuing job with no ID.")
ApplicationDependencies.getJobManager().add(ArchiveAttachmentBackfillJob())
reenqueueWithIncrementedProgress()
return Result.success()
}
@@ -84,11 +91,16 @@ class ArchiveAttachmentBackfillJob private constructor(
val resetCount = SignalDatabase.attachments.resetPendingArchiveBackfills()
if (resetCount > 0) {
Log.w(TAG, "We thought we were done, but $resetCount items were still in progress! Need to run again to retry.")
ApplicationDependencies.getJobManager().add(ArchiveAttachmentBackfillJob())
ApplicationDependencies.getJobManager().add(
ArchiveAttachmentBackfillJob(
progress = (totalCount ?: resetCount) - resetCount,
totalCount = totalCount ?: resetCount
)
)
} else {
Log.i(TAG, "All good! Should be done.")
}
EventBus.getDefault().postSticky(BackupV2Event(type = BackupV2Event.Type.FINISHED, count = totalCount?.toLong() ?: 0, estimatedTotalCount = totalCount?.toLong() ?: 0))
return Result.success()
}
@@ -97,7 +109,7 @@ class ArchiveAttachmentBackfillJob private constructor(
val transferState: AttachmentTable.ArchiveTransferState? = SignalDatabase.attachments.getArchiveTransferState(attachmentRecord.attachmentId)
if (transferState == null) {
Log.w(TAG, "Attachment $attachmentId was not found when looking for the transfer state! Was likely just deleted. Re-enqueuing job with no ID.")
ApplicationDependencies.getJobManager().add(ArchiveAttachmentBackfillJob())
reenqueueWithIncrementedProgress()
return Result.success()
}
@@ -105,19 +117,19 @@ class ArchiveAttachmentBackfillJob private constructor(
if (transferState == AttachmentTable.ArchiveTransferState.FINISHED) {
Log.i(TAG, "Attachment $attachmentId is already finished. Skipping.")
ApplicationDependencies.getJobManager().add(ArchiveAttachmentBackfillJob())
reenqueueWithIncrementedProgress()
return Result.success()
}
if (transferState == AttachmentTable.ArchiveTransferState.PERMANENT_FAILURE) {
Log.i(TAG, "Attachment $attachmentId is already marked as a permanent failure. Skipping.")
ApplicationDependencies.getJobManager().add(ArchiveAttachmentBackfillJob())
reenqueueWithIncrementedProgress()
return Result.success()
}
if (transferState == AttachmentTable.ArchiveTransferState.ATTACHMENT_TRANSFER_PENDING) {
Log.i(TAG, "Attachment $attachmentId is already marked as pending transfer, meaning it's a send attachment that will be uploaded on it's own. Skipping.")
ApplicationDependencies.getJobManager().add(ArchiveAttachmentBackfillJob())
reenqueueWithIncrementedProgress()
return Result.success()
}
@@ -164,7 +176,7 @@ class ArchiveAttachmentBackfillJob private constructor(
if (attachmentRecord == null) {
Log.w(TAG, "$attachmentId was not found after uploading! Possibly deleted in a narrow race condition. Re-enqueuing job with no ID.")
ApplicationDependencies.getJobManager().add(ArchiveAttachmentBackfillJob())
reenqueueWithIncrementedProgress()
return Result.success()
}
@@ -174,7 +186,7 @@ class ArchiveAttachmentBackfillJob private constructor(
Log.d(TAG, "Move complete!")
SignalDatabase.attachments.setArchiveTransferState(attachmentRecord.attachmentId, AttachmentTable.ArchiveTransferState.FINISHED)
ApplicationDependencies.getJobManager().add(ArchiveAttachmentBackfillJob())
reenqueueWithIncrementedProgress()
Result.success()
}
@@ -212,6 +224,15 @@ class ArchiveAttachmentBackfillJob private constructor(
}
}
private fun reenqueueWithIncrementedProgress() {
ApplicationDependencies.getJobManager().add(
ArchiveAttachmentBackfillJob(
totalCount = totalCount,
progress = progress?.inc()?.coerceAtMost(totalCount ?: 0)
)
)
}
override fun onFailure() {
attachmentId?.let { id ->
Log.w(TAG, "Failed to archive $id!")
@@ -261,7 +282,9 @@ class ArchiveAttachmentBackfillJob private constructor(
return ArchiveAttachmentBackfillJob(
parameters = parameters,
attachmentId = data?.attachmentId?.let { AttachmentId(it) },
uploadSpec = data?.uploadSpec
uploadSpec = data?.uploadSpec,
totalCount = data?.totalCount,
progress = data?.count
)
}
}

View File

@@ -6,10 +6,11 @@
package org.thoughtcrime.securesms.jobs
import android.database.Cursor
import org.greenrobot.eventbus.EventBus
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.BuildConfig
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.backup.v2.BackupV2Event
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobmanager.Job
@@ -54,33 +55,41 @@ class BackupMessagesJob private constructor(parameters: Parameters) : BaseJob(pa
override fun onFailure() = Unit
private fun archiveAttachments() {
if (BuildConfig.MESSAGE_BACKUP_RESTORE_ENABLED) {
SignalStore.backup().canReadWriteToArchiveCdn = true
}
private fun archiveAttachments(): Boolean {
if (!SignalStore.backup().canReadWriteToArchiveCdn) return false
val batchSize = 100
var needToBackfill = 0
var totalCount: Int
var progress = 0
SignalDatabase.attachments.getArchivableAttachments().use { cursor ->
totalCount = cursor.count
while (!cursor.isAfterLast) {
val attachments = cursor.readAttachmentBatch(batchSize)
when (val archiveResult = BackupRepository.archiveMedia(attachments)) {
is NetworkResult.Success -> {
Log.i(TAG, "Archive call successful")
for (success in archiveResult.result.sourceNotFoundResponses) {
val attachmentId = archiveResult.result.mediaIdToAttachmentId(success.mediaId)
ApplicationDependencies
.getJobManager()
.startChain(AttachmentUploadJob(attachmentId))
.then(ArchiveAttachmentJob(attachmentId))
.enqueue()
Log.i(TAG, "Attachment $attachmentId not found on cdn, will need to re-upload")
needToBackfill++
}
progress += attachments.size
}
else -> {
Log.e(TAG, "Failed to archive $archiveResult")
}
}
EventBus.getDefault().postSticky(BackupV2Event(BackupV2Event.Type.PROGRESS_ATTACHMENTS, (progress - needToBackfill).toLong(), totalCount.toLong()))
}
}
if (needToBackfill > 0) {
ApplicationDependencies.getJobManager().add(ArchiveAttachmentBackfillJob(totalCount = totalCount, progress = progress - needToBackfill))
return true
}
return false
}
private fun Cursor.readAttachmentBatch(batchSize: Int): List<DatabaseAttachment> {
@@ -96,6 +105,7 @@ class BackupMessagesJob private constructor(parameters: Parameters) : BaseJob(pa
}
override fun onRun() {
EventBus.getDefault().postSticky(BackupV2Event(type = BackupV2Event.Type.PROGRESS_MESSAGES, count = 0, estimatedTotalCount = 0))
val tempBackupFile = BlobProvider.getInstance().forNonAutoEncryptingSingleSessionOnDisk(ApplicationDependencies.getApplication())
val outputStream = FileOutputStream(tempBackupFile)
@@ -104,11 +114,14 @@ class BackupMessagesJob private constructor(parameters: Parameters) : BaseJob(pa
FileInputStream(tempBackupFile).use {
BackupRepository.uploadBackupFile(it, tempBackupFile.length())
}
archiveAttachments()
val needBackfill = archiveAttachments()
if (!tempBackupFile.delete()) {
Log.e(TAG, "Failed to delete temp backup file")
}
SignalStore.backup().lastBackupTime = System.currentTimeMillis()
if (!needBackfill) {
EventBus.getDefault().postSticky(BackupV2Event(BackupV2Event.Type.FINISHED, 0, 0))
}
}
override fun onShouldRetry(e: Exception): Boolean = false

View File

@@ -21,6 +21,7 @@ internal class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
private const val KEY_RESTORE_STATE = "backup.restoreState"
private const val KEY_NEXT_BACKUP_TIME = "backup.nextBackupTime"
private const val KEY_LAST_BACKUP_TIME = "backup.lastBackupTime"
private const val KEY_CDN_BACKUP_DIRECTORY = "backup.cdn.directory"
private const val KEY_CDN_BACKUP_MEDIA_DIRECTORY = "backup.cdn.mediaDirectory"
@@ -49,6 +50,7 @@ internal class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
var optimizeStorage: Boolean by booleanValue(KEY_OPTIMIZE_STORAGE, false)
var nextBackupTime: Long by longValue(KEY_NEXT_BACKUP_TIME, -1)
var lastBackupTime: Long by longValue(KEY_LAST_BACKUP_TIME, -1)
var areBackupsEnabled: Boolean
get() {

View File

@@ -55,4 +55,6 @@ message ArchiveAttachmentJobData {
message ArchiveAttachmentBackfillJobData {
optional uint64 attachmentId = 1;
ResumableUpload uploadSpec = 2;
optional uint32 count = 3;
optional uint32 totalCount = 4;
}