Add Out of Remote Storage sheet.

This commit is contained in:
Alex Hart
2025-08-06 12:54:03 -03:00
committed by Cody Henthorne
parent 50d809029e
commit 53a80589e3
10 changed files with 185 additions and 13 deletions

View File

@@ -420,7 +420,7 @@ object BackupRepository {
ServiceUtil.getNotificationManager(AppDependencies.application).cancel(NotificationIds.INITIAL_BACKUP_FAILED)
}
fun markOutOfRemoteStorageError() {
fun markOutOfRemoteStorageSpaceError() {
val context = AppDependencies.application
val pendingIntent = PendingIntent.getActivity(context, 0, AppSettingsActivity.remoteBackups(context), cancelCurrent())
@@ -434,15 +434,15 @@ object BackupRepository {
ServiceUtil.getNotificationManager(context).notify(NotificationIds.OUT_OF_REMOTE_STORAGE, notification)
SignalStore.backup.isNotEnoughRemoteStorageSpace = true
SignalStore.backup.markNotEnoughRemoteStorageSpace()
}
fun clearOutOfRemoteStorageError() {
SignalStore.backup.isNotEnoughRemoteStorageSpace = false
fun clearOutOfRemoteStorageSpaceError() {
SignalStore.backup.clearNotEnoughRemoteStorageSpace()
ServiceUtil.getNotificationManager(AppDependencies.application).cancel(NotificationIds.OUT_OF_REMOTE_STORAGE)
}
fun shouldDisplayOutOfStorageSpaceUx(): Boolean {
fun shouldDisplayOutOfRemoteStorageSpaceUx(): Boolean {
if (shouldNotDisplayBackupFailedMessaging()) {
return false
}
@@ -450,6 +450,18 @@ object BackupRepository {
return SignalStore.backup.isNotEnoughRemoteStorageSpace
}
fun shouldDisplayOutOfRemoteStorageSpaceSheet(): Boolean {
if (shouldNotDisplayBackupFailedMessaging()) {
return false
}
return SignalStore.backup.shouldDisplayNotEnoughRemoteStorageSpaceSheet
}
fun dismissOutOfRemoteStorageSpaceSheet() {
SignalStore.backup.dismissNotEnoughRemoteStorageSpaceSheet()
}
/**
* Whether the yellow dot should be displayed on the conversation list avatar.
*/

View File

@@ -36,6 +36,7 @@ object BackupAlertDelegate {
} else if (BackupRepository.shouldDisplayNoManualBackupForTimeoutSheet()) {
NoManualBackupBottomSheet().show(fragmentManager, FRAGMENT_TAG)
BackupRepository.displayManualBackupNotCreatedInThresholdNotification()
} else if (BackupRepository.shouldDisplayOutOfRemoteStorageSpaceSheet()) {
}
displayBackupDownloadNotifier(fragmentManager)

View File

@@ -0,0 +1,117 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui
import android.content.DialogInterface
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withLink
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
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.components.contactsupport.ContactSupportDialogFragment
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
import org.thoughtcrime.securesms.util.CommunicationActions
class NoRemoteStorageSpaceAvailableBottomSheet : ComposeBottomSheetDialogFragment() {
@Composable
override fun SheetContent() {
val context = LocalContext.current
NoRemoteStorageSpaceAvailableBottomSheetContent(
onLearnMoreClick = {
CommunicationActions.openBrowserLink(context, context.getString(R.string.backup_failed_support_url))
},
onContactSupportClick = {
ContactSupportDialogFragment.create(
subject = R.string.BackupAlertBottomSheet_network_failure_support_email,
filter = R.string.BackupAlertBottomSheet_export_failure_filter
).show(parentFragmentManager, null)
dismissAllowingStateLoss()
},
onOkClick = {
dismissAllowingStateLoss()
}
)
}
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
BackupRepository.dismissOutOfRemoteStorageSpaceSheet()
}
}
@Composable
private fun NoRemoteStorageSpaceAvailableBottomSheetContent(
onLearnMoreClick: () -> Unit,
onContactSupportClick: () -> Unit,
onOkClick: () -> Unit
) {
val primaryActionButtonLabel = stringResource(R.string.BackupAlertBottomSheet__contact_support)
val primaryActionButtonState = remember(primaryActionButtonLabel, onContactSupportClick) {
BackupAlertActionButtonState(
label = primaryActionButtonLabel,
callback = onContactSupportClick
)
}
val secondaryActionButtonLabel = stringResource(android.R.string.ok)
val secondaryActionButtonState = remember(secondaryActionButtonLabel, onOkClick) {
BackupAlertActionButtonState(
label = secondaryActionButtonLabel,
callback = onOkClick
)
}
BackupAlertBottomSheetContainer(
icon = {
BackupAlertIcon(iconColors = BackupsIconColors.Warning)
},
title = stringResource(R.string.BackupAlertBottomSheet__backup_failed),
primaryActionButtonState = primaryActionButtonState,
secondaryActionButtonState = secondaryActionButtonState
) {
val text = buildAnnotatedString {
append(stringResource(id = R.string.BackupAlertBottomSheet__an_error_occurred_and))
append(" ")
withLink(
LinkAnnotation.Clickable(tag = "learn-more") {
onLearnMoreClick()
}
) {
withStyle(SpanStyle(color = MaterialTheme.colorScheme.primary)) {
append(stringResource(id = R.string.BackupAlertBottomSheet__learn_more))
}
}
}
BackupAlertText(
text = text,
modifier = Modifier.padding(bottom = 36.dp)
)
}
}
@SignalPreview
@Composable
private fun NoRemoteStorageSpaceAvailableBottomSheetContentPreview() {
Previews.BottomSheetPreview {
NoRemoteStorageSpaceAvailableBottomSheetContent({}, {}, {})
}
}

View File

@@ -76,7 +76,7 @@ class AppSettingsViewModel : ViewModel() {
private fun getBackupFailureState(): BackupFailureState {
return if (!RemoteConfig.messageBackups) {
BackupFailureState.NONE
} else if (BackupRepository.shouldDisplayOutOfStorageSpaceUx()) {
} else if (BackupRepository.shouldDisplayOutOfRemoteStorageSpaceUx()) {
BackupFailureState.OUT_OF_STORAGE_SPACE
} else if (BackupRepository.shouldDisplayBackupFailedSettingsRow()) {
BackupFailureState.BACKUP_FAILED

View File

@@ -270,7 +270,7 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
}
private suspend fun performStateRefresh(lastPurchase: InAppPaymentTable.InAppPayment?) {
if (BackupRepository.shouldDisplayOutOfStorageSpaceUx()) {
if (BackupRepository.shouldDisplayOutOfRemoteStorageSpaceUx()) {
val paidType = BackupRepository.getPaidType()
if (paidType is NetworkResult.Success) {
@@ -278,7 +278,7 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
val estimatedSize = SignalDatabase.attachments.getEstimatedArchiveMediaSize().bytes
if (estimatedSize + 300.mebiBytes <= remoteStorageAllowance) {
BackupRepository.clearOutOfRemoteStorageError()
BackupRepository.clearOutOfRemoteStorageSpaceError()
}
_state.update {
@@ -301,7 +301,7 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
backupsFrequency = SignalStore.backup.backupFrequency,
canBackUpUsingCellular = SignalStore.backup.backupWithCellular,
canRestoreUsingCellular = SignalStore.backup.restoreWithCellular,
isOutOfStorageSpace = BackupRepository.shouldDisplayOutOfStorageSpaceUx(),
isOutOfStorageSpace = BackupRepository.shouldDisplayOutOfRemoteStorageSpaceUx(),
hasRedemptionError = lastPurchase?.data?.error?.data_ == "409"
)
}

View File

@@ -554,7 +554,7 @@ fun Screen(
Rows.TextRow(
text = "Mark out of remote storage space",
onClick = {
BackupRepository.markOutOfRemoteStorageError()
BackupRepository.markOutOfRemoteStorageSpaceError()
}
)

View File

@@ -148,7 +148,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) {
BackupRepository.markOutOfRemoteStorageError()
BackupRepository.markOutOfRemoteStorageSpaceError()
return Result.failure()
}

View File

@@ -75,6 +75,7 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
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_NOT_ENOUGH_REMOTE_STORAGE_SPACE_DISPLAY_SHEET = "backup.not.enough.remote.storage.space.display.sheet"
private const val KEY_MANUAL_NO_BACKUP_NOTIFIED = "backup.manual.no.backup.notified"
private const val KEY_USER_MANUALLY_SKIPPED_MEDIA_RESTORE = "backup.user.manually.skipped.media.restore"
@@ -233,6 +234,8 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
Log.i(TAG, "Setting backup tier to $value", Throwable(), true)
val serializedValue = MessageBackupTier.serialize(value)
val storedValue = MessageBackupTier.deserialize(getLong(KEY_BACKUP_TIER, -1))
if (value != null) {
store.beginWrite()
.putLong(KEY_BACKUP_TIER, serializedValue)
@@ -240,6 +243,12 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
.putBoolean(KEY_BACKUP_TIMESTAMP_RESTORED, true)
.apply()
if (storedValue != value) {
clearNotEnoughRemoteStorageSpace()
clearMessageBackupFailure()
clearMessageBackupFailureSheetWatermark()
}
deletionState = DeletionState.NONE
} else {
putLong(KEY_BACKUP_TIER, serializedValue)
@@ -356,7 +365,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)
val isNotEnoughRemoteStorageSpace by booleanValue(KEY_NOT_ENOUGH_REMOTE_STORAGE_SPACE, false)
val shouldDisplayNotEnoughRemoteStorageSpaceSheet by booleanValue(KEY_NOT_ENOUGH_REMOTE_STORAGE_SPACE_DISPLAY_SHEET, false)
var isNoBackupForManualUploadNotified by booleanValue(KEY_MANUAL_NO_BACKUP_NOTIFIED, false)
@@ -383,6 +393,36 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
NoRemoteArchiveGarbageCollectionPendingConstraint.Observer.notifyListeners()
}
/**
* When we are told by the server that we are out of storage space, we should show
* UX treatment to make the user aware of this.
*/
fun markNotEnoughRemoteStorageSpace() {
store.beginWrite()
.putBoolean(KEY_NOT_ENOUGH_REMOTE_STORAGE_SPACE, true)
.putBoolean(KEY_NOT_ENOUGH_REMOTE_STORAGE_SPACE_DISPLAY_SHEET, false)
.apply()
}
/**
* When we've regained enough space, we can remove the error.
*/
fun clearNotEnoughRemoteStorageSpace() {
store.beginWrite()
.putBoolean(KEY_NOT_ENOUGH_REMOTE_STORAGE_SPACE, false)
.putBoolean(KEY_NOT_ENOUGH_REMOTE_STORAGE_SPACE_DISPLAY_SHEET, false)
.apply()
}
/**
* Dismisses the sheet so as not to irritate the user.
*/
fun dismissNotEnoughRemoteStorageSpaceSheet() {
store.beginWrite()
.putBoolean(KEY_NOT_ENOUGH_REMOTE_STORAGE_SPACE_DISPLAY_SHEET, false)
.apply()
}
fun markMessageBackupFailure() {
store.beginWrite()
.putBoolean(KEY_BACKUP_FAIL, true)

View File

@@ -46,7 +46,7 @@ class MainToolbarViewModel : ViewModel() {
internalStateFlow.update {
it.copy(
hasFailedBackups = BackupRepository.shouldDisplayBackupFailedIndicator() || BackupRepository.shouldDisplayBackupAlreadyRedeemedIndicator(),
isOutOfRemoteStorageSpace = BackupRepository.shouldDisplayOutOfStorageSpaceUx(),
isOutOfRemoteStorageSpace = BackupRepository.shouldDisplayOutOfRemoteStorageSpaceUx(),
hasPassphrase = !SignalStore.settings.passphraseDisabled
)
}

View File

@@ -7951,6 +7951,8 @@
<string name="BackupAlertBottomSheet__couldnt_redeem_your_backups_subscription">Couldn\'t redeem your backups subscription</string>
<!-- Dialog text for when a backup fails to be created and ways to fix it -->
<string name="BackupAlertBottomSheet__an_error_occurred">An error occurred and your backup could not be completed. Make sure you\'re on the latest version of Signal and try again. If this problem persists, contact support.</string>
<!-- Dialog text for when a backup fails to be created and ways to fix it. Used for out of remote storage space error. -->
<string name="BackupAlertBottomSheet__an_error_occurred_and">An error occurred and your backup could not be completed. If this problem persists, contact support.</string>
<!-- Dialog action button that will allow you to check for any Signal version updates -->
<string name="BackupAlertBottomSheet__check_for_update">Check for update</string>
<!-- Backup redemption error sheet text line 1 -->