From b35b1db4bccf6d9f0add8e646510fbe2aaf2816b Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Mon, 16 Jun 2025 13:14:33 -0300 Subject: [PATCH] Add wiring and notification for out of remote space error. --- .../securesms/backup/v2/BackupRepository.kt | 36 ++++++++++++++++++- .../remote/RemoteBackupsSettingsFragment.kt | 7 +++- .../remote/RemoteBackupsSettingsViewModel.kt | 31 ++++++++++------ .../InternalBackupPlaygroundFragment.kt | 8 +++++ .../jobs/CopyAttachmentToArchiveJob.kt | 2 +- .../securesms/keyvalue/BackupValues.kt | 3 ++ .../notifications/NotificationIds.java | 1 + app/src/main/res/values/strings.xml | 5 +++ 8 files changed, 79 insertions(+), 14 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 4ae1285a84..6b4c76deda 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 @@ -5,10 +5,12 @@ package org.thoughtcrime.securesms.backup.v2 +import android.app.PendingIntent import android.database.Cursor import android.os.Environment import android.os.StatFs import androidx.annotation.WorkerThread +import androidx.core.app.NotificationCompat import kotlinx.coroutines.withContext import okio.ByteString import okio.ByteString.Companion.toByteString @@ -17,6 +19,7 @@ import org.signal.core.util.Base64 import org.signal.core.util.ByteSize import org.signal.core.util.CursorUtil import org.signal.core.util.EventTimer +import org.signal.core.util.PendingIntentFlags.cancelCurrent import org.signal.core.util.Stopwatch import org.signal.core.util.bytes import org.signal.core.util.concurrent.LimitedWorker @@ -38,6 +41,7 @@ import org.signal.core.util.withinTransaction import org.signal.libsignal.zkgroup.VerificationFailedException import org.signal.libsignal.zkgroup.backups.BackupLevel import org.signal.libsignal.zkgroup.profiles.ProfileKey +import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.attachments.Attachment import org.thoughtcrime.securesms.attachments.Cdn import org.thoughtcrime.securesms.attachments.DatabaseAttachment @@ -62,6 +66,7 @@ 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.AppSettingsActivity import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider @@ -90,9 +95,12 @@ import org.thoughtcrime.securesms.keyvalue.KeyValueStore import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.keyvalue.isDecisionPending import org.thoughtcrime.securesms.net.SignalNetwork +import org.thoughtcrime.securesms.notifications.NotificationChannels +import org.thoughtcrime.securesms.notifications.NotificationIds import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.util.RemoteConfig +import org.thoughtcrime.securesms.util.ServiceUtil import org.thoughtcrime.securesms.util.toMillis import org.whispersystems.signalservice.api.AccountEntropyPool import org.whispersystems.signalservice.api.ApplicationErrorAction @@ -291,8 +299,34 @@ object BackupRepository { AppDependencies.jobManager.add(CheckRestoreMediaLeftJob(RestoreAttachmentJob.constructQueueString(RestoreAttachmentJob.RestoreOperation.MANUAL))) } + fun markOutOfRemoteStorageError() { + val context = AppDependencies.application + + val pendingIntent = PendingIntent.getActivity(context, 0, AppSettingsActivity.remoteBackups(context), cancelCurrent()) + val notification = NotificationCompat.Builder(context, NotificationChannels.getInstance().APP_ALERTS) + .setSmallIcon(R.drawable.ic_notification) + .setContentTitle(context.getString(R.string.Notification_backup_storage_full)) + .setContentText(context.getString(R.string.Notification_youve_reached_your_backup_storage_limit)) + .setContentIntent(pendingIntent) + .setAutoCancel(true) + .build() + + ServiceUtil.getNotificationManager(context).notify(NotificationIds.OUT_OF_REMOTE_STORAGE, notification) + + SignalStore.backup.isNotEnoughRemoteStorageSpace = true + } + + fun clearOutOfRemoteStorageError() { + SignalStore.backup.isNotEnoughRemoteStorageSpace = false + ServiceUtil.getNotificationManager(AppDependencies.application).cancel(NotificationIds.OUT_OF_REMOTE_STORAGE) + } + fun shouldDisplayOutOfStorageSpaceUx(): Boolean { - return false // TODO [message-backups] Wire into actual error handling. + if (shouldNotDisplayBackupFailedMessaging()) { + return false + } + + return SignalStore.backup.isNotEnoughRemoteStorageSpace } /** diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt index 68af51677f..e93fc5e314 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt @@ -511,6 +511,7 @@ private fun RemoteBackupsSettingsContent( backupsFrequency = state.backupsFrequency, canBackUpUsingCellular = state.canBackUpUsingCellular, canRestoreUsingCellular = state.canRestoreUsingCellular, + canBackUpNow = !state.isOutOfStorageSpace, contentCallbacks = contentCallbacks ) } else { @@ -813,6 +814,7 @@ private fun LazyListScope.appendBackupDetailsItems( backupsFrequency: BackupFrequency, canBackUpUsingCellular: Boolean, canRestoreUsingCellular: Boolean, + canBackUpNow: Boolean, contentCallbacks: ContentCallbacks ) { item { @@ -845,6 +847,7 @@ private fun LazyListScope.appendBackupDetailsItems( item { LastBackupRow( lastBackupTimestamp = lastBackupTimestamp, + enabled = canBackUpNow, onBackupNowClick = contentCallbacks::onBackupNowClick ) } @@ -1421,6 +1424,7 @@ private fun getBackupUploadPhaseProgressString(state: ArchiveUploadProgressState @Composable private fun LastBackupRow( lastBackupTimestamp: Long, + enabled: Boolean, onBackupNowClick: () -> Unit ) { Row( @@ -1462,7 +1466,7 @@ private fun LastBackupRow( } } - Buttons.MediumTonal(onClick = onBackupNowClick) { + Buttons.MediumTonal(onClick = onBackupNowClick, enabled = enabled) { Text(text = stringResource(id = R.string.RemoteBackupsSettingsFragment__back_up_now)) } } @@ -1909,6 +1913,7 @@ private fun LastBackupRowPreview() { Previews.Preview { LastBackupRow( lastBackupTimestamp = -1, + enabled = true, onBackupNowClick = {} ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsViewModel.kt index a7d151c46f..cbd527ee7e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsViewModel.kt @@ -22,6 +22,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.reactive.asFlow import org.signal.core.util.bytes import org.signal.core.util.logging.Log +import org.signal.core.util.mebiBytes import org.signal.core.util.throttleLatest import org.signal.donations.InAppPaymentType import org.thoughtcrime.securesms.backup.ArchiveUploadProgress @@ -223,6 +224,25 @@ class RemoteBackupsSettingsViewModel : ViewModel() { } private suspend fun performStateRefresh(lastPurchase: InAppPaymentTable.InAppPayment?) { + if (BackupRepository.shouldDisplayOutOfStorageSpaceUx()) { + val paidType = BackupRepository.getBackupsType(MessageBackupTier.PAID) as? MessageBackupsType.Paid + + if (paidType != null) { + val remoteStorageAllowance = paidType.storageAllowanceBytes.bytes + val estimatedSize = SignalDatabase.attachments.getEstimatedArchiveMediaSize().bytes + + if (estimatedSize + 300.mebiBytes <= remoteStorageAllowance) { + BackupRepository.clearOutOfRemoteStorageError() + } + + _state.update { + it.copy( + totalAllowedStorageSpace = estimatedSize.toUnitString() + ) + } + } + } + _state.update { it.copy( tier = SignalStore.backup.backupTier, @@ -237,17 +257,6 @@ class RemoteBackupsSettingsViewModel : ViewModel() { ) } - if (BackupRepository.shouldDisplayOutOfStorageSpaceUx()) { - val paidType = BackupRepository.getBackupsType(MessageBackupTier.PAID) as? MessageBackupsType.Paid - if (paidType != null) { - _state.update { - it.copy( - totalAllowedStorageSpace = paidType.storageAllowanceBytes.bytes.toUnitString() - ) - } - } - } - val state = BackupStateRepository.resolveBackupState(lastPurchase) _state.update { it.copy(backupState = state) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundFragment.kt index dd07d58a20..4b3249af74 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundFragment.kt @@ -70,6 +70,7 @@ import org.signal.core.util.Base64 import org.signal.core.util.Hex import org.signal.core.util.getLength import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.backup.v2.BackupRepository import org.thoughtcrime.securesms.backup.v2.MessageBackupTier import org.thoughtcrime.securesms.components.settings.app.internal.backup.InternalBackupPlaygroundViewModel.DialogState import org.thoughtcrime.securesms.components.settings.app.internal.backup.InternalBackupPlaygroundViewModel.ScreenState @@ -521,6 +522,13 @@ fun Screen( } ) + Rows.TextRow( + text = "Mark out of remote storage space", + onClick = { + BackupRepository.markOutOfRemoteStorageError() + } + ) + Spacer(modifier = Modifier.height(8.dp)) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/CopyAttachmentToArchiveJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/CopyAttachmentToArchiveJob.kt index b4873d8bdd..1a6dcb9c5e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/CopyAttachmentToArchiveJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/CopyAttachmentToArchiveJob.kt @@ -130,7 +130,7 @@ class CopyAttachmentToArchiveJob private constructor(private val attachmentId: A val remoteStorageQuota = getServerQuota() ?: return Result.retry(defaultBackoff()).logW(TAG, "[$attachmentId] Failed to fetch server quota! Retrying.") if (SignalDatabase.attachments.getEstimatedArchiveMediaSize() > remoteStorageQuota.inWholeBytes) { - // [TODO] Handle too much data case + BackupRepository.markOutOfRemoteStorageError() return Result.failure() } 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 55b099b380..f2c661b97e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt @@ -71,6 +71,7 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) { private const val KEY_BACKUP_FAIL_SPACE_REMAINING = "backup.failed.space.remaining" private const val KEY_BACKUP_ALREADY_REDEEMED = "backup.already.redeemed" private const val KEY_INVALID_BACKUP_VERSION = "backup.invalid.version" + private const val KEY_NOT_ENOUGH_REMOTE_STORAGE_SPACE = "backup.not.enough.remote.storage.space" private const val KEY_USER_MANUALLY_SKIPPED_MEDIA_RESTORE = "backup.user.manually.skipped.media.restore" private const val KEY_BACKUP_EXPIRED_AND_DOWNGRADED = "backup.expired.and.downgraded" @@ -314,6 +315,8 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) { /** Store that lets you interact with media ZK credentials. */ val mediaCredentials = CredentialStore(KEY_MEDIA_CREDENTIALS, KEY_MEDIA_CDN_READ_CREDENTIALS, KEY_MEDIA_CDN_READ_CREDENTIALS_TIMESTAMP) + var isNotEnoughRemoteStorageSpace by booleanValue(KEY_NOT_ENOUGH_REMOTE_STORAGE_SPACE, false) + /** * If true, it means we have been told that remote storage is full, but we have not yet run any of our "garbage collection" tasks, like committing deletes * or pruning orphaned media. diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationIds.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationIds.java index d660265518..16992d7453 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationIds.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationIds.java @@ -32,6 +32,7 @@ public final class NotificationIds { public static final int STORY_MESSAGE_DELIVERY_FAILURE = 900000; public static final int UNREGISTERED_NOTIFICATION_ID = 20230102; public static final int NEW_LINKED_DEVICE = 120400; + public static final int OUT_OF_REMOTE_STORAGE = 120500; private NotificationIds() { } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a2b9dd0b3c..edab413f01 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -8586,5 +8586,10 @@ Unticked + + Backup storage full + + You\'ve reached your backup storage limit. Free up space in Signal to continue backing up chats and media. +