From 437b1a3d98d985c229faf3bd97d193b848a9fccb Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Wed, 2 Jul 2025 11:24:36 -0400 Subject: [PATCH] Restore timestamp instead of tier during manual registration remote backup restore flow. --- .../securesms/backup/v2/BackupRepository.kt | 62 ++++------ .../InternalBackupPlaygroundViewModel.kt | 2 +- .../securesms/keyvalue/BackupValues.kt | 7 +- .../securesms/pin/PinRestoreViewModel.kt | 8 +- .../ui/RegistrationCheckpoint.kt | 2 +- .../ui/RegistrationViewModel.kt | 35 +++--- .../ui/restore/EnterBackupKeyFragment.kt | 6 +- .../ui/restore/EnterBackupKeyViewModel.kt | 4 +- .../ui/restore/RemoteRestoreActivity.kt | 112 +++++++++++++++--- .../ui/restore/RemoteRestoreViewModel.kt | 35 +++--- .../ui/shared/RegistrationScreen.kt | 76 +++++++++--- .../securesms/restore/RestoreViewModel.kt | 6 +- app/src/main/res/values/strings.xml | 2 + .../api/SignalServiceMessageReceiver.java | 3 +- .../internal/push/PushServiceSocket.java | 7 +- 15 files changed, 235 insertions(+), 132 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt index d3e70e9839..7d3f3e01ec 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt @@ -1324,17 +1324,16 @@ object BackupRepository { } } - fun getBackupFileLastModified(): NetworkResult { + fun getBackupFileLastModified(): NetworkResult { 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 = 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. * diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundViewModel.kt index de69a87067..571f5041a6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundViewModel.kt @@ -261,7 +261,7 @@ class InternalBackupPlaygroundViewModel : ViewModel() { fun checkRemoteBackupState() { disposables += Single .fromCallable { - BackupRepository.restoreBackupTier(SignalStore.account.requireAci()) + BackupRepository.restoreBackupFileTimestamp() BackupRepository.debugGetRemoteBackupState() } .subscribeOn(Schedulers.io()) diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt index 2ee637375c..46856a7d40 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt @@ -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. diff --git a/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreViewModel.kt index 0bf182baa6..08d7556bec 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreViewModel.kt @@ -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 -> diff --git a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/RegistrationCheckpoint.kt b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/RegistrationCheckpoint.kt index 6040c7431c..ae3c99af83 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/RegistrationCheckpoint.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/RegistrationCheckpoint.kt @@ -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 } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/RegistrationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/RegistrationViewModel.kt index fef3efb300..ee0e5c9b73 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/RegistrationViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/RegistrationViewModel.kt @@ -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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/EnterBackupKeyFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/EnterBackupKeyFragment.kt index 8afd0ba11d..820b196fe9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/EnterBackupKeyFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/EnterBackupKeyFragment.kt @@ -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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/EnterBackupKeyViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/EnterBackupKeyViewModel.kt index 8cd2d5ed2c..38e5702814 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/EnterBackupKeyViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/EnterBackupKeyViewModel.kt @@ -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 ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/RemoteRestoreActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/RemoteRestoreActivity.kt index c24c56405e..784a1cb174 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/RemoteRestoreActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/RemoteRestoreActivity.kt @@ -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() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/RemoteRestoreViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/RemoteRestoreViewModel.kt index 1491baff2e..3e915d3216 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/RemoteRestoreViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/restore/RemoteRestoreViewModel.kt @@ -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 + } } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/shared/RegistrationScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/shared/RegistrationScreen.kt index 922795333d..17b950abf5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/shared/RegistrationScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/shared/RegistrationScreen.kt @@ -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") + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/restore/RestoreViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/restore/RestoreViewModel.kt index 0d944a4818..36058e6839 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/restore/RestoreViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/restore/RestoreViewModel.kt @@ -75,7 +75,7 @@ class RestoreViewModel : ViewModel() { } fun getAvailableRestoreMethods(): List { - 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) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6a576b8680..624d609e54 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1493,6 +1493,8 @@ Update Signal Not now + + If you choose not to restore now, you won\'t be able to restore later. Your media will restore in the background. Notify me for Mentions diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java index 52ab2be071..b16ca723e7 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java @@ -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 headers, String cdnPath) throws MissingConfigurationException, IOException { + public @Nonnull ZonedDateTime getCdnLastModifiedTime(int cdnNumber, Map headers, String cdnPath) throws MissingConfigurationException, IOException { return socket.getCdnLastModifiedTime(cdnNumber, headers, cdnPath); } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java index 3c381a9989..be29b95805 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java @@ -677,8 +677,7 @@ public class PushServiceSocket { } } - @Nullable - public ZonedDateTime getCdnLastModifiedTime(int cdnNumber, Map headers, String path) throws MissingConfigurationException, PushNetworkException, NonSuccessfulResponseCodeException { + public @Nonnull ZonedDateTime getCdnLastModifiedTime(int cdnNumber, Map 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 {