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 usedSpace: Long,
|
||||||
val mediaCount: 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.unit.dp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
|
||||||
import org.signal.core.ui.BottomSheets
|
import org.signal.core.ui.BottomSheets
|
||||||
import org.signal.core.ui.Buttons
|
import org.signal.core.ui.Buttons
|
||||||
import org.signal.core.ui.Previews
|
import org.signal.core.ui.Previews
|
||||||
import org.signal.core.util.money.FiatMoney
|
|
||||||
import org.thoughtcrime.securesms.R
|
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.donate.gateway.GatewayResponse
|
||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.GooglePayButton
|
import org.thoughtcrime.securesms.components.settings.app.subscription.models.GooglePayButton
|
||||||
import org.thoughtcrime.securesms.databinding.PaypalButtonBinding
|
import org.thoughtcrime.securesms.databinding.PaypalButtonBinding
|
||||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||||
import java.math.BigDecimal
|
|
||||||
import java.util.Currency
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MessageBackupsCheckoutSheet(
|
fun MessageBackupsCheckoutSheet(
|
||||||
messageBackupsType: MessageBackupsType,
|
messageBackupTier: MessageBackupTier,
|
||||||
availablePaymentGateways: List<GatewayResponse.Gateway>,
|
availablePaymentGateways: List<GatewayResponse.Gateway>,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onPaymentGatewaySelected: (GatewayResponse.Gateway) -> Unit
|
onPaymentGatewaySelected: (GatewayResponse.Gateway) -> Unit
|
||||||
@@ -57,7 +54,7 @@ fun MessageBackupsCheckoutSheet(
|
|||||||
modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
|
modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
|
||||||
) {
|
) {
|
||||||
SheetContent(
|
SheetContent(
|
||||||
messageBackupsType = messageBackupsType,
|
messageBackupTier = messageBackupTier,
|
||||||
availablePaymentGateways = availablePaymentGateways,
|
availablePaymentGateways = availablePaymentGateways,
|
||||||
onPaymentGatewaySelected = onPaymentGatewaySelected
|
onPaymentGatewaySelected = onPaymentGatewaySelected
|
||||||
)
|
)
|
||||||
@@ -66,13 +63,16 @@ fun MessageBackupsCheckoutSheet(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SheetContent(
|
private fun SheetContent(
|
||||||
messageBackupsType: MessageBackupsType,
|
messageBackupTier: MessageBackupTier,
|
||||||
availablePaymentGateways: List<GatewayResponse.Gateway>,
|
availablePaymentGateways: List<GatewayResponse.Gateway>,
|
||||||
onPaymentGatewaySelected: (GatewayResponse.Gateway) -> Unit
|
onPaymentGatewaySelected: (GatewayResponse.Gateway) -> Unit
|
||||||
) {
|
) {
|
||||||
val resources = LocalContext.current.resources
|
val resources = LocalContext.current.resources
|
||||||
val formattedPrice = remember(messageBackupsType.pricePerMonth) {
|
val backupTypeDetails = remember(messageBackupTier) {
|
||||||
FiatMoneyUtil.format(resources, messageBackupsType.pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
getTierDetails(messageBackupTier)
|
||||||
|
}
|
||||||
|
val formattedPrice = remember(backupTypeDetails.pricePerMonth) {
|
||||||
|
FiatMoneyUtil.format(resources, backupTypeDetails.pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
@@ -88,7 +88,7 @@ private fun SheetContent(
|
|||||||
)
|
)
|
||||||
|
|
||||||
MessageBackupsTypeBlock(
|
MessageBackupsTypeBlock(
|
||||||
messageBackupsType = messageBackupsType,
|
messageBackupsType = backupTypeDetails,
|
||||||
isSelected = false,
|
isSelected = false,
|
||||||
onSelected = {},
|
onSelected = {},
|
||||||
enabled = false,
|
enabled = false,
|
||||||
@@ -221,29 +221,6 @@ private fun CreditOrDebitCardButton(
|
|||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
private fun MessageBackupsCheckoutSheetPreview() {
|
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()
|
val availablePaymentGateways = GatewayResponse.Gateway.values().toList()
|
||||||
|
|
||||||
Previews.Preview {
|
Previews.Preview {
|
||||||
@@ -252,7 +229,7 @@ private fun MessageBackupsCheckoutSheetPreview() {
|
|||||||
modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
|
modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
|
||||||
) {
|
) {
|
||||||
SheetContent(
|
SheetContent(
|
||||||
messageBackupsType = paidTier,
|
messageBackupTier = MessageBackupTier.PAID,
|
||||||
availablePaymentGateways = availablePaymentGateways,
|
availablePaymentGateways = availablePaymentGateways,
|
||||||
onPaymentGatewaySelected = {}
|
onPaymentGatewaySelected = {}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ class MessageBackupsFlowActivity : PassphraseRequiredActivity() {
|
|||||||
|
|
||||||
fun MessageBackupsScreen.next() {
|
fun MessageBackupsScreen.next() {
|
||||||
val nextScreen = viewModel.goToNextScreen(this)
|
val nextScreen = viewModel.goToNextScreen(this)
|
||||||
|
if (nextScreen == MessageBackupsScreen.COMPLETED) {
|
||||||
|
finishAfterTransition()
|
||||||
|
return
|
||||||
|
}
|
||||||
if (nextScreen != this) {
|
if (nextScreen != this) {
|
||||||
navController.navigate(nextScreen.name)
|
navController.navigate(nextScreen.name)
|
||||||
}
|
}
|
||||||
@@ -88,9 +92,9 @@ class MessageBackupsFlowActivity : PassphraseRequiredActivity() {
|
|||||||
|
|
||||||
composable(route = MessageBackupsScreen.TYPE_SELECTION.name) {
|
composable(route = MessageBackupsScreen.TYPE_SELECTION.name) {
|
||||||
MessageBackupsTypeSelectionScreen(
|
MessageBackupsTypeSelectionScreen(
|
||||||
selectedBackupsType = state.selectedMessageBackupsType,
|
selectedBackupTier = state.selectedMessageBackupTier,
|
||||||
availableBackupsTypes = state.availableBackupsTypes,
|
availableBackupTiers = state.availableBackupTiers,
|
||||||
onMessageBackupsTypeSelected = viewModel::onMessageBackupsTypeUpdated,
|
onMessageBackupsTierSelected = viewModel::onMessageBackupTierUpdated,
|
||||||
onNavigationClick = navController::popOrFinish,
|
onNavigationClick = navController::popOrFinish,
|
||||||
onReadMoreClicked = {},
|
onReadMoreClicked = {},
|
||||||
onNextClicked = { MessageBackupsScreen.TYPE_SELECTION.next() }
|
onNextClicked = { MessageBackupsScreen.TYPE_SELECTION.next() }
|
||||||
@@ -99,7 +103,7 @@ class MessageBackupsFlowActivity : PassphraseRequiredActivity() {
|
|||||||
|
|
||||||
dialog(route = MessageBackupsScreen.CHECKOUT_SHEET.name) {
|
dialog(route = MessageBackupsScreen.CHECKOUT_SHEET.name) {
|
||||||
MessageBackupsCheckoutSheet(
|
MessageBackupsCheckoutSheet(
|
||||||
messageBackupsType = state.selectedMessageBackupsType!!,
|
messageBackupTier = state.selectedMessageBackupTier!!,
|
||||||
availablePaymentGateways = state.availablePaymentGateways,
|
availablePaymentGateways = state.availablePaymentGateways,
|
||||||
onDismissRequest = navController::popOrFinish,
|
onDismissRequest = navController::popOrFinish,
|
||||||
onPaymentGatewaySelected = {
|
onPaymentGatewaySelected = {
|
||||||
|
|||||||
@@ -5,13 +5,14 @@
|
|||||||
|
|
||||||
package org.thoughtcrime.securesms.backup.v2.ui.subscription
|
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.components.settings.app.subscription.donate.gateway.GatewayResponse
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
|
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
|
||||||
|
|
||||||
data class MessageBackupsFlowState(
|
data class MessageBackupsFlowState(
|
||||||
val selectedMessageBackupsType: MessageBackupsType? = null,
|
val selectedMessageBackupTier: MessageBackupTier? = null,
|
||||||
val availableBackupsTypes: List<MessageBackupsType> = emptyList(),
|
val availableBackupTiers: List<MessageBackupTier> = emptyList(),
|
||||||
val selectedPaymentGateway: GatewayResponse.Gateway? = null,
|
val selectedPaymentGateway: GatewayResponse.Gateway? = null,
|
||||||
val availablePaymentGateways: List<GatewayResponse.Gateway> = emptyList(),
|
val availablePaymentGateways: List<GatewayResponse.Gateway> = emptyList(),
|
||||||
val pin: String = "",
|
val pin: String = "",
|
||||||
|
|||||||
@@ -5,14 +5,28 @@
|
|||||||
|
|
||||||
package org.thoughtcrime.securesms.backup.v2.ui.subscription
|
package org.thoughtcrime.securesms.backup.v2.ui.subscription
|
||||||
|
|
||||||
|
import android.text.TextUtils
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.lifecycle.ViewModel
|
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.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.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() {
|
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
|
val state: State<MessageBackupsFlowState> = internalState
|
||||||
|
|
||||||
@@ -40,16 +54,27 @@ class MessageBackupsFlowViewModel : ViewModel() {
|
|||||||
internalState.value = state.value.copy(selectedPaymentGateway = gateway)
|
internalState.value = state.value.copy(selectedPaymentGateway = gateway)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onMessageBackupsTypeUpdated(messageBackupsType: MessageBackupsType) {
|
fun onMessageBackupTierUpdated(messageBackupTier: MessageBackupTier) {
|
||||||
internalState.value = state.value.copy(selectedMessageBackupsType = messageBackupsType)
|
internalState.value = state.value.copy(selectedMessageBackupTier = messageBackupTier)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun validatePinAndUpdateState(): MessageBackupsScreen {
|
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
|
return MessageBackupsScreen.TYPE_SELECTION
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun validateTypeAndUpdateState(): MessageBackupsScreen {
|
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 {
|
private fun validateGatewayAndUpdateState(): MessageBackupsScreen {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import androidx.compose.foundation.text.KeyboardOptions
|
|||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.LocalTextStyle
|
import androidx.compose.material3.LocalTextStyle
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TextField
|
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.dimensionResource
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
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.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -52,93 +54,95 @@ fun MessageBackupsPinConfirmationScreen(
|
|||||||
onNextClick: () -> Unit
|
onNextClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
Surface {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
|
|
||||||
) {
|
|
||||||
LazyColumn(
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxSize()
|
||||||
.weight(1f)
|
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
|
||||||
) {
|
) {
|
||||||
item {
|
LazyColumn(
|
||||||
Text(
|
modifier = Modifier
|
||||||
text = "Enter your PIN", // TODO [message-backups] Finalized copy
|
.fillMaxWidth()
|
||||||
style = MaterialTheme.typography.headlineMedium,
|
.weight(1f)
|
||||||
modifier = Modifier.padding(top = 40.dp)
|
) {
|
||||||
)
|
item {
|
||||||
}
|
Text(
|
||||||
|
text = "Enter your PIN", // TODO [message-backups] Finalized copy
|
||||||
item {
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
Text(
|
modifier = Modifier.padding(top = 40.dp)
|
||||||
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(
|
item {
|
||||||
value = pin,
|
Text(
|
||||||
onValueChange = onPinChanged,
|
text = "Enter your Signal PIN to enable backups", // TODO [message-backups] Finalized copy
|
||||||
textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center),
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
keyboardActions = KeyboardActions(
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
onDone = { onNextClick() }
|
modifier = Modifier.padding(top = 16.dp)
|
||||||
),
|
)
|
||||||
keyboardOptions = KeyboardOptions(
|
}
|
||||||
keyboardType = keyboardType
|
|
||||||
),
|
item {
|
||||||
modifier = Modifier
|
// TODO [message-backups] Confirm default focus state
|
||||||
.padding(top = 72.dp)
|
val keyboardType = remember(pinKeyboardType) {
|
||||||
.fillMaxWidth()
|
when (pinKeyboardType) {
|
||||||
.focusRequester(focusRequester)
|
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(
|
||||||
Box(
|
contentAlignment = Alignment.BottomEnd,
|
||||||
contentAlignment = Alignment.Center,
|
modifier = Modifier
|
||||||
modifier = Modifier
|
.fillMaxWidth()
|
||||||
.fillMaxWidth()
|
.padding(vertical = 16.dp)
|
||||||
.padding(top = 48.dp)
|
) {
|
||||||
|
Buttons.LargeTonal(
|
||||||
|
onClick = onNextClick
|
||||||
) {
|
) {
|
||||||
PinKeyboardTypeToggle(
|
Text(
|
||||||
pinKeyboardType = pinKeyboardType,
|
text = "Next" // TODO [message-backups] Finalized copy
|
||||||
onPinKeyboardTypeSelected = onPinKeyboardTypeSelected
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Box(
|
LaunchedEffect(Unit) {
|
||||||
contentAlignment = Alignment.BottomEnd,
|
focusRequester.requestFocus()
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(vertical = 16.dp)
|
|
||||||
) {
|
|
||||||
Buttons.LargeTonal(
|
|
||||||
onClick = onNextClick
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "Next" // TODO [message-backups] Finalized copy
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
focusRequester.requestFocus()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -96,22 +96,22 @@ fun MessageBackupsPinEducationScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Buttons.LargePrimary(
|
Buttons.LargePrimary(
|
||||||
onClick = onGeneratePinClick,
|
onClick = onUseCurrentPinClick,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Generate a new $recommendedPinSize-digit PIN" // TODO [message-backups] Finalized copy
|
text = "Use current Signal PIN" // TODO [message-backups] Finalized copy
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = onUseCurrentPinClick,
|
onClick = onGeneratePinClick,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(bottom = 16.dp)
|
.padding(bottom = 16.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
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.style.TextAlign
|
||||||
import androidx.compose.ui.text.withAnnotation
|
import androidx.compose.ui.text.withAnnotation
|
||||||
import androidx.compose.ui.text.withStyle
|
import androidx.compose.ui.text.withStyle
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import org.signal.core.ui.Buttons
|
import org.signal.core.ui.Buttons
|
||||||
import org.signal.core.ui.Previews
|
import org.signal.core.ui.Previews
|
||||||
import org.signal.core.ui.Scaffolds
|
import org.signal.core.ui.Scaffolds
|
||||||
|
import org.signal.core.ui.SignalPreview
|
||||||
import org.signal.core.ui.theme.SignalTheme
|
import org.signal.core.ui.theme.SignalTheme
|
||||||
import org.signal.core.util.money.FiatMoney
|
import org.signal.core.util.money.FiatMoney
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.util.Currency
|
import java.util.Currency
|
||||||
@@ -59,9 +60,9 @@ import java.util.Currency
|
|||||||
@OptIn(ExperimentalTextApi::class)
|
@OptIn(ExperimentalTextApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MessageBackupsTypeSelectionScreen(
|
fun MessageBackupsTypeSelectionScreen(
|
||||||
selectedBackupsType: MessageBackupsType?,
|
selectedBackupTier: MessageBackupTier?,
|
||||||
availableBackupsTypes: List<MessageBackupsType>,
|
availableBackupTiers: List<MessageBackupTier>,
|
||||||
onMessageBackupsTypeSelected: (MessageBackupsType) -> Unit,
|
onMessageBackupsTierSelected: (MessageBackupTier) -> Unit,
|
||||||
onNavigationClick: () -> Unit,
|
onNavigationClick: () -> Unit,
|
||||||
onReadMoreClicked: () -> Unit,
|
onReadMoreClicked: () -> Unit,
|
||||||
onNextClicked: () -> Unit
|
onNextClicked: () -> Unit
|
||||||
@@ -128,13 +129,16 @@ fun MessageBackupsTypeSelectionScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
itemsIndexed(
|
itemsIndexed(
|
||||||
availableBackupsTypes,
|
availableBackupTiers,
|
||||||
{ _, item -> item.title }
|
{ _, item -> item }
|
||||||
) { index, item ->
|
) { index, item ->
|
||||||
|
val type = remember(item) {
|
||||||
|
getTierDetails(item)
|
||||||
|
}
|
||||||
MessageBackupsTypeBlock(
|
MessageBackupsTypeBlock(
|
||||||
messageBackupsType = item,
|
messageBackupsType = type,
|
||||||
isSelected = item == selectedBackupsType,
|
isSelected = item == selectedBackupTier,
|
||||||
onSelected = { onMessageBackupsTypeSelected(item) },
|
onSelected = { onMessageBackupsTierSelected(item) },
|
||||||
modifier = Modifier.padding(top = if (index == 0) 20.dp else 18.dp)
|
modifier = Modifier.padding(top = if (index == 0) 20.dp else 18.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -154,54 +158,16 @@ fun MessageBackupsTypeSelectionScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
@SignalPreview
|
||||||
@Composable
|
@Composable
|
||||||
private fun MessageBackupsTypeSelectionScreenPreview() {
|
private fun MessageBackupsTypeSelectionScreenPreview() {
|
||||||
val freeTier = MessageBackupsType(
|
var selectedBackupsType by remember { mutableStateOf(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"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
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) }
|
|
||||||
|
|
||||||
Previews.Preview {
|
Previews.Preview {
|
||||||
MessageBackupsTypeSelectionScreen(
|
MessageBackupsTypeSelectionScreen(
|
||||||
selectedBackupsType = selectedBackupsType,
|
selectedBackupTier = MessageBackupTier.FREE,
|
||||||
availableBackupsTypes = listOf(freeTier, paidTier),
|
availableBackupTiers = listOf(MessageBackupTier.FREE, MessageBackupTier.PAID),
|
||||||
onMessageBackupsTypeSelected = { selectedBackupsType = it },
|
onMessageBackupsTierSelected = { selectedBackupsType = it },
|
||||||
onNavigationClick = {},
|
onNavigationClick = {},
|
||||||
onReadMoreClicked = {},
|
onReadMoreClicked = {},
|
||||||
onNextClicked = {}
|
onNextClicked = {}
|
||||||
@@ -272,7 +238,51 @@ private fun formatCostPerMonth(pricePerMonth: FiatMoney): String {
|
|||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
data class MessageBackupsType(
|
data class MessageBackupsType(
|
||||||
|
val tier: MessageBackupTier,
|
||||||
val pricePerMonth: FiatMoney,
|
val pricePerMonth: FiatMoney,
|
||||||
val title: String,
|
val title: String,
|
||||||
val features: ImmutableList<MessageBackupsTypeFeature>
|
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() {
|
fun refresh() {
|
||||||
val backupsEnabled = SignalStore.settings().isBackupEnabled && BackupUtil.canUserAccessBackupDirectory(ApplicationDependencies.getApplication())
|
val backupsEnabled = SignalStore.settings().isBackupEnabled && BackupUtil.canUserAccessBackupDirectory(ApplicationDependencies.getApplication())
|
||||||
if (store.state.localBackupsEnabled != backupsEnabled) {
|
val remoteBackupsEnabled = SignalStore.backup().areBackupsEnabled
|
||||||
store.update { it.copy(localBackupsEnabled = backupsEnabled) }
|
|
||||||
|
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
|
package org.thoughtcrime.securesms.components.settings.app.chats.backups
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Box
|
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.AlertDialog
|
||||||
import androidx.compose.material3.AlertDialogDefaults
|
import androidx.compose.material3.AlertDialogDefaults
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
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.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.fragment.findNavController
|
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.Buttons
|
||||||
import org.signal.core.ui.Dialogs
|
import org.signal.core.ui.Dialogs
|
||||||
import org.signal.core.ui.Dividers
|
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.SignalPreview
|
||||||
import org.signal.core.ui.Snackbars
|
import org.signal.core.ui.Snackbars
|
||||||
import org.signal.core.ui.Texts
|
import org.signal.core.ui.Texts
|
||||||
import org.signal.core.util.money.FiatMoney
|
|
||||||
import org.thoughtcrime.securesms.R
|
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.MessageBackupsFlowActivity
|
||||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsFrequency
|
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.compose.ComposeFragment
|
||||||
|
import org.thoughtcrime.securesms.conversation.v2.registerForLifecycle
|
||||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||||
import org.thoughtcrime.securesms.util.DateUtils
|
import org.thoughtcrime.securesms.util.DateUtils
|
||||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||||
import org.thoughtcrime.securesms.util.viewModel
|
import org.thoughtcrime.securesms.util.viewModel
|
||||||
import java.math.BigDecimal
|
|
||||||
import java.util.Currency
|
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -75,13 +81,14 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
|
|||||||
val callbacks = remember { Callbacks() }
|
val callbacks = remember { Callbacks() }
|
||||||
|
|
||||||
RemoteBackupsSettingsContent(
|
RemoteBackupsSettingsContent(
|
||||||
messageBackupsType = state.messageBackupsType,
|
messageBackupTier = state.messageBackupsTier,
|
||||||
lastBackupTimestamp = state.lastBackupTimestamp,
|
lastBackupTimestamp = state.lastBackupTimestamp,
|
||||||
canBackUpUsingCellular = state.canBackUpUsingCellular,
|
canBackUpUsingCellular = state.canBackUpUsingCellular,
|
||||||
backupsFrequency = state.backupsFrequency,
|
backupsFrequency = state.backupsFrequency,
|
||||||
requestedDialog = state.dialog,
|
requestedDialog = state.dialog,
|
||||||
requestedSnackbar = state.snackbar,
|
requestedSnackbar = state.snackbar,
|
||||||
contentCallbacks = callbacks
|
contentCallbacks = callbacks,
|
||||||
|
backupProgress = state.backupProgress
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +111,7 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onBackupNowClick() {
|
override fun onBackupNowClick() {
|
||||||
// TODO [message-backups] Enqueue immediate backup
|
viewModel.onBackupNowClick()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTurnOffAndDeleteBackupsClick() {
|
override fun onTurnOffAndDeleteBackupsClick() {
|
||||||
@@ -135,6 +142,16 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
|
|||||||
findNavController().safeNavigate(R.id.action_remoteBackupsSettingsFragment_to_backupsTypeSettingsFragment)
|
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
|
@Composable
|
||||||
private fun RemoteBackupsSettingsContent(
|
private fun RemoteBackupsSettingsContent(
|
||||||
messageBackupsType: MessageBackupsType?,
|
messageBackupTier: MessageBackupTier?,
|
||||||
lastBackupTimestamp: Long,
|
lastBackupTimestamp: Long,
|
||||||
canBackUpUsingCellular: Boolean,
|
canBackUpUsingCellular: Boolean,
|
||||||
backupsFrequency: MessageBackupsFrequency,
|
backupsFrequency: MessageBackupsFrequency,
|
||||||
requestedDialog: RemoteBackupsSettingsState.Dialog,
|
requestedDialog: RemoteBackupsSettingsState.Dialog,
|
||||||
requestedSnackbar: RemoteBackupsSettingsState.Snackbar,
|
requestedSnackbar: RemoteBackupsSettingsState.Snackbar,
|
||||||
contentCallbacks: ContentCallbacks
|
contentCallbacks: ContentCallbacks,
|
||||||
|
backupProgress: BackupV2Event?
|
||||||
) {
|
) {
|
||||||
val snackbarHostState = remember {
|
val snackbarHostState = remember {
|
||||||
SnackbarHostState()
|
SnackbarHostState()
|
||||||
@@ -183,13 +201,13 @@ private fun RemoteBackupsSettingsContent(
|
|||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
BackupTypeRow(
|
BackupTypeRow(
|
||||||
messageBackupsType = messageBackupsType,
|
messageBackupTier = messageBackupTier,
|
||||||
onEnableBackupsClick = contentCallbacks::onEnableBackupsClick,
|
onEnableBackupsClick = contentCallbacks::onEnableBackupsClick,
|
||||||
onChangeBackupsTypeClick = contentCallbacks::onBackupsTypeClick
|
onChangeBackupsTypeClick = contentCallbacks::onBackupsTypeClick
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (messageBackupsType == null) {
|
if (messageBackupTier == null) {
|
||||||
item {
|
item {
|
||||||
Rows.TextRow(
|
Rows.TextRow(
|
||||||
text = "Payment history",
|
text = "Payment history",
|
||||||
@@ -205,11 +223,17 @@ private fun RemoteBackupsSettingsContent(
|
|||||||
Texts.SectionHeader(text = "Backup Details")
|
Texts.SectionHeader(text = "Backup Details")
|
||||||
}
|
}
|
||||||
|
|
||||||
item {
|
if (backupProgress == null || backupProgress.type == BackupV2Event.Type.FINISHED) {
|
||||||
LastBackupRow(
|
item {
|
||||||
lastBackupTimestamp = lastBackupTimestamp,
|
LastBackupRow(
|
||||||
onBackupNowClick = {}
|
lastBackupTimestamp = lastBackupTimestamp,
|
||||||
)
|
onBackupNowClick = contentCallbacks::onBackupNowClick
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
item {
|
||||||
|
InProgressBackupRow(progress = backupProgress.count.toInt(), totalProgress = backupProgress.estimatedTotalCount.toInt())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
item {
|
item {
|
||||||
@@ -326,14 +350,16 @@ private fun RemoteBackupsSettingsContent(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun BackupTypeRow(
|
private fun BackupTypeRow(
|
||||||
messageBackupsType: MessageBackupsType?,
|
messageBackupTier: MessageBackupTier?,
|
||||||
onEnableBackupsClick: () -> Unit,
|
onEnableBackupsClick: () -> Unit,
|
||||||
onChangeBackupsTypeClick: () -> Unit
|
onChangeBackupsTypeClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val messageBackupsType = if (messageBackupTier != null) getTierDetails(messageBackupTier) else null
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable(enabled = messageBackupsType != null, onClick = onChangeBackupsTypeClick)
|
.clickable(enabled = messageBackupTier != null, onClick = onChangeBackupsTypeClick)
|
||||||
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
|
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
|
||||||
.padding(top = 16.dp, bottom = 14.dp)
|
.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
|
@Composable
|
||||||
private fun LastBackupRow(
|
private fun LastBackupRow(
|
||||||
lastBackupTimestamp: Long,
|
lastBackupTimestamp: Long,
|
||||||
@@ -448,46 +502,48 @@ private fun BackupFrequencyDialog(
|
|||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismiss
|
onDismissRequest = onDismiss
|
||||||
) {
|
) {
|
||||||
Column(
|
Surface {
|
||||||
modifier = Modifier
|
Column(
|
||||||
.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,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
color = AlertDialogDefaults.containerColor,
|
||||||
|
shape = AlertDialogDefaults.shape
|
||||||
|
)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 24.dp)
|
|
||||||
.padding(bottom = 24.dp)
|
|
||||||
) {
|
) {
|
||||||
TextButton(onClick = onDismiss) {
|
Text(
|
||||||
Text(text = stringResource(id = android.R.string.cancel))
|
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() {
|
private fun RemoteBackupsSettingsContentPreview() {
|
||||||
Previews.Preview {
|
Previews.Preview {
|
||||||
RemoteBackupsSettingsContent(
|
RemoteBackupsSettingsContent(
|
||||||
messageBackupsType = null,
|
messageBackupTier = null,
|
||||||
lastBackupTimestamp = -1,
|
lastBackupTimestamp = -1,
|
||||||
canBackUpUsingCellular = false,
|
canBackUpUsingCellular = false,
|
||||||
backupsFrequency = MessageBackupsFrequency.NEVER,
|
backupsFrequency = MessageBackupsFrequency.NEVER,
|
||||||
requestedDialog = RemoteBackupsSettingsState.Dialog.NONE,
|
requestedDialog = RemoteBackupsSettingsState.Dialog.NONE,
|
||||||
requestedSnackbar = RemoteBackupsSettingsState.Snackbar.NONE,
|
requestedSnackbar = RemoteBackupsSettingsState.Snackbar.NONE,
|
||||||
contentCallbacks = object : ContentCallbacks {}
|
contentCallbacks = object : ContentCallbacks {},
|
||||||
|
backupProgress = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -525,11 +582,7 @@ private fun RemoteBackupsSettingsContentPreview() {
|
|||||||
private fun BackupTypeRowPreview() {
|
private fun BackupTypeRowPreview() {
|
||||||
Previews.Preview {
|
Previews.Preview {
|
||||||
BackupTypeRow(
|
BackupTypeRow(
|
||||||
messageBackupsType = MessageBackupsType(
|
messageBackupTier = MessageBackupTier.PAID,
|
||||||
title = "Text + all media",
|
|
||||||
pricePerMonth = FiatMoney(BigDecimal.valueOf(3L), Currency.getInstance(Locale.US)),
|
|
||||||
features = persistentListOf()
|
|
||||||
),
|
|
||||||
onChangeBackupsTypeClick = {},
|
onChangeBackupsTypeClick = {},
|
||||||
onEnableBackupsClick = {}
|
onEnableBackupsClick = {}
|
||||||
)
|
)
|
||||||
@@ -547,6 +600,14 @@ private fun LastBackupRowPreview() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SignalPreview
|
||||||
|
@Composable
|
||||||
|
private fun InProgressRowPreview() {
|
||||||
|
Previews.Preview {
|
||||||
|
InProgressBackupRow(50, 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@SignalPreview
|
@SignalPreview
|
||||||
@Composable
|
@Composable
|
||||||
private fun TurnOffAndDeleteBackupsDialogPreview() {
|
private fun TurnOffAndDeleteBackupsDialogPreview() {
|
||||||
|
|||||||
@@ -5,17 +5,19 @@
|
|||||||
|
|
||||||
package org.thoughtcrime.securesms.components.settings.app.chats.backups
|
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.MessageBackupsFrequency
|
||||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
|
|
||||||
|
|
||||||
data class RemoteBackupsSettingsState(
|
data class RemoteBackupsSettingsState(
|
||||||
val messageBackupsType: MessageBackupsType? = null,
|
val messageBackupsTier: MessageBackupTier? = null,
|
||||||
val canBackUpUsingCellular: Boolean = false,
|
val canBackUpUsingCellular: Boolean = false,
|
||||||
val backupSize: Long = 0,
|
val backupSize: Long = 0,
|
||||||
val backupsFrequency: MessageBackupsFrequency = MessageBackupsFrequency.DAILY,
|
val backupsFrequency: MessageBackupsFrequency = MessageBackupsFrequency.DAILY,
|
||||||
val lastBackupTimestamp: Long = 0,
|
val lastBackupTimestamp: Long = 0,
|
||||||
val dialog: Dialog = Dialog.NONE,
|
val dialog: Dialog = Dialog.NONE,
|
||||||
val snackbar: Snackbar = Snackbar.NONE
|
val snackbar: Snackbar = Snackbar.NONE,
|
||||||
|
val backupProgress: BackupV2Event? = null
|
||||||
) {
|
) {
|
||||||
enum class Dialog {
|
enum class Dialog {
|
||||||
NONE,
|
NONE,
|
||||||
|
|||||||
@@ -8,13 +8,30 @@ package org.thoughtcrime.securesms.components.settings.app.chats.backups
|
|||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.lifecycle.ViewModel
|
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.backup.v2.ui.subscription.MessageBackupsFrequency
|
||||||
|
import org.thoughtcrime.securesms.jobs.BackupMessagesJob
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ViewModel for state management of RemoteBackupsSettingsFragment
|
* ViewModel for state management of RemoteBackupsSettingsFragment
|
||||||
*/
|
*/
|
||||||
class RemoteBackupsSettingsViewModel : ViewModel() {
|
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
|
val state: State<RemoteBackupsSettingsState> = internalState
|
||||||
|
|
||||||
@@ -38,6 +55,17 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
|||||||
|
|
||||||
fun turnOffAndDeleteBackups() {
|
fun turnOffAndDeleteBackups() {
|
||||||
// TODO [message-backups] -- Delete.
|
// TODO [message-backups] -- Delete.
|
||||||
|
SignalStore.backup().areBackupsEnabled = false
|
||||||
internalState.value = state.value.copy(snackbar = RemoteBackupsSettingsState.Snackbar.BACKUP_DELETED_AND_TURNED_OFF)
|
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.core.util.money.FiatMoney
|
||||||
import org.signal.donations.PaymentSourceType
|
import org.signal.donations.PaymentSourceType
|
||||||
import org.thoughtcrime.securesms.R
|
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.MessageBackupsFlowActivity
|
||||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
|
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
|
||||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||||
@@ -186,6 +187,7 @@ private fun BackupsTypeSettingsContentPreview() {
|
|||||||
BackupsTypeSettingsContent(
|
BackupsTypeSettingsContent(
|
||||||
state = BackupsTypeSettingsState(
|
state = BackupsTypeSettingsState(
|
||||||
backupsType = MessageBackupsType(
|
backupsType = MessageBackupsType(
|
||||||
|
tier = MessageBackupTier.PAID,
|
||||||
pricePerMonth = FiatMoney(BigDecimal.valueOf(3), Currency.getInstance("USD")),
|
pricePerMonth = FiatMoney(BigDecimal.valueOf(3), Currency.getInstance("USD")),
|
||||||
title = "Text + all media",
|
title = "Text + all media",
|
||||||
features = persistentListOf()
|
features = persistentListOf()
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
package org.thoughtcrime.securesms.jobs
|
package org.thoughtcrime.securesms.jobs
|
||||||
|
|
||||||
|
import org.greenrobot.eventbus.EventBus
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.signal.protos.resumableuploads.ResumableUpload
|
import org.signal.protos.resumableuploads.ResumableUpload
|
||||||
import org.thoughtcrime.securesms.attachments.Attachment
|
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.DatabaseAttachment
|
||||||
import org.thoughtcrime.securesms.attachments.PointerAttachment
|
import org.thoughtcrime.securesms.attachments.PointerAttachment
|
||||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
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.AttachmentTable
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||||
@@ -34,7 +36,9 @@ import kotlin.time.Duration.Companion.days
|
|||||||
class ArchiveAttachmentBackfillJob private constructor(
|
class ArchiveAttachmentBackfillJob private constructor(
|
||||||
parameters: Parameters,
|
parameters: Parameters,
|
||||||
private var attachmentId: AttachmentId?,
|
private var attachmentId: AttachmentId?,
|
||||||
private var uploadSpec: ResumableUpload?
|
private var uploadSpec: ResumableUpload?,
|
||||||
|
private var totalCount: Int?,
|
||||||
|
private var progress: Int?
|
||||||
) : Job(parameters) {
|
) : Job(parameters) {
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = Log.tag(ArchiveAttachmentBackfillJob::class.java)
|
private val TAG = Log.tag(ArchiveAttachmentBackfillJob::class.java)
|
||||||
@@ -42,7 +46,7 @@ class ArchiveAttachmentBackfillJob private constructor(
|
|||||||
const val KEY = "ArchiveAttachmentBackfillJob"
|
const val KEY = "ArchiveAttachmentBackfillJob"
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() : this(
|
constructor(progress: Int? = null, totalCount: Int? = null) : this(
|
||||||
parameters = Parameters.Builder()
|
parameters = Parameters.Builder()
|
||||||
.setQueue("ArchiveAttachmentBackfillJob")
|
.setQueue("ArchiveAttachmentBackfillJob")
|
||||||
.setMaxInstancesForQueue(2)
|
.setMaxInstancesForQueue(2)
|
||||||
@@ -51,7 +55,9 @@ class ArchiveAttachmentBackfillJob private constructor(
|
|||||||
.addConstraint(NetworkConstraint.KEY)
|
.addConstraint(NetworkConstraint.KEY)
|
||||||
.build(),
|
.build(),
|
||||||
attachmentId = null,
|
attachmentId = null,
|
||||||
uploadSpec = null
|
uploadSpec = null,
|
||||||
|
totalCount = totalCount,
|
||||||
|
progress = progress
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun serialize(): ByteArray {
|
override fun serialize(): ByteArray {
|
||||||
@@ -64,6 +70,7 @@ class ArchiveAttachmentBackfillJob private constructor(
|
|||||||
override fun getFactoryKey(): String = KEY
|
override fun getFactoryKey(): String = KEY
|
||||||
|
|
||||||
override fun run(): Result {
|
override fun run(): Result {
|
||||||
|
EventBus.getDefault().postSticky(BackupV2Event(BackupV2Event.Type.PROGRESS_ATTACHMENTS, progress?.toLong() ?: 0, totalCount?.toLong() ?: 0))
|
||||||
var attachmentRecord: DatabaseAttachment? = if (attachmentId != null) {
|
var attachmentRecord: DatabaseAttachment? = if (attachmentId != null) {
|
||||||
Log.i(TAG, "Retrying $attachmentId")
|
Log.i(TAG, "Retrying $attachmentId")
|
||||||
SignalDatabase.attachments.getAttachment(attachmentId!!)
|
SignalDatabase.attachments.getAttachment(attachmentId!!)
|
||||||
@@ -73,7 +80,7 @@ class ArchiveAttachmentBackfillJob private constructor(
|
|||||||
|
|
||||||
if (attachmentRecord == null && attachmentId != null) {
|
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.")
|
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()
|
return Result.success()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,11 +91,16 @@ class ArchiveAttachmentBackfillJob private constructor(
|
|||||||
val resetCount = SignalDatabase.attachments.resetPendingArchiveBackfills()
|
val resetCount = SignalDatabase.attachments.resetPendingArchiveBackfills()
|
||||||
if (resetCount > 0) {
|
if (resetCount > 0) {
|
||||||
Log.w(TAG, "We thought we were done, but $resetCount items were still in progress! Need to run again to retry.")
|
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 {
|
} else {
|
||||||
Log.i(TAG, "All good! Should be done.")
|
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()
|
return Result.success()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +109,7 @@ class ArchiveAttachmentBackfillJob private constructor(
|
|||||||
val transferState: AttachmentTable.ArchiveTransferState? = SignalDatabase.attachments.getArchiveTransferState(attachmentRecord.attachmentId)
|
val transferState: AttachmentTable.ArchiveTransferState? = SignalDatabase.attachments.getArchiveTransferState(attachmentRecord.attachmentId)
|
||||||
if (transferState == null) {
|
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.")
|
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()
|
return Result.success()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,19 +117,19 @@ class ArchiveAttachmentBackfillJob private constructor(
|
|||||||
|
|
||||||
if (transferState == AttachmentTable.ArchiveTransferState.FINISHED) {
|
if (transferState == AttachmentTable.ArchiveTransferState.FINISHED) {
|
||||||
Log.i(TAG, "Attachment $attachmentId is already finished. Skipping.")
|
Log.i(TAG, "Attachment $attachmentId is already finished. Skipping.")
|
||||||
ApplicationDependencies.getJobManager().add(ArchiveAttachmentBackfillJob())
|
reenqueueWithIncrementedProgress()
|
||||||
return Result.success()
|
return Result.success()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (transferState == AttachmentTable.ArchiveTransferState.PERMANENT_FAILURE) {
|
if (transferState == AttachmentTable.ArchiveTransferState.PERMANENT_FAILURE) {
|
||||||
Log.i(TAG, "Attachment $attachmentId is already marked as a permanent failure. Skipping.")
|
Log.i(TAG, "Attachment $attachmentId is already marked as a permanent failure. Skipping.")
|
||||||
ApplicationDependencies.getJobManager().add(ArchiveAttachmentBackfillJob())
|
reenqueueWithIncrementedProgress()
|
||||||
return Result.success()
|
return Result.success()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (transferState == AttachmentTable.ArchiveTransferState.ATTACHMENT_TRANSFER_PENDING) {
|
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.")
|
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()
|
return Result.success()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,7 +176,7 @@ class ArchiveAttachmentBackfillJob private constructor(
|
|||||||
|
|
||||||
if (attachmentRecord == null) {
|
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.")
|
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()
|
return Result.success()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,7 +186,7 @@ class ArchiveAttachmentBackfillJob private constructor(
|
|||||||
Log.d(TAG, "Move complete!")
|
Log.d(TAG, "Move complete!")
|
||||||
|
|
||||||
SignalDatabase.attachments.setArchiveTransferState(attachmentRecord.attachmentId, AttachmentTable.ArchiveTransferState.FINISHED)
|
SignalDatabase.attachments.setArchiveTransferState(attachmentRecord.attachmentId, AttachmentTable.ArchiveTransferState.FINISHED)
|
||||||
ApplicationDependencies.getJobManager().add(ArchiveAttachmentBackfillJob())
|
reenqueueWithIncrementedProgress()
|
||||||
Result.success()
|
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() {
|
override fun onFailure() {
|
||||||
attachmentId?.let { id ->
|
attachmentId?.let { id ->
|
||||||
Log.w(TAG, "Failed to archive $id!")
|
Log.w(TAG, "Failed to archive $id!")
|
||||||
@@ -261,7 +282,9 @@ class ArchiveAttachmentBackfillJob private constructor(
|
|||||||
return ArchiveAttachmentBackfillJob(
|
return ArchiveAttachmentBackfillJob(
|
||||||
parameters = parameters,
|
parameters = parameters,
|
||||||
attachmentId = data?.attachmentId?.let { AttachmentId(it) },
|
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
|
package org.thoughtcrime.securesms.jobs
|
||||||
|
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
|
import org.greenrobot.eventbus.EventBus
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.thoughtcrime.securesms.BuildConfig
|
|
||||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
||||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||||
|
import org.thoughtcrime.securesms.backup.v2.BackupV2Event
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||||
import org.thoughtcrime.securesms.jobmanager.Job
|
import org.thoughtcrime.securesms.jobmanager.Job
|
||||||
@@ -54,33 +55,41 @@ class BackupMessagesJob private constructor(parameters: Parameters) : BaseJob(pa
|
|||||||
|
|
||||||
override fun onFailure() = Unit
|
override fun onFailure() = Unit
|
||||||
|
|
||||||
private fun archiveAttachments() {
|
private fun archiveAttachments(): Boolean {
|
||||||
if (BuildConfig.MESSAGE_BACKUP_RESTORE_ENABLED) {
|
if (!SignalStore.backup().canReadWriteToArchiveCdn) return false
|
||||||
SignalStore.backup().canReadWriteToArchiveCdn = true
|
|
||||||
}
|
|
||||||
val batchSize = 100
|
val batchSize = 100
|
||||||
|
var needToBackfill = 0
|
||||||
|
var totalCount: Int
|
||||||
|
var progress = 0
|
||||||
SignalDatabase.attachments.getArchivableAttachments().use { cursor ->
|
SignalDatabase.attachments.getArchivableAttachments().use { cursor ->
|
||||||
|
totalCount = cursor.count
|
||||||
while (!cursor.isAfterLast) {
|
while (!cursor.isAfterLast) {
|
||||||
val attachments = cursor.readAttachmentBatch(batchSize)
|
val attachments = cursor.readAttachmentBatch(batchSize)
|
||||||
|
|
||||||
when (val archiveResult = BackupRepository.archiveMedia(attachments)) {
|
when (val archiveResult = BackupRepository.archiveMedia(attachments)) {
|
||||||
is NetworkResult.Success -> {
|
is NetworkResult.Success -> {
|
||||||
|
Log.i(TAG, "Archive call successful")
|
||||||
for (success in archiveResult.result.sourceNotFoundResponses) {
|
for (success in archiveResult.result.sourceNotFoundResponses) {
|
||||||
val attachmentId = archiveResult.result.mediaIdToAttachmentId(success.mediaId)
|
val attachmentId = archiveResult.result.mediaIdToAttachmentId(success.mediaId)
|
||||||
ApplicationDependencies
|
Log.i(TAG, "Attachment $attachmentId not found on cdn, will need to re-upload")
|
||||||
.getJobManager()
|
needToBackfill++
|
||||||
.startChain(AttachmentUploadJob(attachmentId))
|
|
||||||
.then(ArchiveAttachmentJob(attachmentId))
|
|
||||||
.enqueue()
|
|
||||||
}
|
}
|
||||||
|
progress += attachments.size
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
Log.e(TAG, "Failed to archive $archiveResult")
|
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> {
|
private fun Cursor.readAttachmentBatch(batchSize: Int): List<DatabaseAttachment> {
|
||||||
@@ -96,6 +105,7 @@ class BackupMessagesJob private constructor(parameters: Parameters) : BaseJob(pa
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onRun() {
|
override fun onRun() {
|
||||||
|
EventBus.getDefault().postSticky(BackupV2Event(type = BackupV2Event.Type.PROGRESS_MESSAGES, count = 0, estimatedTotalCount = 0))
|
||||||
val tempBackupFile = BlobProvider.getInstance().forNonAutoEncryptingSingleSessionOnDisk(ApplicationDependencies.getApplication())
|
val tempBackupFile = BlobProvider.getInstance().forNonAutoEncryptingSingleSessionOnDisk(ApplicationDependencies.getApplication())
|
||||||
|
|
||||||
val outputStream = FileOutputStream(tempBackupFile)
|
val outputStream = FileOutputStream(tempBackupFile)
|
||||||
@@ -104,11 +114,14 @@ class BackupMessagesJob private constructor(parameters: Parameters) : BaseJob(pa
|
|||||||
FileInputStream(tempBackupFile).use {
|
FileInputStream(tempBackupFile).use {
|
||||||
BackupRepository.uploadBackupFile(it, tempBackupFile.length())
|
BackupRepository.uploadBackupFile(it, tempBackupFile.length())
|
||||||
}
|
}
|
||||||
|
val needBackfill = archiveAttachments()
|
||||||
archiveAttachments()
|
|
||||||
if (!tempBackupFile.delete()) {
|
if (!tempBackupFile.delete()) {
|
||||||
Log.e(TAG, "Failed to delete temp backup file")
|
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
|
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_RESTORE_STATE = "backup.restoreState"
|
||||||
|
|
||||||
private const val KEY_NEXT_BACKUP_TIME = "backup.nextBackupTime"
|
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_DIRECTORY = "backup.cdn.directory"
|
||||||
private const val KEY_CDN_BACKUP_MEDIA_DIRECTORY = "backup.cdn.mediaDirectory"
|
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 optimizeStorage: Boolean by booleanValue(KEY_OPTIMIZE_STORAGE, false)
|
||||||
|
|
||||||
var nextBackupTime: Long by longValue(KEY_NEXT_BACKUP_TIME, -1)
|
var nextBackupTime: Long by longValue(KEY_NEXT_BACKUP_TIME, -1)
|
||||||
|
var lastBackupTime: Long by longValue(KEY_LAST_BACKUP_TIME, -1)
|
||||||
|
|
||||||
var areBackupsEnabled: Boolean
|
var areBackupsEnabled: Boolean
|
||||||
get() {
|
get() {
|
||||||
|
|||||||
@@ -55,4 +55,6 @@ message ArchiveAttachmentJobData {
|
|||||||
message ArchiveAttachmentBackfillJobData {
|
message ArchiveAttachmentBackfillJobData {
|
||||||
optional uint64 attachmentId = 1;
|
optional uint64 attachmentId = 1;
|
||||||
ResumableUpload uploadSpec = 2;
|
ResumableUpload uploadSpec = 2;
|
||||||
|
optional uint32 count = 3;
|
||||||
|
optional uint32 totalCount = 4;
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user