From 18f7a88d66b2b0610b835aec4f676f6365d7de33 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Fri, 20 Jun 2025 16:32:34 -0300 Subject: [PATCH] Add support filter after backup export failure. --- .../securesms/backup/v2/BackupRepository.kt | 32 +++++++++ .../backup/v2/ui/BackupAlertBottomSheet.kt | 34 +++++++++- .../ContactSupportDialogFragment.kt | 67 +++++++++++++++++++ .../contactsupport/ContactSupportViewModel.kt | 6 +- .../InternalBackupPlaygroundFragment.kt | 19 +++++- .../securesms/jobs/BackupMessagesJob.kt | 5 +- .../notifications/NotificationIds.java | 1 + app/src/main/res/values/strings.xml | 13 +++- 8 files changed, 166 insertions(+), 11 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/contactsupport/ContactSupportDialogFragment.kt 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 4d308fc78a..acd2608db0 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 @@ -9,6 +9,7 @@ import android.app.PendingIntent import android.database.Cursor import android.os.Environment import android.os.StatFs +import androidx.annotation.Discouraged import androidx.annotation.WorkerThread import androidx.core.app.NotificationCompat import kotlinx.coroutines.withContext @@ -302,6 +303,37 @@ object BackupRepository { AppDependencies.jobManager.add(CheckRestoreMediaLeftJob(RestoreAttachmentJob.constructQueueString(RestoreAttachmentJob.RestoreOperation.MANUAL))) } + fun markBackupFailure() { + SignalStore.backup.markMessageBackupFailure() + ArchiveUploadProgress.onMainBackupFileUploadFailure() + + if (!SignalStore.backup.hasBackupBeenUploaded) { + Log.w(TAG, "Failure of initial backup. Displaying notification.") + displayInitialBackupFailureNotification() + } + } + + @Discouraged("This is only public to allow internal settings to call it directly.") + fun displayInitialBackupFailureNotification() { + 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_failed)) + .setContentText(context.getString(R.string.Notification_an_error_occurred_and_your_backup)) + .setContentIntent(pendingIntent) + .setAutoCancel(true) + .build() + + ServiceUtil.getNotificationManager(context).notify(NotificationIds.INITIAL_BACKUP_FAILED, notification) + } + + fun clearBackupFailure() { + SignalStore.backup.clearMessageBackupFailure() + ServiceUtil.getNotificationManager(AppDependencies.application).cancel(NotificationIds.INITIAL_BACKUP_FAILED) + } + fun markOutOfRemoteStorageError() { val context = AppDependencies.application 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 fc78daf93f..bf1c59509e 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 @@ -33,11 +33,17 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.LinkAnnotation +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.withLink +import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat import androidx.core.os.BundleCompat @@ -53,6 +59,7 @@ import org.signal.core.ui.compose.theme.SignalTheme import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.backup.v2.BackupRepository import org.thoughtcrime.securesms.billing.launchManageBackupsSubscription +import org.thoughtcrime.securesms.components.contactsupport.ContactSupportDialogFragment import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment import org.thoughtcrime.securesms.jobs.BackupMessagesJob @@ -139,7 +146,12 @@ class BackupAlertBottomSheet : ComposeBottomSheetDialogFragment() { is BackupAlert.DiskFull -> { displaySkipRestoreDialog() } - BackupAlert.BackupFailed -> CommunicationActions.openBrowserLink(requireContext(), requireContext().getString(R.string.backup_failed_support_url)) + BackupAlert.BackupFailed -> { + ContactSupportDialogFragment.create( + subject = R.string.BackupAlertBottomSheet_network_failure_support_email, + filter = R.string.BackupAlertBottomSheet_export_failure_filter + ).show(parentFragmentManager, null) + } BackupAlert.CouldNotRedeemBackup -> CommunicationActions.openBrowserLink(requireContext(), requireContext().getString(R.string.backup_support_url)) // TODO [backups] final url } @@ -399,8 +411,24 @@ private fun DiskFullBody(requiredSpace: String) { @Composable private fun BackupFailedBody() { + val context = LocalContext.current + val text = buildAnnotatedString { + append(stringResource(id = R.string.BackupAlertBottomSheet__an_error_occurred)) + append(" ") + + withLink( + LinkAnnotation.Clickable(tag = "learn-more") { + CommunicationActions.openBrowserLink(context, context.getString(R.string.backup_failed_support_url)) + } + ) { + withStyle(SpanStyle(color = MaterialTheme.colorScheme.primary)) { + append(stringResource(id = R.string.BackupAlertBottomSheet__learn_more)) + } + } + } + Text( - text = stringResource(id = R.string.BackupAlertBottomSheet__an_error_occurred), + text = text, textAlign = TextAlign.Center, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(bottom = 36.dp) @@ -463,7 +491,7 @@ private fun rememberSecondaryActionResource(backupAlert: BackupAlert): Int { is BackupAlert.MediaBackupsAreOff -> error("Not supported.") 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 + is BackupAlert.BackupFailed -> R.string.BackupAlertBottomSheet__contact_support BackupAlert.CouldNotRedeemBackup -> R.string.BackupAlertBottomSheet__learn_more } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/contactsupport/ContactSupportDialogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/contactsupport/ContactSupportDialogFragment.kt new file mode 100644 index 0000000000..174e0dc607 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/contactsupport/ContactSupportDialogFragment.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.contactsupport + +import androidx.annotation.StringRes +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.core.os.bundleOf +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import org.thoughtcrime.securesms.compose.ComposeDialogFragment +import org.thoughtcrime.securesms.util.viewModel + +/** + * Three-option contact support dialog fragment. + */ +class ContactSupportDialogFragment : ComposeDialogFragment() { + + companion object { + private const val SUBJECT = "subject" + private const val FILTER = "filter" + + fun create( + @StringRes subject: Int, + @StringRes filter: Int + ): ContactSupportDialogFragment { + return ContactSupportDialogFragment().apply { + arguments = bundleOf( + SUBJECT to subject, + FILTER to filter + ) + } + } + } + + private val contactSupportViewModel: ContactSupportViewModel by viewModel { + ContactSupportViewModel( + showInitially = true + ) + } + + private val subject: Int by lazy { requireArguments().getInt(SUBJECT) } + private val filter: Int by lazy { requireArguments().getInt(FILTER) } + + @Composable + override fun DialogContent() { + val contactSupportState by contactSupportViewModel.state.collectAsStateWithLifecycle() + + SendSupportEmailEffect( + contactSupportState = contactSupportState, + subjectRes = subject, + filterRes = filter + ) { + contactSupportViewModel.hideContactSupport() + dismissAllowingStateLoss() + } + + if (contactSupportState.show) { + ContactSupportDialog( + showInProgress = contactSupportState.showAsProgress, + callbacks = contactSupportViewModel + ) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/contactsupport/ContactSupportViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/contactsupport/ContactSupportViewModel.kt index 2fcba2a180..60eac499a9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/contactsupport/ContactSupportViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/contactsupport/ContactSupportViewModel.kt @@ -18,10 +18,12 @@ import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogRepository /** * Intended to be used to drive [ContactSupportDialog]. */ -class ContactSupportViewModel : ViewModel(), ContactSupportCallbacks { +class ContactSupportViewModel( + val showInitially: Boolean = false +) : ViewModel(), ContactSupportCallbacks { private val submitDebugLogRepository: SubmitDebugLogRepository = SubmitDebugLogRepository() - private val store: MutableStateFlow = MutableStateFlow(ContactSupportState()) + private val store: MutableStateFlow = MutableStateFlow(ContactSupportState(show = showInitially)) val state: StateFlow = store.asStateFlow() 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 4b3249af74..d57b1c3f94 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 @@ -72,6 +72,8 @@ 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.backup.v2.ui.BackupAlert +import org.thoughtcrime.securesms.backup.v2.ui.BackupAlertBottomSheet 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.compose.ComposeFragment @@ -231,6 +233,12 @@ class InternalBackupPlaygroundFragment : ComposeFragment() { } .setNegativeButton("Cancel", null) .show() + }, + onDisplayInitialBackupFailureSheet = { + BackupRepository.displayInitialBackupFailureNotification() + BackupAlertBottomSheet + .create(BackupAlert.BackupFailed) + .show(parentFragmentManager, null) } ) }, @@ -314,7 +322,8 @@ fun Screen( onImportEncryptedBackupFromDiskClicked: () -> Unit = {}, onImportEncryptedBackupFromDiskDismissed: () -> Unit = {}, onImportEncryptedBackupFromDiskConfirmed: (aci: String, backupKey: String) -> Unit = { _, _ -> }, - onDeleteRemoteBackup: () -> Unit = {} + onDeleteRemoteBackup: () -> Unit = {}, + onDisplayInitialBackupFailureSheet: () -> Unit = {} ) { val context = LocalContext.current val scrollState = rememberScrollState() @@ -506,11 +515,17 @@ fun Screen( Dividers.Default() + Rows.TextRow( + text = "Display initial backup failure sheet", + label = "This will display the error sheet immediately and force the notification to display.", + onClick = onDisplayInitialBackupFailureSheet + ) + Rows.TextRow( text = "Mark backup failure", label = "This will display the error sheet when returning to the chats list.", onClick = { - SignalStore.backup.internalSetBackupFailedErrorState() + BackupRepository.markBackupFailure() } ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupMessagesJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupMessagesJob.kt index 34c23bf664..f21c9dc2d8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupMessagesJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupMessagesJob.kt @@ -92,8 +92,7 @@ class BackupMessagesJob private constructor( override fun onFailure() { if (!isCanceled) { Log.w(TAG, "Failed to backup user messages. Marking failure state.") - SignalStore.backup.markMessageBackupFailure() - ArchiveUploadProgress.onMainBackupFileUploadFailure() + BackupRepository.markBackupFailure() } } @@ -200,7 +199,7 @@ class BackupMessagesJob private constructor( ArchiveUploadProgress.onMessageBackupFinishedEarly() } - SignalStore.backup.clearMessageBackupFailure() + BackupRepository.clearBackupFailure() SignalDatabase.backupMediaSnapshots.commitPendingRows() AppDependencies.jobManager.add(ArchiveCommitAttachmentDeletesJob()) 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 181e5436ea..fd61eae3d6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationIds.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationIds.java @@ -35,6 +35,7 @@ public final class NotificationIds { 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; + public static final int INITIAL_BACKUP_FAILED = 120501; private NotificationIds() { } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6c88be5dcb..6d57c8d141 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -7878,6 +7878,8 @@ Learn more + + Contact support Skip restore @@ -8501,7 +8503,12 @@ Signal Android Backup restore network error - Signal Android Backup restore network error + Android SignalBackups Import Failed + + + Signal Android Backup export network error + + Android SignalBackups Export Failed Submit debug log? @@ -8595,6 +8602,10 @@ Backup storage full You\'ve reached your backup storage limit. Free up space in Signal to continue backing up chats and media. + + Backup failed + + An error occurred and your backup could not be completed. Tap for details.