mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 04:58:45 +00:00
Add new dialog and sheet for handling offloaded media after a subscription is canceled or expires.
This commit is contained in:
committed by
Greyson Parrelli
parent
18b5354944
commit
1424dd6892
@@ -9,6 +9,7 @@ import android.database.Cursor
|
||||
import android.os.Environment
|
||||
import android.os.StatFs
|
||||
import androidx.annotation.WorkerThread
|
||||
import kotlinx.coroutines.withContext
|
||||
import okio.ByteString
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
@@ -19,6 +20,7 @@ import org.signal.core.util.EventTimer
|
||||
import org.signal.core.util.Stopwatch
|
||||
import org.signal.core.util.bytes
|
||||
import org.signal.core.util.concurrent.LimitedWorker
|
||||
import org.signal.core.util.concurrent.SignalDispatchers
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.forceForeignKeyConstraintsEnabled
|
||||
import org.signal.core.util.fullWalCheckpoint
|
||||
@@ -58,6 +60,7 @@ import org.thoughtcrime.securesms.backup.v2.stream.EncryptedBackupReader
|
||||
import org.thoughtcrime.securesms.backup.v2.stream.EncryptedBackupWriter
|
||||
import org.thoughtcrime.securesms.backup.v2.stream.PlainTextBackupReader
|
||||
import org.thoughtcrime.securesms.backup.v2.stream.PlainTextBackupWriter
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.BackupAlert
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider
|
||||
@@ -124,8 +127,8 @@ import java.util.Currency
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
object BackupRepository {
|
||||
|
||||
@@ -391,45 +394,43 @@ object BackupRepository {
|
||||
return SignalStore.backup.hasBackupBeenUploaded && System.currentTimeMillis().milliseconds > SignalStore.backup.nextBackupFailureSheetSnoozeTime
|
||||
}
|
||||
|
||||
fun snoozeYourMediaWillBeDeletedTodaySheet() {
|
||||
SignalStore.backup.lastCheckInSnoozeMillis = System.currentTimeMillis()
|
||||
fun snoozeDownloadYourBackupData() {
|
||||
SignalStore.backup.snoozeDownloadNotifier()
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the "Your media will be deleted today" sheet should be displayed.
|
||||
*/
|
||||
suspend fun shouldDisplayYourMediaWillBeDeletedTodaySheet(): Boolean {
|
||||
if (shouldNotDisplayBackupFailedMessaging() || !SignalStore.backup.hasBackupBeenUploaded || !SignalStore.backup.optimizeStorage) {
|
||||
return false
|
||||
suspend fun getDownloadYourBackupData(): BackupAlert.DownloadYourBackupData? {
|
||||
if (shouldNotDisplayBackupFailedMessaging()) {
|
||||
return null
|
||||
}
|
||||
|
||||
val paidType = try {
|
||||
getPaidType()
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Failed to retrieve paid type.", e)
|
||||
return false
|
||||
val state = SignalStore.backup.backupDownloadNotifierState ?: return null
|
||||
val nextSheetDisplayTime = state.lastSheetDisplaySeconds.seconds + state.intervalSeconds.seconds
|
||||
|
||||
val remainingAttachmentSize = withContext(SignalDispatchers.IO) {
|
||||
SignalDatabase.attachments.getRemainingRestorableAttachmentSize()
|
||||
}
|
||||
|
||||
if (paidType == null) {
|
||||
Log.w(TAG, "Paid type is not available on this device.")
|
||||
return false
|
||||
if (remainingAttachmentSize <= 0L) {
|
||||
SignalStore.backup.clearDownloadNotifierState()
|
||||
return null
|
||||
}
|
||||
|
||||
val lastCheckIn = SignalStore.backup.lastCheckInMillis.milliseconds
|
||||
if (lastCheckIn == 0.milliseconds) {
|
||||
Log.w(TAG, "LastCheckIn has not yet been set.")
|
||||
return false
|
||||
}
|
||||
|
||||
val lastSnoozeTime = SignalStore.backup.lastCheckInSnoozeMillis.milliseconds
|
||||
val now = System.currentTimeMillis().milliseconds
|
||||
val mediaTtl = paidType.mediaTtl
|
||||
val mediaExpiration = lastCheckIn + mediaTtl
|
||||
|
||||
val isNowAfterSnooze = now < lastSnoozeTime || now >= lastSnoozeTime + 4.hours
|
||||
val isNowWithin24HoursOfMediaExpiration = now < mediaExpiration && (mediaExpiration - now) <= 1.days
|
||||
return if (nextSheetDisplayTime <= now) {
|
||||
val lastDay = state.entitlementExpirationSeconds.seconds - 1.days
|
||||
|
||||
return isNowAfterSnooze && isNowWithin24HoursOfMediaExpiration
|
||||
BackupAlert.DownloadYourBackupData(
|
||||
isLastDay = now >= lastDay,
|
||||
formattedSize = remainingAttachmentSize.bytes.toUnitString(),
|
||||
type = state.type
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun shouldNotDisplayBackupFailedMessaging(): Boolean {
|
||||
@@ -1088,6 +1089,7 @@ object BackupRepository {
|
||||
SignalStore.backup.backupTier = MessageBackupTier.PAID
|
||||
SignalStore.backup.lastCheckInMillis = System.currentTimeMillis()
|
||||
SignalStore.backup.lastCheckInSnoozeMillis = 0
|
||||
SignalStore.backup.clearDownloadNotifierState()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -56,6 +56,7 @@ import org.thoughtcrime.securesms.billing.launchManageBackupsSubscription
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
||||
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.jobs.BackupMessagesJob
|
||||
import org.thoughtcrime.securesms.keyvalue.protos.BackupDownloadNotifierState
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.PlayStoreUtil
|
||||
import org.signal.core.ui.R as CoreUiR
|
||||
@@ -110,7 +111,7 @@ class BackupAlertBottomSheet : ComposeBottomSheetDialogFragment() {
|
||||
BackupAlert.FailedToRenew -> launchManageBackupsSubscription()
|
||||
is BackupAlert.MediaBackupsAreOff -> error("Use MediaBackupsAreOffBottomSheet instead.")
|
||||
|
||||
BackupAlert.MediaWillBeDeletedToday -> {
|
||||
is BackupAlert.DownloadYourBackupData -> {
|
||||
performFullMediaDownload()
|
||||
}
|
||||
|
||||
@@ -134,10 +135,7 @@ class BackupAlertBottomSheet : ComposeBottomSheetDialogFragment() {
|
||||
is BackupAlert.CouldNotCompleteBackup -> Unit
|
||||
BackupAlert.FailedToRenew -> Unit
|
||||
is BackupAlert.MediaBackupsAreOff -> error("Use MediaBackupsAreOffBottomSheet instead.")
|
||||
BackupAlert.MediaWillBeDeletedToday -> {
|
||||
displayLastChanceDialog()
|
||||
}
|
||||
|
||||
is BackupAlert.DownloadYourBackupData -> Unit
|
||||
is BackupAlert.DiskFull -> {
|
||||
displaySkipRestoreDialog()
|
||||
}
|
||||
@@ -153,23 +151,12 @@ class BackupAlertBottomSheet : ComposeBottomSheetDialogFragment() {
|
||||
|
||||
when (backupAlert) {
|
||||
is BackupAlert.CouldNotCompleteBackup, BackupAlert.BackupFailed -> BackupRepository.markBackupFailedSheetDismissed()
|
||||
is BackupAlert.MediaWillBeDeletedToday -> BackupRepository.snoozeYourMediaWillBeDeletedTodaySheet()
|
||||
is BackupAlert.DownloadYourBackupData -> BackupRepository.snoozeDownloadYourBackupData()
|
||||
is BackupAlert.ExpiredAndDowngraded -> BackupRepository.markBackupExpiredAndDowngradedSheetDismissed()
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
private fun displayLastChanceDialog() {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.BackupAlertBottomSheet__media_will_be_deleted)
|
||||
.setMessage(R.string.BackupAlertBottomSheet__the_media_stored_in_your_backup)
|
||||
.setPositiveButton(R.string.BackupAlertBottomSheet__download) { _, _ ->
|
||||
performFullMediaDownload()
|
||||
}
|
||||
.setNegativeButton(R.string.BackupAlertBottomSheet__dont_download, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun displaySkipRestoreDialog() {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle((R.string.BackupAlertBottomSheet__skip_restore_question))
|
||||
@@ -255,7 +242,7 @@ fun BackupAlertSheetContent(
|
||||
)
|
||||
|
||||
BackupAlert.FailedToRenew -> PaymentProcessingBody()
|
||||
BackupAlert.MediaWillBeDeletedToday -> MediaWillBeDeletedTodayBody()
|
||||
is BackupAlert.DownloadYourBackupData -> DownloadYourBackupData(backupAlert.formattedSize)
|
||||
is BackupAlert.DiskFull -> DiskFullBody(requiredSpace = backupAlert.requiredSpace)
|
||||
BackupAlert.BackupFailed -> BackupFailedBody()
|
||||
BackupAlert.CouldNotRedeemBackup -> CouldNotRedeemBackup()
|
||||
@@ -377,9 +364,9 @@ private fun PaymentProcessingBody() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MediaWillBeDeletedTodayBody() {
|
||||
private fun DownloadYourBackupData(formattedSize: String) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.BackupAlertBottomSheet__your_signal_media_backup_plan_has_been),
|
||||
text = stringResource(id = R.string.BackupAlertBottomSheet__you_have_s_of_media_thats_not_on_this_device, formattedSize),
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(bottom = 24.dp)
|
||||
@@ -426,7 +413,7 @@ private fun rememberBackupsIconColors(backupAlert: BackupAlert): BackupsIconColo
|
||||
when (backupAlert) {
|
||||
BackupAlert.ExpiredAndDowngraded, BackupAlert.FailedToRenew, is BackupAlert.MediaBackupsAreOff -> error("Not icon-based options.")
|
||||
is BackupAlert.CouldNotCompleteBackup, BackupAlert.BackupFailed, is BackupAlert.DiskFull, BackupAlert.CouldNotRedeemBackup -> BackupsIconColors.Warning
|
||||
BackupAlert.MediaWillBeDeletedToday -> BackupsIconColors.Error
|
||||
is BackupAlert.DownloadYourBackupData -> BackupsIconColors.Error
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -437,7 +424,13 @@ private fun titleString(backupAlert: BackupAlert): String {
|
||||
is BackupAlert.CouldNotCompleteBackup -> stringResource(R.string.BackupAlertBottomSheet__couldnt_complete_backup)
|
||||
BackupAlert.FailedToRenew -> stringResource(R.string.BackupAlertBottomSheet__your_backups_subscription_failed_to_renew)
|
||||
is BackupAlert.MediaBackupsAreOff -> error("Use MediaBackupsAreOffBottomSheet instead.")
|
||||
BackupAlert.MediaWillBeDeletedToday -> stringResource(R.string.BackupAlertBottomSheet__your_media_will_be_deleted_today)
|
||||
is BackupAlert.DownloadYourBackupData -> {
|
||||
if (backupAlert.isLastDay) {
|
||||
stringResource(R.string.BackupAlertBottomSheet__download_your_backup_data_today)
|
||||
} else {
|
||||
stringResource(R.string.BackupAlertBottomSheet__download_your_backup_data)
|
||||
}
|
||||
}
|
||||
is BackupAlert.DiskFull -> stringResource(R.string.BackupAlertBottomSheet__free_up_s_on_this_device, backupAlert.requiredSpace)
|
||||
BackupAlert.BackupFailed -> stringResource(R.string.BackupAlertBottomSheet__backup_failed)
|
||||
BackupAlert.CouldNotRedeemBackup -> stringResource(R.string.BackupAlertBottomSheet__couldnt_redeem_your_backups_subscription)
|
||||
@@ -453,7 +446,7 @@ private fun primaryActionString(
|
||||
is BackupAlert.CouldNotCompleteBackup -> stringResource(R.string.BackupAlertBottomSheet__back_up_now)
|
||||
BackupAlert.FailedToRenew -> stringResource(R.string.BackupAlertBottomSheet__manage_subscription)
|
||||
is BackupAlert.MediaBackupsAreOff -> error("Not supported.")
|
||||
BackupAlert.MediaWillBeDeletedToday -> stringResource(R.string.BackupAlertBottomSheet__download_media_now)
|
||||
is BackupAlert.DownloadYourBackupData -> stringResource(R.string.BackupAlertBottomSheet__download_backup_now)
|
||||
is BackupAlert.DiskFull -> stringResource(R.string.BackupAlertBottomSheet__got_it)
|
||||
is BackupAlert.BackupFailed -> stringResource(R.string.BackupAlertBottomSheet__check_for_update)
|
||||
BackupAlert.CouldNotRedeemBackup -> stringResource(R.string.BackupAlertBottomSheet__got_it)
|
||||
@@ -468,7 +461,7 @@ private fun rememberSecondaryActionResource(backupAlert: BackupAlert): Int {
|
||||
is BackupAlert.CouldNotCompleteBackup -> R.string.BackupAlertBottomSheet__try_later
|
||||
BackupAlert.FailedToRenew, BackupAlert.ExpiredAndDowngraded -> R.string.BackupAlertBottomSheet__not_now
|
||||
is BackupAlert.MediaBackupsAreOff -> error("Not supported.")
|
||||
BackupAlert.MediaWillBeDeletedToday -> R.string.BackupAlertBottomSheet__dont_download_media
|
||||
is BackupAlert.DownloadYourBackupData -> R.string.BackupAlertBottomSheet__dont_download_backup
|
||||
is BackupAlert.DiskFull -> R.string.BackupAlertBottomSheet__skip_restore
|
||||
is BackupAlert.BackupFailed -> R.string.BackupAlertBottomSheet__learn_more
|
||||
BackupAlert.CouldNotRedeemBackup -> R.string.BackupAlertBottomSheet__learn_more
|
||||
@@ -501,7 +494,10 @@ private fun BackupAlertSheetContentPreviewPayment() {
|
||||
private fun BackupAlertSheetContentPreviewDelete() {
|
||||
Previews.BottomSheetPreview {
|
||||
BackupAlertSheetContent(
|
||||
backupAlert = BackupAlert.MediaWillBeDeletedToday
|
||||
backupAlert = BackupAlert.DownloadYourBackupData(
|
||||
isLastDay = false,
|
||||
formattedSize = "2.3MB"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -580,9 +576,16 @@ sealed class BackupAlert : Parcelable {
|
||||
) : BackupAlert()
|
||||
|
||||
/**
|
||||
* TODO [backups] - This value is driven as "60D after the last time the user pinged their backup"
|
||||
* When a user's subscription becomes cancelled or has a payment failure, we will alert the user
|
||||
* up to two times regarding their media deletion via a sheet, and once in the last 4 hours with a dialog.
|
||||
*
|
||||
* This value drives viewing the sheet.
|
||||
*/
|
||||
data object MediaWillBeDeletedToday : BackupAlert()
|
||||
data class DownloadYourBackupData(
|
||||
val isLastDay: Boolean,
|
||||
val formattedSize: String,
|
||||
val type: BackupDownloadNotifierState.Type = BackupDownloadNotifierState.Type.SHEET
|
||||
) : BackupAlert()
|
||||
|
||||
/**
|
||||
* The disk is full. Contains a value representing the amount of space that must be freed.
|
||||
|
||||
@@ -9,11 +9,12 @@ import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.coroutineScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.core.util.concurrent.SignalDispatchers
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.keyvalue.protos.BackupDownloadNotifierState
|
||||
|
||||
/**
|
||||
* Delegate that controls whether and which backup alert sheet is displayed.
|
||||
@@ -27,12 +28,25 @@ object BackupAlertDelegate {
|
||||
BackupAlertBottomSheet.create(BackupAlert.BackupFailed).show(fragmentManager, null)
|
||||
} else if (BackupRepository.shouldDisplayCouldNotCompleteBackupSheet()) {
|
||||
BackupAlertBottomSheet.create(BackupAlert.CouldNotCompleteBackup(daysSinceLastBackup = SignalStore.backup.daysSinceLastBackup)).show(fragmentManager, null)
|
||||
} else if (withContext(Dispatchers.IO) { BackupRepository.shouldDisplayYourMediaWillBeDeletedTodaySheet() }) {
|
||||
BackupAlertBottomSheet.create(BackupAlert.MediaWillBeDeletedToday).show(fragmentManager, null)
|
||||
} else if (BackupRepository.shouldDisplayBackupExpiredAndDowngradedSheet()) {
|
||||
BackupAlertBottomSheet.create(BackupAlert.ExpiredAndDowngraded).show(fragmentManager, null)
|
||||
}
|
||||
|
||||
displayBackupDownloadNotifier(fragmentManager)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun displayBackupDownloadNotifier(fragmentManager: FragmentManager) {
|
||||
val downloadYourBackupToday = withContext(SignalDispatchers.IO) { BackupRepository.getDownloadYourBackupData() }
|
||||
when (downloadYourBackupToday?.type) {
|
||||
BackupDownloadNotifierState.Type.SHEET -> {
|
||||
BackupAlertBottomSheet.create(downloadYourBackupToday).show(fragmentManager, null)
|
||||
}
|
||||
BackupDownloadNotifierState.Type.DIALOG -> {
|
||||
DownloadYourBackupTodayDialog.create(downloadYourBackupToday).show(fragmentManager, null)
|
||||
}
|
||||
null -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui
|
||||
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.core.os.BundleCompat
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import org.signal.core.ui.compose.Dialogs
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.compose.ComposeDialogFragment
|
||||
|
||||
/**
|
||||
* Displays a "last chance" dialog to the user to begin a media restore.
|
||||
*/
|
||||
class DownloadYourBackupTodayDialog : ComposeDialogFragment() {
|
||||
|
||||
companion object {
|
||||
|
||||
private const val ARGS = "args"
|
||||
|
||||
fun create(downloadYourBackupData: BackupAlert.DownloadYourBackupData): DialogFragment {
|
||||
return DownloadYourBackupTodayDialog().apply {
|
||||
arguments = bundleOf(ARGS to downloadYourBackupData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val backupAlert: BackupAlert.DownloadYourBackupData by lazy(LazyThreadSafetyMode.NONE) {
|
||||
BundleCompat.getParcelable(requireArguments(), ARGS, BackupAlert.DownloadYourBackupData::class.java)!!
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun DialogContent() {
|
||||
DownloadYourBackupTodayDialogContent(
|
||||
sizeToDownload = backupAlert.formattedSize,
|
||||
onConfirm = {
|
||||
BackupRepository.resumeMediaRestore()
|
||||
},
|
||||
onDismiss = {
|
||||
BackupRepository.snoozeDownloadYourBackupData()
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DownloadYourBackupTodayDialogContent(
|
||||
sizeToDownload: String,
|
||||
onConfirm: () -> Unit = {},
|
||||
onDismiss: () -> Unit = {}
|
||||
) {
|
||||
Dialogs.SimpleAlertDialog(
|
||||
title = stringResource(R.string.DownloadYourBackupTodayDialog__download_your_backup_today),
|
||||
body = stringResource(R.string.DownloadYourBackupTodayDialog__you_have_s_of_backup_data, sizeToDownload),
|
||||
confirm = stringResource(R.string.DownloadYourBackupTodayDialog__download),
|
||||
dismiss = stringResource(R.string.DownloadYourBackupTodayDialog__dont_download),
|
||||
dismissColor = MaterialTheme.colorScheme.error,
|
||||
onDismiss = onDismiss,
|
||||
onConfirm = onConfirm
|
||||
)
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun DownloadYourBackupTodayDialogContentPreview() {
|
||||
Previews.Preview {
|
||||
DownloadYourBackupTodayDialogContent(
|
||||
sizeToDownload = "2.3GB"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1028,7 +1028,7 @@ private fun BackupCard(
|
||||
)
|
||||
} else if (backupState is RemoteBackupsSettingsState.BackupState.Canceled) {
|
||||
CallToActionButton(
|
||||
text = stringResource(R.string.RemoteBackupsSettingsFragment__renew),
|
||||
text = stringResource(R.string.RemoteBackupsSettingsFragment__resubscribe),
|
||||
enabled = buttonsEnabled,
|
||||
onClick = { onBackupTypeActionButtonClicked(MessageBackupTier.FREE) }
|
||||
)
|
||||
@@ -1634,8 +1634,7 @@ private fun BackupReadyToDownloadRow(
|
||||
onDownloadClick: () -> Unit = {}
|
||||
) {
|
||||
val string = if (backupState is RemoteBackupsSettingsState.BackupState.Canceled) {
|
||||
val days = (backupState.renewalTime - System.currentTimeMillis().milliseconds).inWholeDays.toInt()
|
||||
pluralStringResource(R.plurals.RemoteBackupsSettingsFragment__you_have_s_of_backup_data, days, ready.bytes, days)
|
||||
stringResource(R.string.RemoteBackupsSettingsFragment__you_have_s_of_backup_data, ready.bytes)
|
||||
} else {
|
||||
stringResource(R.string.RemoteBackupsSettingsFragment__you_have_s_of_backup_data_not_on_device, ready.bytes)
|
||||
}
|
||||
@@ -1841,6 +1840,24 @@ private fun BackupReadyToDownloadPreview() {
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun BackupReadyToDownloadAfterCancelPreview() {
|
||||
Previews.Preview {
|
||||
BackupReadyToDownloadRow(
|
||||
ready = BackupRestoreState.Ready("12GB"),
|
||||
backupState = RemoteBackupsSettingsState.BackupState.Canceled(
|
||||
messageBackupsType = MessageBackupsType.Paid(
|
||||
pricePerMonth = FiatMoney(BigDecimal.ONE, Currency.getInstance("USD")),
|
||||
storageAllowanceBytes = 10.gibiBytes.bytes,
|
||||
mediaTtl = 30.days
|
||||
),
|
||||
renewalTime = System.currentTimeMillis().days + 10.days
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun LastBackupRowPreview() {
|
||||
|
||||
@@ -26,11 +26,14 @@ import org.thoughtcrime.securesms.jobmanager.CoroutineJob
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.net.SignalNetwork
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import org.whispersystems.signalservice.api.storage.IAPSubscriptionId
|
||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
||||
import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration
|
||||
import kotlin.concurrent.withLock
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* Checks and rectifies state pertaining to backups subscriptions.
|
||||
@@ -114,6 +117,8 @@ class BackupSubscriptionCheckJob private constructor(parameters: Parameters) : C
|
||||
val activeSubscription = RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP).getOrNull()
|
||||
val hasActiveSignalSubscription = activeSubscription?.isActive == true
|
||||
|
||||
checkForFailedOrCanceledSubscriptionState(activeSubscription)
|
||||
|
||||
Log.i(TAG, "Synchronizing backup tier with value from server.")
|
||||
BackupRepository.getBackupTier().runIfSuccessful {
|
||||
SignalStore.backup.backupTier = it
|
||||
@@ -148,6 +153,32 @@ class BackupSubscriptionCheckJob private constructor(parameters: Parameters) : C
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for a payment failure / subscription cancellation. If either of these things occur, we will mark when to display
|
||||
* the "download your data" notifier sheet.
|
||||
*/
|
||||
private fun checkForFailedOrCanceledSubscriptionState(activeSubscription: ActiveSubscription?) {
|
||||
val containsFailedPaymentOrCancellation = activeSubscription?.isFailedPayment == true || activeSubscription?.isCanceled == true
|
||||
if (containsFailedPaymentOrCancellation && activeSubscription.activeSubscription != null) {
|
||||
Log.i(TAG, "Subscription either has a payment failure or has been canceled.")
|
||||
|
||||
val response = SignalNetwork.account.whoAmI()
|
||||
response.runIfSuccessful { whoAmI ->
|
||||
val backupExpiration = whoAmI.entitlements?.backup?.expirationSeconds?.seconds
|
||||
if (backupExpiration != null) {
|
||||
Log.i(TAG, "Marking subscription failed or canceled.")
|
||||
SignalStore.backup.setDownloadNotifierToTriggerAtHalfwayPoint(backupExpiration)
|
||||
} else {
|
||||
Log.w(TAG, "Failed to mark, no entitlement was found on WhoAmIResponse")
|
||||
}
|
||||
}
|
||||
|
||||
if (response.getCause() != null) {
|
||||
Log.w(TAG, "Failed to get WhoAmI from service.", response.getCause())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun enqueueRedemptionForNewToken(localDevicePurchaseToken: String, localProductPrice: FiatMoney) {
|
||||
RecurringInAppPaymentRepository.ensureSubscriberIdSync(
|
||||
subscriberType = InAppPaymentSubscriberRecord.Type.BACKUP,
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.keyvalue
|
||||
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.keyvalue.BackupValues.Companion.TAG
|
||||
import org.thoughtcrime.securesms.keyvalue.protos.BackupDownloadNotifierState
|
||||
import kotlin.math.max
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* Manages setting and snoozing notifiers informing the user to download their backup
|
||||
* before it is deleted from the Signal service.
|
||||
*
|
||||
* This is only meant to be delegated to from [BackupValues]
|
||||
*/
|
||||
object BackupDownloadNotifierUtil {
|
||||
|
||||
/**
|
||||
* Sets the notifier to trigger half way between now and the entitlement expiration time.
|
||||
*
|
||||
* @param state The current state, or null.
|
||||
* @param entitlementExpirationTime The time the user's backup entitlement expires
|
||||
* @param now The current time, for testing.
|
||||
*
|
||||
* @return the new state value.
|
||||
*/
|
||||
fun setDownloadNotifierToTriggerAtHalfwayPoint(
|
||||
state: BackupDownloadNotifierState?,
|
||||
entitlementExpirationTime: Duration,
|
||||
now: Duration = System.currentTimeMillis().milliseconds
|
||||
): BackupDownloadNotifierState? {
|
||||
if (state?.entitlementExpirationSeconds == entitlementExpirationTime.inWholeSeconds) {
|
||||
Log.d(TAG, "Entitlement expiration time already present.")
|
||||
return state
|
||||
}
|
||||
|
||||
if (now >= entitlementExpirationTime) {
|
||||
Log.i(TAG, "Entitlement expiration time is in the past. Clearing state.")
|
||||
return null
|
||||
}
|
||||
|
||||
val timeRemaining = entitlementExpirationTime - now
|
||||
val halfWayPoint = (entitlementExpirationTime - timeRemaining / 2)
|
||||
val lastDay = entitlementExpirationTime - 1.days
|
||||
|
||||
val nextIntervalSeconds: Duration = when {
|
||||
timeRemaining <= 1.days -> 0.seconds
|
||||
timeRemaining <= 4.days -> lastDay - now
|
||||
else -> halfWayPoint - now
|
||||
}
|
||||
|
||||
return BackupDownloadNotifierState(
|
||||
entitlementExpirationSeconds = entitlementExpirationTime.inWholeSeconds,
|
||||
lastSheetDisplaySeconds = now.inWholeSeconds,
|
||||
intervalSeconds = nextIntervalSeconds.inWholeSeconds,
|
||||
type = BackupDownloadNotifierState.Type.SHEET
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the notifier to trigger either one day before or four hours before expiration.
|
||||
*
|
||||
* @param state The current state, or null.
|
||||
* @param now The current time, for testing.
|
||||
*
|
||||
* @return The new state value.
|
||||
*/
|
||||
fun snoozeDownloadNotifier(
|
||||
state: BackupDownloadNotifierState?,
|
||||
now: Duration = System.currentTimeMillis().milliseconds
|
||||
): BackupDownloadNotifierState? {
|
||||
state ?: return null
|
||||
|
||||
if (state.type == BackupDownloadNotifierState.Type.DIALOG) {
|
||||
Log.i(TAG, "Clearing state after dismissing download notifier dialog.")
|
||||
return null
|
||||
}
|
||||
|
||||
val lastDay = state.entitlementExpirationSeconds.seconds - 1.days
|
||||
|
||||
return if (now >= lastDay) {
|
||||
val fourHoursPriorToExpiration = state.entitlementExpirationSeconds.seconds - 4.hours
|
||||
|
||||
state.newBuilder()
|
||||
.lastSheetDisplaySeconds(now.inWholeSeconds)
|
||||
.intervalSeconds(max(0L, (fourHoursPriorToExpiration - now).inWholeSeconds))
|
||||
.type(BackupDownloadNotifierState.Type.DIALOG)
|
||||
.build()
|
||||
} else {
|
||||
val timeUntilLastDay = lastDay - now
|
||||
|
||||
state.newBuilder()
|
||||
.lastSheetDisplaySeconds(now.inWholeSeconds)
|
||||
.intervalSeconds(timeUntilLastDay.inWholeSeconds)
|
||||
.type(BackupDownloadNotifierState.Type.SHEET)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NoRemoteArchiveGarbageCollectionPendingConstraint
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.RestoreAttachmentConstraintObserver
|
||||
import org.thoughtcrime.securesms.keyvalue.protos.ArchiveUploadProgressState
|
||||
import org.thoughtcrime.securesms.keyvalue.protos.BackupDownloadNotifierState
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.whispersystems.signalservice.api.archive.ArchiveServiceCredential
|
||||
@@ -52,6 +53,7 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
|
||||
|
||||
private const val KEY_CDN_MEDIA_PATH = "backup.cdn.mediaPath"
|
||||
|
||||
private const val KEY_BACKUP_DOWNLOAD_NOTIFIER_STATE = "backup.downloadNotifierState"
|
||||
private const val KEY_BACKUP_OVER_CELLULAR = "backup.useCellular"
|
||||
private const val KEY_RESTORE_OVER_CELLULAR = "backup.restore.useCellular"
|
||||
private const val KEY_OPTIMIZE_STORAGE = "backup.optimizeStorage"
|
||||
@@ -97,6 +99,9 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
|
||||
var optimizeStorage: Boolean by booleanValue(KEY_OPTIMIZE_STORAGE, false)
|
||||
var backupWithCellular: Boolean by booleanValue(KEY_BACKUP_OVER_CELLULAR, false)
|
||||
|
||||
var backupDownloadNotifierState: BackupDownloadNotifierState? by protoValue(KEY_BACKUP_DOWNLOAD_NOTIFIER_STATE, BackupDownloadNotifierState.ADAPTER)
|
||||
private set
|
||||
|
||||
var restoreWithCellular: Boolean
|
||||
get() = getBoolean(KEY_RESTORE_OVER_CELLULAR, false)
|
||||
set(value) {
|
||||
@@ -252,6 +257,28 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
|
||||
*/
|
||||
var spaceAvailableOnDiskBytes: Long by longValue(KEY_BACKUP_FAIL_SPACE_REMAINING, -1L)
|
||||
|
||||
/**
|
||||
* Sets the notifier to trigger half way between now and the entitlement expiration time.
|
||||
*/
|
||||
fun setDownloadNotifierToTriggerAtHalfwayPoint(entitlementExpirationTime: Duration) {
|
||||
backupDownloadNotifierState = BackupDownloadNotifierUtil.setDownloadNotifierToTriggerAtHalfwayPoint(backupDownloadNotifierState, entitlementExpirationTime)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the notifier to trigger 24hrs before the end of the grace period.
|
||||
*
|
||||
*/
|
||||
fun snoozeDownloadNotifier() {
|
||||
backupDownloadNotifierState = BackupDownloadNotifierUtil.snoozeDownloadNotifier(backupDownloadNotifierState)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the notifier state, done when the user subscribes to the paid tier.
|
||||
*/
|
||||
fun clearDownloadNotifierState() {
|
||||
backupDownloadNotifierState = null
|
||||
}
|
||||
|
||||
fun internalSetBackupFailedErrorState() {
|
||||
markMessageBackupFailure()
|
||||
putLong(KEY_BACKUP_FAIL_SHEET_SNOOZE_TIME, 0)
|
||||
|
||||
@@ -49,4 +49,16 @@ message ArchiveUploadProgressState {
|
||||
uint64 backupFileTotalBytes = 6;
|
||||
uint64 mediaUploadedBytes = 7;
|
||||
uint64 mediaTotalBytes = 8;
|
||||
}
|
||||
|
||||
message BackupDownloadNotifierState {
|
||||
enum Type {
|
||||
SHEET = 0;
|
||||
DIALOG = 1;
|
||||
}
|
||||
|
||||
uint64 entitlementExpirationSeconds = 1;
|
||||
uint64 lastSheetDisplaySeconds = 2;
|
||||
uint64 intervalSeconds = 3;
|
||||
Type type = 4;
|
||||
}
|
||||
@@ -7835,9 +7835,11 @@
|
||||
<!-- Sheet body part 2 when media backups have been disabled. -->
|
||||
<string name="BackupAlertBottomSheet__you_can_begin_paying_for_backups_again">You can begin paying for backups again at any time to continue backing up all your media. </string>
|
||||
<!-- Sheet title when user\'s media will be deleted today -->
|
||||
<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 -->
|
||||
<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__download_your_backup_data_today">Download your backup data today</string>
|
||||
<!-- Sheet title when user\'s media will be deleted at the end of their grace period -->
|
||||
<string name="BackupAlertBottomSheet__download_your_backup_data">Download your backup data</string>
|
||||
<!-- Sheet body part 1 when user\'s media will be deleted after grace period lapses -->
|
||||
<string name="BackupAlertBottomSheet__you_have_s_of_media_thats_not_on_this_device">You have %1$s of media that\'s not on this device. The media and attachments stored in your backup will be permanently deleted without a paid subscription.</string>
|
||||
<!-- Sheet title when user does not have enough space to download their backup. Placeholder is formatted byte size, for example 12GB. -->
|
||||
<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. -->
|
||||
@@ -7876,9 +7878,9 @@
|
||||
<!-- Primary action button text prompting user to subscriber. Placeholder is formatted price -->
|
||||
<string name="BackupAlertBottomSheet__subscribe_for_s_month">Subscribe for %1$s/month</string>
|
||||
<!-- Primary action button text prompting user to download their media -->
|
||||
<string name="BackupAlertBottomSheet__download_media_now">Download media now</string>
|
||||
<string name="BackupAlertBottomSheet__download_backup_now">Download backup now</string>
|
||||
<!-- Secondary action button text to dismiss sheet, skipping media download -->
|
||||
<string name="BackupAlertBottomSheet__dont_download_media">Don\'t download media</string>
|
||||
<string name="BackupAlertBottomSheet__dont_download_backup">Don\'t download backup</string>
|
||||
<!-- Secondary generic action button to dismiss sheet without performing an action -->
|
||||
<string name="BackupAlertBottomSheet__not_now">Not now</string>
|
||||
<!-- Secondary action button to dismiss could not complete backup sheet -->
|
||||
@@ -8176,6 +8178,8 @@
|
||||
<item quantity="one">Your subscription on this device is valid for the next %1$d day. Renew to continue using Signal Backups</item>
|
||||
<item quantity="other">Your subscription on this device is valid for the next %1$d days. Renew to continue using Signal Backups</item>
|
||||
</plurals>
|
||||
<!-- Button label to start subscription resubscribe after cancellation -->
|
||||
<string name="RemoteBackupsSettingsFragment__resubscribe">Resubscribe</string>
|
||||
<!-- Button label to start subscription renewal -->
|
||||
<string name="RemoteBackupsSettingsFragment__renew">Renew</string>
|
||||
<!-- Button label to learn more about why subscription disappeared -->
|
||||
@@ -8190,10 +8194,7 @@
|
||||
<item quantity="other">Processing %1$s of ~%2$s messages (%3$d%%)</item>
|
||||
</plurals>
|
||||
<!-- Displayed in row when backup is available for download and users subscription has expired. First placeholder is data size e.g. 12MB, second is days before expiration -->
|
||||
<plurals name="RemoteBackupsSettingsFragment__you_have_s_of_backup_data">
|
||||
<item quantity="one">You have %1$s of backup data that’s not on this device. Your backup will be deleted when your subscription ends in %2$d day.</item>
|
||||
<item quantity="other">You have %1$s of backup data that’s not on this device. Your backup will be deleted when your subscription ends in %2$d days.</item>
|
||||
</plurals>
|
||||
<string name="RemoteBackupsSettingsFragment__you_have_s_of_backup_data">You have %1$s of backup data that\'s not on this device. Without a paid subscription media can\'t be offloaded.</string>
|
||||
<!-- Display in a row when backup is available for download but the user has canceled the restore. Place holder is data size e.g. 12MB -->
|
||||
<string name="RemoteBackupsSettingsFragment__you_have_s_of_backup_data_not_on_device">You have %1$s of backup data that’s not on this device. </string>
|
||||
<!-- Displayed in row when backup is available for download to let user start download -->
|
||||
@@ -8238,6 +8239,16 @@
|
||||
<!-- Text displayed while downloading backup during deletion. First placeholder is amount downloaded, second is amount remaining, last is percentage. -->
|
||||
<string name="RemoteBackupsSettingsFragment__downloading_s_of_s_d">Downloading: %1$s of %2$s (%3$d%%)</string>
|
||||
|
||||
<!-- DownloadYourBackupTodayDialog -->
|
||||
<!-- Dialog title -->
|
||||
<string name="DownloadYourBackupTodayDialog__download_your_backup_today">Download your backup today</string>
|
||||
<!-- Dialog body. Placeholder is human readable byte count, for example 2.3 MB -->
|
||||
<string name="DownloadYourBackupTodayDialog__you_have_s_of_backup_data">You have %1$s of backup data that\'s not on this device. Your media and attachments will be permanently deleted without a paid subscription.</string>
|
||||
<!-- Dialog positive action label -->
|
||||
<string name="DownloadYourBackupTodayDialog__download">Download</string>
|
||||
<!-- Dialog negative action label -->
|
||||
<string name="DownloadYourBackupTodayDialog__dont_download">Don\'t download</string>
|
||||
|
||||
<!-- SubscriptionNotFoundBottomSheet -->
|
||||
<!-- Displayed as a bottom sheet title -->
|
||||
<string name="SubscriptionNotFoundBottomSheet__subscription_not_found">Subscription not found</string>
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.keyvalue
|
||||
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isNull
|
||||
import org.junit.Test
|
||||
import org.thoughtcrime.securesms.keyvalue.protos.BackupDownloadNotifierState
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class BackupDownloadNotifierUtilTest {
|
||||
@Test
|
||||
fun `Given within one day of expiration, when I setDownloadNotifierToTriggerAtHalfwayPoint, then I expect 0 interval`() {
|
||||
val expectedIntervalFromNow = 0.seconds
|
||||
val expiration = 30.days
|
||||
val now = 29.days
|
||||
|
||||
val expected = BackupDownloadNotifierState(
|
||||
entitlementExpirationSeconds = expiration.inWholeSeconds,
|
||||
lastSheetDisplaySeconds = now.inWholeSeconds,
|
||||
intervalSeconds = expectedIntervalFromNow.inWholeSeconds,
|
||||
type = BackupDownloadNotifierState.Type.SHEET
|
||||
)
|
||||
|
||||
val result = BackupDownloadNotifierUtil.setDownloadNotifierToTriggerAtHalfwayPoint(
|
||||
entitlementExpirationTime = expiration,
|
||||
now = now,
|
||||
state = null
|
||||
)
|
||||
|
||||
assertThat(result).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given within four days of expiration, when I setDownloadNotifierToTriggerAtHalfwayPoint, then I expect to be notified on the last day`() {
|
||||
val expectedIntervalFromNow = 3.days
|
||||
val expiration = 30.days
|
||||
val now = 26.days
|
||||
|
||||
val expected = BackupDownloadNotifierState(
|
||||
entitlementExpirationSeconds = expiration.inWholeSeconds,
|
||||
lastSheetDisplaySeconds = now.inWholeSeconds,
|
||||
intervalSeconds = expectedIntervalFromNow.inWholeSeconds,
|
||||
type = BackupDownloadNotifierState.Type.SHEET
|
||||
)
|
||||
|
||||
val result = BackupDownloadNotifierUtil.setDownloadNotifierToTriggerAtHalfwayPoint(
|
||||
entitlementExpirationTime = expiration,
|
||||
now = now,
|
||||
state = null
|
||||
)
|
||||
|
||||
assertThat(result).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given more than four days until expiration, when I setDownloadNotifierToTriggerAtHalfwayPoint, then I expect to be notified at the halfway point`() {
|
||||
val expectedIntervalFromNow = 5.days
|
||||
val expiration = 30.days
|
||||
val now = 20.days
|
||||
|
||||
val expected = BackupDownloadNotifierState(
|
||||
entitlementExpirationSeconds = expiration.inWholeSeconds,
|
||||
lastSheetDisplaySeconds = now.inWholeSeconds,
|
||||
intervalSeconds = expectedIntervalFromNow.inWholeSeconds,
|
||||
type = BackupDownloadNotifierState.Type.SHEET
|
||||
)
|
||||
|
||||
val result = BackupDownloadNotifierUtil.setDownloadNotifierToTriggerAtHalfwayPoint(
|
||||
entitlementExpirationTime = expiration,
|
||||
now = now,
|
||||
state = null
|
||||
)
|
||||
|
||||
assertThat(result).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given an expired entitlement, when I setDownloadNotifierToTriggerAtHalfwayPoint, then I expect null`() {
|
||||
val expiration = 28.days
|
||||
val now = 29.days
|
||||
|
||||
val result = BackupDownloadNotifierUtil.setDownloadNotifierToTriggerAtHalfwayPoint(
|
||||
entitlementExpirationTime = expiration,
|
||||
now = now,
|
||||
state = null
|
||||
)
|
||||
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given a repeat expiration time, when I setDownloadNotifierToTriggerAtHalfwayPoint, then I expect to return the exact same state`() {
|
||||
val expiration = 30.days
|
||||
val now = 20.days
|
||||
|
||||
val expectedState = BackupDownloadNotifierState(
|
||||
entitlementExpirationSeconds = expiration.inWholeSeconds,
|
||||
intervalSeconds = 0L,
|
||||
lastSheetDisplaySeconds = 0L,
|
||||
type = BackupDownloadNotifierState.Type.DIALOG
|
||||
)
|
||||
|
||||
val result = BackupDownloadNotifierUtil.setDownloadNotifierToTriggerAtHalfwayPoint(
|
||||
entitlementExpirationTime = expiration,
|
||||
now = now,
|
||||
state = expectedState
|
||||
)
|
||||
|
||||
assertThat(result).isEqualTo(expectedState)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given a null state, when I snoozeDownloadNotifier, then I expect null`() {
|
||||
val result = BackupDownloadNotifierUtil.snoozeDownloadNotifier(state = null)
|
||||
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given a DIALOG type, when I snoozeDownloadNotifier, then I expect null`() {
|
||||
val state = BackupDownloadNotifierState(
|
||||
entitlementExpirationSeconds = 0L,
|
||||
intervalSeconds = 0L,
|
||||
lastSheetDisplaySeconds = 0L,
|
||||
type = BackupDownloadNotifierState.Type.DIALOG
|
||||
)
|
||||
|
||||
val result = BackupDownloadNotifierUtil.snoozeDownloadNotifier(state = state)
|
||||
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given within one day of expiration, when I snoozeDownloadNotifier, then I expect dialog 4hrs before expiration`() {
|
||||
val now = 0.hours
|
||||
val expiration = 12.hours
|
||||
val state = BackupDownloadNotifierState(
|
||||
entitlementExpirationSeconds = expiration.inWholeSeconds,
|
||||
lastSheetDisplaySeconds = 0,
|
||||
intervalSeconds = 0
|
||||
)
|
||||
|
||||
val expected = BackupDownloadNotifierState(
|
||||
entitlementExpirationSeconds = expiration.inWholeSeconds,
|
||||
lastSheetDisplaySeconds = now.inWholeSeconds,
|
||||
intervalSeconds = 8.hours.inWholeSeconds,
|
||||
type = BackupDownloadNotifierState.Type.DIALOG
|
||||
)
|
||||
|
||||
val result = BackupDownloadNotifierUtil.snoozeDownloadNotifier(
|
||||
state = state,
|
||||
now = now
|
||||
)
|
||||
|
||||
assertThat(result).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given more than one day until expiration, when I snoozeDownloadNotifier, then I expect sheet one day before expiration`() {
|
||||
val now = 0.days
|
||||
val expiration = 5.days
|
||||
val state = BackupDownloadNotifierState(
|
||||
entitlementExpirationSeconds = expiration.inWholeSeconds,
|
||||
lastSheetDisplaySeconds = 0,
|
||||
intervalSeconds = 0
|
||||
)
|
||||
|
||||
val expected = BackupDownloadNotifierState(
|
||||
entitlementExpirationSeconds = expiration.inWholeSeconds,
|
||||
lastSheetDisplaySeconds = now.inWholeSeconds,
|
||||
intervalSeconds = 4.days.inWholeSeconds,
|
||||
type = BackupDownloadNotifierState.Type.SHEET
|
||||
)
|
||||
|
||||
val result = BackupDownloadNotifierUtil.snoozeDownloadNotifier(
|
||||
state = state,
|
||||
now = now
|
||||
)
|
||||
|
||||
assertThat(result).isEqualTo(expected)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user