Do regv3 storage service restore flows right.

This commit is contained in:
Cody Henthorne
2025-02-26 09:45:14 -05:00
committed by Greyson Parrelli
parent a31ed28b5f
commit 0b3a949264
24 changed files with 291 additions and 176 deletions

View File

@@ -17,16 +17,8 @@ import org.thoughtcrime.securesms.BaseActivity
import org.thoughtcrime.securesms.MainActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.keyvalue.isDecisionPending
import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity
import org.thoughtcrime.securesms.pin.PinRestoreActivity
import org.thoughtcrime.securesms.profiles.AvatarHelper
import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.registration.sms.SmsRetrieverReceiver
import org.thoughtcrime.securesms.registrationv3.ui.restore.RemoteRestoreActivity
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
import org.thoughtcrime.securesms.util.RemoteConfig
/**
* Activity to hold the entire registration process.
@@ -69,40 +61,9 @@ class RegistrationActivity : BaseActivity() {
SignalStore.misc.shouldShowLinkedDevicesReminder = sharedViewModel.isReregister
}
if (SignalStore.storageService.needsAccountRestore) {
Log.i(TAG, "Performing pin restore.")
startActivity(Intent(this, PinRestoreActivity::class.java))
finish()
} else {
val isProfileNameEmpty = Recipient.self().profileName.isEmpty
val isAvatarEmpty = !AvatarHelper.hasAvatar(this, Recipient.self().id)
val needsProfile = isProfileNameEmpty || isAvatarEmpty
val needsPin = !SignalStore.svr.hasOptedInWithAccess()
Log.i(TAG, "Pin restore flow not required. Profile name empty: $isProfileNameEmpty | Profile avatar empty: $isAvatarEmpty | Needs PIN: $needsPin")
if (!needsProfile && !needsPin) {
sharedViewModel.completeRegistration()
}
val startIntent = MainActivity.clearTop(this)
val nextIntent: Intent? = when {
needsPin -> CreateSvrPinActivity.getIntentForPinCreate(this@RegistrationActivity)
SignalStore.registration.restoreDecisionState.isDecisionPending && RemoteConfig.messageBackups -> RemoteRestoreActivity.getIntent(this@RegistrationActivity)
needsProfile -> CreateProfileActivity.getIntentForUserProfile(this@RegistrationActivity)
else -> null
}
if (nextIntent != null) {
startIntent.putExtra("next_intent", nextIntent)
}
Log.d(TAG, "Launching ${startIntent.component} with next_intent: ${nextIntent?.component}")
startActivity(startIntent)
finish()
ActivityNavigator.applyPopAnimationsToPendingTransition(this)
}
startActivity(MainActivity.clearTop(this))
finish()
ActivityNavigator.applyPopAnimationsToPendingTransition(this)
}
private inner class SmsRetrieverObserver : DefaultLifecycleObserver {

View File

@@ -75,4 +75,22 @@ data class RegistrationState(
}
}
}
fun toNavigationStateOnly(): NavigationState {
return NavigationState(challengesRequested, challengesPresented, captchaToken, registrationCheckpoint, canSkipSms)
}
/**
* Subset of [RegistrationState] useful for deciding on navigation. Prevents other properties updating from re-triggering
* navigation decisions.
*/
data class NavigationState(
val challengesRequested: List<Challenge>,
val challengesPresented: Set<Challenge>,
val captchaToken: String? = null,
val registrationCheckpoint: RegistrationCheckpoint,
val canSkipSms: Boolean
) {
val challengesRemaining: List<Challenge> = challengesRequested.filterNot { it in challengesPresented }
}
}

View File

@@ -23,7 +23,6 @@ import kotlinx.coroutines.flow.updateAndGet
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.signal.core.util.Base64
import org.signal.core.util.Stopwatch
import org.signal.core.util.isNotNullOrBlank
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.backup.v2.BackupRepository
@@ -32,9 +31,6 @@ import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileContentUpdateJob
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileKeyUpdateJob
import org.thoughtcrime.securesms.jobs.ProfileUploadJob
import org.thoughtcrime.securesms.jobs.ReclaimUsernameAndLinkJob
import org.thoughtcrime.securesms.jobs.StorageAccountRestoreJob
import org.thoughtcrime.securesms.jobs.StorageSyncJob
import org.thoughtcrime.securesms.keyvalue.NewAccount
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.keyvalue.Skipped
@@ -75,6 +71,7 @@ import org.thoughtcrime.securesms.registration.ui.toE164
import org.thoughtcrime.securesms.registration.util.RegistrationUtil
import org.thoughtcrime.securesms.registration.viewmodel.SvrAuthCredentialSet
import org.thoughtcrime.securesms.registrationv3.data.RegistrationRepository
import org.thoughtcrime.securesms.registrationv3.ui.restore.StorageServiceRestore
import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.dualsim.MccMncProducer
@@ -85,7 +82,6 @@ import org.whispersystems.signalservice.api.svr.Svr3Credentials
import org.whispersystems.signalservice.internal.push.AuthCredentials
import java.io.IOException
import java.nio.charset.StandardCharsets
import java.util.concurrent.TimeUnit
import kotlin.jvm.optionals.getOrNull
import kotlin.math.max
import kotlin.time.Duration.Companion.milliseconds
@@ -880,37 +876,29 @@ class RegistrationViewModel : ViewModel() {
RegistrationRepository.registerAccountLocally(context, metadata)
if (!remoteResult.storageCapable && SignalStore.registration.restoreDecisionState.isDecisionPending) {
// Not being storage capable is a high signal that account is new and there's no data to restore
Log.v(TAG, "Not storage capable and still pending restore decision, likely an account with no data to restore, skipping post register restore")
SignalStore.registration.restoreDecisionState = RestoreDecisionState.NewAccount
}
if (reglockEnabled || SignalStore.svr.hasOptedInWithAccess()) {
SignalStore.onboarding.clearAll()
if (!SignalStore.registration.restoreDecisionState.isDecisionPending) {
Log.d(TAG, "No pending restore decisions, can restore account from storage service")
StorageServiceRestore.restore()
}
}
if (reglockEnabled || SignalStore.svr.hasOptedInWithAccess()) {
val stopwatch = Stopwatch("post-reg-storage-service")
AppDependencies.jobManager.runSynchronously(StorageAccountRestoreJob(), StorageAccountRestoreJob.LIFESPAN)
stopwatch.split("account-restore")
AppDependencies.jobManager
.startChain(StorageSyncJob())
.then(ReclaimUsernameAndLinkJob())
.enqueueAndBlockUntilCompletion(TimeUnit.SECONDS.toMillis(10))
stopwatch.split("storage-sync")
if (SignalStore.account.restoredAccountEntropyPool) {
Log.d(TAG, "Restoring backup tier")
BackupRepository.restoreBackupTier(SignalStore.account.requireAci())
stopwatch.split("backup-tier")
stopwatch.stop(TAG)
}
refreshRemoteConfig()
val checkpoint = if (SignalStore.registration.restoreDecisionState.isDecisionPending &&
SignalStore.registration.restoreDecisionState.isWantingManualRemoteRestore &&
!SignalStore.backup.isBackupTierRestored
(!SignalStore.backup.isBackupTierRestored || SignalStore.backup.lastBackupTime == 0L)
) {
RegistrationCheckpoint.BACKUP_TIER_NOT_RESTORED
} else {
@@ -932,8 +920,11 @@ class RegistrationViewModel : ViewModel() {
SignalStore.registration.restoreDecisionState = RestoreDecisionState.intendToRestore(hasOldDevice, fromRemote)
}
fun skipRestoreAfterRegistration() {
fun skipRestore() {
SignalStore.registration.restoreDecisionState = RestoreDecisionState.Skipped
}
fun resumeNormalRegistration() {
store.update {
it.copy(registrationCheckpoint = RegistrationCheckpoint.LOCAL_REGISTRATION_COMPLETE)
}
@@ -941,7 +932,7 @@ class RegistrationViewModel : ViewModel() {
fun restoreBackupTier() {
store.update {
it.copy(registrationCheckpoint = RegistrationCheckpoint.SERVICE_REGISTRATION_COMPLETED)
it.copy(inProgress = true, registrationCheckpoint = RegistrationCheckpoint.SERVICE_REGISTRATION_COMPLETED)
}
viewModelScope.launch {
@@ -949,7 +940,7 @@ class RegistrationViewModel : ViewModel() {
val tierUnknown = BackupRepository.restoreBackupTier(SignalStore.account.requireAci()) == null
delay(max(0L, 500L - (System.currentTimeMillis() - start)))
if (tierUnknown) {
if (tierUnknown || SignalStore.backup.lastBackupTime == 0L) {
store.update {
it.copy(registrationCheckpoint = RegistrationCheckpoint.BACKUP_TIER_NOT_RESTORED)
}

View File

@@ -160,18 +160,24 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
handleRegistrationErrorResponse(it)
sharedViewModel.registerAccountErrorShown()
}
if (sharedState.challengesRequested.contains(Challenge.CAPTCHA) && sharedState.captchaToken.isNotNullOrBlank()) {
sharedViewModel.submitCaptchaToken(requireContext())
} else if (sharedState.challengesRemaining.isNotEmpty()) {
handleChallenges(sharedState.challengesRemaining)
} else if (sharedState.registrationCheckpoint >= RegistrationCheckpoint.PHONE_NUMBER_CONFIRMED && sharedState.canSkipSms) {
moveToEnterPinScreen()
} else if (sharedState.registrationCheckpoint >= RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED) {
moveToVerificationEntryScreen()
}
}
sharedViewModel
.uiState
.map { it.toNavigationStateOnly() }
.distinctUntilChanged()
.observe(viewLifecycleOwner) { sharedState ->
if (sharedState.challengesRequested.contains(Challenge.CAPTCHA) && sharedState.captchaToken.isNotNullOrBlank()) {
sharedViewModel.submitCaptchaToken(requireContext())
} else if (sharedState.challengesRemaining.isNotEmpty()) {
handleChallenges(sharedState.challengesRemaining)
} else if (sharedState.registrationCheckpoint >= RegistrationCheckpoint.PHONE_NUMBER_CONFIRMED && sharedState.canSkipSms) {
moveToEnterPinScreen()
} else if (sharedState.registrationCheckpoint >= RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED) {
moveToVerificationEntryScreen()
}
}
fragmentViewModel
.uiState
.map { it.phoneNumberRegionCode }

View File

@@ -47,7 +47,7 @@ class EnterBackupKeyFragment : ComposeFragment() {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
sharedViewModel
.state
.map { it.registerAccountError }
@@ -60,7 +60,7 @@ class EnterBackupKeyFragment : ComposeFragment() {
}
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
sharedViewModel
.state
.filter { it.registrationCheckpoint == RegistrationCheckpoint.BACKUP_TIER_NOT_RESTORED }
@@ -94,18 +94,31 @@ class EnterBackupKeyFragment : ComposeFragment() {
},
onLearnMore = { CommunicationActions.openBrowserLink(requireContext(), LEARN_MORE_URL) },
onSkip = { findNavController().safeNavigate(EnterBackupKeyFragmentDirections.goToEnterPhoneNumber(EnterPhoneNumberMode.RESTART_AFTER_COLLECTION)) }
) {
ErrorContent(
state = state,
onBackupTierRetry = { sharedViewModel.restoreBackupTier() },
onSkipRestoreAfterRegistration = sharedViewModel::skipRestoreAfterRegistration,
onBackupTierNotRestoredDismiss = viewModel::hideRestoreBackupKeyFailed,
onRegistrationErrorDismiss = viewModel::clearRegistrationError,
onBackupKeyHelp = { CommunicationActions.openBrowserLink(requireContext(), LEARN_MORE_URL) }
)
}
onSkip = {
sharedViewModel.skipRestore()
findNavController().safeNavigate(EnterBackupKeyFragmentDirections.goToEnterPhoneNumber(EnterPhoneNumberMode.RESTART_AFTER_COLLECTION))
},
dialogContent = {
if (state.showStorageAccountRestoreProgress) {
Dialogs.IndeterminateProgressDialog()
} else {
ErrorContent(
state = state,
onBackupTierRetry = { sharedViewModel.restoreBackupTier() },
onSkipRestoreAfterRegistration = {
viewLifecycleOwner.lifecycleScope.launch {
sharedViewModel.skipRestore()
viewModel.performStorageServiceAccountRestoreIfNeeded()
sharedViewModel.resumeNormalRegistration()
}
},
onBackupTierNotRestoredDismiss = viewModel::hideRestoreBackupKeyFailed,
onRegistrationErrorDismiss = viewModel::clearRegistrationError,
onBackupKeyHelp = { CommunicationActions.openBrowserLink(requireContext(), LEARN_MORE_URL) }
)
}
}
)
}
}

View File

@@ -73,7 +73,7 @@ fun EnterBackupKeyScreen(
onNextClicked: () -> Unit = {},
onLearnMore: () -> Unit = {},
onSkip: () -> Unit = {},
errorContent: @Composable () -> Unit
dialogContent: @Composable () -> Unit
) {
val coroutineScope = rememberCoroutineScope()
val sheetState = rememberModalBottomSheetState(
@@ -185,7 +185,7 @@ fun EnterBackupKeyScreen(
}
}
errorContent()
dialogContent()
}
}

View File

@@ -101,6 +101,13 @@ class EnterBackupKeyViewModel : ViewModel() {
}
}
suspend fun performStorageServiceAccountRestoreIfNeeded() {
if (SignalStore.account.restoredAccountEntropyPool || SignalStore.svr.masterKeyForInitialDataRestore != null) {
store.update { it.copy(showBackupTierNotRestoreError = false, showStorageAccountRestoreProgress = true) }
StorageServiceRestore.restore()
}
}
data class EnterBackupKeyState(
val backupKeyValid: Boolean = false,
val requiredLength: Int,
@@ -109,6 +116,7 @@ class EnterBackupKeyViewModel : ViewModel() {
val showRegistrationError: Boolean = false,
val showBackupTierNotRestoreError: Boolean = false,
val registerAccountResult: RegisterAccountResult? = null,
val aepValidationError: AEPValidationError? = null
val aepValidationError: AEPValidationError? = null,
val showStorageAccountRestoreProgress: Boolean = false
)
}

View File

@@ -56,7 +56,6 @@ import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsTypeFe
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsTypeFeatureRow
import org.thoughtcrime.securesms.conversation.v2.registerForLifecycle
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity
import org.thoughtcrime.securesms.registrationv3.ui.shared.RegistrationScreen
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.PlayStoreUtil
@@ -93,7 +92,8 @@ class RemoteRestoreActivity : BaseActivity() {
.firstOrNull()
if (restored != null) {
continueRegistration(restored.missingProfileData)
startActivity(MainActivity.clearTop(this@RemoteRestoreActivity))
finish()
}
}
@@ -106,12 +106,15 @@ class RemoteRestoreActivity : BaseActivity() {
state = state,
onRestoreBackupClick = { viewModel.restore() },
onCancelClick = {
if (state.isRemoteRestoreOnlyOption) {
viewModel.skipRestore()
startActivity(MainActivity.clearTop(this))
}
lifecycleScope.launch {
if (state.isRemoteRestoreOnlyOption) {
viewModel.skipRestore()
viewModel.performStorageServiceAccountRestoreIfNeeded()
startActivity(MainActivity.clearTop(this@RemoteRestoreActivity))
}
finish()
finish()
}
},
onErrorDialogDismiss = { viewModel.clearError() },
onUpdateSignal = {
@@ -129,20 +132,6 @@ class RemoteRestoreActivity : BaseActivity() {
fun onEvent(restoreEvent: RestoreV2Event) {
viewModel.updateRestoreProgress(restoreEvent)
}
private fun continueRegistration(missingProfileData: Boolean) {
val main = MainActivity.clearTop(this)
if (missingProfileData) {
val profile = CreateProfileActivity.getIntentForUserProfile(this)
profile.putExtra("next_intent", main)
startActivity(profile)
} else {
startActivity(main)
}
finish()
}
}
@Composable
@@ -171,12 +160,16 @@ private fun RestoreFromBackupContent(
}
RemoteRestoreViewModel.ScreenState.LoadState.NOT_FOUND -> {
RestoreFailedDialog(onDismiss = onCancelClick)
BackupNotFoundDialog(onDismiss = onCancelClick)
}
RemoteRestoreViewModel.ScreenState.LoadState.FAILURE -> {
RestoreFailedDialog(onDismiss = onCancelClick)
}
RemoteRestoreViewModel.ScreenState.LoadState.STORAGE_SERVICE_RESTORE -> {
Dialogs.IndeterminateProgressDialog()
}
}
}
@@ -403,6 +396,19 @@ private fun ProgressDialogPreview() {
}
}
@Composable
fun BackupNotFoundDialog(
onDismiss: () -> Unit = {}
) {
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(android.R.string.ok),
onConfirm = onDismiss,
onDismiss = onDismiss
)
}
@Composable
fun RestoreFailedDialog(
onDismiss: () -> Unit = {}

View File

@@ -27,14 +27,11 @@ import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobmanager.JobTracker
import org.thoughtcrime.securesms.jobs.BackupRestoreJob
import org.thoughtcrime.securesms.jobs.BackupRestoreMediaJob
import org.thoughtcrime.securesms.jobs.ProfileUploadJob
import org.thoughtcrime.securesms.jobs.SyncArchivedMediaJob
import org.thoughtcrime.securesms.keyvalue.Completed
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.keyvalue.Skipped
import org.thoughtcrime.securesms.registration.util.RegistrationUtil
import org.thoughtcrime.securesms.registrationv3.data.QuickRegistrationRepository
import org.thoughtcrime.securesms.registrationv3.data.RegistrationRepository
import org.whispersystems.signalservice.api.registration.RestoreMethod
class RemoteRestoreViewModel(isOnlyRestoreOption: Boolean) : ViewModel() {
@@ -58,7 +55,7 @@ class RemoteRestoreViewModel(isOnlyRestoreOption: Boolean) : ViewModel() {
viewModelScope.launch(Dispatchers.IO) {
val tier: MessageBackupTier? = BackupRepository.restoreBackupTier(SignalStore.account.requireAci())
store.update {
if (tier != null) {
if (tier != null && SignalStore.backup.lastBackupTime > 0) {
it.copy(
loadState = ScreenState.LoadState.LOADED,
backupTier = SignalStore.backup.backupTier,
@@ -66,7 +63,7 @@ class RemoteRestoreViewModel(isOnlyRestoreOption: Boolean) : ViewModel() {
backupSize = SignalStore.backup.totalBackupSize.bytes
)
} else {
if (SignalStore.backup.isBackupTierRestored) {
if (SignalStore.backup.isBackupTierRestored || SignalStore.backup.lastBackupTime == 0L) {
it.copy(loadState = ScreenState.LoadState.NOT_FOUND)
} else if (it.loadState == ScreenState.LoadState.LOADING) {
it.copy(loadState = ScreenState.LoadState.FAILURE)
@@ -108,12 +105,9 @@ class RemoteRestoreViewModel(isOnlyRestoreOption: Boolean) : ViewModel() {
Log.i(TAG, "Restore successful")
SignalStore.registration.restoreDecisionState = RestoreDecisionState.Completed
if (!RegistrationRepository.isMissingProfileData()) {
RegistrationUtil.maybeMarkRegistrationComplete()
AppDependencies.jobManager.add(ProfileUploadJob())
}
StorageServiceRestore.restore()
store.update { it.copy(importState = ImportState.Restored(RegistrationRepository.isMissingProfileData())) }
store.update { it.copy(importState = ImportState.Restored) }
}
JobTracker.JobState.PENDING,
@@ -155,6 +149,13 @@ class RemoteRestoreViewModel(isOnlyRestoreOption: Boolean) : ViewModel() {
}
}
suspend fun performStorageServiceAccountRestoreIfNeeded() {
if (SignalStore.account.restoredAccountEntropyPool || SignalStore.svr.masterKeyForInitialDataRestore != null) {
store.update { it.copy(loadState = ScreenState.LoadState.STORAGE_SERVICE_RESTORE) }
StorageServiceRestore.restore()
}
}
data class ScreenState(
val isRemoteRestoreOnlyOption: Boolean = false,
val backupTier: MessageBackupTier? = null,
@@ -170,14 +171,14 @@ class RemoteRestoreViewModel(isOnlyRestoreOption: Boolean) : ViewModel() {
}
enum class LoadState {
LOADING, LOADED, NOT_FOUND, FAILURE
LOADING, LOADED, NOT_FOUND, FAILURE, STORAGE_SERVICE_RESTORE
}
}
sealed interface ImportState {
data object None : ImportState
data object InProgress : ImportState
data class Restored(val missingProfileData: Boolean) : ImportState
data object Restored : ImportState
data object Failed : ImportState
}
}

View File

@@ -29,7 +29,7 @@ class SelectManualRestoreMethodFragment : ComposeFragment() {
private val sharedViewModel by activityViewModels<RegistrationViewModel>()
private val launchRestoreActivity = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
private val localBackupRestore = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
when (val resultCode = result.resultCode) {
Activity.RESULT_OK -> {
sharedViewModel.onBackupSuccessfullyRestored()
@@ -47,7 +47,10 @@ class SelectManualRestoreMethodFragment : ComposeFragment() {
SelectRestoreMethodScreen(
restoreMethods = listOf(RestoreMethod.FROM_SIGNAL_BACKUPS, RestoreMethod.FROM_LOCAL_BACKUP_V1),
onRestoreMethodClicked = this::startRestoreMethod,
onSkip = { findNavController().safeNavigate(SelectManualRestoreMethodFragmentDirections.goToEnterPhoneNumber(EnterPhoneNumberMode.NORMAL)) }
onSkip = {
sharedViewModel.skipRestore()
findNavController().safeNavigate(SelectManualRestoreMethodFragmentDirections.goToEnterPhoneNumber(EnterPhoneNumberMode.NORMAL))
}
)
}
@@ -59,7 +62,7 @@ class SelectManualRestoreMethodFragment : ComposeFragment() {
}
RestoreMethod.FROM_LOCAL_BACKUP_V1 -> {
sharedViewModel.intendToRestore(hasOldDevice = false, fromRemote = false)
launchRestoreActivity.launch(RestoreActivity.getLocalRestoreIntent(requireContext()))
localBackupRestore.launch(RestoreActivity.getLocalRestoreIntent(requireContext()))
}
RestoreMethod.FROM_OLD_DEVICE -> error("Device transfer not supported in manual restore flow")
RestoreMethod.FROM_LOCAL_BACKUP_V2 -> error("Not currently supported")

View File

@@ -5,6 +5,7 @@
package org.thoughtcrime.securesms.registrationv3.ui.restore
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
@@ -24,7 +25,8 @@ import org.thoughtcrime.securesms.registrationv3.ui.shared.RegistrationScreen
fun SelectRestoreMethodScreen(
restoreMethods: List<RestoreMethod>,
onRestoreMethodClicked: (RestoreMethod) -> Unit = {},
onSkip: () -> Unit = {}
onSkip: () -> Unit = {},
extraContent: @Composable ColumnScope.() -> Unit = {}
) {
RegistrationScreen(
title = stringResource(id = R.string.SelectRestoreMethodFragment__restore_or_transfer_account),
@@ -46,6 +48,8 @@ fun SelectRestoreMethodScreen(
onRowClick = { onRestoreMethodClicked(method) }
)
}
extraContent()
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registrationv3.ui.restore
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.signal.core.util.Stopwatch
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobmanager.enqueueBlocking
import org.thoughtcrime.securesms.jobmanager.runJobBlocking
import org.thoughtcrime.securesms.jobs.ProfileUploadJob
import org.thoughtcrime.securesms.jobs.ReclaimUsernameAndLinkJob
import org.thoughtcrime.securesms.jobs.StorageAccountRestoreJob
import org.thoughtcrime.securesms.jobs.StorageSyncJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.registration.util.RegistrationUtil
import org.thoughtcrime.securesms.registrationv3.data.RegistrationRepository
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
object StorageServiceRestore {
private val TAG = Log.tag(StorageServiceRestore::class)
/**
* Restore account data from Storage Service in a quasi-blocking manner. Uses existing jobs
* to perform the restore but will not wait indefinitely for them to finish so may return prior
* to completing the restore.
*/
suspend fun restore() {
withContext(Dispatchers.IO) {
val stopwatch = Stopwatch("storage-service-restore")
SignalStore.storageService.needsAccountRestore = false
AppDependencies.jobManager.runJobBlocking(StorageAccountRestoreJob(), StorageAccountRestoreJob.LIFESPAN.milliseconds)
stopwatch.split("account-restore")
AppDependencies
.jobManager
.startChain(StorageSyncJob())
.then(ReclaimUsernameAndLinkJob())
.enqueueBlocking(10.seconds)
stopwatch.split("storage-sync-restore")
stopwatch.stop(TAG)
val isMissingProfileData = RegistrationRepository.isMissingProfileData()
if (!isMissingProfileData) {
RegistrationUtil.maybeMarkRegistrationComplete()
AppDependencies.jobManager.add(ProfileUploadJob())
}
}
}
}

View File

@@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
@@ -49,7 +50,7 @@ fun RegistrationScreen(
title: String,
subtitle: String,
bottomContent: @Composable (BoxScope.() -> Unit),
mainContent: @Composable () -> Unit
mainContent: @Composable ColumnScope.() -> Unit
) {
RegistrationScreen(title, AnnotatedString(subtitle), bottomContent, mainContent)
}
@@ -62,7 +63,7 @@ fun RegistrationScreen(
title: String,
subtitle: AnnotatedString?,
bottomContent: @Composable (BoxScope.() -> Unit),
mainContent: @Composable () -> Unit
mainContent: @Composable ColumnScope.() -> Unit
) {
Surface {
Column(

View File

@@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.registrationv3.ui.phonenumber.EnterPhoneNumber
import org.thoughtcrime.securesms.util.BackupUtil
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.navigation.safeNavigate
import org.thoughtcrime.securesms.util.visible
/**
* First screen that is displayed on the very first app launch.
@@ -49,6 +50,7 @@ class WelcomeFragment : LoggingFragment(R.layout.fragment_registration_welcome_v
binding.welcomeContinueButton.setOnClickListener { onContinueClicked() }
binding.welcomeTermsButton.setOnClickListener { onTermsClicked() }
binding.welcomeTransferOrRestore.setOnClickListener { onRestoreOrTransferClicked() }
binding.welcomeTransferOrRestore.visible = !sharedViewModel.isReregister
childFragmentManager.setFragmentResultListener(RestoreWelcomeBottomSheet.REQUEST_KEY, viewLifecycleOwner) { requestKey, bundle ->
if (requestKey == RestoreWelcomeBottomSheet.REQUEST_KEY) {