mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-23 04:28:35 +00:00
Basic settings functionality for message backup.
This commit is contained in:
@@ -650,3 +650,8 @@ class BackupMetadata(
|
||||
val usedSpace: Long,
|
||||
val mediaCount: Long
|
||||
)
|
||||
|
||||
enum class MessageBackupTier {
|
||||
FREE,
|
||||
PAID
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 = {}
|
||||
)
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 = "",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,7 +54,7 @@ fun MessageBackupsPinConfirmationScreen(
|
||||
onNextClick: () -> Unit
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
Surface {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@@ -102,7 +104,8 @@ fun MessageBackupsPinConfirmationScreen(
|
||||
modifier = Modifier
|
||||
.padding(top = 72.dp)
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester)
|
||||
.focusRequester(focusRequester),
|
||||
visualTransformation = PasswordVisualTransformation()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -140,6 +143,7 @@ fun MessageBackupsPinConfirmationScreen(
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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!"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,12 +223,18 @@ private fun RemoteBackupsSettingsContent(
|
||||
Texts.SectionHeader(text = "Backup Details")
|
||||
}
|
||||
|
||||
if (backupProgress == null || backupProgress.type == BackupV2Event.Type.FINISHED) {
|
||||
item {
|
||||
LastBackupRow(
|
||||
lastBackupTimestamp = lastBackupTimestamp,
|
||||
onBackupNowClick = {}
|
||||
onBackupNowClick = contentCallbacks::onBackupNowClick
|
||||
)
|
||||
}
|
||||
} else {
|
||||
item {
|
||||
InProgressBackupRow(progress = backupProgress.count.toInt(), totalProgress = backupProgress.estimatedTotalCount.toInt())
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.TextRow(text = {
|
||||
@@ -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,6 +502,7 @@ private fun BackupFrequencyDialog(
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss
|
||||
) {
|
||||
Surface {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(
|
||||
@@ -492,6 +547,7 @@ private fun BackupFrequencyDialog(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -55,4 +55,6 @@ message ArchiveAttachmentJobData {
|
||||
message ArchiveAttachmentBackfillJobData {
|
||||
optional uint64 attachmentId = 1;
|
||||
ResumableUpload uploadSpec = 2;
|
||||
optional uint32 count = 3;
|
||||
optional uint32 totalCount = 4;
|
||||
}
|
||||
Reference in New Issue
Block a user