From 6adddf4a0ca7666ebc42342c902c9e10418d9ce8 Mon Sep 17 00:00:00 2001 From: Clark Date: Tue, 11 Jun 2024 10:10:45 -0400 Subject: [PATCH] Add display of last backup time to restore flow. --- .../securesms/backup/v2/BackupRepository.kt | 13 +++++++++---- .../v2/ui/subscription/RemoteRestoreViewModel.kt | 10 +++++++++- .../backup/InternalBackupPlaygroundViewModel.kt | 5 ++++- .../v2/ui/restore/RemoteRestoreActivity.kt | 15 ++++++++++++++- app/src/main/res/values/strings.xml | 4 ++++ .../api/SignalServiceMessageReceiver.java | 8 ++++---- .../internal/push/PushServiceSocket.java | 15 +++++++++------ 7 files changed, 53 insertions(+), 17 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 b7999fdc75..296a4f251b 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 @@ -41,6 +41,7 @@ import org.thoughtcrime.securesms.groups.GroupId import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.util.toMillis import org.whispersystems.signalservice.api.NetworkResult import org.whispersystems.signalservice.api.StatusCodeErrorAction import org.whispersystems.signalservice.api.archive.ArchiveGetMediaItemsResponse @@ -62,6 +63,7 @@ import java.io.File import java.io.InputStream import java.io.OutputStream import java.lang.Exception +import java.time.ZonedDateTime import kotlin.time.Duration.Companion.milliseconds object BackupRepository { @@ -355,7 +357,7 @@ object BackupRepository { } is NetworkResult.Success } - fun checkForBackupFile(): Boolean { + fun getBackupFileLastModified(): NetworkResult { val api = AppDependencies.signalServiceAccountManager.archiveApi val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey() @@ -368,9 +370,9 @@ object BackupRepository { val (cdnCredentials, info) = pair val messageReceiver = AppDependencies.signalServiceMessageReceiver NetworkResult.fromFetch { - messageReceiver.checkBackupExistence(info.cdn!!, cdnCredentials, "backups/${info.backupDir}/${info.backupName}") + messageReceiver.getCdnLastModifiedTime(info.cdn!!, cdnCredentials, "backups/${info.backupDir}/${info.backupName}") } - } is NetworkResult.Success + } } /** @@ -605,7 +607,10 @@ object BackupRepository { fun restoreBackupTier(): MessageBackupTier? { // TODO: more complete error handling try { - checkForBackupFile() + val lastModified = getBackupFileLastModified().successOrThrow() + if (lastModified != null) { + SignalStore.backup().lastBackupTime = lastModified.toMillis() + } } catch (e: Exception) { Log.i(TAG, "Could not check for backup file.", e) SignalStore.backup().backupTier = null diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/RemoteRestoreViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/RemoteRestoreViewModel.kt index 29cb2dfcf8..797adde9c9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/RemoteRestoreViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/RemoteRestoreViewModel.kt @@ -32,7 +32,14 @@ import kotlin.time.Duration.Companion.seconds class RemoteRestoreViewModel : ViewModel() { val disposables = CompositeDisposable() - private val _state: MutableState = mutableStateOf(ScreenState(backupTier = SignalStore.backup().backupTier, importState = ImportState.NONE, restoreProgress = null)) + private val _state: MutableState = mutableStateOf( + ScreenState( + backupTier = SignalStore.backup().backupTier, + backupTime = SignalStore.backup().lastBackupTime, + importState = ImportState.NONE, + restoreProgress = null + ) + ) val state: State = _state @@ -78,6 +85,7 @@ class RemoteRestoreViewModel : ViewModel() { data class ScreenState( val backupTier: MessageBackupTier?, + val backupTime: Long, val importState: ImportState, val restoreProgress: RestoreV2Event? ) 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 04594e4dbd..58a12c053e 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 @@ -143,7 +143,10 @@ class InternalBackupPlaygroundViewModel : ViewModel() { _state.value = _state.value.copy(remoteBackupState = RemoteBackupState.Unknown) disposables += Single - .fromCallable { BackupRepository.getRemoteBackupState() } + .fromCallable { + BackupRepository.restoreBackupTier() + BackupRepository.getRemoteBackupState() + } .subscribeOn(Schedulers.io()) .subscribe { result -> when { diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/restore/RemoteRestoreActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/restore/RemoteRestoreActivity.kt index 649742078a..23e4f75d05 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/restore/RemoteRestoreActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/restore/RemoteRestoreActivity.kt @@ -30,6 +30,7 @@ import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle @@ -63,7 +64,9 @@ import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.registration.RegistrationUtil import org.thoughtcrime.securesms.restore.transferorrestore.TransferOrRestoreMoreOptionsDialog +import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.Util +import java.util.Locale class RemoteRestoreActivity : BaseActivity() { companion object { @@ -93,6 +96,7 @@ class RemoteRestoreActivity : BaseActivity() { TransferOrRestoreMoreOptionsDialog.show(fragmentManager = supportFragmentManager, skipOnly = false) }, state.backupTier, + state.backupTime, state.backupTier != MessageBackupTier.PAID ) if (state.importState == RemoteRestoreViewModel.ImportState.RESTORED) { @@ -238,6 +242,7 @@ class RemoteRestoreActivity : BaseActivity() { onCancelClick = {}, onMoreOptionsClick = {}, MessageBackupTier.PAID, + System.currentTimeMillis(), true ) } @@ -250,6 +255,7 @@ class RemoteRestoreActivity : BaseActivity() { onCancelClick: () -> Unit, onMoreOptionsClick: () -> Unit, tier: MessageBackupTier?, + lastBackupTime: Long, cancelable: Boolean ) { Column( @@ -264,7 +270,14 @@ class RemoteRestoreActivity : BaseActivity() { ) val yourLastBackupText = buildAnnotatedString { - append("Your last backup was made on March 5, 2024 at 9:00am.") // TODO [message-backups] Finalized copy. + append( + stringResource( + id = R.string.RemoteRestoreActivity__backup_created_at, + DateUtils.formatDateWithoutDayOfWeek(Locale.getDefault(), lastBackupTime), + DateUtils.getOnlyTimeString(LocalContext.current, lastBackupTime) + ) + + ) append(" ") if (tier != MessageBackupTier.PAID) { withStyle(SpanStyle(fontWeight = FontWeight.SemiBold)) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e41f0a8bd5..3c96a1bb7f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -6987,6 +6987,10 @@ Unknown + + + Your last backup was made on %1$s at %2$s. + Chat limits 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 540c66236d..18876cf8f8 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 @@ -14,7 +14,6 @@ import org.signal.libsignal.protocol.InvalidMessageException; import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations; import org.signal.libsignal.zkgroup.profiles.ProfileKey; import org.whispersystems.signalservice.api.backup.BackupKey; -import org.whispersystems.signalservice.api.backup.MediaId; import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream; import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil; import org.whispersystems.signalservice.api.crypto.ProfileCipherInputStream; @@ -28,7 +27,6 @@ import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; import org.whispersystems.signalservice.api.push.ServiceId.ACI; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException; -import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; import org.whispersystems.signalservice.api.util.CredentialsProvider; import org.whispersystems.signalservice.internal.ServiceResponse; import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; @@ -45,6 +43,7 @@ import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -225,8 +224,9 @@ public class SignalServiceMessageReceiver { socket.retrieveBackup(cdnNumber, headers, cdnPath, destination, 1_000_000_000L, listener); } - public boolean checkBackupExistence(int cdnNumber, Map headers, String cdnPath) throws MissingConfigurationException, IOException { - return socket.checkForBackup(cdnNumber, headers, cdnPath); + @Nullable + public ZonedDateTime getCdnLastModifiedTime(int cdnNumber, Map headers, String cdnPath) throws MissingConfigurationException, IOException { + return socket.getCdnLastModifiedTime(cdnNumber, headers, cdnPath); } public InputStream retrieveSticker(byte[] packId, byte[] packKey, int stickerId) 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 10b1114326..ee6998bad8 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 @@ -171,6 +171,8 @@ import java.nio.charset.StandardCharsets; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -964,10 +966,6 @@ public class PushServiceSocket { downloadFromCdn(destination, cdnNumber, headers, cdnPath, maxSizeBytes, listener); } - public boolean checkForBackup(int cdnNumber, Map headers, String cdnPath) throws PushNetworkException, MissingConfigurationException, NonSuccessfulResponseCodeException { - return checkExistsOnCdn(cdnNumber, headers, cdnPath); - } - public void retrieveAttachment(int cdnNumber, Map headers, SignalServiceAttachmentRemoteId cdnPath, File destination, long maxSizeBytes, ProgressListener listener) throws IOException, MissingConfigurationException { @@ -1722,7 +1720,8 @@ public class PushServiceSocket { } } - private boolean checkExistsOnCdn(int cdnNumber, Map headers, String path) throws MissingConfigurationException, PushNetworkException, NonSuccessfulResponseCodeException { + @Nullable + public ZonedDateTime getCdnLastModifiedTime(int cdnNumber, Map headers, String path) throws MissingConfigurationException, PushNetworkException, NonSuccessfulResponseCodeException { ConnectionHolder[] cdnNumberClients = cdnClientsMap.get(cdnNumber); if (cdnNumberClients == null) { throw new MissingConfigurationException("Attempted to download from unsupported CDN number: " + cdnNumber + ", Our configuration supports: " + cdnClientsMap.keySet()); @@ -1752,7 +1751,11 @@ public class PushServiceSocket { try (Response response = call.execute()) { if (response.isSuccessful()) { - return true; + String lastModified = response.header("Last-Modified"); + if (lastModified == null) { + return null; + } + return ZonedDateTime.parse(lastModified, DateTimeFormatter.RFC_1123_DATE_TIME); } else { throw new NonSuccessfulResponseCodeException(response.code(), "Response: " + response); }