mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 04:58:45 +00:00
Restore timestamp instead of tier during manual registration remote backup restore flow.
This commit is contained in:
@@ -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.
|
||||
*
|
||||
|
||||
@@ -261,7 +261,7 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
|
||||
fun checkRemoteBackupState() {
|
||||
disposables += Single
|
||||
.fromCallable {
|
||||
BackupRepository.restoreBackupTier(SignalStore.account.requireAci())
|
||||
BackupRepository.restoreBackupFileTimestamp()
|
||||
BackupRepository.debugGetRemoteBackupState()
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user