Update backups bottom sheet data handling.

This commit is contained in:
Alex Hart
2024-11-06 09:31:20 -04:00
committed by Greyson Parrelli
parent 3901c52e45
commit f14f7f7478
16 changed files with 163 additions and 114 deletions

View File

@@ -125,6 +125,11 @@ object BackupRepository {
} }
} }
@JvmStatic
fun skipMediaRestore() {
// TODO [backups] -- Clear the error as necessary
}
/** /**
* Whether the yellow dot should be displayed on the conversation list avatar. * Whether the yellow dot should be displayed on the conversation list avatar.
*/ */

View File

@@ -6,7 +6,7 @@
package org.thoughtcrime.securesms.backup.v2.ui package org.thoughtcrime.securesms.backup.v2.ui
import android.os.Parcelable import android.os.Parcelable
import androidx.annotation.StringRes import androidx.appcompat.app.AlertDialog
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@@ -37,6 +37,7 @@ import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.core.os.BundleCompat import androidx.core.os.BundleCompat
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
@@ -46,6 +47,7 @@ import org.signal.core.ui.Buttons
import org.signal.core.ui.Previews import org.signal.core.ui.Previews
import org.signal.core.ui.SignalPreview import org.signal.core.ui.SignalPreview
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.billing.launchManageBackupsSubscription import org.thoughtcrime.securesms.billing.launchManageBackupsSubscription
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
@@ -63,6 +65,7 @@ class BackupAlertBottomSheet : ComposeBottomSheetDialogFragment() {
companion object { companion object {
private const val ARG_ALERT = "alert" private const val ARG_ALERT = "alert"
@JvmStatic
fun create(backupAlert: BackupAlert): BackupAlertBottomSheet { fun create(backupAlert: BackupAlert): BackupAlertBottomSheet {
return BackupAlertBottomSheet().apply { return BackupAlertBottomSheet().apply {
arguments = bundleOf(ARG_ALERT to backupAlert) arguments = bundleOf(ARG_ALERT to backupAlert)
@@ -94,17 +97,17 @@ class BackupAlertBottomSheet : ComposeBottomSheetDialogFragment() {
@Stable @Stable
private fun performPrimaryAction() { private fun performPrimaryAction() {
when (backupAlert) { when (backupAlert) {
BackupAlert.COULD_NOT_COMPLETE_BACKUP -> { BackupAlert.CouldNotCompleteBackup -> {
BackupMessagesJob.enqueue() BackupMessagesJob.enqueue()
startActivity(AppSettingsActivity.remoteBackups(requireContext())) startActivity(AppSettingsActivity.remoteBackups(requireContext()))
} }
BackupAlert.PAYMENT_PROCESSING -> launchManageBackupsSubscription() BackupAlert.FailedToRenew -> launchManageBackupsSubscription()
BackupAlert.MEDIA_BACKUPS_ARE_OFF, BackupAlert.MEDIA_WILL_BE_DELETED_TODAY -> { BackupAlert.MediaBackupsAreOff, BackupAlert.MediaWillBeDeletedToday -> {
performFullMediaDownload() performFullMediaDownload()
} }
BackupAlert.DISK_FULL -> Unit is BackupAlert.DiskFull -> Unit
} }
dismissAllowingStateLoss() dismissAllowingStateLoss()
@@ -113,20 +116,22 @@ class BackupAlertBottomSheet : ComposeBottomSheetDialogFragment() {
@Stable @Stable
private fun performSecondaryAction() { private fun performSecondaryAction() {
when (backupAlert) { when (backupAlert) {
BackupAlert.COULD_NOT_COMPLETE_BACKUP -> { BackupAlert.CouldNotCompleteBackup -> {
// TODO [backups] - Dismiss and notify later // TODO [backups] - Dismiss and notify later
} }
BackupAlert.PAYMENT_PROCESSING -> Unit BackupAlert.FailedToRenew -> Unit
BackupAlert.MEDIA_BACKUPS_ARE_OFF -> { BackupAlert.MediaBackupsAreOff -> {
// TODO [backups] - Silence and remind on last day // TODO [backups] - Silence and remind on last day
} }
BackupAlert.MEDIA_WILL_BE_DELETED_TODAY -> { BackupAlert.MediaWillBeDeletedToday -> {
displayLastChanceDialog() displayLastChanceDialog()
} }
BackupAlert.DISK_FULL -> Unit is BackupAlert.DiskFull -> {
displaySkipRestoreDialog()
}
} }
dismissAllowingStateLoss() dismissAllowingStateLoss()
@@ -143,6 +148,23 @@ class BackupAlertBottomSheet : ComposeBottomSheetDialogFragment() {
.show() .show()
} }
private fun displaySkipRestoreDialog() {
MaterialAlertDialogBuilder(requireContext())
.setTitle((R.string.BackupAlertBottomSheet__skip_restore_question))
.setMessage(R.string.BackupAlertBottomSheet__if_you_skip_restore)
.setPositiveButton(R.string.BackupAlertBottomSheet__skip) { _, _ ->
BackupRepository.skipMediaRestore()
}
.setNegativeButton(android.R.string.cancel, null)
.create()
.apply {
setOnShowListener {
getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(ContextCompat.getColor(requireContext(), R.color.signal_colorError))
}
}
.show()
}
private fun performFullMediaDownload() { private fun performFullMediaDownload() {
// TODO [backups] -- We need to force this to download everything // TODO [backups] -- We need to force this to download everything
AppDependencies.jobManager.add(BackupRestoreMediaJob()) AppDependencies.jobManager.add(BackupRestoreMediaJob())
@@ -167,7 +189,7 @@ private fun BackupAlertSheetContent(
Spacer(modifier = Modifier.size(26.dp)) Spacer(modifier = Modifier.size(26.dp))
when (backupAlert) { when (backupAlert) {
BackupAlert.PAYMENT_PROCESSING, BackupAlert.MEDIA_BACKUPS_ARE_OFF -> { BackupAlert.FailedToRenew, BackupAlert.MediaBackupsAreOff -> {
Box { Box {
Image( Image(
painter = painterResource(id = R.drawable.image_signal_backups), painter = painterResource(id = R.drawable.image_signal_backups),
@@ -200,24 +222,21 @@ private fun BackupAlertSheetContent(
} }
Text( Text(
text = stringResource(id = rememberTitleResource(backupAlert = backupAlert)), text = titleString(backupAlert = backupAlert),
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier.padding(top = 16.dp, bottom = 6.dp) modifier = Modifier.padding(top = 16.dp, bottom = 6.dp)
) )
when (backupAlert) { when (backupAlert) {
BackupAlert.COULD_NOT_COMPLETE_BACKUP -> CouldNotCompleteBackup( BackupAlert.CouldNotCompleteBackup -> CouldNotCompleteBackup(
daysSinceLastBackup = 7 // TODO [backups] daysSinceLastBackup = 7 // TODO [backups]
) )
BackupAlert.PAYMENT_PROCESSING -> PaymentProcessingBody() BackupAlert.FailedToRenew -> PaymentProcessingBody()
BackupAlert.MEDIA_BACKUPS_ARE_OFF -> MediaBackupsAreOffBody(30) // TODO [backups] -- Get this value from backend BackupAlert.MediaBackupsAreOff -> MediaBackupsAreOffBody(30) // TODO [backups] -- Get this value from backend
BackupAlert.MEDIA_WILL_BE_DELETED_TODAY -> MediaWillBeDeletedTodayBody() BackupAlert.MediaWillBeDeletedToday -> MediaWillBeDeletedTodayBody()
BackupAlert.DISK_FULL -> DiskFullBody( is BackupAlert.DiskFull -> DiskFullBody(requiredSpace = backupAlert.requiredSpace)
requiredSpace = "12 GB", // TODO [backups] Where does this value come from?
daysUntilDeletion = 30 // TODO [backups] Where does this value come from?
)
} }
val secondaryActionResource = rememberSecondaryActionResource(backupAlert = backupAlert) val secondaryActionResource = rememberSecondaryActionResource(backupAlert = backupAlert)
@@ -299,19 +318,16 @@ private fun MediaWillBeDeletedTodayBody() {
} }
@Composable @Composable
private fun DiskFullBody( private fun DiskFullBody(requiredSpace: String) {
requiredSpace: String,
daysUntilDeletion: Long
) {
Text( Text(
text = stringResource(id = R.string.BackupAlertBottomSheet__your_device_does_not_have_enough_free_space, requiredSpace), text = stringResource(id = R.string.BackupAlertBottomSheet__to_finish_downloading_your_signal_backup, requiredSpace),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(bottom = 24.dp) modifier = Modifier.padding(bottom = 24.dp)
) )
Text( Text(
text = pluralStringResource(id = R.plurals.BackupAlertBottomSheet__if_you_choose_skip, daysUntilDeletion.toInt(), daysUntilDeletion), // TODO [backups] Learn More link text = stringResource(R.string.BackupAlertBottomSheet__to_free_up_space_offload),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(bottom = 36.dp) modifier = Modifier.padding(bottom = 36.dp)
@@ -322,24 +338,21 @@ private fun DiskFullBody(
private fun rememberBackupsIconColors(backupAlert: BackupAlert): BackupsIconColors { private fun rememberBackupsIconColors(backupAlert: BackupAlert): BackupsIconColors {
return remember(backupAlert) { return remember(backupAlert) {
when (backupAlert) { when (backupAlert) {
BackupAlert.PAYMENT_PROCESSING, BackupAlert.MEDIA_BACKUPS_ARE_OFF -> error("Not icon-based options.") BackupAlert.FailedToRenew, BackupAlert.MediaBackupsAreOff -> error("Not icon-based options.")
BackupAlert.COULD_NOT_COMPLETE_BACKUP, BackupAlert.DISK_FULL -> BackupsIconColors.Warning BackupAlert.CouldNotCompleteBackup, is BackupAlert.DiskFull -> BackupsIconColors.Warning
BackupAlert.MEDIA_WILL_BE_DELETED_TODAY -> BackupsIconColors.Error BackupAlert.MediaWillBeDeletedToday -> BackupsIconColors.Error
} }
} }
} }
@Composable @Composable
@StringRes private fun titleString(backupAlert: BackupAlert): String {
private fun rememberTitleResource(backupAlert: BackupAlert): Int { return when (backupAlert) {
return remember(backupAlert) { BackupAlert.CouldNotCompleteBackup -> stringResource(R.string.BackupAlertBottomSheet__couldnt_complete_backup)
when (backupAlert) { BackupAlert.FailedToRenew -> stringResource(R.string.BackupAlertBottomSheet__your_backups_subscription_failed_to_renew)
BackupAlert.COULD_NOT_COMPLETE_BACKUP -> R.string.BackupAlertBottomSheet__couldnt_complete_backup BackupAlert.MediaBackupsAreOff -> stringResource(R.string.BackupAlertBottomSheet__your_backups_subscription_expired)
BackupAlert.PAYMENT_PROCESSING -> R.string.BackupAlertBottomSheet__your_backups_subscription_failed_to_renew BackupAlert.MediaWillBeDeletedToday -> stringResource(R.string.BackupAlertBottomSheet__your_media_will_be_deleted_today)
BackupAlert.MEDIA_BACKUPS_ARE_OFF -> R.string.BackupAlertBottomSheet__your_backups_subscription_expired is BackupAlert.DiskFull -> stringResource(R.string.BackupAlertBottomSheet__free_up_s_on_this_device, backupAlert.requiredSpace)
BackupAlert.MEDIA_WILL_BE_DELETED_TODAY -> R.string.BackupAlertBottomSheet__your_media_will_be_deleted_today
BackupAlert.DISK_FULL -> R.string.BackupAlertBottomSheet__cant_complete_download
}
} }
} }
@@ -349,11 +362,11 @@ private fun primaryActionString(
pricePerMonth: String pricePerMonth: String
): String { ): String {
return when (backupAlert) { return when (backupAlert) {
BackupAlert.COULD_NOT_COMPLETE_BACKUP -> stringResource(R.string.BackupAlertBottomSheet__back_up_now) BackupAlert.CouldNotCompleteBackup -> stringResource(R.string.BackupAlertBottomSheet__back_up_now)
BackupAlert.PAYMENT_PROCESSING -> stringResource(R.string.BackupAlertBottomSheet__manage_subscription) BackupAlert.FailedToRenew -> stringResource(R.string.BackupAlertBottomSheet__manage_subscription)
BackupAlert.MEDIA_BACKUPS_ARE_OFF -> stringResource(R.string.BackupAlertBottomSheet__subscribe_for_s_month, pricePerMonth) BackupAlert.MediaBackupsAreOff -> stringResource(R.string.BackupAlertBottomSheet__subscribe_for_s_month, pricePerMonth)
BackupAlert.MEDIA_WILL_BE_DELETED_TODAY -> stringResource(R.string.BackupAlertBottomSheet__download_media_now) BackupAlert.MediaWillBeDeletedToday -> stringResource(R.string.BackupAlertBottomSheet__download_media_now)
BackupAlert.DISK_FULL -> stringResource(android.R.string.ok) is BackupAlert.DiskFull -> stringResource(R.string.BackupAlertBottomSheet__got_it)
} }
} }
@@ -361,11 +374,11 @@ private fun primaryActionString(
private fun rememberSecondaryActionResource(backupAlert: BackupAlert): Int { private fun rememberSecondaryActionResource(backupAlert: BackupAlert): Int {
return remember(backupAlert) { return remember(backupAlert) {
when (backupAlert) { when (backupAlert) {
BackupAlert.COULD_NOT_COMPLETE_BACKUP -> android.R.string.cancel // TODO [backups] -- Finalized copy BackupAlert.CouldNotCompleteBackup -> R.string.BackupAlertBottomSheet__try_later
BackupAlert.PAYMENT_PROCESSING -> R.string.BackupAlertBottomSheet__not_now BackupAlert.FailedToRenew -> R.string.BackupAlertBottomSheet__not_now
BackupAlert.MEDIA_BACKUPS_ARE_OFF -> R.string.BackupAlertBottomSheet__not_now BackupAlert.MediaBackupsAreOff -> R.string.BackupAlertBottomSheet__not_now
BackupAlert.MEDIA_WILL_BE_DELETED_TODAY -> R.string.BackupAlertBottomSheet__dont_download_media BackupAlert.MediaWillBeDeletedToday -> R.string.BackupAlertBottomSheet__dont_download_media
BackupAlert.DISK_FULL -> R.string.BackupAlertBottomSheet__skip is BackupAlert.DiskFull -> R.string.BackupAlertBottomSheet__skip_restore
} }
} }
} }
@@ -375,7 +388,7 @@ private fun rememberSecondaryActionResource(backupAlert: BackupAlert): Int {
private fun BackupAlertSheetContentPreviewGeneric() { private fun BackupAlertSheetContentPreviewGeneric() {
Previews.BottomSheetPreview { Previews.BottomSheetPreview {
BackupAlertSheetContent( BackupAlertSheetContent(
backupAlert = BackupAlert.COULD_NOT_COMPLETE_BACKUP backupAlert = BackupAlert.CouldNotCompleteBackup
) )
} }
} }
@@ -385,7 +398,7 @@ private fun BackupAlertSheetContentPreviewGeneric() {
private fun BackupAlertSheetContentPreviewPayment() { private fun BackupAlertSheetContentPreviewPayment() {
Previews.BottomSheetPreview { Previews.BottomSheetPreview {
BackupAlertSheetContent( BackupAlertSheetContent(
backupAlert = BackupAlert.PAYMENT_PROCESSING backupAlert = BackupAlert.FailedToRenew
) )
} }
} }
@@ -395,7 +408,7 @@ private fun BackupAlertSheetContentPreviewPayment() {
private fun BackupAlertSheetContentPreviewMedia() { private fun BackupAlertSheetContentPreviewMedia() {
Previews.BottomSheetPreview { Previews.BottomSheetPreview {
BackupAlertSheetContent( BackupAlertSheetContent(
backupAlert = BackupAlert.MEDIA_BACKUPS_ARE_OFF, backupAlert = BackupAlert.MediaBackupsAreOff,
pricePerMonth = "$2.99" pricePerMonth = "$2.99"
) )
} }
@@ -406,7 +419,7 @@ private fun BackupAlertSheetContentPreviewMedia() {
private fun BackupAlertSheetContentPreviewDelete() { private fun BackupAlertSheetContentPreviewDelete() {
Previews.BottomSheetPreview { Previews.BottomSheetPreview {
BackupAlertSheetContent( BackupAlertSheetContent(
backupAlert = BackupAlert.MEDIA_WILL_BE_DELETED_TODAY backupAlert = BackupAlert.MediaWillBeDeletedToday
) )
} }
} }
@@ -416,16 +429,28 @@ private fun BackupAlertSheetContentPreviewDelete() {
private fun BackupAlertSheetContentPreviewDiskFull() { private fun BackupAlertSheetContentPreviewDiskFull() {
Previews.BottomSheetPreview { Previews.BottomSheetPreview {
BackupAlertSheetContent( BackupAlertSheetContent(
backupAlert = BackupAlert.DISK_FULL backupAlert = BackupAlert.DiskFull(requiredSpace = "12GB")
) )
} }
} }
/**
* All necessary information to display the sheet should be handed in through the specific alert.
*/
@Parcelize @Parcelize
enum class BackupAlert : Parcelable { sealed class BackupAlert : Parcelable {
COULD_NOT_COMPLETE_BACKUP,
PAYMENT_PROCESSING, data object CouldNotCompleteBackup : BackupAlert()
MEDIA_BACKUPS_ARE_OFF,
MEDIA_WILL_BE_DELETED_TODAY, data object FailedToRenew : BackupAlert()
DISK_FULL
data object MediaBackupsAreOff : BackupAlert()
data object MediaWillBeDeletedToday : BackupAlert()
/**
* The disk is full. Contains a value representing the amount of space that must be freed.
*
*/
data class DiskFull(val requiredSpace: String) : BackupAlert()
} }

View File

@@ -10,6 +10,7 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.coroutineScope import androidx.lifecycle.coroutineScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.thoughtcrime.securesms.backup.v2.BackupRepository
/** /**
* Delegate that controls whether and which backup alert sheet is displayed. * Delegate that controls whether and which backup alert sheet is displayed.
@@ -19,12 +20,12 @@ object BackupAlertDelegate {
fun delegate(fragmentManager: FragmentManager, lifecycle: Lifecycle) { fun delegate(fragmentManager: FragmentManager, lifecycle: Lifecycle) {
lifecycle.coroutineScope.launch { lifecycle.coroutineScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) { lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
// TODO [backups] if (BackupRepository.shouldDisplayBackupFailedSheet()) {
// 1. Get unnotified backup upload failures BackupAlertBottomSheet.create(BackupAlert.CouldNotCompleteBackup).show(fragmentManager, null)
// 2. Get unnotified backup download failures }
// 3. Get unnotified backup payment failures
// Decide which do display // TODO [backups]
// Get unnotified backup download failures & display sheet
} }
} }
} }

View File

@@ -57,7 +57,7 @@ private const val NONE = -1
@Composable @Composable
fun BackupStatusBanner( fun BackupStatusBanner(
data: BackupStatusData, data: BackupStatusData,
onSkipClick: () -> Unit = {}, onActionClick: (BackupStatusData) -> Unit = {},
onDismissClick: () -> Unit = {}, onDismissClick: () -> Unit = {},
contentPadding: PaddingValues = PaddingValues(horizontal = 12.dp, vertical = 8.dp) contentPadding: PaddingValues = PaddingValues(horizontal = 12.dp, vertical = 8.dp)
) { ) {
@@ -119,7 +119,7 @@ fun BackupStatusBanner(
if (data.actionRes != NONE) { if (data.actionRes != NONE) {
Buttons.Small( Buttons.Small(
onClick = onSkipClick, onClick = { onActionClick(data) },
modifier = Modifier.padding(start = 8.dp) modifier = Modifier.padding(start = 8.dp)
) { ) {
Text(text = stringResource(id = data.actionRes)) Text(text = stringResource(id = data.actionRes))
@@ -250,7 +250,7 @@ sealed interface BackupStatusData {
get() = stringResource(R.string.BackupStatus__free_up_s_of_space_to_download_your_media, requiredSpace) get() = stringResource(R.string.BackupStatus__free_up_s_of_space_to_download_your_media, requiredSpace)
override val iconColors: BackupsIconColors = BackupsIconColors.Warning override val iconColors: BackupsIconColors = BackupsIconColors.Warning
override val actionRes: Int = R.string.registration_activity__skip override val actionRes: Int = R.string.BackupStatus__details
} }
/** /**

View File

@@ -73,7 +73,7 @@ class MediaRestoreProgressBanner(private val listener: RestoreProgressBannerList
override fun DisplayBanner(model: BackupStatusData, contentPadding: PaddingValues) { override fun DisplayBanner(model: BackupStatusData, contentPadding: PaddingValues) {
BackupStatusBanner( BackupStatusBanner(
data = model, data = model,
onSkipClick = listener::onSkip, onActionClick = listener::onActionClick,
onDismissClick = listener::onDismissComplete onDismissClick = listener::onDismissComplete
) )
} }
@@ -124,12 +124,12 @@ class MediaRestoreProgressBanner(private val listener: RestoreProgressBannerList
} }
interface RestoreProgressBannerListener { interface RestoreProgressBannerListener {
fun onSkip() fun onActionClick(data: BackupStatusData)
fun onDismissComplete() fun onDismissComplete()
} }
private object EmptyListener : RestoreProgressBannerListener { private object EmptyListener : RestoreProgressBannerListener {
override fun onSkip() = Unit override fun onActionClick(data: BackupStatusData) = Unit
override fun onDismissComplete() = Unit override fun onDismissComplete() = Unit
} }
} }

View File

@@ -119,7 +119,7 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
} }
fun skipMediaRestore() { fun skipMediaRestore() {
// TODO [backups] -- Clear the error as necessary BackupRepository.skipMediaRestore()
} }
fun cancelMediaRestore() { fun cancelMediaRestore() {

View File

@@ -5,7 +5,6 @@ import org.signal.donations.InAppPaymentType
import org.signal.donations.PaymentSourceType import org.signal.donations.PaymentSourceType
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.database.model.InAppPaymentReceiptRecord import org.thoughtcrime.securesms.database.model.InAppPaymentReceiptRecord
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.Environment import org.thoughtcrime.securesms.util.Environment
import org.thoughtcrime.securesms.util.LocaleRemoteConfig import org.thoughtcrime.securesms.util.LocaleRemoteConfig
@@ -28,9 +27,9 @@ object InAppDonations {
return isCreditCardAvailable() || isPayPalAvailable() || isGooglePayAvailable() || isSEPADebitAvailable() || isIDEALAvailable() return isCreditCardAvailable() || isPayPalAvailable() || isGooglePayAvailable() || isSEPADebitAvailable() || isIDEALAvailable()
} }
fun isPaymentSourceAvailable(paymentSourceType: PaymentSourceType, inAppPaymentType: InAppPaymentType): Boolean { fun isDonationsPaymentSourceAvailable(paymentSourceType: PaymentSourceType, inAppPaymentType: InAppPaymentType): Boolean {
if (inAppPaymentType == InAppPaymentType.RECURRING_BACKUP) { if (inAppPaymentType == InAppPaymentType.RECURRING_BACKUP) {
return paymentSourceType == PaymentSourceType.GooglePlayBilling && AppDependencies.billingApi.isApiAvailable() error("Not supported.")
} }
return when (paymentSourceType) { return when (paymentSourceType) {

View File

@@ -134,9 +134,9 @@ class InAppPaymentsBottomSheetDelegate(
}.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribeBy { inAppPayments -> }.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribeBy { inAppPayments ->
for (payment in inAppPayments) { for (payment in inAppPayments) {
if (isPaymentProcessingError(payment.state, payment.data)) { if (isPaymentProcessingError(payment.state, payment.data)) {
BackupAlertBottomSheet.create(BackupAlert.COULD_NOT_COMPLETE_BACKUP).show(fragmentManager, null) BackupAlertBottomSheet.create(BackupAlert.CouldNotCompleteBackup).show(fragmentManager, null)
} else if (isUnexpectedCancellation(payment.state, payment.data)) { } else if (isUnexpectedCancellation(payment.state, payment.data)) {
BackupAlertBottomSheet.create(BackupAlert.MEDIA_BACKUPS_ARE_OFF).show(fragmentManager, null) BackupAlertBottomSheet.create(BackupAlert.MediaBackupsAreOff).show(fragmentManager, null)
} }
} }
} }
@@ -146,7 +146,7 @@ class InAppPaymentsBottomSheetDelegate(
InAppPaymentsRepository.getExpiredBackupDeletionState() InAppPaymentsRepository.getExpiredBackupDeletionState()
}.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribeBy { }.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribeBy {
if (it == InAppPaymentsRepository.ExpiredBackupDeletionState.DELETE_TODAY) { if (it == InAppPaymentsRepository.ExpiredBackupDeletionState.DELETE_TODAY) {
BackupAlertBottomSheet.create(BackupAlert.MEDIA_WILL_BE_DELETED_TODAY).show(fragmentManager, null) BackupAlertBottomSheet.create(BackupAlert.MediaWillBeDeletedToday).show(fragmentManager, null)
} }
} }
} }

View File

@@ -101,7 +101,7 @@ class InAppPaymentCheckoutDelegate(
} }
fun handleGatewaySelectionResponse(inAppPayment: InAppPaymentTable.InAppPayment) { fun handleGatewaySelectionResponse(inAppPayment: InAppPaymentTable.InAppPayment) {
if (InAppDonations.isPaymentSourceAvailable(inAppPayment.data.paymentMethodType.toPaymentSourceType(), inAppPayment.type)) { if (InAppDonations.isDonationsPaymentSourceAvailable(inAppPayment.data.paymentMethodType.toPaymentSourceType(), inAppPayment.type)) {
when (inAppPayment.data.paymentMethodType) { when (inAppPayment.data.paymentMethodType) {
InAppPaymentData.PaymentMethodType.GOOGLE_PAY -> launchGooglePay(inAppPayment) InAppPaymentData.PaymentMethodType.GOOGLE_PAY -> launchGooglePay(inAppPayment)
InAppPaymentData.PaymentMethodType.PAYPAL -> launchPayPal(inAppPayment) InAppPaymentData.PaymentMethodType.PAYPAL -> launchPayPal(inAppPayment)

View File

@@ -26,11 +26,11 @@ class GatewaySelectorViewModel(
GatewaySelectorState( GatewaySelectorState(
gatewayOrderStrategy = GatewayOrderStrategy.getStrategy(), gatewayOrderStrategy = GatewayOrderStrategy.getStrategy(),
inAppPayment = args.inAppPayment, inAppPayment = args.inAppPayment,
isCreditCardAvailable = InAppDonations.isPaymentSourceAvailable(PaymentSourceType.Stripe.CreditCard, args.inAppPayment.type), isCreditCardAvailable = InAppDonations.isDonationsPaymentSourceAvailable(PaymentSourceType.Stripe.CreditCard, args.inAppPayment.type),
isGooglePayAvailable = InAppDonations.isPaymentSourceAvailable(PaymentSourceType.Stripe.GooglePay, args.inAppPayment.type), isGooglePayAvailable = InAppDonations.isDonationsPaymentSourceAvailable(PaymentSourceType.Stripe.GooglePay, args.inAppPayment.type),
isPayPalAvailable = InAppDonations.isPaymentSourceAvailable(PaymentSourceType.PayPal, args.inAppPayment.type), isPayPalAvailable = InAppDonations.isDonationsPaymentSourceAvailable(PaymentSourceType.PayPal, args.inAppPayment.type),
isSEPADebitAvailable = InAppDonations.isPaymentSourceAvailable(PaymentSourceType.Stripe.SEPADebit, args.inAppPayment.type), isSEPADebitAvailable = InAppDonations.isDonationsPaymentSourceAvailable(PaymentSourceType.Stripe.SEPADebit, args.inAppPayment.type),
isIDEALAvailable = InAppDonations.isPaymentSourceAvailable(PaymentSourceType.Stripe.IDEAL, args.inAppPayment.type) isIDEALAvailable = InAppDonations.isDonationsPaymentSourceAvailable(PaymentSourceType.Stripe.IDEAL, args.inAppPayment.type)
) )
) )
private val disposables = CompositeDisposable() private val disposables = CompositeDisposable()

View File

@@ -33,7 +33,6 @@ import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Parcelable;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
@@ -94,11 +93,10 @@ import org.thoughtcrime.securesms.MainNavigator;
import org.thoughtcrime.securesms.MuteDialog; import org.thoughtcrime.securesms.MuteDialog;
import org.thoughtcrime.securesms.NewConversationActivity; import org.thoughtcrime.securesms.NewConversationActivity;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.backup.v2.ArchiveValidator;
import org.thoughtcrime.securesms.backup.v2.BackupRepository;
import org.thoughtcrime.securesms.backup.v2.ui.BackupAlert; import org.thoughtcrime.securesms.backup.v2.ui.BackupAlert;
import org.thoughtcrime.securesms.backup.v2.ui.BackupAlertBottomSheet; import org.thoughtcrime.securesms.backup.v2.ui.BackupAlertBottomSheet;
import org.thoughtcrime.securesms.backup.v2.ui.BackupAlertDelegate; import org.thoughtcrime.securesms.backup.v2.ui.BackupAlertDelegate;
import org.thoughtcrime.securesms.backup.v2.ui.status.BackupStatusData;
import org.thoughtcrime.securesms.badges.models.Badge; import org.thoughtcrime.securesms.badges.models.Badge;
import org.thoughtcrime.securesms.badges.self.expired.ExpiredOneTimeBadgeBottomSheetDialogFragment; import org.thoughtcrime.securesms.badges.self.expired.ExpiredOneTimeBadgeBottomSheetDialogFragment;
import org.thoughtcrime.securesms.badges.self.expired.MonthlyDonationCanceledBottomSheetDialogFragment; import org.thoughtcrime.securesms.badges.self.expired.MonthlyDonationCanceledBottomSheetDialogFragment;
@@ -163,7 +161,6 @@ import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile; import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile;
import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.profiles.manage.UsernameEditFragment; import org.thoughtcrime.securesms.profiles.manage.UsernameEditFragment;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.ratelimit.RecaptchaProofBottomSheetFragment; import org.thoughtcrime.securesms.ratelimit.RecaptchaProofBottomSheetFragment;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
@@ -178,12 +175,10 @@ import org.thoughtcrime.securesms.util.AppStartup;
import org.thoughtcrime.securesms.util.BottomSheetUtil; import org.thoughtcrime.securesms.util.BottomSheetUtil;
import org.thoughtcrime.securesms.util.CachedInflater; import org.thoughtcrime.securesms.util.CachedInflater;
import org.thoughtcrime.securesms.util.ConversationUtil; import org.thoughtcrime.securesms.util.ConversationUtil;
import org.thoughtcrime.securesms.util.RemoteConfig;
import org.thoughtcrime.securesms.util.ServiceUtil; import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.SignalLocalMetrics; import org.thoughtcrime.securesms.util.SignalLocalMetrics;
import org.thoughtcrime.securesms.util.SignalProxyUtil; import org.thoughtcrime.securesms.util.SignalProxyUtil;
import org.thoughtcrime.securesms.util.SnapToTopDataObserver; import org.thoughtcrime.securesms.util.SnapToTopDataObserver;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.WindowUtil; import org.thoughtcrime.securesms.util.WindowUtil;
import org.thoughtcrime.securesms.util.adapter.mapping.PagingMappingAdapter; import org.thoughtcrime.securesms.util.adapter.mapping.PagingMappingAdapter;
@@ -193,10 +188,6 @@ import org.thoughtcrime.securesms.util.views.Stub;
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper; import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState; import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
@@ -581,10 +572,6 @@ public class ConversationListFragment extends MainFragment implements ActionMode
if (this.bannerManager != null) { if (this.bannerManager != null) {
this.bannerManager.updateContent(bannerView.get()); this.bannerManager.updateContent(bannerView.get());
} }
if (BackupRepository.shouldDisplayBackupFailedSheet()) {
BackupAlertBottomSheet.Companion.create(BackupAlert.COULD_NOT_COMPLETE_BACKUP).show(getParentFragmentManager(), null);
}
} }
@Override @Override
@@ -928,8 +915,11 @@ public class ConversationListFragment extends MainFragment implements ActionMode
}), }),
new MediaRestoreProgressBanner(new MediaRestoreProgressBanner.RestoreProgressBannerListener() { new MediaRestoreProgressBanner(new MediaRestoreProgressBanner.RestoreProgressBannerListener() {
@Override @Override
public void onSkip() { public void onActionClick(@NonNull BackupStatusData backupStatusData) {
// TODO [backups] add skip restore ability if (backupStatusData instanceof BackupStatusData.NotEnoughFreeSpace) {
BackupAlertBottomSheet.create(new BackupAlert.DiskFull(((BackupStatusData.NotEnoughFreeSpace) backupStatusData).getRequiredSpace()))
.show(getParentFragmentManager(), null);
}
} }
@Override @Override

View File

@@ -6,20 +6,31 @@
package org.thoughtcrime.securesms.dependencies package org.thoughtcrime.securesms.dependencies
import android.content.Context import android.content.Context
import org.signal.billing.BillingError
import org.signal.core.util.billing.BillingDependencies import org.signal.core.util.billing.BillingDependencies
import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration
import java.util.Locale
/** /**
* Dependency object for Google Play Billing. * Dependency object for Google Play Billing.
*/ */
object GooglePlayBillingDependencies : BillingDependencies { object GooglePlayBillingDependencies : BillingDependencies {
private const val BILLING_PRODUCT_ID_NOT_AVAILABLE = -1000
override val context: Context get() = AppDependencies.application override val context: Context get() = AppDependencies.application
override suspend fun getProductId(): String { override suspend fun getProductId(): String {
return "backup" // TODO [backups] This really shouldn't be hardcoded into the app. val config = AppDependencies.donationsService.getDonationsConfiguration(Locale.getDefault())
if (config.result.isPresent) {
return config.result.get().backupConfiguration.backupLevelConfigurationMap[SubscriptionsConfiguration.BACKUPS_LEVEL]?.playProductId ?: throw BillingError(BILLING_PRODUCT_ID_NOT_AVAILABLE)
} else {
throw BillingError(BILLING_PRODUCT_ID_NOT_AVAILABLE)
}
} }
override suspend fun getBasePlanId(): String { override suspend fun getBasePlanId(): String {
return "monthly" // TODO [backups] This really shouldn't be hardcoded into the app. return "monthly"
} }
} }

View File

@@ -7431,15 +7431,12 @@
<string name="BackupAlertBottomSheet__your_media_will_be_deleted_today">Your media will be deleted today</string> <string name="BackupAlertBottomSheet__your_media_will_be_deleted_today">Your media will be deleted today</string>
<!-- Sheet body part 1 when user\'s media will be deleted today --> <!-- Sheet body part 1 when user\'s media will be deleted today -->
<string name="BackupAlertBottomSheet__your_signal_media_backup_plan_has_been">Your Signal media backup plan has been canceled because we couldn\'t process your payment. This is your last chance to download the media in your backup before it is deleted.</string> <string name="BackupAlertBottomSheet__your_signal_media_backup_plan_has_been">Your Signal media backup plan has been canceled because we couldn\'t process your payment. This is your last chance to download the media in your backup before it is deleted.</string>
<!-- Sheet title when user does not have enough space to download their backup --> <!-- Sheet title when user does not have enough space to download their backup. Placeholder is formatted byte size, for example 12GB. -->
<string name="BackupAlertBottomSheet__cant_complete_download">Can\'t complete download</string> <string name="BackupAlertBottomSheet__free_up_s_on_this_device">Free up %1$s on this device</string>
<!-- Sheet body part 1 when user does not have enough space to download their backup. Placeholder is the amount of space needed. --> <!-- Sheet body part 1 when user does not have enough space to download their backup. Placeholder is the amount of space needed. -->
<string name="BackupAlertBottomSheet__your_device_does_not_have_enough_free_space">Your device does not have enough free space. Free up %1$s of space to download the media stored in your backup.</string> <string name="BackupAlertBottomSheet__to_finish_downloading_your_signal_backup">To finish downloading your Signal Backup your device needs %1$s of storage space.</string>
<!-- Sheet body part 2 when user does not have enough space to download their backup. Placeholder is the number of days until deletion --> <!-- Sheet body part 2 when user does not have enough space to download their backup. -->
<plurals name="BackupAlertBottomSheet__if_you_choose_skip"> <string name="BackupAlertBottomSheet__to_free_up_space_offload">To free up space offload or delete unused apps or content large in file size.</string>
<item quantity="one">If you choose \"Skip\" the media in your backup will be deleted in %1$d day.</item>
<item quantity="other">If you choose \"Skip\" the media in your backup will be deleted in %1$d days.</item>
</plurals>
<!-- Sheet title when user payment failed to process --> <!-- Sheet title when user payment failed to process -->
<string name="BackupAlertBottomSheet__your_backups_subscription_failed_to_renew">Your backups subscription failed to renew</string> <string name="BackupAlertBottomSheet__your_backups_subscription_failed_to_renew">Your backups subscription failed to renew</string>
<!-- Sheet body when user payment failed to process --> <!-- Sheet body when user payment failed to process -->
@@ -7451,9 +7448,11 @@
<!-- Clickable text to learn more about the content of this bottom sheet --> <!-- Clickable text to learn more about the content of this bottom sheet -->
<string name="BackupAlertBottomSheet__learn_more">Learn more</string> <string name="BackupAlertBottomSheet__learn_more">Learn more</string>
<!-- Secondary action button text when user does not have enough free space to download their backup. --> <!-- Secondary action button text when user does not have enough free space to download their backup. -->
<string name="BackupAlertBottomSheet__skip">Skip</string> <string name="BackupAlertBottomSheet__skip_restore">Skip restore</string>
<!-- Primary action button to start backup immediately --> <!-- Primary action button to start backup immediately -->
<string name="BackupAlertBottomSheet__back_up_now">Back up now</string> <string name="BackupAlertBottomSheet__back_up_now">Back up now</string>
<!-- Primary action button when user doesn't have enough space -->
<string name="BackupAlertBottomSheet__got_it">Got it</string>
<!-- Primary action button to manage subscription in Google Play --> <!-- Primary action button to manage subscription in Google Play -->
<string name="BackupAlertBottomSheet__manage_subscription">Manage subscription</string> <string name="BackupAlertBottomSheet__manage_subscription">Manage subscription</string>
<!-- Primary action button text prompting user to subscriber. Placeholder is formatted price --> <!-- Primary action button text prompting user to subscriber. Placeholder is formatted price -->
@@ -7464,6 +7463,8 @@
<string name="BackupAlertBottomSheet__dont_download_media">Don\'t download media</string> <string name="BackupAlertBottomSheet__dont_download_media">Don\'t download media</string>
<!-- Secondary generic action button to dismiss sheet without performing an action --> <!-- Secondary generic action button to dismiss sheet without performing an action -->
<string name="BackupAlertBottomSheet__not_now">Not now</string> <string name="BackupAlertBottomSheet__not_now">Not now</string>
<!-- Secondary action button to dismiss could not complete backup sheet -->
<string name="BackupAlertBottomSheet__try_later">Try later</string>
<!-- Dialog title for last chance to download backup --> <!-- Dialog title for last chance to download backup -->
<string name="BackupAlertBottomSheet__media_will_be_deleted">Media will be deleted</string> <string name="BackupAlertBottomSheet__media_will_be_deleted">Media will be deleted</string>
<!-- Dialog message for last chance to download backup --> <!-- Dialog message for last chance to download backup -->
@@ -7472,6 +7473,12 @@
<string name="BackupAlertBottomSheet__download">Download</string> <string name="BackupAlertBottomSheet__download">Download</string>
<!-- Dialog action to not download now --> <!-- Dialog action to not download now -->
<string name="BackupAlertBottomSheet__dont_download">Don\'t download</string> <string name="BackupAlertBottomSheet__dont_download">Don\'t download</string>
<!-- Dialog action to skip media download -->
<string name="BackupAlertBottomSheet__skip">Skip</string>
<!-- Dialog title for skipping media restore -->
<string name="BackupAlertBottomSheet__skip_restore_question">Skip restore?</string>
<!-- Dialog text for skipping media restore -->
<string name="BackupAlertBottomSheet__if_you_skip_restore">If you skip restore the remaining media and attachments in your backup will be deleted the next time your device completes a new backup.</string>
<!-- BackupStatus --> <!-- BackupStatus -->
<!-- Status title when user does not have enough free space to download their media. Placeholder is required disk space. --> <!-- Status title when user does not have enough free space to download their media. Placeholder is required disk space. -->
@@ -7482,6 +7489,8 @@
<string name="BackupStatus__restore_paused">Restore paused</string> <string name="BackupStatus__restore_paused">Restore paused</string>
<!-- Status title for banner when user has completed restoring restore media from a backup --> <!-- Status title for banner when user has completed restoring restore media from a backup -->
<string name="BackupStatus__restore_complete">Restore complete</string> <string name="BackupStatus__restore_complete">Restore complete</string>
<!-- Status action label for seeing more details about storage space -->
<string name="BackupStatus__details">Details</string>
<!-- Status subtitle for banner when restoring media pauses for Wi-Fi --> <!-- Status subtitle for banner when restoring media pauses for Wi-Fi -->
<string name="BackupStatus__status_waiting_for_wifi">Waiting for Wi-Fi…</string> <string name="BackupStatus__status_waiting_for_wifi">Waiting for Wi-Fi…</string>

View File

@@ -249,8 +249,10 @@ internal class BillingApiImpl(
* Returns whether or not subscriptions are supported by a user's device. Lack of subscription support is generally due * Returns whether or not subscriptions are supported by a user's device. Lack of subscription support is generally due
* to out-of-date Google Play API * to out-of-date Google Play API
*/ */
override fun isApiAvailable(): Boolean { override suspend fun isApiAvailable(): Boolean {
return billingClient.isFeatureSupported(BillingClient.FeatureType.SUBSCRIPTIONS).responseCode == BillingResponseCode.OK return doOnConnectionReady {
billingClient.isFeatureSupported(BillingClient.FeatureType.SUBSCRIPTIONS).responseCode == BillingResponseCode.OK
}
} }
private suspend fun queryProductsInternal(): ProductDetailsResult { private suspend fun queryProductsInternal(): ProductDetailsResult {

View File

@@ -19,7 +19,7 @@ interface BillingApi {
*/ */
fun getBillingPurchaseResults(): Flow<BillingPurchaseResult> = emptyFlow() fun getBillingPurchaseResults(): Flow<BillingPurchaseResult> = emptyFlow()
fun isApiAvailable(): Boolean = false suspend fun isApiAvailable(): Boolean = false
suspend fun queryProduct(): BillingProduct? = null suspend fun queryProduct(): BillingProduct? = null

View File

@@ -105,9 +105,16 @@ public class SubscriptionsConfiguration {
@JsonProperty("storageAllowanceBytes") @JsonProperty("storageAllowanceBytes")
private long storageAllowanceBytes; private long storageAllowanceBytes;
@JsonProperty("playProductId")
private String playProductId;
public long getStorageAllowanceBytes() { public long getStorageAllowanceBytes() {
return storageAllowanceBytes; return storageAllowanceBytes;
} }
public String getPlayProductId() {
return playProductId;
}
} }
public Map<String, CurrencyConfiguration> getCurrencies() { public Map<String, CurrencyConfiguration> getCurrencies() {