Provide retry UX for tier restore network failures.

This commit is contained in:
Cody Henthorne
2025-03-31 11:30:49 -04:00
committed by Greyson Parrelli
parent 9b527f7c6c
commit eb44dd4318
11 changed files with 363 additions and 36 deletions

View File

@@ -0,0 +1,93 @@
/*
* 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.LaunchedEffect
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import org.signal.core.ui.compose.Dialogs
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalPreview
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.SupportEmailUtil
interface ContactSupportCallbacks {
fun submitWithDebuglog()
fun submitWithoutDebuglog()
fun cancel()
object Empty : ContactSupportCallbacks {
override fun submitWithDebuglog() = Unit
override fun submitWithoutDebuglog() = Unit
override fun cancel() = Unit
}
}
/**
* Three-option contact support dialog.
*/
@Composable
fun ContactSupportDialog(
showInProgress: Boolean,
callbacks: ContactSupportCallbacks
) {
if (showInProgress) {
Dialogs.IndeterminateProgressDialog()
} else {
Dialogs.AdvancedAlertDialog(
title = stringResource(R.string.ContactSupportDialog_submit_debug_log),
body = stringResource(R.string.ContactSupportDialog_your_debug_logs),
positive = stringResource(R.string.ContactSupportDialog_submit_with_debug),
onPositive = { callbacks.submitWithDebuglog() },
neutral = stringResource(R.string.ContactSupportDialog_submit_without_debug),
onNeutral = { callbacks.submitWithoutDebuglog() },
negative = stringResource(android.R.string.cancel),
onNegative = { callbacks.cancel() }
)
}
}
/**
* Used in conjunction with [ContactSupportDialog] and [ContactSupportViewModel] to trigger
* sending an email when ready.
*/
@Composable
fun SendSupportEmailEffect(
contactSupportState: ContactSupportViewModel.ContactSupportState,
@StringRes subjectRes: Int,
@StringRes filterRes: Int,
hide: () -> Unit
) {
val context = LocalContext.current
LaunchedEffect(contactSupportState.sendEmail) {
if (contactSupportState.sendEmail) {
val subject = context.getString(subjectRes)
val prefix = if (contactSupportState.debugLogUrl != null) {
"\n${context.getString(R.string.HelpFragment__debug_log)} ${contactSupportState.debugLogUrl}\n\n"
} else {
""
}
val body = SupportEmailUtil.generateSupportEmailBody(context, filterRes, prefix, null)
CommunicationActions.openEmail(context, SupportEmailUtil.getSupportEmailAddress(context), subject, body)
hide()
}
}
}
@SignalPreview
@Composable
private fun ContactSupportDialogPreview() {
Previews.Preview {
ContactSupportDialog(
false,
ContactSupportCallbacks.Empty
)
}
}

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.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.signal.core.util.orNull
import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogRepository
/**
* Intended to be used to drive [ContactSupportDialog].
*/
class ContactSupportViewModel : ViewModel(), ContactSupportCallbacks {
private val submitDebugLogRepository: SubmitDebugLogRepository = SubmitDebugLogRepository()
private val store: MutableStateFlow<ContactSupportState> = MutableStateFlow(ContactSupportState())
val state: StateFlow<ContactSupportState> = store.asStateFlow()
fun showContactSupport() {
store.update { it.copy(show = true) }
}
fun hideContactSupport() {
store.update { ContactSupportState() }
}
fun contactSupport(includeLogs: Boolean) {
viewModelScope.launch {
if (includeLogs) {
store.update { it.copy(showAsProgress = true) }
submitDebugLogRepository.buildAndSubmitLog { result ->
store.update { ContactSupportState(sendEmail = true, debugLogUrl = result.orNull()) }
}
} else {
store.update { ContactSupportState(sendEmail = true) }
}
}
}
override fun submitWithDebuglog() {
contactSupport(true)
}
override fun submitWithoutDebuglog() {
contactSupport(false)
}
override fun cancel() {
hideContactSupport()
}
data class ContactSupportState(
val show: Boolean = false,
val showAsProgress: Boolean = false,
val sendEmail: Boolean = false,
val debugLogUrl: String? = null
)
}

View File

@@ -32,6 +32,14 @@ val RestoreDecisionState.isWantingManualRemoteRestore: Boolean
else -> false
}
val RestoreDecisionState.includeDeviceToDeviceTransfer: Boolean
get() = when (this.decisionState) {
RestoreDecisionState.State.INTEND_TO_RESTORE -> {
this.intendToRestoreData?.hasOldDevice == true
}
else -> true
}
/** Has a final decision been made regarding restoring. */
val RestoreDecisionState.isTerminal: Boolean
get() = !isDecisionPending

View File

@@ -87,6 +87,7 @@ import kotlin.jvm.optionals.getOrNull
import kotlin.math.max
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
/**
* ViewModel shared across all of registration.
@@ -898,7 +899,14 @@ class RegistrationViewModel : ViewModel() {
if (SignalStore.account.restoredAccountEntropyPool) {
Log.d(TAG, "Restoring backup tier")
BackupRepository.restoreBackupTier(SignalStore.account.requireAci())
var tries = 0
while (tries < 3 && !SignalStore.backup.isBackupTierRestored) {
if (tries > 0) {
delay(1.seconds)
}
BackupRepository.restoreBackupTier(SignalStore.account.requireAci())
tries++
}
}
refreshRemoteConfig()

View File

@@ -23,6 +23,9 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import org.signal.core.ui.compose.Dialogs
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.contactsupport.ContactSupportDialog
import org.thoughtcrime.securesms.components.contactsupport.ContactSupportViewModel
import org.thoughtcrime.securesms.components.contactsupport.SendSupportEmailEffect
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult
import org.thoughtcrime.securesms.registrationv3.ui.RegistrationCheckpoint
@@ -42,6 +45,7 @@ class EnterBackupKeyFragment : ComposeFragment() {
private val sharedViewModel by activityViewModels<RegistrationViewModel>()
private val viewModel by viewModels<EnterBackupKeyViewModel>()
private val contactSupportViewModel: ContactSupportViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@@ -75,6 +79,15 @@ class EnterBackupKeyFragment : ComposeFragment() {
override fun FragmentContent() {
val state by viewModel.state.collectAsStateWithLifecycle()
val sharedState by sharedViewModel.state.collectAsStateWithLifecycle()
val contactSupportState: ContactSupportViewModel.ContactSupportState by contactSupportViewModel.state.collectAsStateWithLifecycle()
SendSupportEmailEffect(
contactSupportState = contactSupportState,
subjectRes = R.string.EnterBackupKey_network_failure_support_email,
filterRes = R.string.EnterBackupKey_network_failure_support_email_filter
) {
contactSupportViewModel.hideContactSupport()
}
EnterBackupKeyScreen(
backupKey = viewModel.backupKey,
@@ -92,27 +105,31 @@ class EnterBackupKeyFragment : ComposeFragment() {
pin = null
)
},
onLearnMore = { CommunicationActions.openBrowserLink(requireContext(), LEARN_MORE_URL) },
onSkip = {
sharedViewModel.skipRestore()
findNavController().safeNavigate(EnterBackupKeyFragmentDirections.goToEnterPhoneNumber(EnterPhoneNumberMode.RESTART_AFTER_COLLECTION))
},
dialogContent = {
if (state.showStorageAccountRestoreProgress) {
Dialogs.IndeterminateProgressDialog()
if (contactSupportState.show) {
ContactSupportDialog(
showInProgress = contactSupportState.showAsProgress,
callbacks = contactSupportViewModel
)
} else {
ErrorContent(
state = state,
onBackupTierRetry = { sharedViewModel.restoreBackupTier() },
onSkipRestoreAfterRegistration = {
onBackupTierRetry = {
viewModel.incrementBackupTierRetry()
sharedViewModel.restoreBackupTier()
},
onAbandonRemoteRestoreAfterRegistration = {
viewLifecycleOwner.lifecycleScope.launch {
sharedViewModel.skipRestore()
viewModel.performStorageServiceAccountRestoreIfNeeded()
sharedViewModel.resumeNormalRegistration()
}
},
onBackupTierNotRestoredDismiss = viewModel::hideRestoreBackupKeyFailed,
onContactSupport = { contactSupportViewModel.showContactSupport() },
onRegistrationErrorDismiss = viewModel::clearRegistrationError,
onBackupKeyHelp = { CommunicationActions.openBrowserLink(requireContext(), LEARN_MORE_URL) }
)
@@ -126,19 +143,53 @@ class EnterBackupKeyFragment : ComposeFragment() {
private fun ErrorContent(
state: EnterBackupKeyViewModel.EnterBackupKeyState,
onBackupTierRetry: () -> Unit = {},
onSkipRestoreAfterRegistration: () -> Unit = {},
onAbandonRemoteRestoreAfterRegistration: () -> Unit = {},
onBackupTierNotRestoredDismiss: () -> Unit = {},
onContactSupport: () -> Unit = {},
onRegistrationErrorDismiss: () -> Unit = {},
onBackupKeyHelp: () -> Unit = {}
) {
if (state.showBackupTierNotRestoreError) {
if (state.showBackupTierNotRestoreError == EnterBackupKeyViewModel.TierRestoreError.NETWORK_ERROR) {
if (state.tierRetryAttempts > 1) {
Dialogs.AdvancedAlertDialog(
title = stringResource(R.string.EnterBackupKey_cant_restore_backup),
body = stringResource(R.string.EnterBackupKey_your_backup_cant_be_restored_right_now),
positive = stringResource(R.string.EnterBackupKey_try_again),
neutral = stringResource(R.string.EnterBackupKey_contact_support),
negative = stringResource(android.R.string.cancel),
onPositive = {
onBackupTierNotRestoredDismiss()
onBackupTierRetry()
},
onNeutral = {
onBackupTierNotRestoredDismiss()
onContactSupport()
},
onNegative = {
onBackupTierNotRestoredDismiss()
onAbandonRemoteRestoreAfterRegistration()
}
)
} else {
Dialogs.SimpleAlertDialog(
title = stringResource(R.string.EnterBackupKey_cant_restore_backup),
body = stringResource(R.string.EnterBackupKey_your_backup_cant_be_restored_right_now),
confirm = stringResource(R.string.EnterBackupKey_try_again),
dismiss = stringResource(android.R.string.cancel),
onConfirm = onBackupTierRetry,
onDeny = onAbandonRemoteRestoreAfterRegistration,
onDismiss = onBackupTierNotRestoredDismiss,
onDismissRequest = {}
)
}
} else if (state.showBackupTierNotRestoreError == EnterBackupKeyViewModel.TierRestoreError.NOT_FOUND) {
Dialogs.SimpleAlertDialog(
title = stringResource(R.string.EnterBackupKey_backup_not_found),
body = stringResource(R.string.EnterBackupKey_backup_key_you_entered_is_correct_but_no_backup),
confirm = stringResource(R.string.EnterBackupKey_try_again),
dismiss = stringResource(R.string.EnterBackupKey_skip_restore),
onConfirm = onBackupTierRetry,
onDeny = onSkipRestoreAfterRegistration,
onDeny = onAbandonRemoteRestoreAfterRegistration,
onDismiss = onBackupTierNotRestoredDismiss
)
} else if (state.showRegistrationError) {

View File

@@ -88,7 +88,7 @@ class EnterBackupKeyViewModel : ViewModel() {
fun handleBackupTierNotRestored() {
store.update {
it.copy(
showBackupTierNotRestoreError = true
showBackupTierNotRestoreError = if (SignalStore.backup.isBackupTierRestored) TierRestoreError.NOT_FOUND else TierRestoreError.NETWORK_ERROR
)
}
}
@@ -96,16 +96,13 @@ class EnterBackupKeyViewModel : ViewModel() {
fun hideRestoreBackupKeyFailed() {
store.update {
it.copy(
showBackupTierNotRestoreError = false
showBackupTierNotRestoreError = null
)
}
}
suspend fun performStorageServiceAccountRestoreIfNeeded() {
if (SignalStore.account.restoredAccountEntropyPool || SignalStore.svr.masterKeyForInitialDataRestore != null) {
store.update { it.copy(showBackupTierNotRestoreError = false, showStorageAccountRestoreProgress = true) }
StorageServiceRestore.restore()
}
fun incrementBackupTierRetry() {
store.update { it.copy(tierRetryAttempts = it.tierRetryAttempts + 1) }
}
data class EnterBackupKeyState(
@@ -114,9 +111,14 @@ class EnterBackupKeyViewModel : ViewModel() {
val chunkLength: Int,
val isRegistering: Boolean = false,
val showRegistrationError: Boolean = false,
val showBackupTierNotRestoreError: Boolean = false,
val showBackupTierNotRestoreError: TierRestoreError? = null,
val registerAccountResult: RegisterAccountResult? = null,
val aepValidationError: AEPValidationError? = null,
val showStorageAccountRestoreProgress: Boolean = false
val tierRetryAttempts: Int = 0
)
enum class TierRestoreError {
NOT_FOUND,
NETWORK_ERROR
}
}

View File

@@ -9,6 +9,7 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -54,6 +55,10 @@ import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.backup.v2.RestoreV2Event
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsTypeFeature
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsTypeFeatureRow
import org.thoughtcrime.securesms.components.contactsupport.ContactSupportCallbacks
import org.thoughtcrime.securesms.components.contactsupport.ContactSupportDialog
import org.thoughtcrime.securesms.components.contactsupport.ContactSupportViewModel
import org.thoughtcrime.securesms.components.contactsupport.SendSupportEmailEffect
import org.thoughtcrime.securesms.conversation.v2.registerForLifecycle
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.registrationv3.ui.shared.RegistrationScreen
@@ -81,6 +86,8 @@ class RemoteRestoreActivity : BaseActivity() {
RemoteRestoreViewModel(intent.getBooleanExtra(KEY_ONLY_OPTION, false))
}
private val contactSupportViewModel: ContactSupportViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -99,12 +106,24 @@ class RemoteRestoreActivity : BaseActivity() {
setContent {
val state: RemoteRestoreViewModel.ScreenState by viewModel.state.collectAsStateWithLifecycle()
val contactSupportState: ContactSupportViewModel.ContactSupportState by contactSupportViewModel.state.collectAsStateWithLifecycle()
SendSupportEmailEffect(
contactSupportState = contactSupportState,
subjectRes = R.string.EnterBackupKey_network_failure_support_email,
filterRes = R.string.EnterBackupKey_network_failure_support_email_filter
) {
contactSupportViewModel.hideContactSupport()
}
SignalTheme {
Surface {
RestoreFromBackupContent(
state = state,
contactSupportState = contactSupportState,
onRestoreBackupClick = { viewModel.restore() },
onRetryRestoreTier = { viewModel.reload() },
onContactSupport = { contactSupportViewModel.showContactSupport() },
onCancelClick = {
lifecycleScope.launch {
if (state.isRemoteRestoreOnlyOption) {
@@ -116,10 +135,11 @@ class RemoteRestoreActivity : BaseActivity() {
finish()
}
},
onErrorDialogDismiss = { viewModel.clearError() },
onImportErrorDialogDismiss = { viewModel.clearError() },
onUpdateSignal = {
PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(this)
}
},
contactSupportCallbacks = contactSupportViewModel
)
}
}
@@ -137,10 +157,14 @@ class RemoteRestoreActivity : BaseActivity() {
@Composable
private fun RestoreFromBackupContent(
state: RemoteRestoreViewModel.ScreenState,
contactSupportState: ContactSupportViewModel.ContactSupportState = ContactSupportViewModel.ContactSupportState(),
onRestoreBackupClick: () -> Unit = {},
onRetryRestoreTier: () -> Unit = {},
onContactSupport: () -> Unit = {},
onCancelClick: () -> Unit = {},
onErrorDialogDismiss: () -> Unit = {},
onUpdateSignal: () -> Unit = {}
onImportErrorDialogDismiss: () -> Unit = {},
onUpdateSignal: () -> Unit = {},
contactSupportCallbacks: ContactSupportCallbacks = ContactSupportCallbacks.Empty
) {
when (state.loadState) {
RemoteRestoreViewModel.ScreenState.LoadState.LOADING -> {
@@ -154,7 +178,7 @@ private fun RestoreFromBackupContent(
state = state,
onRestoreBackupClick = onRestoreBackupClick,
onCancelClick = onCancelClick,
onErrorDialogDismiss = onErrorDialogDismiss,
onImportErrorDialogDismiss = onImportErrorDialogDismiss,
onUpdateSignal = onUpdateSignal
)
}
@@ -164,7 +188,19 @@ private fun RestoreFromBackupContent(
}
RemoteRestoreViewModel.ScreenState.LoadState.FAILURE -> {
RestoreFailedDialog(onDismiss = onCancelClick)
if (contactSupportState.show) {
ContactSupportDialog(
showInProgress = contactSupportState.showAsProgress,
callbacks = contactSupportCallbacks
)
} else {
TierRestoreFailedDialog(
loadAttempts = state.loadAttempts,
onRetryRestore = onRetryRestoreTier,
onContactSupport = onContactSupport,
onCancel = onCancelClick
)
}
}
RemoteRestoreViewModel.ScreenState.LoadState.STORAGE_SERVICE_RESTORE -> {
@@ -178,7 +214,7 @@ private fun BackupAvailableContent(
state: RemoteRestoreViewModel.ScreenState,
onRestoreBackupClick: () -> Unit,
onCancelClick: () -> Unit,
onErrorDialogDismiss: () -> Unit,
onImportErrorDialogDismiss: () -> Unit,
onUpdateSignal: () -> Unit
) {
val subtitle = if (state.backupSize.bytes > 0) {
@@ -247,9 +283,9 @@ private fun BackupAvailableContent(
is RemoteRestoreViewModel.ImportState.Restored -> Unit
RemoteRestoreViewModel.ImportState.Failed -> {
if (SignalStore.backup.hasInvalidBackupVersion) {
InvalidBackupVersionDialog(onUpdateSignal = onUpdateSignal, onDismiss = onErrorDialogDismiss)
InvalidBackupVersionDialog(onUpdateSignal = onUpdateSignal, onDismiss = onImportErrorDialogDismiss)
} else {
RestoreFailedDialog(onDismiss = onErrorDialogDismiss)
RestoreFailedDialog(onDismiss = onImportErrorDialogDismiss)
}
}
}
@@ -370,7 +406,7 @@ private fun RestoreProgressDialog(restoreProgress: RestoreV2Event?) {
val progressBytes = restoreProgress.count.toUnitString()
val totalBytes = restoreProgress.estimatedTotalCount.toUnitString()
Text(
text = stringResource(id = R.string.RemoteRestoreActivity__s_of_s_s, progressBytes, totalBytes, "%.2f%%".format(restoreProgress.getProgress())),
text = stringResource(id = R.string.RemoteRestoreActivity__s_of_s_s, progressBytes, totalBytes, "%.2f%%".format(restoreProgress.getProgress() * 100)),
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(bottom = 12.dp)
)
@@ -422,6 +458,37 @@ fun RestoreFailedDialog(
)
}
@Composable
fun TierRestoreFailedDialog(
loadAttempts: Int = 0,
onRetryRestore: () -> Unit = {},
onContactSupport: () -> Unit = {},
onCancel: () -> Unit = {}
) {
if (loadAttempts > 2) {
Dialogs.AdvancedAlertDialog(
title = stringResource(R.string.EnterBackupKey_cant_restore_backup),
body = stringResource(R.string.EnterBackupKey_your_backup_cant_be_restored_right_now),
positive = stringResource(R.string.EnterBackupKey_try_again),
neutral = stringResource(R.string.EnterBackupKey_contact_support),
negative = stringResource(android.R.string.cancel),
onPositive = onRetryRestore,
onNeutral = onContactSupport,
onNegative = onCancel
)
} else {
Dialogs.SimpleAlertDialog(
title = stringResource(R.string.EnterBackupKey_cant_restore_backup),
body = stringResource(R.string.EnterBackupKey_your_backup_cant_be_restored_right_now),
confirm = stringResource(R.string.EnterBackupKey_try_again),
dismiss = stringResource(android.R.string.cancel),
onConfirm = onRetryRestore,
onDeny = onCancel,
onDismissRequest = {}
)
}
}
@SignalPreview
@Composable
private fun RestoreFailedDialogPreview() {

View File

@@ -51,7 +51,12 @@ class RemoteRestoreViewModel(isOnlyRestoreOption: Boolean) : ViewModel() {
val state: StateFlow<ScreenState> = store.asStateFlow()
init {
reload()
}
fun reload() {
viewModelScope.launch(Dispatchers.IO) {
store.update { it.copy(loadState = ScreenState.LoadState.LOADING, loadAttempts = it.loadAttempts + 1) }
val tier: MessageBackupTier? = BackupRepository.restoreBackupTier(SignalStore.account.requireAci())
store.update {
if (tier != null && SignalStore.backup.lastBackupTime > 0) {
@@ -161,7 +166,8 @@ class RemoteRestoreViewModel(isOnlyRestoreOption: Boolean) : ViewModel() {
val backupSize: ByteSize = 0.bytes,
val importState: ImportState = ImportState.None,
val restoreProgress: RestoreV2Event? = null,
val loadState: LoadState = if (backupTier != null) LoadState.LOADED else LoadState.LOADING
val loadState: LoadState = if (backupTier != null) LoadState.LOADED else LoadState.LOADING,
val loadAttempts: Int = 0
) {
fun isLoaded(): Boolean {

View File

@@ -20,6 +20,7 @@ import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.database.model.databaseprotos.RestoreDecisionState
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.keyvalue.Skipped
import org.thoughtcrime.securesms.keyvalue.includeDeviceToDeviceTransfer
import org.thoughtcrime.securesms.keyvalue.skippedRestoreChoice
import org.thoughtcrime.securesms.registrationv3.data.QuickRegistrationRepository
import org.thoughtcrime.securesms.registrationv3.ui.restore.RestoreMethod
@@ -74,8 +75,13 @@ class RestoreViewModel : ViewModel() {
}
fun getAvailableRestoreMethods(): List<RestoreMethod> {
if (SignalStore.registration.isOtherDeviceAndroid || SignalStore.registration.restoreDecisionState.skippedRestoreChoice) {
val methods = mutableListOf(RestoreMethod.FROM_OLD_DEVICE, RestoreMethod.FROM_LOCAL_BACKUP_V1)
if (SignalStore.registration.isOtherDeviceAndroid || SignalStore.registration.restoreDecisionState.skippedRestoreChoice || !SignalStore.backup.isBackupTierRestored) {
val methods = mutableListOf(RestoreMethod.FROM_LOCAL_BACKUP_V1)
if (SignalStore.registration.restoreDecisionState.includeDeviceToDeviceTransfer) {
methods.add(0, RestoreMethod.FROM_OLD_DEVICE)
}
when (SignalStore.backup.backupTier) {
MessageBackupTier.FREE -> methods.add(1, RestoreMethod.FROM_SIGNAL_BACKUPS)
MessageBackupTier.PAID -> methods.add(0, RestoreMethod.FROM_SIGNAL_BACKUPS)

View File

@@ -8292,6 +8292,25 @@
<string name="EnterBackupKey_backup_key_you_entered_is_correct_but_no_backup">The backup key you entered is correct, but there is no backup associated with it. If you still have your old phone, make sure backups are enabled and that a backup has been completed and try again.</string>
<!-- Dialog button shown during registration and trying to restore a remote backup to instead skip said backup process -->
<string name="EnterBackupKey_skip_restore">Skip restore</string>
<!-- Dialog title text when encountering backup restore network error -->
<string name="EnterBackupKey_cant_restore_backup">Can\'t restore backup</string>
<!-- Dialog body text when encountering backup restore network error -->
<string name="EnterBackupKey_your_backup_cant_be_restored_right_now">Your backup can\'t be restored right now. Check your internet connection and try again. </string>
<!-- Dialog button text to contact support on network error -->
<string name="EnterBackupKey_contact_support">Contact support</string>
<!-- Email subject when contacting support on a restore backup network issue -->
<string name="EnterBackupKey_network_failure_support_email">Signal Android Backup restore network error</string>
<string name="EnterBackupKey_network_failure_support_email_filter" translatable="false">Signal Android Backup restore network error</string>
<!-- Title of dialog asking to submit debuglogs -->
<string name="ContactSupportDialog_submit_debug_log">Submit debug log?</string>
<!-- Body of dialog explaining what debug logs are and that they are optional to submit -->
<string name="ContactSupportDialog_your_debug_logs">Your debug logs will help us troubleshoot your issue faster. Submitting your logs is optional.</string>
<!-- Button option to submit debug log when contacting support -->
<string name="ContactSupportDialog_submit_with_debug">Submit with debug log</string>
<!-- Button option to not submit debug log when contacting support -->
<string name="ContactSupportDialog_submit_without_debug">Submit without debug log</string>
<!-- Title for restore via qr screen -->
<string name="RestoreViaQr_title">Scan this code with your old phone</string>

View File

@@ -388,9 +388,9 @@ private fun AdvancedAlertDialogPreview() {
AdvancedAlertDialog(
title = "Title text",
body = "Body message text.",
positive = "Continue",
neutral = "Learn more",
negative = "Not now",
positive = "Positive",
neutral = "Neutral",
negative = "Negative",
onPositive = {},
onNegative = {},
onNeutral = {}