mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-15 07:28:30 +00:00
Provide retry UX for tier restore network failures.
This commit is contained in:
committed by
Greyson Parrelli
parent
9b527f7c6c
commit
eb44dd4318
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
Reference in New Issue
Block a user