mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-21 03:28:47 +00:00
Improve free tier UX around media.
This commit is contained in:
committed by
Jeffrey Starke
parent
c5753b96ff
commit
cbfdc4b57a
@@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.widthIn
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.dimensionResource
|
import androidx.compose.ui.res.dimensionResource
|
||||||
@@ -29,6 +30,7 @@ import org.signal.core.ui.compose.Buttons
|
|||||||
import org.signal.core.ui.compose.Previews
|
import org.signal.core.ui.compose.Previews
|
||||||
import org.signal.core.ui.compose.SignalPreview
|
import org.signal.core.ui.compose.SignalPreview
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.components.settings.app.backups.BackupStateObserver
|
||||||
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
|
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
|
||||||
import org.thoughtcrime.securesms.jobs.BackupMessagesJob
|
import org.thoughtcrime.securesms.jobs.BackupMessagesJob
|
||||||
import org.signal.core.ui.R as CoreUiR
|
import org.signal.core.ui.R as CoreUiR
|
||||||
@@ -51,15 +53,15 @@ class CreateBackupBottomSheet : ComposeBottomSheetDialogFragment() {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun SheetContent() {
|
override fun SheetContent() {
|
||||||
|
val isPaidTier: Boolean = remember { BackupStateObserver.getNonIOBackupState().isLikelyPaidTier() }
|
||||||
|
|
||||||
CreateBackupBottomSheetContent(
|
CreateBackupBottomSheetContent(
|
||||||
|
isPaidTier = isPaidTier,
|
||||||
onBackupNowClick = {
|
onBackupNowClick = {
|
||||||
BackupMessagesJob.enqueue()
|
BackupMessagesJob.enqueue()
|
||||||
setFragmentResult(REQUEST_KEY, bundleOf(REQUEST_KEY to Result.BACKUP_STARTED))
|
setFragmentResult(REQUEST_KEY, bundleOf(REQUEST_KEY to Result.BACKUP_STARTED))
|
||||||
isResultSet = true
|
isResultSet = true
|
||||||
dismissAllowingStateLoss()
|
dismissAllowingStateLoss()
|
||||||
},
|
|
||||||
onBackupLaterClick = {
|
|
||||||
dismissAllowingStateLoss()
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -80,8 +82,8 @@ class CreateBackupBottomSheet : ComposeBottomSheetDialogFragment() {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun CreateBackupBottomSheetContent(
|
private fun CreateBackupBottomSheetContent(
|
||||||
onBackupNowClick: () -> Unit,
|
isPaidTier: Boolean,
|
||||||
onBackupLaterClick: () -> Unit
|
onBackupNowClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
@@ -106,8 +108,14 @@ private fun CreateBackupBottomSheetContent(
|
|||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val body = if (isPaidTier) {
|
||||||
|
stringResource(id = R.string.CreateBackupBottomSheet__depending_on_the_size)
|
||||||
|
} else {
|
||||||
|
stringResource(id = R.string.CreateBackupBottomSheet__free_tier)
|
||||||
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(id = R.string.CreateBackupBottomSheet__depending_on_the_size),
|
text = body,
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
@@ -128,11 +136,22 @@ private fun CreateBackupBottomSheetContent(
|
|||||||
|
|
||||||
@SignalPreview
|
@SignalPreview
|
||||||
@Composable
|
@Composable
|
||||||
private fun CreateBackupBottomSheetContentPreview() {
|
private fun CreateBackupBottomSheetContentPaidPreview() {
|
||||||
Previews.BottomSheetPreview {
|
Previews.BottomSheetPreview {
|
||||||
CreateBackupBottomSheetContent(
|
CreateBackupBottomSheetContent(
|
||||||
onBackupNowClick = {},
|
isPaidTier = true,
|
||||||
onBackupLaterClick = {}
|
onBackupNowClick = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SignalPreview
|
||||||
|
@Composable
|
||||||
|
private fun CreateBackupBottomSheetContentFreePreview() {
|
||||||
|
Previews.BottomSheetPreview {
|
||||||
|
CreateBackupBottomSheetContent(
|
||||||
|
isPaidTier = false,
|
||||||
|
onBackupNowClick = {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ import org.thoughtcrime.securesms.help.HelpFragment
|
|||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.keyvalue.protos.ArchiveUploadProgressState
|
import org.thoughtcrime.securesms.keyvalue.protos.ArchiveUploadProgressState
|
||||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||||
|
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||||
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
|
||||||
@@ -285,6 +286,14 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
|
|||||||
override fun onIncludeDebuglogClick(newState: Boolean) {
|
override fun onIncludeDebuglogClick(newState: Boolean) {
|
||||||
viewModel.setIncludeDebuglog(newState)
|
viewModel.setIncludeDebuglog(newState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onMediaBackupSizeClick() {
|
||||||
|
viewModel.requestDialog(RemoteBackupsSettingsState.Dialog.FREE_TIER_MEDIA_EXPLAINER)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFreeTierBackupSizeLearnMore() {
|
||||||
|
CommunicationActions.openBrowserLink(requireContext(), "https://support.signal.org/hc/articles/9708267671322")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun displayBackupKey() {
|
private fun displayBackupKey() {
|
||||||
@@ -387,6 +396,8 @@ private interface ContentCallbacks {
|
|||||||
fun onDisplayDownloadingBackupDialog() = Unit
|
fun onDisplayDownloadingBackupDialog() = Unit
|
||||||
fun onManageStorageClick() = Unit
|
fun onManageStorageClick() = Unit
|
||||||
fun onIncludeDebuglogClick(newState: Boolean) = Unit
|
fun onIncludeDebuglogClick(newState: Boolean) = Unit
|
||||||
|
fun onMediaBackupSizeClick() = Unit
|
||||||
|
fun onFreeTierBackupSizeLearnMore() = Unit
|
||||||
|
|
||||||
object Empty : ContentCallbacks
|
object Empty : ContentCallbacks
|
||||||
}
|
}
|
||||||
@@ -634,6 +645,18 @@ private fun RemoteBackupsSettingsContent(
|
|||||||
onResumeOverCellularClick = contentCallbacks::onRestoreUsingCellularClick
|
onResumeOverCellularClick = contentCallbacks::onRestoreUsingCellularClick
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RemoteBackupsSettingsState.Dialog.FREE_TIER_MEDIA_EXPLAINER -> {
|
||||||
|
Dialogs.SimpleAlertDialog(
|
||||||
|
title = stringResource(R.string.RemoteBackupsSettingsFragment__free_tier_storage_title),
|
||||||
|
body = pluralStringResource(R.plurals.RemoteBackupsSettingsFragment__backup_frequency_dialog_body, state.freeTierMediaRetentionDays, state.freeTierMediaRetentionDays),
|
||||||
|
confirm = stringResource(android.R.string.ok),
|
||||||
|
dismiss = stringResource(R.string.RemoteBackupsSettingsFragment__learn_more),
|
||||||
|
onConfirm = {},
|
||||||
|
onDismiss = contentCallbacks::onDialogDismissed,
|
||||||
|
onDeny = contentCallbacks::onFreeTierBackupSizeLearnMore
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val snackbarMessageId = remember(state.snackbar) {
|
val snackbarMessageId = remember(state.snackbar) {
|
||||||
@@ -902,6 +925,7 @@ private fun LazyListScope.appendBackupDetailsItems(
|
|||||||
item {
|
item {
|
||||||
InProgressBackupRow(
|
InProgressBackupRow(
|
||||||
archiveUploadProgressState = backupProgress,
|
archiveUploadProgressState = backupProgress,
|
||||||
|
isPaidTier = state.tier == MessageBackupTier.PAID,
|
||||||
canBackupMessagesRun = state.canBackupMessagesJobRun,
|
canBackupMessagesRun = state.canBackupMessagesJobRun,
|
||||||
canBackupUsingCellular = state.canBackUpUsingCellular,
|
canBackupUsingCellular = state.canBackUpUsingCellular,
|
||||||
cancelArchiveUpload = contentCallbacks::onCancelUploadClick
|
cancelArchiveUpload = contentCallbacks::onCancelUploadClick
|
||||||
@@ -909,15 +933,15 @@ private fun LazyListScope.appendBackupDetailsItems(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.backupState.isLikelyPaidTier()) {
|
item {
|
||||||
item {
|
val sizeText = if (state.backupMediaSize < 0L) {
|
||||||
val sizeText = if (state.backupMediaSize < 0L) {
|
stringResource(R.string.RemoteBackupsSettingsFragment__calculating)
|
||||||
stringResource(R.string.RemoteBackupsSettingsFragment__calculating)
|
} else {
|
||||||
} else {
|
state.backupMediaSize.bytes.toUnitString()
|
||||||
state.backupMediaSize.bytes.toUnitString()
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Rows.TextRow(text = {
|
Rows.TextRow(
|
||||||
|
text = {
|
||||||
Column {
|
Column {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(id = R.string.RemoteBackupsSettingsFragment__backup_size),
|
text = stringResource(id = R.string.RemoteBackupsSettingsFragment__backup_size),
|
||||||
@@ -930,8 +954,13 @@ private fun LazyListScope.appendBackupDetailsItems(
|
|||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
}
|
onClick = if (state.backupMediaSize >= 0L && state.tier == MessageBackupTier.FREE) {
|
||||||
|
{ contentCallbacks.onMediaBackupSizeClick() }
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
item {
|
item {
|
||||||
@@ -1353,6 +1382,7 @@ private fun SubscriptionMismatchMissingGooglePlayCard(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun InProgressBackupRow(
|
private fun InProgressBackupRow(
|
||||||
archiveUploadProgressState: ArchiveUploadProgressState,
|
archiveUploadProgressState: ArchiveUploadProgressState,
|
||||||
|
isPaidTier: Boolean,
|
||||||
canBackupMessagesRun: Boolean = true,
|
canBackupMessagesRun: Boolean = true,
|
||||||
canBackupUsingCellular: Boolean = true,
|
canBackupUsingCellular: Boolean = true,
|
||||||
cancelArchiveUpload: () -> Unit = {}
|
cancelArchiveUpload: () -> Unit = {}
|
||||||
@@ -1390,7 +1420,7 @@ private fun InProgressBackupRow(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = getProgressStateMessage(archiveUploadProgressState, canBackupMessagesRun, canBackupUsingCellular),
|
text = getProgressStateMessage(archiveUploadProgressState, isPaidTier, canBackupMessagesRun, canBackupUsingCellular),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
@@ -1428,11 +1458,11 @@ private fun ArchiveProgressIndicator(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun getProgressStateMessage(archiveUploadProgressState: ArchiveUploadProgressState, canBackupMessagesRun: Boolean, canBackupUsingCellular: Boolean): String {
|
private fun getProgressStateMessage(archiveUploadProgressState: ArchiveUploadProgressState, isPaidTier: Boolean, canBackupMessagesRun: Boolean, canBackupUsingCellular: Boolean): String {
|
||||||
return when (archiveUploadProgressState.state) {
|
return when (archiveUploadProgressState.state) {
|
||||||
ArchiveUploadProgressState.State.None, ArchiveUploadProgressState.State.UserCanceled -> stringResource(R.string.RemoteBackupsSettingsFragment__processing_backup)
|
ArchiveUploadProgressState.State.None, ArchiveUploadProgressState.State.UserCanceled -> stringResource(R.string.RemoteBackupsSettingsFragment__processing_backup)
|
||||||
ArchiveUploadProgressState.State.Export -> getBackupExportPhaseProgressString(archiveUploadProgressState, canBackupMessagesRun, canBackupUsingCellular)
|
ArchiveUploadProgressState.State.Export -> getBackupExportPhaseProgressString(archiveUploadProgressState, canBackupMessagesRun, canBackupUsingCellular)
|
||||||
ArchiveUploadProgressState.State.UploadBackupFile, ArchiveUploadProgressState.State.UploadMedia -> getBackupUploadPhaseProgressString(archiveUploadProgressState)
|
ArchiveUploadProgressState.State.UploadBackupFile, ArchiveUploadProgressState.State.UploadMedia -> getBackupUploadPhaseProgressString(archiveUploadProgressState, isPaidTier)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1464,12 +1494,16 @@ private fun getBackupExportPhaseProgressString(state: ArchiveUploadProgressState
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun getBackupUploadPhaseProgressString(state: ArchiveUploadProgressState): String {
|
private fun getBackupUploadPhaseProgressString(state: ArchiveUploadProgressState, isPaidTier: Boolean): String {
|
||||||
val formattedTotalBytes = state.uploadBytesTotal.bytes.toUnitString()
|
val formattedTotalBytes = state.uploadBytesTotal.bytes.toUnitString()
|
||||||
val formattedUploadedBytes = state.uploadBytesUploaded.bytes.toUnitString()
|
val formattedUploadedBytes = state.uploadBytesUploaded.bytes.toUnitString()
|
||||||
val percent = (state.uploadProgress() * 100).toInt()
|
val percent = (state.uploadProgress() * 100).toInt()
|
||||||
|
|
||||||
return stringResource(R.string.RemoteBackupsSettingsFragment__uploading_s_of_s_d, formattedUploadedBytes, formattedTotalBytes, percent)
|
return if (isPaidTier) {
|
||||||
|
stringResource(R.string.RemoteBackupsSettingsFragment__uploading_s_of_s_d, formattedUploadedBytes, formattedTotalBytes, percent)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.RemoteBackupsSettingsFragment__uploading_d, percent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -1949,18 +1983,20 @@ private fun LastBackupRowPreview() {
|
|||||||
private fun InProgressRowPreview() {
|
private fun InProgressRowPreview() {
|
||||||
Previews.Preview {
|
Previews.Preview {
|
||||||
Column {
|
Column {
|
||||||
InProgressBackupRow(archiveUploadProgressState = ArchiveUploadProgressState())
|
InProgressBackupRow(archiveUploadProgressState = ArchiveUploadProgressState(), isPaidTier = true)
|
||||||
InProgressBackupRow(
|
InProgressBackupRow(
|
||||||
archiveUploadProgressState = ArchiveUploadProgressState(
|
archiveUploadProgressState = ArchiveUploadProgressState(
|
||||||
state = ArchiveUploadProgressState.State.Export,
|
state = ArchiveUploadProgressState.State.Export,
|
||||||
backupPhase = ArchiveUploadProgressState.BackupPhase.BackupPhaseNone
|
backupPhase = ArchiveUploadProgressState.BackupPhase.BackupPhaseNone
|
||||||
)
|
),
|
||||||
|
isPaidTier = true
|
||||||
)
|
)
|
||||||
InProgressBackupRow(
|
InProgressBackupRow(
|
||||||
archiveUploadProgressState = ArchiveUploadProgressState(
|
archiveUploadProgressState = ArchiveUploadProgressState(
|
||||||
state = ArchiveUploadProgressState.State.Export,
|
state = ArchiveUploadProgressState.State.Export,
|
||||||
backupPhase = ArchiveUploadProgressState.BackupPhase.Account
|
backupPhase = ArchiveUploadProgressState.BackupPhase.Account
|
||||||
)
|
),
|
||||||
|
isPaidTier = true
|
||||||
)
|
)
|
||||||
InProgressBackupRow(
|
InProgressBackupRow(
|
||||||
archiveUploadProgressState = ArchiveUploadProgressState(
|
archiveUploadProgressState = ArchiveUploadProgressState(
|
||||||
@@ -1968,7 +2004,8 @@ private fun InProgressRowPreview() {
|
|||||||
backupPhase = ArchiveUploadProgressState.BackupPhase.Message,
|
backupPhase = ArchiveUploadProgressState.BackupPhase.Message,
|
||||||
frameExportCount = 1,
|
frameExportCount = 1,
|
||||||
frameTotalCount = 1
|
frameTotalCount = 1
|
||||||
)
|
),
|
||||||
|
isPaidTier = true
|
||||||
)
|
)
|
||||||
InProgressBackupRow(
|
InProgressBackupRow(
|
||||||
archiveUploadProgressState = ArchiveUploadProgressState(
|
archiveUploadProgressState = ArchiveUploadProgressState(
|
||||||
@@ -1976,7 +2013,8 @@ private fun InProgressRowPreview() {
|
|||||||
backupPhase = ArchiveUploadProgressState.BackupPhase.Message,
|
backupPhase = ArchiveUploadProgressState.BackupPhase.Message,
|
||||||
frameExportCount = 1000,
|
frameExportCount = 1000,
|
||||||
frameTotalCount = 100_000
|
frameTotalCount = 100_000
|
||||||
)
|
),
|
||||||
|
isPaidTier = true
|
||||||
)
|
)
|
||||||
InProgressBackupRow(
|
InProgressBackupRow(
|
||||||
archiveUploadProgressState = ArchiveUploadProgressState(
|
archiveUploadProgressState = ArchiveUploadProgressState(
|
||||||
@@ -1984,7 +2022,8 @@ private fun InProgressRowPreview() {
|
|||||||
backupPhase = ArchiveUploadProgressState.BackupPhase.Message,
|
backupPhase = ArchiveUploadProgressState.BackupPhase.Message,
|
||||||
frameExportCount = 1_000_000,
|
frameExportCount = 1_000_000,
|
||||||
frameTotalCount = 100_000
|
frameTotalCount = 100_000
|
||||||
)
|
),
|
||||||
|
isPaidTier = true
|
||||||
)
|
)
|
||||||
InProgressBackupRow(
|
InProgressBackupRow(
|
||||||
archiveUploadProgressState = ArchiveUploadProgressState(
|
archiveUploadProgressState = ArchiveUploadProgressState(
|
||||||
@@ -1994,7 +2033,19 @@ private fun InProgressRowPreview() {
|
|||||||
backupFileTotalBytes = 50.mebiBytes.inWholeBytes,
|
backupFileTotalBytes = 50.mebiBytes.inWholeBytes,
|
||||||
mediaUploadedBytes = 0,
|
mediaUploadedBytes = 0,
|
||||||
mediaTotalBytes = 0
|
mediaTotalBytes = 0
|
||||||
)
|
),
|
||||||
|
isPaidTier = true
|
||||||
|
)
|
||||||
|
InProgressBackupRow(
|
||||||
|
archiveUploadProgressState = ArchiveUploadProgressState(
|
||||||
|
state = ArchiveUploadProgressState.State.UploadBackupFile,
|
||||||
|
backupPhase = ArchiveUploadProgressState.BackupPhase.BackupPhaseNone,
|
||||||
|
backupFileUploadedBytes = 10.mebiBytes.inWholeBytes,
|
||||||
|
backupFileTotalBytes = 50.mebiBytes.inWholeBytes,
|
||||||
|
mediaUploadedBytes = 0,
|
||||||
|
mediaTotalBytes = 0
|
||||||
|
),
|
||||||
|
isPaidTier = false
|
||||||
)
|
)
|
||||||
InProgressBackupRow(
|
InProgressBackupRow(
|
||||||
archiveUploadProgressState = ArchiveUploadProgressState(
|
archiveUploadProgressState = ArchiveUploadProgressState(
|
||||||
@@ -2004,7 +2055,8 @@ private fun InProgressRowPreview() {
|
|||||||
backupFileTotalBytes = 50.mebiBytes.inWholeBytes,
|
backupFileTotalBytes = 50.mebiBytes.inWholeBytes,
|
||||||
mediaUploadedBytes = 100.mebiBytes.inWholeBytes,
|
mediaUploadedBytes = 100.mebiBytes.inWholeBytes,
|
||||||
mediaTotalBytes = 1.gibiBytes.inWholeBytes
|
mediaTotalBytes = 1.gibiBytes.inWholeBytes
|
||||||
)
|
),
|
||||||
|
isPaidTier = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ data class RemoteBackupsSettingsState(
|
|||||||
val canBackupMessagesJobRun: Boolean = false,
|
val canBackupMessagesJobRun: Boolean = false,
|
||||||
val backupMediaDetails: BackupMediaDetails? = null,
|
val backupMediaDetails: BackupMediaDetails? = null,
|
||||||
val showBackupCreateFailedError: Boolean = false,
|
val showBackupCreateFailedError: Boolean = false,
|
||||||
val showBackupCreateCouldNotCompleteError: Boolean = false
|
val showBackupCreateCouldNotCompleteError: Boolean = false,
|
||||||
|
val freeTierMediaRetentionDays: Int = -1
|
||||||
) {
|
) {
|
||||||
|
|
||||||
data class BackupMediaDetails(
|
data class BackupMediaDetails(
|
||||||
@@ -50,7 +51,8 @@ data class RemoteBackupsSettingsState(
|
|||||||
SUBSCRIPTION_NOT_FOUND,
|
SUBSCRIPTION_NOT_FOUND,
|
||||||
SKIP_MEDIA_RESTORE_PROTECTION,
|
SKIP_MEDIA_RESTORE_PROTECTION,
|
||||||
CANCEL_MEDIA_RESTORE_PROTECTION,
|
CANCEL_MEDIA_RESTORE_PROTECTION,
|
||||||
RESTORE_OVER_CELLULAR_PROTECTION
|
RESTORE_OVER_CELLULAR_PROTECTION,
|
||||||
|
FREE_TIER_MEDIA_EXPLAINER
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class Snackbar {
|
enum class Snackbar {
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ import org.thoughtcrime.securesms.backup.DeletionState
|
|||||||
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgress
|
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgress
|
||||||
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgressState.RestoreStatus
|
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgressState.RestoreStatus
|
||||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||||
|
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||||
|
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
|
||||||
|
import org.thoughtcrime.securesms.components.settings.app.backups.BackupState
|
||||||
import org.thoughtcrime.securesms.components.settings.app.backups.BackupStateObserver
|
import org.thoughtcrime.securesms.components.settings.app.backups.BackupStateObserver
|
||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||||
@@ -47,6 +50,8 @@ import org.thoughtcrime.securesms.util.Environment
|
|||||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||||
import org.whispersystems.signalservice.api.NetworkResult
|
import org.whispersystems.signalservice.api.NetworkResult
|
||||||
|
import kotlin.time.Duration.Companion.days
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -163,11 +168,12 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
BackupStateObserver(viewModelScope).backupState.collect { state ->
|
BackupStateObserver(viewModelScope).backupState.collect { state ->
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(backupState = state)
|
it.copy(backupState = state)
|
||||||
}
|
}
|
||||||
|
refreshState(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,8 +264,10 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
|||||||
|
|
||||||
private fun refreshBackupMediaSizeState() {
|
private fun refreshBackupMediaSizeState() {
|
||||||
_state.update {
|
_state.update {
|
||||||
|
val (mediaSize, mediaRetentionDays) = getBackupMediaSize(it.tier, (it.backupState as? BackupState.WithTypeAndRenewalTime)?.messageBackupsType)
|
||||||
it.copy(
|
it.copy(
|
||||||
backupMediaSize = getBackupMediaSize(),
|
backupMediaSize = mediaSize,
|
||||||
|
freeTierMediaRetentionDays = mediaRetentionDays,
|
||||||
backupMediaDetails = if (RemoteConfig.internalUser || Environment.IS_STAGING) {
|
backupMediaDetails = if (RemoteConfig.internalUser || Environment.IS_STAGING) {
|
||||||
RemoteBackupsSettingsState.BackupMediaDetails(
|
RemoteBackupsSettingsState.BackupMediaDetails(
|
||||||
awaitingRestore = SignalDatabase.attachments.getRemainingRestorableAttachmentSize().bytes,
|
awaitingRestore = SignalDatabase.attachments.getRemainingRestorableAttachmentSize().bytes,
|
||||||
@@ -287,7 +295,7 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
|||||||
|
|
||||||
if (paidType is NetworkResult.Success) {
|
if (paidType is NetworkResult.Success) {
|
||||||
val remoteStorageAllowance = paidType.result.storageAllowanceBytes.bytes
|
val remoteStorageAllowance = paidType.result.storageAllowanceBytes.bytes
|
||||||
val estimatedSize = SignalDatabase.attachments.getEstimatedArchiveMediaSize().bytes
|
val estimatedSize = getBackupMediaSize(paidType.result.tier, paidType.result).first.bytes
|
||||||
|
|
||||||
if (estimatedSize + 300.mebiBytes <= remoteStorageAllowance) {
|
if (estimatedSize + 300.mebiBytes <= remoteStorageAllowance) {
|
||||||
BackupRepository.clearOutOfRemoteStorageSpaceError()
|
BackupRepository.clearOutOfRemoteStorageSpaceError()
|
||||||
@@ -303,13 +311,16 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val (mediaSize, mediaRetentionDays) = getBackupMediaSize(_state.value.tier, (_state.value.backupState as? BackupState.WithTypeAndRenewalTime)?.messageBackupsType)
|
||||||
|
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
tier = SignalStore.backup.backupTier,
|
tier = SignalStore.backup.backupTier,
|
||||||
backupsEnabled = SignalStore.backup.areBackupsEnabled,
|
backupsEnabled = SignalStore.backup.areBackupsEnabled,
|
||||||
lastBackupTimestamp = SignalStore.backup.lastBackupTime,
|
lastBackupTimestamp = SignalStore.backup.lastBackupTime,
|
||||||
canBackupMessagesJobRun = BackupMessagesConstraint.isMet(AppDependencies.application),
|
canBackupMessagesJobRun = BackupMessagesConstraint.isMet(AppDependencies.application),
|
||||||
backupMediaSize = getBackupMediaSize(),
|
backupMediaSize = mediaSize,
|
||||||
|
freeTierMediaRetentionDays = mediaRetentionDays,
|
||||||
canBackUpUsingCellular = SignalStore.backup.backupWithCellular,
|
canBackUpUsingCellular = SignalStore.backup.backupWithCellular,
|
||||||
canRestoreUsingCellular = SignalStore.backup.restoreWithCellular,
|
canRestoreUsingCellular = SignalStore.backup.restoreWithCellular,
|
||||||
isOutOfStorageSpace = BackupRepository.shouldDisplayOutOfRemoteStorageSpaceUx(),
|
isOutOfStorageSpace = BackupRepository.shouldDisplayOutOfRemoteStorageSpaceUx(),
|
||||||
@@ -320,11 +331,39 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getBackupMediaSize(): Long {
|
private fun getBackupMediaSize(tier: MessageBackupTier?, messageBackupsType: MessageBackupsType?): Pair<Long, Int> {
|
||||||
return if (SignalStore.backup.hasBackupBeenUploaded || SignalStore.backup.lastBackupTime > 0L) {
|
if (tier == null) {
|
||||||
SignalDatabase.attachments.getEstimatedArchiveMediaSize()
|
return -1L to 0
|
||||||
|
}
|
||||||
|
|
||||||
|
val mediaRetentionDays = if (messageBackupsType is MessageBackupsType.Free) {
|
||||||
|
messageBackupsType.mediaRetentionDays
|
||||||
} else {
|
} else {
|
||||||
0L
|
when (tier) {
|
||||||
|
MessageBackupTier.FREE -> {
|
||||||
|
when (val result = BackupRepository.getFreeType()) {
|
||||||
|
is NetworkResult.Success -> result.result.mediaRetentionDays
|
||||||
|
else -> RemoteConfig.messageQueueTime.milliseconds.inWholeDays.toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageBackupTier.PAID -> 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (SignalStore.backup.hasBackupBeenUploaded || SignalStore.backup.lastBackupTime > 0L) {
|
||||||
|
when (tier) {
|
||||||
|
MessageBackupTier.PAID -> SignalDatabase.attachments.getPaidEstimatedArchiveMediaSize() to -1
|
||||||
|
MessageBackupTier.FREE -> {
|
||||||
|
if (mediaRetentionDays > 0) {
|
||||||
|
SignalDatabase.attachments.getFreeEstimatedArchiveMediaSize(System.currentTimeMillis() - mediaRetentionDays.days.inWholeMilliseconds) to mediaRetentionDays
|
||||||
|
} else {
|
||||||
|
-1L to -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
0L to mediaRetentionDays
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3009,28 +3009,40 @@ class AttachmentTable(
|
|||||||
.readToList { AttachmentId(it.requireLong(ID)) }
|
.readToList { AttachmentId(it.requireLong(ID)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getEstimatedArchiveMediaSize(): Long {
|
fun getPaidEstimatedArchiveMediaSize(): Long {
|
||||||
val estimatedThumbnailCount = readableDatabase
|
return getEstimatedArchiveMediaSize()
|
||||||
.select("COUNT(*)")
|
}
|
||||||
.from(
|
|
||||||
"""
|
fun getFreeEstimatedArchiveMediaSize(afterTimestamp: Long): Long {
|
||||||
(
|
return getEstimatedArchiveMediaSize(afterTimestamp)
|
||||||
SELECT DISTINCT $DATA_HASH_END, $REMOTE_KEY
|
}
|
||||||
FROM $TABLE_NAME INNER JOIN ${MessageTable.TABLE_NAME} AS m ON $TABLE_NAME.$MESSAGE_ID = m.${MessageTable.ID}
|
|
||||||
WHERE
|
private fun getEstimatedArchiveMediaSize(afterTimestamp: Long = 0L): Long {
|
||||||
$DATA_FILE NOT NULL AND
|
val estimatedThumbnailCount = if (afterTimestamp == 0L) {
|
||||||
$DATA_HASH_END NOT NULL AND
|
readableDatabase
|
||||||
$REMOTE_KEY NOT NULL AND
|
.select("COUNT(*)")
|
||||||
$TRANSFER_STATE = $TRANSFER_PROGRESS_DONE AND
|
.from(
|
||||||
$ARCHIVE_TRANSFER_STATE != ${ArchiveTransferState.PERMANENT_FAILURE.value} AND
|
"""
|
||||||
($CONTENT_TYPE LIKE 'image/%' OR $CONTENT_TYPE LIKE 'video/%') AND
|
(
|
||||||
$CONTENT_TYPE != 'image/svg+xml' AND
|
SELECT DISTINCT $DATA_HASH_END, $REMOTE_KEY
|
||||||
${getMessageDoesNotExpireWithinTimeoutClause(tablePrefix = "m")}
|
FROM $TABLE_NAME INNER JOIN ${MessageTable.TABLE_NAME} AS m ON $TABLE_NAME.$MESSAGE_ID = m.${MessageTable.ID}
|
||||||
|
WHERE
|
||||||
|
$DATA_FILE NOT NULL AND
|
||||||
|
$DATA_HASH_END NOT NULL AND
|
||||||
|
$REMOTE_KEY NOT NULL AND
|
||||||
|
$TRANSFER_STATE = $TRANSFER_PROGRESS_DONE AND
|
||||||
|
$ARCHIVE_TRANSFER_STATE != ${ArchiveTransferState.PERMANENT_FAILURE.value} AND
|
||||||
|
($CONTENT_TYPE LIKE 'image/%' OR $CONTENT_TYPE LIKE 'video/%') AND
|
||||||
|
$CONTENT_TYPE != 'image/svg+xml' AND
|
||||||
|
${getMessageDoesNotExpireWithinTimeoutClause(tablePrefix = "m")}
|
||||||
|
)
|
||||||
|
"""
|
||||||
)
|
)
|
||||||
"""
|
.run()
|
||||||
)
|
.readToSingleLong(0L)
|
||||||
.run()
|
} else {
|
||||||
.readToSingleLong(0L)
|
0
|
||||||
|
}
|
||||||
|
|
||||||
val uploadedAttachmentBytes = readableDatabase
|
val uploadedAttachmentBytes = readableDatabase
|
||||||
.rawQuery(
|
.rawQuery(
|
||||||
@@ -3045,6 +3057,7 @@ class AttachmentTable(
|
|||||||
$REMOTE_KEY NOT NULL AND
|
$REMOTE_KEY NOT NULL AND
|
||||||
$TRANSFER_STATE = $TRANSFER_PROGRESS_DONE AND
|
$TRANSFER_STATE = $TRANSFER_PROGRESS_DONE AND
|
||||||
$ARCHIVE_TRANSFER_STATE != ${ArchiveTransferState.PERMANENT_FAILURE.value} AND
|
$ARCHIVE_TRANSFER_STATE != ${ArchiveTransferState.PERMANENT_FAILURE.value} AND
|
||||||
|
${if (afterTimestamp > 0) "m.${MessageTable.DATE_RECEIVED} >= $afterTimestamp AND" else ""}
|
||||||
${getMessageDoesNotExpireWithinTimeoutClause(tablePrefix = "m")}
|
${getMessageDoesNotExpireWithinTimeoutClause(tablePrefix = "m")}
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ class CopyAttachmentToArchiveJob private constructor(private val attachmentId: A
|
|||||||
Log.w(TAG, "[$attachmentId] Insufficient storage space! Can't upload!")
|
Log.w(TAG, "[$attachmentId] Insufficient storage space! Can't upload!")
|
||||||
val remoteStorageQuota = getServerQuota() ?: return Result.retry(defaultBackoff()).logW(TAG, "[$attachmentId] Failed to fetch server quota! Retrying.")
|
val remoteStorageQuota = getServerQuota() ?: return Result.retry(defaultBackoff()).logW(TAG, "[$attachmentId] Failed to fetch server quota! Retrying.")
|
||||||
|
|
||||||
if (SignalDatabase.attachments.getEstimatedArchiveMediaSize() > remoteStorageQuota.inWholeBytes) {
|
if (SignalDatabase.attachments.getPaidEstimatedArchiveMediaSize() > remoteStorageQuota.inWholeBytes) {
|
||||||
BackupRepository.markOutOfRemoteStorageSpaceError()
|
BackupRepository.markOutOfRemoteStorageSpaceError()
|
||||||
return Result.failure()
|
return Result.failure()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ object QuickRegistrationRepository {
|
|||||||
MessageBackupTier.FREE -> RegistrationProvisionMessage.Tier.FREE
|
MessageBackupTier.FREE -> RegistrationProvisionMessage.Tier.FREE
|
||||||
null -> null
|
null -> null
|
||||||
},
|
},
|
||||||
backupSizeBytes = SignalDatabase.attachments.getEstimatedArchiveMediaSize().takeIf { it > 0 },
|
backupSizeBytes = if (SignalStore.backup.backupTier == MessageBackupTier.PAID) SignalDatabase.attachments.getPaidEstimatedArchiveMediaSize().takeIf { it > 0 } else null,
|
||||||
restoreMethodToken = restoreMethodToken,
|
restoreMethodToken = restoreMethodToken,
|
||||||
aciIdentityKeyPublic = SignalStore.account.aciIdentityKey.publicKey.serialize().toByteString(),
|
aciIdentityKeyPublic = SignalStore.account.aciIdentityKey.publicKey.serialize().toByteString(),
|
||||||
aciIdentityKeyPrivate = SignalStore.account.aciIdentityKey.privateKey.serialize().toByteString(),
|
aciIdentityKeyPrivate = SignalStore.account.aciIdentityKey.privateKey.serialize().toByteString(),
|
||||||
|
|||||||
@@ -862,8 +862,10 @@
|
|||||||
<!-- CreateBackupBottomSheet -->
|
<!-- CreateBackupBottomSheet -->
|
||||||
<!-- Bottom sheet title -->
|
<!-- Bottom sheet title -->
|
||||||
<string name="CreateBackupBottomSheet__you_are_all_set">You\'re all set. Start your backup now.</string>
|
<string name="CreateBackupBottomSheet__you_are_all_set">You\'re all set. Start your backup now.</string>
|
||||||
<!-- Bottom sheet message -->
|
<!-- Bottom sheet paid message -->
|
||||||
<string name="CreateBackupBottomSheet__depending_on_the_size">Depending on the size of your backup, this could take a long time. You can use your phone as you normally do while the backup takes place.</string>
|
<string name="CreateBackupBottomSheet__depending_on_the_size">Depending on the size of your backup, this could take a long time. You can use your phone as you normally do while the backup takes place.</string>
|
||||||
|
<!-- Bottom sheet free message -->
|
||||||
|
<string name="CreateBackupBottomSheet__free_tier">Media is added to your backup as you send and receive messages.</string>
|
||||||
|
|
||||||
<!-- Headline text for a bottom sheet dialog shown when the restoration of the media backup fails. -->
|
<!-- Headline text for a bottom sheet dialog shown when the restoration of the media backup fails. -->
|
||||||
<string name="RestoreMediaFailedBottomSheet__Cant_restore_media">Can\'t restore media</string>
|
<string name="RestoreMediaFailedBottomSheet__Cant_restore_media">Can\'t restore media</string>
|
||||||
@@ -8188,6 +8190,13 @@
|
|||||||
<string name="RemoteBackupsSettingsFragment__to_view_your_key">To view your key, confirm it\'s you</string>
|
<string name="RemoteBackupsSettingsFragment__to_view_your_key">To view your key, confirm it\'s you</string>
|
||||||
<!-- Row label for cancelling and deleting backup -->
|
<!-- Row label for cancelling and deleting backup -->
|
||||||
<string name="RemoteBackupsSettingsFragment__turn_off_and_delete_backup">Turn off and delete backup</string>
|
<string name="RemoteBackupsSettingsFragment__turn_off_and_delete_backup">Turn off and delete backup</string>
|
||||||
|
<!-- Dialog title for explainer text to on how backup size works for free tier -->
|
||||||
|
<string name="RemoteBackupsSettingsFragment__free_tier_storage_title">Backup size</string>
|
||||||
|
<!-- Dialog message for explainer text to on how backup size works for free tier -->
|
||||||
|
<plurals name="RemoteBackupsSettingsFragment__backup_frequency_dialog_body">
|
||||||
|
<item quantity="one">Your backup includes all of your text messages and your last %1$d day of media. The size will change as new media is received and old media expires.</item>
|
||||||
|
<item quantity="other">Your backup includes all of your text messages and your last %1$d days of media. The size will change as new media is received and old media expires.</item>
|
||||||
|
</plurals>
|
||||||
<!-- Snackbar text displayed when backup has been deleted and turned off -->
|
<!-- Snackbar text displayed when backup has been deleted and turned off -->
|
||||||
<string name="RemoteBackupsSettingsFragment__backup_deleted_and_turned_off">Backup deleted and turned off.</string>
|
<string name="RemoteBackupsSettingsFragment__backup_deleted_and_turned_off">Backup deleted and turned off.</string>
|
||||||
<!-- Snackbar text displayed when backup type is downgraded -->
|
<!-- Snackbar text displayed when backup type is downgraded -->
|
||||||
@@ -8331,6 +8340,8 @@
|
|||||||
<string name="RemoteBackupsSettingsFragment__a_network_error_occurred">A network error occurred. Please check your internet connection and try again.</string>
|
<string name="RemoteBackupsSettingsFragment__a_network_error_occurred">A network error occurred. Please check your internet connection and try again.</string>
|
||||||
<!-- Progress message when backup file is being uploaded. First placeholder and second placeholder are formatted byte sizes (2 MB) and third is percent completion. -->
|
<!-- Progress message when backup file is being uploaded. First placeholder and second placeholder are formatted byte sizes (2 MB) and third is percent completion. -->
|
||||||
<string name="RemoteBackupsSettingsFragment__uploading_s_of_s_d">Uploading: %1$s of %2$s (%3$d%%)</string>
|
<string name="RemoteBackupsSettingsFragment__uploading_s_of_s_d">Uploading: %1$s of %2$s (%3$d%%)</string>
|
||||||
|
<!-- Progress message when backup file is being uploaded. Placeholder is percent completion. -->
|
||||||
|
<string name="RemoteBackupsSettingsFragment__uploading_d">Uploading: %1$d%%</string>
|
||||||
<!-- Button label to see more details about redemption error -->
|
<!-- Button label to see more details about redemption error -->
|
||||||
<string name="RemoteBackupsSettingsFragment__details">Details</string>
|
<string name="RemoteBackupsSettingsFragment__details">Details</string>
|
||||||
<!-- Text displayed when there was an error deleting backup -->
|
<!-- Text displayed when there was an error deleting backup -->
|
||||||
|
|||||||
Reference in New Issue
Block a user