Add support filter after backup export failure.

This commit is contained in:
Alex Hart
2025-06-20 16:32:34 -03:00
committed by Cody Henthorne
parent eeae9579d9
commit 18f7a88d66
8 changed files with 166 additions and 11 deletions

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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
)
}
}
}

View File

@@ -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<ContactSupportState> = MutableStateFlow(ContactSupportState())
private val store: MutableStateFlow<ContactSupportState> = MutableStateFlow(ContactSupportState(show = showInitially))
val state: StateFlow<ContactSupportState> = store.asStateFlow()

View File

@@ -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()
}
)

View File

@@ -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())

View File

@@ -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() { }