Store restoreDecision separately in regV5.

This commit is contained in:
Greyson Parrelli
2026-06-25 16:56:22 -04:00
committed by Michelle Tang
parent 2c35a6a3fe
commit e07a3e05bf
22 changed files with 58 additions and 45 deletions
@@ -27,10 +27,10 @@ import org.signal.core.util.StreamUtil
import org.signal.core.util.crypto.AttachmentSecretProvider
import org.signal.core.util.logging.Log
import org.signal.registration.PreExistingRegistrationData
import org.signal.registration.RestoreDecision
import org.signal.registration.StorageController
import org.signal.registration.StoredProfileData
import org.signal.registration.proto.RegistrationData
import org.signal.registration.proto.RestoreDecision
import org.signal.registration.screens.localbackuprestore.LocalBackupInfo
import org.signal.registration.screens.remotebackuprestore.RemoteBackupRestoreProgress
import org.thoughtcrime.securesms.backup.FullBackupImporter
@@ -244,26 +244,18 @@ class AppRegistrationStorageController(private val context: Context) : StorageCo
SignalStore.svr.masterKeyForInitialDataRestore = MasterKey(data.temporaryMasterKey.toByteArray())
}
applyRestoreDecision(data.restoreDecision)
RegistrationUtil.maybeMarkRegistrationComplete()
}
/**
* Translates the registration module's [RestoreDecision] into the app's [RestoreDecisionState] so the rest of the app
* knows whether we're a fresh account, skipped a restore, or successfully restored data. Only applied while the
* decision is still pending, since the state machine is otherwise terminal.
*/
private fun applyRestoreDecision(decision: RestoreDecision) {
override suspend fun setRestoreDecision(decision: RestoreDecision) = withContext(Dispatchers.IO) {
if (!SignalStore.registration.restoreDecisionState.isDecisionPending) {
return
return@withContext
}
SignalStore.registration.restoreDecisionState = when (decision) {
RestoreDecision.NEW_ACCOUNT -> RestoreDecisionState.NewAccount
RestoreDecision.SKIPPED -> RestoreDecisionState.Skipped
RestoreDecision.COMPLETED -> RestoreDecisionState.Completed
RestoreDecision.UNSET -> return
}
}
@@ -27,6 +27,7 @@ import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.signal.registration.NetworkController
import org.signal.registration.NewRegistrationData
import org.signal.registration.PreExistingRegistrationData
import org.signal.registration.RestoreDecision
import org.signal.registration.StorageController
import org.signal.registration.StoredProfileData
import org.signal.registration.proto.ProvisioningData
@@ -196,6 +197,11 @@ class DemoStorageController(private val context: Context) : StorageController {
Unit
}
override suspend fun setRestoreDecision(decision: RestoreDecision) = withContext(Dispatchers.IO) {
Log.i(TAG, "[setRestoreDecision] Recording restore decision: $decision")
RegistrationPreferences.restoreDecision = decision
}
override suspend fun scanLocalBackupFolder(folderUri: Uri): List<LocalBackupInfo> = withContext(Dispatchers.IO) {
val folder = DocumentFile.fromTreeUri(context, folderUri) ?: return@withContext emptyList()
val children = folder.listFiles()
@@ -225,6 +225,7 @@ private fun RegistrationInfo(data: MainScreenState.ExistingRegistrationState) {
RegistrationField(label = "PNI", value = data.pni)
RegistrationField(label = "AEP", value = data.aep)
RegistrationField(label = "Temporary Master Key", value = data.temporaryMasterKey ?: "null")
RegistrationField(label = "Restore Decision", value = data.restoreDecision ?: "(unset)")
if (data.pinsOptedOut) {
RegistrationField(label = "PINs Opted Out", value = "Yes")
} else {
@@ -377,7 +378,8 @@ private fun MainScreenWithRegistrationPreview() {
pin = "1234",
registrationLockEnabled = true,
pinsOptedOut = false,
temporaryMasterKey = null
temporaryMasterKey = null,
restoreDecision = "COMPLETED"
),
profileState = MainScreenState.ProfileState(
givenName = "Ada",
@@ -27,7 +27,8 @@ data class MainScreenState(
val pin: String?,
val registrationLockEnabled: Boolean,
val pinsOptedOut: Boolean,
val temporaryMasterKey: String?
val temporaryMasterKey: String?,
val restoreDecision: String?
)
data class ProfileState(
@@ -74,7 +74,8 @@ class MainScreenViewModel(
pinsOptedOut = RegistrationPreferences.pinsOptedOut,
temporaryMasterKey = RegistrationPreferences.temporaryMasterKey?.let {
Base64.encodeWithPadding(it.serialize())
}
},
restoreDecision = RegistrationPreferences.restoreDecision?.name
)
} else {
null
@@ -19,6 +19,7 @@ import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.signal.registration.NetworkController
import org.signal.registration.NewRegistrationData
import org.signal.registration.PreExistingRegistrationData
import org.signal.registration.RestoreDecision
/**
* SharedPreferences-based storage for registration data that doesn't need
@@ -58,6 +59,7 @@ object RegistrationPreferences {
private const val KEY_PROFILE_FAMILY_NAME = "profile_family_name"
private const val KEY_PROFILE_AVATAR = "profile_avatar"
private const val KEY_PROFILE_DISCOVERABLE = "profile_discoverable"
private const val KEY_RESTORE_DECISION = "restore_decision"
fun init(context: Application) {
this.context = context
@@ -141,6 +143,10 @@ object RegistrationPreferences {
get() = prefs.getBoolean(KEY_FETCHES_MESSAGES, true)
set(value) = prefs.edit { putBoolean(KEY_FETCHES_MESSAGES, value) }
var restoreDecision: RestoreDecision?
get() = prefs.getString(KEY_RESTORE_DECISION, null)?.let { runCatching { RestoreDecision.valueOf(it) }.getOrNull() }
set(value) = prefs.edit { putString(KEY_RESTORE_DECISION, value?.name) }
var profileGivenName: String
get() = prefs.getString(KEY_PROFILE_GIVEN_NAME, "") ?: ""
set(value) = prefs.edit { putString(KEY_PROFILE_GIVEN_NAME, value) }
@@ -41,7 +41,6 @@ import org.signal.registration.NetworkController.SessionMetadata
import org.signal.registration.NetworkController.SvrCredentials
import org.signal.registration.NetworkController.UpdateSessionError
import org.signal.registration.proto.ProvisioningData
import org.signal.registration.proto.RestoreDecision
import org.signal.registration.proto.SvrCredential
import org.signal.registration.screens.localbackuprestore.LocalBackupInfo
import org.signal.registration.screens.remotebackuprestore.RemoteBackupRestoreProgress
@@ -543,10 +542,7 @@ class RegistrationRepository(val context: Context, val networkController: Networ
*/
suspend fun setRestoreDecision(decision: RestoreDecision): Unit = withContext(Dispatchers.IO) {
Log.i(TAG, "[setRestoreDecision] Recording restore decision: $decision")
storageController.updateInProgressRegistrationData {
this.restoreDecision = decision
}
storageController.commitRegistrationData()
storageController.setRestoreDecision(decision)
}
suspend fun getPreExistingRegistrationData(): PreExistingRegistrationData? {
@@ -0,0 +1,17 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.registration
/**
* The terminal restore decision the user reached during registration. Mirrors the terminal states of the app's
* RestoreDecisionState. We intentionally do not model the transient pending states (START / INTEND_TO_RESTORE)
* here, as the registration flow performs any restore inline before completing.
*/
enum class RestoreDecision {
NEW_ACCOUNT,
SKIPPED,
COMPLETED
}
@@ -83,6 +83,12 @@ interface StorageController {
*/
suspend fun commitRegistrationData()
/**
* Persists the terminal [RestoreDecision] the user reached during registration directly to permanent app state,
* so the rest of the app knows whether we're a fresh account, skipped a restore, or successfully restored data.
*/
suspend fun setRestoreDecision(decision: RestoreDecision)
/**
* Begins restoring from a V1 (.backup) file identified by the given [uri].
*
@@ -13,7 +13,7 @@ import kotlinx.coroutines.flow.StateFlow
import org.signal.core.util.logging.Log
import org.signal.registration.RegistrationFlowEvent
import org.signal.registration.RegistrationRepository
import org.signal.registration.proto.RestoreDecision
import org.signal.registration.RestoreDecision
import org.signal.registration.screens.EventDrivenViewModel
class DeviceTransferCompleteViewModel(
@@ -23,7 +23,7 @@ import org.signal.core.util.logging.Log
import org.signal.registration.RegistrationFlowEvent
import org.signal.registration.RegistrationRepository
import org.signal.registration.RegistrationRoute
import org.signal.registration.proto.RestoreDecision
import org.signal.registration.RestoreDecision
import org.signal.registration.screens.EventDrivenViewModel
import org.signal.registration.screens.util.navigateBack
import org.signal.registration.screens.util.navigateTo
@@ -21,7 +21,7 @@ import org.signal.registration.NetworkController
import org.signal.registration.RegistrationFlowEvent
import org.signal.registration.RegistrationFlowState
import org.signal.registration.RegistrationRepository
import org.signal.registration.proto.RestoreDecision
import org.signal.registration.RestoreDecision
import org.signal.registration.screens.EventDrivenViewModel
/**
@@ -21,7 +21,7 @@ import org.signal.registration.RegistrationFlowEvent
import org.signal.registration.RegistrationFlowState
import org.signal.registration.RegistrationRepository
import org.signal.registration.RegistrationRoute
import org.signal.registration.proto.RestoreDecision
import org.signal.registration.RestoreDecision
import org.signal.registration.screens.EventDrivenViewModel
import org.signal.registration.screens.util.navigateTo
@@ -23,7 +23,7 @@ import org.signal.libsignal.net.RequestResult
import org.signal.registration.NetworkController
import org.signal.registration.RegistrationFlowEvent
import org.signal.registration.RegistrationRepository
import org.signal.registration.proto.RestoreDecision
import org.signal.registration.RestoreDecision
import org.signal.registration.screens.EventDrivenViewModel
import org.signal.registration.screens.util.navigateBack
import kotlin.coroutines.CoroutineContext
@@ -23,7 +23,7 @@ import org.signal.registration.RegistrationFlowEvent
import org.signal.registration.RegistrationFlowState
import org.signal.registration.RegistrationRepository
import org.signal.registration.RegistrationRoute
import org.signal.registration.proto.RestoreDecision
import org.signal.registration.RestoreDecision
import org.signal.registration.screens.EventDrivenViewModel
import org.signal.registration.screens.util.navigateTo
@@ -52,20 +52,6 @@ message RegistrationData {
// JSON-serialized flow state snapshot (from saveFlowState/restoreFlowState)
string flowStateJson = 21;
// The terminal restore decision the user reached during this flow. The app translates this into its own
// RestoreDecisionState when committing, so the rest of the app knows whether we're a fresh account, skipped a
// restore, or successfully restored data.
RestoreDecision restoreDecision = 25;
}
// Mirrors the terminal states of the app's RestoreDecisionState. We intentionally do not model the transient
// pending states (START / INTEND_TO_RESTORE) here, as the new flow performs any restore inline before completing.
enum RestoreDecision {
UNSET = 0;
NEW_ACCOUNT = 1;
SKIPPED = 2;
COMPLETED = 3;
}
message SvrCredential {
@@ -18,7 +18,7 @@ import org.junit.Before
import org.junit.Test
import org.signal.registration.RegistrationFlowEvent
import org.signal.registration.RegistrationRepository
import org.signal.registration.proto.RestoreDecision
import org.signal.registration.RestoreDecision
@OptIn(ExperimentalCoroutinesApi::class)
class DeviceTransferCompleteViewModelTest {
@@ -33,7 +33,7 @@ import org.signal.core.ui.navigation.ResultEventBus
import org.signal.registration.RegistrationFlowEvent
import org.signal.registration.RegistrationRepository
import org.signal.registration.RegistrationRoute
import org.signal.registration.proto.RestoreDecision
import org.signal.registration.RestoreDecision
import java.time.LocalDateTime
@OptIn(ExperimentalCoroutinesApi::class)
@@ -33,7 +33,7 @@ import org.signal.registration.NetworkController
import org.signal.registration.RegistrationFlowEvent
import org.signal.registration.RegistrationFlowState
import org.signal.registration.RegistrationRepository
import org.signal.registration.proto.RestoreDecision
import org.signal.registration.RestoreDecision
@OptIn(ExperimentalCoroutinesApi::class)
class PinCreationViewModelTest {
@@ -24,7 +24,7 @@ import org.signal.registration.RegistrationFlowEvent
import org.signal.registration.RegistrationFlowState
import org.signal.registration.RegistrationRepository
import org.signal.registration.RegistrationRoute
import org.signal.registration.proto.RestoreDecision
import org.signal.registration.RestoreDecision
class PinEntryForSvrRestoreViewModelTest {
@@ -30,7 +30,7 @@ import org.signal.libsignal.net.RequestResult
import org.signal.registration.NetworkController
import org.signal.registration.RegistrationFlowEvent
import org.signal.registration.RegistrationRepository
import org.signal.registration.proto.RestoreDecision
import org.signal.registration.RestoreDecision
@OptIn(ExperimentalCoroutinesApi::class)
class RemoteBackupRestoreViewModelTest {
@@ -23,7 +23,7 @@ import org.signal.registration.RegistrationFlowEvent
import org.signal.registration.RegistrationFlowState
import org.signal.registration.RegistrationRepository
import org.signal.registration.RegistrationRoute
import org.signal.registration.proto.RestoreDecision
import org.signal.registration.RestoreDecision
class ArchiveRestoreSelectionViewModelTest {