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? { fun restoreBackupTier(aci: ACI): MessageBackupTier? {
// TODO: more complete error handling val tierResult = getBackupTier(aci)
try { when {
val lastModified = getBackupFileLastModified().successOrThrow() tierResult is NetworkResult.Success -> {
if (lastModified != null) { SignalStore.backup.backupTier = tierResult.result
SignalStore.backup.lastBackupTime = lastModified.toMillis() 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) { 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() 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_USED_MEDIA_SPACE = "backup.usedMediaSpace"
private const val KEY_BACKUP_LAST_PROTO_SIZE = "backup.lastProtoSize" private const val KEY_BACKUP_LAST_PROTO_SIZE = "backup.lastProtoSize"
private const val KEY_BACKUP_TIER = "backup.backupTier" 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_LATEST_BACKUP_TIER = "backup.latestBackupTier"
private const val KEY_LAST_CHECK_IN_MILLIS = "backup.lastCheckInMilliseconds" private const val KEY_LAST_CHECK_IN_MILLIS = "backup.lastCheckInMilliseconds"
private const val KEY_LAST_CHECK_IN_SNOOZE_MILLIS = "backup.lastCheckInSnoozeMilliseconds" private const val KEY_LAST_CHECK_IN_SNOOZE_MILLIS = "backup.lastCheckInSnoozeMilliseconds"
@ -167,12 +168,15 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
store.beginWrite() store.beginWrite()
.putLong(KEY_BACKUP_TIER, serializedValue) .putLong(KEY_BACKUP_TIER, serializedValue)
.putLong(KEY_LATEST_BACKUP_TIER, serializedValue) .putLong(KEY_LATEST_BACKUP_TIER, serializedValue)
.putBoolean(KEY_BACKUP_TIER_RESTORED, true)
.apply() .apply()
} else { } else {
putLong(KEY_BACKUP_TIER, serializedValue) 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. * 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 LOCAL_REGISTRATION_DATA = "registration.local_registration_data"
private const val RESTORE_COMPLETED = "registration.backup_restore_completed" private const val RESTORE_COMPLETED = "registration.backup_restore_completed"
private const val RESTORE_METHOD_TOKEN = "registration.restore_method_token" 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" 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 hasUploadedProfile: Boolean by booleanValue(HAS_UPLOADED_PROFILE, true)
var sessionId: String? by stringValue(SESSION_ID, null) var sessionId: String? by stringValue(SESSION_ID, null)
var sessionE164: String? by stringValue(SESSION_E164, 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) var restoreMethodToken: String? by stringValue(RESTORE_METHOD_TOKEN, null)
@get:JvmName("isRestoringOnNewDevice") @get:JvmName("isRestoringOnNewDevice")

View file

@ -86,9 +86,10 @@ object QuickRegistrationRepository {
backupTimestampMs = SignalStore.backup.lastBackupTime.coerceAtLeast(0L), backupTimestampMs = SignalStore.backup.lastBackupTime.coerceAtLeast(0L),
tier = when (SignalStore.backup.backupTier) { tier = when (SignalStore.backup.backupTier) {
MessageBackupTier.PAID -> RegistrationProvisionMessage.Tier.PAID MessageBackupTier.PAID -> RegistrationProvisionMessage.Tier.PAID
MessageBackupTier.FREE, MessageBackupTier.FREE -> RegistrationProvisionMessage.Tier.FREE
null -> RegistrationProvisionMessage.Tier.FREE null -> null
}, },
backupSizeBytes = SignalStore.backup.totalBackupSize,
restoreMethodToken = restoreMethodToken restoreMethodToken = restoreMethodToken
) )
) )
@ -145,7 +146,7 @@ object QuickRegistrationRepository {
Log.d(TAG, "Waiting for restore method with token: ***${restoreMethodToken.takeLast(4)}") Log.d(TAG, "Waiting for restore method with token: ***${restoreMethodToken.takeLast(4)}")
while (retries-- > 0 && result !is NetworkResult.Success && coroutineContext.isActive) { 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 val api = AppDependencies.registrationApi
result = api.waitForRestoreMethod(restoreMethodToken) result = api.waitForRestoreMethod(restoreMethodToken)
Log.d(TAG, "Result: $result") Log.d(TAG, "Result: $result")
@ -155,7 +156,7 @@ object QuickRegistrationRepository {
Log.i(TAG, "Restore method selected on new device ${result.result}") Log.i(TAG, "Restore method selected on new device ${result.result}")
return result.result return result.result
} else { } else {
Log.w(TAG, "Failed to determine restore method, using default") Log.w(TAG, "Failed to determine restore method, using DECLINE")
return RestoreMethod.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.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -30,10 +29,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource 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.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope 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.profiles.edit.CreateProfileActivity
import org.thoughtcrime.securesms.registrationv3.ui.shared.RegistrationScreen import org.thoughtcrime.securesms.registrationv3.ui.shared.RegistrationScreen
import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.viewModel
import java.util.Locale import java.util.Locale
/** /**
@ -70,12 +66,19 @@ import java.util.Locale
*/ */
class RemoteRestoreActivity : BaseActivity() { class RemoteRestoreActivity : BaseActivity() {
companion object { 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -100,7 +103,14 @@ class RemoteRestoreActivity : BaseActivity() {
RestoreFromBackupContent( RestoreFromBackupContent(
state = state, state = state,
onRestoreBackupClick = { viewModel.restore() }, onRestoreBackupClick = { viewModel.restore() },
onCancelClick = { finish() }, onCancelClick = {
if (state.isRemoteRestoreOnlyOption) {
viewModel.skipRestore()
startActivity(MainActivity.clearTop(this))
}
finish()
},
onErrorDialogDismiss = { viewModel.clearError() } onErrorDialogDismiss = { viewModel.clearError() }
) )
} }
@ -137,25 +147,57 @@ private fun RestoreFromBackupContent(
onCancelClick: () -> Unit = {}, onCancelClick: () -> Unit = {},
onErrorDialogDismiss: () -> Unit = {} onErrorDialogDismiss: () -> Unit = {}
) { ) {
val subtitle = buildAnnotatedString { when (state.loadState) {
append( RemoteRestoreViewModel.ScreenState.LoadState.LOADING -> {
stringResource( Dialogs.IndeterminateProgressDialog(
id = R.string.RemoteRestoreActivity__backup_created_at, message = stringResource(R.string.RemoteRestoreActivity__fetching_backup_details)
DateUtils.formatDateWithoutDayOfWeek(Locale.getDefault(), state.backupTime),
DateUtils.getOnlyTimeString(LocalContext.current, state.backupTime)
) )
)
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( RegistrationScreen(
title = stringResource(id = R.string.RemoteRestoreActivity__restore_from_backup), title = stringResource(id = R.string.RemoteRestoreActivity__restore_from_backup),
subtitle = if (state.isLoaded()) subtitle else null, subtitle = subtitle,
bottomContent = { bottomContent = {
Column { Column {
if (state.isLoaded()) { if (state.isLoaded()) {
@ -171,45 +213,31 @@ private fun RestoreFromBackupContent(
onClick = onCancelClick, onClick = onCancelClick,
modifier = Modifier.fillMaxWidth() 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) { Column(
RemoteRestoreViewModel.ScreenState.LoadState.LOADING -> { modifier = Modifier
Dialogs.IndeterminateProgressDialog( .fillMaxWidth()
message = stringResource(R.string.RemoteRestoreActivity__fetching_backup_details) .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) { when (state.importState) {
@ -229,6 +257,7 @@ private fun RestoreFromBackupContentPreview() {
state = RemoteRestoreViewModel.ScreenState( state = RemoteRestoreViewModel.ScreenState(
backupTier = MessageBackupTier.PAID, backupTier = MessageBackupTier.PAID,
backupTime = System.currentTimeMillis(), backupTime = System.currentTimeMillis(),
backupSize = 1234567.bytes,
importState = RemoteRestoreViewModel.ImportState.None, importState = RemoteRestoreViewModel.ImportState.None,
restoreProgress = null restoreProgress = null
) )

View file

@ -16,6 +16,8 @@ import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext 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.signal.core.util.logging.Log
import org.thoughtcrime.securesms.backup.v2.BackupRepository import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier 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.jobs.SyncArchivedMediaJob
import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.registration.util.RegistrationUtil import org.thoughtcrime.securesms.registration.util.RegistrationUtil
import org.thoughtcrime.securesms.registrationv3.data.QuickRegistrationRepository
import org.thoughtcrime.securesms.registrationv3.data.RegistrationRepository import org.thoughtcrime.securesms.registrationv3.data.RegistrationRepository
import org.whispersystems.signalservice.api.registration.RestoreMethod
class RemoteRestoreViewModel : ViewModel() { class RemoteRestoreViewModel(isOnlyRestoreOption: Boolean) : ViewModel() {
companion object { companion object {
private val TAG = Log.tag(RemoteRestoreViewModel::class) private val TAG = Log.tag(RemoteRestoreViewModel::class)
@ -38,8 +42,10 @@ class RemoteRestoreViewModel : ViewModel() {
private val store: MutableStateFlow<ScreenState> = MutableStateFlow( private val store: MutableStateFlow<ScreenState> = MutableStateFlow(
ScreenState( ScreenState(
isRemoteRestoreOnlyOption = isOnlyRestoreOption,
backupTier = SignalStore.backup.backupTier, 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 { init {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val restored = BackupRepository.restoreBackupTier(SignalStore.account.requireAci()) != null val tier: MessageBackupTier? = BackupRepository.restoreBackupTier(SignalStore.account.requireAci())
store.update { store.update {
if (restored) { if (tier != null) {
it.copy( it.copy(
loadState = ScreenState.LoadState.LOADED, loadState = ScreenState.LoadState.LOADED,
backupTier = SignalStore.backup.backupTier, backupTier = SignalStore.backup.backupTier,
backupTime = SignalStore.backup.lastBackupTime backupTime = SignalStore.backup.lastBackupTime,
backupSize = SignalStore.backup.totalBackupSize.bytes
) )
} else { } else {
it.copy( if (SignalStore.backup.isBackupTierRestored) {
loadState = ScreenState.LoadState.FAILURE 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) } store.update { it.copy(importState = ImportState.InProgress) }
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
QuickRegistrationRepository.setRestoreMethodForOldDevice(RestoreMethod.REMOTE_BACKUP)
val jobStateFlow = callbackFlow { val jobStateFlow = callbackFlow {
val listener = JobTracker.JobListener { _, jobState -> val listener = JobTracker.JobListener { _, jobState ->
trySend(jobState) trySend(jobState)
@ -129,9 +142,21 @@ class RemoteRestoreViewModel : ViewModel() {
store.update { it.copy(importState = ImportState.None, restoreProgress = null) } 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( data class ScreenState(
val isRemoteRestoreOnlyOption: Boolean = false,
val backupTier: MessageBackupTier? = null, val backupTier: MessageBackupTier? = null,
val backupTime: Long = -1, val backupTime: Long = -1,
val backupSize: ByteSize = 0.bytes,
val importState: ImportState = ImportState.None, val importState: ImportState = ImportState.None,
val restoreProgress: RestoreV2Event? = null, val restoreProgress: RestoreV2Event? = null,
val loadState: LoadState = if (backupTier != null) LoadState.LOADED else LoadState.LOADING val loadState: LoadState = if (backupTier != null) LoadState.LOADED else LoadState.LOADING
@ -141,12 +166,8 @@ class RemoteRestoreViewModel : ViewModel() {
return loadState == LoadState.LOADED return loadState == LoadState.LOADED
} }
fun isLoading(): Boolean {
return loadState == LoadState.LOADING
}
enum class LoadState { 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.SignalPreview
import org.signal.core.ui.horizontalGutters import org.signal.core.ui.horizontalGutters
import org.signal.core.ui.theme.SignalTheme import org.signal.core.ui.theme.SignalTheme
import org.signal.registration.proto.RegistrationProvisionMessage
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCode import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCode
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeData import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeData
import org.thoughtcrime.securesms.compose.ComposeFragment import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.registrationv3.ui.RegistrationViewModel import org.thoughtcrime.securesms.registrationv3.ui.RegistrationViewModel
import org.thoughtcrime.securesms.registrationv3.ui.shared.RegistrationScreen 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. * Show QR code on new device to allow registration and restore via old device.
@ -84,7 +86,11 @@ class RestoreViaQrFragment : ComposeFragment() {
.mapNotNull { it.provisioningMessage } .mapNotNull { it.provisioningMessage }
.distinctUntilChanged() .distinctUntilChanged()
.collect { message -> .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 kotlinx.coroutines.flow.update
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.signal.registration.proto.RegistrationProvisionMessage 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.components.settings.app.usernamelinks.QrCodeData
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.dependencies.AppDependencies
@ -80,6 +81,18 @@ class RestoreViaQrViewModel : ViewModel() {
if (result is SecondaryProvisioningCipher.RegistrationProvisionResult.Success) { if (result is SecondaryProvisioningCipher.RegistrationProvisionResult.Success) {
Log.i(TAG, "Saving restore method token: ***${result.message.restoreMethodToken.takeLast(4)}") Log.i(TAG, "Saving restore method token: ***${result.message.restoreMethodToken.takeLast(4)}")
SignalStore.registration.restoreMethodToken = result.message.restoreMethodToken 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) } store.update { it.copy(isRegistering = true, provisioningMessage = result.message, qrState = QrState.Scanned) }
} else { } else {
store.update { it.copy(showProvisioningError = true, qrState = QrState.Scanned) } store.update { it.copy(showProvisioningError = true, qrState = QrState.Scanned) }

View file

@ -34,7 +34,7 @@ fun SelectRestoreMethodScreen(
onClick = onSkip, onClick = onSkip,
modifier = Modifier.align(Alignment.Center) 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.PassphraseRequiredActivity
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.RestoreDirections import org.thoughtcrime.securesms.RestoreDirections
import org.thoughtcrime.securesms.registrationv3.ui.restore.RemoteRestoreActivity
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
import org.thoughtcrime.securesms.util.RemoteConfig import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.util.navigation.safeNavigate 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)) val navTarget = NavTarget.deserialize(intent.getIntExtra(EXTRA_NAV_TARGET, NavTarget.LEGACY_LANDING.value))
when (navTarget) { 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.LOCAL_RESTORE -> navController.safeNavigate(RestoreDirections.goDirectlyToChooseLocalBackup())
NavTarget.TRANSFER -> navController.safeNavigate(RestoreDirections.goDirectlyToDeviceTransfer()) NavTarget.TRANSFER -> navController.safeNavigate(RestoreDirections.goDirectlyToDeviceTransfer())
else -> Unit else -> Unit

View file

@ -11,6 +11,9 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData import androidx.lifecycle.asLiveData
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update 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 import org.thoughtcrime.securesms.restore.transferorrestore.BackupRestorationType
/** /**
@ -51,4 +54,29 @@ class RestoreViewModel : ViewModel() {
fun getBackupFileUri(): Uri? = store.value.backupFile fun getBackupFileUri(): Uri? = store.value.backupFile
fun getNextIntent(): Intent? = store.value.nextIntent 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 package org.thoughtcrime.securesms.restore.selection
import android.content.Intent
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import kotlinx.coroutines.launch 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.RemoteRestoreActivity
import org.thoughtcrime.securesms.registrationv3.ui.restore.RestoreMethod import org.thoughtcrime.securesms.registrationv3.ui.restore.RestoreMethod
import org.thoughtcrime.securesms.registrationv3.ui.restore.SelectRestoreMethodScreen import org.thoughtcrime.securesms.registrationv3.ui.restore.SelectRestoreMethodScreen
import org.thoughtcrime.securesms.restore.RestoreViewModel
import org.thoughtcrime.securesms.util.navigation.safeNavigate import org.thoughtcrime.securesms.util.navigation.safeNavigate
import org.whispersystems.signalservice.api.registration.RestoreMethod as ApiRestoreMethod 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. * Provide options to select restore/transfer operation and flow during quick registration.
*/ */
class SelectRestoreMethodFragment : ComposeFragment() { class SelectRestoreMethodFragment : ComposeFragment() {
private val viewModel: RestoreViewModel by activityViewModels()
@Composable @Composable
override fun FragmentContent() { override fun FragmentContent() {
SelectRestoreMethodScreen( 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, onRestoreMethodClicked = this::startRestoreMethod,
onSkip = { onSkip = {
SignalStore.registration.markSkippedTransferOrRestore() SignalStore.registration.markSkippedTransferOrRestore()
@ -54,7 +58,7 @@ class SelectRestoreMethodFragment : ComposeFragment() {
} }
when (method) { 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_OLD_DEVICE -> findNavController().safeNavigate(SelectRestoreMethodFragmentDirections.goToDeviceTransfer())
RestoreMethod.FROM_LOCAL_BACKUP_V1 -> findNavController().safeNavigate(SelectRestoreMethodFragmentDirections.goToLocalBackupRestore()) RestoreMethod.FROM_LOCAL_BACKUP_V1 -> findNavController().safeNavigate(SelectRestoreMethodFragmentDirections.goToLocalBackupRestore())
RestoreMethod.FROM_LOCAL_BACKUP_V2 -> error("Not currently supported") 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 <fragment
android:id="@+id/grantPermissionsFragment" android:id="@+id/grantPermissionsFragment"
android:name="org.thoughtcrime.securesms.registrationv3.ui.permissions.GrantPermissionsFragment" android:name="org.thoughtcrime.securesms.registrationv3.ui.permissions.GrantPermissionsFragment"
android:label="fragment_grant_permissions"> android:label="fragment_grant_permissions"
tools:ignore="NewApi">
<argument <argument
android:name="welcomeUserSelection" android:name="welcomeUserSelection"
@ -59,8 +60,34 @@
<fragment <fragment
android:id="@+id/restoreViaQr" android:id="@+id/restoreViaQr"
android:name="org.thoughtcrime.securesms.registrationv3.ui.restore.RestoreViaQrFragment"> 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>
<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 <fragment
android:id="@+id/selectRestoreMethod" android:id="@+id/selectRestoreMethod"
android:name="org.thoughtcrime.securesms.registrationv3.ui.restore.SelectManualRestoreMethodFragment"> 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> <string name="RemoteRestoreActivity__all_of_your_messages">All of your messages</string>
<!-- Screen title for restoring from backup --> <!-- Screen title for restoring from backup -->
<string name="RemoteRestoreActivity__restore_from_backup">Restore from backup</string> <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 --> <!-- Section title for explaining what your backup includes -->
<string name="RemoteRestoreActivity__your_backup_includes">Your backup includes:</string> <string name="RemoteRestoreActivity__your_backup_includes">Your backup includes:</string>
<!-- Primary action button copy for starting restoration --> <!-- Primary action button copy for starting restoration -->
<string name="RemoteRestoreActivity__restore_backup">Restore backup</string> <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> <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 --> <!-- Progress dialog label while fetching backup info if we don\'t already have it -->
<string name="RemoteRestoreActivity__fetching_backup_details">Fetching backup details…</string> <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 --> <!-- GroupMentionSettingDialog -->
<string name="GroupMentionSettingDialog_notify_me_for_mentions">Notify me for Mentions</string> <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__restore_or_transfer">Restore or transfer</string>
<string name="registration_activity__transfer_account">Transfer account</string> <string name="registration_activity__transfer_account">Transfer account</string>
<string name="registration_activity__skip">Skip</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__chat_backups">Chat backups</string>
<string name="preferences_chats__transfer_account">Transfer account</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> <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 --> <!-- Restore Complete bottom sheet dialog button text to dismiss sheet -->
<string name="RestoreCompleteBottomSheet_button">Okay</string> <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 --> <!-- EOF -->
</resources> </resources>

View file

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

View file

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