Restore timestamp instead of tier during manual registration remote backup restore flow.

This commit is contained in:
Cody Henthorne
2025-07-02 11:24:36 -04:00
committed by GitHub
parent ec8bb17bff
commit 437b1a3d98
15 changed files with 235 additions and 132 deletions

View File

@@ -1324,17 +1324,16 @@ object BackupRepository {
}
}
fun getBackupFileLastModified(): NetworkResult<ZonedDateTime?> {
fun getBackupFileLastModified(): NetworkResult<ZonedDateTime> {
return initBackupAndFetchAuth()
.then { credential ->
SignalNetwork.archive.getBackupInfo(SignalStore.account.requireAci(), credential.messageBackupAccess)
}
.then { info -> getCdnReadCredentials(CredentialType.MESSAGE, info.cdn ?: Cdn.CDN_3.cdnNumber).map { it.headers to info } }
.then { info -> getCdnReadCredentials(CredentialType.MESSAGE, info.cdn ?: RemoteConfig.backupFallbackArchiveCdn).map { it.headers to info } }
.then { pair ->
val (cdnCredentials, info) = pair
val messageReceiver = AppDependencies.signalServiceMessageReceiver
NetworkResult.fromFetch {
messageReceiver.getCdnLastModifiedTime(info.cdn!!, cdnCredentials, "backups/${info.backupDir}/${info.backupName}")
AppDependencies.signalServiceMessageReceiver.getCdnLastModifiedTime(info.cdn!!, cdnCredentials, "backups/${info.backupDir}/${info.backupName}")
}
}
}
@@ -1516,48 +1515,29 @@ object BackupRepository {
.also { Log.i(TAG, "getCdnReadCredentialsResult: ${it::class.simpleName}") }
}
fun restoreBackupTier(aci: ACI): MessageBackupTier? {
val tierResult = getBackupTier(aci)
fun restoreBackupFileTimestamp(): RestoreTimestampResult {
val timestampResult: NetworkResult<ZonedDateTime> = getBackupFileLastModified()
when {
tierResult is NetworkResult.Success -> {
SignalStore.backup.backupTier = tierResult.result
Log.d(TAG, "Backup tier restored: ${SignalStore.backup.backupTier}")
timestampResult is NetworkResult.Success -> {
SignalStore.backup.lastBackupTime = timestampResult.result.toMillis()
SignalStore.backup.isBackupTimestampRestored = true
SignalStore.uiHints.markHasEverEnabledRemoteBackups()
return RestoreTimestampResult.Success(SignalStore.backup.lastBackupTime)
}
tierResult is NetworkResult.StatusCodeError && tierResult.code == 404 -> {
Log.i(TAG, "Backups not enabled")
SignalStore.backup.backupTier = null
timestampResult is NetworkResult.StatusCodeError && timestampResult.code == 404 -> {
Log.i(TAG, "No backup file exists")
SignalStore.backup.lastBackupTime = 0L
SignalStore.backup.isBackupTimestampRestored = true
return RestoreTimestampResult.NotFound
}
else -> {
Log.w(TAG, "Could not retrieve backup tier.", tierResult.getCause())
return SignalStore.backup.backupTier
Log.w(TAG, "Could not check for backup file.", timestampResult.getCause())
return RestoreTimestampResult.Failure
}
}
SignalStore.backup.isBackupTierRestored = true
if (SignalStore.backup.backupTier != null) {
val timestampResult = getBackupFileLastModified()
when {
timestampResult is NetworkResult.Success -> {
SignalStore.backup.lastBackupTime = timestampResult.result?.toMillis() ?: 0L
}
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()
}
return SignalStore.backup.backupTier
}
fun verifyBackupKeyAssociatedWithAccount(aci: ACI, aep: AccountEntropyPool): MessageBackupTier? {
@@ -1944,6 +1924,12 @@ sealed interface RemoteRestoreResult {
data object Failure : RemoteRestoreResult
}
sealed interface RestoreTimestampResult {
data class Success(val timestamp: Long) : RestoreTimestampResult
data object NotFound : RestoreTimestampResult
data object Failure : RestoreTimestampResult
}
/**
* Iterator that reads values from the given cursor. Expects that REMOTE_DIGEST is present and non-null, and ARCHIVE_CDN is present.
*

View File

@@ -261,7 +261,7 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
fun checkRemoteBackupState() {
disposables += Single
.fromCallable {
BackupRepository.restoreBackupTier(SignalStore.account.requireAci())
BackupRepository.restoreBackupFileTimestamp()
BackupRepository.debugGetRemoteBackupState()
}
.subscribeOn(Schedulers.io())

View File

@@ -40,7 +40,7 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
private const val KEY_BACKUP_LAST_PROTO_SIZE = "backup.lastProtoSize"
private const val KEY_BACKUP_TIER = "backup.backupTier"
private const val KEY_BACKUP_TIER_INTERNAL_OVERRIDE = "backup.backupTier.internalOverride"
private const val KEY_BACKUP_TIER_RESTORED = "backup.backupTierRestored"
private const val KEY_BACKUP_TIMESTAMP_RESTORED = "backup.backupTimeRestored"
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"
@@ -215,7 +215,7 @@ 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)
.putBoolean(KEY_BACKUP_TIMESTAMP_RESTORED, true)
.apply()
deletionState = DeletionState.NONE
@@ -227,7 +227,8 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
/** An internal setting that can override the backup tier for a user. */
var backupTierInternalOverride: MessageBackupTier? by enumValue(KEY_BACKUP_TIER_INTERNAL_OVERRIDE, null, MessageBackupTier.Serializer).withPrecondition { RemoteConfig.internalUser }
var isBackupTierRestored: Boolean by booleanValue(KEY_BACKUP_TIER_RESTORED, false)
/** Set to true if we successfully restored a backup file timestamp or didn't find a file at all so a "no timestamp" value is restored. */
var isBackupTimestampRestored: Boolean by booleanValue(KEY_BACKUP_TIMESTAMP_RESTORED, false)
/**
* When uploading a backup, we store the progress state here so that it can remain across app restarts.

View File

@@ -7,8 +7,6 @@ import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign
import io.reactivex.rxjava3.schedulers.Schedulers
import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
import org.thoughtcrime.securesms.lock.v2.SvrConstants
import org.thoughtcrime.securesms.util.DefaultValueLiveData
@@ -37,11 +35,7 @@ class PinRestoreViewModel : ViewModel() {
}
disposables += Single
.fromCallable {
val response = repo.restoreMasterKeyPostRegistration(pin, pinKeyboardType)
BackupRepository.restoreBackupTier(SignalStore.account.requireAci())
response
}
.fromCallable { repo.restoreMasterKeyPostRegistration(pin, pinKeyboardType) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { result ->

View File

@@ -23,6 +23,6 @@ enum class RegistrationCheckpoint {
PIN_ENTERED,
VERIFICATION_CODE_VALIDATED,
SERVICE_REGISTRATION_COMPLETED,
BACKUP_TIER_NOT_RESTORED,
BACKUP_TIMESTAMP_NOT_RESTORED,
LOCAL_REGISTRATION_COMPLETE
}

View File

@@ -27,11 +27,9 @@ import org.signal.core.util.isNotNullOrBlank
import org.signal.core.util.logging.Log
import org.signal.libsignal.protocol.IdentityKeyPair
import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.backup.v2.RestoreTimestampResult
import org.thoughtcrime.securesms.database.model.databaseprotos.RestoreDecisionState
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileContentUpdateJob
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileKeyUpdateJob
import org.thoughtcrime.securesms.jobs.ProfileUploadJob
import org.thoughtcrime.securesms.jobs.ReclaimUsernameAndLinkJob
import org.thoughtcrime.securesms.keyvalue.NewAccount
import org.thoughtcrime.securesms.keyvalue.SignalStore
@@ -71,7 +69,6 @@ import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequ
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.TokenNotAccepted
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.UnknownError
import org.thoughtcrime.securesms.registration.ui.toE164
import org.thoughtcrime.securesms.registration.util.RegistrationUtil
import org.thoughtcrime.securesms.registration.viewmodel.SvrAuthCredentialSet
import org.thoughtcrime.securesms.registrationv3.data.RegistrationRepository
import org.thoughtcrime.securesms.registrationv3.ui.restore.StorageServiceRestore
@@ -916,24 +913,27 @@ class RegistrationViewModel : ViewModel() {
}
if (SignalStore.account.restoredAccountEntropyPool) {
Log.d(TAG, "Restoring backup tier")
Log.d(TAG, "Restoring backup timestamp")
var tries = 0
while (tries < 3 && !SignalStore.backup.isBackupTierRestored) {
while (tries < 3) {
if (tries > 0) {
delay(1.seconds)
}
BackupRepository.restoreBackupTier(SignalStore.account.requireAci())
if (BackupRepository.restoreBackupFileTimestamp() !is RestoreTimestampResult.Failure) {
break
}
tries++
}
}
refreshRemoteConfig()
val checkpoint = if (SignalStore.registration.restoreDecisionState.isDecisionPending &&
val checkpoint = if (
SignalStore.registration.restoreDecisionState.isDecisionPending &&
SignalStore.registration.restoreDecisionState.isWantingManualRemoteRestore &&
(!SignalStore.backup.isBackupTierRestored || SignalStore.backup.lastBackupTime == 0L)
SignalStore.backup.lastBackupTime == 0L
) {
RegistrationCheckpoint.BACKUP_TIER_NOT_RESTORED
RegistrationCheckpoint.BACKUP_TIMESTAMP_NOT_RESTORED
} else {
RegistrationCheckpoint.LOCAL_REGISTRATION_COMPLETE
}
@@ -963,19 +963,19 @@ class RegistrationViewModel : ViewModel() {
}
}
fun restoreBackupTier() {
fun checkForBackupFile() {
store.update {
it.copy(inProgress = true, registrationCheckpoint = RegistrationCheckpoint.SERVICE_REGISTRATION_COMPLETED)
}
viewModelScope.launch {
viewModelScope.launch(Dispatchers.IO) {
val start = System.currentTimeMillis()
val tierUnknown = BackupRepository.restoreBackupTier(SignalStore.account.requireAci()) == null
val result = BackupRepository.restoreBackupFileTimestamp()
delay(max(0L, 500L - (System.currentTimeMillis() - start)))
if (tierUnknown || SignalStore.backup.lastBackupTime == 0L) {
if (result !is RestoreTimestampResult.Success) {
store.update {
it.copy(registrationCheckpoint = RegistrationCheckpoint.BACKUP_TIER_NOT_RESTORED)
it.copy(registrationCheckpoint = RegistrationCheckpoint.BACKUP_TIMESTAMP_NOT_RESTORED)
}
} else {
store.update {
@@ -985,11 +985,6 @@ class RegistrationViewModel : ViewModel() {
}
}
fun completeRegistration() {
AppDependencies.jobManager.startChain(ProfileUploadJob()).then(listOf(MultiDeviceProfileKeyUpdateJob(), MultiDeviceProfileContentUpdateJob())).enqueue()
RegistrationUtil.maybeMarkRegistrationComplete()
}
fun networkErrorShown() {
store.update {
it.copy(networkError = null)

View File

@@ -67,9 +67,9 @@ class EnterBackupKeyFragment : ComposeFragment() {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
sharedViewModel
.state
.filter { it.registrationCheckpoint == RegistrationCheckpoint.BACKUP_TIER_NOT_RESTORED }
.filter { it.registrationCheckpoint == RegistrationCheckpoint.BACKUP_TIMESTAMP_NOT_RESTORED }
.collect {
viewModel.handleBackupTierNotRestored()
viewModel.handleBackupTimestampNotRestored()
}
}
}
@@ -124,7 +124,7 @@ class EnterBackupKeyFragment : ComposeFragment() {
state = state,
onBackupTierRetry = {
viewModel.incrementBackupTierRetry()
sharedViewModel.restoreBackupTier()
sharedViewModel.checkForBackupFile()
},
onAbandonRemoteRestoreAfterRegistration = {
viewLifecycleOwner.lifecycleScope.launch {

View File

@@ -86,10 +86,10 @@ class EnterBackupKeyViewModel : ViewModel() {
}
}
fun handleBackupTierNotRestored() {
fun handleBackupTimestampNotRestored() {
store.update {
it.copy(
showBackupTierNotRestoreError = if (SignalStore.backup.isBackupTierRestored) TierRestoreError.NOT_FOUND else TierRestoreError.NETWORK_ERROR
showBackupTierNotRestoreError = if (SignalStore.backup.isBackupTimestampRestored) TierRestoreError.NOT_FOUND else TierRestoreError.NETWORK_ERROR
)
}
}

View File

@@ -14,13 +14,17 @@ import androidx.activity.viewModels
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
@@ -29,8 +33,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -65,6 +73,7 @@ import org.thoughtcrime.securesms.components.contactsupport.SendSupportEmailEffe
import org.thoughtcrime.securesms.conversation.v2.registerForLifecycle
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.registrationv3.ui.shared.RegistrationScreen
import org.thoughtcrime.securesms.registrationv3.ui.shared.RegistrationScreenTitleSubtitle
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.PlayStoreUtil
import org.thoughtcrime.securesms.util.viewModel
@@ -264,8 +273,34 @@ private fun BackupAvailableContent(
}
RegistrationScreen(
title = stringResource(id = R.string.RemoteRestoreActivity__restore_from_backup),
subtitle = subtitle,
topContent = {
if (state.backupTier != null) {
RegistrationScreenTitleSubtitle(
title = stringResource(id = R.string.RemoteRestoreActivity__restore_from_backup),
subtitle = AnnotatedString(subtitle)
)
} else {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.symbol_backup_24),
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier
.size(64.dp)
.background(color = SignalTheme.colors.colorSurface2, shape = CircleShape)
.padding(12.dp)
.align(Alignment.CenterHorizontally)
)
Spacer(modifier = Modifier.size(16.dp))
Text(
text = stringResource(id = R.string.RemoteRestoreActivity__restore_from_backup),
style = MaterialTheme.typography.headlineMedium.copy(textAlign = TextAlign.Center),
modifier = Modifier.fillMaxWidth()
)
}
},
bottomContent = {
Column {
if (state.isLoaded()) {
@@ -286,26 +321,48 @@ private fun BackupAvailableContent(
}
}
) {
Column(
modifier = Modifier
.fillMaxWidth()
.background(color = SignalTheme.colors.colorSurface2, shape = RoundedCornerShape(18.dp))
.padding(horizontal = 20.dp)
.padding(top = 20.dp, bottom = 18.dp)
) {
if (state.backupTier != null) {
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, state.backupMediaTTL).forEach {
MessageBackupsTypeFeatureRow(
messageBackupsTypeFeature = it,
iconTint = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(start = 16.dp, top = 6.dp)
)
}
}
Text(
text = stringResource(id = R.string.RemoteRestoreActivity__your_backup_includes),
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(bottom = 6.dp)
text = stringResource(R.string.RemoteRestoreActivity__if_you_choose_not_to_restore),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.secondary,
modifier = Modifier.padding(top = 16.dp)
)
} else {
Text(
text = subtitle,
style = MaterialTheme.typography.bodyLarge.copy(textAlign = TextAlign.Center),
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(vertical = 20.dp)
)
getFeatures(state.backupTier, state.backupMediaTTL).forEach {
MessageBackupsTypeFeatureRow(
messageBackupsTypeFeature = it,
iconTint = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(start = 16.dp, top = 6.dp)
)
}
Text(
text = stringResource(R.string.RemoteRestoreActivity__if_you_choose_not_to_restore),
style = MaterialTheme.typography.bodyLarge.copy(textAlign = TextAlign.Center),
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
when (state.importState) {
@@ -340,6 +397,23 @@ private fun RestoreFromBackupContentPreview() {
}
}
@SignalPreview
@Composable
private fun RestoreFromBackupUnknownTierPreview() {
Previews.Preview {
RestoreFromBackupContent(
state = RemoteRestoreViewModel.ScreenState(
loadState = RemoteRestoreViewModel.ScreenState.LoadState.LOADED,
backupTier = null,
backupTime = System.currentTimeMillis(),
backupSize = 0.bytes,
importState = RemoteRestoreViewModel.ImportState.Restored,
restoreProgress = null
)
)
}
}
@SignalPreview
@Composable
private fun RestoreFromBackupContentLoadingPreview() {

View File

@@ -20,6 +20,7 @@ import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.backup.v2.RemoteRestoreResult
import org.thoughtcrime.securesms.backup.v2.RestoreTimestampResult
import org.thoughtcrime.securesms.backup.v2.RestoreV2Event
import org.thoughtcrime.securesms.database.model.databaseprotos.RestoreDecisionState
import org.thoughtcrime.securesms.keyvalue.Completed
@@ -54,22 +55,28 @@ class RemoteRestoreViewModel(isOnlyRestoreOption: Boolean) : ViewModel() {
fun reload() {
viewModelScope.launch(Dispatchers.IO) {
store.update { it.copy(loadState = ScreenState.LoadState.LOADING, loadAttempts = it.loadAttempts + 1) }
val tier: MessageBackupTier? = BackupRepository.restoreBackupTier(SignalStore.account.requireAci())
val result = BackupRepository.restoreBackupFileTimestamp()
store.update {
if (tier != null && SignalStore.backup.lastBackupTime > 0) {
it.copy(
loadState = ScreenState.LoadState.LOADED,
backupTier = SignalStore.backup.backupTier,
backupTime = SignalStore.backup.lastBackupTime,
backupSize = SignalStore.registration.restoreBackupMediaSize.bytes
)
} else {
if (SignalStore.backup.isBackupTierRestored || SignalStore.backup.lastBackupTime == 0L) {
when (result) {
is RestoreTimestampResult.Success -> {
it.copy(
loadState = ScreenState.LoadState.LOADED,
backupTier = SignalStore.backup.backupTier,
backupTime = SignalStore.backup.lastBackupTime,
backupSize = SignalStore.registration.restoreBackupMediaSize.bytes
)
}
is RestoreTimestampResult.NotFound -> {
it.copy(loadState = ScreenState.LoadState.NOT_FOUND)
} else if (it.loadState == ScreenState.LoadState.LOADING) {
it.copy(loadState = ScreenState.LoadState.FAILURE)
} else {
it
}
else -> {
if (it.loadState == ScreenState.LoadState.LOADING) {
it.copy(loadState = ScreenState.LoadState.FAILURE)
} else {
it
}
}
}
}

View File

@@ -38,6 +38,7 @@ import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalPreview
import org.signal.core.ui.compose.horizontalGutters
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.dependencies.GooglePlayBillingDependencies.context
import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogActivity
private const val TAP_TARGET = 8
@@ -62,7 +63,46 @@ fun RegistrationScreen(
fun RegistrationScreen(
title: String,
subtitle: AnnotatedString?,
bottomContent: @Composable (BoxScope.() -> Unit),
bottomContent: @Composable BoxScope.() -> Unit,
mainContent: @Composable ColumnScope.() -> Unit
) {
RegistrationScreen(
topContent = { RegistrationScreenTitleSubtitle(title, subtitle) },
bottomContent = bottomContent,
mainContent = mainContent
)
}
@Composable
fun RegistrationScreenTitleSubtitle(
title: String,
subtitle: AnnotatedString?
) {
Text(
text = title,
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.fillMaxWidth()
)
if (subtitle != null) {
Text(
text = subtitle,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 16.dp)
)
}
Spacer(modifier = Modifier.height(40.dp))
}
/**
* A base framework for rendering the various v3 registration screens.
*/
@Composable
fun RegistrationScreen(
topContent: @Composable ColumnScope.() -> Unit,
bottomContent: @Composable BoxScope.() -> Unit,
mainContent: @Composable ColumnScope.() -> Unit
) {
Surface {
@@ -84,9 +124,7 @@ fun RegistrationScreen(
.padding(top = 40.dp, bottom = 16.dp)
.horizontalGutters()
) {
Text(
text = title,
style = MaterialTheme.typography.headlineMedium,
Column(
modifier = Modifier
.fillMaxWidth()
.clickable {
@@ -102,19 +140,10 @@ fun RegistrationScreen(
previousToast = Toast.makeText(context, context.resources.getQuantityString(R.plurals.RegistrationActivity_debug_log_hint, remaining, remaining), Toast.LENGTH_SHORT).apply { show() }
}
}
)
if (subtitle != null) {
Text(
text = subtitle,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 16.dp)
)
) {
topContent()
}
Spacer(modifier = Modifier.height(40.dp))
mainContent()
}
@@ -151,3 +180,20 @@ private fun RegistrationScreenPreview() {
}
}
}
@SignalPreview
@Composable
private fun RegistrationScreenNoTitlePreview() {
Previews.Preview {
RegistrationScreen(
topContent = { Text("Top content") },
bottomContent = {
TextButton(onClick = {}) {
Text("Bottom Button")
}
}
) {
Text("Main content")
}
}
}

View File

@@ -75,7 +75,7 @@ class RestoreViewModel : ViewModel() {
}
fun getAvailableRestoreMethods(): List<RestoreMethod> {
if (SignalStore.registration.isOtherDeviceAndroid || SignalStore.registration.restoreDecisionState.skippedRestoreChoice || !SignalStore.backup.isBackupTierRestored) {
if (SignalStore.registration.isOtherDeviceAndroid || SignalStore.registration.restoreDecisionState.skippedRestoreChoice || !SignalStore.backup.isBackupTimestampRestored) {
val methods = mutableListOf(RestoreMethod.FROM_LOCAL_BACKUP_V1)
if (SignalStore.registration.restoreDecisionState.includeDeviceToDeviceTransfer) {
@@ -85,7 +85,7 @@ class RestoreViewModel : ViewModel() {
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) {
null -> if (!SignalStore.backup.isBackupTimestampRestored) {
methods.add(1, RestoreMethod.FROM_SIGNAL_BACKUPS)
}
}
@@ -93,7 +93,7 @@ class RestoreViewModel : ViewModel() {
return methods
}
if (SignalStore.backup.backupTier != null || !SignalStore.backup.isBackupTierRestored) {
if (SignalStore.backup.backupTier != null || !SignalStore.backup.isBackupTimestampRestored) {
return listOf(RestoreMethod.FROM_SIGNAL_BACKUPS)
}

View File

@@ -1493,6 +1493,8 @@
<string name="RemoteRestoreActivity__update_signal">Update Signal</string>
<!-- Text label button to dismiss the dialog -->
<string name="RemoteRestoreActivity__not_now">Not now</string>
<!-- Text shown on restore screen as information on what will happen if you skip -->
<string name="RemoteRestoreActivity__if_you_choose_not_to_restore">If you choose not to restore now, you won\'t be able to restore later. Your media will restore in the background.</string>
<!-- GroupMentionSettingDialog -->
<string name="GroupMentionSettingDialog_notify_me_for_mentions">Notify me for Mentions</string>

View File

@@ -200,8 +200,7 @@ public class SignalServiceMessageReceiver {
socket.retrieveBackup(cdnNumber, headers, cdnPath, destination, 1_000_000_000L, listener);
}
@Nullable
public ZonedDateTime getCdnLastModifiedTime(int cdnNumber, Map<String, String> headers, String cdnPath) throws MissingConfigurationException, IOException {
public @Nonnull ZonedDateTime getCdnLastModifiedTime(int cdnNumber, Map<String, String> headers, String cdnPath) throws MissingConfigurationException, IOException {
return socket.getCdnLastModifiedTime(cdnNumber, headers, cdnPath);
}

View File

@@ -677,8 +677,7 @@ public class PushServiceSocket {
}
}
@Nullable
public ZonedDateTime getCdnLastModifiedTime(int cdnNumber, Map<String, String> headers, String path) throws MissingConfigurationException, PushNetworkException, NonSuccessfulResponseCodeException {
public @Nonnull ZonedDateTime getCdnLastModifiedTime(int cdnNumber, Map<String, String> headers, String path) throws MissingConfigurationException, PushNetworkException, NonSuccessfulResponseCodeException, MalformedResponseException {
ConnectionHolder[] cdnNumberClients = cdnClientsMap.get(cdnNumber);
if (cdnNumberClients == null) {
throw new MissingConfigurationException("Attempted to download from unsupported CDN number: " + cdnNumber + ", Our configuration supports: " + cdnClientsMap.keySet());
@@ -690,7 +689,7 @@ public class PushServiceSocket {
.readTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
.build();
Request.Builder request = new Request.Builder().url(connectionHolder.getUrl() + "/" + path).get();
Request.Builder request = new Request.Builder().url(connectionHolder.getUrl() + "/" + path).head();
if (connectionHolder.getHostHeader().isPresent()) {
request.addHeader("Host", connectionHolder.getHostHeader().get());
@@ -710,7 +709,7 @@ public class PushServiceSocket {
if (response.isSuccessful()) {
String lastModified = response.header("Last-Modified");
if (lastModified == null) {
return null;
throw new MalformedResponseException("No Last-Modified header in response");
}
return ZonedDateTime.parse(lastModified, DateTimeFormatter.RFC_1123_DATE_TIME);
} else {