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? {
|
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) }
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
|
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
|
<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">
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue