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
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
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")

View file

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M14 21c0.41 0 0.75-0.34 0.75-0.75S14.41 19.5 14 19.5h-4c-0.41 0-0.75 0.34-0.75 0.75S9.59 21 10 21h4Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M12 3.13c-0.62 0-1.13 0.5-1.13 1.12 0 0.62 0.5 1.13 1.13 1.13 0.62 0 1.13-0.5 1.13-1.13 0-0.62-0.5-1.13-1.13-1.13Z"/>
<path
android:fillColor="#FF000000"
android:fillType="evenOdd"
android:pathData="M10.26 0.38c-0.8 0-1.47 0-2 0.04C7.7 0.46 7.2 0.56 6.74 0.8c-0.73 0.37-1.32 0.96-1.7 1.7C4.82 2.95 4.72 3.44 4.68 4 4.62 4.55 4.62 5.2 4.63 6.02V18c0 0.8 0 1.47 0.04 2 0.04 0.56 0.14 1.05 0.38 1.52 0.37 0.73 0.96 1.32 1.7 1.7 0.46 0.23 0.95 0.33 1.5 0.37 0.54 0.05 1.2 0.05 2.01 0.05h3.48c0.8 0 1.47 0 2-0.05 0.56-0.04 1.05-0.14 1.52-0.38 0.73-0.37 1.32-0.96 1.7-1.7 0.23-0.46 0.33-0.95 0.37-1.5 0.05-0.54 0.05-1.2 0.05-2.01V6c0-0.8 0-1.47-0.05-2-0.04-0.56-0.14-1.05-0.38-1.52-0.37-0.73-0.96-1.32-1.7-1.7-0.46-0.23-0.95-0.33-1.5-0.37-0.54-0.05-1.2-0.05-2.01-0.04h-3.48ZM7.54 2.35C7.7 2.26 7.95 2.2 8.4 2.16c0.46-0.03 1.05-0.04 1.9-0.04h3.4c0.85 0 1.44 0 1.9 0.04 0.45 0.04 0.69 0.1 0.86 0.2 0.4 0.2 0.73 0.53 0.93 0.93 0.1 0.17 0.16 0.41 0.2 0.86 0.03 0.46 0.04 1.05 0.04 1.9v11.9c0 0.85 0 1.44-0.04 1.9-0.04 0.45-0.1 0.69-0.2 0.86-0.2 0.4-0.53 0.73-0.93 0.93-0.17 0.1-0.41 0.16-0.86 0.2-0.46 0.03-1.05 0.04-1.9 0.04h-3.4c-0.85 0-1.44 0-1.9-0.04-0.45-0.04-0.69-0.1-0.86-0.2-0.4-0.2-0.73-0.53-0.93-0.93-0.1-0.17-0.16-0.41-0.2-0.86-0.03-0.46-0.04-1.05-0.04-1.9V6.05c0-0.85 0-1.44 0.04-1.9 0.04-0.45 0.1-0.69 0.2-0.86 0.2-0.4 0.53-0.73 0.93-0.93Z"/>
</vector>

View file

@ -48,7 +48,8 @@
<fragment
android:id="@+id/grantPermissionsFragment"
android:name="org.thoughtcrime.securesms.registrationv3.ui.permissions.GrantPermissionsFragment"
android:label="fragment_grant_permissions">
android:label="fragment_grant_permissions"
tools:ignore="NewApi">
<argument
android:name="welcomeUserSelection"
@ -59,8 +60,34 @@
<fragment
android:id="@+id/restoreViaQr"
android:name="org.thoughtcrime.securesms.registrationv3.ui.restore.RestoreViaQrFragment">
<action
android:id="@+id/go_to_noBackupToRestore"
app:destination="@id/noBackupToRestore"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim"
app:popUpTo="@id/signup"/>
</fragment>
<fragment
android:id="@+id/noBackupToRestore"
android:name="org.thoughtcrime.securesms.registrationv3.ui.restore.NoBackupToRestoreFragment">
<action
android:id="@+id/restart_registration_flow"
app:destination="@id/signup"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim"
app:popUpTo="@id/signup"/>
</fragment>
<fragment
android:id="@+id/selectRestoreMethod"
android:name="org.thoughtcrime.securesms.registrationv3.ui.restore.SelectManualRestoreMethodFragment">

View file

@ -1356,16 +1356,18 @@
<string name="RemoteRestoreActivity__all_of_your_messages">All of your messages</string>
<!-- Screen title for restoring from backup -->
<string name="RemoteRestoreActivity__restore_from_backup">Restore from backup</string>
<!-- Notice about what media will be included in backups. Placeholder is days, and is currently fixed at 30. -->
<string name="RemoteRestoreActivity__only_media_sent_or_received">Only media sent or received in the past %1$d days is included.</string>
<!-- Section title for explaining what your backup includes -->
<string name="RemoteRestoreActivity__your_backup_includes">Your backup includes:</string>
<!-- Primary action button copy for starting restoration -->
<string name="RemoteRestoreActivity__restore_backup">Restore backup</string>
<!-- Displayed at restore time to tell the user when their last backup was made. %$1s is replaced with the date (e.g. March 5, 2024) and %$2s is replaced with the time (e.g. 9:00am) -->
<!-- Displayed at restore time to tell the user when their last backup was made. %1$s is replaced with the date (e.g. March 5, 2024) and %2$s is replaced with the time (e.g. 9:00am) -->
<string name="RemoteRestoreActivity__backup_created_at">Your last backup was made on %1$s at %2$s.</string>
<!-- Displayed at restore time to tell the user when their last backup was made and size. %1$s is replaced with the date (e.g. March 5, 2024), %2$s is replaced with the time (e.g. 9:00am), %3$1 is replaced with size (e.g., 1.2GB) -->
<string name="RemoteRestoreActivity__backup_created_at_with_size">Your last backup was made on %1$s at %2$s. Your backup size is %3$s.</string>
<!-- Progress dialog label while fetching backup info if we don\'t already have it -->
<string name="RemoteRestoreActivity__fetching_backup_details">Fetching backup details…</string>
<!-- Text label button to skip restore from remote -->
<string name="RemoteRestoreActivity__skip_restore">Skip restore</string>
<!-- GroupMentionSettingDialog -->
<string name="GroupMentionSettingDialog_notify_me_for_mentions">Notify me for Mentions</string>
@ -4315,6 +4317,8 @@
<string name="registration_activity__restore_or_transfer">Restore or transfer</string>
<string name="registration_activity__transfer_account">Transfer account</string>
<string name="registration_activity__skip">Skip</string>
<!-- Text label button to skip restoring -->
<string name="registration_activity__skip_restore">Skip restore</string>
<string name="preferences_chats__chat_backups">Chat backups</string>
<string name="preferences_chats__transfer_account">Transfer account</string>
<string name="preferences_chats__transfer_account_to_a_new_android_device">Transfer account to a new Android device</string>
@ -7956,6 +7960,18 @@
<!-- Restore Complete bottom sheet dialog button text to dismiss sheet -->
<string name="RestoreCompleteBottomSheet_button">Okay</string>
<!-- No Backup to Restore screen title -->
<string name="NoBackupToRestore_title">No Backup to Restore</string>
<!-- No Backup to Restore screen subtitle -->
<string name="NoBackupToRestore_subtitle">Because you\'re moving from iPhone to Android, the only way to transfer your messages and media is by enabling Signal Backups on your old device.</string>
<!-- No Backup to Restore step 1 to enable backups on old device -->
<string name="NoBackupToRestore_step1">Open Signal on your old device</string>
<!-- No Backup to Restore step 2 to enable backups on old device -->
<string name="NoBackupToRestore_step2">Tap Settings > Backups</string>
<!-- No Backup to Restore step 3 to enable backups on old device -->
<string name="NoBackupToRestore_step3">Enable backups and wait until your backup is complete</string>
<!-- No backup to Restore tonal cta to skip restore -->
<string name="NoBackupToRestore_skip_restore">Skip restore</string>
<!-- EOF -->
</resources>

View file

@ -25,6 +25,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
@ -156,7 +157,7 @@ object Dialogs {
Spacer(modifier = Modifier.size(24.dp))
CircularProgressIndicator()
Spacer(modifier = Modifier.size(20.dp))
Text(message)
Text(text = message, textAlign = TextAlign.Center)
}
},
modifier = Modifier

View file

@ -25,7 +25,8 @@ message RegistrationProvisionMessage {
string pin = 4;
Platform platform = 5;
uint64 backupTimestampMs = 6;
Tier tier = 7;
string restoreMethodToken = 8;
reserved 9; // iOSDeviceTransferMessage
optional Tier tier = 7;
uint64 backupSizeBytes = 8;
string restoreMethodToken = 9;
reserved 10; // iOSDeviceTransferMessage
}