From bae86d127faea386a363a53b5228a189ea908698 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Fri, 22 Nov 2024 10:23:23 -0400 Subject: [PATCH] Add "your media will be deleted today" mechanism based off last checkin time and media TTL. --- .../securesms/backup/v2/BackupRepository.kt | 48 ++++++++++++++++++- .../backup/v2/ui/BackupAlertBottomSheet.kt | 1 + .../backup/v2/ui/BackupAlertDelegate.kt | 7 ++- .../subscription/InAppPaymentsRepository.kt | 27 ----------- .../InAppPaymentsBottomSheetDelegate.kt | 11 ----- .../securesms/jobs/BackupRefreshJob.kt | 1 + .../jobs/InAppPaymentRedemptionJob.kt | 1 + .../securesms/keyvalue/BackupValues.kt | 12 +++++ .../securesms/keyvalue/InAppPaymentValues.kt | 7 +-- 9 files changed, 65 insertions(+), 50 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt index 640d77181f..c2a4d1271c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt @@ -101,6 +101,7 @@ import java.time.ZonedDateTime 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 org.signal.libsignal.messagebackup.MessageBackupKey as LibSignalMessageBackupKey @@ -237,6 +238,49 @@ object BackupRepository { return System.currentTimeMillis().milliseconds > SignalStore.backup.nextBackupFailureSheetSnoozeTime } + fun snoozeYourMediaWillBeDeletedTodaySheet() { + SignalStore.backup.lastCheckInSnoozeMillis = System.currentTimeMillis() + } + + /** + * Whether or not the "Your media will be deleted today" sheet should be displayed. + */ + suspend fun shouldDisplayYourMediaWillBeDeletedTodaySheet(): Boolean { + if (shouldNotDisplayBackupFailedMessaging() || !SignalStore.backup.optimizeStorage) { + return false + } + + val paidType = try { + withContext(Dispatchers.IO) { + getPaidType() + } + } catch (e: IOException) { + Log.w(TAG, "Failed to retrieve paid type.", e) + return false + } + + if (paidType == null) { + Log.w(TAG, "Paid type is not available on this device.") + return false + } + + 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 isNowAfterSnooze && isNowWithin24HoursOfMediaExpiration + } + private fun shouldNotDisplayBackupFailedMessaging(): Boolean { return !RemoteConfig.messageBackups || !SignalStore.backup.areBackupsEnabled || !SignalStore.backup.hasBackupBeenUploaded } @@ -1178,7 +1222,7 @@ object BackupRepository { } } - private suspend fun getFreeType(): MessageBackupsType { + private suspend fun getFreeType(): MessageBackupsType.Free { val config = getSubscriptionsConfiguration() return MessageBackupsType.Free( @@ -1186,7 +1230,7 @@ object BackupRepository { ) } - private suspend fun getPaidType(): MessageBackupsType? { + private suspend fun getPaidType(): MessageBackupsType.Paid? { val config = getSubscriptionsConfiguration() val product = AppDependencies.billingApi.queryProduct() ?: return null val backupLevelConfiguration = config.backupConfiguration.backupLevelConfigurationMap[SubscriptionsConfiguration.BACKUPS_LEVEL] ?: return null diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/BackupAlertBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/BackupAlertBottomSheet.kt index ba6c4a2bcf..436c410271 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/BackupAlertBottomSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/BackupAlertBottomSheet.kt @@ -154,6 +154,7 @@ class BackupAlertBottomSheet : UpgradeToPaidTierBottomSheet() { when (backupAlert) { is BackupAlert.CouldNotCompleteBackup -> BackupRepository.markBackupFailedSheetDismissed() + is BackupAlert.MediaWillBeDeletedToday -> BackupRepository.snoozeYourMediaWillBeDeletedTodaySheet() else -> Unit } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/BackupAlertDelegate.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/BackupAlertDelegate.kt index 1ddbc8a08e..18b7249b80 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/BackupAlertDelegate.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/BackupAlertDelegate.kt @@ -25,10 +25,9 @@ object BackupAlertDelegate { BackupAlertBottomSheet.create(BackupAlert.CouldNotCompleteBackup(daysSinceLastBackup = SignalStore.backup.daysSinceLastBackup)).show(fragmentManager, null) } - // TODO [backups] Check if media will be deleted within 24hrs and display warning sheet. - - // TODO [backups] - // Get unnotified backup download failures & display sheet + if (BackupRepository.shouldDisplayYourMediaWillBeDeletedTodaySheet()) { + BackupAlertBottomSheet.create(BackupAlert.MediaWillBeDeletedToday) + } } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/InAppPaymentsRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/InAppPaymentsRepository.kt index 32b71bc3f7..0a492a4c05 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/InAppPaymentsRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/InAppPaymentsRepository.kt @@ -59,7 +59,6 @@ import java.util.concurrent.locks.Lock import kotlin.jvm.optionals.getOrNull import kotlin.time.Duration import kotlin.time.Duration.Companion.days -import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds /** @@ -390,32 +389,6 @@ object InAppPaymentsRepository { } } - /** - * Determines if we are in the timeout period to display the "your backup will be deleted today" message - */ - @WorkerThread - fun getExpiredBackupDeletionState(): ExpiredBackupDeletionState { - val inAppPayment = SignalDatabase.inAppPayments.getByLatestEndOfPeriod(InAppPaymentType.RECURRING_BACKUP) - if (inAppPayment == null) { - Log.w(TAG, "InAppPayment for recurring backup not found for last day check. Clearing check.") - SignalStore.inAppPayments.showLastDayToDownloadMediaDialog = false - return ExpiredBackupDeletionState.NONE - } - - val now = SignalStore.misc.estimatedServerTime.milliseconds - val lastEndOfPeriod = inAppPayment.endOfPeriod - val displayDialogStart = lastEndOfPeriod + backupExpirationTimeout - val displayDialogEnd = lastEndOfPeriod + backupExpirationDeletion - - return if (now in displayDialogStart..displayDialogEnd) { - ExpiredBackupDeletionState.DELETE_TODAY - } else if (now > displayDialogEnd) { - ExpiredBackupDeletionState.EXPIRED - } else { - ExpiredBackupDeletionState.NONE - } - } - @JvmStatic @WorkerThread fun setShouldCancelSubscriptionBeforeNextSubscribeAttempt(subscriber: InAppPaymentSubscriberRecord, shouldCancel: Boolean) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/completed/InAppPaymentsBottomSheetDelegate.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/completed/InAppPaymentsBottomSheetDelegate.kt index f81c086248..07c8abedbd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/completed/InAppPaymentsBottomSheetDelegate.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/completed/InAppPaymentsBottomSheetDelegate.kt @@ -19,7 +19,6 @@ import org.thoughtcrime.securesms.badges.Badges import org.thoughtcrime.securesms.badges.self.expired.MonthlyDonationCanceledBottomSheetDialogFragment import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPendingBottomSheet import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPendingBottomSheetArgs -import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository import org.thoughtcrime.securesms.components.settings.app.subscription.thanks.ThanksForYourSupportBottomSheetDialogFragment import org.thoughtcrime.securesms.components.settings.app.subscription.thanks.ThanksForYourSupportBottomSheetDialogFragmentArgs import org.thoughtcrime.securesms.database.InAppPaymentTable @@ -140,16 +139,6 @@ class InAppPaymentsBottomSheetDelegate( } } } - - if (SignalStore.inAppPayments.showLastDayToDownloadMediaDialog) { - lifecycleDisposable += Single.fromCallable { - InAppPaymentsRepository.getExpiredBackupDeletionState() - }.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribeBy { - if (it == InAppPaymentsRepository.ExpiredBackupDeletionState.DELETE_TODAY) { - BackupAlertBottomSheet.create(BackupAlert.MediaWillBeDeletedToday).show(fragmentManager, null) - } - } - } } private fun isUnexpectedCancellation(inAppPaymentState: InAppPaymentTable.State, inAppPaymentData: InAppPaymentData): Boolean { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupRefreshJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupRefreshJob.kt index 5a3c4af7d4..52be59e42c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupRefreshJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupRefreshJob.kt @@ -82,6 +82,7 @@ class BackupRefreshJob private constructor( return when (result) { is NetworkResult.Success -> { SignalStore.backup.lastCheckInMillis = System.currentTimeMillis() + SignalStore.backup.lastCheckInSnoozeMillis = 0 Result.success() } else -> { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentRedemptionJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentRedemptionJob.kt index d8d3160c3b..a2c7b1144a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentRedemptionJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentRedemptionJob.kt @@ -259,6 +259,7 @@ class InAppPaymentRedemptionJob private constructor( Log.i(TAG, "Setting backup tier to PAID", true) SignalStore.backup.backupTier = MessageBackupTier.PAID SignalStore.backup.lastCheckInMillis = System.currentTimeMillis() + SignalStore.backup.lastCheckInSnoozeMillis = 0 } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt index 75ab10bf35..2282fe09cd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt @@ -36,6 +36,7 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) { private const val KEY_BACKUP_TIER = "backup.backupTier" private const val KEY_LATEST_BACKUP_TIER = "backup.latestBackupTier" private const val KEY_LAST_CHECK_IN_MILLIS = "backup.lastCheckInMilliseconds" + private const val KEY_LAST_CHECK_IN_SNOOZE_MILLIS = "backup.lastCheckInSnoozeMilliseconds" private const val KEY_NEXT_BACKUP_TIME = "backup.nextBackupTime" private const val KEY_LAST_BACKUP_TIME = "backup.lastBackupTime" @@ -95,8 +96,19 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) { var userManuallySkippedMediaRestore: Boolean by booleanValue(KEY_USER_MANUALLY_SKIPPED_MEDIA_RESTORE, false) + /** + * The last time the device notified the server that the archive is still in use. + */ var lastCheckInMillis: Long by longValue(KEY_LAST_CHECK_IN_MILLIS, 0L) + /** + * The time we last displayed the "Your media will be deleted today" sheet. + * + * Set when the user dismisses the "Your media will be deleted today" alert + * Cleared when the system performs a check-in or the user subscribes to backups. + */ + var lastCheckInSnoozeMillis: Long by longValue(KEY_LAST_CHECK_IN_SNOOZE_MILLIS, 0) + /** * Key used to backup messages. */ diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InAppPaymentValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InAppPaymentValues.kt index 753813c3be..ebd8316962 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InAppPaymentValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InAppPaymentValues.kt @@ -71,7 +71,6 @@ class InAppPaymentValues internal constructor(store: KeyValueStore) : SignalStor private const val SUBSCRIPTION_CANCELATION_TIMESTAMP = "donation.subscription.cancelation.timestamp" private const val SUBSCRIPTION_CANCELATION_WATERMARK = "donation.subscription.cancelation.watermark" private const val SHOW_CANT_PROCESS_DIALOG = "show.cant.process.dialog" - private const val SHOW_LAST_DAY_TO_DOWNLOAD_MEDIA_DIALOG = "inapppayment.show.last.day.to.download.media.dialog" /** * The current request context for subscription. This should be stored until either @@ -162,8 +161,7 @@ class InAppPaymentValues internal constructor(store: KeyValueStore) : SignalStor SUBSCRIPTION_EOP_STARTED_TO_CONVERT, SUBSCRIPTION_EOP_STARTED_TO_REDEEM, SUBSCRIPTION_EOP_REDEEMED, - SUBSCRIPTION_PAYMENT_SOURCE_TYPE, - SHOW_LAST_DAY_TO_DOWNLOAD_MEDIA_DIALOG + SUBSCRIPTION_PAYMENT_SOURCE_TYPE ) private val recurringDonationCurrencyPublisher: Subject by lazy { BehaviorSubject.createDefault(getSubscriptionCurrency(InAppPaymentSubscriberRecord.Type.DONATION)) } @@ -420,9 +418,6 @@ class InAppPaymentValues internal constructor(store: KeyValueStore) : SignalStor @get:JvmName("showCantProcessDialog") var showMonthlyDonationCanceledDialog: Boolean by booleanValue(SHOW_CANT_PROCESS_DIALOG, true) - @get:JvmName("showLastDayToDownloadMediaDialog") - var showLastDayToDownloadMediaDialog: Boolean by booleanValue(SHOW_LAST_DAY_TO_DOWNLOAD_MEDIA_DIALOG, false) - /** * Denotes that the previous attempt to subscribe failed in some way. Either an * automatic renewal failed resulting in an unexpected expiration, or payment failed