diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/status/BackupStatusRow.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/status/BackupStatusRow.kt index d39f831a5e..fa1d5f5fac 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/status/BackupStatusRow.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/status/BackupStatusRow.kt @@ -82,6 +82,7 @@ fun BackupStatusRow( is BackupStatusData.RestoringMedia -> { Text( text = getRestoringMediaString(backupStatusData), + color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(horizontal = dimensionResource(CoreUiR.dimen.gutter)) ) } @@ -168,15 +169,15 @@ private fun getRestoringMediaString(backupStatusData: BackupStatusData.Restoring return when (backupStatusData.restoreStatus) { BackupStatusData.RestoreStatus.NORMAL -> { stringResource( - R.string.BackupStatusRow__downloading_s_of_s_s, + R.string.BackupStatusRow__restoring_s_of_s_s, backupStatusData.bytesDownloaded.toUnitString(2), backupStatusData.bytesTotal.toUnitString(2), "%d".format((backupStatusData.progress * 100).roundToInt()) ) } - BackupStatusData.RestoreStatus.LOW_BATTERY -> stringResource(R.string.BackupStatus__status_device_has_low_battery) - BackupStatusData.RestoreStatus.WAITING_FOR_INTERNET -> stringResource(R.string.BackupStatus__status_no_internet) - BackupStatusData.RestoreStatus.WAITING_FOR_WIFI -> stringResource(R.string.BackupStatus__status_waiting_for_wifi) + BackupStatusData.RestoreStatus.LOW_BATTERY -> stringResource(R.string.BackupStatusRow__restore_device_has_low_battery) + BackupStatusData.RestoreStatus.WAITING_FOR_INTERNET -> stringResource(R.string.BackupStatusRow__restore_no_internet) + BackupStatusData.RestoreStatus.WAITING_FOR_WIFI -> stringResource(R.string.BackupStatusRow__restore_waiting_for_wifi) BackupStatusData.RestoreStatus.FINISHED -> stringResource(R.string.BackupStatus__restore_complete) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/MediaRestoreProgressBanner.kt b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/MediaRestoreProgressBanner.kt index 0bfbbe0414..86d486475e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/MediaRestoreProgressBanner.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/MediaRestoreProgressBanner.kt @@ -108,7 +108,7 @@ class MediaRestoreProgressBanner(private val listener: RestoreProgressBannerList .throttleLatest(1.seconds) .map { when { - !WifiConstraint.isMet(AppDependencies.application) -> BackupStatusData.RestoringMedia(restoreStatus = BackupStatusData.RestoreStatus.WAITING_FOR_WIFI) + !WifiConstraint.isMet(AppDependencies.application) && !SignalStore.backup.restoreWithCellular -> BackupStatusData.RestoringMedia(restoreStatus = BackupStatusData.RestoreStatus.WAITING_FOR_WIFI) !NetworkConstraint.isMet(AppDependencies.application) -> BackupStatusData.RestoringMedia(restoreStatus = BackupStatusData.RestoreStatus.WAITING_FOR_INTERNET) !BatteryNotLowConstraint.isMet() -> BackupStatusData.RestoringMedia(restoreStatus = BackupStatusData.RestoreStatus.LOW_BATTERY) else -> { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt index 0e5296bca0..3b82b0adb5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt @@ -141,6 +141,7 @@ class RemoteBackupsSettingsFragment : ComposeFragment() { backupsEnabled = state.backupsEnabled, lastBackupTimestamp = state.lastBackupTimestamp, canBackUpUsingCellular = state.canBackUpUsingCellular, + canRestoreUsingCellular = state.canRestoreUsingCellular, backupsFrequency = state.backupsFrequency, requestedDialog = state.dialog, requestedSnackbar = state.snackbar, @@ -239,6 +240,10 @@ class RemoteBackupsSettingsFragment : ComposeFragment() { override fun onLearnMoreAboutBackupFailure() { BackupAlertBottomSheet.create(BackupAlert.BackupFailed).show(parentFragmentManager, null) } + + override fun onRestoreUsingCellularClick(canUseCellular: Boolean) { + viewModel.setCanRestoreUsingCellular(canUseCellular) + } } private fun displayBackupKey() { @@ -321,6 +326,7 @@ private interface ContentCallbacks { fun onLearnMoreAboutLostSubscription() = Unit fun onContactSupport() = Unit fun onLearnMoreAboutBackupFailure() = Unit + fun onRestoreUsingCellularClick(canUseCellular: Boolean) = Unit } @Composable @@ -330,6 +336,7 @@ private fun RemoteBackupsSettingsContent( backupRestoreState: BackupRestoreState, lastBackupTimestamp: Long, canBackUpUsingCellular: Boolean, + canRestoreUsingCellular: Boolean, backupsFrequency: BackupFrequency, requestedDialog: RemoteBackupsSettingsState.Dialog, requestedSnackbar: RemoteBackupsSettingsState.Snackbar, @@ -403,6 +410,14 @@ private fun RemoteBackupsSettingsContent( onLearnMoreClick = contentCallbacks::onLearnMoreAboutBackupFailure ) } + + item { + Rows.ToggleRow( + checked = canRestoreUsingCellular, + text = stringResource(id = R.string.RemoteBackupsSettingsFragment__restore_using_cellular), + onCheckChanged = contentCallbacks::onRestoreUsingCellularClick + ) + } } else if (backupRestoreState is BackupRestoreState.Ready && backupState is RemoteBackupsSettingsState.BackupState.Canceled) { item { BackupReadyToDownloadRow( @@ -420,6 +435,7 @@ private fun RemoteBackupsSettingsContent( backupSize = backupSize, backupsFrequency = backupsFrequency, canBackUpUsingCellular = canBackUpUsingCellular, + canRestoreUsingCellular = canRestoreUsingCellular, contentCallbacks = contentCallbacks ) } else { @@ -540,6 +556,7 @@ private fun LazyListScope.appendBackupDetailsItems( backupSize: Long, backupsFrequency: BackupFrequency, canBackUpUsingCellular: Boolean, + canRestoreUsingCellular: Boolean, contentCallbacks: ContentCallbacks ) { item { @@ -1205,6 +1222,7 @@ private fun RemoteBackupsSettingsContentPreview() { backupsEnabled = true, lastBackupTimestamp = -1, canBackUpUsingCellular = false, + canRestoreUsingCellular = false, backupsFrequency = BackupFrequency.MANUAL, requestedDialog = RemoteBackupsSettingsState.Dialog.NONE, requestedSnackbar = RemoteBackupsSettingsState.Snackbar.NONE, diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsState.kt index 8cec27df51..a3db161bb4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsState.kt @@ -14,6 +14,7 @@ import kotlin.time.Duration.Companion.seconds data class RemoteBackupsSettingsState( val backupsEnabled: Boolean, val canBackUpUsingCellular: Boolean = false, + val canRestoreUsingCellular: Boolean = false, val backupState: BackupState = BackupState.Loading, val backupSize: Long = 0, val backupsFrequency: BackupFrequency = BackupFrequency.DAILY, diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsViewModel.kt index 915af9618a..937e186578 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsViewModel.kt @@ -61,7 +61,8 @@ class RemoteBackupsSettingsViewModel : ViewModel() { lastBackupTimestamp = SignalStore.backup.lastBackupTime, backupSize = SignalStore.backup.totalBackupSize, backupsFrequency = SignalStore.backup.backupFrequency, - canBackUpUsingCellular = SignalStore.backup.backupWithCellular + canBackUpUsingCellular = SignalStore.backup.backupWithCellular, + canRestoreUsingCellular = SignalStore.backup.restoreWithCellular ) ) @@ -109,6 +110,11 @@ class RemoteBackupsSettingsViewModel : ViewModel() { _state.update { it.copy(canBackUpUsingCellular = canBackUpUsingCellular) } } + fun setCanRestoreUsingCellular(canRestoreUsingCellular: Boolean) { + SignalStore.backup.restoreWithCellular = canRestoreUsingCellular + _state.update { it.copy(canRestoreUsingCellular = canRestoreUsingCellular) } + } + fun setBackupsFrequency(backupsFrequency: BackupFrequency) { SignalStore.backup.backupFrequency = backupsFrequency _state.update { it.copy(backupsFrequency = backupsFrequency) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/impl/RestoreAttachmentConstraint.kt b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/impl/RestoreAttachmentConstraint.kt new file mode 100644 index 0000000000..187ebbfc26 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/impl/RestoreAttachmentConstraint.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.jobmanager.impl + +import android.app.Application +import android.app.job.JobInfo +import android.content.Context +import org.thoughtcrime.securesms.jobmanager.Constraint +import org.thoughtcrime.securesms.keyvalue.SignalStore + +/** + * Constraint that, when added, means that a job cannot be performed unless the user either has Wifi or, if they enabled it, cellular + */ +class RestoreAttachmentConstraint(private val application: Application) : Constraint { + + companion object { + const val KEY = "RestoreAttachmentConstraint" + + fun isMet(context: Context): Boolean { + if (SignalStore.backup.restoreWithCellular) { + return NetworkConstraint.isMet(context) + } + return WifiConstraint.isMet(context) + } + } + + override fun isMet(): Boolean { + return isMet(application) + } + + override fun getFactoryKey(): String = KEY + + override fun applyToJobInfo(jobInfoBuilder: JobInfo.Builder) = Unit + + class Factory(val application: Application) : Constraint.Factory { + override fun create(): RestoreAttachmentConstraint { + return RestoreAttachmentConstraint(application) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/impl/RestoreAttachmentConstraintObserver.kt b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/impl/RestoreAttachmentConstraintObserver.kt new file mode 100644 index 0000000000..06e33fdbdf --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/impl/RestoreAttachmentConstraintObserver.kt @@ -0,0 +1,28 @@ +package org.thoughtcrime.securesms.jobmanager.impl + +import org.thoughtcrime.securesms.dependencies.AppDependencies +import org.thoughtcrime.securesms.jobmanager.ConstraintObserver + +/** + * An observer for the [RestoreAttachmentConstraint]. This is called + * when users change whether or not restoring is allowed via cellular + */ +object RestoreAttachmentConstraintObserver : ConstraintObserver { + + private const val REASON = "RestoreAttachmentConstraint" + + private var notifier: ConstraintObserver.Notifier? = null + + override fun register(notifier: ConstraintObserver.Notifier) { + this.notifier = notifier + } + + /** + * Let the observer know that the restore using cellular flag has changed. + */ + fun onChange() { + if (RestoreAttachmentConstraint.isMet(AppDependencies.application)) { + notifier?.onConstraintMet(REASON) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index b6f615e84f..c4b0d9c7c9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -25,6 +25,8 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraintObserver; import org.thoughtcrime.securesms.jobmanager.impl.NetworkOrCellServiceConstraint; import org.thoughtcrime.securesms.jobmanager.impl.NotInCallConstraint; import org.thoughtcrime.securesms.jobmanager.impl.NotInCallConstraintObserver; +import org.thoughtcrime.securesms.jobmanager.impl.RestoreAttachmentConstraint; +import org.thoughtcrime.securesms.jobmanager.impl.RestoreAttachmentConstraintObserver; import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraint; import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraintObserver; import org.thoughtcrime.securesms.jobmanager.impl.WifiConstraint; @@ -386,6 +388,7 @@ public final class JobManagerFactories { put(NotInCallConstraint.KEY, new NotInCallConstraint.Factory()); put(SqlCipherMigrationConstraint.KEY, new SqlCipherMigrationConstraint.Factory(application)); put(WifiConstraint.KEY, new WifiConstraint.Factory(application)); + put(RestoreAttachmentConstraint.KEY, new RestoreAttachmentConstraint.Factory(application)); }}; } @@ -397,7 +400,8 @@ public final class JobManagerFactories { new DecryptionsDrainedConstraintObserver(), new NotInCallConstraintObserver(), ChangeNumberConstraintObserver.INSTANCE, - DataRestoreConstraintObserver.INSTANCE); + DataRestoreConstraintObserver.INSTANCE, + RestoreAttachmentConstraintObserver.INSTANCE); } public static List getJobMigrations(@NonNull Application application) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RestoreAttachmentJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/RestoreAttachmentJob.kt index 12b9c79335..ab561c9a43 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RestoreAttachmentJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RestoreAttachmentJob.kt @@ -20,7 +20,7 @@ import org.thoughtcrime.securesms.events.PartProgressEvent import org.thoughtcrime.securesms.jobmanager.Job import org.thoughtcrime.securesms.jobmanager.JobLogger.format import org.thoughtcrime.securesms.jobmanager.impl.BatteryNotLowConstraint -import org.thoughtcrime.securesms.jobmanager.impl.WifiConstraint +import org.thoughtcrime.securesms.jobmanager.impl.RestoreAttachmentConstraint import org.thoughtcrime.securesms.jobs.protos.RestoreAttachmentJobData import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.mms.MmsException @@ -108,7 +108,7 @@ class RestoreAttachmentJob private constructor( private constructor(messageId: Long, attachmentId: AttachmentId, manual: Boolean, queue: String) : this( Parameters.Builder() .setQueue(queue) - .addConstraint(WifiConstraint.KEY) + .addConstraint(RestoreAttachmentConstraint.KEY) .addConstraint(BatteryNotLowConstraint.KEY) .setLifespan(TimeUnit.DAYS.toMillis(30)) .build(), 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 ef0ed8b456..4fc2b9945c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt @@ -7,6 +7,7 @@ import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.backup.RestoreState import org.thoughtcrime.securesms.backup.v2.BackupFrequency import org.thoughtcrime.securesms.backup.v2.MessageBackupTier +import org.thoughtcrime.securesms.jobmanager.impl.RestoreAttachmentConstraintObserver import org.thoughtcrime.securesms.keyvalue.protos.ArchiveUploadProgressState import org.thoughtcrime.securesms.util.Util import org.whispersystems.signalservice.api.archive.ArchiveServiceCredential @@ -48,6 +49,7 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) { private const val KEY_CDN_MEDIA_PATH = "backup.cdn.mediaPath" private const val KEY_BACKUP_OVER_CELLULAR = "backup.useCellular" + private const val KEY_RESTORE_OVER_CELLULAR = "backup.restore.useCellular" private const val KEY_OPTIMIZE_STORAGE = "backup.optimizeStorage" private const val KEY_BACKUPS_INITIALIZED = "backup.initialized" @@ -82,6 +84,13 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) { var optimizeStorage: Boolean by booleanValue(KEY_OPTIMIZE_STORAGE, false) var backupWithCellular: Boolean by booleanValue(KEY_BACKUP_OVER_CELLULAR, false) + var restoreWithCellular: Boolean + get() = getBoolean(KEY_RESTORE_OVER_CELLULAR, false) + set(value) { + putBoolean(KEY_RESTORE_OVER_CELLULAR, value) + RestoreAttachmentConstraintObserver.onChange() + } + var nextBackupTime: Long by longValue(KEY_NEXT_BACKUP_TIME, -1) var lastBackupTime: Long get() = getLong(KEY_LAST_BACKUP_TIME, -1) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9961736f12..b4ddf25eef 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -7542,7 +7542,13 @@ Cancel download - Downloading: %1$s of %2$s (%3$s%%) + Restoring: %1$s of %2$s (%3$s%%) + + Restore paused: Waiting for Wi-Fi… + + Restore paused: No internet… + + Restore paused: Device has low battery Not enough space to download your Backup. To continue free up %1$s of space. @@ -7651,6 +7657,8 @@ Payment history Backup details + + Restore using cellular Backup size