Flesh out restore paths for regv3.
This commit is contained in:
parent
9833101cd1
commit
f42bd0f374
18 changed files with 415 additions and 101 deletions
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
16
app/src/main/res/drawable/symbol_device_phone_24.xml
Normal file
16
app/src/main/res/drawable/symbol_device_phone_24.xml
Normal 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>
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue