Flesh out restore paths for regv3.

This commit is contained in:
Cody Henthorne
2024-11-25 09:56:53 -05:00
committed by GitHub
parent 9833101cd1
commit f42bd0f374
18 changed files with 415 additions and 101 deletions

View File

@@ -1164,25 +1164,43 @@ object BackupRepository {
}
fun restoreBackupTier(aci: ACI): MessageBackupTier? {
// TODO: more complete error handling
try {
val lastModified = getBackupFileLastModified().successOrThrow()
if (lastModified != null) {
SignalStore.backup.lastBackupTime = lastModified.toMillis()
val tierResult = getBackupTier(aci)
when {
tierResult is NetworkResult.Success -> {
SignalStore.backup.backupTier = tierResult.result
Log.d(TAG, "Backup tier restored: ${SignalStore.backup.backupTier}")
}
tierResult is NetworkResult.StatusCodeError && tierResult.code == 404 -> {
Log.i(TAG, "Backups not enabled")
SignalStore.backup.backupTier = null
}
else -> {
Log.w(TAG, "Could not retrieve backup tier.", tierResult.getCause())
return SignalStore.backup.backupTier
}
} catch (e: Exception) {
Log.i(TAG, "Could not check for backup file.", e)
SignalStore.backup.backupTier = null
return null
}
SignalStore.backup.backupTier = try {
getBackupTier(aci).successOrThrow()
} catch (e: Exception) {
Log.i(TAG, "Could not retrieve backup tier.", e)
null
}
SignalStore.backup.isBackupTierRestored = true
if (SignalStore.backup.backupTier != null) {
val timestampResult = getBackupFileLastModified()
when {
timestampResult is NetworkResult.Success -> {
timestampResult.result?.let { SignalStore.backup.lastBackupTime = it.toMillis() }
}
timestampResult is NetworkResult.StatusCodeError && timestampResult.code == 404 -> {
Log.i(TAG, "No backup file exists")
SignalStore.backup.lastBackupTime = 0L
}
else -> {
Log.w(TAG, "Could not check for backup file.", timestampResult.getCause())
}
}
SignalStore.uiHints.markHasEverEnabledRemoteBackups()
}

View File

@@ -34,6 +34,7 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
private const val KEY_BACKUP_USED_MEDIA_SPACE = "backup.usedMediaSpace"
private const val KEY_BACKUP_LAST_PROTO_SIZE = "backup.lastProtoSize"
private const val KEY_BACKUP_TIER = "backup.backupTier"
private const val KEY_BACKUP_TIER_RESTORED = "backup.backupTierRestored"
private const val KEY_LATEST_BACKUP_TIER = "backup.latestBackupTier"
private const val KEY_LAST_CHECK_IN_MILLIS = "backup.lastCheckInMilliseconds"
private const val KEY_LAST_CHECK_IN_SNOOZE_MILLIS = "backup.lastCheckInSnoozeMilliseconds"
@@ -167,12 +168,15 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
store.beginWrite()
.putLong(KEY_BACKUP_TIER, serializedValue)
.putLong(KEY_LATEST_BACKUP_TIER, serializedValue)
.putBoolean(KEY_BACKUP_TIER_RESTORED, true)
.apply()
} else {
putLong(KEY_BACKUP_TIER, serializedValue)
}
}
var isBackupTierRestored: Boolean by booleanValue(KEY_BACKUP_TIER_RESTORED, false)
/**
* When uploading a backup, we store the progress state here so that it can remain across app restarts.
*/

View File

@@ -15,6 +15,7 @@ class RegistrationValues internal constructor(store: KeyValueStore) : SignalStor
private const val LOCAL_REGISTRATION_DATA = "registration.local_registration_data"
private const val RESTORE_COMPLETED = "registration.backup_restore_completed"
private const val RESTORE_METHOD_TOKEN = "registration.restore_method_token"
private const val IS_OTHER_DEVICE_ANDROID = "registration.is_other_device_android"
private const val RESTORING_ON_NEW_DEVICE = "registration.restoring_on_new_device"
}
@@ -60,6 +61,8 @@ class RegistrationValues internal constructor(store: KeyValueStore) : SignalStor
var hasUploadedProfile: Boolean by booleanValue(HAS_UPLOADED_PROFILE, true)
var sessionId: String? by stringValue(SESSION_ID, null)
var sessionE164: String? by stringValue(SESSION_E164, null)
var isOtherDeviceAndroid: Boolean by booleanValue(IS_OTHER_DEVICE_ANDROID, false)
var restoreMethodToken: String? by stringValue(RESTORE_METHOD_TOKEN, null)
@get:JvmName("isRestoringOnNewDevice")

View File

@@ -86,9 +86,10 @@ object QuickRegistrationRepository {
backupTimestampMs = SignalStore.backup.lastBackupTime.coerceAtLeast(0L),
tier = when (SignalStore.backup.backupTier) {
MessageBackupTier.PAID -> RegistrationProvisionMessage.Tier.PAID
MessageBackupTier.FREE,
null -> RegistrationProvisionMessage.Tier.FREE
MessageBackupTier.FREE -> RegistrationProvisionMessage.Tier.FREE
null -> null
},
backupSizeBytes = SignalStore.backup.totalBackupSize,
restoreMethodToken = restoreMethodToken
)
)
@@ -145,7 +146,7 @@ object QuickRegistrationRepository {
Log.d(TAG, "Waiting for restore method with token: ***${restoreMethodToken.takeLast(4)}")
while (retries-- > 0 && result !is NetworkResult.Success && coroutineContext.isActive) {
Log.d(TAG, "Remaining tries $retries...")
Log.d(TAG, "Waiting, remaining tries: $retries")
val api = AppDependencies.registrationApi
result = api.waitForRestoreMethod(restoreMethodToken)
Log.d(TAG, "Result: $result")
@@ -155,7 +156,7 @@ object QuickRegistrationRepository {
Log.i(TAG, "Restore method selected on new device ${result.result}")
return result.result
} else {
Log.w(TAG, "Failed to determine restore method, using default")
Log.w(TAG, "Failed to determine restore method, using DECLINE")
return RestoreMethod.DECLINE
}
}

View File

@@ -0,0 +1,118 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registrationv3.ui.restore
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.fragment.findNavController
import org.signal.core.ui.Buttons
import org.signal.core.ui.Previews
import org.signal.core.ui.SignalPreview
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.registrationv3.ui.shared.RegistrationScreen
import org.thoughtcrime.securesms.util.navigation.safeNavigate
/**
* Shown when the old device is iOS and they are trying to transfer/restore on Android without a Signal Backup.
*/
class NoBackupToRestoreFragment : ComposeFragment() {
@Composable
override fun FragmentContent() {
NoBackupToRestoreContent(
onSkipRestore = {},
onCancel = {
findNavController().safeNavigate(NoBackupToRestoreFragmentDirections.restartRegistrationFlow())
}
)
}
}
@Composable
private fun NoBackupToRestoreContent(
onSkipRestore: () -> Unit = {},
onCancel: () -> Unit = {}
) {
RegistrationScreen(
title = stringResource(id = R.string.NoBackupToRestore_title),
subtitle = stringResource(id = R.string.NoBackupToRestore_subtitle),
bottomContent = {
Column {
Buttons.LargeTonal(
onClick = onSkipRestore,
modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(id = R.string.NoBackupToRestore_skip_restore))
}
TextButton(
onClick = onCancel,
modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(id = android.R.string.cancel))
}
}
}
) {
Column(
verticalArrangement = Arrangement.spacedBy(24.dp),
modifier = Modifier.padding(horizontal = 32.dp)
) {
StepRow(icon = painterResource(R.drawable.symbol_device_phone_24), text = stringResource(id = R.string.NoBackupToRestore_step1))
StepRow(icon = painterResource(R.drawable.symbol_backup_24), text = stringResource(id = R.string.NoBackupToRestore_step2))
StepRow(icon = painterResource(R.drawable.symbol_check_circle_24), text = stringResource(id = R.string.NoBackupToRestore_step3))
}
}
}
@Composable
private fun StepRow(
icon: Painter,
text: String
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = icon,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
contentDescription = null
)
Spacer(modifier = Modifier.width(16.dp))
Text(
text = text,
style = MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.onSurfaceVariant)
)
}
}
@SignalPreview
@Composable
private fun NoBackupToRestoreContentPreview() {
Previews.Preview {
NoBackupToRestoreContent()
}
}

View File

@@ -9,7 +9,6 @@ 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
@@ -30,10 +29,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
@@ -63,6 +58,7 @@ import org.thoughtcrime.securesms.conversation.v2.registerForLifecycle
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.viewModel
import java.util.Locale
/**
@@ -70,12 +66,19 @@ import java.util.Locale
*/
class RemoteRestoreActivity : BaseActivity() {
companion object {
fun getIntent(context: Context): Intent {
return Intent(context, RemoteRestoreActivity::class.java)
private const val KEY_ONLY_OPTION = "ONLY_OPTION"
fun getIntent(context: Context, isOnlyOption: Boolean = false): Intent {
return Intent(context, RemoteRestoreActivity::class.java).apply {
putExtra(KEY_ONLY_OPTION, isOnlyOption)
}
}
}
private val viewModel: RemoteRestoreViewModel by viewModels()
private val viewModel: RemoteRestoreViewModel by viewModel {
RemoteRestoreViewModel(intent.getBooleanExtra(KEY_ONLY_OPTION, false))
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -100,7 +103,14 @@ class RemoteRestoreActivity : BaseActivity() {
RestoreFromBackupContent(
state = state,
onRestoreBackupClick = { viewModel.restore() },
onCancelClick = { finish() },
onCancelClick = {
if (state.isRemoteRestoreOnlyOption) {
viewModel.skipRestore()
startActivity(MainActivity.clearTop(this))
}
finish()
},
onErrorDialogDismiss = { viewModel.clearError() }
)
}
@@ -137,25 +147,57 @@ private fun RestoreFromBackupContent(
onCancelClick: () -> Unit = {},
onErrorDialogDismiss: () -> Unit = {}
) {
val subtitle = buildAnnotatedString {
append(
stringResource(
id = R.string.RemoteRestoreActivity__backup_created_at,
DateUtils.formatDateWithoutDayOfWeek(Locale.getDefault(), state.backupTime),
DateUtils.getOnlyTimeString(LocalContext.current, state.backupTime)
when (state.loadState) {
RemoteRestoreViewModel.ScreenState.LoadState.LOADING -> {
Dialogs.IndeterminateProgressDialog(
message = stringResource(R.string.RemoteRestoreActivity__fetching_backup_details)
)
)
append(" ")
if (state.backupTier != MessageBackupTier.PAID) {
withStyle(SpanStyle(fontWeight = FontWeight.SemiBold)) {
append(stringResource(id = R.string.RemoteRestoreActivity__only_media_sent_or_received))
}
}
RemoteRestoreViewModel.ScreenState.LoadState.LOADED -> {
BackupAvailableContent(
state = state,
onRestoreBackupClick = onRestoreBackupClick,
onCancelClick = onCancelClick,
onErrorDialogDismiss = onErrorDialogDismiss
)
}
RemoteRestoreViewModel.ScreenState.LoadState.NOT_FOUND -> {
RestoreFailedDialog(onDismiss = onCancelClick)
}
RemoteRestoreViewModel.ScreenState.LoadState.FAILURE -> {
RestoreFailedDialog(onDismiss = onCancelClick)
}
}
}
@Composable
private fun BackupAvailableContent(
state: RemoteRestoreViewModel.ScreenState,
onRestoreBackupClick: () -> Unit,
onCancelClick: () -> Unit,
onErrorDialogDismiss: () -> Unit
) {
val subtitle = if (state.backupSize.bytes > 0) {
stringResource(
id = R.string.RemoteRestoreActivity__backup_created_at_with_size,
DateUtils.formatDateWithoutDayOfWeek(Locale.getDefault(), state.backupTime),
DateUtils.getOnlyTimeString(LocalContext.current, state.backupTime),
state.backupSize.toUnitString()
)
} else {
stringResource(
id = R.string.RemoteRestoreActivity__backup_created_at,
DateUtils.formatDateWithoutDayOfWeek(Locale.getDefault(), state.backupTime),
DateUtils.getOnlyTimeString(LocalContext.current, state.backupTime)
)
}
RegistrationScreen(
title = stringResource(id = R.string.RemoteRestoreActivity__restore_from_backup),
subtitle = if (state.isLoaded()) subtitle else null,
subtitle = subtitle,
bottomContent = {
Column {
if (state.isLoaded()) {
@@ -171,45 +213,31 @@ private fun RestoreFromBackupContent(
onClick = onCancelClick,
modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(id = android.R.string.cancel))
Text(text = stringResource(id = if (state.isRemoteRestoreOnlyOption) R.string.RemoteRestoreActivity__skip_restore else android.R.string.cancel))
}
}
}
) {
when (state.loadState) {
RemoteRestoreViewModel.ScreenState.LoadState.LOADING -> {
Dialogs.IndeterminateProgressDialog(
message = stringResource(R.string.RemoteRestoreActivity__fetching_backup_details)
Column(
modifier = Modifier
.fillMaxWidth()
.background(color = SignalTheme.colors.colorSurface2, shape = RoundedCornerShape(18.dp))
.padding(horizontal = 20.dp)
.padding(top = 20.dp, bottom = 18.dp)
) {
Text(
text = stringResource(id = R.string.RemoteRestoreActivity__your_backup_includes),
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(bottom = 6.dp)
)
getFeatures(state.backupTier).forEach {
MessageBackupsTypeFeatureRow(
messageBackupsTypeFeature = it,
iconTint = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(start = 16.dp, top = 6.dp)
)
}
RemoteRestoreViewModel.ScreenState.LoadState.LOADED -> {
Column(
modifier = Modifier
.fillMaxWidth()
.background(color = SignalTheme.colors.colorSurface2, shape = RoundedCornerShape(18.dp))
.padding(horizontal = 20.dp)
.padding(top = 20.dp, bottom = 18.dp)
) {
Text(
text = stringResource(id = R.string.RemoteRestoreActivity__your_backup_includes),
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(bottom = 6.dp)
)
getFeatures(state.backupTier).forEach {
MessageBackupsTypeFeatureRow(
messageBackupsTypeFeature = it,
iconTint = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(start = 16.dp, top = 6.dp)
)
}
}
}
RemoteRestoreViewModel.ScreenState.LoadState.FAILURE -> {
RestoreFailedDialog(onDismiss = onCancelClick)
}
}
when (state.importState) {
@@ -229,6 +257,7 @@ private fun RestoreFromBackupContentPreview() {
state = RemoteRestoreViewModel.ScreenState(
backupTier = MessageBackupTier.PAID,
backupTime = System.currentTimeMillis(),
backupSize = 1234567.bytes,
importState = RemoteRestoreViewModel.ImportState.None,
restoreProgress = null
)

View File

@@ -16,6 +16,8 @@ import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.signal.core.util.ByteSize
import org.signal.core.util.bytes
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
@@ -28,9 +30,11 @@ import org.thoughtcrime.securesms.jobs.ProfileUploadJob
import org.thoughtcrime.securesms.jobs.SyncArchivedMediaJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
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 : ViewModel() {
class RemoteRestoreViewModel(isOnlyRestoreOption: Boolean) : ViewModel() {
companion object {
private val TAG = Log.tag(RemoteRestoreViewModel::class)
@@ -38,8 +42,10 @@ class RemoteRestoreViewModel : ViewModel() {
private val store: MutableStateFlow<ScreenState> = MutableStateFlow(
ScreenState(
isRemoteRestoreOnlyOption = isOnlyRestoreOption,
backupTier = SignalStore.backup.backupTier,
backupTime = SignalStore.backup.lastBackupTime
backupTime = SignalStore.backup.lastBackupTime,
backupSize = SignalStore.backup.totalBackupSize.bytes
)
)
@@ -47,18 +53,23 @@ class RemoteRestoreViewModel : ViewModel() {
init {
viewModelScope.launch(Dispatchers.IO) {
val restored = BackupRepository.restoreBackupTier(SignalStore.account.requireAci()) != null
val tier: MessageBackupTier? = BackupRepository.restoreBackupTier(SignalStore.account.requireAci())
store.update {
if (restored) {
if (tier != null) {
it.copy(
loadState = ScreenState.LoadState.LOADED,
backupTier = SignalStore.backup.backupTier,
backupTime = SignalStore.backup.lastBackupTime
backupTime = SignalStore.backup.lastBackupTime,
backupSize = SignalStore.backup.totalBackupSize.bytes
)
} else {
it.copy(
loadState = ScreenState.LoadState.FAILURE
)
if (SignalStore.backup.isBackupTierRestored) {
it.copy(loadState = ScreenState.LoadState.NOT_FOUND)
} else if (it.loadState == ScreenState.LoadState.LOADING) {
it.copy(loadState = ScreenState.LoadState.FAILURE)
} else {
it
}
}
}
}
@@ -69,6 +80,8 @@ class RemoteRestoreViewModel : ViewModel() {
store.update { it.copy(importState = ImportState.InProgress) }
withContext(Dispatchers.IO) {
QuickRegistrationRepository.setRestoreMethodForOldDevice(RestoreMethod.REMOTE_BACKUP)
val jobStateFlow = callbackFlow {
val listener = JobTracker.JobListener { _, jobState ->
trySend(jobState)
@@ -129,9 +142,21 @@ class RemoteRestoreViewModel : ViewModel() {
store.update { it.copy(importState = ImportState.None, restoreProgress = null) }
}
fun skipRestore() {
SignalStore.registration.markSkippedTransferOrRestore()
viewModelScope.launch {
withContext(Dispatchers.IO) {
QuickRegistrationRepository.setRestoreMethodForOldDevice(RestoreMethod.DECLINE)
}
}
}
data class ScreenState(
val isRemoteRestoreOnlyOption: Boolean = false,
val backupTier: MessageBackupTier? = null,
val backupTime: Long = -1,
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
@@ -141,12 +166,8 @@ class RemoteRestoreViewModel : ViewModel() {
return loadState == LoadState.LOADED
}
fun isLoading(): Boolean {
return loadState == LoadState.LOADING
}
enum class LoadState {
LOADING, LOADED, FAILURE
LOADING, LOADED, NOT_FOUND, FAILURE
}
}

View File

@@ -59,12 +59,14 @@ import org.signal.core.ui.Previews
import org.signal.core.ui.SignalPreview
import org.signal.core.ui.horizontalGutters
import org.signal.core.ui.theme.SignalTheme
import org.signal.registration.proto.RegistrationProvisionMessage
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCode
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeData
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.registrationv3.ui.RegistrationViewModel
import org.thoughtcrime.securesms.registrationv3.ui.shared.RegistrationScreen
import org.thoughtcrime.securesms.util.navigation.safeNavigate
/**
* Show QR code on new device to allow registration and restore via old device.
@@ -84,7 +86,11 @@ class RestoreViaQrFragment : ComposeFragment() {
.mapNotNull { it.provisioningMessage }
.distinctUntilChanged()
.collect { message ->
sharedViewModel.registerWithBackupKey(requireContext(), message.accountEntropyPool, message.e164, message.pin)
if (message.platform == RegistrationProvisionMessage.Platform.ANDROID || message.tier != null) {
sharedViewModel.registerWithBackupKey(requireContext(), message.accountEntropyPool, message.e164, message.pin)
} else {
findNavController().safeNavigate(RestoreViaQrFragmentDirections.goToNoBackupToRestore())
}
}
}
}

View File

@@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import org.signal.core.util.logging.Log
import org.signal.registration.proto.RegistrationProvisionMessage
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeData
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.dependencies.AppDependencies
@@ -80,6 +81,18 @@ class RestoreViaQrViewModel : ViewModel() {
if (result is SecondaryProvisioningCipher.RegistrationProvisionResult.Success) {
Log.i(TAG, "Saving restore method token: ***${result.message.restoreMethodToken.takeLast(4)}")
SignalStore.registration.restoreMethodToken = result.message.restoreMethodToken
SignalStore.registration.isOtherDeviceAndroid = result.message.platform == RegistrationProvisionMessage.Platform.ANDROID
if (result.message.backupTimestampMs > 0) {
SignalStore.backup.backupTier = result.message.tier.let {
when (it) {
RegistrationProvisionMessage.Tier.FREE -> MessageBackupTier.FREE
RegistrationProvisionMessage.Tier.PAID -> MessageBackupTier.PAID
null -> null
}
}
SignalStore.backup.lastBackupTime = result.message.backupTimestampMs
SignalStore.backup.usedBackupMediaSpace = result.message.backupSizeBytes
}
store.update { it.copy(isRegistering = true, provisioningMessage = result.message, qrState = QrState.Scanned) }
} else {
store.update { it.copy(showProvisioningError = true, qrState = QrState.Scanned) }

View File

@@ -34,7 +34,7 @@ fun SelectRestoreMethodScreen(
onClick = onSkip,
modifier = Modifier.align(Alignment.Center)
) {
Text(text = stringResource(R.string.registration_activity__skip))
Text(text = stringResource(R.string.registration_activity__skip_restore))
}
}
) {

View File

@@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.BaseActivity
import org.thoughtcrime.securesms.PassphraseRequiredActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.RestoreDirections
import org.thoughtcrime.securesms.registrationv3.ui.restore.RemoteRestoreActivity
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.util.navigation.safeNavigate
@@ -62,7 +63,14 @@ class RestoreActivity : BaseActivity() {
val navTarget = NavTarget.deserialize(intent.getIntExtra(EXTRA_NAV_TARGET, NavTarget.LEGACY_LANDING.value))
when (navTarget) {
NavTarget.NEW_LANDING -> navController.safeNavigate(RestoreDirections.goDirectlyToNewLanding())
NavTarget.NEW_LANDING -> {
if (sharedViewModel.hasMultipleRestoreMethods()) {
navController.safeNavigate(RestoreDirections.goDirectlyToNewLanding())
} else {
startActivity(RemoteRestoreActivity.getIntent(this, isOnlyOption = true))
finish()
}
}
NavTarget.LOCAL_RESTORE -> navController.safeNavigate(RestoreDirections.goDirectlyToChooseLocalBackup())
NavTarget.TRANSFER -> navController.safeNavigate(RestoreDirections.goDirectlyToDeviceTransfer())
else -> Unit

View File

@@ -11,6 +11,9 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.registrationv3.ui.restore.RestoreMethod
import org.thoughtcrime.securesms.restore.transferorrestore.BackupRestorationType
/**
@@ -51,4 +54,29 @@ class RestoreViewModel : ViewModel() {
fun getBackupFileUri(): Uri? = store.value.backupFile
fun getNextIntent(): Intent? = store.value.nextIntent
fun hasMultipleRestoreMethods(): Boolean {
return getAvailableRestoreMethods().size > 1
}
fun getAvailableRestoreMethods(): List<RestoreMethod> {
if (SignalStore.registration.isOtherDeviceAndroid) {
val methods = mutableListOf(RestoreMethod.FROM_OLD_DEVICE, RestoreMethod.FROM_LOCAL_BACKUP_V1)
when (SignalStore.backup.backupTier) {
MessageBackupTier.FREE -> methods.add(1, RestoreMethod.FROM_SIGNAL_BACKUPS)
MessageBackupTier.PAID -> methods.add(0, RestoreMethod.FROM_SIGNAL_BACKUPS)
null -> if (!SignalStore.backup.isBackupTierRestored) {
methods.add(1, RestoreMethod.FROM_SIGNAL_BACKUPS)
}
}
return methods
}
if (SignalStore.backup.backupTier != null || !SignalStore.backup.isBackupTierRestored) {
return listOf(RestoreMethod.FROM_SIGNAL_BACKUPS)
}
return emptyList()
}
}

View File

@@ -5,8 +5,8 @@
package org.thoughtcrime.securesms.restore.selection
import android.content.Intent
import androidx.compose.runtime.Composable
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import kotlinx.coroutines.launch
@@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.registrationv3.data.QuickRegistrationRepositor
import org.thoughtcrime.securesms.registrationv3.ui.restore.RemoteRestoreActivity
import org.thoughtcrime.securesms.registrationv3.ui.restore.RestoreMethod
import org.thoughtcrime.securesms.registrationv3.ui.restore.SelectRestoreMethodScreen
import org.thoughtcrime.securesms.restore.RestoreViewModel
import org.thoughtcrime.securesms.util.navigation.safeNavigate
import org.whispersystems.signalservice.api.registration.RestoreMethod as ApiRestoreMethod
@@ -24,10 +25,13 @@ import org.whispersystems.signalservice.api.registration.RestoreMethod as ApiRes
* Provide options to select restore/transfer operation and flow during quick registration.
*/
class SelectRestoreMethodFragment : ComposeFragment() {
private val viewModel: RestoreViewModel by activityViewModels()
@Composable
override fun FragmentContent() {
SelectRestoreMethodScreen(
restoreMethods = listOf(RestoreMethod.FROM_SIGNAL_BACKUPS, RestoreMethod.FROM_OLD_DEVICE, RestoreMethod.FROM_LOCAL_BACKUP_V1), // TODO [backups] make dynamic
restoreMethods = viewModel.getAvailableRestoreMethods(),
onRestoreMethodClicked = this::startRestoreMethod,
onSkip = {
SignalStore.registration.markSkippedTransferOrRestore()
@@ -54,7 +58,7 @@ class SelectRestoreMethodFragment : ComposeFragment() {
}
when (method) {
RestoreMethod.FROM_SIGNAL_BACKUPS -> startActivity(Intent(requireContext(), RemoteRestoreActivity::class.java))
RestoreMethod.FROM_SIGNAL_BACKUPS -> startActivity(RemoteRestoreActivity.getIntent(requireContext()))
RestoreMethod.FROM_OLD_DEVICE -> findNavController().safeNavigate(SelectRestoreMethodFragmentDirections.goToDeviceTransfer())
RestoreMethod.FROM_LOCAL_BACKUP_V1 -> findNavController().safeNavigate(SelectRestoreMethodFragmentDirections.goToLocalBackupRestore())
RestoreMethod.FROM_LOCAL_BACKUP_V2 -> error("Not currently supported")