Add wiring and notification for out of remote space error.

This commit is contained in:
Alex Hart
2025-06-16 13:14:33 -03:00
committed by Michelle Tang
parent fed6a0db5e
commit b35b1db4bc
8 changed files with 79 additions and 14 deletions

View File

@@ -5,10 +5,12 @@
package org.thoughtcrime.securesms.backup.v2 package org.thoughtcrime.securesms.backup.v2
import android.app.PendingIntent
import android.database.Cursor import android.database.Cursor
import android.os.Environment import android.os.Environment
import android.os.StatFs import android.os.StatFs
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import androidx.core.app.NotificationCompat
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okio.ByteString import okio.ByteString
import okio.ByteString.Companion.toByteString 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.ByteSize
import org.signal.core.util.CursorUtil import org.signal.core.util.CursorUtil
import org.signal.core.util.EventTimer import org.signal.core.util.EventTimer
import org.signal.core.util.PendingIntentFlags.cancelCurrent
import org.signal.core.util.Stopwatch import org.signal.core.util.Stopwatch
import org.signal.core.util.bytes import org.signal.core.util.bytes
import org.signal.core.util.concurrent.LimitedWorker 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.VerificationFailedException
import org.signal.libsignal.zkgroup.backups.BackupLevel import org.signal.libsignal.zkgroup.backups.BackupLevel
import org.signal.libsignal.zkgroup.profiles.ProfileKey import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.attachments.Attachment import org.thoughtcrime.securesms.attachments.Attachment
import org.thoughtcrime.securesms.attachments.Cdn import org.thoughtcrime.securesms.attachments.Cdn
import org.thoughtcrime.securesms.attachments.DatabaseAttachment 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.stream.PlainTextBackupWriter
import org.thoughtcrime.securesms.backup.v2.ui.BackupAlert import org.thoughtcrime.securesms.backup.v2.ui.BackupAlert
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType 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.components.settings.app.subscription.RecurringInAppPaymentRepository
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider 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.SignalStore
import org.thoughtcrime.securesms.keyvalue.isDecisionPending import org.thoughtcrime.securesms.keyvalue.isDecisionPending
import org.thoughtcrime.securesms.net.SignalNetwork 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.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.RemoteConfig import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.util.ServiceUtil
import org.thoughtcrime.securesms.util.toMillis import org.thoughtcrime.securesms.util.toMillis
import org.whispersystems.signalservice.api.AccountEntropyPool import org.whispersystems.signalservice.api.AccountEntropyPool
import org.whispersystems.signalservice.api.ApplicationErrorAction import org.whispersystems.signalservice.api.ApplicationErrorAction
@@ -291,8 +299,34 @@ object BackupRepository {
AppDependencies.jobManager.add(CheckRestoreMediaLeftJob(RestoreAttachmentJob.constructQueueString(RestoreAttachmentJob.RestoreOperation.MANUAL))) 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 { fun shouldDisplayOutOfStorageSpaceUx(): Boolean {
return false // TODO [message-backups] Wire into actual error handling. if (shouldNotDisplayBackupFailedMessaging()) {
return false
}
return SignalStore.backup.isNotEnoughRemoteStorageSpace
} }
/** /**

View File

@@ -511,6 +511,7 @@ private fun RemoteBackupsSettingsContent(
backupsFrequency = state.backupsFrequency, backupsFrequency = state.backupsFrequency,
canBackUpUsingCellular = state.canBackUpUsingCellular, canBackUpUsingCellular = state.canBackUpUsingCellular,
canRestoreUsingCellular = state.canRestoreUsingCellular, canRestoreUsingCellular = state.canRestoreUsingCellular,
canBackUpNow = !state.isOutOfStorageSpace,
contentCallbacks = contentCallbacks contentCallbacks = contentCallbacks
) )
} else { } else {
@@ -813,6 +814,7 @@ private fun LazyListScope.appendBackupDetailsItems(
backupsFrequency: BackupFrequency, backupsFrequency: BackupFrequency,
canBackUpUsingCellular: Boolean, canBackUpUsingCellular: Boolean,
canRestoreUsingCellular: Boolean, canRestoreUsingCellular: Boolean,
canBackUpNow: Boolean,
contentCallbacks: ContentCallbacks contentCallbacks: ContentCallbacks
) { ) {
item { item {
@@ -845,6 +847,7 @@ private fun LazyListScope.appendBackupDetailsItems(
item { item {
LastBackupRow( LastBackupRow(
lastBackupTimestamp = lastBackupTimestamp, lastBackupTimestamp = lastBackupTimestamp,
enabled = canBackUpNow,
onBackupNowClick = contentCallbacks::onBackupNowClick onBackupNowClick = contentCallbacks::onBackupNowClick
) )
} }
@@ -1421,6 +1424,7 @@ private fun getBackupUploadPhaseProgressString(state: ArchiveUploadProgressState
@Composable @Composable
private fun LastBackupRow( private fun LastBackupRow(
lastBackupTimestamp: Long, lastBackupTimestamp: Long,
enabled: Boolean,
onBackupNowClick: () -> Unit onBackupNowClick: () -> Unit
) { ) {
Row( 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)) Text(text = stringResource(id = R.string.RemoteBackupsSettingsFragment__back_up_now))
} }
} }
@@ -1909,6 +1913,7 @@ private fun LastBackupRowPreview() {
Previews.Preview { Previews.Preview {
LastBackupRow( LastBackupRow(
lastBackupTimestamp = -1, lastBackupTimestamp = -1,
enabled = true,
onBackupNowClick = {} onBackupNowClick = {}
) )
} }

View File

@@ -22,6 +22,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.reactive.asFlow import kotlinx.coroutines.reactive.asFlow
import org.signal.core.util.bytes import org.signal.core.util.bytes
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.signal.core.util.mebiBytes
import org.signal.core.util.throttleLatest import org.signal.core.util.throttleLatest
import org.signal.donations.InAppPaymentType import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.backup.ArchiveUploadProgress import org.thoughtcrime.securesms.backup.ArchiveUploadProgress
@@ -223,6 +224,25 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
} }
private suspend fun performStateRefresh(lastPurchase: InAppPaymentTable.InAppPayment?) { 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 { _state.update {
it.copy( it.copy(
tier = SignalStore.backup.backupTier, 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) val state = BackupStateRepository.resolveBackupState(lastPurchase)
_state.update { _state.update {
it.copy(backupState = state) it.copy(backupState = state)

View File

@@ -70,6 +70,7 @@ import org.signal.core.util.Base64
import org.signal.core.util.Hex import org.signal.core.util.Hex
import org.signal.core.util.getLength import org.signal.core.util.getLength
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier 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.DialogState
import org.thoughtcrime.securesms.components.settings.app.internal.backup.InternalBackupPlaygroundViewModel.ScreenState 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)) Spacer(modifier = Modifier.height(8.dp))
} }
} }

View File

@@ -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.") val remoteStorageQuota = getServerQuota() ?: return Result.retry(defaultBackoff()).logW(TAG, "[$attachmentId] Failed to fetch server quota! Retrying.")
if (SignalDatabase.attachments.getEstimatedArchiveMediaSize() > remoteStorageQuota.inWholeBytes) { if (SignalDatabase.attachments.getEstimatedArchiveMediaSize() > remoteStorageQuota.inWholeBytes) {
// [TODO] Handle too much data case BackupRepository.markOutOfRemoteStorageError()
return Result.failure() return Result.failure()
} }

View File

@@ -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_FAIL_SPACE_REMAINING = "backup.failed.space.remaining"
private const val KEY_BACKUP_ALREADY_REDEEMED = "backup.already.redeemed" private const val KEY_BACKUP_ALREADY_REDEEMED = "backup.already.redeemed"
private const val KEY_INVALID_BACKUP_VERSION = "backup.invalid.version" 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_USER_MANUALLY_SKIPPED_MEDIA_RESTORE = "backup.user.manually.skipped.media.restore"
private const val KEY_BACKUP_EXPIRED_AND_DOWNGRADED = "backup.expired.and.downgraded" 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. */ /** 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) 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 * 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. * or pruning orphaned media.

View File

@@ -32,6 +32,7 @@ public final class NotificationIds {
public static final int STORY_MESSAGE_DELIVERY_FAILURE = 900000; public static final int STORY_MESSAGE_DELIVERY_FAILURE = 900000;
public static final int UNREGISTERED_NOTIFICATION_ID = 20230102; public static final int UNREGISTERED_NOTIFICATION_ID = 20230102;
public static final int NEW_LINKED_DEVICE = 120400; public static final int NEW_LINKED_DEVICE = 120400;
public static final int OUT_OF_REMOTE_STORAGE = 120500;
private NotificationIds() { } private NotificationIds() { }

View File

@@ -8586,5 +8586,10 @@
<!-- Accessibility label describing an unchecked checkbox. --> <!-- Accessibility label describing an unchecked checkbox. -->
<string name="SignalCheckbox_accessibility_unchecked_description">Unticked</string> <string name="SignalCheckbox_accessibility_unchecked_description">Unticked</string>
<!-- Notification title for when user is out of remote storage -->
<string name="Notification_backup_storage_full">Backup storage full</string>
<!-- Notification body for when user is out of remote storage -->
<string name="Notification_youve_reached_your_backup_storage_limit">You\'ve reached your backup storage limit. Free up space in Signal to continue backing up chats and media.</string>
<!-- EOF --> <!-- EOF -->
</resources> </resources>